/*
  IQХимия — общий слой компонентов дизайн-системы.

  Перенесено из iq-chemistry-main/web/src/styles/app.css (автор — kkkavun) с
  двумя принципиальными отличиями от исходника:

  1. ШРИФТЫ НЕ С GOOGLE. В оригинале стоял @import с fonts.googleapis.com. У нас
     ученики в Казахстане, где google-домены режут — тот же троттлинг, из-за
     которого весь проект ходит в Firestore через свой /api-прокси. Шрифт оттуда
     либо приедет через несколько секунд, либо не приедет вовсе. Поэтому файлы
     лежат у нас же (/assets/fonts) и раздаются с того же домена, что страница
     (см. css/fonts.css). Если файл почему-то не доехал, включается системный
     фолбэк из --font-body — страница остаётся читаемой.

  2. НИЧЕГО НЕ КРАСИТ САМО ПО СЕБЕ. Страницы проекта — статические HTML со
     своими <style>, и глобальные правила по тегам ломали бы их при подключении.
     Здесь только классы плюс минимальная база, а фон-миллиметровка включается
     классом .lab-grid на <body>, а не сама.

  Подключать ПЕРЕД собственным <style> страницы, чтобы правила страницы
  выигрывали при равной специфичности.
*/

/* Версия в импортах обязательна: без неё браузер, у которого tokens.css уже
   лежит в кеше, не перезапросит его при правке — сама app.css обновится, а
   значения переменных останутся старыми, и правила будут ссылаться в пустоту.
   Поднимать при каждом изменении токенов. */
@import './tokens.css?v=2';

/* Гарнитуры — отдельным файлом: он собран автоматически из ответа Google Fonts
   и разложен по подмножествам символов, руками его не правят. */
@import './fonts.css?v=1';

/* ── База ────────────────────────────────────────────────────────────────── */
*,
*::before,
*::after { box-sizing: border-box; }

/* Атрибут hidden браузер реализует правилом display: none в СВОЕЙ таблице
   стилей, а она слабее любого нашего правила. Стоит объявить элементу
   display: grid — и hidden перестаёт работать: именно так все недели на карте
   курса оказались раскрыты и не сворачивались. Восстанавливаем ожидаемое
   поведение для всего проекта. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--t-16);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Фон-миллиметровка: лабораторный лист вместо пустоты. Только по классу —
   position: fixed на псевдоэлементе body ломает страницы, где body участвует
   в раскладке. */
body.lab-grid::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 32px 32px;
  mask-image: radial-gradient(ellipse 120% 80% at 50% 0%, #000 20%, transparent 75%);
}

/*
  Акцидентная гарнитура (Unbounded) закреплена за заголовком страницы и
  формулировкой вопроса. Ниже по иерархии работает основная: два характерных
  начертания рядом делают текст дробным и «скачущим».
*/
.h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-32);
  letter-spacing: -0.03em;
  line-height: var(--lh-tight);
  margin: 0;
}

.h2, .h3, .h4 {
  font-family: var(--font-body);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: var(--lh-snug);
  margin: 0;
}

.h2 { font-size: var(--t-25); }
.h3 { font-size: var(--t-21); }
.h4 { font-size: var(--t-18); }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* ── Утилиты ─────────────────────────────────────────────────────────────── */
.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum';
}

.eyebrow {
  font-size: var(--t-13);
  font-weight: 500;
  color: var(--text-3);
}

.muted { color: var(--text-2); }
.dim { color: var(--text-3); }
.stack { display: grid; }
.row { display: flex; align-items: center; }
.grow { flex: 1; }

/* Только для скринридера: подпись, которую не должно быть видно глазами */
.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

/* ── Кнопки ──────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 44px;
  padding: 0 var(--s-6);
  border: 1px solid transparent;
  border-radius: var(--r-btn);
  background: var(--surface-3);
  color: var(--text);
  font: inherit;
  font-size: var(--t-16);
  font-weight: 600;
  letter-spacing: -0.01em;
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}

.btn:hover { background: var(--border); }
.btn:active { transform: translateY(1px); }

.btn:disabled {
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--border-3);
  cursor: not-allowed;
  transform: none;
}

.btn--primary { background: var(--action); color: var(--on-action); }
.btn--primary:hover { background: var(--action-hover); }
.btn--primary:active { background: var(--action-press); }

/* --right-solid, а не --right: белый текст на исходной заливке не проходил
   по контрасту (см. tokens.css) */
.btn--right { background: var(--right-solid); color: var(--on-right); }
.btn--right:hover { background: var(--right-solid-hover); }

.btn--ghost {
  background: transparent;
  border-color: var(--border-2);
  color: var(--text-1);
}
.btn--ghost:hover { background: var(--surface-2); border-color: var(--border-3); }

.btn--danger { color: var(--wrong-ink); }
.btn--danger:hover { background: var(--wrong-quiet); border-color: var(--wrong); }

.btn--sm {
  min-height: 34px;
  padding: 0 var(--s-3);
  font-size: var(--t-13);
  border-radius: var(--r-md);
}

.btn--wide { width: 100%; }

/* На сенсорном экране мелкая кнопка дорастает до пальца: 34px хороши мышью
   в плотной админской таблице, но на телефоне в такую цель не попасть
   (44px — общая рекомендация и WCAG 2.5.5, и Apple HIG). */
@media (pointer: coarse) {
  .btn--sm { min-height: 44px; padding: 0 var(--s-4); }
}

/* Кнопка, свёрстанная ссылкой, не наследует ссылочный цвет */
a.btn, a.btn:hover { color: var(--text); text-decoration: none; }
a.btn--primary, a.btn--primary:hover, a.btn--primary:active { color: var(--on-action); }
a.btn--right, a.btn--right:hover { color: var(--on-right); }

/* ── Плашки и счётчики ───────────────────────────────────────────────────── */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  height: 26px;
  padding: 0 var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: var(--t-13);
  font-weight: 500;
  color: var(--text-1);
  white-space: nowrap;
}

.chip--num { font-family: var(--font-mono); font-size: var(--t-12); }
/* Единица измерения в плашке («балла», «дней», «%»).
   В исходнике она приглушена прозрачностью 0.7 — но прозрачность не меняет
   цвет, а подмешивает фон, и фактический контраст падал до 2,7 при норме 4,5.
   Проверил расчётом все значения: проходит только полная непрозрачность,
   поэтому второстепенность передаём начертанием, а не блёклостью. */
.chip__unit { color: currentColor; font-weight: 400; }

.chip--xp     { border-color: var(--xp);     background: var(--xp-quiet);     color: var(--xp-ink); }
.chip--streak { border-color: var(--streak); background: var(--streak-quiet); color: var(--streak-ink); }
.chip--right  { border-color: var(--right);  background: var(--right-quiet);  color: var(--right-ink); }
.chip--wrong  { border-color: var(--wrong);  background: var(--wrong-quiet);  color: var(--wrong-ink); }
.chip--hint   { border-color: var(--hint);   background: var(--hint-quiet);   color: var(--hint-ink); }

/* Серия прервана — плашка гаснет, а не кричит красным */
.chip--streak[data-risk='true'] {
  border-color: var(--border-2);
  background: var(--surface-2);
  color: var(--text-3);
}

/* ── Полоса уровня ───────────────────────────────────────────────────────── */
.level { display: grid; gap: var(--s-2); }

.level__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
}

.level__rank { font-size: var(--t-18); font-weight: 600; letter-spacing: -0.01em; }
.level__xp   { font-size: var(--t-12); color: var(--text-3); }

.level__track {
  position: relative;
  height: 10px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.level__fill {
  height: 100%;
  width: var(--fill, 0%);
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--ph-11), var(--ph-13));
  transition: width var(--t-liquid);
}

/* ── Карточки ────────────────────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-6);
}

.card--flat { border-radius: var(--r-md); padding: var(--s-4); }

.card__title {
  font-size: var(--t-18);
  font-weight: 600;
  letter-spacing: -0.01em;
  /* Заголовок внутри карточки-ссылки не должен окрашиваться как ссылка */
  color: var(--text);
}

a.card, a.card--flat { text-decoration: none; }
a.card:hover .card__title,
a.card--flat:hover .card__title { color: var(--action-hover); }

.grid-cards {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

/* ── Шапка ───────────────────────────────────────────────────────────────── */
.nav {
  position: sticky;
  top: 0;
  z-index: 40;
  min-height: var(--nav-h);
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
  padding: var(--s-2) var(--s-4);
  background: var(--scrim);
  backdrop-filter: blur(14px) saturate(1.4);
  border-bottom: 1px solid var(--border);
}

.nav__brand {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-16);
  letter-spacing: -0.04em;
  color: var(--text);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.nav__links { display: flex; align-items: center; gap: var(--s-1); margin-left: var(--s-4); }

.nav__link {
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-btn);
  color: var(--text-2);
  font-size: var(--t-13);
  font-weight: 500;
  text-decoration: none;
  transition: background var(--t-fast), color var(--t-fast);
}

.nav__link:hover { background: var(--surface-3); color: var(--text); }
.nav__link[aria-current='page'] { background: var(--action-quiet); color: var(--text); }

/* Правая группа: имя, служебные кнопки, выход */
.nav__stats {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-1);
  height: 32px;
  min-width: 32px;
  padding: 0 var(--s-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--text-1);
  font: inherit;
  font-size: var(--t-13);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}

.iconbtn:hover { background: var(--surface-2); border-color: var(--border-3); color: var(--text); }

/* Та же оговорка, что у .btn--sm: пальцем в 32px не попасть */
@media (pointer: coarse) {
  .iconbtn { height: 44px; padding: 0 var(--s-3); }
  .nav__brand { min-height: 44px; }
}

/* На узком экране у служебных кнопок остаётся только значок. Иначе шапка
   администратора (шесть кнопок с подписями) разъезжалась на три ряда и
   съедала пятую часть экрана телефона — а она ещё и липкая. */
@media (max-width: 620px) {
  .nav__label { display: none; }
  .iconbtn { padding: 0 var(--s-2); }
}

/* ── Полоса объявления ───────────────────────────────────────────────────── */
.banner {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-width: 0;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--hint);
  background: var(--hint-quiet);
  color: var(--hint-ink);
  font-size: var(--t-13);
  font-weight: 500;
}

/* Длинное объявление обрезается многоточием, а не растягивает шапку */
.banner__text,
.banner__body { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.banner__icon { flex: none; }

/* ── Дашборд: карточка ученика ───────────────────────────────────────────── */
.hero {
  display: grid;
  gap: var(--s-6);
  padding: var(--s-6);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
}

.hero__head { display: flex; gap: var(--s-6); flex-wrap: wrap; align-items: flex-end; }
.hero__intro { flex: 1; min-width: 220px; }
.hero__level { flex: 1; min-width: 240px; }
.hero__chips { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* Заголовок раздела: обычное начертание вместо разряженного верхнего регистра */
.section__title { font-size: var(--t-15); font-weight: 600; color: var(--text-2); }

/* ── Карточка курса ──────────────────────────────────────────────────────── */
.course {
  position: relative;
  display: grid;
  /* Одинаковые дорожки у всех карточек: обложка, заголовок, описание, шкала,
     кнопки — иначе курс без обложки ломает выравнивание в ряду. */
  grid-template-rows: auto auto 1fr auto auto;
  gap: var(--s-4);
  /* БЕЗ overflow:hidden — иначе .picker__menu кнопки «Управление» (внутри
     .course__actions, у самого низа карточки) обрезается границей карточки
     и физически не может показаться. Обложка обрезает себя сама (у
     .course__cover свой overflow:hidden и радиус), так что здесь он ничему
     не был нужен. */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-6);
  transition: border-color var(--t-fast), transform var(--t-fast);
}

.course:hover { border-color: var(--border-3); transform: translateY(-2px); }

.course__cover {
  aspect-ratio: 16 / 7;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border);
}

.course__cover img { width: 100%; height: 100%; object-fit: cover; display: block; }

/*
  Обложки нет — рисуем шестиугольную сетку, отсылающую к строению вещества.
  Цветной градиент здесь был бы декорацией без отношения к предмету.
*/
.course__cover--drawn {
  position: relative;
  display: grid;
  place-items: center;
  background-color: var(--surface-2);
  background-image:
    radial-gradient(circle, var(--border-3) 1.5px, transparent 1.6px),
    radial-gradient(circle, var(--border-2) 1.5px, transparent 1.6px);
  background-size: 26px 26px, 26px 26px;
  background-position: 0 0, 13px 13px;
}

.course__mark {
  position: relative;
  font-family: var(--font-display);
  font-size: var(--t-21);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-2);
  max-width: 90%;
  overflow-wrap: anywhere;
  text-align: center;
}

.course__meta { display: flex; flex-wrap: wrap; gap: var(--s-2); align-content: start; }
.course__desc { font-size: var(--t-15); color: var(--text-2); }
.course__actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* ══ ПОДПИСНОЙ ЭЛЕМЕНТ: титрационная колонна ═══════════════════════════════
   Карта курса — стеклянная колонна: пройденное поднимает уровень жидкости,
   цвет каждой недели взят со шкалы индикатора. Прогресс читается одним
   взглядом по высоте заливки, а не по числу в углу.
   ─────────────────────────────────────────────────────────────────────── */
.titration {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: var(--s-6);
  align-items: start;
}

/* Колонна прогресса занимала место В ТОЙ ЖЕ ширине, что и карточки выше
   (объявление, «Остановились на…»), и недели из-за неё были у́же их на 120px —
   ровно на колонну с зазором. Выносим колонну ВЛЕВО, за пределы колонки
   контента: недели получают ту же ширину, что и карточки над ними.
   Порог 1060px не случаен: сдвиг влево равен 96+24=120px, а слева от main
   (max-width 800) свободно ровно (viewport−800)/2+16 — при 1008px это 0,
   и на более узком экране блок вылез бы за край. Ниже порога раскладка
   прежняя, у́же — но целиком на экране. */
@media (min-width: 1060px) {
  .titration {
    margin-left: calc(-1 * (96px + var(--s-6)));
    width: calc(100% + 96px + var(--s-6));
  }
}

/* Колонки грида по умолчанию не сжимаются уже своего min-content, а min-content
   у списка недель большой (названия уроков, плашки, кнопки). Без этого на
   телефоне страница уезжала вбок на 50 пикселей. Правило нужно на КАЖДОМ
   уровне вложенности: min-width: auto стоит у элемента грида по умолчанию,
   и одного разрешения сверху не хватает — распирает первый же уровень ниже. */
.titration > *,
.weeks > *,
.week > *,
.nodes > * { min-width: 0; }

.column {
  position: sticky;
  top: calc(var(--nav-h) + var(--s-4));
  height: min(70vh, 620px);
  display: grid;
  /* Явные дорожки: без них стекло получает высоту по содержимому (ноль) и
     колонна занимает четверть отведённого места. */
  grid-template-rows: 1fr auto;
  grid-template-columns: 1fr;
  justify-items: center;
}

.column__glass {
  position: relative;
  width: 56px;
  height: 100%;
  border-radius: 28px 28px 12px 12px;
  background: linear-gradient(100deg, var(--glass-tint-a), var(--glass-tint-b) 40%, var(--glass-tint-a));
  border: 1px solid var(--border-2);
  box-shadow: inset 0 0 24px var(--glass-shadow), var(--shadow-1);
  overflow: hidden;
}

/* Блик на стекле — одна полоса, а не «стеклянный» эффект ради эффекта */
.column__glass::after {
  content: '';
  position: absolute;
  top: 8px;
  left: 10px;
  width: 6px;
  height: calc(100% - 16px);
  border-radius: var(--r-pill);
  background: linear-gradient(180deg, var(--glass-shine), transparent);
  pointer-events: none;
}

.column__liquid {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: var(--fill, 0%);
  background: linear-gradient(
    180deg,
    var(--ph-13) 0%,
    var(--ph-11) 22%,
    var(--ph-09) 42%,
    var(--ph-07) 60%,
    var(--ph-05) 76%,
    var(--ph-03) 89%,
    var(--ph-01) 100%
  );
  background-size: 100% var(--full-height, 100%);
  background-position: bottom;
  transition: height var(--t-liquid);
}

/* Мениск: жидкость в стекле имеет вогнутую поверхность */
.column__meniscus {
  position: absolute;
  left: 0;
  width: 100%;
  height: 12px;
  bottom: calc(var(--fill, 0%) - 6px);
  background: radial-gradient(ellipse 60% 100% at 50% 100%, var(--glass-shine), transparent 70%);
  transition: bottom var(--t-liquid);
  pointer-events: none;
}

.column__ticks {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.column__tick {
  position: absolute;
  right: 0;
  width: 12px;
  height: 1px;
  background: var(--border-3);
}

.column__tick[data-major='true'] {
  width: 20px;
  background: var(--text-3);
}

.column__readout {
  margin-top: var(--s-3);
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--t-12);
  color: var(--text-2);
}

.column__readout b {
  display: block;
  font-size: var(--t-18);
  font-weight: 700;
  color: var(--text);
}

/* ── Недели и узлы ───────────────────────────────────────────────────────── */
.weeks { display: grid; gap: var(--s-6); }

.week {
  display: grid;
  gap: var(--s-3);
  /* Цвет полосы недели задаётся инлайном из шкалы индикатора */
  --band: var(--ph-09);
}

/* Заголовок недели — переключатель: программа на 12 недель не должна
   раскрываться целиком, иначе карта превращается в бесконечный список. */
.week__head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  padding: var(--s-2) 0;
  text-align: left;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--border);
  color: inherit;
  transition: border-color var(--t-fast);
}

.week__head:hover { border-color: var(--band); }

/* Заголовок недели — основная цель нажатия на карте курса */
@media (pointer: coarse) { .week__head { min-height: 44px; } }

.week__dot {
  width: 10px;
  height: 10px;
  border-radius: var(--r-pill);
  background: transparent;
  border: 2px solid var(--band);
  box-shadow: 0 0 12px -4px var(--band);
  flex: none;
}

.week__dot[data-filled='true'] { background: var(--band); }

.week__title {
  font-size: var(--t-16);
  font-weight: 600;
  letter-spacing: -0.01em;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.week__here {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--t-12);
  color: var(--active);
}

.week__gauge {
  flex: none;
  width: 56px;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--border);
  overflow: hidden;
}

.week__gaugefill {
  display: block;
  height: 100%;
  width: var(--fill, 0%);
  background: var(--band);
  transition: width var(--t-mid);
}

.week__count {
  font-family: var(--font-mono);
  font-size: var(--t-12);
  color: var(--text-3);
  flex: none;
}

.week__chevron {
  flex: none;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-2);
  font-family: var(--font-mono);
  font-size: var(--t-13);
  color: var(--text-2);
}

.week__head:hover .week__chevron { border-color: var(--band); color: var(--text); }



.nodes { display: grid; gap: var(--s-2); }

.nodes .node {
  animation: rise 240ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: calc(var(--i, 0) * 24ms);
}

@media (prefers-reduced-motion: reduce) {
  .nodes .node { animation: none; }
}

.node {
  position: relative;
  display: grid;
  grid-template-columns: 44px 1fr auto;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  padding: var(--s-3);
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: inherit;
  transition: border-color var(--t-fast), background var(--t-fast), transform var(--t-fast);
}

.node:hover:not(:disabled) {
  border-color: var(--border-3);
  background: var(--surface-2);
  transform: translateX(2px);
}

.node:disabled { cursor: not-allowed; }

.node__cell {
  position: relative;
  width: 44px;
  height: 44px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 12px;
  border: 1px solid var(--border-2);
  background: var(--surface-2);
  overflow: hidden;
}

/* Скол на закрытом углу — «огранка» освоенной ячейки */
.node__cell::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--band);
  opacity: 0;
  transition: opacity var(--t-mid);
}

.node__glyph {
  position: relative;
  z-index: 1;
  font-family: var(--font-mono);
  font-size: var(--t-12);
  font-weight: 700;
  color: var(--text-3);
}

/* display: grid обязателен: .node__body в разметке — <span>, а обрезка
   многоточием на строчных элементах не работает вовсе. Без этого длинное
   название урока вылезало за экран и тащило страницу вбок на 45px. */
.node__body { display: grid; min-width: 0; }

.node__title {
  display: block;
  font-size: var(--t-13);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.node__sub {
  font-family: var(--font-mono);
  font-size: var(--t-12);
  color: var(--text-3);
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.node__side {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: none;
}

/* Урок подсвечен собственным цветом — в неделе вперемешку уроки и
   тренажёры, и на глаз проще сразу отличить «читать» от «решать», не
   вчитываясь в подпись под названием. Стоит ДО состояний намеренно: у
   locked/noaccess ниже есть свой background и border-color, они должны
   перебивать подсветку типа, а не наоборот — закрытый урок не должен
   звать открыть его. */
.node[data-type='lesson'] {
  background: var(--action-quiet);
  border-color: var(--action);
}
.node[data-type='lesson'] .node__glyph { color: var(--action-ink); }

/* Состояния узла — четыре разных читаемых образа, а не оттенки одного */
.node[data-state='locked'] {
  background: var(--bg);
  border-color: var(--surface-3);
}

.node[data-state='locked'] .node__title { color: var(--text-3); }

.node[data-state='locked'] .node__cell {
  background: repeating-linear-gradient(
    -45deg,
    var(--surface-2) 0 4px,
    var(--surface) 4px 8px
  );
}

.node[data-state='available'] {
  border-color: var(--active);
}

.node[data-state='available'] .node__cell {
  border-color: var(--active);
  box-shadow: inset 0 -12px 16px -12px var(--active);
}

/* Значки типа урока — эмодзи, они рисуются своими цветами и на color не
   реагируют; правило работает для текстовых значков состояния. */
.node[data-state='available'] .node__glyph { color: var(--active-ink); }

.node[data-state='inprogress'] .node__cell::before {
  opacity: 0.35;
  background: var(--active);
}

.node[data-state='inprogress'] .node__glyph { color: var(--text); }

.node[data-state='completed'] {
  border-color: var(--right);
}

.node[data-state='completed'] .node__cell::before { opacity: 1; }

.node[data-state='completed'] .node__cell {
  border-color: var(--band);
  clip-path: polygon(0 0, 100% 0, 100% 72%, 72% 100%, 0 100%);
}

/* Галочка на цветной полосе недели.
   В исходнике здесь стоял --on-right, потому что тема была тёмной и текст на
   ярких заливках был тёмным. У нас тема светлая, --on-right стал белым, и
   галочка на жёлтой полосе давала контраст 1,67 — её просто не было видно.
   Тёмные чернила проходят на всех семи цветах шкалы (худший случай 4,2). */
.node[data-state='completed'] .node__glyph { color: #04160c; }

.node__gate {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-family: var(--font-mono);
  font-size: var(--t-12);
  color: var(--hint);
}


/* Пятое состояние, которого в исходнике не было: урок открыт другой группе.
   От «закрыт» отличается тем, что ждать бесполезно — надо писать преподавателю,
   поэтому и образ другой: не штриховка ожидания, а погашенная ячейка. */
.node[data-state='noaccess'] {
  background: var(--bg);
  border-color: var(--surface-3);
}
.node[data-state='noaccess'] .node__title { color: var(--text-3); }
.node[data-state='noaccess'] .node__cell { filter: grayscale(1); opacity: .75; }
.node__note {
  font-family: var(--font-mono);
  font-size: var(--t-12);
  margin-top: 2px;
}
.node__note--gate { color: var(--hint-ink); }
.node__note--deny { color: var(--wrong-ink); }

/* Кнопка внутри узла: узел сам по себе кликабельный, но у нас в разметке
   исторически отдельная кнопка справа — оставляем её как вторичное действие. */
.node__side .lesson-btn { flex: none; }

/* Телефон: колонна уезжает наверх и ложится горизонтально — вертикальная
   на 70vh съела бы весь экран, а рядом с ней не осталось бы места неделям. */
@media (max-width: 700px) {
  .titration { grid-template-columns: 1fr; gap: var(--s-4); }
  .column {
    position: static;
    height: auto;
    grid-template-columns: 1fr auto;
    grid-template-rows: auto;
    align-items: center;
    gap: var(--s-3);
  }
  .column__glass {
    width: 100%;
    height: 20px;
    border-radius: var(--r-pill);
  }
  .column__glass::after { top: 5px; left: 8px; width: calc(100% - 16px); height: 5px;
    background: linear-gradient(90deg, var(--glass-shine), transparent); }
  .column__liquid {
    height: 100%;
    width: var(--fill, 0%);
    background: linear-gradient(90deg, var(--ph-01) 0%, var(--ph-03) 11%, var(--ph-05) 24%,
      var(--ph-07) 40%, var(--ph-09) 58%, var(--ph-11) 78%, var(--ph-13) 100%);
    transition: width var(--t-liquid);
  }
  .column__meniscus, .column__ticks { display: none; }
  .column__readout { margin-top: 0; white-space: nowrap; }
}


/* ── Появление элементов ─────────────────────────────────────────────────── */
@keyframes pop-in {
  from { opacity: 0; transform: translateY(-4px) scale(0.97); }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
}


/* ── Переход-«заливка»: волна закрывает экран снизу и уходит вверх ────────── */
.wipe {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  transform: translateY(calc(100% + 72px));
  will-change: transform;
}

/* В покое волна не просто уехала за край, а невидима: край с кромкой не должен
   выглядывать ни при каком пересчёте вьюпорта (мобильная адресная строка,
   полностраничные скриншоты). */
.wipe[data-phase='idle'] { visibility: hidden; }

.wipe__crest {
  position: absolute;
  top: -63px;
  left: 0;
  width: 100%;
  height: 64px;
  display: block;
}

.wipe__body {
  position: absolute;
  inset: 0;
  background: var(--bg-elevated);
  border-top: 0;
}

.wipe[data-phase='cover'] {
  animation: wipe-cover 360ms cubic-bezier(0.55, 0, 0.55, 0.2) forwards;
}

.wipe[data-phase='reveal'] {
  animation: wipe-reveal 420ms cubic-bezier(0.3, 0.6, 0.3, 1) forwards;
}

@keyframes wipe-cover {
  from { transform: translateY(calc(100% + 72px)); }
  to { transform: translateY(0); }
}

@keyframes wipe-reveal {
  from { transform: translateY(0); }
  to { transform: translateY(-108%); }
}

/* Волна слегка «дышит» пока едет */
.wipe[data-phase='cover'] .wipe__wave,
.wipe[data-phase='reveal'] .wipe__wave {
  animation: wave-sway 780ms ease-in-out infinite alternate;
}

@keyframes wave-sway {
  from { transform: translateX(0); }
  to { transform: translateX(-24px); }
}

@media (prefers-reduced-motion: reduce) {
  .wipe { display: none; }
}


/* ── Итог попытки ────────────────────────────────────────────────────────── */
.result {
  display: grid;
  gap: var(--s-6);
  max-width: var(--max-content);
  margin: 0 auto;
  text-align: center;
}

.score {
  display: grid;
  place-items: center;
  gap: var(--s-1);
}

.score__ring {
  --pct: 0;
  position: relative;
  width: 168px;
  height: 168px;
  border-radius: var(--r-pill);
  background: conic-gradient(var(--ring-color, var(--right)) calc(var(--pct) * 1%), var(--surface-2) 0);
  display: grid;
  place-items: center;
  transition: background var(--t-liquid);
}

.score__ring::after {
  content: '';
  position: absolute;
  inset: 12px;
  border-radius: var(--r-pill);
  background: var(--bg);
}

.score__value {
  position: relative;
  z-index: 1;
  font-family: var(--font-mono);
  font-size: var(--t-25);
  font-weight: 700;
  letter-spacing: -0.04em;
}

.rewards {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.review { display: grid; gap: var(--s-2); text-align: left; }

.review__row {
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: var(--t-13);
}

.review__mark {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-13);
}

.review__row[data-correct='true'] .review__mark { background: var(--right-quiet); color: var(--right-ink); }
.review__row[data-correct='false'] .review__mark { background: var(--wrong-quiet); color: var(--wrong-ink); }


/* ── Таблица лидеров ─────────────────────────────────────────────────────── */
.lead {
  display: grid;
  gap: var(--s-6);
}

.lead__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
}

.lead__tabs {
  display: inline-flex;
  gap: var(--s-1);
  padding: var(--s-1);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}

.lead__tab {
  padding: var(--s-2) var(--s-4);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text-2);
  font-size: var(--t-13);
  font-weight: 600;
  transition: background var(--t-fast), color var(--t-fast);
}

.lead__tab[aria-selected='true'] { background: var(--action); color: var(--on-action); }

/* Пьедестал: 2 — 1 — 3, центральная колонна выше */
.podium {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-3);
  align-items: end;
  padding-top: var(--s-4);
}

.podium__spot {
  display: grid;
  justify-items: center;
  gap: var(--s-1);
  text-align: center;
  animation: rise 480ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

.podium__spot[data-place='2'] { animation-delay: 120ms; }
.podium__spot[data-place='3'] { animation-delay: 200ms; }

.podium__medal {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  font-size: var(--t-18);
  font-weight: 700;
  color: var(--bg);
  background: var(--medal);
  box-shadow: 0 4px 14px -4px var(--medal);
  margin-bottom: var(--s-1);
}

.podium__spot[data-place='1'] { --medal: var(--medal-1); }
.podium__spot[data-place='2'] { --medal: var(--medal-2); }
.podium__spot[data-place='3'] { --medal: var(--medal-3); }

.podium__spot[data-place='1'] .podium__medal { width: 54px; height: 54px; font-size: var(--t-21); }

.podium__name {
  font-weight: 700;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.podium__rank { color: var(--text-3); font-size: var(--t-12); }
.podium__xp { color: var(--text-2); font-size: var(--t-13); }

/* Сама тумба: высота по месту, сверху полоса цвета медали */
.podium__block {
  width: 100%;
  margin-top: var(--s-2);
  border-radius: var(--r-md) var(--r-md) 0 0;
  border: 1px solid var(--border);
  border-bottom: 0;
  background: linear-gradient(var(--surface-2), transparent 140%);
  border-top: 3px solid var(--medal);
  height: 56px;
}

.podium__spot[data-place='1'] .podium__block { height: 96px; }
.podium__spot[data-place='2'] .podium__block { height: 72px; }

.podium__spot[data-me] .podium__name { color: var(--action-ink); }

/* Строки 4+ */
.lead__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-1);
}

.lead__row {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto minmax(120px, 200px);
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  animation: rise 320ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

.lead__row[data-me] {
  border-color: var(--action);
  background: var(--action-quiet);
}

.lead__place { color: var(--text-3); font-weight: 600; text-align: right; }

.lead__who { display: grid; min-width: 0; }
.lead__name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lead__ranktitle { color: var(--text-3); font-size: var(--t-12); }

.lead__metag {
  margin-left: var(--s-2);
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--action);
  color: var(--on-action);
  font-size: var(--t-12);
  font-weight: 600;
  vertical-align: 2px;
}

.lead__streak {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--streak-ink, var(--text-2));
  font-size: var(--t-13);
}

.lead__score { display: grid; gap: 4px; justify-items: end; }

/* Бар — доля от опыта лидера, чтобы разрыв был виден глазами */
.lead__bar {
  width: 100%;
  height: 4px;
  border-radius: 999px;
  background: var(--surface-2);
  position: relative;
  overflow: hidden;
}

.lead__bar::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--w);
  border-radius: inherit;
  background: linear-gradient(90deg, var(--active), var(--xp));
}

.lead__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  flex-wrap: wrap;
  font-size: var(--t-13);
}

.lead__mine {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-2);
}

.lead__mine :first-child { color: var(--medal-1); }

@media (max-width: 620px) {
  .lead__row { grid-template-columns: 30px minmax(0, 1fr) auto 96px; gap: var(--s-2); }
  .podium { gap: var(--s-2); }
  .podium__xp { font-size: var(--t-12); }
}

/* Цифра на медали. В исходнике она набрана цветом фона страницы (--bg), что
   работает в тёмной теме и разваливается в светлой. Считал по каждому металлу:
   тёмные чернила проходят на золоте (6,8) и серебре (5,2), но на бронзе дают
   3,9 — там нужен белый (4,6). Единого цвета на все три нет, поэтому цвет
   задан вместе с самим металлом. */
.podium__spot[data-place='1'] .podium__medal,
.podium__spot[data-place='2'] .podium__medal { color: #0e1626; }
.podium__spot[data-place='3'] .podium__medal { color: #ffffff; }

/* Экран результата: у нас он живёт в .result-card, а не в .result, и кольцо
   вырезано цветом карточки, а не страницы. */
.result-card .score__ring::after { background: var(--surface); }
.score__ring[data-tone='mid']  { --ring-color: var(--hint); }
.score__ring[data-tone='low']  { --ring-color: var(--wrong); }

@media (prefers-reduced-motion: reduce) {
  .podium__spot, .lead__row { animation: none; }
}

/* ── Маскот ──────────────────────────────────────────────────────────────── */
.mole { display: block; }

.mole__wing {
  transform-origin: 29px 32px;
  animation: breathe 3.4s ease-in-out infinite;
}

.mole__wing--right { transform-origin: 35px 32px; }

.mole[data-mood='happy'] .mole__wing { animation: flap 220ms ease-in-out 3; }
.mole[data-mood='wrong'] .mole__wing { animation: droop var(--t-mid) forwards; }
.mole[data-mood='sleep'] .mole__wing { animation: breathe 6s ease-in-out infinite; }

@keyframes breathe {
  0%, 100% { transform: scaleX(1); }
  50% { transform: scaleX(0.94); }
}

@keyframes flap {
  0%, 100% { transform: scaleX(1) scaleY(1); }
  50% { transform: scaleX(0.7) scaleY(1.08); }
}

@keyframes droop {
  to { transform: rotate(8deg) translateY(3px); }
}

@media (prefers-reduced-motion: reduce) {
  .mole__wing { animation: none !important; }
}

.mole-say {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: var(--s-3);
  align-items: center;
}

.mole-say__bubble {
  position: relative;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: var(--t-13);
  color: var(--text-1);
}

.mole-say__bubble::before {
  content: '';
  position: absolute;
  left: -5px;
  top: 50%;
  width: 8px;
  height: 8px;
  transform: translateY(-50%) rotate(45deg);
  background: var(--surface-2);
  border-left: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}


/* ── Карточки-флешкарты ──────────────────────────────────────────────────── */
.deck {
  display: grid;
  gap: var(--s-6);
  max-width: 560px;
  margin: 0 auto;
}

.flip {
  position: relative;
  min-height: 260px;
  perspective: 1400px;
}

.flip__inner {
  position: relative;
  width: 100%;
  min-height: 260px;
  transform-style: preserve-3d;
  transition: transform var(--t-mid);
}

.flip[data-open='true'] .flip__inner { transform: rotateY(180deg); }

.flip__face {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--s-8);
  text-align: center;
  border-radius: var(--r-lg);
  border: 1px solid var(--border-2);
  backface-visibility: hidden;
  font-size: var(--t-21);
  font-weight: 600;
  line-height: var(--lh-snug);
}

.flip__face--front { background: var(--surface); }

/* Оборот карточки берёт --sheet-2, а не --sheet: в исходнике тема тёмная и
   «бумага» там заметно светлее фона, а в нашей светлой --sheet это тот же
   белый, что и лицо карточки, — переворот переставал читаться. */
.flip__face--back {
  transform: rotateY(180deg);
  background: var(--sheet-2);
  color: var(--sheet-text);
  border-color: var(--sheet-edge);
}

.boxes { display: flex; gap: var(--s-1); justify-content: center; }

.boxes__cell {
  width: 26px;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--border);
}

.boxes__cell[data-on='true'] { background: var(--ph-09); }

/* ── Видеоурок ───────────────────────────────────────────────────────────── */
.video-player {
  aspect-ratio: 16 / 9;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--border);
  background: #000;
  box-shadow: var(--shadow-2);
}

.video-player iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.video-fallback {
  margin-top: var(--s-2);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--hint-ink);
  font-size: var(--t-13);
  line-height: 1.5;
}
.video-fallback a { color: inherit; text-decoration: underline; }

/* Тайм-коды: перемотка видео по плану урока */
.outline {
  display: grid;
  gap: var(--s-1);
  margin-top: var(--s-3);
}

.outline__row {
  display: grid;
  grid-template-columns: 72px 1fr;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--surface);
  font-size: var(--t-13);
}

.outline__row--seek {
  grid-template-columns: 72px 1fr auto;
  align-items: center;
  border: 1px solid transparent;
  text-align: left;
  color: var(--text-1);
  transition: border-color var(--t-fast), background var(--t-fast), transform var(--t-fast);
}

.outline__row--seek:hover {
  background: var(--surface-2);
  border-color: var(--border-2);
  transform: translateX(2px);
}

.outline__row--seek[data-active='true'] {
  border-color: var(--action);
  background: var(--action-quiet);
}

.outline__play { color: var(--text-3); opacity: 0; transition: opacity var(--t-fast); }
.outline__row--seek:hover .outline__play,
.outline__row--seek[data-active='true'] .outline__play { opacity: 1; color: var(--action-ink); }

.outline__time {
  font-family: var(--font-mono);
  font-size: var(--t-12);
  color: var(--text-3);
}

/* ── Вложения ────────────────────────────────────────────────────────────── */
.files { display: grid; gap: var(--s-2); }

.file {
  display: grid;
  grid-template-columns: 40px 1fr auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface);
  transition: border-color var(--t-fast), background var(--t-fast);
}

.file:hover { border-color: var(--action); background: var(--surface-2); }

/* Плашка файла теперь <button>, а не <a>: файл забирается запросом с
   ID-токеном, обычная навигация его не отправит. Кнопка не наследует шрифт,
   цвет и выравнивание — возвращаем их, чтобы вид остался прежним. */
button.file {
  width: 100%;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.file__ext {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-sm);
  background: var(--action-quiet);
  /* Чернильный тон, а не заливочный: на --action-quiet заливочный давал
     3,1 в тёмной теме. Чернила рассчитаны как раз на такой фон. */
  color: var(--action-ink);
  font-family: var(--font-mono);
  font-size: var(--t-12);
  font-weight: 700;
}

.file__name {
  font-size: var(--t-13);
  font-weight: 500;
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
}


/* ── Публичный лендинг ───────────────────────────────────────────────────── */
.landing {
  min-height: 100dvh;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--s-4) var(--s-10);
  display: grid;
  gap: var(--s-12);
}

/* Тот же случай, что в .titration и .profgrid: элемент грида не сжимается уже
   своего min-content. У шапки он больше ширины телефона (логотип + три кнопки),
   и страница уезжала вбок на 4px. */
.landing > * { min-width: 0; }

.landing__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s-4) 0;
}

.landing__logo {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-display);
  font-size: var(--t-16);
}

.landing__bar-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

.landing__hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s-8);
  align-items: center;
  padding: var(--s-8) 0 0;
}

.landing__hero h1 {
  font-size: clamp(28px, 4.5vw, 44px);
  line-height: 1.12;
  margin-top: var(--s-2);
  max-width: 15ch;
}

.landing__sub {
  margin-top: var(--s-4);
  max-width: 52ch;
  color: var(--text-2);
  font-size: var(--t-15);
}

.landing__cta {
  display: flex;
  gap: var(--s-3);
  margin-top: var(--s-6);
  flex-wrap: wrap;
}

.landing__hero-mole {
  padding: var(--s-6);
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--active-quiet), transparent);
}

.landing__section h2,
.landing__final h2 {
  /* --t-22 в шкале нет, ближайшая ступень вверх */
  font-size: var(--t-21);
}

/* Секции «Курсы» и финального призыва вернулись вместе с прежним лендингом
   на боевом (index.html откачен). На зеркале живёт новый лендинг, который их
   не использует, — там эти правила просто не к чему применить. --s-5 в шкале
   нет, поэтому margin-top перекрывается ниже, как и у соседних блоков. */
.landing__courses {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-4);
}

.landing__course h3 { margin-top: var(--s-3); font-size: var(--t-15); }
.landing__course p { margin-top: var(--s-1); }

.landing__course-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1);
  margin-top: var(--s-3);
}

.landing__final {
  display: grid;
  justify-items: center;
  text-align: center;
  padding: var(--s-16) var(--s-6);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background:
    radial-gradient(80% 120% at 50% 120%, var(--active-quiet), transparent 70%),
    var(--surface);
}

.landing__final h2 { font-size: var(--t-25); }

.landing__final p {
  margin: var(--s-3) 0 var(--s-6);
  font-size: var(--t-15);
}

.landing__features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-5);
  /* start, а не stretch: раскрытая карточка растёт вниз сама, а соседние
     по ряду не обязаны тянуться следом до её высоты. */
  align-items: start;
}

.landing__feature h3 { margin-top: var(--s-3); font-size: var(--t-15); }
.landing__feature p { margin-top: var(--s-2); }

.landing__feature-icon {
  display: inline-flex;
  width: 40px;
  height: 40px;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-md);
  background: var(--active-quiet);
  color: var(--active);
}

/* ── «Что внутри»: раскрывающиеся карточки ───────────────────────────────────
   Построены на <details>/<summary>: раскрытие — родная механика браузера,
   работает без JS и озвучивается скринридером как «свёрнуто/развёрнуто». */
.feature-accordion {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  overflow: hidden;
  transition: border-color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
}
.feature-accordion:hover { border-color: var(--border-3); transform: translateY(-2px); }
.feature-accordion[open] {
  border-color: var(--action);
  box-shadow: var(--shadow-2);
  transform: none;                 /* раскрытая карточка стоит на месте */
}

.feature-accordion summary,
.feature-accordion__summary-like {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4);
  cursor: pointer;
  list-style: none;                /* убираем родной треугольник… */
}
.feature-accordion summary::-webkit-details-marker { display: none; }  /* …и его же в Safari */
.feature-accordion__summary-like { cursor: default; }

/* Клавиатурная обводка — на всей шапке карточки, а не только на тексте */
.feature-accordion summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
  border-radius: var(--r-lg);
}

.feature-accordion__icon {
  display: inline-flex;
  width: 48px; height: 48px; flex: none;
  align-items: center; justify-content: center;
  border-radius: var(--r-md);
  background: var(--active-quiet);
  font-size: var(--t-21);
  transition: background var(--t-fast);
}
.feature-accordion[open] .feature-accordion__icon { background: var(--action-quiet); }

.feature-accordion__head { display: grid; gap: 2px; min-width: 0; }
.feature-accordion__title { font-size: var(--t-16); font-weight: 600; color: var(--text); }
.feature-accordion__hint  { font-size: var(--t-13); color: var(--text-3); }

.feature-accordion__chevron {
  margin-left: auto; flex: none;
  color: var(--text-3);
  font-size: var(--t-16); line-height: 1;
  transition: transform var(--t-mid), color var(--t-fast);
}
.feature-accordion[open] .feature-accordion__chevron { transform: rotate(180deg); color: var(--action-ink); }

/* Высоту <details> анимировать без трюков нельзя, поэтому проявляем сам текст:
   лёгкое всплытие читается как раскрытие и не требует замера высоты. */
.feature-accordion__body {
  padding: 0 var(--s-4) var(--s-4);
  /* Отступ слева = ширина значка + зазор: текст выравнивается по заголовку */
  padding-left: calc(var(--s-4) + 48px + var(--s-4));
  color: var(--text-2);
  font-size: var(--t-15);
  line-height: var(--lh-body);
  animation: feature-accordion-in var(--t-mid) both;
}
@keyframes feature-accordion-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .feature-accordion__body { animation: none; }
}
/* На узком экране значок и текст в одну колонку не помещаются — выравнивание
   по левому краю карточки, без отступа под значок. */
@media (max-width: 480px) {
  .feature-accordion__body { padding-left: var(--s-4); }
}

.landing__steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-5);
  list-style: none;
  padding: 0;
}

.landing__step {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  color: var(--text-2);
}

.landing__step-n {
  display: inline-flex;
  width: 28px;
  height: 28px;
  flex: none;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--action);
  color: var(--on-action);
  font-weight: 600;
}


.landing__footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2) var(--s-6);
  flex-wrap: wrap;
  color: var(--text-3);
  font-size: var(--t-13);
}

/* Появление секций при скролле — класс вешает JS (см. index.html), а не
   разметка, так что без JS секции остаются видимыми сразу. Свой тайминг, а
   не --t-liquid: та кривая с лёгким перелётом (см. tokens.css) подходит
   для отклика на действие, а для появления при чтении страницы естественнее
   плавное затухание без отскока — cubic-bezier(0.16,1,0.3,1) («ease-out-expo»,
   быстрый старт, долгое плавное торможение) и сдвиг поменьше, чтобы не
   выглядело как «прыжок». Экран уменьшения анимации — своя же короткая
   переопределяющая пара строк, без завязки на --t-liquid. */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 900ms cubic-bezier(0.16, 1, 0.3, 1), transform 900ms cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .reveal { transition-duration: 1ms; }
}

.landing__footer-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

/* Подпись автора: заметна тому, кто ищет, и не спорит с контентом */
.made {
  color: var(--text-3);
  font-size: var(--t-12);
  text-decoration: none;
}

.made:hover { color: var(--text-2); }

/* В ПОТОКЕ, а не absolute. Раньше подпись висела по bottom: 16px у .auth —
   и на высокой форме регистрации (карточка занимает всю высоту экрана и выше)
   налезала прямо на её нижний край. Отдельной строкой грида она просто встаёт
   под карточкой при любой её высоте. */
.auth__made {
  justify-self: center;
  margin-top: var(--s-4);
}

/* Уголок в кабинете: не спорит с контентом, на мобиле поднимается над таббаром */
.made--corner {
  position: fixed;
  right: var(--s-3);
  bottom: var(--s-3);
  z-index: 30;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  background: var(--scrim);
  backdrop-filter: blur(10px);
  border: 1px solid var(--border);
}

/* В исходнике уголок поднимался над нижней панелью; у нас её нет,
   поэтому просто уважаем безопасную зону айфона. */
@media (max-width: 620px) {
  .made--corner { bottom: calc(env(safe-area-inset-bottom) + var(--s-3)); }
}

@media (max-width: 760px) {
  .landing { gap: var(--s-8); }
  .landing__hero { grid-template-columns: 1fr; }
  /* Портрет и маскот поднимаются над текстом: на узком экране лицо должно
     встречать первым, а не оказываться под тремя абзацами. */
  .landing__hero-mole,
  .landing__hero-figure { justify-self: center; order: -1; }
}

/* Подпись автора набрана мелко намеренно, но пальцу нужна зона в 44px —
   увеличиваем её отступами, не трогая кегль. */
@media (pointer: coarse) {
  .made { display: inline-flex; align-items: center; min-height: 44px; padding: 0 var(--s-2); }
}


/* --s-10 и --s-5 в нашей шкале нет — она идёт 8, 12, 16, 24, 32, 48… */
.landing { padding: 0 var(--s-4) var(--s-12); }
.landing__features, .landing__steps { margin-top: var(--s-4); }

/* Фото преподавателя рядом с Молью: в исходнике на первом экране только
   маскот, но у нас туда загружается настоящее фото — оно ценнее рисунка,
   поэтому Моль встаёт рядом, а не вместо. */
/* Левая колонка первого экрана. В исходнике у неё не было своего класса —
   текст лежал прямо в .landing__hero; нам он нужен, чтобы на телефоне менять
   местами портрет и текст. */
.landing__hero-copy { min-width: 0; }

.landing__hero-figure {
  position: relative;
  display: grid;
  place-items: center;
  gap: var(--s-2);
}
.landing__hero-photo {
  width: 220px;
  height: 220px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-2);
}
/* Моль уходит в угол ТОЛЬКО когда есть фото. Без фото она остаётся в потоке —
   иначе колонка первого экрана схлопывалась в ноль и маскот пропадал. */
.landing__hero-figure[data-has-photo='true'] .mole {
  position: absolute;
  right: -8px;
  bottom: -4px;
}

@media (max-width: 760px) {
  .landing__hero-photo { width: 168px; height: 168px; }
}

/* Карточка курса на узком экране: рядом с текстом обложке тесно — переносим
   её наверх, текст читается на полную ширину. */
@media (max-width: 480px) {
  .trial-course-card { flex-direction: column; text-align: center; }
  .trial-course-card img { width: 88px; height: 88px; }
}

/* ── Видео-знакомство ────────────────────────────────────────────────────── */
.landing__video-intro { text-align: center; }
.landing__video-intro p { color: var(--text-2); font-size: var(--t-15); max-width: 56ch; margin: var(--s-2) auto 0; }

.landing__video-wrap {
  position: relative;
  width: 100%;
  max-width: 720px;
  margin: var(--s-6) auto 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-2);
  aspect-ratio: 16 / 9;
  background: var(--surface-3);
}
.landing__video-wrap iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ── Отзывы: карусель со свайпом ─────────────────────────────────────────────
   overflow-x + scroll-snap вместо JS-библиотеки: тач-свайп на телефоне
   работает нативно у браузера, стрелки — просто scrollBy() по той же оси. */
.landing__testimonials-track {
  display: flex;
  gap: var(--s-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  margin-top: var(--s-6);
  padding: var(--s-1) var(--s-1) var(--s-4);
  scrollbar-width: none;
}
.landing__testimonials-track::-webkit-scrollbar { display: none; }

.testimonial-card {
  flex: 0 0 min(320px, 82vw);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.testimonial-card__quote {
  flex: 1;
  color: var(--text-1);
  font-size: var(--t-15);
  line-height: var(--lh-body);
}
.testimonial-card__quote::before {
  content: '“';
  display: block;
  color: var(--action);
  font-size: 2em;
  line-height: 1;
  font-family: var(--font-display);
  margin-bottom: var(--s-1);
}

.testimonial-card__person { display: flex; align-items: center; gap: var(--s-3); }
.testimonial-card__avatar {
  width: 44px; height: 44px; flex: none;
  border-radius: 50%; object-fit: cover;
  background: var(--action-quiet);
}
.testimonial-card__avatar--fallback {
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; color: var(--action-ink);
}
.testimonial-card__name { font-weight: 600; font-size: var(--t-15); color: var(--text); }
.testimonial-card__meta { font-size: var(--t-13); color: var(--text-3); }

.landing__testimonials-nav {
  display: flex;
  justify-content: center;
  gap: var(--s-3);
  margin-top: var(--s-1);
}
.testimonials-arrow {
  width: 40px; height: 40px;
  border-radius: 50%;
  border: 1px solid var(--border-2);
  background: var(--surface);
  color: var(--text-1);
  font-size: var(--t-16);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.testimonials-arrow:hover { background: var(--surface-2); border-color: var(--action); color: var(--action-ink); }

/* На тач-экране свайп и так работает, а стрелки только занимают место —
   реальная кнопка мышью нужна на десктопе, поэтому прячем по типу указателя,
   а не по ширине экрана (планшет с мышью должен их видеть). */
@media (pointer: coarse) { .landing__testimonials-nav { display: none; } }

/* ── Пробный урок ────────────────────────────────────────────────────────── */
.landing__trial {
  display: grid;
  justify-items: center;
  text-align: center;
  gap: var(--s-3);
  padding: var(--s-8) var(--s-6);
  border-radius: var(--r-lg);
  border: 1.5px dashed var(--action);
  background: var(--action-quiet);
}
.landing__trial h2 { font-size: var(--t-21); }
.landing__trial p { color: var(--text-2); font-size: var(--t-15); max-width: 46ch; }
.landing__trial .btn { margin-top: var(--s-2); }

/* Карточка курса внутри блока «Пробный урок» — обложка + название,
   подтягивается из самого курса (см. index.html), а не задаётся отдельно.
   Блок вокруг (.landing__trial) уже цветной с пунктиром, поэтому карточка
   сама — плотная, светлая, с тенью: должна читаться как настоящая карточка
   товара, а не как ещё один текстовый абзац внутри баннера. */
.trial-course-card {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  max-width: 440px;
  width: 100%;
  padding: var(--s-4);
  text-align: left;
  background: var(--surface);
  box-shadow: var(--shadow-2);
  border-color: var(--border-2);
}
.trial-course-card img {
  width: 100px; height: 100px; flex: none;
  border-radius: var(--r-md);
  object-fit: cover;
  box-shadow: var(--shadow-1);
}
.trial-course-card__eyebrow {
  display: block;
  font-size: var(--t-12); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--action-ink);
  margin-bottom: var(--s-1);
}
.trial-course-card h4 { font-size: var(--t-18); color: var(--text); }
.trial-course-card p { margin-top: var(--s-1); font-size: var(--t-13); max-width: none; }

/* ── Тизер «Полезные материалы» ──────────────────────────────────────────── */
.landing__materials-teaser {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  padding: var(--s-6);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: linear-gradient(120deg, var(--active-quiet), var(--surface) 70%);
  text-decoration: none;
  transition: border-color var(--t-fast);
}
.landing__materials-teaser:hover { border-color: var(--active); }
.landing__materials-teaser-icon {
  flex: none;
  width: 56px; height: 56px;
  display: flex; align-items: center; justify-content: center;
  font-size: 28px;
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--border);
}
.landing__materials-teaser-text { flex: 1; min-width: 200px; }
.landing__materials-teaser-text h3 { font-size: var(--t-18); color: var(--text); }
.landing__materials-teaser-text p { font-size: var(--t-13); color: var(--text-2); margin-top: var(--s-1); }

/* ── Видео-разборы (YouTube) ─────────────────────────────────────────────── */
.landing__videos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-4);
}
.video-card {
  display: flex;
  flex-direction: column;
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: var(--surface);
  overflow: hidden;
  text-decoration: none;
  transition: border-color var(--t-fast), transform var(--t-fast);
}
.video-card:hover { border-color: var(--active); transform: translateY(-2px); }
.video-card__thumb {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--surface-2);
}
.video-card__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.video-card__play {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--t-25);
  color: #fff;
  background: rgba(10, 16, 32, 0.25);
  transition: background var(--t-fast);
}
.video-card:hover .video-card__play { background: rgba(10, 16, 32, 0.45); }
.video-card__title {
  padding: var(--s-3) var(--s-4);
  font-size: var(--t-15);
  font-weight: 600;
  color: var(--text);
}

/* ── Контакты и соцсети ──────────────────────────────────────────────────── */
.landing__contacts { display: grid; gap: var(--s-6); justify-items: center; text-align: center; }
.landing__contact-row { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--s-3); }

/* Одна плашка на всё — и контакты, и соцсети: иконка + название ресурса.
   Раньше соцсети были маленькими кружками без подписи, но иконку соцсети
   узнают не все, а название убирает всякую неоднозначность. Раскладывать
   их в два отдельных ряда тоже перестали: выглядят одинаково, а ряды по
   одному и по два элемента читались как случайный перенос, а не как замысел. */
.contact-pill {
  display: inline-flex; align-items: center; gap: var(--s-3);
  padding: 0 var(--s-6); height: 56px;
  border-radius: var(--r-pill);
  border: 1.5px solid var(--border-2);
  background: var(--surface);
  color: var(--text-1);
  text-decoration: none;
  font-size: var(--t-15);
  font-weight: 600;
  box-shadow: var(--shadow-1);
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast), transform var(--t-fast);
}
.contact-pill__icon { font-size: var(--t-21); line-height: 1; }
/* Фирменный логотип вместо эмодзи. currentColor — значок красится тем же
   цветом, что и подпись рядом, и меняется вместе с ней при наведении. */
.contact-pill__logo { width: 22px; height: 22px; flex: none; fill: currentColor; }
.contact-pill:hover {
  background: var(--action-quiet); border-color: var(--action); color: var(--action-ink);
  transform: translateY(-1px);
}

/* ── Страница «Полезные материалы» ───────────────────────────────────────── */
.materials__intro { text-align: center; max-width: 60ch; margin: 0 auto; color: var(--text-2); font-size: var(--t-15); }
.materials__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-6);
}
.material-card { display: flex; flex-direction: column; gap: var(--s-3); }
.material-card__icon {
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px;
  border-radius: var(--r-md);
  background: var(--active-quiet);
  color: var(--active-ink);
}
.material-card h3 { font-size: var(--t-16); color: var(--text); }
.material-card p { flex: 1; font-size: var(--t-13); color: var(--text-2); }
.material-card .btn { align-self: flex-start; }

/* ── Тренажёр ────────────────────────────────────────────────────────────── */
.runner {
  display: grid;
  gap: var(--s-6);
  max-width: var(--max-content);
  margin: 0 auto;
}

.runner__bar {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-3);
}

.progress {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.progress__fill {
  height: 100%;
  width: var(--fill, 0%);
  background: linear-gradient(90deg, var(--ph-09), var(--ph-11));
  transition: width var(--t-mid);
}

.prompt {
  font-family: var(--font-display);
  font-size: var(--t-21);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: var(--lh-snug);
}

.prompt img {
  max-width: 100%;
  border-radius: var(--r-md);
  margin-top: var(--s-4);
  border: 1px solid var(--border);
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-2px); }
  75% { transform: translateX(2px); }
}

/* Осадок: награда за верный ответ — кристаллы выпадают из варианта */
.precipitate {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}

.shard {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 7px;
  height: 7px;
  background: var(--right);
  clip-path: polygon(50% 0, 100% 38%, 78% 100%, 22% 100%, 0 38%);
  opacity: 0;
  animation: fall 620ms cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}

@keyframes fall {
  0% { opacity: 1; transform: translate(0, 0) scale(0.4) rotate(0deg); }
  100% {
    opacity: 0;
    transform: translate(var(--dx), var(--dy)) scale(1) rotate(var(--dr));
  }
}

@media (prefers-reduced-motion: reduce) {
  .shard,
  .option[data-verdict='wrong'] { animation: none; }
}

/* Обратная связь после ответа */
.verdict {
  display: grid;
  gap: var(--s-3);
  padding: var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface);
  animation: rise var(--t-mid) both;
}

.verdict[data-correct='true'] { border-color: var(--right); }
.verdict[data-correct='false'] { border-color: var(--wrong); }

.verdict__head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: var(--t-18);
  font-weight: 600;
}

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

.explain {
  font-size: var(--t-13);
  line-height: var(--lh-body);
  color: var(--text-1);
}

.explain img {
  max-width: 100%;
  border-radius: var(--r-sm);
  margin-top: var(--s-2);
}

.explain p { margin: var(--s-2) 0; }


/* Осадок выпадает в разборе ответов, а не сразу после выбора: правильность у
   нас становится известна только после сдачи — ключи ответов до этого момента
   в браузер не приходят (см. /api/questions). Смысл тот же, что в исходнике —
   награда за верный ответ, — просто момент другой. */
.review-opt { position: relative; }

.resume {
  display: grid;
  gap: var(--s-3);
  transition: border-color var(--t-fast);
}

.resume:hover { border-color: var(--border-3); }

.resume__meta {
  display: flex;
  gap: var(--s-2);
  align-items: baseline;
  font-size: var(--t-13);
  color: var(--text-3);
}

.resume__xp { margin-left: auto; color: var(--xp-ink); }


/* Утилиты исходника, которых у нас ещё не было */
.tech {
  font-size: var(--t-12);
  font-weight: 600;
  color: var(--text-3);
}

.disclosure { cursor: pointer; font-size: var(--t-15); font-weight: 600; }
.crumb { margin-top: var(--s-1); font-size: var(--t-13); color: var(--text-3); }
.page--narrow { max-width: var(--max-content); margin: 0 auto; }

/* Конспект в исходнике зовётся .sheet; у нас исторически .lesson-card.
   Оставляем оба имени, чтобы разметку можно было писать как в образце. */
.sheet {
  background: var(--sheet);
  color: var(--sheet-text);
  border: 1px solid var(--sheet-edge);
  border-radius: var(--r-lg);
  padding: var(--s-8) var(--s-6);
  box-shadow: var(--shadow-2);
}

/* Место пропавшей иллюстрации: файла нет, а битую картинку показывать нельзя */
.missing-figure {
  display: grid;
  place-items: center;
  margin: var(--s-4) 0;
  padding: var(--s-6);
  min-height: 96px;
  border: 1px dashed var(--border-3);
  border-radius: var(--r-md);
  background: repeating-linear-gradient(-45deg, var(--border-2) 0 8px, transparent 8px 16px);
}
.missing-figure figcaption { font-size: var(--t-13); color: var(--text-3); }

/* Нижняя панель разделов: только на телефоне */
.tabbar {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-2) calc(var(--s-1) + env(safe-area-inset-bottom));
  background: var(--scrim);
  backdrop-filter: blur(14px) saturate(1.4);
  border-top: 1px solid var(--border);
}

.tabbar__link {
  flex: 1;
  padding: var(--s-3) var(--s-2);
  border-radius: var(--r-md);
  text-align: center;
  font-size: var(--t-13);
  font-weight: 600;
  color: var(--text-2);
  transition: background var(--t-fast), color var(--t-fast);
}

.tabbar__link[aria-current='page'] {
  background: var(--action-quiet);
  color: var(--text);
}

@media (max-width: 620px) {
  .nav__exitlabel { display: none; }
  .nav__exiticon { display: block; }
  .nav__exit { padding: 0 var(--s-2); }
}


/* Кнопка выхода в шапке: на узком экране от неё остаётся значок. */
.nav__exiticon { display: none; }
@media (max-width: 620px) {
  .nav__exitlabel { display: none; }
  .nav__exiticon { display: block; }
  .nav__exit { padding: 0 var(--s-2); }
}

/* Моноширинная метка внутри значковой кнопки (код языка, номер) */
.iconbtn__code { font-family: var(--font-mono); font-size: var(--t-12); font-weight: 500; }

/* Панель показывается только там, где её включили классом на body: на страницах
   урока и тренажёра снизу и так есть свои кнопки, а вторая панель поверх них
   съела бы экран. */
@media (max-width: 620px) {
  body.has-tabbar .tabbar { display: flex; }
  body.has-tabbar { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
}

/* ── Кнопка генерации пароля ─────────────────────────────────────────────── */
/* Во всю ширину формы — как «Зарегистрироваться» под ней (.btn--wide).
   Раньше стояло justify-self: start, и кнопка обрезалась по своему тексту:
   две соседние кнопки одна под другой были разной ширины и смотрелись
   как случайно съехавшие. */
.auth__suggest { justify-self: stretch; }

/* Поля, живущие только в регистрации, раскрываются плавно (grid 0fr → 1fr) */
.auth__extra {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--t-mid), opacity var(--t-mid);
}

.auth__extra[data-open='true'] {
  grid-template-rows: 1fr;
  opacity: 1;
}

.auth__extra-inner {
  overflow: hidden;
  min-height: 0;
  display: grid;
}

/* Прячем обводку фокуса, пока блок схлопнут */
.auth__extra:not([data-open='true']) .auth__extra-inner { visibility: hidden; }


/* ── Сетка профиля 2×2: карточка, достижения, активность, Моль ───────────── */
.profgrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-4);
  align-items: stretch;
}

/* Колонки одной высоты: нижняя карточка добирает остаток, низ выровнен */
.profgrid__col {
  display: grid;
  gap: var(--s-4);
  grid-template-rows: auto 1fr;
  min-width: 0;
}

/* Разрешение сжиматься нужно на КАЖДОМ уровне: min-width: auto стоит у
   элемента грида по умолчанию, и одного разрешения на колонке не хватает.
   Карточка с календарём требовала 449px в колонке шириной 426 — вылезала
   наружу и заезжала под соседнюю колонку. Теперь календарь прокручивается
   внутри себя, как и задумано его собственным overflow-x. */
.profgrid__col > *,
.profgrid__col > * > * { min-width: 0; }

/* Дотянутая карточка распределяет контент, а не жмёт его к верху */
.profgrid__col > :last-child {
  display: grid;
  align-content: space-between;
}

/* Карточка с единственным блоком (Моль) — по центру */
.profgrid__col > :last-child:has(> :only-child) {
  align-content: center;
}

/* В полуширинной колонке достижения ужимаются до двух столбцов */
.profgrid .badges {
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}

@media (max-width: 900px) {
  .profgrid { grid-template-columns: 1fr; }
}

/* ── Достижения ──────────────────────────────────────────────────────────── */
.badges {
  display: grid;
  gap: var(--s-2);
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}

.badge {
  display: grid;
  grid-template-columns: 40px 1fr;
  gap: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface);
}

.badge[data-unlocked='true'] { border-color: var(--xp); }

.badge__icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-3);
}

.badge[data-unlocked='true'] .badge__icon {
  background: var(--xp-quiet);
  color: var(--xp-ink);
}

.badge__name { font-size: var(--t-13); font-weight: 600; }
.badge__hint { font-size: var(--t-12); color: var(--text-3); }

.badge__track {
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--border);
  margin-top: var(--s-1);
  overflow: hidden;
}

.badge__fill {
  height: 100%;
  width: var(--fill, 0%);
  background: var(--xp);
  transition: width var(--t-mid);
}


/* ── Тепловая карта активности: колонка — неделя, строка — день ──────────── */
.heatmap {
  --heat-cell: 13px;
  --heat-gap: 3px;
  margin-top: var(--s-3);
  font-size: var(--t-12);
  color: var(--text-3);
}

.heatmap__scroll {
  overflow-x: auto;
  padding-bottom: var(--s-1);
}

/* Подписи месяцев выровнены по тем же колонкам-неделям, что и клетки. */
.heatmap__months {
  display: grid;
  grid-template-columns: repeat(26, var(--heat-cell));
  gap: var(--heat-gap);
  margin-left: calc(28px + var(--s-2));
  margin-bottom: var(--s-1);
  white-space: nowrap;
}

.heatmap__body {
  display: flex;
  gap: var(--s-2);
}

.heatmap__days {
  display: grid;
  grid-template-rows: repeat(7, var(--heat-cell));
  gap: var(--heat-gap);
  width: 28px;
  text-align: right;
}

.heat {
  display: flex;
  gap: var(--heat-gap);
}

.heat__week {
  display: grid;
  grid-template-rows: repeat(7, var(--heat-cell));
  gap: var(--heat-gap);
}

.heat__cell {
  width: var(--heat-cell);
  height: var(--heat-cell);
  border-radius: 3px;
  background: var(--surface-2);
}

.heat__cell[data-level='1'] { background: var(--active-quiet); }
.heat__cell[data-level='2'] { background: var(--active); opacity: 0.55; }
.heat__cell[data-level='3'] { background: var(--right); opacity: 0.75; }
.heat__cell[data-level='4'] { background: var(--right); }
/* Раньше будущие дни прятались (visibility:hidden) — это было рассчитано на
   скользящее окно, где «будущее» — максимум пара дней текущей недели.
   Теперь диапазон фиксированный (август—декабрь) и специально показывает
   ещё не наступившие месяцы — прячь их, и от них ничего не останется.
   Пустая (level 0) ячейка и так выглядит одинаково для «нет активности» и
   «ещё не наступило» — отдельного вида не делаем, различать не к чему. */

.heatmap__legend {
  display: flex;
  align-items: center;
  gap: var(--heat-gap);
  margin-top: var(--s-2);
  justify-content: flex-end;
}

.heatmap__legend span { margin: 0 var(--s-1); }
.heatmap__legend .heat__cell { display: inline-block; }


.picker { position: relative; }

.picker__menu {
  position: absolute;
  top: calc(100% + var(--s-2));
  right: 0;
  z-index: 49;
  margin: 0;
  padding: var(--s-1);
  list-style: none;
  min-width: 160px;
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-2);
  transform-origin: top right;
  opacity: 0;
  transform: translateY(-4px) scale(0.97);
  visibility: hidden;
  transition: opacity var(--t-fast), transform var(--t-fast), visibility var(--t-fast);
}

.picker__menu[data-open='true'] {
  opacity: 1;
  transform: none;
  visibility: visible;
}

.picker__item {
  display: block;
  width: 100%;
  padding: var(--s-2) var(--s-3);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  text-align: left;
  font-size: var(--t-13);
  color: var(--text-1);
  transition: background var(--t-fast), color var(--t-fast);
}

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

/* Пункт меню — такая же цель нажатия, как кнопка */
@media (pointer: coarse) { .picker__item { min-height: 44px; } }
.picker__item[aria-selected='true'] { background: var(--action-quiet); color: var(--action-ink); }


/* ── Модальное окно ──────────────────────────────────────────────────────────
   В исходнике модалок не было — собрано на тех же токенах. */
.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100;
  padding: var(--s-4);
  background: rgb(15 22 38 / 0.45);
  justify-content: center;
  align-items: center;
  overflow-y: auto;
}

.modal-overlay.active { display: flex; }

.modal {
  width: 440px;
  max-width: 100%;
  display: grid;
  gap: var(--s-4);
  padding: var(--s-6);
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-2);
}

.modal__title { font-size: var(--t-18); font-weight: 600; letter-spacing: -0.01em; }
.modal__btns { display: flex; gap: var(--s-2); justify-content: flex-end; flex-wrap: wrap; }

/* ── Формы ───────────────────────────────────────────────────────────────── */
.field { display: grid; gap: var(--s-2); }

.field__label {
  font-size: var(--t-13);
  font-weight: 500;
  color: var(--text-2);
}

.input,
.textarea,
.select {
  width: 100%;
  min-height: 44px;
  padding: var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--border-2);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  transition: border-color var(--t-fast), background var(--t-fast);
}

/* Список: свой шеврон вместо системного, пункты в цветах темы.
   ОТЛИЧИЕ ОТ ИСХОДНИКА: у нас основная тема светлая, поэтому по умолчанию
   стоит тёмный шеврон, а светлый — под тёмной темой (в оригинале наоборот). */
.select {
  appearance: none;
  padding-right: 38px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1.5 1.75 6 6.25l4.5-4.5' stroke='%234d5a73' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  cursor: pointer;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1.5 1.75 6 6.25l4.5-4.5' stroke='%2397a5c0' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
}

:root[data-theme='dark'] .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1.5 1.75 6 6.25l4.5-4.5' stroke='%2397a5c0' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

.select:hover { border-color: var(--border-3); background-color: var(--surface-3); }
.select option { background: var(--bg-elevated); color: var(--text); }

.input:hover,
.textarea:hover { border-color: var(--border-3); }

.input:focus,
.textarea:focus,
.select:focus {
  border-color: var(--action);
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

.textarea {
  min-height: 120px;
  font-family: var(--font-mono);
  font-size: var(--t-13);
  line-height: 1.55;
  resize: vertical;
}

.alert {
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--wrong);
  background: var(--wrong-quiet);
  color: var(--wrong-ink);
  font-size: var(--t-13);
}

.alert--ok {
  border-color: var(--right);
  background: var(--right-quiet);
  color: var(--right-ink);
}

/* Место под сообщение об ошибке обычно висит в разметке заранее и заполняется
   скриптом. Пустая рамка на его месте выглядит как поломка, поэтому пустой
   .alert не показываем вовсе. */
.alert:empty { display: none; }

/* ── Вход ────────────────────────────────────────────────────────────────── */
.auth {
  position: relative;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--s-6);
}

/* Угол настроек: тема (и язык, когда он появится) — над карточкой входа */
.auth__prefs {
  position: absolute;
  top: var(--s-4);
  right: var(--s-4);
  display: flex;
  gap: var(--s-2);
  align-items: center;
}

.auth__card {
  width: 100%;
  max-width: 420px;
  display: grid;
  gap: var(--s-4);
  padding: var(--s-8);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-2);
}

.auth__tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-1);
  padding: var(--s-1);
  border-radius: var(--r-btn);
  background: var(--bg);
  border: 1px solid var(--border);
}

.auth__tab {
  padding: var(--s-2);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--text-2);
  font: inherit;
  font-size: var(--t-13);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}

.auth__tab[aria-selected='true'] { background: var(--action); color: var(--on-action); }

/* Вкладка «Вход/Регистрация» — первая цель нажатия на странице */
@media (pointer: coarse) { .auth__tab { min-height: 44px; } }

/* ── Пустые состояния ────────────────────────────────────────────────────── */
.empty {
  display: grid;
  gap: var(--s-3);
  place-items: center;
  padding: var(--s-12) var(--s-6);
  text-align: center;
  border-radius: var(--r-lg);
  border: 1px dashed var(--border-2);
  background: var(--surface);
}

.spinner {
  width: 28px;
  height: 28px;
  border-radius: var(--r-pill);
  border: 2px solid var(--border-2);
  border-top-color: var(--action);
  animation: iq-spin 720ms linear infinite;
  margin: var(--s-12) auto;
}

@keyframes iq-spin { to { transform: rotate(360deg); } }

/* Уважаем системную настройку «меньше движения»: длинные заливки и вращение
   раздражают тех, кому от них плохо, а смысла не несут. */
@media (prefers-reduced-motion: reduce) {
  .level__fill { transition: none; }
  .spinner { animation-duration: 2s; }
}
