/* ==========================================================================
   Дизайн-система платформы автопроверки.

   Один файл, без сборщика и без внешних шрифтов — только системные стеки.
   Сдержанно: это инструмент, которым читают условие и правят код, а не
   витрина. Единственная вольность — мелкие детали в духе CI-чек-листа
   (значки статусов с точкой, моноширинные цифры для баллов и id), потому
   что платформа и есть автоматический чек-лист поверх студенческого кода.
   ========================================================================== */

:root {
  /* --- Светлая палитра (опорные значения, см. task-8-brief.md) --- */
  --bg: #ffffff;
  --surface: #f7f7f8;
  --surface-2: #eeeef1;
  --border: #e2e2e5;
  --text: #17171a;
  --muted: #6b6b73;

  --accent: #2f5bd0;
  --accent-weak: #e8edfb;
  --pass: #15803d;
  --pass-bg: #ecfdf3;
  --fail: #b91c1c;
  --fail-bg: #fef2f2;
  --warn: #b45309;
  --warn-bg: #fffbeb;

  --radius: 10px;
  --radius-sm: 6px;
  --gap: 1rem;
  --gap-sm: .5rem;
  --gap-lg: 2rem;
  /* --maxw — ширина страницы, шапки (.masthead-inner/.topbar-inner) и полосы
     разделов: одна переменная на все три и одна на ВСЕ страницы платформы.
     Раньше страницы делились на обычные (--maxw: 120rem) и «широкие»
     (--maxw-wide: 160rem, список — widePages в ui.go) — идея была в том, что
     таблицам и коду нужно больше места, чем прозе. На практике владелец ходит
     между разделами, и разная ширина соседних страниц дёргает взгляд сильнее,
     чем помогает любая из них по отдельности — жалоба при приёмке беты.
     Значение — то, что раньше было --maxw-wide: жалоб на «слишком широко» не
     было, была ровно обратная («страница выглядит колонкой посреди пустоты»
     на 2560px мониторе). --measure ниже по-прежнему держит колонку прозы
     уже: снятие --maxw страницы не трогает ширину читаемого текста. */
  --maxw: 160rem;
  /* --measure — ширина именно ТЕКСТА условия тренажёра (README, internal/mdlite)
     и пустых состояний: --maxw специально не ограничивает её тоже, иначе
     подъём --maxw вытянул бы и строку прозы, а длинная строка читается хуже
     (жалоба владельца ровно об этом — «markdown в узком окне», где на самом
     деле мешала не ширина страницы). Значение — точный расчёт нынешней
     ширины .readme на странице тренажёра при старом --maxw: 68rem, минус
     отступы .page (2×var(--gap)), минус разрыв .two-col (var(--gap-lg)) пополам
     между колонками, минус padding .card (2×var(--gap-lg)) — то есть строка
     прозы не становится ДЛИННЕЕ нынешней, а остаётся ровно такой же. */
  --measure: 28rem;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "Cascadia Code", Consolas, monospace;

  --shadow-sm: 0 1px 2px rgba(23, 23, 26, .06);

  /* Подсветка синтаксиса редактора (editor-init.js, vendor/editor.js).
     Комментарий и операторы намеренно не заводят своих переменных —
     переиспользуют --muted/--text, чтобы не дублировать смену темы. */
  --cm-keyword: #7a3e9d;
  --cm-string: #1a7f37;
  --cm-number: #0550ae;
  --cm-type: #0550ae;
  --cm-function: #8a4b00;

  color-scheme: light;
}

/* Тёмная палитра по системной настройке — только если тему не переключили
   вручную (иначе кнопка-переключатель работала бы лишь в одну сторону). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #121316;
    --surface: #1a1b1f;
    --surface-2: #202127;
    --border: #2c2d33;
    --text: #e9e9ec;
    --muted: #9a9aa2;

    --accent-weak: #1c2440;
    --pass-bg: #0f2b1d;
    --fail-bg: #3a1315;
    --warn-bg: #3a2a0d;

    --cm-keyword: #c792ea;
    --cm-string: #7ec699;
    --cm-number: #82aaff;
    --cm-type: #82aaff;
    --cm-function: #ffcb6b;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
    color-scheme: dark;
  }
}

/* Ручной выбор темы всегда побеждает системный — и в ту, и в другую сторону. */
:root[data-theme="dark"] {
  --bg: #121316;
  --surface: #1a1b1f;
  --surface-2: #202127;
  --border: #2c2d33;
  --text: #e9e9ec;
  --muted: #9a9aa2;

  --accent-weak: #1c2440;
  --pass-bg: #0f2b1d;
  --fail-bg: #3a1315;
  --warn-bg: #3a2a0d;

  --cm-keyword: #c792ea;
  --cm-string: #7ec699;
  --cm-number: #82aaff;
  --cm-type: #82aaff;
  --cm-function: #ffcb6b;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
  color-scheme: dark;
}
:root[data-theme="light"] { color-scheme: light; }

/* --------------------------------- Сброс -------------------------------- */

* { box-sizing: border-box; }

html { color-scheme: light dark; }

body {
  margin: 0;
  /* Колонка во всю высоту окна: подвал прижимается к низу и на короткой
     странице (правила, вход, пустое состояние), а не висит сразу под текстом
     посреди экрана. Ссылки на справку и правила должны находиться там, где их
     ищут, — внизу окна, а не в случайном месте. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font: 400 1rem/1.65 var(--font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Видимый фокус обязателен — часть аудитории пользуется только клавиатурой. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* ------------------------------ Типографика ------------------------------ */

h1, h2, h3, h4 { font-family: var(--font); font-weight: 700; line-height: 1.25; margin: 0 0 .5em; letter-spacing: -.01em; }
h1 { font-size: 1.75rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.0625rem; }

p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code, kbd { font: 400 .875em/1 var(--mono); background: var(--surface-2); border-radius: 4px; padding: .15em .4em; }

/* Блок кода из README (internal/mdlite): моноширинный, с горизонтальной
   прокруткой вместо переноса — код важнее, чем ширина колонки. */
pre {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--gap-sm) var(--gap);
  overflow-x: auto;
  margin: 0 0 1em;
}
pre:last-child { margin-bottom: 0; }
pre code { background: none; padding: 0; }

ul { margin: 0 0 1em; padding-left: 1.25em; }
ul:last-child { margin-bottom: 0; }
li { margin-bottom: .25em; }

.muted { color: var(--muted); }

/* -------------------------------- Разметка -------------------------------- */

.page {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--gap-lg) var(--gap) calc(var(--gap-lg) * 2);
  /* Растёт на всю свободную высоту, чтобы подвал ниже оказался у нижнего края
     окна, а не сразу за коротким текстом. width — потому что flex-элемент
     иначе сжимается по содержимому и max-width перестаёт центрировать. */
  flex: 1 0 auto;
  width: 100%;
  box-sizing: border-box;
}

/* Подвал со справкой и правилами (layout.html). Приглушён намеренно: это
   ссылки, которые нужны один раз в начале, и спорить за внимание с рабочей
   частью страницы им незачем. */
.footer {
  border-top: 1px solid var(--border);
  margin-top: var(--gap-lg);
  flex: 0 0 auto;
}
.footer-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--gap) var(--gap);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--gap);
  font-size: .875rem;
  color: var(--muted);
}
/* Копирайт и версия сборки уходят вправо: слева то, куда ходят (справка,
   правила), справа то, что читают глазами и называют в поддержке. */
.footer-meta { margin-left: auto; display: flex; gap: var(--gap); align-items: baseline; }
.footer-version { font: 400 .75rem/1 var(--mono); }
.footer a { color: var(--muted); }
.footer a:hover { color: var(--text); }

/* Тонкая всегда-видимая полоса сверху: марка платформы + переключатель темы.
   Не завязана на аккаунт — держит ориентацию даже на странице входа. */
.masthead {
  border-bottom: 1px solid var(--border);
}
.masthead-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: .75rem var(--gap);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
}

.brand {
  font: 700 1.0625rem/1 var(--mono);
  letter-spacing: -.02em;
  color: var(--text);
}
.brand:hover { text-decoration: none; }
.brand-dot { color: var(--accent); }

/* Шапка аккаунта: рисуется только вошедшему — навигация слева, адрес,
   квота и выход справа. Анонимный посетитель её не видит вовсе. */
.topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.topbar-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: .625rem var(--gap);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-sm) var(--gap);
}

.nav { display: flex; align-items: center; gap: var(--gap); flex-wrap: wrap; }
.nav a { color: var(--text); font-weight: 500; }
.nav a:hover { color: var(--accent); text-decoration: none; }
.nav a[aria-current="page"] {
  color: var(--accent);
  background: var(--accent-weak);
  border-radius: var(--radius-sm);
  padding: .25rem .5rem;
  margin: -.25rem -.5rem;
}

.topbar-account {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  flex-wrap: wrap;
  font-size: .875rem;
}
.account-email {
  color: var(--muted);
  text-decoration: none;
}
/* Адрес в шапке ведёт в профиль, и это должно быть видно до наведения: без
   подчёркивания он читается как подпись, а не как ссылка. Пунктир мягче
   сплошной линии — шапка не должна спорить с содержимым страницы. */
.account-email { border-bottom: 1px dotted var(--border); }
.account-email:hover { color: var(--fg); border-bottom-color: var(--muted); }
.account-email[aria-current="page"] { color: var(--fg); border-bottom-style: solid; }

.quota {
  font: 600 .8125rem/1 var(--mono);
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: .3em .65em;
}
.quota b { color: var(--text); font-weight: 700; }

.inline-form { margin: 0; display: inline-flex; }

/* Поле «скопировать адрес» — ссылки записи в поток (manage_cohort_links.html).
   Именно поле, а не ссылка: адрес копируют в статью, а переход по нему записал
   бы в поток самого методиста. Моноширинный шрифт — по адресу сверяют код
   глазами. */
.copy-field {
  width: 100%;
  font: .8125rem/1.4 var(--mono);
  background: var(--surface-2);
}

/* Уведомление о выпущенной ссылке. Обычный .notice — inline-flex, то есть
   ширина по содержимому и всё в одну строку: адрес внутри него сжимался до
   обрезка в треть длины, а скопировать нужно весь. Здесь блок занимает строку,
   а текст и поле идут столбиком. */
.notice--copy {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: .5rem;
  width: 100%;
}
.notice--copy p { margin: 0; }

/* Плавающий переключатель темы — единственный элемент, который не должен
   пропадать вместе с шапкой аккаунта у анонимного посетителя, поэтому живёт
   в masthead. Без JS кнопка остаётся скрытой (см. app.js): ничего не
   ломается, страница просто следует системной теме. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0;
}
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }

/* --------------------------------- Формы ---------------------------------- */

.field { display: flex; flex-direction: column; gap: .35rem; margin: 0 0 var(--gap); }
.field:last-child { margin-bottom: 0; }
.field > span { font-size: .8125rem; font-weight: 600; color: var(--muted); }

input[type="text"],
input[type="email"],
input[type="file"],
select {
  font: 400 1rem/1.4 var(--font);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .55em .7em;
}
input[type="text"]:focus,
input[type="email"]:focus,
select:focus {
  border-color: var(--accent);
}

/* select появился с формой создания потока (manage_course.html) — первый
   выпадающий список в разделе управления, отдельного компонента под него
   раньше не было. */

.stack { display: flex; flex-direction: column; }
.stack-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gap-sm); }

.divider {
  display: flex;
  align-items: center;
  gap: .75em;
  margin: var(--gap) 0;
  color: var(--muted);
  font-size: .8125rem;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }

/* --------------------------------- Кнопки --------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4em;
  font: 600 .9375rem/1 var(--font);
  padding: .6em 1em;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  background: var(--surface-2);
  color: var(--text);
}
.btn:hover { text-decoration: none; }
.btn-sm { font-size: .8125rem; padding: .4em .75em; }

.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { filter: brightness(1.08); }

.btn-secondary { background: var(--bg); border-color: var(--border); color: var(--text); }
.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }

.btn-ghost { background: transparent; border-color: var(--border); color: var(--muted); }
.btn-ghost:hover { color: var(--text); border-color: var(--text); }

/* Необратимое действие (отвязка провайдера у чужого аккаунта,
   admin_user_card.html / admin_unlink_confirm.html) — не должно выглядеть
   как обычная btn-secondary, иначе промах соседней кнопкой в таблице
   отвязывает не того. */
.btn-danger { background: var(--fail-bg); border-color: var(--fail); color: var(--fail); }
.btn-danger:hover { background: var(--fail); border-color: var(--fail); color: #fff; }

/* -------------------------------- Карточки -------------------------------- */

/* .page-header — шапка страницы раздела управления: заголовок слева, кнопки
   заведения (новая редакция/поток/методист/курс и т.п.) справа сверху, а не
   под таблицей и не в аккордеоне (задача «Кнопка сверху и отдельный экран» —
   фикс-волна после просмотра беты, взамен снятых <details class="disclosure">
   — форма теперь отдельный экран со своим адресом, см. manage_course.html и
   соседние шаблоны). На узком экране кнопки переносятся под заголовок —
   .stack-row и так это умеет (flex-wrap), второго правила не нужно. */
.page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap);
  margin: 0 0 var(--gap);
}
.page-header h1 { margin-bottom: 0; }

.card {
  display: block;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--gap-lg);
  margin: 0 0 var(--gap);
}
.card:last-child { margin-bottom: 0; }
.card--auth { max-width: 26rem; }

/* Карточка-ссылка (список тренажёров, trainers.html): та же .card, но кликабельна целиком. */
a.card:hover { border-color: var(--accent); box-shadow: 0 2px 10px rgba(23, 23, 26, .08); text-decoration: none; }

/* Закрытый элемент карты курса (trainers.html): это div.card, а не a.card —
   карточка намеренно некликабельна. Без отдельного правила она выглядела
   ровно как соседняя ссылка, и студент не понимал, почему одни карточки
   открываются, а другие «не работают». Приглушаем фон и границу и снимаем
   тень: карточка читается как выключенная ещё до того, как в неё ткнут. */
.card-closed {
  background: var(--surface-2);
  border-style: dashed;
  box-shadow: none;
  opacity: .72;
}

/* Сетка карточек тренажёров — сама подбирает число колонок по ширине экрана. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 22rem));
  gap: var(--gap);
}

/* Consecutive page sections need a consistent visual boundary. */
main.page > section + section,
.manage-content > section + section { margin-top: var(--gap-lg); }
/* Карточки в ряду разной высоты — жалоба владельца при приёмке (короткое
   название в одну строку рядом с двухстрочным). Сама карточка и раньше
   растягивалась на высоту ряда (grid по умолчанию стретчит элементы), но
   .score-meter и .card-foot стояли в обычном потоке и оставались на месте,
   которое им задавало содержимое ВЫШЕ (заголовок) — у карточки с коротким
   заголовком кружок балла и подвал повисали заметно выше, чем у соседней с
   двухстрочным. flex-колонка + margin-top:auto у .score-meter прибивает
   кружок и подвал к низу уже растянутой карточки — теперь они на одной
   линии независимо от числа строк в заголовке.
   margin:0 — не просто сброс: .card:last-child{margin-bottom:0} (ниже по
   файлу) снимает нижний отступ только у последней карточки ВСЕЙ сетки, а не
   у последней в каждом ряду. Внутри строки это давало последней карточке
   ряда на var(--gap) больше высоты после стретча, чем у соседей, — та же
   «рваная» строка, только из-за отступа, а не заголовка. Внутри .grid
   расстояние между карточками и так задаёт gap самого грида, свой margin
   карточкам не нужен и создавал бы этот перекос. */
.grid > .card,
.grid > .card-closed {
  display: flex;
  flex-direction: column;
  margin: 0;
}
.grid > .card .score-meter { margin-top: auto; }

/* Цвет дуги по исходу последней попытки. Синий — прошла, красный — нет.
   Значение по умолчанию задано на самом .score-meter, чтобы карточка без
   модификатора (проверка ещё идёт, попыток не было) осталась прежней. */
.score-meter { --meter: var(--accent); }
.score-meter-fail { --meter: var(--fail); }
.grid > .card-closed p:last-child { margin-top: auto; }

.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--gap-sm); margin-bottom: var(--gap); }
.card-title { font-weight: 700; font-size: 1.0625rem; color: var(--text); }
.card-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--gap-sm); margin-top: var(--gap); flex-wrap: wrap; font-size: .8125rem; }

/* Круговой индикатор лучшего балла — --v задаётся инлайн (0..100), доля
   закрашенной дуги. conic-gradient + маска-подложка вместо SVG: тот же
   эффект без лишнего элемента разметки. */
.score-meter {
  --v: 0;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  /* Цвет дуги — переменная, а не жёсткий --accent: он подменяется на
     --fail, когда последняя попытка не прошла (модификатор ниже). Балл 75 из
     100 сам по себе не говорит, засчитано это или нет: порог у каждого
     тренажёра свой, и одинаково синий кружок у сданного и несданного заставлял
     читать значок статуса в подвале, чтобы понять исход. */
  background: conic-gradient(var(--meter) calc(var(--v) * 1%), var(--surface-2) 0);
  margin: 0 auto var(--gap-sm);
}
.score-meter span {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  background: var(--surface);
  display: grid;
  place-items: center;
  font: 700 .9375rem/1 var(--mono);
  color: var(--text);
}

/* ----------------------- Переключатель вида списка ------------------------ */

/* .view-switch — «Карточки / Список» на trainers.html. Обычные ссылки
   (?view=cards/?view=list), не форма и не JS — pageTrainers (student.go)
   читает query, обновляет куку trainers_view и рисует нужный вид тем же
   запросом. aria-current="true" ставит сам шаблон по уже разрешённому
   .View — читается и глазами (заливка активного пункта), и скринридером. */
.view-switch {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.view-switch a {
  padding: .4em .9em;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--bg);
}
.view-switch a:hover { color: var(--accent); text-decoration: none; }
.view-switch a + a { border-left: 1px solid var(--border); }
.view-switch a[aria-current="true"] { background: var(--accent-weak); color: var(--accent); }

/* .trainer-list/.trainer-row — компактный вид того же списка: строка на
   тренажёр вместо карточки, те же сведения (название, версия, балл,
   попытки, статус), без кругового индикатора — на десятке тренажёров он
   как раз и перегружает экран, ради чего вид вообще завели. */
.trainer-list {
  list-style: none;
  margin: 0 0 var(--gap);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.trainer-list li + li { border-top: 1px solid var(--border); }
.trainer-row {
  display: flex;
  align-items: center;
  gap: var(--gap);
  padding: .55em var(--gap);
  background: var(--surface);
  color: var(--text);
}
a.trainer-row:hover { background: var(--surface-2); text-decoration: none; }
/* Закрытый элемент — та же приглушённая подача, что у .card-closed
   (нельзя перепутать со строкой, ведущей на страницу решения). */
.trainer-row--closed { opacity: .72; }
.trainer-row-title {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.trainer-row-version { flex: 0 0 auto; font-family: var(--mono); font-size: .875rem; }
.trainer-row-score { flex: 0 0 2.5ch; text-align: right; font: 700 1rem/1 var(--mono); color: var(--text); }
.trainer-row-attempts { flex: 0 0 auto; font-size: .8125rem; white-space: nowrap; }
.trainer-row-reason { flex: 0 0 auto; font-size: .8125rem; }
@media (max-width: 640px) {
  .trainer-row { flex-wrap: wrap; row-gap: .3rem; }
  .trainer-row-title { flex-basis: 100%; white-space: normal; }
}

/* Две колонки на странице тренажёра: условие слева, редактор справа. */
.two-col { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--gap-lg); align-items: start; }
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }

/* .readme — карточка с условием тренажёра (README, internal/mdlite.ToHTML).
   Ограничиваем шириной --measure только сам текст (абзацы, списки,
   заголовки) — саму карточку не трогаем, у неё есть все основания расти
   вместе с --maxw. Блок кода (<pre><code>, единственная разметка mdlite
   шире текста) НАРОЧНО не в списке селектора — это и есть вторая половина
   жалобы владельца: markdown жался в узкое окно не из-за ширины страницы, а
   из-за того, что код и текст делили одну колонку. Теперь код пользуется
   всей шириной карточки, а строка текста не растёт. mdlite не вкладывает
   теги друг в друга (см. её комментарий) — прямых потомков достаточно. */
.readme > :where(p, ul, h1, h2, h3) { max-width: var(--measure); }

/* Страницы-документы (/help, /rules): вся страница — узкая колонка по центру,
   текст занимает её целиком.

   Без этого выходит худшее из двух: карточка растянута на всю ширину экрана, а
   абзацы внутри обрезаны по --measure и жмутся к левому краю — на широком
   мониторе текст выглядит прижатым к углу пустого листа.

   Ограничение и центрирование стоят на ОБЁРТКЕ, а не на карточке: заголовок
   страницы лежит выше карточки, и центрируй мы только её — заголовок остался
   бы у левого края, а текст съехал вправо. */
.doc { max-width: 46rem; margin: 0 auto; }
.doc .readme > :where(p, ul, ol, h1, h2, h3, h4) { max-width: none; }

/* Оглавление документа. На узком экране скрыто: там прокрутка дешевле, чем
   лишний блок между заголовком и текстом. */
.doc-toc { display: none; }

/* Широкий экран: слева оглавление, справа текст. Раньше на 2000 пикселях
   пустовали две трети ширины — колонка текста по центру, а по бокам ничего.
   Расширять саму колонку было нельзя (строка в 200 знаков не читается),
   поэтому пространство занято тем, что помогает читать: списком разделов,
   который остаётся на месте при прокрутке.

   1200px — порог, ниже которого две колонки уже теснят текст. Ровно та
   ширина, на которой оглавление в 12rem помещается, не отнимая у текста
   его 46rem. */
@media (min-width: 1200px) {
  .doc {
    max-width: 62rem;
    display: grid;
    grid-template-columns: 12rem minmax(0, 46rem);
    gap: var(--gap-lg);
    align-items: start;
  }
  .doc-toc {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    position: sticky;
    top: var(--gap);
    padding-top: 3.2rem;   /* выравнивание по первому заголовку справа */
    font-size: .875rem;
  }
  .doc-toc a { color: var(--muted); }
  .doc-toc a:hover { color: var(--text); }
}

/* .empty — то же для пустых состояний: подсказка «что делать дальше» — тоже
   проза, а не таблица, и не обязана растягиваться на всю ширину страницы. */
.empty p { max-width: var(--measure); margin-left: auto; margin-right: auto; }

/* Вкладки «Текст / Архив (zip)» на странице тренажёра — переключатель на
   input[type=radio]+label, без JS: страница отдаёт строгий CSP без
   unsafe-inline (см. securityHeaders в static.go), инлайн-обработчики всё
   равно не выполнились бы. */
.tabs input[type="radio"] { position: absolute; opacity: 0; }
.tabs label {
  display: inline-block;
  padding: .4em .9em;
  margin: 0 .25em -1px 0;
  border: 1px solid var(--border);
  border-bottom-color: transparent;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  cursor: pointer;
  color: var(--muted);
  font-size: .875rem;
  font-weight: 600;
}
.tabs input:checked + label { color: var(--text); background: var(--surface); border-bottom-color: var(--surface); }
.tabs input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }
.tab-panel {
  display: none;
  border: 1px solid var(--border);
  border-radius: 0 var(--radius-sm) var(--radius-sm) var(--radius-sm);
  padding: var(--gap);
}
#tab-text:checked ~ .tab-panel[data-tab="text"],
#tab-zip:checked ~ .tab-panel[data-tab="zip"] { display: block; }

/* ------------------- Полноэкранный режим редактора ------------------------
   Для тех, кто решает в IDE, а сюда приходит вставить код и отправить: окно
   рядом с редактором, условие под рукой, но не занимает половину экрана.

   Всё на двух чекбоксах и соседних селекторах — ни строчки JS, как и вкладки
   выше: CSP страницы запрещает inline-скрипты. Работает и с выключенным
   JavaScript целиком.

   Состояний три:
     обычное      — две колонки, кнопка «Развернуть»;
     развёрнутое  — редактор во весь экран, условия нет, кнопка «Условие»;
     подглядывание — поверх редактора панель с условием.                     */

.focus-input { position: absolute; opacity: 0; pointer-events: none; }

/* Кнопки режима — справа от ярлыков вкладок.
   Абсолютным позиционированием, а не flex у .tabs: панели вкладок лежат
   ВНУТРИ .tabs (см. селектор #tab-text:checked ~ .tab-panel выше), и flex
   выстроил бы их в строку рядом с ярлыками. */
.tabs { position: relative; }
.tabs-actions { position: absolute; right: 0; top: 0; display: inline-flex; gap: .4rem; }

/* Подписи задаются здесь, а не в шаблоне: они зависят от состояния чекбокса,
   о котором шаблон не знает. */
/* Без значков вроде ⛶: глифа нет в системных шрифтах Windows, и на месте
   кнопки появляется пустой квадрат — проверено предпросмотром. */
.focus-btn::after { content: "Развернуть"; }
#focus-mode:checked ~ .two-col .focus-btn::after { content: "Свернуть"; }
.peek-open::after { content: "Условие"; }

/* Кнопка «Условие» нужна только в развёрнутом режиме: в обычном условие и так
   на экране слева, и вторая его копия поверх редактора только мешала бы.
   Селекторы с родителем (.tabs-actions/.two-col), а не голые классы: правило
   .btn идёт ниже по файлу и при равной специфичности перебило бы display. */
.tabs-actions .peek-open,
.two-col .peek-close,
.peek-backdrop { display: none; }
#focus-mode:checked ~ .two-col .tabs-actions .peek-open { display: inline-block; }

/* Развёрнутый режим: карточка редактора занимает окно целиком. Не
   requestFullscreen — тот требует JS и жеста пользователя, а прячет ещё и
   адресную строку, которая тут не мешает. */
#focus-mode:checked ~ .two-col {
  position: fixed;
  inset: 0;
  z-index: 50;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  padding: var(--gap);
  overflow: auto;
  background: var(--bg);
}
#focus-mode:checked ~ .two-col .readme { display: none; }

/* Ради этого режим и делался: поле кода занимает освободившуюся высоту.
   Вычитается меньше, чем в обычном режиме (26rem под шапку страницы, вкладки
   и кнопки) — шапки здесь нет, остаются только вкладки и ряд кнопок. */
#focus-mode:checked ~ .two-col .editor-pane { max-height: calc(100vh - 12rem); }
#focus-mode:checked ~ .two-col .editor,
#focus-mode:checked ~ .two-col .cm-editor { min-height: calc(100vh - 16rem); }

/* Подглядывание: условие панелью поверх редактора, слева — там же, где оно
   было в обычном режиме, чтобы взгляд не искал его заново. */
#focus-mode:checked ~ #peek-readme:checked ~ .two-col .readme {
  display: block;
  position: fixed;
  inset: 0 auto 0 0;
  width: min(46rem, 92vw);
  z-index: 60;
  margin: 0;
  border-radius: 0;
  overflow: auto;
  box-shadow: 0 0 2rem rgb(0 0 0 / .35);
  /* Снимаем max-height из медиазапроса ниже (calc(100vh - 12rem) под шапку и
     кнопки): здесь панель идёт от края до края, шапки над ней нет, и остаток
     в 12rem оказывался пустым провалом внизу. */
  max-height: none;
}
#focus-mode:checked ~ #peek-readme:checked ~ .two-col .readme .peek-close {
  display: inline-block;
  position: sticky;
  top: 0;
  float: right;
  z-index: 1;
}
/* Подложка гасит подглядывание кликом мимо панели — привычное поведение
   всплывающих окон, и единственный способ закрыть её мышью, не целясь в
   кнопку. */
#focus-mode:checked ~ #peek-readme:checked ~ .peek-backdrop {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgb(0 0 0 / .35);
  cursor: pointer;
}

/* На узком экране разворачивать нечего — колонки и так одна под другой, а
   панель условия во весь экран отняла бы у редактора всё место. */
@media (max-width: 900px) {
  .tabs-actions { display: none; }
  #focus-mode:checked ~ .two-col { position: static; padding: 0; overflow: visible; }
  #focus-mode:checked ~ .two-col .readme { display: block; }
}

/* Результат проверки — левый кант окрашен в цвет статуса, как строка лога CI. */
.result-card { border-left: 3px solid var(--border); }
.result-card[data-status="passed"] { border-left-color: var(--pass); }
.result-card[data-status="failed"] { border-left-color: var(--fail); }
.result-id { font: 400 .875rem/1 var(--font); color: var(--muted); margin-bottom: .75rem; }
.result-id code { font-size: 1em; }

/* --------------------------------- Значки --------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: .45em;
  font: 600 .75rem/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: .04em;
  padding: .4em .7em;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--muted);
}
.badge::before {
  content: "";
  width: .5em;
  height: .5em;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.badge-pass { color: var(--pass); background: var(--pass-bg); border-color: var(--pass); }
.badge-fail { color: var(--fail); background: var(--fail-bg); border-color: var(--fail); }
.badge-run  { color: var(--warn); background: var(--warn-bg); border-color: var(--warn); }
.badge-run::before { animation: badge-pulse 1.4s ease-in-out infinite; }
/* Отметка о находках в присланном коде (manage_submissions.html). Цвет
   предупреждения, а не отказа: это повод посмотреть, а не вердикт. Точки
   слева нет — она у остальных значков означает состояние работы, а здесь
   значок про другое. */
.badge-warn { color: var(--warn); background: var(--warn-bg); border-color: var(--warn); text-decoration: none; }
.badge-warn::before { display: none; }
/* Погашенная отметка: работу посмотрели. Не исчезает вовсе, а приглушается —
   иначе «здесь ничего не нашли» и «здесь уже разобрались» выглядели бы
   одинаково, и вернуться к разобранному было бы не по чему. */
.badge-muted { color: var(--muted); background: transparent; border-color: var(--border); text-decoration: none; }
.badge-muted::before { display: none; }
/* Кнопка гашения на карточке работы: отделена от таблицы находок, но не
   вынесена в отдельный блок — она часть того же разбора. */
.finding-review { margin: 0 0 var(--gap); }
.finding-review p { margin: 0 0 .4rem; }
@keyframes badge-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Счётчик прогонов рядом с бейджем теста («3 из 10 прогонов», result.html):
   тесты гоняются с -count=10, и доля важнее самого вердикта — «3 из 10» это
   плавающая гонка, «0 из 10» стабильно неверный код. Приглушён намеренно:
   вердикт остаётся главным в строке. */
.runs { margin-left: .6em; font-size: .8125rem; white-space: nowrap; }

/* --------------------------------- Баллы ---------------------------------- */

.score { font-size: 1rem; }
.score b { font: 700 1.5rem/1 var(--mono); color: var(--text); margin-left: .35em; }

/* --------------------------------- Таблицы --------------------------------- */

.table { width: 100%; border-collapse: collapse; font-size: .9375rem; }
.table th, .table td { text-align: left; padding: .55em .7em; border-bottom: 1px solid var(--border); }
.table th { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); font-weight: 600; }
.table td.num, .table th.num { text-align: right; font-family: var(--mono); }
.table tbody tr:hover { background: var(--surface-2); }

/* Начало новой группы строк, объединённых одним тренажёром (состав редакции,
   manage_version.html) — более заметная граница сверху вместо обычной
   тонкой между версиями одного и того же тренажёра. */
.table tr.version-group-start td { border-top: 2px solid var(--border); }
/* Продолжение группы версий: подпись мельче имени тренажёра и с отступом —
   строка должна читаться как «принадлежит верхней», а не как отдельная. */
.version-cont { font-size: .85em; padding-left: .75rem; }

/* Предупреждение перед действием, которое сразу отражается на живых
   студентах (сохранение состава редакции, по которой уже учится поток) —
   теми же токенами --warn/--warn-bg, что и .badge-run, но статичная строка
   текста рядом с кнопкой, а не пульсирующий значок статуса. */
.notice {
  display: inline-flex;
  align-items: center;
  padding: .5em .85em;
  border-radius: var(--radius-sm);
  border: 1px solid var(--warn);
  background: var(--warn-bg);
  color: var(--warn);
  font-size: .8125rem;
}

/* ---------------------------- Редактор / вывод кода ------------------------ */

.editor {
  font: .875rem/1.6 var(--mono);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--gap-sm) var(--gap);
  white-space: pre-wrap;
  word-break: break-word;
  resize: vertical;
}
textarea.editor { width: 100%; min-height: 10rem; }

/* CodeMirror (editor-init.js): textarea.editor остаётся в DOM всегда (без
   JS — единственное поле, с JS — источник истины для отправки формы),
   .editor-host — обёртка вокруг её ShadowRoot с редактором. Скругление и
   рамку держит именно обёртка: стили внутри ShadowRoot (тема CodeMirror,
   см. scripts/vendor-editor.sh) снаружи не достать, инкапсуляция тени
   этого не позволяет — так и задумано. */
.editor-host {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.editor-host:focus-within { border-color: var(--accent); }

/* Прячем исходную textarea классом, а не style="" — CSP страницы не
   допускает unsafe-inline (см. cmd/api/static.go, securityHeaders). */
.editor-source-hidden { display: none; }

/* --------------------------------- Пустые списки ---------------------------- */

.empty {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: var(--gap-lg);
  text-align: center;
  color: var(--muted);
}

.link-list { list-style: none; padding: 0; margin: 0 0 var(--gap); display: flex; flex-direction: column; gap: var(--gap-sm); }
.link-list li { display: flex; align-items: center; gap: var(--gap-sm); }
.link-list:last-child { margin-bottom: 0; }

/* -------------------------------- Узкий экран ------------------------------- */

@media (max-width: 640px) {
  .page { padding: var(--gap) var(--gap) var(--gap-lg); }
  .topbar-inner { flex-direction: column; align-items: stretch; }
  .topbar-account { justify-content: space-between; }
  .card { padding: var(--gap); }
}

/* -------------------------------- Чек-листы ------------------------------- */
.version-checklist { margin: 0 0 var(--gap-lg); padding: var(--gap); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.version-checklist h2 { margin-bottom: .5rem; }
.checklist { margin: 0; padding-left: 1.25rem; }
.checklist li { color: var(--muted); }
.checklist-done { color: var(--pass) !important; }

/* ------------------------- Администрирование пользователей ---------------- */

.page-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--gap); margin-bottom: var(--gap); }
.eyebrow { margin-bottom: .25rem; color: var(--accent); font: 700 .75rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.admin-stats { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--gap-sm); margin: 0 0 var(--gap); }
/* Обзор управления (manage_overview.html, задача 3 плана «Навигация») —
   блоков там от одного до пяти плиток в зависимости от роли, фиксированная
   сетка .admin-stats (ровно 5 колонок, для страницы пользователей) сюда не
   подходит: две плитки растянулись бы на всю ширину. auto-fit сам решает,
   сколько колонок влезает, той же плиткой .admin-stat. */
.overview-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--gap-sm); margin: 0 0 var(--gap); }
.overview-section { margin: 0 0 var(--gap-lg); }
.overview-section h2 { margin: 0 0 .6rem; }
.admin-stat { padding: .8rem 1rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
.admin-stat span { display: block; color: var(--muted); font-size: .75rem; line-height: 1.2; }
.admin-stat b { display: block; margin-top: .2rem; font: 700 1.35rem/1 var(--mono); }
/* Плитка помеченных работ на обзоре: показывается только когда счётчик не
   ноль, поэтому она обязана бросаться в глаза среди соседних — цвет
   предупреждения по рамке и подписи. Число остаётся обычным: кричащая цифра
   читалась бы как авария, а это повод посмотреть. */
.admin-stat--warn { border-color: var(--warn); background: var(--warn-bg); }
.admin-stat--warn span { color: var(--warn); }
.admin-filter { display: flex; align-items: end; gap: var(--gap-sm); margin: 0 0 var(--gap); }
.admin-filter .field { flex: 1; margin: 0; }
/* Флажок в строке фильтров: подпись рядом с квадратиком, а не над ним, иначе
   он выпадает из ряда полей с их подписями сверху. */
.admin-filter .field-check { flex: 0 0 auto; display: flex; align-items: center; gap: .4rem; white-space: nowrap; }
.admin-filter .field-check span { color: var(--text); font-size: .85rem; }
.admin-filter .field-check input { width: auto; }
.admin-filter input { width: 100%; }
.admin-users { display: grid; gap: var(--gap); }
.admin-user { padding: 1.1rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-sm); }
.admin-user--online { border-left: 3px solid var(--pass); }
.admin-user-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--gap); }
.admin-user-head h2 { margin: 0; font-size: 1rem; }
.admin-user-head p { margin: .15rem 0 0; font-size: .875rem; }
.admin-user-meta { display: grid; grid-template-columns: 1.5fr repeat(3, 1fr); gap: var(--gap); margin: 1rem 0; }
.admin-user-meta div { min-width: 0; }
.admin-user-meta dt { margin-bottom: .2rem; color: var(--muted); font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.admin-user-meta dd { margin: 0; font-size: .875rem; }
.cohort-tag { display: inline-block; margin: 0 .3rem .25rem 0; padding: .2rem .45rem; border-radius: 999px; background: var(--accent-weak); color: var(--accent); font: 600 .75rem/1 var(--mono); }
.admin-user-actions { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: var(--gap); padding-top: 1rem; border-top: 1px solid var(--border); }
.role-form fieldset { display: flex; flex-wrap: wrap; gap: .4rem .85rem; margin: 0 0 .6rem; padding: 0; border: 0; }
.role-form legend { width: 100%; margin-bottom: .25rem; color: var(--muted); font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.role-form label { font-size: .875rem; white-space: nowrap; }
.quota-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gap-sm); }
.quota-form input[type="number"] { width: 8rem; font: 400 .875rem/1.4 var(--font); color: var(--text); background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .45em .6em; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
@media (max-width: 800px) { .admin-stats { grid-template-columns: repeat(2, 1fr); } .admin-user-meta { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .page-head, .admin-user-head, .admin-filter { align-items: stretch; flex-direction: column; } .admin-user-meta { grid-template-columns: 1fr; gap: .65rem; } .admin-user-actions { align-items: stretch; } .quota-form { justify-content: flex-start; } }

.admin-filter { align-items: end; flex-wrap: wrap; }
.admin-filter-search { flex: 1 1 16rem; }
.admin-filter-cohorts { flex: 1 1 16rem; }
.admin-filter-size { flex: 0 0 8rem; }
.admin-filter-cohorts select { min-height: 7.4rem; }
.admin-filter .field small { font-weight: 400; }
.admin-filter-actions { display: flex; gap: var(--gap-sm); }
.pagination { display: flex; justify-content: center; align-items: center; gap: var(--gap); margin-top: var(--gap-lg); }
.pagination-status { color: var(--muted); font-size: .875rem; }
.pagination-status b { color: var(--text); font-family: var(--mono); }
@media (max-width: 640px) { .admin-filter-search, .admin-filter-cohorts, .admin-filter-size { flex-basis: 100%; } .admin-filter-actions { justify-content: stretch; } .admin-filter-actions .btn { flex: 1; } .pagination { justify-content: space-between; gap: var(--gap-sm); } }
.admin-stat { color: var(--text); }
a.admin-stat:hover { border-color: var(--accent); text-decoration: none; }
.quick-filter { display: flex; align-items: center; flex-wrap: wrap; gap: var(--gap-sm); margin: 0 0 var(--gap); }
/* Раздел управления: слева вертикальное меню, справа содержимое
   (manage_layout.html). minmax(...,220px) вместо фиксированной ширины —
   колонка меню не растягивается на широких экранах, но и не проваливается
   ниже места, нужного «Пользователи»/«Управление» в одну строку.

   Ниже 900px — тот же порог, на котором .two-col уже схлопывает две
   колонки в одну (см. выше) — меню становится горизонтальной полосой над
   содержимым: та же разметка, только display меняет ось и убирает правую
   границу, которая на полосе была бы сверху вниз посреди текста. Список
   пунктов и порядок в DOM не меняются — работает без JavaScript. */
.manage-shell { display: grid; grid-template-columns: minmax(160px, 220px) minmax(0, 1fr); gap: var(--gap-lg); align-items: start; }
.manage-nav { display: flex; flex-direction: column; gap: .3rem; padding-right: var(--gap); border-right: 1px solid var(--border); position: sticky; top: var(--gap); }
.manage-nav a { padding: .4rem .6rem; border-radius: var(--radius-sm); color: var(--text); font-weight: 500; }
.manage-nav a:hover { background: var(--surface); text-decoration: none; }
/* Текущий раздел — тот же сигнал, что и aria-current у ассистивных
   технологий, продублированный в CSS через сам атрибут: одна метка на
   разметку и на стиль, вместо второго класса, который можно забыть
   проставить синхронно. */
.manage-nav a[aria-current="page"] { background: var(--accent-weak); color: var(--accent); font-weight: 600; }
.workflow-links { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; margin: 0 0 var(--gap-lg); padding: var(--gap-sm) var(--gap); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: .875rem; }
.workflow-links strong { color: var(--text); }
@media (max-width: 900px) {
  .manage-shell { grid-template-columns: 1fr; }
  .manage-nav { flex-direction: row; flex-wrap: wrap; gap: .5rem; padding-right: 0; padding-bottom: var(--gap-sm); border-right: 0; border-bottom: 1px solid var(--border); position: static; }
}

/* Хлебные крошки глубоких страниц раздела управления (задача 4 плана
   «Навигация», breadcrumbs.html): курс → редакция → банк вопросов → группа
   и т.п. Приглушённый цвет ссылок и разделителя — тот же приём, что у
   .muted, — крошки не должны спорить с h1 страницы за внимание; текущий
   уровень (без ссылки) — единственный элемент обычного цвета текста. */
.breadcrumbs { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; margin: 0 0 var(--gap-sm); font-size: .875rem; }
.breadcrumbs a { color: var(--muted); }
.breadcrumbs a:hover { color: var(--accent); text-decoration: none; }
.breadcrumbs [aria-current="page"] { color: var(--text); font-weight: 600; }
.breadcrumbs-sep { color: var(--border); }

/* --------------------------- Публичные потоки ----------------------------- */
.masthead-link { margin-left: auto; color: var(--muted); font-size: .875rem; font-weight: 600; }
.masthead-link:hover { color: var(--accent); text-decoration: none; }
.public-policy { margin: 0 0 var(--gap); padding: .7rem .9rem; border-left: 3px solid var(--accent); border-radius: var(--radius-sm); background: var(--accent-weak); }
.public-cohorts { display: grid; gap: var(--gap); }
.public-cohort { display: flex; align-items: center; justify-content: space-between; gap: var(--gap); }
.public-cohort h2 { margin: 0 0 .35rem; font-size: 1.1rem; }
.public-cohort p { margin: .35rem 0; }
.public-cohort-action { min-width: 13rem; text-align: right; }
/* --warn-weak никогда не существовала как переменная — background
   схлопывался в прозрачный (invalid at computed-value time), и
   .notice.notice--warning выглядела бледнее обычной .notice вместо более
   заметного предупреждения. --warn-bg — тот же токен, что использует база
   .notice; попутный фикс, замеченный при переводе отвязки провайдера на
   этот модификатор (см. admin_user_card.html, admin_unlink_confirm.html). */
.notice--warning { border-color: var(--warn); background: var(--warn-bg); }
/* Отказ формы под <details> (задача «Формы под кнопки», manage_course.html/
   manage_cohort.html) — тот же .notice, что и предупреждения выше, но
   токенами --fail/--fail-bg (те же, что у .badge-fail): раскрытый блок
   обязан явно читаться как «здесь ошибка», а не как ещё одно предупреждение
   вперемешку с переносом преподавателей. */
.notice--error { border-color: var(--fail); background: var(--fail-bg); color: var(--fail); }
@media (max-width: 640px) { .public-cohort { align-items: stretch; flex-direction: column; } .public-cohort-action { min-width: 0; text-align: left; } }
.public-settings { display: flex; align-items: end; justify-content: space-between; gap: var(--gap); margin-bottom: var(--gap); }
.public-settings h2 { margin: 0 0 .3rem; font-size: 1rem; }
.public-settings p { margin: 0; }
.public-settings .field { margin: 0; min-width: 12rem; }
.public-settings input { width: 100%; }
@media (max-width: 640px) { .public-settings { align-items: stretch; flex-direction: column; } }

/* ------------------------- Прохождение теста ------------------------------ */

/* Вопрос попытки и разбора (quiz_attempt.html, quiz_result.html). Карточка
   уже даёт рамку и отступы — здесь только внутренняя раскладка. */
.question + .question { margin-top: 1rem; }
.question-index {
  display: flex;
  align-items: center;
  gap: .6em;
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  font-weight: 600;
}
.question-body { margin: .5rem 0 1rem; }

.options { list-style: none; padding: 0; margin: 0; display: grid; gap: .5rem; }
.options li {
  display: flex;
  align-items: baseline;
  gap: .5em;
  padding: .55em .7em;
  border: 1px solid var(--border);
  border-radius: 6px;
}
/* Отмеченный студентом вариант в разборе. Намеренно нейтральный фон, а не
   зелёный или красный: верность показывается отдельным значком и только
   после исчерпания попыток — цвет ответа выдавал бы ключ раньше срока. */
.options li.option-picked { background: var(--surface-2); border-color: var(--muted); }
.options label { display: flex; align-items: baseline; gap: .5em; cursor: pointer; }
/* Абзацы внутри варианта не должны раздвигать строку: mdlite оборачивает
   текст в <p>, а вариант — это одна строка списка. */
.option-body p { margin: 0; }
.option-body pre { margin: .3em 0; }

.list { list-style: none; padding: 0; margin: 0; display: grid; gap: .4rem; }

/* Подпись рядом с кнопкой «Форматировать»: причина отказа gofmt (позиция и
   текст ошибки) либо «уже отформатировано». Скрыта, пока сказать нечего. */
.format-note { display: block; margin-top: .35rem; font-size: .8125rem; }

/* Звёзды средней оценки (trainer_history.html). Символом, а не картинкой:
   страница отдаёт строгий CSP и не грузит ничего извне, а половинная звезда
   рисуется градиентом по тому же символу — без второго ресурса и без SVG. */
.stars { white-space: nowrap; letter-spacing: .05em; }
.star { color: var(--muted); }
.star-full { color: #e0a800; }
.star-half {
  background: linear-gradient(90deg, #e0a800 50%, var(--muted) 50%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.rating { display: flex; align-items: baseline; gap: var(--gap-sm); }

/* .editor-pane — поля решения внутри формы. Колонкой, как и сама форма
   (.stack): обёртка появилась ради прокрутки (ниже), и раскладку менять она
   не должна. */
.editor-pane { display: flex; flex-direction: column; }

/* Ни условие, ни решение не должны растягивать страницу: и то и другое
   уводит кнопку «Отправить» вниз за край окна. На широком экране обе колонки
   получают собственную прокрутку и остаются в высоте окна — прокручивается
   содержимое, а не страница.

   Справа прокручивается .editor-pane (обёртка вокруг полей решения), а не вся
   колонка: кнопки стоят после неё и потому всегда на виду. Ограничь мы
   колонку целиком — кнопки уехали бы в ту же прокрутку, только покороче.

   Вычитаемые высоты — под шапку страницы, вкладки и ряд кнопок; они
   приблизительны намеренно: точная величина требовала бы цепочки
   flex-контейнеров через .tabs, где ярлыки вкладок обязаны остаться в строку.

   Только там, где колонки стоят рядом: ниже 1024px они идут одна под другой,
   и внутренняя прокрутка внутри страничной мешала бы читать. */
@media (min-width: 1024px) {
  .two-col > .readme {
    max-height: calc(100vh - 12rem);
    overflow-y: auto;
  }
  .editor-pane {
    max-height: calc(100vh - 26rem);
    overflow-y: auto;
  }
  /* Вывод проверки сборки бывает длинным (ошибки компилятора) — иначе он
     утащил бы кнопки вверх за край окна, вернув ту же проблему снизу. */
  #build-check { max-height: 30vh; overflow-y: auto; }
}

/* Отметка об оценке в карточке тренажёра (trainers.html). Своя, не средняя —
   см. комментарий в шаблоне. Приглашение оценить выделено слабее значка: это
   просьба, а не состояние работы, и спорить с баллом за внимание она не
   должна. */
.rated { font-size: .8125rem; white-space: nowrap; }
.rated-ask { opacity: .75; }

/* «Сдано ранее» — свёрнутая группа на родном details/summary, без JS. */
.done-group { margin-top: var(--gap); }
.done-group > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: .875rem;
  padding: .3em 0;
}
.done-group > summary:hover { color: var(--text); }
.done-group > .trainer-list { margin-top: .4rem; }

/* Таблицы в условии тренажёра. Класса на них нет: markdown рендерит goldmark,
   и приписывать свой класс значило бы разбирать вывод разметки после него.
   Стилизуем по месту — внутри .readme, — а не глобально: у таблиц раздела
   управления свой класс .table и свои размеры. */
.readme table {
  border-collapse: collapse;
  margin: 0 0 1em;
  font-size: .9375rem;
  display: block;
  overflow-x: auto;
}
.readme th, .readme td {
  border: 1px solid var(--border);
  padding: .4em .7em;
  text-align: left;
}
.readme th { background: var(--surface-2); font-weight: 600; }

/* Administrative tables keep their columns on a phone instead of squeezing
   labels and actions into unreadable fragments. The table itself becomes the
   local horizontal scroll region, so the page never grows wider than viewport. */
 (max-width: 640px) {
  .table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  .table th, .table td { white-space: nowrap; }
}

/* Test submission remains available after a long question list. */
.quiz-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--gap); margin-top: var(--gap-lg); padding: var(--gap) 0; border-top: 1px solid var(--border); }
.quiz-actions p { margin: 0; }
@media (max-width: 640px) { .quiz-actions { position: sticky; bottom: 0; background: var(--bg); padding: var(--gap-sm) 0; } .quiz-actions .btn { flex: 0 0 auto; } }

/* Своя строка в таблице рекордсменов (leaderboard.html). */
.table tr.row-self > td { background: var(--surface-2); font-weight: 500; }
.table tr.row-self > td:first-child { box-shadow: inset 3px 0 0 0 var(--accent); }

/* Теги тренажёров (manage_trainers.html, manage_trainer.html, trainer.html).

   Не .badge: тот означает СОСТОЯНИЕ работы — прошла, упала, идёт, — и точка
   слева у него ровно про это. Тег состояния не описывает, он описывает
   предмет, и одинаковый вид у двух разных смыслов сбивал бы с толку там, где
   они стоят рядом: карточка задания несёт и то и другое.

   Два вида различаются заливкой, а не только текстом: тема и технология
   отвечают на разные вопросы («чему учит» и «что трогают руками»), и в полосе
   из трёх десятков взгляд должен делить их не читая. */
.tag {
  display: inline-flex;
  align-items: baseline;
  gap: .35em;
  font-size: .75rem;
  line-height: 1.4;
  padding: .25em .6em;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--muted);
  white-space: nowrap;
}
a.tag:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }
/* Технология — с оттенком акцента: их меньше, и ищут их прицельно. */
.tag-tech { background: var(--accent-weak); }
/* Выбранный тег: обводка, а не только цвет, — на фоне трёх десятков соседей
   разница в оттенке теряется. */
.tag-on { color: var(--accent); border-color: var(--accent); font-weight: 600; }

/* Строка тегов под названием задания. */
.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: .3em;
  margin: .35rem 0 0;
}

/* Полоса фильтра над каталогом: три десятка тегов переносятся в несколько
   строк, и это нормально — список короче каталога, который он фильтрует. */
.tag-filter {
  display: flex;
  flex-wrap: wrap;
  gap: .35em;
  margin: 0 0 var(--gap);
}

/* Поиск над таблицей выбора заданий (manage_version_pins.html). Кнопки тегов
   там же — обычные .tag, но <button>, а не ссылка: фильтр живёт в браузере и
   адреса не меняет. */
.pin-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-sm);
  margin: 0 0 var(--gap-sm);
}
.pin-filter input[type="search"] {
  flex: 1 1 22rem;
  max-width: 32rem;
}
.tag-filter button.tag {
  font: inherit;
  font-size: .75rem;
  cursor: pointer;
}

/* Материалы занятия: разбор темы на странице тренажёра и в программе курса
   (trainer.html, trainers.html). Ссылок обычно одна-две, поэтому строкой, а не
   списком с маркерами. */
.lesson-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-sm);
  margin: .35rem 0 0;
}
.lesson-links a { font-size: .875rem; }
.lesson-links a::after {
  /* Ссылка уводит на внешний сайт и открывается в новой вкладке — значок
     говорит об этом до нажатия, а не после. */
  content: " ↗";
  font-size: .8em;
}
.lesson-links-block { margin: 0 0 var(--gap); }

/* Программа курса — свёрнутый список занятий потока (trainers.html). Занятие
   без тренажёра карточки не имеет, и это единственное место, где студент его
   видит. */
.lesson-plan {
  list-style: none;
  margin: 0;
  padding: 0;
}
.lesson-plan li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--gap-sm);
  padding: .3em 0;
  border-bottom: 1px solid var(--border);
}
.lesson-plan li:last-child { border-bottom: 0; }
.lesson-plan .lesson-num { font-variant-numeric: tabular-nums; }
.lesson-plan .lesson-title { flex: 1 1 20rem; }
.lesson-plan .lesson-links { margin: 0; }
