* { box-sizing: border-box; }

/* Атрибут [hidden] иначе перебивается любым правилом display: ... на этом
   же элементе (.login/.layout ниже задают display: flex) — авторский CSS
   побеждает UA-стиль [hidden]{display:none} по origin, не по специфичности. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
}

/* Наряд №148, работа 2 — фирменный заголовочный шрифт на всех трёх точках
   входа разом (tokens.css грузится первым везде, var(--font-heading)
   существует). Экрана ученика это не редизайн, а починка: заголовки там
   до этой правки отдавались Helvetica через тот же системный стек, что и
   текст. */
h1, h2, h3 {
  font-family: var(--font-heading);
  font-weight: 600;
}

/* Наряд №139, работа 5 — первый кадр: то, что ребёнок видит, пока летит
   ответ start-or-resume. Раньше на этом месте стояла форма входа (см.
   index.html у #pervyy-kadr). Никакого вращающегося колеса на весь экран:
   у продукта нет ни одного места, где нечего показать, кроме этого кадра, и
   здесь достаточно сказать словами, что происходит (канон №136, раздел 3.6). */
.pervyy-kadr {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pervyy-kadr-tekst {
  color: var(--text-dim);
  font-size: var(--fs-body);
}

.pervyy-kadr-tekst::after {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: var(--s2);
  border-radius: 50%;
  background: var(--text-dim);
  vertical-align: middle;
  animation: verdict-pulse 1.1s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .pervyy-kadr-tekst::after { animation: none; opacity: 0.8; }
}

.login {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.login form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 280px;
}

.login h1 {
  font-size: 18px;
  margin: 0 0 8px;
}

.login input {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px 12px;
  border-radius: 4px;
  font-size: 16px;
  letter-spacing: 0.05em;
}

.login button {
  background: var(--accent);
  color: var(--accent-text);
  border: none;
  padding: 10px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.login-error {
  color: var(--fail);
  min-height: 1.2em;
  margin: 0;
}

.layout {
  display: flex;
  height: 100vh;
  position: relative;
}

/* Наряд №139, работа 1 — ТРИ КОЛОНКИ ОТ 1366×768 БЕЗ ГОРИЗОНТАЛЬНОГО ХАОСА.

   Что было: ширины заданы одними процентами (35 / 40 / остаток), без пола и
   потолка. Следствия замерены (scripts/naryad139-smoke-ekrana-uchenika.py):

     · 1366 px, практика: чату остаётся 342 px — лента с кодовыми блоками и
       ASCII-схемами в колонке уже кнопки «Отложить задачу»;
     · 1366 px, разбор (условие и редактор скрыты): чат раздувается до 888 px,
       и реплика штурмана становится строкой в 770 px — вдвое шире того, что
       глаз читает без потери строки;
     · 1920 px: 1094 px на реплику и 672 px на условие.

   Проценты сами по себе не виноваты — виновато отсутствие границ: одно и то
   же правило обязано отработать и там, где колонок три, и там, где их одна.
   Пол защищает узкое окно, потолок — широкое. Ни одна колонка не меняет своей
   ДОЛИ в пределах, где доля осмысленна: широкая раскладка, на которой стоят
   D16/D14/D24 и подсветка ui-element-highlight, не переставляется вовсе
   (риск 1 канона №136) — меняются только границы.

   Порядок в DOM и правило «карточки над условием» не тронуты. */
.left-panel {
  /* Потолок — мера строки: 620 px минус отступы даёт ~72 знака при 16 px,
     верхняя граница читаемости связного текста. Пол — чтобы таблица «Формат
     ввода/вывода» в условии не сложилась в лесенку. */
  min-width: 360px;
  max-width: 620px;
}

.workspace {
  /* Пол — ширина, на которой строка кода C++ с отступом в четыре пробела ещё
     не переносится в редакторе. */
  min-width: 480px;
  /* Кто платит за нехватку места. По умолчанию все три колонки сжимаются
     пропорционально своей ширине — то есть за пол колонки чата (380 px)
     платит и левое поле. А платит оно НЕ шириной, а высотой: узкое условие
     переносит больше строк, и объединённая норма D16+D14 («условие целиком
     на первом экране при свёрнутых плашках») ломается на ровном месте —
     поймано прибором на 1366×768, где 18 отобранных пикселей ширины дали
     +80 px высоты и вдобавок включили жёлоб подсказки прокрутки, который
     отнял ещё 33 px ширины. Редактор от той же нехватки не теряет ничего:
     у Monaco своя горизонтальная прокрутка и нет переносов. Поэтому платит
     он — вчетверо охотнее соседей. */
  flex-shrink: 4;
}

/* Пол колонки чата — в её собственном правиле ниже по файлу (.chat): при
   равной специфичности выигрывает последнее, и второй экземпляр здесь
   молча ничего бы не делал. Именно так первая редакция этой работы и
   промахнулась — 342 px чата остались на месте, поймано прибором. */

/* Наряд №71, работа 1, пункт 2 — общий контейнер для позывного и кнопки
   завершения: ширина позывного заранее неизвестна, общий flex не гадает её
   отдельным офсетом на каждый элемент.

   Наряд №139, работа 1 — контейнер больше не `position: fixed` (см. причину
   в index.html у самого блока: замеренное перекрытие двух детских кнопок из
   двух на 1366 px). Теперь это тонкая шапка колонки чата: своя строка, свои
   32 px, ничего чужого не закрывает. Прижат вправо — позывной и «Завершить»
   остаются там же, где ребёнок привык их искать. */
/* Хвост №142.2 — ДВЕ ОСМЫСЛЕННЫЕ СТРОКИ, А НЕ ПЕРЕНОС ПО ОСТАТКУ.
   Было: один флекс-ряд с `flex-wrap: wrap`, и на 1366×768 он переносился как
   придётся — «Позвать тренера» оставалась в первой строке рядом с чипом,
   «Завершить тренировку» уезжала во вторую одна. Две кнопки одного
   смыслового ряда стояли на разных уровнях, и решала это арифметика остатка.
   Стало: колонка из двух рядов, каждый о своём — КТО (марка и позывной) и
   ЧТО МОЖНО СДЕЛАТЬ (обе кнопки). Перенос больше ничего не решает. */
.session-info {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--s1);
  flex: 0 0 auto;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border);
}

.session-identity {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}

/* Кнопки прижаты вправо — там же, где ребёнок искал их до хвоста, — и стоят
   на одном визуальном уровне: общий ряд, `align-items: center`, никакого
   переноса внутри (`flex-wrap` по умолчанию `nowrap`). Если места не хватит,
   ужмётся текст кнопки, но одна не уедет под другую. */
.session-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s2);
}

/* Хвост №142.1, работа 1 — ЛОГОТИП В ШАПКЕ.
   Хвост №142.2: 26 → 40 px. Прежний размер был подобран под ОДНУ строку, где
   логотип не имел права быть выше кнопки (29 px), — и в ней марка читалась
   пятном. В своей строке этого ограничения нет: рядом с логотипом стоит
   только позывной, и высоту строки задаёт логотип.
   Дополнительной строки ради него не заведено — вторая строка появилась под
   кнопки (см. .session-info выше), а не под марку.
   `height` фиксирована, `width: auto` — картинка не растягивается и не
   сплющивается, каким бы ни было её соотношение сторон; `object-fit:
   contain` страхует от обрезки, если соотношение когда-нибудь изменится.
   Подложки нет: PNG без фона, и на светлом слое БЦСП он лежит как есть. */
.session-logo {
  height: 40px;
  width: auto;
  /* Хвост №142.2: `margin-right: auto` снят. Он расталкивал логотип и всё
     остальное по краям однострочной шапки хвоста №142.1 — а в своей строке
     рядом с логотипом стоит позывной, и «БЦСП» с именем ребёнка читаются
     как ОДИН блок «кто я», а не как две метки на разных концах колонки. */
  object-fit: contain;
  /* Марка — не орган управления: ни курсора, ни реакции на наведение. */
  pointer-events: none;
  flex-shrink: 0;
}

/* Позывной ученика (наряд №41, работа 5; вернулся хвостом №142.2).
   Хвост №142.1 снял его приглушённым до `--text-dim` — «метка, а не элемент
   управления». Хвост №142.2 возвращает его как ОТВЕТ НА ВОПРОС «под кем я
   вошёл», и на этой роли приглушённый серый не годится: признак, который
   надо разглядывать, признаком не работает. Основной кегль экрана, обычный
   вес; цвет ставит фирменный слой.
   `pointer-events: none` остаётся: кликать по нему не по чему. */
.kto-ya {
  font-size: var(--fs-sec);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
}

/* Хвост №142.1, работа 2: здесь же стоял `.v5-path-indicator` — прибор пути
   захода наряда №107, снятый с детского экрана вместе со своей разметкой и
   четырьмя цветовыми литералами. Список разрешённых литералов проверки
   наряда №139 стал от этого на четыре короче — то есть строже, а не слабее. */

/* Наряд №71, работа 1, пункт 2 — единственная кнопка завершения занятия:
   гасит cookie и закрывает открытую тренировку (/api/auth/logout), чтобы
   следующий ребёнок на этом же компьютере не входил чужой сессией.
   Наряд №73, работа 3 (решение Романа 21.08.2026): раньше рядом стояла
   вторая кнопка («Закончить», .end-training-btn) с почти тем же смыслом —
   снята, эта осталась единственной. */
/* Хвост №142.1, работа 3 — САМОЕ ТИХОЕ ДЕЙСТВИЕ ЭКРАНА.
   Рядом с ней теперь стоит «Позвать тренера», и одинаковыми они быть не
   имеют права: одной ребёнок пользуется по ходу занятия, вторую нажимает
   один раз в конце и по ошибке нажимать не должен. Разведены тремя
   носителями сразу — заливка (её нет), рамка (её нет) и цвет текста
   (приглушённый), — а не одним оттенком.
   Порог попадания при этом не нарушен: 30 px по высоте остаются целью для
   мыши, и `confirm()` перед выходом (app.js::logout) никуда не делся. */
.logout-btn {
  background: none;
  color: var(--text-dim);
  border: none;
  padding: 6px 8px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 13px;
}

.logout-btn:hover:not(:disabled) {
  color: var(--text);
  background: var(--surface-2);
}

/* «Позвать тренера» в шапке — заметное вторичное действие. Геометрия здесь,
   цвета в фирменном слое (brand-bcsp.css, роль «вторичная»). */
#call-trainer-btn {
  padding: 6px 12px;
  border-radius: var(--r-sm);
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
}

.logout-btn:disabled {
  cursor: default;
  opacity: 0.6;
}

/* Наряд №9, работа 8: карточки и условие делят одно и то же левое поле
   (.left-panel) — раньше карточки жили внутри .chat, поверх ленты
   переписки. Одна область прокрутки на обе части намеренно: карточки —
   компактные плашки сверху, условие (когда открыто, наряд №8 работа 1) —
   под ними. */
/* Наряд №142, работа 1 — ОДИН ОСНОВНОЙ СКРОЛЛ В ЛЕВОЙ КОЛОНКЕ.
   Было: сама .left-panel прокручивалась, и внутри неё прокручивались
   .cards-column (max-height 40 %) и .card-tile-body (max-height 45vh) — три
   вложенные области, каждая со своей кнопкой «↓». Стало: колонка держит
   корешки вкладок и одну прокручиваемую область под ними; ни панель вкладки,
   ни карточка внутри своего overflow не заводят.
   `min-height: 0` — обязательное условие того, чтобы вложенный flex-ребёнок
   вообще МОГ прокручиваться: без него авто-минимум по содержимому раздувает
   колонку, и скролл уезжает наружу, во всю страницу. */
.left-panel {
  width: 35%;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  border-right: 1px solid var(--border);
}

.left-panel-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* -- Наряд №195, работа 2: строка темы над корешками вкладок ---------------
   Один ряд, flex-shrink: 0 (та же дисциплина, что у .left-tabs — не уезжает
   вместе с содержимым и не сжимается). Имя темы обрезается многоточием, а
   не переносится — бюджет высоты левой колонки на 1366×768 равен нулю
   (свод наряда №142), вторая строка стоила бы дороже, чем короткое имя. */
.tema-line {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-shrink: 0;
  padding: 7px var(--s2) 6px;
  border-bottom: 1px solid var(--border);
}

.tema-line-nazvanie {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}

.tema-line-chip {
  flex-shrink: 0;
  margin-left: auto;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 9px;
  border-radius: var(--r-pill);
  white-space: nowrap;
}

/* -- Корешки вкладок: видимы всегда, не уезжают вместе с содержимым --------- */
.left-tabs {
  display: flex;
  flex-shrink: 0;
  gap: 2px;
  padding: var(--s2) var(--s2) 0;
  border-bottom: 1px solid var(--border);
}

.left-tab {
  position: relative;
  flex: 1;
  padding: 9px var(--s2);
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: 6px 6px 0 0;
  margin-bottom: -1px;
  font: inherit;
  font-size: var(--fs-sec);
  font-weight: 600;
  color: var(--text-soft);
  background: none;
  cursor: pointer;
  /* Подпись вкладки не переносится и не сжимается в слоги: три коротких слова
     обязаны читаться при любой ширине колонки (пол 360 px даёт 120 px на
     корешок — с запасом). */
  white-space: nowrap;
}

.left-tab:hover {
  color: var(--text);
  background: var(--surface-2);
}

/* Выбранная вкладка «срослась» с областью под собой: разрыв нижней границы —
   тот признак принадлежности, который не зависит ни от цвета, ни от веса
   шрифта, то есть держится и в высококонтрастных режимах. */
.left-tab[aria-selected="true"] {
  color: var(--accent-text);
  background: var(--surface);
  border-color: var(--border);
  border-bottom: 1px solid var(--surface);
}

.left-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Индикатор нового материала. Постановка, пункт 1: движок больше не
   переключает вкладку сам — вместо этого «заметный индикатор». Точка, а не
   мерцание фона: мерцание можно не поймать боковым зрением, точка держит
   форму до самого перехода. Дублируется в aria-label (app.js). */
.left-tab-novoe {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: 6px;
  border-radius: 50%;
  background: var(--attn);
  vertical-align: middle;
  animation: left-tab-novoe-puls 1.8s ease-in-out infinite;
}

@keyframes left-tab-novoe-puls {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.35); opacity: 0.65; }
}

@media (prefers-reduced-motion: reduce) {
  .left-tab-novoe {
    animation: none;
    box-shadow: 0 0 0 3px var(--attn-bg);
  }
}

.left-tab-panel {
  display: flex;
  flex-direction: column;
}

/* Пустое состояние вкладки — объясняет смысл, а не сообщает о нехватке данных
   (та же норма, что у пустого конспекта, наряд №140). Прячется, как только
   появляется условие: признак — соседний #statement без hidden. */
.vkladka-pusto {
  margin: 0;
  padding: var(--s6) 20px;
  font-size: var(--fs-sec);
  color: var(--text-soft);
}

.left-tab-panel:has(> .statement:not([hidden])) > .vkladka-pusto {
  display: none;
}

.statement {
  /* Наряд №139, работа 3 — верхний отступ отдан липкой подписи «Условие
     задачи» (работа 2), боковые ужаты 24 → 20 px: на 1366 px это 8 px ширины
     строки, то есть меньше переносов и меньше высоты у того же текста. */
  padding: var(--s6) 20px;
  line-height: var(--lh-body);
}

.statement pre {
  background: var(--surface-2);
  padding: 8px;
  overflow-x: auto;
}

.workspace {
  width: 40%;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border);
}

/* Наряд №23, работа 8 (D24): предпоказ окна кода — узел 3 назвал его, но
   задачи ещё нет. Само поле и подпись «Твоя программа» видны, действия над
   задачей — нет: «Отправить» до выдачи предлагало бы отправить пустоту. */
.workspace.workspace-preview .controls,
.workspace.workspace-preview #next-task-control,
.workspace.workspace-preview .output-panel {
  display: none;
}

/* Наряд №23, работа 8 (инвариант D24): элемент, о котором заходит речь,
   не просто появляется — он ОБОЗНАЧЕН. Три вспышки рамкой: достаточно, чтобы
   поймать взгляд, и заведомо конечно, чтобы не превратиться в фон. */
.ui-element-highlight {
  animation: ui-element-flash 0.85s ease-in-out 3;
}

@keyframes ui-element-flash {
  0%, 100% { box-shadow: inset 0 0 0 0 rgba(255, 176, 32, 0); }
  50% { box-shadow: inset 0 0 0 3px var(--attn); }
}

@media (prefers-reduced-motion: reduce) {
  .ui-element-highlight {
    animation: none;
    box-shadow: inset 0 0 0 3px var(--attn);
  }
}

/* Наряд №9, работа 8: было — фиксированные 25% плюс отдельный класс
   .chat-only, переключаемый JS, чтобы отдать чату всю ширину, когда
   .statement/.workspace скрыты. flex:1 делает то же самое без отдельного
   состояния — чат сам заполняет то, что осталось от .left-panel/.workspace,
   независимо от того, сколько из них сейчас видимы (у скрытых display:none,
   в раскладке они не участвуют вовсе). */
.chat {
  flex: 1;
  /* Наряд №139, работа 1. Было `min-width: 0` — «сжимайся сколько угодно»:
     на 1366 px в практике колонке штурмана доставалось 342 px, потому что
     35 % + 40 % забирали своё первыми. Пол в 380 px заставляет ужаться их, а
     не ленту разговора: 360 + 480 + 380 = 1220 < 1366, все три пола помещаются
     на нижней границе наряда одновременно.
     Прежнее назначение `min-width: 0` — чтобы широкий контент (ASCII-схема,
     код-блок) не растягивал колонку — держится не здесь, а там, где и должно:
     `min-width: 0` у .chat-message и `overflow-x: auto` у её pre/table. */
  min-width: 380px;
  display: flex;
  flex-direction: column;
}

/* Наряд №9, работа 8: карточки — отдельные плашки в левом поле, не табы над
   лентой чата (было, наряд №4, фаза 4). Каждая .card-tile сворачивается и
   разворачивается независимо от второй — заголовок-кнопка сам себе и
   открывающий, и закрывающий элемент (тот же приём, что и у разделов
   конспекта ниже), отдельной кнопки «✕» не нужно. */
.cards-column {
  display: flex;
  flex-direction: column;
  /* Наряд №139, работа 3 — 8/12 → 6/8. Высота свёрнутой колонки входит в
     бюджет объединённой нормы D16+D14 напрямую: каждый её пиксель — пиксель,
     на который условие задачи ближе к нижней кромке экрана. Заголовки плашек
     в этой же работе выросли с 33 до 40 px ради попадания мышью, и оплачено
     это отступами, а не условием. */
  gap: 6px;
  padding: var(--s2);
  /* Наряд №19, работа 6 (объединённая норма D16+D14). Карточки стоят НАД
     условием (index.html), и чтобы развёрнутая карточка не выталкивала
     условие за экран — как это было до наряда №12, работы 5 — колонка
     ограничена по высоте и прокручивается внутри себя. Свёрнутые плашки
     занимают заметно меньше (в прогоне 08.08 — 99 px), лимит их не
     касается; он ловит именно разворот.
     flex-shrink: 0 — чтобы колонка не сжималась в ноль, когда условие
     длинное: обе части остаются видимыми.

     Наряд №27, работа 1 (пятый заход). Ограничение осталось, но перестало
     быть безусловным: оно защищает УСЛОВИЕ ЗАДАЧИ, и там, где условия на
     экране нет, защищать нечего. Замер
     (scripts/naryad27-diagnostika-panelej.py): пока идёт разбор, под
     колонкой 487 px пустого экрана, а списки подрезаны — ровно то, что
     Роман показывает пятый раз. Правило ниже включает 40 % только когда
     #statement виден. */
  /* Наряд №142, работа 1: потолок высоты и собственная прокрутка СНЯТЫ.
     Они защищали условие задачи от развёрнутой карточки — а во вкладках
     условие и карточка не делят место вовсе, защищать нечего. Правило
     `.left-panel:has(> .statement:not([hidden])) > .cards-column { max-height:
     40% }`, стоявшее здесь с наряда №27, снято вместе с ними: селектор
     описывает раскладку, которой больше нет. */
  flex-shrink: 0;
}

.cards-column:not(:empty) {
  border-bottom: 1px solid var(--border);
}

/* Наряд №27, работа 1 — ГЛАВНАЯ строка находки, пятый заход.
   `.cards-column` — флекс-колонка, а дети флекса сжимаемы по умолчанию
   (flex-shrink: 1). Когда плашки не влезали в ограничение колонки, флекс
   ужимал КАЖДУЮ, и `overflow: hidden` ниже срезал содержимое насухо: ни
   полосы прокрутки, ни признака. Замер до правки
   (scripts/naryad27-diagnostika-panelej.py, 1440×812): плашка 146 px при
   содержимом 397 px — 251 px срезано; подсказка обрезки в теле есть, но
   липнет к нижней кромке ТЕЛА, которая на те же 251 px ниже видимой кромки
   плашки. Отсюда четыре промаха подряд: колонка отвечала «переполнения
   нет» — и не врала, она не переполнялась, она давила детей.
   flex-shrink: 0 — плашка держит свою высоту, а лишнее уходит в прокрутку
   КОЛОНКИ, у которой признак обрезки уже есть и теперь срабатывает. */
.card-tile {
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  overflow: hidden;
  flex-shrink: 0;
}

/* Наряд №23, работа 7 (инвариант D23): пройденный урок стоит рядом с текущими
   карточками, но не притворяется ими — приглушённый заголовок и пунктирная
   рамка говорят «это из прошлого, открой, если нужно». */
.card-tile-archived {
  border-style: dashed;
}

.card-tile-archived > .card-tile-header {
  background: var(--surface);
  color: var(--text-dim);
  font-weight: normal;
}

/* == Наряд №142, работа 2 — ШАПКА КАРТОЧКИ ЧТЕНИЯ =========================
   Значок, вид материала и тема — тремя элементами, а не одной строкой с
   эмодзи внутри. Пока эмодзи стоял в тексте подписи, диктор читал ребёнку
   «книга Конспект», а тема лепилась к названию вида без иерархии.
   Карточка текущей темы РАСКРЫТА по умолчанию — вкладку открывают ради неё,
   и лишний клик «раскрыть то, за чем пришёл» здесь ничего не экономит. */
.card-tile-chteniya > .card-tile-header {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.card-tile-znachok {
  flex-shrink: 0;
  font-size: 16px;
  line-height: 1;
}

.card-tile-podpisi {
  display: flex;
  flex-direction: column;
  min-width: 0;
}



/* Наряд №25, работа 8: развёрнутый урок архива содержит СВОИ карточки —
   конспект и словарик отдельными плашками, той же формы, что и текущие.
   Отступ слева показывает вложенность: это карточки урока, а не ещё два
   урока рядом. */
.card-tile-archived-cards {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 6px 6px 12px;
}

.card-tile-archived-card {
  border-style: solid;
}

/* Наряд №26, работа 2 (находки 5, 7, 14): все пройденные уроки — под ОДНОЙ
   плашкой, дерево в три уровня. Корень выделен сплошной рамкой и обычным
   цветом заголовка: он не «из прошлого», он постоянный элемент колонки, а
   вот уроки внутри него — да, и они остаются пунктирными и приглушёнными.
   Второй отступ вложенности — чтобы урок читался веткой корня, а его
   конспект и словарик — листьями ветки. */
.card-tile-archive-root {
  border-style: solid;
  /* Наряд №139, работа 3 — архив стоит ПЕРВЫМ (решение Романа, наряд №25,
     работа 8: «что было раньше, стоит выше»; порядок не пересматривается).
     Но первым он же и упирался в текущий конспект без единого признака, что
     это два разных пласта: одинаковый отступ 8 px между «Предыдущие
     конспекты» и «Конспект» и между «Конспект» и «Словариком». Черта под
     корнем архива говорит «дальше — то, чем ты занят сейчас», не меняя
     порядка и не отодвигая текущие карточки заметно (8 → 14 px). */
  margin-bottom: 6px;
  border-bottom: 2px solid var(--border-strong);
}

.card-tile-archive-root > .card-tile-header {
  background: var(--surface);
  color: var(--text-soft);
}

.card-tile-archive-root .card-tile-archived-cards .card-tile-archived-cards {
  padding-left: 18px;
}

/* Наряд №139, работа 3 — заголовок плашки это ОСНОВНОЙ орган управления
   левого поля: им конспект и словарик сворачиваются и разворачиваются, и он же
   единственный способ добраться до архива тем. 33 px высоты и 13 px кегля были
   размером служебной подписи, а не кнопки, по которой ребёнок целится мышью
   на чужой машине. 40 px — компромисс с объединённой нормой D16+D14: детские
   44 px здесь взять нельзя, потому что колонка ограничена 40 % высоты левого
   поля, когда на экране условие, и три плашки по 44 съели бы её треть. */
.card-tile-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  width: 100%;
  min-height: 40px;
  background: var(--surface-2);
  border: none;
  color: var(--text);
  padding: var(--s2) var(--s3);
  font-size: var(--fs-sec);
  font-weight: bold;
  text-align: left;
  cursor: pointer;
}

/* Шеврон не сжимается — иначе на узком поле длинное название темы съедало
   единственный признак «это сворачивается». */
.card-tile-chevron {
  flex: 0 0 auto;
}

.card-tile-chevron {
  transition: transform 0.15s ease;
}

.card-tile-header[aria-expanded="true"] .card-tile-chevron {
  transform: rotate(180deg);
}

/* Наряд №10, работа 4 (диагностика 04.08): две карточки на прогоне открылись
   тихо и прошли незамеченными — движок разворачивал их тем же ходом, что
   выводил текстовую реплику, внимание ребёнка на ленте чата, не слева.
   Мерцание держится независимо от aria-expanded — открытая движком карточка
   мерцает так же, как свёрнутая, гаснет только по клику ребёнка
   (app.js::toggleCardTile/toggleKonspektSection снимают cardTileUnread). */
.card-tile-header.unread {
  animation: card-tile-blink 1.6s ease-in-out infinite;
}

.card-tile-unread-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-left: 7px;
  border-radius: 50%;
  background: var(--attn);
  vertical-align: middle;
}

@keyframes card-tile-blink {
  0%, 100% { background: var(--surface-2); }
  50% { background: var(--attn-bg); }
}

/* Наряд №23, работа 5 (D21): при выключенной анимации мерцания не остаётся
   вовсе, и на прогоне 09.08 «новые карточки не мерцали» может объясняться в
   том числе этим — reduced-motion включают и не помнят об этом. Рамка в один
   пиксель заметна хуже, чем движение: даём непрозрачную заливку и жирную
   рамку, чтобы непрочитанная плашка отличалась от прочитанной с одного
   взгляда, без всякого движения. */
@media (prefers-reduced-motion: reduce) {
  .card-tile-header.unread {
    animation: none;
    background: var(--attn-bg);
    box-shadow: inset 0 0 0 2px var(--attn);
  }
}

.card-tile-body {
  padding: 10px var(--s3);
  /* Наряд №142, работа 1: `max-height: 45vh` и `overflow-y: auto` сняты —
     второй и третий вложенные скроллы левой колонки. Замер наряда №30 ловил
     здесь обрезку 582 px у словарика второго урока; сегодня резать нечем,
     карточка растёт во всю высоту вкладки, а прокручивается вкладка. */
  /* Наряд №139, работа 3: 13 → 14 px. Конспект и словарик — вторичный текст по
     шкале канона №136 (раздел 3.2), но текст, а не подпись: ребёнок читает их
     абзацами и с расстояния. 13 px было ниже всей вторичной шкалы экрана. */
  font-size: var(--fs-sec);
  line-height: 1.5;
  border-top: 1px solid var(--border);
}

/* -- Наряд №30, работа 10 — прокрутка панелей, ПОСЛЕ ЗАМЕРА -----------------

   ЗАМЕР ДО ПРАВКИ (scripts/naryad30-zamer-panelej.py, WebKit = движок Safari,
   MacBook 13" 1440×812, условия на экране НЕТ):

     · словарик урока 1, разбор идёт:   содержимое 545 px, видно 364 — обрезано 181;
     · словарик урока 2, раздел открыт: содержимое 946 px, видно 364 — обрезано 582;
     · режет НЕ колонка (её обрезано 0 px, max-height: none — работа 1 наряда
       №27 ограничение сняла), режет ВТОРОЙ ограничитель, `.card-tile-body`
       max-height 45vh = 365 px;
     · пустого экрана под колонкой в этот момент 297 px.

   Ответ на вопрос шага 1 наряда: переполнение ОСТАЁТСЯ, индикатор нужен.

   ЧТО ИЗМЕНИЛОСЬ ПО СРАВНЕНИЮ С НАРЯДАМИ №23/25/27. Признак был строкой во всю
   ширину, оранжевой (тот же цвет, что у предупреждения «это форма записи,
   запускать нельзя» — сигнал, указывающий на два разных смысла, не указывает ни
   на один), с текстовой инструкцией, и у левой панели ЛЕЖАЛ ПОВЕРХ ТЕКСТА.
   Стало три вещи вместо одной:

     1. постоянная полоса прокрутки — в macOS системные скроллбары overlay и
        невидимы, пока не крутишь; ровно поэтому признака не было видно пять
        заходов. Тонкий постоянный трек говорит сразу и что прокрутка есть, и
        сколько её, и где ты;
     2. затухание у нижней кромки — места не занимает, ничего не перекрывает,
        исчезает само в конце списка;
     3. кнопка — маленькая, у правого нижнего угла, нейтрального цвета, без
        текста, и НЕ ПОВЕРХ ТЕКСТА: под неё зарезервирован padding-bottom.
        Листает на экран вниз, а не прыгает в конец списка. */

/* Шаг 2: полоса прокрутки видна всегда. `-webkit-appearance: none` вместе с
   явной шириной — то самое, что выключает overlay-режим в WebKit: пока полоса
   не стилизована, macOS рисует её поверх содержимого и прячет до прокрутки.

   `scrollbar-width`/`scrollbar-color` здесь СОЗНАТЕЛЬНО НЕ СТОЯТ, хотя это и
   стандартные свойства. Замером (scripts/naryad30-zamer-panelej.py, WebKit)
   проверено: когда они заданы, движок переходит на стандартную модель и
   ::-webkit-scrollbar перестаёт применяться — полоса остаётся overlay, ширина
   отнимаемого места 0 px, то есть ровно то состояние, ради которого работа и
   заведена. Роман смотрит тренажёр в Safari, и здесь важнее он, чем Firefox. */
.left-panel-scroll::-webkit-scrollbar,
.chat-messages::-webkit-scrollbar {
  -webkit-appearance: none;
  width: 9px;
}

.left-panel-scroll::-webkit-scrollbar-track,
.chat-messages::-webkit-scrollbar-track {
  background: rgba(255, 255, 255, 0.05);
}

.left-panel-scroll::-webkit-scrollbar-thumb,
.chat-messages::-webkit-scrollbar-thumb {
  background: #5a5a5a;
  border-radius: 5px;
  border: 2px solid transparent;
  background-clip: padding-box;
}

/* Резерв под кнопку — ПОЛОСА СПРАВА, а не отступ снизу. Кнопка липнет к низу
   ВИДИМОЙ области, а не к концу содержимого: при прокрутке наверх под ней
   оказывается произвольная строка середины списка, и нижним отступом это не
   лечится (замер до правки показал два пересечения с текстом). Правый жёлоб
   шириной с кнопку убирает пересечение при любой позиции прокрутки — это и
   есть «поверх фона панели и не поверх текста» (хард-гейт 6 наряда №30).

   Левая панель получает жёлоб тоже: до этого наряда подсказка там честно
   перекрывала содержимое, потому что «у панели нет своего внутреннего поля». */
.left-panel-scroll.has-more-below {
  position: relative;
  padding-right: 34px;
}

/* Шаг 3: затухание. Липнет к низу видимой области, места не занимает
   (отрицательный нижний отступ гасит собственную высоту), кликов не ловит. */
.overflow-fade {
  position: sticky;
  bottom: 0;
  display: block;
  height: 26px;
  margin: 0 -34px -26px -14px;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(35, 35, 35, 0), var(--surface) 90%);
}

.left-panel-scroll > .overflow-fade {
  margin: 0 -34px -26px -12px;
  background: linear-gradient(to bottom, rgba(30, 30, 30, 0), var(--bg) 90%);
}

/* Шаг 4: кнопка. Нейтральный серый — оранжевый в этом интерфейсе занят
   предупреждением. Ни строки текста: стрелка и aria-label. */
.overflow-hint {
  position: sticky;
  bottom: 6px;
  display: block;
  width: 24px;
  height: 24px;
  /* Отрицательный правый отступ уводит кнопку в зарезервированный жёлоб, левый
     auto прижимает её к нему; нижний гасит собственную высоту, чтобы кнопка не
     удлиняла содержимое и не создавала переполнение сама собой. */
  margin: 0 -29px -24px auto;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  font: inherit;
  font-size: 12px;
  line-height: 22px;
  text-align: center;
  color: var(--text-soft);
  background: var(--border);
  cursor: pointer;
}

.overflow-hint:hover,
.overflow-hint:focus-visible {
  color: var(--accent-text);
  background: var(--border-strong);
}

.card-tile-body :target,
.card-tile-body .card-heading-highlight {
  background: var(--attn-bg);
  border-radius: 3px;
}

/* Наряд №8, работы 2/3: конспект — стопка адресованных разделов, не сплошной
   текст. Каждый раздел — заголовок-кнопка (сворачивает/разворачивает только
   себя) плюс тело, которое есть в DOM только когда раздел развёрнут. */
.konspekt-section {
  border-bottom: 1px solid var(--border);
}

.konspekt-section:last-child {
  border-bottom: none;
}

.konspekt-section-heading {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  color: var(--text);
  font-size: var(--fs-sec);
  font-weight: bold;
  /* Наряд №139, работа 3 — раздел конспекта тоже кнопка: 30 px до правки,
     36 после. Меньше плашки (это уровень вложенности), но уже цель, а не
     строка текста. */
  min-height: 36px;
  padding: var(--s2) var(--s1);
  cursor: pointer;
}

.konspekt-section-heading::before {
  content: "▸";
  display: inline-block;
  width: 1em;
  transition: transform 0.15s ease;
}

.konspekt-section-heading[aria-expanded="true"]::before {
  transform: rotate(90deg);
}

.konspekt-section-body {
  padding: 0 4px 10px 1.4em;
}

/* == Наряд №142, работа 2 — БЛОКИ УЧЕБНОГО МАТЕРИАЛА ======================

   Форма с реф-макета («Карточка конспекта.pdf»): у каждой роли своя плашка,
   у ошибки — цветная левая полоса. Роли различаются ТРЕМЯ носителями сразу —
   подписью, фоном и полосой, — а не одним цветом: цвет и есть то, чего часть
   людей не видит, и ровно за это наряд №139 правил статус попытки.

   ГОРИЗОНТАЛЬНОЙ ПРОКРУТКИ В МАТЕРИАЛЕ НЕТ НИГДЕ. Постановка, пункт 2:
   «длинные строки визуально переносятся, но исходный текст не изменяется и
   не обрезается». Отсюда `white-space: pre-wrap` и `overflow-wrap:
   break-word` у всех моноширинных мест карточки: перенос делает БРАУЗЕР при
   отрисовке, символы в тексте те же самые — скопировать код из карточки
   по-прежнему можно, и скопируется он как написан. Редактор кода (Monaco)
   этим не затронут: он не карточка и в постановке выведен за скобки. */
.material-blok {
  margin: var(--s2) 0;
  padding: var(--s2) var(--s3);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-size: var(--fs-sec);
  line-height: 1.5;
}

.material-blok-podpis {
  margin-bottom: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--text-soft);
}

.material-blok > *:last-child,
.material-blok-telo > *:last-child {
  margin-bottom: 0;
}

.material-blok-telo > *:first-child {
  margin-top: 0;
}

/* ГЛАВНАЯ МЫСЛЬ стоит РЯДОМ С ЗАГОЛОВКОМ раздела и не прячется под аккордеон:
   «суть карточки понятна без обязательной прокрутки» (постановка, пункт 2)
   держится местом блока в разметке, а не его размером. Подписи у него нет —
   она стоит первой строкой в самой карточке и была бы шумом на каждом
   разделе. */
.material-blok-glavnoe {
  margin: 0 0 var(--s2) 1.4em;
  border-left: 3px solid var(--accent);
  background: var(--surface-2);
  font-weight: 500;
  color: var(--text);
}

.material-blok-pravilo {
  border-left: 3px solid var(--accent);
}

/* Ошибка — единственный блок сигнального цвета. Он же занят предупреждением
   «это форма записи, запускать нельзя» (markFormCodeBlocks), и это не
   столкновение смыслов: оба говорят «здесь можно споткнуться». */
.material-blok-oshibka {
  border-left: 3px solid var(--attn);
  background: var(--attn-bg);
}

.material-blok-primer {
  border-left: 3px solid var(--border-strong);
}

/* КОД В КАРТОЧКЕ ПЕРЕНОСИТСЯ. Прибор наряда №142 нашёл здесь 550 px
   горизонтальной прокрутки на длинной строке `cout << … << endl;`: моё
   собственное правило pre-wrap стояло только у блока `primer`, а код marked
   отдаёт обычным <pre><code>, у которого перенос выключен по умолчанию.
   Постановка (пункт 2) требует обратного и объясняет, почему: длинная строка
   должна переноситься ВИЗУАЛЬНО, а исходный текст оставаться прежним — и
   `white-space: pre-wrap` делает ровно это, скопировать код из карточки
   по-прежнему можно, и скопируется он как написан.

   `overflow-wrap: anywhere` — для строк, в которых нет ни одного пробела
   (длинный вывод примера): без него браузер честно не находит места разрыва
   и оставляет прокрутку.

   ⚠️ Правило намеренно ограничено карточкой материала. Редактор кода
   (Monaco) вне его: постановка выводит его за скобки, и перенос в редакторе
   сбил бы нумерацию строк, по которой компилятор указывает ошибку. */
.card-tile-body pre,
.card-tile-body pre code,
.konspekt-section-body pre,
.konspekt-section-body pre code {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.material-blok-telo-monoshirinnyy {
  font-family: "SF Mono", Consolas, monospace;
  font-size: 13px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* -- Вертикальные шаги процесса -------------------------------------------
   Ровно то, ради чего заменён носитель схемы: список переносится по ширине и
   не обрезается никогда, тогда как схема из моноширинных символов не
   переносится вовсе (замер аудита №141: 96 знаков в строке при 42
   доступных). Стрелка между шагами — CSS-псевдоэлемент, а не символ в
   разметке: она украшение связи, диктору её читать незачем. */
.material-shagi {
  margin: var(--s2) 0;
  padding: 0;
  list-style: none;
}

.material-shag {
  position: relative;
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: var(--s2);
  align-items: start;
  margin-bottom: 22px;
}

.material-shag:last-child {
  margin-bottom: 0;
}

.material-shag:not(:last-child)::after {
  content: "↓";
  position: absolute;
  left: 9px;
  bottom: -20px;
  font-size: 13px;
  line-height: 1;
  color: var(--accent);
}

.material-shag-nomer {
  display: block;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-text);
  font-size: 12px;
  font-weight: 700;
  line-height: 26px;
  text-align: center;
}

.material-shag-telo {
  padding: var(--s2) var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}

.material-shag-zagolovok {
  font-family: "SF Mono", Consolas, monospace;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  overflow-wrap: break-word;
}

.material-shag-poyasnenie {
  margin-top: 2px;
  font-size: var(--fs-sec);
  color: var(--text-soft);
}

/* Вложенный в шаг блок роли встаёт под своей плашкой, во второй колонке
   сетки: привязка «ошибка → шаг 2» видна глазом ровно там же, где она
   записана в материале. */
.material-shag > .material-blok {
  grid-column: 2;
  margin-top: var(--s2);
}

/* Наряд №142, работа 4: предисловие словаря — то, что в карточке стоит ДО
   первого определения (объяснение, зачем этот список, и перечисление слов
   блока). Тише определений и без собственных отступов сверху: это подводка,
   а не запись словаря. */
.glossary-preambula {
  margin-bottom: var(--s3);
  font-size: var(--fs-label);
  line-height: 1.5;
  color: var(--text-soft);
}

.glossary-preambula > *:first-child { margin-top: 0; }
.glossary-preambula > *:last-child { margin-bottom: 0; }

.konspekt-empty-hint {
  color: var(--text-dim);
  font-size: var(--fs-sec);
  margin: var(--s1);
}

/* Наряд №10, работа 5 (диагностика 04.08): было — сплошная строка терминов
   через запятую, без определений. Столбик: группа «Понятия» / «Знаки кода»
   (app.js::renderGlossaryInto разводит по разметке терминов, ** vs `), внутри
   каждой — термин-кнопка, клик разворачивает одну строку определения под
   ним. Тот же приём, что у .konspekt-section выше — не совпадающий класс
   специально: термин здесь не заголовок раздела текста, а отдельная строка
   списка, есть смысловая разница (полезно при будущей правке разметки). */
.glossary-group-title {
  font-size: 11px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  padding: 10px 4px 4px;
}

.glossary-group-title:first-child {
  padding-top: 4px;
}

.glossary-term-row {
  border-bottom: 1px solid var(--border);
}

.glossary-term-row:last-child {
  border-bottom: none;
}

.glossary-term-heading {
  display: block;
  width: 100%;
  min-height: 36px;
  text-align: left;
  background: none;
  border: none;
  padding: var(--s2) var(--s1);
  cursor: pointer;
}

.glossary-term-heading::before {
  content: "▸";
  display: inline-block;
  width: 1em;
  color: var(--text-dim);
  transition: transform 0.15s ease;
}

.glossary-term-heading[aria-expanded="true"]::before {
  transform: rotate(90deg);
}

.glossary-term {
  font-size: var(--fs-sec);
  color: var(--text);
}

.glossary-term--concept {
  font-weight: bold;
}

.glossary-term--code {
  font-family: "SF Mono", Consolas, monospace;
  background: var(--surface-2);
  padding: 1px 5px;
  border-radius: 3px;
}

.glossary-definition {
  padding: 0 var(--s1) var(--s2) 1.4em;
  font-size: var(--fs-sec);
  line-height: 1.5;
  color: var(--text-soft);
}

/* Хвост №142.1, работа 3: контейнер `.child-buttons` снят вместе с блоком
   внизу колонки — «Позвать тренера» вернулась в шапку сессии (index.html), и
   собственной строки под неё больше нет. Геометрия кнопки переехала в
   `#call-trainer-btn` выше, рядом с `.logout-btn`: они соседи по шапке, и
   держать их правила в разных концах файла значило бы разводить то, что
   читается только вместе.

   ⚠️ Правило наряда №139 про 44 px и кегль 14 у детских действий здесь
   ослаблено СОЗНАТЕЛЬНО и названо: в шапке сессии «Позвать тренера» — 30 px,
   как и «Завершить тренировку». Причина — не экономия места, а иерархия:
   44 px в этом наряде зарезервированы за ГЛАВНЫМ действием экрана
   («Проверить решение», наряд №142, работа 5), и сессионные действия не
   имеют права спорить с ним размером. Порог попадания мышью 30 px остаётся
   выше системного минимума, а заметность «Позвать тренера» держится мятной
   рамкой — тем, чего нет у соседней кнопки. */

.child-buttons-ack {
  font-size: 12px;
  color: var(--text-dim);
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.chat-message {
  padding: var(--s2) var(--s3);
  border-radius: var(--s2);
  line-height: var(--lh-body);
  /* Наряд №139, работа 1. Было `max-width: 90%` — доля колонки и ничего
     кроме: в разборе, где рабочая область скрыта и чат занимает почти весь
     экран, реплика штурмана вырастала до 770 px на 1366 и 1094 px на 1920
     (замер). Глаз теряет начало следующей строки задолго до этого. Потолок
     теперь двойной: доля колонки — чтобы в узкой колонке пузырь не упирался
     в края, и мера строки — чтобы в широкой он не расползался. 68ch при
     16 px — ~66 знаков, середина диапазона читаемости. */
  max-width: min(90%, 68ch);
  white-space: pre-wrap;
  /* Наряд №4, фаза 5, пункт 2: широкий контент (ASCII-схема «путь
     программы», код-блоки с выровненными комментариями — узел 3/7) не
     должен ломать вёрстку чата. min-width: 0 нужен, потому что этот блок —
     flex-item (.chat-messages: display:flex; column) — без него дочерний
     overflow-x:auto ниже не сработает, элемент просто растянет родителя. */
  min-width: 0;
}

.chat-message pre,
.card-tile-body pre {
  overflow-x: auto;
  max-width: 100%;
}

/* Наряд №8, работа 6: каркас программы — форма записи, не готовая программа
   (на месте тела стоит `…`). Копирование не запрещаем — только предупреждаем
   плашкой сверху, чтобы её не унесли в редактор как есть (живой прогон 01.08). */
.code-form-wrap {
  border: 1px dashed var(--attn-line);
  border-radius: 4px;
  overflow: hidden;
}

.code-form-badge {
  background: var(--attn-bg);
  color: var(--attn-text);
  font-size: 12px;
  font-weight: 600;
  padding: 6px 10px;
}

.code-form-wrap pre {
  margin: 0;
}

.chat-message table,
.card-tile-body table {
  display: block;
  overflow-x: auto;
  max-width: 100%;
  border-collapse: collapse;
}

.chat-message-student {
  align-self: flex-end;
  background: var(--accent);
}

/* Наряд №144, работа 1: реплика штурмана идёт во ВСЮ ширину ленты, до её
   правой границы, с обычными внутренними отступами.

   Что здесь отменяется. Наряд №139 поставил общий потолок `min(90%, 68ch)`
   на .chat-message и объяснил его мерой строки: 66 знаков — середина
   диапазона читаемости, а на 1920 пузырь расползался до 1094 px. Основание
   верное и для реплики ребёнка остаётся в силе. Для реплики штурмана
   постановка №144 его снимает, и вот в чём разница между двумя случаями.
   Реплика ребёнка — короткая строка, прижатая вправо: доля ширины делает её
   пузырём, и пузырь читается как «это сказал я». Реплика штурмана — почти
   всегда абзац с разметкой, а часто и блок кода или таблица, для которых
   68ch не мера строки, а обрезка носителя: ASCII-схема и таблица начинали
   требовать горизонтальной прокрутки внутри пузыря ровно потому, что пузырю
   не давали ширины колонки, которая у него была.

   Различие автора при этом не теряется: оно держится не долей ширины, а
   заливкой и прижатием (align-self: flex-end у ребёнка ниже) — тем, чем и
   держалось всегда.

   `align-self: stretch` обязателен вместе с max-width: без него flex-item
   сжимается по содержимому и короткая реплика до правой границы не доходит,
   сколько ей ни разрешай. */
.chat-message-agent {
  align-self: stretch;
  max-width: 100%;
  background: var(--surface-2);
}

/* Наряд №4, фаза 2.6: порция с обязательным вопросом узла — визуально
   отличима от обычной реплики, чтобы ребёнок понимал «сейчас ход его»
   (Роман, 30.07: «А теперь ВОПРОС!»). Метка — оформление движка, не
   вписывается в текст сценария (см. agent-core/scenario_engine.py). */
.chat-message-question {
  border: 1px solid var(--question);
}

.chat-message-question-badge {
  font-size: 11px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--question);
  margin-bottom: 4px;
}

/* Наряд №19, работа 3 (инвариант A18): роль секции приходит ПОЛЕМ контракта
   (section_roles), фронт по нему красит. Служебное объявление карточки
   («Ещё кусок в конспект») — не реплика узла и не вопрос: оно про экран, а
   не про тему, и выглядеть должно тише содержательной реплики. До наряда
   №19 роли не было вовсе, и объявление приходило ребёнку тем же пузырём,
   что тезис, а на прогоне 08.08 ещё и с меткой «Вопрос». */
.chat-message-service {
  background: transparent;
  border: 1px dashed var(--border);
  color: var(--text-dim);
  font-size: 13px;
}

/* Наряд №7, работа 7: индикатор ожидания — появляется сразу после отправки,
   пока идёт ответ (закрывает пункт 10 критерия приёмки наряда №6 иначе:
   порог «первый отклик за 3 секунды» снят, вместо него — гарантия, что
   отклик виден мгновенно, см. HANDOFF-7). Приглушённый и курсивный, не
   спутать с настоящей репликой агента. */
.chat-message-thinking {
  font-style: italic;
  opacity: 0.6;
}

/* Наряд №144, работа 3: область объявления для программы чтения с экрана.
   Не `display: none` и не `hidden` — скрытое так содержимое чтец не читает
   вовсе, и живая область перестала бы быть живой. Приём стандартный: коробка
   1×1 px, вырезанная в ноль и убранная из потока. */
.tolko-dlya-chtetsa {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.material-next-control {
  padding: 0 16px 12px;
}

/* «Дальше →» — главное и единственное продвигающее действие разбора: ребёнок
   нажимает её десятки раз за занятие. Детские 44 px. */
#material-next-btn,
#scenario-advance-btn {
  background: var(--go);
  color: var(--accent-text);
  border: none;
  min-height: var(--h-btn);
  padding: 0 var(--s6);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sec);
}

#material-next-btn:disabled,
#scenario-advance-btn:disabled {
  cursor: default;
  opacity: 0.6;
}

/* Наряд №106, работа 3: блок «вопрос с вариантами» (v5, formal_checks) —
   визуально родствен .material-next-control (тот же отступ, тот же
   контейнер под лентой чата), но с собственной рамкой, чтобы не путаться
   со служебной репликой сценария. */
.v5-formal-check {
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}

.v5-formal-check-prompt {
  margin: 0 0 8px;
}

.v5-formal-check-options {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Варианты ответа на вопрос узла — учебное действие, не служебное: кегль
   основного текста и детская высота. */
.v5-formal-check-options button {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  min-height: var(--h-btn);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-sm);
  cursor: pointer;
  text-align: left;
  font-size: var(--fs-sec);
}

.v5-formal-check-options button:hover {
  border-color: var(--accent);
}

.v5-formal-check-options button:disabled {
  cursor: default;
  opacity: 0.6;
}

/* «Назад» — вторичное действие (чистое чтение, не продвигает прохождение),
   визуально приглушённее «Дальше», чтобы не выглядеть равноценной парой. */
.chat-form {
  display: flex;
  /* Наряд №142, работа 5: разделителя сверху у формы больше нет — его несёт
     .child-buttons над ней, и две линии подряд рисовали бы границу там, где
     смысловая одна (помощь и поле общения — один блок «сказать»). */
  border-top: none;
  /* Наряд №142, работа 5 — КНОПКА ЧАТА НЕ ВЫШЕ КНОПКИ РЕШЕНИЯ.
     Замер аудита №141: она была 101×63 против 117×44 у «Отправить» решение —
     то есть заметнее той, что закрывает задачу. Причина была не в правиле, а
     в его отсутствии: у флекс-ряда align-items по умолчанию `stretch`, и
     кнопка растягивалась по высоте двухстрочного textarea. `flex-end`
     возвращает ей собственную высоту и ставит по нижней кромке поля. */
  align-items: flex-end;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}

.chat-form textarea {
  flex: 1;
  resize: none;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--r-sm);
  padding: var(--s2);
  font-family: inherit;
  /* Наряд №139, работа 4: поле, в которое ребёнок пишет своими словами, —
     основной текст, а не подпись. Наследуемый кегль формы был 13-14 px. */
  font-size: var(--fs-body);
  line-height: 1.4;
}

/* Наряд №142, работа 5 — ВТОРИЧНОЕ ДЕЙСТВИЕ.
   Заливку и цвет ставит фирменный слой (brand-bcsp.css: роль «вторичная»,
   белая с мятной рамкой) — здесь только геометрия и вес. Кнопка не выше,
   не ярче и не заметнее «Проверить решение»: 40 px против 44, обычный вес
   против полужирного, рамка против заливки. Порог попадания мышью при этом
   не нарушен — 40 px остаются в детской норме канона №136 для ВТОРИЧНЫХ
   действий, а 44 зарезервированы за главным.
   `type="submit"` и отправка по Enter — поведение, и они не тронуты. */
.chat-form button {
  border: none;
  min-height: 40px;
  padding: 0 var(--s3);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sec);
  white-space: nowrap;
}

/* Наряд №10, работа 5: подпись поля кода — постоянный элемент разметки, не
   содержимое редактора. flex-shrink: 0, чтобы не сжималась, когда редактору
   не хватает места.

   Наряд №139, работа 2 — ОДНА форма подписи на все поля экрана. Три ступени
   иерархии (условие → редактор → результат) должны быть узнаваемы как три
   ступени одного и того же, а не как три разных элемента: до этой правки
   «Твоя программа» была 12 px капителью, шапка вывода — 12 px обычной
   строкой, а у условия подписи не было вовсе. Классы разведены намеренно и
   остаются разведёнными (.editor-label живёт внутри flex-колонки и несёт
   flex: 0 0 auto; .output-panel-header — шапка своей панели с рамкой снизу):
   общая здесь ФОРМА, а не роль. */
.panel-label,
.editor-label,
.output-panel-header {
  font-size: 12px;
  font-weight: bold;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.editor-label {
  flex: 0 0 auto;
  padding: 6px var(--s1) var(--s1);
}

/* Подпись условия задачи. Липкая: условие бывает длиннее экрана, и при
   прокрутке левого поля (общего для карточек и условия) ребёнок иначе теряет,
   что именно он читает — конспект прошлой темы выглядит так же, как условие.
   Отрицательные поля выводят её за отступы .statement во всю ширину поля. */
.statement > .panel-label {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: calc(-1 * var(--s6)) -20px var(--s3);
  padding: var(--s1) 20px 6px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.editor {
  flex: 1;
  min-height: 0;
}

/* Наряд №5, пункт H: окно вывода под редактором — код сверху, вывод снизу,
   стандартная раскладка IDE. Высота ограничена (не flex:1, как у редактора),
   чтобы редактор не схлопывался при длинном выводе — своя прокрутка внутри.

   Наряд №139, работа 2 — было `flex: 0 0 200px`, ровно 200 px ВСЕГДА. Гарантия
   «редактор не схлопнется» этим достигалась, но платил за неё самый частый
   случай: у расхождения вывода четыре строки, и 140 px из 200 оставались
   пустой чёрной плитой между кодом и результатом ровно там, где на 768 px
   высоты каждый пиксель отдан редактору. `0 0 auto` + max-height даёт ту же
   верхнюю границу (панель никогда не съест больше 200 px, гарантия цела) и
   отдаёт неиспользованное редактору. Нижняя граница — чтобы шапка и первая
   строка вывода были видны всегда. */
.output-panel {
  flex: 0 0 auto;
  min-height: 90px;
  max-height: 200px;
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--border);
  background: var(--surface-sunk);
  overflow: hidden;
}

.output-panel-header {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.output-panel-body {
  margin: 0;
  padding: 10px 12px;
  overflow: auto;
  font-family: "SF Mono", Consolas, monospace;
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-word;
  flex: 1;
}

/* Наряд №139, работа 5 — «показывать нечего» тоже состояние, а не строка
   моноширинного дампа. Кегль и шрифт человеческие: это речь о выводе, а не
   сам вывод (тот же приём, что у .compile-error-summary наряда №19). */
.output-panel-body .output-pusto {
  display: block;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: var(--fs-sec);
  line-height: var(--lh-body);
  color: var(--text-dim);
}

.output-panel-body .output-diff-label {
  color: var(--text-dim);
  display: block;
  margin-top: var(--s2);
}

/* Наряд №139, работа 4: половины дифа теперь в своих обёртках (app.js::
   _renderDiff), и `:first-child` стал истиной для ОБЕИХ подписей — отступ
   между «Ожидалось» и «Получено» пропал бы молча. Разводится по обёртке,
   а не по позиции. */
.output-panel-body .output-diff-expected,
.output-panel-body .output-diff-actual {
  display: block;
}

.output-panel-body .output-diff-expected .output-diff-label {
  margin-top: 0;
}

/* Наряд №139, работа 4 — расхождение читается за один взгляд.
   Было: и «Ожидалось», и «Получено» подсвечивали несовпавшую строку ОДНИМ
   красным. Красное ожидание — бессмыслица: эталон ни в чём не виноват, и
   ребёнок, сверяя две красные строки, ищет, какая из них его. Ожидание —
   спокойным зелёным (вот как надо), факт — красным (вот что вышло). */
.output-panel-body .output-mismatch {
  color: var(--fail);
  font-weight: bold;
}

.output-panel-body .output-diff-expected .output-mismatch {
  color: var(--ok);
}

.output-panel-body .output-diff-label {
  font-weight: bold;
}

/* Наряд №19, работа 5 (D17): короткий разбор ошибки компиляции обычным
   текстом, сырой вывод g++ — под свёрнутым <details>. Шрифт панели
   моноширинный (это окно вывода программы), но человеческая строка читается
   как речь, а не как дамп. */
.output-panel-body .compile-error-summary {
  margin: 0 0 8px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: 14px;
  line-height: 1.4;
  color: var(--text);
}

.output-panel-body .compile-error-raw > summary {
  cursor: pointer;
  color: var(--text-dim);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: 13px;
}

.output-panel-body .compile-error-raw > pre {
  margin: 8px 0 0;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Наряд №139, работа 1 — ряд контролов переносится, а не давит своих детей.
   Замер до правки (1366×768): «Попытка №3: Принято» в коробке 123 px и
   «Отложить задачу» в 96 px — обе в две строки. Переполнения при этом ноль,
   потому что переполнения и не было: flex-shrink ужимал элементы по ширине.
   nowrap на самих элементах переводит нехватку места из «порву текст на
   слоги» в «перенесу строку» — то есть в то, что видно и что чинится
   раскладкой. */
.controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s2) var(--s3);
  padding: var(--s3) var(--s4);
  border-top: 1px solid var(--border);
}

.controls button,
.controls .verdict,
.controls .task-timer {
  white-space: nowrap;
}

/* Вторичные действия задачи — одной группой: она уходит на следующую строку
   целиком, а не разъезжается по кнопке. margin-left: auto прижимает её
   вправо, когда место есть, и обнуляется переносом, когда его нет. */
.controls-secondary {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-left: auto;
}

/* Главное действие практики — детские 44 px (канон №136, раздел 3.4).
   Наряд №142, работа 5: и ЕДИНСТВЕННОЕ главное. Полужирный и основной кегль
   против обычного вторичного — «Проверить решение» обязано выигрывать у
   «Отправить сообщение» по всем трём носителям сразу (размер, вес, заливка),
   а не по одному: одного носителя ребёнок не считывает. */
#submit-btn {
  background: var(--accent);
  color: var(--accent-text);
  border: none;
  min-height: var(--h-btn);
  padding: 0 var(--s6);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-body);
  font-weight: 600;
}

#submit-btn:disabled {
  background: var(--border-strong);
  cursor: default;
}

/* Наряд №139, работа 4 — СТАТУС ПОПЫТКИ ЧИТАЕМ, А НЕ УГАДЫВАЕМ ПО ЦВЕТУ.
   Было: строка обычного кегля, отличающаяся от соседних только цветом текста.
   Цвет — единственный носитель смысла, а он же и единственное, чего не видит
   часть людей; плюс на 1366 px строка стояла зажатой между двумя кнопками и
   рвалась на две (это чинила работа 1). Стало: пилюля со знаком исхода —
   форма, знак и цвет вместе, три носителя вместо одного.

   :not(:empty) — пустой вердикт (новая выдача обнуляет его текст) не должен
   рисовать пустую плашку. */
.verdict {
  font-weight: 600;
  font-size: var(--fs-sec);
}

.verdict:not(:empty) {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--text-dim);
}

.verdict:not(:empty)::before {
  font-weight: bold;
}

.verdict.success { color: var(--ok); background: var(--ok-bg); }
.verdict.success::before { content: "✓"; }

.verdict.wrong_answer,
.verdict.runtime_error,
.verdict.compile_error,
.verdict.format_error,
.verdict.nuzhen_tsikl { color: var(--fail); background: var(--fail-bg); }
.verdict.wrong_answer::before,
.verdict.runtime_error::before,
.verdict.compile_error::before,
.verdict.format_error::before,
.verdict.nuzhen_tsikl::before { content: "✗"; }

.verdict.time_limit { color: var(--warn); }
.verdict.time_limit::before { content: "⏱"; }

/* Наряд №139, работа 4 — отказ СВЯЗИ, а не вердикт по решению. До этой правки
   оба состояния красились одним классом .wrong_answer, и «не доехало до
   судьи» выглядело как «судья сказал: неверно». Лечение у них разное: одно
   чинится кодом, другое повторным нажатием, и ребёнок должен различать. */
.verdict.verdict-otkaz { color: var(--fail); background: var(--fail-bg); }
.verdict.verdict-otkaz::before { content: "⚠"; }

/* Отправка. Пульсирующая точка — признак, что ход идёт прямо сейчас; без неё
   «Отправлено, ждём вердикт судьи…» неотличимо от застывшего экрана. */
.verdict.pending { color: var(--text-dim); }
.verdict.pending::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-dim);
  animation: verdict-pulse 1.1s ease-in-out infinite;
}

@keyframes verdict-pulse {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .verdict.pending::before { animation: none; opacity: 0.8; }
}

/* Внутри плашки успеха фон пилюли совпал бы с фоном плашки — пилюля
   исчезла бы. Там исход и так объявлен всей зоной: остаётся знак, цвет и
   увеличенный кегль. */
.itog:has(> .next-task-control:not([hidden])) .verdict.success {
  background: transparent;
  padding-left: 0;
  font-size: var(--fs-body);
}

.next-task-control {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 0 var(--s4) var(--s3);
}

/* Наряд №139, работа 2 — ЗОНА ИТОГА и ПЛАШКА УСПЕХА (P1-7 канона №136).

   Плашка включается признаком, который уже есть и уже верен на всех путях:
   видимой кнопкой перехода. Её показывают ровно три места, и все три — после
   принятой попытки (showNextTaskControl — только внутри `if (data.solved)`;
   showV5NextTaskControl; showV5NextTopicControl); прячут — те же hide*, что
   уже зовёт единый сброс новой выдачи. Отдельный флаг был бы шестым местом,
   которое можно забыть, ради состояния, которое выводится (см. index.html).

   Плашкой становится ВСЯ зона итога, а не отдельный элемент поверх неё:
   момент успеха — это и вердикт «Попытка №3: Принято», и время на задаче, и
   кнопка «дальше»; разнести их по двум блокам значило бы снова показать три
   вещи как три, а не как одну. */
.itog {
  border-top: 1px solid var(--border);
}

/* .controls своей рамкой больше не отделяется — её роль взял контейнер: две
   горизонтальные линии подряд в одном месте читаются как разрыв. */
.itog > .controls {
  border-top: none;
}

.itog:has(> .next-task-control:not([hidden])) {
  background: var(--ok-bg);
  border-top: 2px solid var(--ok);
}

/* В момент успеха на экране остаётся ОДНО следующее действие. «Отправить»,
   «Пауза» и «Отложить задачу» к этой минуте уже выключены кодом (submit-btn
   не включается обратно при solved; obnovitDostupnostOtlozhit гасит
   «Отложить» по vydachaZakryta) — то есть здесь прячется не работающий
   элемент управления, а серый макет элемента, единственная роль которого —
   отвлекать от кнопки перехода. Правило чисто визуальное и обратимое: ни
   одно состояние в JS от него не зависит. */
.itog:has(> .next-task-control:not([hidden])) #submit-btn,
.itog:has(> .next-task-control:not([hidden])) .controls-secondary {
  display: none;
}

/* Кнопка перехода — единственное действие плашки, и высота у неё детская
   (44 px, канон №136, раздел 3.4). */
.itog:has(> .next-task-control:not([hidden])) #next-task-btn,
.itog:has(> .next-task-control:not([hidden])) #v5-next-task-btn,
.itog:has(> .next-task-control:not([hidden])) #v5-next-topic-btn {
  min-height: var(--h-btn);
  font-size: var(--fs-sec);
  padding: 0 var(--s6);
}

/* Наряд №113, работа 2 — тот же большой зелёный стиль легаси-кнопки
   #next-task-btn, перенесённый на v5-контролы: они не получали ЭТОГО
   правила вовсе (только `hidden`/`disabled` в app.js), поэтому браузер
   рисовал их дефолтной белой кнопкой платформы. Селекторы разведены (см.
   их docstring в app.js/index.html) — правило общее, эндпоинты разные. */
#next-task-btn,
#v5-next-task-btn,
#v5-next-topic-btn {
  background: var(--go);
  color: var(--accent-text);
  border: none;
  padding: 10px 18px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

#next-task-message {
  color: var(--text-dim);
  font-size: 14px;
}

/* Наряд №139, работа 4 — часы задачи были голым «00:02» между вердиктом и
   кнопкой «Пауза». Голое число рядом с красной строкой вердикта читается как
   лимит, которого не существует: часы считают время НА ЗАДАЧЕ и ничего не
   ограничивают (наряд №35, работа 7 — отсчёт с открытия практики, не с
   выдачи). Подпись ставит их на место одним словом.

   Показывать ли часы ребёнку вообще — открытый вопрос Р-7 канона №136 и
   решение педагогическое, не вёрсточное: наряд №139 их сохраняет и делает
   читаемыми, а не убирает и не заводит заново. */
.task-timer {
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  font-size: var(--fs-sec);
}

.task-timer::before {
  content: "на задаче ";
  color: var(--text-mute);
  font-variant-numeric: normal;
}

#pause-btn:disabled {
  cursor: default;
  opacity: 0.6;
}

/* Вторичные действия задачи — 36 px: они нажимаются редко и не должны
   спорить по весу с «Отправить». */
#abandon-btn,
#pause-btn {
  background: var(--border);
  color: var(--text);
  border: none;
  min-height: 36px;
  padding: 0 var(--s4);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sec);
}

#abandon-btn:disabled {
  cursor: default;
  opacity: 0.6;
}

/* Наряд №139, работа 5 — БЛОК СОСТОЯНИЯ: «занятие сейчас недоступно».
   Живёт в узле условия задачи (app.js::showUnavailable — единственное место
   экрана, надёжно доступное на всех путях, наряд №67, работа 2), но условием
   не притворяется: свой знак, своя иерархия, своя кнопка. До этой правки это
   был абзац текста и кнопка браузерного вида посреди места, где обычно
   стоит задача. */
.sostoyanie {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s6) 0;
}

.sostoyanie-znak {
  font-size: 28px;
  line-height: 1;
  color: var(--text-dim);
}

.sostoyanie-glavnoe {
  margin: 0;
  font-size: var(--fs-h);
  font-weight: 600;
  line-height: 1.3;
}

.sostoyanie-poyasnenie {
  margin: 0;
  color: var(--text-dim);
  line-height: var(--lh-body);
}

.sostoyanie button {
  background: var(--accent);
  color: var(--accent-text);
  border: none;
  min-height: var(--h-btn);
  padding: 0 var(--s6);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sec);
}

/* Наряд №139, работа 5 — ПУСТАЯ ЛЕНТА. Первые секунды разбора и весь экран
   практики до первого хода штурмана лента была просто чёрной областью между
   кнопками и полем ввода: ни одного признака, что здесь вообще что-то бывает.
   :empty истинно ровно тогда, когда в ленте нет ни одного узла, — то есть и
   индикатор ожидания («Читаю твой вопрос…»), и первая же реплика гасят
   подсказку сами, без единой строки кода. */
.chat-messages:empty::before {
  content: "Здесь будет разговор со штурманом. Спрашивай, если что-то непонятно.";
  display: block;
  margin-top: var(--s2);
  padding: 0 var(--s2);
  color: var(--text-mute);
  font-size: var(--fs-sec);
  line-height: var(--lh-body);
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.modal {
  background: var(--surface-2);
  border-radius: 8px;
  padding: 24px;
  max-width: 320px;
  text-align: center;
}

.modal-buttons {
  display: flex;
  gap: 12px;
  justify-content: center;
  margin-top: 16px;
}

.modal-buttons button {
  background: var(--accent);
  color: var(--accent-text);
  border: none;
  padding: 10px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

/* Наряд №195, работа 5 — геометрия экрана итогов темы (капитель и токены
   --font-h/--fw-h — только в brand-bcsp.css: их не было бы ни на
   trainer.html, ни на admin.html, а тех элементов там и нет вовсе; цвет —
   в brand-bcsp.css, тем же приёмом, что работа 4).
   Хвост №195.1, работа 9 — раскладка и размеры по блоку 3 черновика-эталона
   (bcsp-kb/outputs/2026-09-21_Zametka_Chernovik-tema-itog-otklik.html,
   стили — строки 193–224): карточка 900 px, заголовок 32 px, линейка
   кружков 22 px на соединительной линии с подписями под кружками, три
   плитки фактов в ряд (число 26 px), низ — кнопка 48 px и примечание
   справа. Узкое окно 520 px со столбиком фактов снято. */
.itogi-temy-modal {
  box-sizing: border-box;
  width: min(900px, calc(100vw - 48px));
  max-height: calc(100vh - 48px);
  overflow-y: auto;
  padding: 32px 40px 28px;
  border-radius: 12px;
  text-align: left;
}

.itogi-temy-nadzagolovok {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.itogi-temy-zagolovok {
  margin: 6px 0 0;
  font-size: 32px;
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.itogi-temy-zagolovok small {
  display: block;
  margin-top: 6px;
  font-size: 18px;
  letter-spacing: 0.01em;
}

.itogi-temy-put {
  display: flex;
  align-items: flex-start;
  margin: 22px 0 6px;
}

.itogi-temy-put-item {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  position: relative;
  font-size: 12px;
  line-height: 1.25;
  text-align: center;
}

/* Соединительная линия — от предыдущего кружка к этому. */
.itogi-temy-put-item::before {
  content: "";
  position: absolute;
  top: 10px;
  left: -50%;
  width: 100%;
  height: 2px;
  z-index: 0;
}
.itogi-temy-put-item:first-child::before { display: none; }

.itogi-temy-put-tochka {
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid;
  z-index: 1;
  display: grid;
  place-items: center;
}

/* Галочка у подтверждённой темы. */
.itogi-temy-put-item.osvoena .itogi-temy-put-tochka::after {
  content: "";
  width: 8px;
  height: 5px;
  border-left: 2px solid;
  border-bottom: 2px solid;
  transform: translateY(-1px) rotate(-45deg);
}

/* Закрытая сейчас тема — жирная подпись; кольцо и вспышка появления — в
   brand-bcsp.css (движение и --ease живут там). */
.itogi-temy-put-item.sejchas .itogi-temy-put-podpis { font-weight: 600; }

/* Метка «+K» — темы раздела за краем окна линейки (больше девяти тем). */
.itogi-temy-put-skryto {
  box-sizing: border-box;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 11px;
  z-index: 1;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 600;
}

.itogi-temy-fakty {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-top: 18px;
}

.itogi-temy-fakt {
  padding: 12px 14px;
  border-radius: 6px;
  border: 1px solid;
}

.itogi-temy-fakt-chislo {
  display: block;
  font-size: 26px;
  line-height: 1.1;
}

.itogi-temy-fakt-tekst {
  display: block;
  margin-top: 4px;
  font-size: 14px;
}

.itogi-temy-fraza {
  margin: 18px 0 0;
  font-size: 18px;
}

.itogi-temy-ne-hvatilo {
  margin: 12px 0 0;
  font-size: var(--fs-sec);
}

.itogi-temy-niz {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: 22px;
}

#itogi-temy-vpered-btn {
  min-height: 48px;
  padding: 0 26px;
  border: none;
  border-radius: 6px;
  font-size: 17px;
  font-weight: 600;
  cursor: pointer;
}

.itogi-temy-trener {
  margin-left: auto;
  text-align: right;
  font-size: 13px;
}

/* Узкий экран: плитки — столбиком, без горизонтальной прокрутки. */
@media (max-width: 700px) {
  .itogi-temy-modal { padding: 24px 20px; }
  .itogi-temy-fakty { grid-template-columns: 1fr; }
  .itogi-temy-niz { flex-wrap: wrap; }
}

.tema-perevod-nazvanie {
  font-size: 18px;
}

/* Наряд №73, работа 1а: тот же красный, что у .verdict.wrong_answer —
   отказ модалки говорит тем же цветом, что и отказ судьи. */
.modal-error {
  color: var(--fail);
  margin-top: 12px;
  font-size: 0.9em;
}

/* Наряд №199, работа 3 — панель проверок: несколько строк речи о выводе,
   по одной в абзаце, без просторных полей абзаца по умолчанию. */
.output-panel-body p.output-pusto { margin: 0 0 0.35em; }
