/* ============================================================
   RETRO X7 — sections
   ============================================================ */

/* ============================================================
   HEADER — ТЗ §6
   ============================================================ */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-header);
  height: var(--header-h);
  display: flex;
  align-items: center;
  border-bottom: 1px solid transparent;
  transition: background var(--d-base) var(--e-out),
              border-color var(--d-base) var(--e-out),
              height var(--d-base) var(--e-out);
}
.header.is-compact {
  height: 62px;
  background: rgba(8, 8, 10, 0.82);
  backdrop-filter: blur(14px);
  border-bottom-color: var(--c-line);
}

.header__row { display: flex; align-items: center; gap: var(--s-5); width: 100%; }

.brand { display: inline-flex; align-items: center; gap: 10px; flex: none; }
.brand__logo { width: 34px; height: 34px; }
.brand__text { display: grid; line-height: 1.1; }
.brand__club {
  font-size: var(--t-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-text-3);
}
/* Знак наследует цвет подписи — отдельного тона у него нет, иначе он стал бы
   третьей точкой внимания рядом со знаком бренда и кнопкой. */
.proof__counter-icon {
  inline-size: 1.15em;
  block-size: 1.15em;
  flex: none;
  color: inherit;
}
.brand__server { font-family: var(--f-display); font-variant-numeric: var(--fvn-display); font-size: var(--t-base); letter-spacing: 0.04em; }

.nav { margin-inline: auto; }
.nav__list { display: flex; align-items: center; gap: 4px; }
.nav__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 8px 14px;
  border-radius: var(--r-pill);
  font-size: var(--t-sm);
  color: var(--c-text-2);
  transition: color var(--d-fast) var(--e-out), background var(--d-fast) var(--e-out);
}
.nav__link:hover { color: var(--c-text); background: rgba(244,242,239,0.06); }
.nav__link[aria-current='true'] { color: var(--c-accent); }

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

/* ============================================================
   Общий приём: игровой кадр как фон секции (правка §8)
   Кадр всегда уходит под затемняющий слой, чтобы текст читался.
   ============================================================ */

/* Кадр растворяется на верхней и нижней кромке, поэтому соседние секции
   перетекают друг в друга, а не стыкуются прямоугольным швом. */
/* Фоновый план не совпадает с границами секции: он выходит за них на
   --cam-bleed и ровно на этой длине проявляется и гаснет. Соседние планы
   перекрываются, и стыка не существует физически — раньше каждая секция
   красила собственный прямоугольник, и на границе была ступенька по тону,
   которую не спрятать никакой анимацией. */
.scene {
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--cam-bleed) * -1);
  bottom: calc(var(--cam-bleed) * -1);
  z-index: 0;
  overflow: hidden;
  /* Сцена декоративна и не должна ловить курсор. Без этого её вынос
     (--cam-bleed) заходит на предыдущую секцию и, поскольку следующая
     секция рисуется поверх, перехватывает клики по всему, что оказалось
     рядом с границей: кнопка подарков переставала нажиматься. */
  pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
}
.scene__img { width: 100%; height: 100%; object-fit: cover; }
.scene::after {
  content: '';
  position: absolute; inset: 0;
  background:
    linear-gradient(90deg, rgba(6,6,8,0.96) 0%, rgba(6,6,8,0.88) 34%, rgba(6,6,8,0.55) 64%, rgba(6,6,8,0.38) 100%),
    linear-gradient(0deg, rgba(6,6,8,0.96) 0%, rgba(6,6,8,0.25) 46%, rgba(6,6,8,0.55) 100%);
}
/* Вариант с равномерным затемнением — там, где контент занимает всю ширину */
.scene--even::after {
  background:
    linear-gradient(0deg, rgba(6,6,8,0.97) 0%, rgba(6,6,8,0.82) 42%, rgba(6,6,8,0.86) 100%);
}

.section {
  position: relative;
  padding-block: var(--section-y);
  isolation: isolate;
}
.section > .wrap { position: relative; z-index: var(--z-content); }
.section__head { display: grid; gap: var(--s-3); margin-bottom: var(--s-7); max-width: 780px; }
/* Одна секция из шести выстроена по центральной оси — иначе страница читается
   как один длинный левый столбец без смены ритма. */
.section__head--center { justify-items: center; text-align: center; margin-inline: auto; max-width: 62ch; }

/* ============================================================
   HERO — ТЗ §7
   ============================================================ */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;          /* текст ложится в нижнюю треть: там пусто и темно */
  /* Снизу оставляем место под индикатор, чтобы текст не прижимался к краю */
  padding: var(--header-h) 0 96px;
  /* Именно clip, а не hidden: hidden делает элемент контейнером прокрутки,
     и animation-timeline: view() у всего, что внутри, замеряет неподвижный
     бокс — вход в монитор просто не запускался. */
  overflow: clip;
  /* К нижней кромке фон уходит в прозрачность, а не в сплошной тон:
     сквозь неё проявляется фоновый план следующей секции, и получается
     перекрёстное растворение вместо ступеньки. */
  background: linear-gradient(180deg,
    var(--c-void) 0%, var(--c-void) 56%, transparent 100%);
  isolation: isolate;
}

.hero__scene { position: absolute; inset: 0; z-index: var(--z-scene); overflow: clip; }

/* Кадр комнаты держит свою пропорцию и всегда перекрывает вьюпорт,
   поэтому слой экрана задаётся в процентах один раз и не расползается. */
/* Кадр показывается по всей ширине и прижат к верху: комната видна целиком —
   шкаф, полки, плакат и монитор, — а текст ложится ниже, на затемнённую
   часть сцены. Кроп по ширине убил бы половину обстановки. */
.hero__frame {
  --scene-ar: 2560 / 1087;
  position: absolute;
  top: 0; left: 50%;
  translate: -50% 0;
  width: 100%;
  height: auto;
  aspect-ratio: var(--scene-ar);
}
/* Кадр ниже вьюпорта обрывается — гасим его нижнюю кромку в фон страницы,
   иначе на светлых участках (окно, лампа) виден шов. */
/* Кадр комнаты растворяется собственной маской. Маска героя до него
   не достаёт: она гасит последние --cam-bleed высоты секции, а кадр
   заканчивается выше — отсюда и был жёсткий нижний край фотографии. */
.hero__frame {
  /* Гасим только самую кромку. На 58% растворение съедало стол и монитор —
     главные объекты кадра; задача была убрать обрез, а не кадр. */
  -webkit-mask-image: linear-gradient(180deg, #000 87%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 87%, transparent 100%);
}
.hero__frame::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 26%;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(8,8,10,0) 0%, rgba(8,8,10,0.7) 62%, rgba(8,8,10,0.88) 100%);
}

/* Ночной кадр сам по себе тёмный, а поверх ещё ложится затемнение под текст.
   Поднимаем яркость самой комнаты, чтобы обстановка читалась. */
.hero__room {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: brightness(1.34) contrast(0.94) saturate(1.08);
}

/* Экран монитора — отдельный слой. Внутри крутится РЕАЛЬНЫЙ игровой
   ролик TheBattleClub (ТЗ §7.1). Координаты — из config.hero.screenRect. */
.hero__screen {
  position: absolute;
  top: var(--screen-top, 39.6%);
  left: var(--screen-left, 70.1%);
  width: var(--screen-w, 14.3%);
  height: var(--screen-h, 27.8%);
  overflow: clip;   /* см. .hero — hidden ломает view() у вложенных слоёв */
  /* Форму задаёт clip-path по реальным углам стекла (js/hero.js).
     Свечение выносим на отдельный слой: clip-path обрезал бы тень. */
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

/* Свет люминофора на рамке монитора — под вырезанным слоем экрана */
.hero__screen-glow {
  position: absolute;
  top: var(--screen-top, 38%);
  left: var(--screen-left, 70.3%);
  width: var(--screen-w, 12.5%);
  height: var(--screen-h, 27.5%);
  z-index: 0;
  pointer-events: none;
  /* Свечение — мягкий ореол, а не подсвеченный прямоугольник: у резкого
     края видна форма слоя, особенно на светлом пластике рамки. */
  border-radius: 12%;
  box-shadow:
    0 0 26px 4px rgba(120, 170, 225, 0.13),
    0 0 74px 16px rgba(90, 140, 200, 0.08);
  filter: blur(6px);
}

/* Ролик снят «в игре» и по резкости и цвету не совпадает с фотографией
   комнаты — из-за этого он и выглядел наклейкой. Подгоняем под кадр:
   гасим контраст и насыщенность, добавляем микрорасфокус и холодный сдвиг. */
.hero__screen-media {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: saturate(0.95) contrast(0.97) brightness(1.02) blur(0.25px) hue-rotate(-4deg);
  /* Доворот под наклон монитора: горизонт в кадре должен совпадать
     с верхней гранью стекла. Масштаб компенсирует углы после поворота. */
  transform: rotate(var(--screen-rotate, 0deg)) scale(var(--screen-media-scale));
}
/* Портретный кадр: монитор развёрнут на 11°, поворот на столько же уводит
   углы ролика внутрь стекла — под ними открывалась бы рамка. */
@media (max-aspect-ratio: 1/1) {
  .hero__screen-media { --screen-media-scale: 1.26; }
}

/* Смена роликов — затемнением в духе телепорта Lineage 2: короткая
   холодная вспышка, уход в темноту, под ней подменяется источник,
   затем картинка проявляется снова. */
.hero__screen-flash {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  background: #04070c;
  /* Проявление медленнее ухода — так переход читается плавным,
     а не морганием. */
  transition: opacity var(--tp-in, 1000ms) cubic-bezier(0.33, 0, 0.2, 1);
}
.hero__screen.is-teleport .hero__screen-flash {
  animation: tpDark var(--tp-out, 750ms) cubic-bezier(0.4, 0, 0.5, 1) forwards;
}
@keyframes tpDark {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Холодная вспышка: разгорается и мягко тонет в темноте — перенос в Lineage 2 */
.hero__screen-flash::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  background: radial-gradient(125% 95% at 50% 45%,
    rgba(214, 240, 255, 0.92) 0%, rgba(120, 180, 235, 0.55) 44%, transparent 78%);
}
.hero__screen.is-teleport .hero__screen-flash::after {
  animation: tpFlash var(--tp-out, 750ms) ease-in-out forwards;
}
@keyframes tpFlash {
  0%   { opacity: 0; }
  38%  { opacity: 1; }
  70%  { opacity: 0.45; }
  100% { opacity: 0; }
}

/* Пока экран тёмный, само видео приглушено — подмена не мелькает */
.hero__screen-media { transition: opacity var(--tp-in, 1000ms) cubic-bezier(0.33, 0, 0.2, 1); }
.hero__screen.is-teleport .hero__screen-media {
  opacity: 0.08;
  transition: opacity var(--tp-out, 750ms) ease-in;
}

/* Стекло: затемнение к краям, блик и общая тень внутрь. */
.hero__screen::after {
  content: '';
  position: absolute; inset: 0;
  pointer-events: none;
  background:
    linear-gradient(128deg, rgba(214, 232, 255, 0.16) 0%, rgba(214, 232, 255, 0.05) 26%, transparent 46%),
    radial-gradient(125% 105% at 50% 45%, transparent 52%, rgba(4, 8, 14, 0.55) 100%);
  box-shadow:
    inset 0 0 12px rgba(2, 5, 9, 0.85),
    inset 0 1px 2px rgba(2, 5, 9, 0.9);
}

.hero__crt {
  position: absolute; inset: 0;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0,0,0,0.20) 0px, rgba(0,0,0,0.20) 1px,
    rgba(255,255,255,0.02) 1px, rgba(255,255,255,0.02) 3px);
  mix-blend-mode: multiply;
  animation: crtFlicker 5.5s steps(24) infinite;
}
@keyframes crtFlicker {
  0%, 96%, 100% { opacity: 0.85; }
  97% { opacity: 0.72; }
  98% { opacity: 0.92; }
}
.hero__screen--empty {
  background: radial-gradient(120% 90% at 50% 40%, rgba(120,180,235,0.16), rgba(10,20,34,0.10) 70%);
}

/* ---------- Дождь по стеклу окна ----------
   Слой лежит внутри .hero__frame, поэтому позиция в процентах привязана
   к самому кадру комнаты и не разъезжается при смене ширины.
   Маска гасит потоки там, где перед окном стоят монитор и лампа —
   вода не должна течь поверх них. */
.hero__glass {
  position: absolute;
  top: var(--g-top);
  left: var(--g-left);
  width: var(--g-w);
  height: var(--g-h);
  /* z-index здесь ставить нельзя: он создаёт слой изоляции, и mix-blend-mode
     у видео смешивается с пустотой внутри створки, а не с кадром комнаты —
     вместо подсветки водой окно просто темнеет. Порядок и так верный:
     створки идут после картинки комнаты по дереву. */
  overflow: hidden;   /* жёсткая граница створки: вода не выходит за стекло */
  pointer-events: none;
  /* Наложение стоит на обёртке, а не на самом холсте: Chrome выносит canvas
     и video в отдельный композиторный слой, где mix-blend-mode игнорируется,
     и слой ложится непрозрачным прямоугольником. На обычном div работает. */
  mix-blend-mode: plus-lighter;
}
/* Координаты сняты по фотографии кадра 2560×1087 */
.hero__glass--a { --g-left: 72.15%; --g-top: 0%;     --g-w: 9.15%;  --g-h: 33.60%; }
.hero__glass--b { --g-left: 85.40%; --g-top: 0%;     --g-w: 10.50%; --g-h: 10.40%; }
.hero__glass--c { --g-left: 89.34%; --g-top: 35.42%; --g-w: 5.31%;  --g-h: 17.94%; }
/* Полосу слева от лампы не заводим: замер показал, что там оконный
   переплёт, а стекло начинается уже под самой лампой. Створка на раме —
   ровно то, что запрещает §8. */

/* Края створки гасим маской по обеим осям: без этого слой читается
   прямоугольной заплаткой на стекле. Пересечение двух градиентов даёт
   мягкую рамку со всех четырёх сторон. */
.hero__glass-canvas {
  -webkit-mask-image:
    linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-image:
    linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 7%, #000 93%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* Узкий просвет справа от монитора: там воды должно быть меньше,
   иначе пятно спорит с лампой. */
.hero__glass--c .hero__glass-canvas { --glass-opacity: 0.5; }

/* Слой воды: сгенерированная петля «капли на стекле» на чёрном фоне.
   Рисуется в canvas, а не в <video>, потому что Chrome выносит видео
   в отдельный композиторный слой и там mix-blend-mode просто не работает —
   вместо подсветки водой окно темнело. Холст такой оптимизации не подвержен. */
.hero__glass-canvas {
  position: absolute;
  inset: -8%;
  width: 116%;
  height: 116%;
  opacity: var(--glass-opacity, 0.6);
  /* Кадр снят фронтально, стекло в сцене уходит в перспективу —
     лёгкий наклон подгоняет направление стока под сцену.
     Масштаба здесь нет намеренно: увеличение раздувало капли. */
  transform: rotate(var(--glass-tilt, 0deg));
  pointer-events: none;
}

/* Общий источник кадров: не показывается, только декодирует */
.hero__glass-source {
  position: absolute;
  width: 2px; height: 2px;
  opacity: 0;
  pointer-events: none;
}

/* Затемнение снизу — под текст, но плакат и полки остаются читаемыми */
/* Затемняем ровно ту полосу, где лежит текст, а саму комнату почти не трогаем. */
/* Нижняя кромка фоновых слоёв героя растворяется — сквозь неё проявляется
   фоновый план стадий. Маска висит на фоне, а не на всей секции: иначе
   вместе с фоном гаснет и контент, включая индикатор «листай ниже». */
.hero__scene, .hero__veil {
  -webkit-mask-image: linear-gradient(180deg,
    #000 calc(100% - var(--cam-bleed)), transparent 100%);
  mask-image: linear-gradient(180deg,
    #000 calc(100% - var(--cam-bleed)), transparent 100%);
}
.hero__veil {
  position: absolute; inset: 0;
  z-index: 2;
  background:
    linear-gradient(0deg,
      rgba(6,6,8,0.98) 0%,
      rgba(6,6,8,0.92) 26%,
      rgba(6,6,8,0.55) 42%,
      rgba(6,6,8,0.10) 62%,
      rgba(6,6,8,0.04) 100%),
    linear-gradient(90deg, rgba(6,6,8,0.42) 0%, rgba(6,6,8,0.06) 40%, rgba(6,6,8,0) 100%);
}

/* Контент занимает всю ширину контейнера: заголовок и кнопки слева,
   таймер — отдельным блоком справа. Раньше одна узкая колонка стояла
   посреди пустоты. */
.hero__content {
  position: relative;
  z-index: var(--z-content);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--s-6) var(--s-8);
}

/* Воздух между смысловыми блоками важнее плотности: заголовок, слоган,
   текст и кнопки разведены, но выровнены по одной левой оси. */
.hero__lead {
  display: grid;
  gap: var(--s-5);
  justify-items: start;
  max-width: 46ch;
}
.hero__side { display: grid; gap: var(--s-3); justify-items: end; }

/* Кнопки одной высоты, текст по центру, между ними ровный отступ */
.hero__cta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: stretch;
  margin-top: var(--s-2);
}
.hero__cta .btn { min-height: 52px; padding-inline: 30px; }

/* ---------- Индикатор продолжения страницы ---------- */
/* ТЗ §10: вторичная подсказка, а не кнопка — тонкая нить сверху,
   подпись и двойной шеврон, который медленно уходит вниз по нити. */
.hero__scroll {
  position: absolute;
  left: 50%; bottom: 22px;
  translate: -50% 0;
  z-index: var(--z-content);
  display: grid;
  justify-items: center;
  gap: 6px;
  padding: 10px 18px 6px;
  border-radius: var(--r-pill);
  color: var(--c-text-2);
  transition: color var(--d-fast) var(--e-out);
}
.hero__scroll:hover { color: var(--c-text); }
.hero__scroll::before {
  content: '';
  width: 1px; height: 26px;
  background: linear-gradient(180deg, transparent, var(--c-line-strong));
}
.hero__scroll-text {
  font-size: var(--t-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 500;
}
.hero__scroll-chevrons { display: grid; justify-items: center; height: 18px; }
.hero__scroll-chevrons svg { width: 20px; height: 12px; grid-area: 1 / 1; }
.hero__scroll-chevrons svg:first-child { animation: chev 2.1s var(--e-inout) infinite; }
.hero__scroll-chevrons svg:last-child  { animation: chev 2.1s var(--e-inout) infinite .28s; opacity: 0.45; }
@keyframes chev {
  0%   { transform: translateY(-3px); opacity: 0; }
  35%  { opacity: 1; }
  100% { transform: translateY(8px);  opacity: 0; }
}

.hero__title {
  font-family: var(--f-display); font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: clamp(2.5rem, 5.4vw, 4.75rem);
  line-height: 0.94;
  letter-spacing: -0.02em;
}
.hero__title sup { font-size: 0.72em; vertical-align: 0.14em; letter-spacing: 0; margin-left: 0.06em; }
.hero__slogan {
  font-size: clamp(1.125rem, 2vw, 1.625rem);
  font-weight: 500;
  line-height: var(--lh-snug);
  max-width: 26ch;
}
.hero__sub { color: var(--c-text-2); max-width: 42ch; font-size: var(--t-sm); }
/* .hero__thesis остаётся только как утилитарный мелкий текст в форме
   регистрации; из первого экрана блок удалён. */
.hero__thesis { font-size: var(--t-caption); letter-spacing: 0.06em; color: var(--c-text-3); }

.hero__sound { position: absolute; right: var(--gutter); bottom: 18px; z-index: var(--z-content); }

/* Первый экран прокручивается целиком: видео остаётся внутри монитора. */

/* ============================================================
   СТАДИИ — ТЗ §8, правка §3: без scroll-lock
   Секция занимает один экран, стадии переключаются точками,
   стрелками и кнопками. Пользователь в любой момент прокручивает дальше.
   ============================================================ */

.stages {
  position: relative;
  /* overflow: hidden убран намеренно: он обрезал вынос фонового плана
     за границы секции, и стык с героем оставался жёстким. Карта и так
     клипается собственным окном .stages__mapwin. */
  isolation: isolate;
  padding-block: var(--section-y);
}

/* Окно карты — обычный блок в потоке. Текст стадии идёт под ним, поэтому
   карта больше не воюет с описанием за одно и то же место. */
.stages__mapwin {
  position: relative;
  height: clamp(300px, 44svh, 500px);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.stages__scenes {
  /* Слой уходит в край экрана, а по вертикали держится границ окна. */
  position: absolute;
  top: 0; bottom: 0;
  left: 50%; width: 100vw;
  transform: translateX(-50%);
  z-index: 0;
  overflow: hidden;
  background: #05070b;
}
/* ============================================================
   СТАДИИ: карта, маршрут, точки, панель

   Секция собрана как одна сцена: игровой кадр стадии в глубине,
   поверх него карта с маршрутом, снизу панель. Ничего не «чёрный фон
   с текстом» — слои связаны светом и градиентом, а не поставлены встык.
   ============================================================ */

/* --- Фон: настоящий игровой кадр активной стадии --- */
.stages__backdrop {
  /* Своя температура секции: карта на пергаменте, поэтому тон тёплый. */
  --c-backdrop-tone: var(--scene-tint-map);
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--cam-bleed) * -1);
  bottom: calc(var(--cam-bleed) * -1);
  z-index: 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
}
.stages__bg-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  /* Кадр уходит в глубину: размыт, приглушён и холоднее переднего плана. */
  filter: blur(26px) saturate(0.55) brightness(0.42);
  transform: scale(1.12);
  transition: opacity 700ms var(--e-inout);
}
.stages__bg-img.is-on { opacity: 1; }

/* --- Атмосфера: холодная глубина по краям, тёплый свет у центра сцены --- */
.stages__atmo {
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--cam-bleed) * -1);
  bottom: calc(var(--cam-bleed) * -1);
  z-index: 1;
  pointer-events: none;
  /* Свой клип: туман внутри вынесен на -12% и раньше резался секцией,
     а после снятия с неё overflow стал давать горизонтальный скролл. */
  overflow: hidden;
  /* Маска на весь слой, а не только линейный градиент в фоне: радиальные
     пятна светa не попадали под затухание и обрывались на кромке —
     это и читалось как шов на выходе из блока стадий. */
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
  /* По краям — прозрачность, а не var(--c-bg): сплошной тон на кромке
     и создавал ту самую ступеньку на стыке. */
  background:
    radial-gradient(70% 45% at 50% 40%, rgba(247, 145, 30, 0.12) 0%, transparent 70%),
    radial-gradient(90% 60% at 12% 84%, rgba(58, 96, 150, 0.16) 0%, transparent 72%),
    linear-gradient(180deg, transparent 0%, rgba(9, 11, 16, 0.28) 26%, rgba(9, 11, 16, 0.28) 74%, transparent 100%);
}
/* Медленный туман — один слой, движение по transform, без перерисовки. */
.stages__atmo::after {
  content: '';
  position: absolute; inset: -12%;
  background:
    radial-gradient(38% 26% at 22% 30%, rgba(190, 210, 240, 0.055), transparent 70%),
    radial-gradient(30% 22% at 74% 62%, rgba(190, 210, 240, 0.045), transparent 70%);
  animation: stagesFog 34s ease-in-out infinite alternate;
}
@keyframes stagesFog {
  from { transform: translate3d(-2%, 1%, 0) scale(1); }
  to   { transform: translate3d(3%, -2%, 0) scale(1.08); }
}

.stages__inner { position: relative; z-index: 2; }

/* --- Окно карты --- */
.stages__mapwin {
  position: relative;
  height: clamp(320px, 46svh, 520px);
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--c-line);
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5), inset 0 0 90px rgba(0, 0, 0, 0.5);
}
/* Телефон в ландшафте: минимум в 320px съедал почти весь экран, и карточка
   стадии уходила за кадр целиком. Низкому вьюпорту даём свою шкалу. */
@media (orientation: landscape) and (max-height: 540px) {
  .stages__mapwin { height: clamp(180px, 56svh, 320px); }
}

.stages__scenes {
  /* Слой уходит в край экрана, по вертикали держится границ окна. */
  position: absolute;
  top: 0; bottom: 0;
  left: 50%; width: 100vw;
  transform: translateX(-50%);
  z-index: 0;
  overflow: hidden;
  background: #0a0d13;
}

.stages__map { position: absolute; inset: 0; overflow: hidden; }
/* Обёртка picture не должна участвовать в раскладке: геометрия карты
   задана самому изображению, и лишний бокс между ними сдвинул бы слой
   с метками относительно кадра. */
.stages__map-pic { display: contents; }
.stages__map-img {
  position: absolute;
  left: var(--focus-x, 50%);
  top: var(--focus-y, 46%);
  width: var(--map-w, 250%);
  height: auto;
  max-width: none;
  /* Точка (--px, --py) кадра встаёт в фокус окна: проценты в translate
     считаются от собственного размера слоя, пересчёт не нужен. */
  transform: translate(calc(var(--px, 50%) * -1), calc(var(--py, 50%) * -1));
  transition:
    transform 760ms cubic-bezier(0.32, 0.72, 0.24, 1),
    width 760ms cubic-bezier(0.32, 0.72, 0.24, 1);
  /* Карта на светлом пергаменте. Уводим в тёплую тень, но оставляем
     читаемой: гасить её нельзя — это главный элемент секции. */
  /* ТЗ §23: карта — главный элемент секции, «всё чёрное» недопустимо.
     Тёплую тень оставляем, но яркость и цвет поднимаем до читаемых. */
  filter: brightness(1.02) contrast(1.06) saturate(0.78) sepia(0.12);
  will-change: transform;
}

/* Направленный свет: активная область ярче, края уходят в тень.
   Одна виньетка вместо сплошного затемнения всей карты. */
.stages__map::after {
  content: '';
  position: absolute; inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(30% 40% at var(--focus-x, 50%) var(--focus-y, 46%),
      rgba(255, 196, 110, 0.16) 0%, transparent 62%),
    radial-gradient(115% 88% at var(--focus-x, 50%) var(--focus-y, 46%),
      transparent 32%, rgba(6, 8, 12, 0.5) 72%, rgba(6, 8, 12, 0.88) 100%),
    linear-gradient(180deg, transparent 58%, rgba(6, 8, 12, 0.72) 82%, rgba(6, 8, 12, 0.95) 100%);
}

/* Слой повторяет геометрию картинки один в один — проценты координат
   стадий считаются от кадра карты, а не от окна. Без этого правила метки
   позиционируются относительно окна и разъезжаются по всей секции. */
.stages__layer {
  position: absolute;
  left: var(--focus-x, 50%);
  top: var(--focus-y, 46%);
  width: var(--map-w, 250%);
  aspect-ratio: 1975 / 2618;
  z-index: 2;
  transform: translate(calc(var(--px, 50%) * -1), calc(var(--py, 50%) * -1));
  transition:
    transform 760ms cubic-bezier(0.32, 0.72, 0.24, 1),
    width 760ms cubic-bezier(0.32, 0.72, 0.24, 1);
}

/* --- Метки стадий ---
   Живут внутри слоя карты, поэтому едут вместе с ней бесплатно.
   Собственный масштаб гасится --pin-fix, иначе на ближнем плане
   медальоны раздувались бы вместе с картой.

   Маршрута между метками нет намеренно: одиннадцать локаций разбросаны
   по всему миру, и линия между ними читалась как случайная диагональ
   поперёк карты, а не как дорога. Порядок несёт навигатор и римские цифры. */
.mappin {
  /* Якорь нулевого размера ровно в точке локации, всё остальное висит
     на нём абсолютом. Иначе скрытая подпись занимает место в потоке
     и утаскивает медальон вниз от собственной координаты. */
  position: absolute;
  left: var(--mx); top: var(--my);
  z-index: 2;
  width: 0; height: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  pointer-events: auto;
  transform: scale(var(--pin-fix, 1));
  transform-origin: center;
  transition: transform 760ms cubic-bezier(0.32, 0.72, 0.24, 1);
}

.mappin__medal {
  position: absolute;
  left: 0; top: 0;
  translate: -50% -50%;
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: 50%;
  /* Сердечник плотный: под ним проходят подписи карты, и просвечивать он
     не должен. Тёплый блик сверху даёт медальону объём. */
  background:
    radial-gradient(120% 120% at 50% 18%, #33465f 0%, #16202c 58%, #080c12 100%);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.14);
  transition: all var(--d-fast) var(--e-out);
}
.mappin__medal > * { grid-area: 1 / 1; }

/* Герб расы — водяной знак под цифрой. Гербы проработанные: как основной
   носитель смысла на 30 px они схлопываются в пятно, а фоном дают фактуру
   и всё ещё читаются при наведении. */
.mappin__crest {
  display: block;
  width: 78%; height: 78%;
  justify-self: center; align-self: center;
  background: rgba(198, 214, 236, 0.3);
  -webkit-mask: center / contain no-repeat;
  mask: center / contain no-repeat;
  transition: background var(--d-fast) var(--e-out);
}

/* Номер стадии — то, что читается всегда и с любого расстояния. */
.mappin__num {
  position: relative;
  z-index: 1;
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 600;
  font-size: 17px;
  line-height: 1;
  letter-spacing: 0.02em;
  color: rgba(244, 242, 239, 0.94);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.85);
  transition: color var(--d-fast) var(--e-out), font-size var(--d-fast) var(--e-out);
}

/* Гербовое кольцо: две окружности и четыре засечки по сторонам света —
   медальон перестаёт быть просто кружком и рифмуется с компасом на карте. */
.mappin__ring {
  width: 100%; height: 100%;
  overflow: visible;
  fill: none;
  pointer-events: none;
}
.mappin__ring-out { stroke: rgba(244, 242, 239, 0.42); stroke-width: 3; }
.mappin__ring-in  { stroke: rgba(244, 242, 239, 0.16); stroke-width: 1.4; }
.mappin__ticks path { stroke: rgba(244, 242, 239, 0.34); stroke-width: 2.6; stroke-linecap: round; }

/* Статус — цветом, плотностью и характером кольца сразу (ТЗ §13.3) */
.mappin[data-status='done'] .mappin__ring-out { stroke: var(--c-accent); }
.mappin[data-status='done'] .mappin__num { color: var(--c-accent-hi); }
.mappin[data-status='upcoming'] .mappin__medal { opacity: 0.9; }
.mappin[data-status='locked'] .mappin__medal { opacity: 0.6; }
.mappin[data-status='locked'] .mappin__ring-out { stroke-dasharray: 6 6; }

.mappin:hover .mappin__ring-out,
.mappin:focus-visible .mappin__ring-out { stroke: var(--c-accent); }
.mappin:hover .mappin__num,
.mappin:focus-visible .mappin__num { color: var(--c-accent-hi); }
.mappin:hover .mappin__crest,
.mappin:focus-visible .mappin__crest { background: rgba(247, 145, 30, 0.42); }

/* --- Активная метка --- */
.mappin.is-active { z-index: 4; }
.mappin.is-active .mappin__medal {
  /* Статус upcoming приглушает медальон; у активного это надо снять,
     иначе подписи карты просвечивают сквозь него. */
  opacity: 1;
  width: 66px; height: 66px;
  background:
    radial-gradient(120% 120% at 50% 18%, #3d5675 0%, #182231 58%, #090d14 100%);
  box-shadow:
    0 0 0 5px rgba(247, 145, 30, 0.14),
    0 0 38px 12px rgba(247, 145, 30, 0.4),
    0 10px 24px rgba(0, 0, 0, 0.65),
    inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.mappin.is-active .mappin__ring-out { stroke: var(--c-accent); stroke-width: 3.4; }
.mappin.is-active .mappin__ring-in  { stroke: rgba(255, 192, 30, 0.34); }
.mappin.is-active .mappin__ticks path { stroke: var(--c-accent); }
.mappin.is-active .mappin__num { font-size: 25px; color: var(--c-accent-hi); }
.mappin.is-active .mappin__crest { background: rgba(247, 145, 30, 0.34); }

/* Пульс только у активной: анимация на всех одиннадцати превратила бы
   карту в гирлянду. */
.mappin.is-active .mappin__medal::after {
  content: '';
  position: absolute;
  inset: -4px;
  border: 1px solid var(--c-accent);
  border-radius: 50%;
  animation: pinPulse 2.6s var(--e-out) infinite;
  pointer-events: none;
}
@keyframes pinPulse {
  from { opacity: 0.7; transform: scale(1); }
  to   { opacity: 0; transform: scale(2.1); }
}

/* Подпись — только у активной метки и при наведении. Одиннадцать подписей
   разом превратили бы карту в справочник. */
.mappin__caption {
  position: absolute;
  left: 0; top: 40px;
  translate: -50% 0;
  display: grid;
  justify-items: center;
  gap: 4px;
  width: max-content;
  max-width: 200px;
  text-align: center;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity var(--d-fast) var(--e-out), transform var(--d-fast) var(--e-out);
  pointer-events: none;
}
.mappin.is-active .mappin__caption,
.mappin:hover .mappin__caption,
.mappin:focus-visible .mappin__caption { opacity: 1; transform: none; }
/* Подпись над медальоном — у меток, под которыми стоит соседняя (класс
   ставит скрипт по координатам). Иначе плашка «Стадия · День» ложилась
   на чужой медальон. */
.mappin.is-caption-up .mappin__caption {
  top: auto;
  bottom: 40px;
  transform: translateY(4px);
}
.mappin.is-caption-up.is-active .mappin__caption,
.mappin.is-caption-up:hover .mappin__caption,
.mappin.is-caption-up:focus-visible .mappin__caption { transform: none; }
/* Название стадии поверх карты. Рисуется только там, где на самой карте
   подписи нет: дублировать «Логово Антараса» рядом с готовой подписью —
   мусор. Начертание подогнано под лейблы карты: плотный светлый шрифт
   с тёмной обводкой, а не элемент интерфейса. */
.mappin__mapname {
  position: absolute;
  left: 0; top: -38px;
  translate: -50% 0;
  width: max-content;
  max-width: 190px;
  text-align: center;
  font-family: var(--f-ui);
  font-weight: 700;
  font-size: 15px;
  line-height: 1.15;
  letter-spacing: 0.01em;
  color: #f2efe6;
  -webkit-text-stroke: 3px rgba(14, 12, 9, 0.92);
  paint-order: stroke fill;
  pointer-events: none;
}
.mappin__caption u {
  text-decoration: none;
  font-size: var(--t-caption);
  color: var(--c-text-2);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.95);
}
.mappin__caption em {
  font-style: normal;
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  color: var(--c-accent);
  padding: 5px 12px;
  border: 1px solid var(--c-accent-soft);
  border-radius: 999px;
  background: rgba(10, 13, 19, 0.9);
}

@media (prefers-reduced-motion: reduce) {
  .stages__map-img, .stages__layer, .mappin { transition: none; }
  .mappin.is-active .mappin__medal::after, .stages__atmo::after { animation: none; }
}

/* --- Панель стадии ---
   Слева смысл (номер, статус, название, описание, даты, кнопка),
   справа характеристики карточками. Высота зарезервирована: у стадий
   разной длины название и описание, и без резерва секция дёргалась
   на каждой смене. Резерв только на панели — если раздать min-height
   внутренним элементам, между заголовком и описанием появляется дыра. */
/* Карточка идёт двумя блоками вниз, а не двумя колонками.
   В колоночной раскладке слева под стрелками оставалась пустая треть
   экрана, а справа текст жался в узкую полосу. Теперь шапка стадии
   занимает всю ширину, под ней — факты и подробности, и строка получает
   нормальную длину. */
.stage-panel {
  display: grid;
  gap: var(--s-6);
  align-items: start;
  max-width: none;
  margin-top: var(--s-6);
}
@media (max-width: 1023px) {
  .spec-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.stage-panel__lead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s-3) var(--s-7);
  align-items: start;
}
.stage-panel__lead > .stage-panel__top,
.stage-panel__lead > .stage-panel__title,
.stage-panel__lead > .stage-panel__grade,
.stage-panel__lead > .stage-panel__short { grid-column: 1; }
/* Даты и стрелки уходят в правую колонку шапки — раньше они висели под
   заголовком и оставляли под собой пустоту во всю высоту блока. */
.stage-panel__lead > .stage-panel__facts { grid-column: 2; grid-row: 1 / 3; justify-self: end; }
.stage-panel__lead > .stage-panel__cta   { grid-column: 2; grid-row: 3 / 5; justify-self: end; align-self: start; }
@media (max-width: 767px) {
  .stage-panel__lead { grid-template-columns: 1fr; align-items: start; }
  .stage-panel__lead > * { grid-column: 1 !important; }
}
.stage-panel__top { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.stage-panel__num {
  font-size: var(--t-caption);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-accent);
  font-weight: 600;
}
.stage-panel__title {
  font-family: var(--f-display); font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: var(--t-2xl);
  line-height: 1.04;
  letter-spacing: var(--tr-display);
  text-transform: uppercase;
}
.stage-panel__grade { color: var(--c-text-3); font-size: var(--t-sm); }
.stage-panel__short { color: var(--c-text-2); max-width: 46ch; }

/* Старт и длительность стадии. Два самых конкретных числа на экране стояли
   мелкой серой строкой в углу — рядом с заголовком в 36 пунктов их просто
   не замечали. Даём им рамку и кегль наравне с карточками фактов. */
.stage-panel__facts {
  display: flex;
  gap: var(--s-6);
  flex-wrap: wrap;
  margin-top: var(--s-2);
  padding: var(--s-3) var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  background: linear-gradient(160deg, rgba(30, 25, 18, 0.55), rgba(10, 12, 16, 0.6));
}
.stage-panel__facts dt {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
  margin-bottom: 2px;
}
.stage-panel__facts dd { font-size: var(--t-md); color: var(--c-text); font-variant-numeric: tabular-nums; }
.stage-panel__cta { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center; margin-top: var(--s-3); }

/* Карточки характеристик. Не список одинаковых точек: у каждой группы
   своя подпись и свой глиф, значение выделено. */
/* Ровно две колонки, а не auto-fit: при автоподборе число колонок менялось
   от стадии к стадии, и панель прыгала по высоте на каждой смене. */
.spec-grid {
  display: grid;
  /* Колонок ровно столько, сколько фактов у стадии (--cols из шаблона).
     auto-fit нарезал ряд по 210px и оставлял половину ширины пустой. */
  grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
  gap: var(--s-3);
  align-content: start;
}
@media (max-width: 720px) {
  .spec-grid { grid-template-columns: minmax(0, 1fr); }
}
/* Иконка и подпись стоят в строку: факт считывается с одного взгляда,
   а не читается как ячейка таблицы. */
.spec__head { display: flex; align-items: center; gap: var(--s-2); color: var(--c-text-3); }
.spec__icon { flex: none; width: 20px; height: 20px; }

/* «Главное на стадии» занимает всю ширину ряда и получает свой знак.
   Вес даёт форма и кегль, а не акцентная заливка: акцент точечный (ТЗ §1.1). */
.spec--featured {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--s-4);
  border-color: var(--c-line-strong);
}
.spec--featured .spec__mark {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  border: 1px solid color-mix(in srgb, var(--c-accent) 52%, transparent);
  border-radius: 50%;
  color: var(--c-accent);
}
.spec--featured .spec__icon { width: 24px; height: 24px; }
.spec--featured .spec__label { color: var(--c-accent); }
.spec--featured .spec__value {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-size: var(--t-lg);
  line-height: 1.15;
  margin-top: 2px;
}
.spec {
  /* Одна колонка: подпись с иконкой сверху, значение под ней. Прежние две
     колонки достались от раскладки с отдельной иконкой слева и разносили
     подпись и значение по горизонтали. */
  display: grid;
  gap: var(--s-2);
  /* Подпись сверху, значение снизу. При выключке к верху длинная подпись
     переносилась на вторую строку и утаскивала своё значение вниз: «58»
     стояло ниже соседнего «B-grade 50%», хотя это один ряд фактов. */
  align-content: space-between;
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  background: linear-gradient(160deg, rgba(30, 25, 18, 0.55), rgba(10, 12, 16, 0.6));
  transition: border-color var(--d-fast) var(--e-out), transform var(--d-fast) var(--e-out);
  /* Появление каскадом при смене стадии — порядок задаётся --n из шаблона. */
  animation: specIn 420ms var(--e-out) both;
  animation-delay: calc(var(--n, 0) * 55ms);
}
@keyframes specIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.spec:hover { border-color: var(--c-accent-soft); transform: translateY(-2px); }
.spec__icon { width: 22px; height: 22px; color: var(--c-accent); margin-top: 1px; flex: none; }
.spec__label {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
  margin-bottom: 3px;
}
.spec__value { font-size: var(--t-md); color: var(--c-text); line-height: 1.35; }

/* Максимальный уровень — единственный факт, который является числом, а не
   строкой. Прижатое к левому краю «58» под широкой подписью читалось как
   обрезок; ставим его по центру карточки и набираем крупно — тогда это
   показание, а не хвост подписи. */
.spec--number { justify-items: center; text-align: center; }
.spec--number .spec__value {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-size: var(--t-xl);
  line-height: 1;
}

/* Полный список особенностей — под раскрытием. На экране остаются четыре
   главных блока (ТЗ §18): пять и больше карточек превращают стадию
   в админ-панель, а не в рассказ о мире. */
/* Карточки и раскрытие — одна колонка: как отдельные элементы сетки
   раскрытие уезжало во вторую строку, под текстовый блок. */
.stage-panel__specs { display: grid; gap: var(--s-3); align-content: start; }
.stage-more { margin-top: 0; }
.stage-more > summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 4px;
  list-style: none;
  cursor: pointer;
  font-size: var(--t-caption);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-2);
  transition: color var(--d-fast) var(--e-out);
}
.stage-more > summary::-webkit-details-marker { display: none; }
.stage-more > summary:hover { color: var(--c-accent); }
.stage-more > summary svg { transition: rotate var(--d-fast) var(--e-out); }
.stage-more[open] > summary svg { rotate: 180deg; }
.stage-more__list {
  display: grid;
  gap: var(--s-3);
  margin-top: var(--s-2);
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  background: rgba(10, 12, 16, 0.6);
}
.stage-more__list dt {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: 2px;
}
.stage-more__list dd { font-size: var(--t-sm); color: var(--c-text-2); line-height: 1.45; }

/* Раскрытие «Все особенности» идёт двумя группами — контент и особенности
   (ТЗ §21). Заголовок группы задаёт ту же роль, что раньше играл dt. */
.stage-more__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: var(--s-5) var(--s-7);
  align-items: start;
}
.stage-more__group + .stage-more__group { margin-top: 0; }
.stage-more__group h4 {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: var(--s-2);
}
/* Нумерованные строки с разделителями вместо двух столбцов буллетов:
   так видно структуру, а не стену равноправных пунктов. */
.stage-more__group ol {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: rx7-detail;
}
.stage-more__group li {
  display: grid;
  grid-template-columns: var(--s-5) minmax(0, 1fr);
  gap: var(--s-2);
  padding-block: var(--s-3);
  border-top: 1px solid var(--c-line);
  font-size: var(--t-base, 1rem);
  color: var(--c-text-2);
  line-height: 1.5;
}
.stage-more__group li::before {
  counter-increment: rx7-detail;
  /* Обычные цифры, без ведущего нуля: «01, 02» — декоративная нумерация
     из типового лендинга, она добавляет веса пункту, а не смысла. */
  content: counter(rx7-detail);
  color: var(--c-text-3);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  line-height: 1.9;
}

/* Короткое имя стадии под меткой шкалы: «IV · ПОЛЯ» из макета ТЗ. */
.timeline__short {
  display: block;
  font-size: var(--t-micro);
  letter-spacing: 0.02em;
  color: var(--c-text-3, var(--c-text-2));
  white-space: nowrap;
}



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

/* Навигатор лежит на карте */
/* Шкала вынесена из слоя карты в собственную полосу под ней. Раньше она
   лежала на карте вместе с медальоном стадии: один статус получал два
   одинаково сильных носителя в одном месте, а подложка шкалы съедала карту
   ровно там, где должен читаться маршрут. Карта осталась сценой, шкала —
   интерфейсом. */
.stages__inner > .timeline {
  position: static;
  z-index: auto;
  margin-top: var(--s-5);
  padding: var(--s-4) var(--s-5) 0;
  border-top: 1px solid var(--c-line);
  background: none;
  gap: var(--s-2);
  /* Своя подложка: одной виньетки не хватало, номера налезали на подписи карты. */
  background: linear-gradient(180deg, transparent 0%, rgba(6, 8, 12, 0.72) 42%, rgba(6, 8, 12, 0.94) 100%);
}
.timeline__count {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-2);
  font-variant-numeric: tabular-nums;
}

/* --- Навигатор стадий ---
   Не ряд точек на линии, а гравированная шкала: желобок с подсвеченным
   пройденным участком, ромбовидные узлы и римские цифры. Ромб уже есть
   в языке проекта (углы окна клиента), цифры рифмуются с медальонами
   на карте — навигатор и карта читаются как одна система. */
.timeline { display: grid; gap: var(--s-3); }

.timeline__count {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  align-self: start;
  justify-self: start;   /* в grid по умолчанию тянется на всю ширину */
  padding: 4px 12px 4px 10px;
  border: 1px solid var(--c-line-strong);
  border-radius: 999px;
  background: rgba(10, 13, 19, 0.7);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-2);
  font-variant-numeric: tabular-nums;
}
/* Ромб-маркер перед счётчиком — тот же знак, что в углах окна клиента */
.timeline__count::before {
  content: '';
  width: 7px; height: 7px;
  rotate: 45deg;
  background: var(--c-accent);
  box-shadow: 0 0 8px rgba(247, 145, 30, 0.7);
}

.timeline__track {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  padding-block: 12px;
}
/* Желобок: тёмная прорезь со светлой фаской снизу — шкала выглядит
   вырезанной, а не нарисованной одной линией. */
.timeline__track::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: var(--line-y, 50%);
  height: 3px;
  translate: 0 -50%;
  border-radius: 2px;
  /* На тёмном фоне прорезь читается светлой фаской, а не тенью:
     первый вариант с тёмным градиентом был попросту не виден. */
  background:
    linear-gradient(180deg,
      rgba(0, 0, 0, 0.6) 0%,
      rgba(244, 242, 239, 0.16) 45%,
      rgba(244, 242, 239, 0.26) 100%);
}
.timeline__progress {
  position: absolute;
  left: 0; top: var(--line-y, 50%);
  height: 3px;
  translate: 0 -50%;
  border-radius: 2px;
  background: var(--c-accent-grad);
  box-shadow: 0 0 12px rgba(247, 145, 30, 0.55);
  transition: width var(--d-base) var(--e-cam);
}
/* Огонёк на конце пройденного участка */
.timeline__progress::after {
  content: '';
  position: absolute;
  right: -3px; top: 50%;
  width: 7px; height: 7px;
  translate: 0 -50%;
  border-radius: 50%;
  background: var(--c-accent-hi);
  box-shadow: 0 0 12px 3px rgba(255, 192, 30, 0.6);
}

.timeline__btn {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 8px;
  min-width: var(--tap);
  /* Тач-цель не меньше 44px (ui-ux-rules §2): после переверстки узла
     кнопка схлопывалась до 36. */
  min-height: var(--tap);
  align-content: center;
  color: var(--c-text-3);
}

/* Узел — ромб. Статус читается заливкой, цветом и размером сразу. */
.timeline__node {
  width: 10px; height: 10px;
  rotate: 45deg;
  border: 1.5px solid currentColor;
  background: var(--c-void);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.7);
  transition:
    background var(--d-fast) var(--e-out),
    scale var(--d-fast) var(--e-out),
    border-color var(--d-fast) var(--e-out);
}
.timeline__num {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-size: 13px;
  letter-spacing: 0.1em;
  line-height: 1;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
  transition: color var(--d-fast) var(--e-out);
}

/* Название стадии — при наведении, над узлом. Показывать все одиннадцать
   разом смысла нет: шкала превратится в простыню. */
/* Подсказка у крайних делений прижимается к своему краю, а не центрируется
   по делению: у последнего она выходила за правую кромку и тянула
   горизонтальную прокрутку всей страницы на планшетной ширине. */
.timeline__btn:first-child .timeline__tip { left: 0; translate: 0 0; }
.timeline__btn:last-child .timeline__tip { left: auto; right: 0; translate: 0 0; }
.timeline__tip {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  translate: -50% 0;
  z-index: 3;
  width: max-content;
  max-width: 190px;
  padding: 5px 10px;
  border: 1px solid var(--c-accent-soft);
  border-radius: var(--r-sm);
  background: rgba(8, 10, 14, 0.97);
  box-shadow: var(--sh-card);
  font-size: var(--t-micro);
  color: var(--c-text);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity var(--d-fast) var(--e-out), transform var(--d-fast) var(--e-out);
  pointer-events: none;
}
.timeline__btn:hover .timeline__tip,
.timeline__btn:focus-visible .timeline__tip { opacity: 1; transform: none; }

.timeline__btn:hover { color: var(--c-text); }
.timeline__btn:hover .timeline__node { border-color: var(--c-accent); scale: 1.2; }

.timeline__btn[data-status='done'] { color: var(--c-done); }
.timeline__btn[data-status='done'] .timeline__node { background: currentColor; }
.timeline__btn[data-status='active'] { color: var(--c-accent); }
.timeline__btn[data-status='active'] .timeline__node { background: currentColor; scale: 1.25; }

/* Выбранная стадия — крупный ромб с ореолом и римская цифра акцентом */
.timeline__btn[aria-current='true'],
.timeline__btn[aria-selected='true'] { color: var(--c-accent-hi); }
.timeline__btn[aria-current='true'] .timeline__node,
.timeline__btn[aria-selected='true'] .timeline__node {
  scale: 1.55;
  border-color: var(--c-accent-hi);
  background: var(--c-accent-hi);
  box-shadow: 0 0 0 4px rgba(247, 145, 30, 0.16), 0 0 18px 5px rgba(255, 192, 30, 0.5);
}
.timeline__btn[aria-current='true'] .timeline__num,
.timeline__btn[aria-selected='true'] .timeline__num {
  color: var(--c-accent-hi);
}

.stages__nav { display: flex; gap: var(--s-2); }
.stages__nav button {
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-md);
  color: var(--c-text-2);
}
.stages__nav button:hover:not([disabled]) { color: var(--c-text); background: rgba(244,242,239,0.06); }
.stages__nav button[disabled] { opacity: 0.35; cursor: default; }

/* Mobile: обычный свайп карточками, без закрепления вьюпорта */
.stages__mobile { display: none; }
.stages__slider {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 84%;
  gap: var(--s-3);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  padding-inline: var(--gutter);
  padding-block: var(--s-4);
  margin-inline: calc(var(--gutter) * -1);
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
.stages__slider::-webkit-scrollbar { display: none; }
/* На телефоне 84% оставляли от следующей карточки 60px — в просвет
   попадали её отступ и обрубок заголовка, и рядом с открытой стадией
   всё время висела вторая, нечитаемая. Оставляем узкий срез: край
   карточки виден, текста в нём нет. */
@media (max-width: 560px) {
  .stages__slider { grid-auto-columns: calc(100% - var(--s-5)); }
}
.stage-card {
  position: relative;
  scroll-snap-align: start;
  min-height: 470px;
  display: grid;
  align-content: end;
  gap: var(--s-3);
  padding: var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-lg);
  overflow: hidden;
  isolation: isolate;
}
/* Кадры стадий пришли светлее прежних локационных: дневное небо и
   яркая броня. Приглушаем сам кадр, иначе подписи на нём тонут. */
.stage-card__bg {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%;
  object-fit: cover;
  filter: saturate(0.8) brightness(0.72);
}
.stage-card::after {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(0deg, rgba(6,6,8,0.97) 26%, rgba(6,6,8,0.62) 62%, rgba(6,6,8,0.34) 100%);
}
.stage-card__title { font-family: var(--f-display); font-variant-numeric: var(--fvn-display); font-size: var(--t-xl); line-height: 1.08; text-transform: uppercase; }
.stage-card__details > summary {
  cursor: pointer;
  list-style: none;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--t-sm);
  color: var(--c-accent);
  font-weight: 600;
}
.stage-card__details > summary::-webkit-details-marker { display: none; }
.stage-card__details > summary::after { content: '+'; font-size: 1.1em; }
.stage-card__details[open] > summary::after { content: '–'; }
.stage-card__details .stage-panel__features { margin-top: var(--s-3); }

/* ============================================================
   КЛИЕНТ — ТЗ §9, правка §5: игровая рамка вместо окна Mac
   ============================================================ */

/* Видео слева, загрузка справа. Раньше три одинаково тяжёлые кнопки шли
   первыми, и живой клиент читался приложением к форме загрузки — иерархия
   переворачивалась. Теперь сначала доказательство, потом действие. */
.client__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-6);
}
/* Видео идёт первым в любом раскладе: сначала доказательство, потом
   действие. Раньше три одинаково тяжёлые кнопки шли выше, и живой клиент
   читался приложением к форме загрузки. */
/* Раскладка задаётся колонкой, а не order: у окна клиента тоже класс
   .reveal, и правило по нему перебивало порядок. */
.client__grid > .client-window { order: 1; }
.client__grid > .reveal:not(.client-window) { order: 2; }
/* Две колонки — только там, где хватает ширины. Диапазон свой: ниже по файлу
   есть правило на 1200px, которое иначе перебивает раскладку. */
@media (min-width: 1201px) {
  .client__grid {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    gap: var(--s-8);
    align-items: center;
  }
  /* Видео — в широкую колонку, список и загрузка — в узкую. */
  .client__grid > .client-window { grid-column: 1; }
  .client__grid > .reveal:not(.client-window) { grid-column: 2; }
}
.client__thesis {
  font-family: var(--f-display); font-variant-numeric: var(--fvn-display);
  font-size: var(--t-xl);
  line-height: 1.2;
  color: var(--c-accent);
  padding-left: var(--s-4);
  border-left: 2px solid var(--c-accent);
}
.client__features { display: grid; gap: var(--s-4); margin-top: var(--s-6); }
.client__feature { display: grid; gap: 4px; padding-bottom: var(--s-4); border-bottom: 1px solid var(--c-line); }
.client__feature h3 { font-size: var(--t-md); font-weight: 600; }
.client__feature p { color: var(--c-text-2); font-size: var(--t-sm); }

/* Панель в духе интерфейса Lineage 2: тёмное поле, золотая окантовка,
   угловые засечки. Никаких браузерных, офисных и Apple-элементов. */
.game-panel {
  position: relative;
  padding: 12px;
  border: 1px solid rgba(247,145,30,0.42);
  border-radius: 4px;
  background:
    linear-gradient(180deg, rgba(28,22,14,0.92), rgba(12,11,10,0.95));
  box-shadow:
    inset 0 0 0 1px rgba(255,214,140,0.14),
    inset 0 1px 0 rgba(255,214,140,0.20),
    var(--sh-card);
}
.game-panel::before,
.game-panel::after {
  content: '';
  position: absolute;
  width: 14px; height: 14px;
  border: 1px solid var(--c-accent);
  opacity: 0.85;
}
.game-panel::before { top: 4px; left: 4px; border-right: 0; border-bottom: 0; }
.game-panel::after { bottom: 4px; right: 4px; border-left: 0; border-top: 0; }

.game-panel__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px 10px;
  border-bottom: 1px solid rgba(247,145,30,0.28);
  margin-bottom: 10px;
}
.game-panel__gem {
  width: 9px; height: 9px;
  rotate: 45deg;
  background: var(--c-accent-grad);
  flex: none;
}
.game-panel__name {
  font-size: var(--t-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--c-accent-hi);
}
.game-panel__rule { flex: 1; height: 1px; background: linear-gradient(90deg, rgba(247,145,30,0.4), transparent); }

/* Соотношение задано материалом: запись клиента идёт в 1000×486 (≈2.06:1).
   Фиксируем его, иначе при смене фрагмента прыгает вёрстка (§27).
   Все клипы селектора должны приходить в этой же пропорции. */
.client-video {
  position: relative;
  aspect-ratio: 1000 / 486;
  background: #05070a;
  overflow: hidden;
}
.client-video__poster { width: 100%; height: 100%; object-fit: cover; }
.client-video video, .client-video iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
  object-fit: cover;
}
.client-video__play {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background: rgba(8,8,10,0.32);
  transition: background var(--d-base) var(--e-out);
}
.client-video__play:hover { background: rgba(8,8,10,0.16); }
.client-video__play span {
  width: 66px; height: 66px;
  display: grid; place-items: center;
  rotate: 45deg;
  border: 1px solid var(--c-accent);
  background: linear-gradient(135deg, rgba(255,192,30,0.92), rgba(224,125,18,0.92));
  color: var(--c-text-on-accent);
  box-shadow: var(--sh-accent);
}
.client-video__play span svg { rotate: -45deg; }
.client-video__caption {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid rgba(247,145,30,0.2);
  font-size: var(--t-caption);
  color: var(--c-text-3);
}

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

/* ============================================================
   ПЕРЕХОД 2 и 3: игровой мир → клиент, клиент → регистрация
   ============================================================ */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) and (min-width: 769px) {
    /* Панель клиента «собирается» из игрового кадра */
    .client-window {
      animation: panelAssemble linear both;
      animation-timeline: view();
      animation-range: entry 8% entry 78%;
      will-change: transform, opacity;
    }
    @keyframes panelAssemble {
      from { opacity: 0; transform: perspective(1200px) rotateX(9deg) scale(0.9); }
      to   { opacity: 1; transform: none; }
    }
    /* Регистрация раскрывается */
    .signup__panel {
      animation: unfold linear both;
      animation-timeline: view();
      animation-range: entry 4% entry 66%;
      will-change: transform, opacity;
    }
    @keyframes unfold {
      from { opacity: 0; transform: translateY(28px) scaleY(0.94); }
      to   { opacity: 1; transform: none; }
    }
  }
}

/* ============================================================
   РЕГИСТРАЦИЯ — ТЗ §10, правка §6
   ============================================================ */

/* Регистрация собрана в один блок: персонаж, подарки и форма читаются как
   одно предложение, а не три соседних карточки. Персонаж стоит слева и заходит
   за левую кромку панели — там только внутренний отступ, контент не перекрывается. */
/* Аккаунт и файлы — не два равных действия, а последовательность. Поэтому
   колонки разной ширины и разного веса: слева форма с единственной
   акцентной кнопкой на секцию, справа подчинённый шаг со скачиванием. */
.signup {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 4fr);
  gap: var(--s-9);
  align-items: start;
  justify-items: stretch;
  max-width: 1060px;
  margin-inline: auto;
}
/* Второй шаг получил собственную панель. Без неё скачивание читалось
   примечанием при форме: важное действие висело текстом в воздухе, тогда
   как форма стояла в оправе. Панель нейтральная — акцентный градиент на
   секции остаётся ровно один, на создании аккаунта. */
.signup__files {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: stretch;
  gap: var(--s-3);
  padding: var(--s-6);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
}
.signup__files-title {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-size: var(--t-xl);
  line-height: 1.15;
}
.signup__files-text { color: var(--c-text-2); max-width: 34ch; }
/* Кнопка во всю ширину панели. Акцентная — по решению заказчика: скачать
   клиент так же обязательно, как завести аккаунт, и держать её нейтральной
   значило бы прятать половину сценария. Это единственное место на странице
   с двумя акцентными кнопками, и они здесь не конкурируют: шаги подписаны
   номерами, и второй начинается после первого. */
.signup__files-main { margin-top: var(--s-2); width: 100%; }
/* Запасные способы отделены чертой и набраны одной строкой — так они не
   встают в ряд с кнопкой как равные действия. */
.signup__files-alt {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2) var(--s-4);
  margin-top: var(--s-3);
  padding-top: var(--s-4);
  border-top: 1px solid var(--c-line);
  font-size: var(--t-sm);
  color: var(--c-text-3);
}
@media (max-width: 900px) {
  /* В столбец, файлы — после формы: последовательность шагов сохраняется,
     а рядом на узком экране обе колонки стали бы нечитаемо узкими. */
  .signup { grid-template-columns: 1fr; gap: var(--s-7); }
  .signup__files { padding-top: 0; justify-items: stretch; }
  .signup__files .btn { width: 100%; }
}
/* ТЗ §34. Персонаж встроен в сцену светом, а не наклеен поверх фона:
   тёплое ambient-пятно за спиной, контактная тень под ногами и лёгкая
   цветокоррекция под кадр секции. */
.signup__panel {
  position: relative;
  z-index: 1;
  width: 100%;
  /* Компактнее прежнего: из панели ушли подарки, осталась только форма,
     и большая чёрная плита под ней перекрывала бы фон секции. */
  max-width: 620px;
  display: grid;
  gap: var(--s-4);
  padding: var(--s-6) var(--s-6) var(--s-5);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-lg);
  background: rgba(10, 11, 15, 0.72);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 40px 90px rgba(0, 0, 0, 0.55);
}


.register__form { display: grid; gap: var(--s-4); }

/* Префикс и логин: одна сетка, одинаковая высота и отступы у select и input.
   minmax не даёт колонке разъехаться, когда подпись меняет длину при смене языка. */
.register__row {
  display: grid;
  /* ТЗ §37: префикс вторичен по отношению к логину — примерно 22 / 78. */
  grid-template-columns: minmax(92px, 22%) minmax(0, 1fr);
  gap: var(--s-3);
  align-items: start;
}

/* ТЗ §38. Не системное уведомление в зелёной рамке, а продолжение истории:
   крупный заголовок, одна фраза и один главный CTA. Зелёная рамка здесь
   читалась как алерт формы, хотя это самый радостный момент страницы. */
.register__success {
  display: grid;
  gap: var(--s-4);
  justify-items: start;
  padding: var(--s-7) var(--s-6);
  text-align: left;
}
.register__success h3 {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  line-height: 1.08;
  letter-spacing: var(--tr-display);
}
/* Галочка вместо рамки: подтверждение читается знаком, а не заливкой.
   Знак — SVG в разметке: собрать галочку из одной CSS-полоски не выходит,
   получается косая черта. */
.register__success-mark {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: 50%;
  border: 1px solid var(--c-accent);
  background: rgba(247, 145, 30, 0.1);
  color: var(--c-accent-hi);
  box-shadow: 0 0 26px rgba(247, 145, 30, 0.3);
}
.register__success-mark svg { width: 22px; height: 22px; }
.register__success .lede { white-space: pre-line; max-width: 28ch; }
.register__success h3 { font-family: var(--f-display); font-variant-numeric: var(--fvn-display); font-size: var(--t-xl); }
.register__success-cta { display: flex; gap: var(--s-5); flex-wrap: wrap; align-items: center; margin-top: var(--s-2); }
.form-error {
  padding: 12px 14px;
  border: 1px solid rgba(255, 107, 94, 0.4);
  border-radius: var(--r-md);
  background: rgba(255, 107, 94, 0.08);
  color: var(--c-error);
  font-size: var(--t-sm);
}

/* ============================================================
   ОРЕСТ — ТЗ §11, правка §7
   ============================================================ */

.orest__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 480px); gap: var(--s-8); align-items: start; }

/* .orest__cards / .orest-card удалены: семь одинаковых карточек делали
   из экрана админ-панель. Замена — .orest__list, см. ниже. */

.orest__character {
  width: min(300px, 72%);
  justify-self: center;
  filter: drop-shadow(0 24px 50px rgba(0,0,0,0.75));
}

/* Компактное окно Telegram, не на весь экран */
.tg {
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-lg);
  background: rgba(18,18,22,0.94);
  overflow: hidden;
  box-shadow: var(--sh-card);
}
.tg__bar {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--c-line);
  background: rgba(25,25,32,0.9);
}
.tg__avatar {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--c-accent-grad);
  display: grid; place-items: center;
  color: var(--c-text-on-accent);
  font-weight: 700;
  font-size: var(--t-sm);
  overflow: hidden;
  flex: none;
}
.tg__avatar img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 12%; }
.tg__name { font-size: var(--t-sm); font-weight: 600; }
.tg__status { font-size: var(--t-micro); color: var(--c-success); }
.tg__feed { display: grid; gap: 10px; padding: var(--s-4); min-height: 260px; align-content: end; }
.tg__msg {
  justify-self: start;
  max-width: 88%;
  padding: 10px 13px;
  border-radius: 14px 14px 14px 4px;
  background: var(--c-surface-2);
  font-size: var(--t-sm);
  color: var(--c-text-2);
  opacity: 0;
  translate: 0 8px;
  transition: opacity var(--d-base) var(--e-out), translate var(--d-base) var(--e-out);
}
.tg__msg.is-in { opacity: 1; translate: 0; }
.tg__msg b { color: var(--c-text); }
.tg__msg time { display: block; margin-top: 4px; font-size: var(--t-micro); color: var(--c-text-3); }

/* ============================================================
   ПОДВАЛ — ТЗ §12
   ============================================================ */

/* ТЗ §44: подвал должен ощущаться завершением сайта, а не скрытым блоком.
   Общая вуаль scene--even гасила кадр до 0.97 — здесь она заметно светлее,
   а текст поднят на ступень по палитре. */
.footer .scene--even::after {
  background:
    linear-gradient(0deg, rgba(6, 6, 8, 0.94) 0%, rgba(6, 6, 8, 0.84) 45%, rgba(6, 6, 8, 0.88) 100%);
}
/* Первая попытка (вуаль 0.62, brightness 1.25) вывела арт на передний план,
   и он забивал текст. Задача была поднять читаемость, а не яркость. */
.footer .scene__img { filter: brightness(1.08) saturate(0.8); }
.footer__about p { color: var(--c-text-2); }
.footer__col a { color: var(--c-text); }
.footer__bottom { color: var(--c-text-2); }
.footer__h { color: var(--c-text); }

.footer {
  position: relative;
  border-top: 1px solid var(--c-line-strong);
  padding-block: var(--s-8) var(--s-6);
  isolation: isolate;
  overflow: hidden;
}
.footer > .wrap { position: relative; z-index: var(--z-content); }
.footer__grid { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: var(--s-7); }
.footer__about { display: grid; gap: var(--s-3); align-content: start; max-width: 36ch; }
.footer__about p { color: var(--c-text-3); font-size: var(--t-sm); }
.footer__col { display: grid; gap: 10px; align-content: start; }
.footer__h {
  font-size: var(--t-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--c-text-3);
  margin-bottom: 4px;
}
.footer__col a { font-size: var(--t-sm); color: var(--c-text-2); min-height: 32px; display: inline-flex; align-items: center; }
.footer__col a:hover { color: var(--c-accent); }
.footer__social { display: flex; gap: var(--s-2); margin-top: var(--s-3); }
.footer__social a {
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  color: var(--c-text-2);
}
.footer__social a:hover { color: var(--c-accent); border-color: var(--c-line-strong); }
.footer__bottom {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--s-3);
  margin-top: var(--s-7); padding-top: var(--s-5);
  border-top: 1px solid var(--c-line);
  font-size: var(--t-caption);
  color: var(--c-text-3);
}

/* ============================================================
   Мобильное меню
   ============================================================ */

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: none;
  flex-direction: column;
  gap: var(--s-5);
  padding: calc(var(--header-h) + var(--s-5)) var(--gutter) var(--s-6);
  background: var(--c-void);
  overflow-y: auto;
}
.mobile-menu.is-open { display: flex; }
/* Шапка остаётся над раскрытым меню: в ней живёт крестик, которым
   меню и закрывают. Пока фон меню был почти непрозрачным, крестик
   держался на просвете — стоило докрасить фон, и закрыть было нечем. */
body.has-menu .header { z-index: calc(var(--z-menu) + 1); }
.mobile-menu__list { display: grid; gap: 2px; }
.mobile-menu__list a {
  display: flex; align-items: center;
  min-height: 52px;
  font-family: var(--f-display); font-variant-numeric: var(--fvn-display);
  font-size: var(--t-lg);
  border-bottom: 1px solid var(--c-line);
}
.mobile-menu__langs { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.mobile-menu__langs button {
  min-height: var(--tap); padding: 10px 18px;
  border: 1px solid var(--c-line);
  border-radius: var(--r-pill);
  font-size: var(--t-caption); font-weight: 600;
  color: var(--c-text-2);
}
.mobile-menu__langs button[aria-current='true'] { color: var(--c-accent); border-color: var(--c-accent); }

/* ============================================================
   Появление контента
   ============================================================ */
.js .reveal { opacity: 0; translate: 0 16px; transition: opacity var(--d-slow) var(--e-out), translate var(--d-slow) var(--e-out); }
.js .reveal.is-in { opacity: 1; translate: 0; }

@media (prefers-reduced-motion: reduce) {
  .js .reveal { opacity: 1; translate: 0; }
  .hero__glass span, .hero__crt, .hero__scroll-chevrons svg { animation: none; }
  .hero__scroll-chevrons svg:last-child { display: none; }
  .stages__scene { transition: none; }
}

/* ============================================================
   Портретная сцена — ТЗ §11
   Выбор идёт по пропорции вьюпорта, а не по ширине: планшет в портрете
   должен получать вертикальную композицию, а в альбоме — горизонтальную.
   Раньше условие было по ширине, и на 820×1180 показывался горизонтальный
   кадр, растянутый по высоте, из-за чего слой экрана отрывался от монитора.
   ============================================================ */
@media (max-aspect-ratio: 1 / 1) {
  /* Кадр вписан по ширине экрана и посажен на нижнюю кромку. Раньше ширина
     считалась от высоты вьюпорта и сцена уезжала влево: монитор наполовину
     оказывался за краем, а слой с игрой — мимо стекла. Теперь проценты
     screenRect совпадают с кадром один в один при любой высоте телефона. */
  .hero__frame {
    /* Кадр прижат к нижней кромке героя и гасится маской секции.
       Собственная маска здесь съела бы монитор — главный объект кадра. */
    -webkit-mask-image: none;
    mask-image: none;
    --scene-ar: 1080 / 1935;

    /* Кадр закрывает первый экран целиком. По одной только ширине экрана
       на высоком телефоне (440×956)её не хватало: сверху оставалась
       полоса пустого фона и через весь экран шёл шов там, где начиналась
       фотография. Берём большую из двух ширин — ширину экрана и ту, что
       нужна, чтобы закрыть 100svh по высоте.

       Пропорция обязана остаться ТОЧНОЙ: к ней в процентах привязан слой
       экрана монитора. Поэтому задаём ширину, а высоту выводим из
       aspect-ratio — обратный порядок (высота 100% плюс min-width)
       ломает пропорцию, и слой съезжает со стекла. */
    inline-size: max(100%, calc(100svh * 1080 / 1935));
    block-size: auto;
    inset: auto auto 0 50%;
    translate: -50% 0;
  }

  /* Текст уходит вверх — там тёмная стена с плакатом,
     монитор и стол остаются видны снизу. */
  .hero { align-items: flex-start; padding: calc(var(--header-h) + 12px) 0 84px; }

  /* Слева, под текстом, темнее; справа сверху светлее — там окно,
     иначе дождь по стеклу вообще не читался бы. */
  .hero__veil {
    background:
      linear-gradient(104deg,
        rgba(6,6,8,0.72) 0%, rgba(6,6,8,0.62) 46%, rgba(6,6,8,0.28) 78%, rgba(6,6,8,0.14) 100%),
      linear-gradient(180deg,
        rgba(6,6,8,0.30) 0%, rgba(6,6,8,0.08) 36%, transparent 62%, rgba(6,6,8,0.46) 100%);
  }

  /* Плакат на стене светлее вуали, и лид-текст на нём переставал читаться.
     Локальный скрим под текстовым блоком — контраст держится в любом кадре. */
  .hero__lead { position: relative; isolation: isolate; }
  .hero__lead::before {
    content: '';
    position: absolute;
    inset: -190px -50vw -28px -50vw;
    z-index: -1;
    background: linear-gradient(180deg,
      rgba(6,6,8,0) 0%, rgba(6,6,8,0.28) 32%, rgba(6,6,8,0.52) 54%,
      rgba(6,6,8,0.44) 80%, rgba(6,6,8,0) 100%);
  }
  .hero__sub { color: var(--c-text-2); }

  /* В портретной сцене окно одно, в правой верхней части */
  .hero__glass--a { --g-left: 60.5%; --g-top: 5.0%;  --g-w: 20.5%; --g-h: 27.5%; }
  .hero__glass--b { --g-left: 81.5%; --g-top: 5.0%;  --g-w: 12.0%; --g-h: 27.5%; }
  .hero__glass--c { display: none; }
}

/* ============================================================
   Адаптив — контрольные ширины ТЗ §13.1
   ============================================================ */

@media (max-width: 1200px) {
  .client__grid { grid-template-columns: 1fr; gap: var(--s-6); }
  .orest__grid { grid-template-columns: 1fr; gap: var(--s-6); }
  .footer__grid { grid-template-columns: 1fr 1fr; gap: var(--s-6); }
}

@media (max-width: 1024px) {
  .nav { display: none; }
  .burger { display: grid; }
  .header__actions .lang { display: none; }
}

@media (max-width: 768px) {
  :root { --header-h: 64px; }

  /* Таймер уходит под текстовый блок, кнопки — в столбик */
  .hero__content { grid-template-columns: 1fr; align-items: start; gap: var(--s-5); }
  .hero__lead { max-width: none; gap: var(--s-4); }
  .hero__side { justify-items: stretch; }
  .hero__side .timer { width: 100%; }
  /* Две кнопки первого экрана идут в строку и делят её поровну: столбик
     из двух одинаковых пилюль занимал 116px и отжимал сцену вниз.
     flex-wrap снят намеренно — с ним при нехватке места вторая кнопка
     уезжала во ВТОРОЙ СТОЛБЕЦ, за пределы экрана. */
  .hero__cta { flex-direction: row; flex-wrap: nowrap; align-items: stretch; }
  .hero__cta .btn { flex: 1 1 0; min-width: 0; padding-inline: var(--s-3); }
  /* Три кнопки скачивания в строку дают рваные 2+1 — на телефоне это столбик. */
  .downloads { flex-direction: column; flex-wrap: nowrap; align-items: stretch; }
  .downloads .btn { width: 100%; }
  .hero__sound { display: none; }
  .hero__scroll { bottom: 14px; }
  .hero .placeholder-note { display: none; }

  /* Стадии: только карточки со свайпом */
  .stages { display: block; min-height: 0; }
  /* На мобильном от десктопного блока остаётся только карта с навигатором:
     описание и характеристики читаются карточками ниже. Карта не должна
     превращаться в мелкое нечитаемое изображение — окно ниже, но зум ближе. */
  .stages__desktop { padding-top: 0; }
  /* Заголовок берём из десктопного блока — он идёт над картой.
     Мобильный дубль скрываем, иначе заголовок оказывается под картой. */
  .stages__desktop .stage-panel { display: none; }
  .stages__mobile > .section__head { display: none; }
  .stages__mapwin { height: clamp(240px, 42svh, 340px); }
  /* Крайние номера навигатора упирались в кромку окна — добавляем поля. */
  .stages__inner > .timeline { padding: var(--s-4) var(--s-4) 0; }
  /* Одиннадцать узлов по 44px в ширину не помещаются на телефоне —
     крайние уезжали за кадр. Высота тач-цели остаётся 44. */
  /* Тринадцать точек с двумя рядами подписей на телефон не помещаются.
     Сжимать их дальше некуда — переходим на горизонтальную прокрутку,
     активная точка центрируется сама (ТЗ §4). */
  .timeline__track { gap: 0; }
  /* Прокрутка — только у шкалы стадий. В истории девять лет с длинными
     подписями, там наоборот включено сжатие, и общий флекс сломал бы его. */
  .stages__inner > .timeline { scroll-snap-type: inline mandatory; scroll-padding-inline: var(--s-4); }
  .stages__inner > .timeline .timeline__track {
    justify-content: flex-start;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .stages__inner > .timeline .timeline__track::-webkit-scrollbar { display: none; }
  .stages__inner > .timeline .timeline__btn { flex: 0 0 auto; min-width: 56px; scroll-snap-align: start; }
  /* 11px нарушает минимум в 12px (ui-ux-rules §6). Место освобождаем
     трекингом, а не кеглем. */
  .timeline__num { font-size: var(--t-micro); letter-spacing: 0.01em; }
  .timeline__tip { max-width: 150px; }
  .stages__mobile { display: block; }
  /* На узком экране вуаль ведёт вниз, а не вбок: иначе кадр сразу за текстом
     получает вес постера. */
  /* На узком экране вуаль ведёт вниз, а не вбок: иначе кадр сразу за текстом
     получает вес постера. Верх держим мягче — там сцена работает; ниже, где
     идёт текст, слой плотный: содержимое кадра меняется каждую секунду, и
     серый лид иначе тонет в игровых эффектах. Кромку гасит маска, поэтому
     вуаль не обязана слабеть к низу. */
  .saga__veil {
    background:
      linear-gradient(180deg, var(--overlay-soft) 0%, var(--overlay-strong) 52%,
        var(--overlay-strong) 100%);
  }
  /* Собственного отступа у копира больше нет. Он ставил 24px против 18px
     у --gutter, и на узком экране история стояла по своей оси, а следующая
     секция по общей — блок читался вставленным поверх страницы. Второй
     токен под то же самое заводить нельзя: --gutter уже эта величина. */


  /* Лестница грейдов упиралась в край экрана: подписи и цитаты уходили
     за правую границу. Запрещаем содержимому раздвигать сетку. */
  .why, .why__stage, .why__frame, .ladder, .ladder__step {
    min-width: 0;
    max-width: 100%;
  }
  .why { overflow-x: clip; }
  .ladder {
    width: 100%;
    min-inline-size: 0;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--s-5);
    row-gap: var(--s-7);
  }
  .ladder__step { overflow-wrap: normal; }
  /* Лестница идёт сразу за вводом, отделённая линией, а не длинной паузой. */
  .why .ladder {
    margin-top: var(--s-6);
    padding-top: var(--s-5);
    border-top: 1px solid var(--c-line);
  }


  .register__row { grid-template-columns: 1fr; }
  .footer__grid { grid-template-columns: 1fr; gap: var(--s-5); }
}

/* Порог для строки из двух кнопок. При 375px на каждую приходится 163px
   при нужных 142 — помещаются. При 320px остаётся 136, и подпись в
   16 знаков переносится по слогам; там столбик — единственный честный
   вариант. Граница взята с запасом в несколько пикселей. */
@media (max-width: 339px) {
  .hero__cta { flex-direction: column; }
  .hero__cta .btn { flex: 0 0 auto; padding-inline: var(--s-5); }
}

/* Первый экран на телефоне. */
@media (max-width: 560px) {
  /* Первый экран на телефоне — одна колонка одной ширины.
     Заголовок, кнопки, таймер и плашка ивента стояли каждый своей
     (206, 317 и 334 пикселя), и левый столбец читался рваным списком,
     а не блоком. Ширина у всех теперь общая, а разницу между главным
     и второстепенным несут вес и цвет, а не случайная длина. */
  .hero__lead, .hero__side { justify-items: stretch; }
  .hero__cta { width: 100%; }
  .evbtn { width: 100%; justify-content: space-between; }

  /* Атмосферный подзаголовок уходит с телефона. Слоган уже сказал то же
     самое, а третья текстовая интонация до кнопки отжимала комнату вниз
     на шестьдесят пикселей. В разметке он остаётся — индексируется и
     работает на десктопе. */
  .hero__sub { display: none; }

  /* Подсказка «листай ниже» — тоже с телефона убираем. Пальцем листают
     без приглашения, а стояла она поверх стола и спорила с монитором
     ровно в той части кадра, куда взгляд должен приходить сам. */
  .hero__scroll { display: none; }

  /* Название держит экран. Кегль стоял на нижней границе clamp — 40px,
     всего вдвое крупнее слогана, и первый экран оставался без единой
     точки внимания. Поднимаем на ступень шкалы. */
  .hero__title { font-size: var(--t-3xl); }

  /* «Скачать игру» в шапке. Убирать её нельзя — это единственная
     кнопка загрузки, которая едет с пользователем по всей странице.
     Но в узкой строке она третьим элементом теснила логотип и бургер,
     поэтому на телефоне она мельче: то же действие, меньше веса. */
  .header .header__actions > .btn--primary {
    min-height: 38px;
    padding: 9px var(--s-3);
    font-size: var(--t-micro);
  }
  .header__row { gap: var(--s-3); }

  /* Таймер. Панель занимала 350×113 и ложилась поперёк комнаты ровно
     там, где стоит монитор: первый экран собран вокруг него, а отсчёт
     его перекрывал. Цифры на ступень мельче, отступы вдвое уже —
     уменьшаем оправу, а не смысл: отсчёт остаётся самым крупным
     числом на экране. */
  /* Отсчёт и плашка ивента прижаты к низу экрана. Текст и действие живут
     наверху, служебная информация — внизу, а всё, что между ними, отдано
     комнате: именно в этом промежутке читаются окно, кровать и стол.
     Растягиваем содержимое на всю высоту первого экрана и отправляем
     нижний блок в конец сетки. */
  .hero { align-items: stretch; padding-block-end: var(--hero-bottom-safe); }

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

     display: contents распускает обёртку .hero__side, и оба блока
     становятся строками общей сетки первого экрана. Иначе их не развести
     по разным краям: внутри одной обёртки они всегда едут вместе. */
  .hero__side { display: contents; }
  .hero__content { grid-template-rows: auto auto 1fr; }
  .evbtn { align-self: end; margin-block-start: 0; }

  .hero__side .timer {
    width: 100%;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-4);
    /* Содержимое по центру: подпись шире сетки цифр, и при выравнивании
       по левому краю справа оставалась дыра в полсотни пикселей —
       отсчёт выглядел съехавшим внутри собственной панели. */
    align-items: center;
    /* Подложка легче: панель стоит на фотографии комнаты и на телефоне
       занимает её заметную часть — плотный фон читался заплаткой. */
    background: rgba(11, 11, 14, 0.42);
    border-color: var(--c-line);
  }
  .timer__grid { gap: var(--s-2); }
  .timer__cell { min-width: 40px; }
  .timer__num { font-size: var(--t-md); }
  /* Подпись под числом набрана строчным интерльяжем основного текста —
     на 12 пикселях это лишние семь пикселей высоты у каждой ячейки. */
  .timer__unit { line-height: var(--lh-tight); }
  .timer__sep { font-size: var(--t-md); padding-top: 1px; }
  .timer__online { margin-top: 0; padding-top: var(--s-2); }
}

/* Совсем узкие экраны (320px и уже). Логотип, кнопка и бургер втроём
   требуют 340 пикселей — бургер вылезал за край и обрезался. Кнопку
   убираем: то же действие лежит в бургер-меню отдельной строкой,
   а вот бургером до него ещё нужно суметь дотянуться. */
@media (max-width: 359px) {
  .header .header__actions > .btn--primary { display: none; }
}


@media (pointer: coarse) {
  .footer__col a { min-height: var(--tap); }
}

/* ============================================================
   ХОРЕОГРАФИЯ ПЕРЕХОДОВ

   Всё на scroll-driven анимациях: они идут в композиторе, не вешают
   обработчиков на скролл и не могут его задержать. Мобильные не исключаем —
   там те же переходы, только короче ход и без параллакса фонов
   (фоновый слой на телефоне дороже всего остального вместе взятого).
   ============================================================ */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* Шапка секции приходит снизу и садится на место.
       Дальше по странице ход короче: к третьему экрану приём уже прочитан. */
    .section__head {
      animation: headRise linear both;
      animation-timeline: view();
      animation-range: entry 8% entry 62%;
      will-change: transform, opacity;
    }
    @keyframes headRise {
      from { opacity: 0; transform: translateY(30px); }
      to   { opacity: 1; transform: none; }
    }

    /* Каскад: элементы группы входят друг за другом, а не разом. */
    .stagger > * {
      animation: itemRise linear both;
      animation-timeline: view();
      will-change: transform, opacity;
    }
    @keyframes itemRise {
      from { opacity: 0; transform: translateY(22px); }
      to   { opacity: 1; transform: none; }
    }
    .stagger > *:nth-child(1) { animation-range: entry calc(6% + 0%) entry calc(58% + 0%); }
    .stagger > *:nth-child(2) { animation-range: entry calc(6% + 5%) entry calc(58% + 5%); }
    .stagger > *:nth-child(3) { animation-range: entry calc(6% + 10%) entry calc(58% + 10%); }
    .stagger > *:nth-child(4) { animation-range: entry calc(6% + 15%) entry calc(58% + 15%); }
    .stagger > *:nth-child(5) { animation-range: entry calc(6% + 20%) entry calc(58% + 20%); }
    .stagger > *:nth-child(6) { animation-range: entry calc(6% + 25%) entry calc(58% + 25%); }
    .stagger > *:nth-child(7) { animation-range: entry calc(6% + 30%) entry calc(58% + 30%); }
    .stagger > *:nth-child(8) { animation-range: entry calc(6% + 35%) entry calc(58% + 35%); }
    .stagger > *:nth-child(9) { animation-range: entry calc(6% + 40%) entry calc(58% + 40%); }

    /* Персонаж выходит из-за левой кромки навстречу панели, и дальше
       едва заметно дышит. Дыхание идёт по свойству translate, а вход —
       по transform: объявить обе на transform нельзя, вторая просто
       перебьёт первую. */
    @keyframes charIdle {
      from { translate: 0 0; }
      to   { translate: 0 -7px; }
    }
    @keyframes charStep {
      from { opacity: 0; transform: translateX(-46px) scale(0.97); }
      to   { opacity: 1; transform: none; }
    }
  }

  /* Параллакс игрового кадра: фон идёт медленнее контента, и секции
     читаются слоями, а не стопкой прямоугольников. Только десктоп. */
  @media (prefers-reduced-motion: no-preference) and (min-width: 769px) {
    .scene__img {
      animation: sceneDrift linear both;
      animation-timeline: view();
      animation-range: cover;
      will-change: transform;
    }
    @keyframes sceneDrift {
      from { transform: translateY(-5%) scale(1.1); }
      to   { transform: translateY(5%) scale(1.1); }
    }
  }
}


/* ============================================================
   КАМЕРА ОДНОГО ДУБЛЯ

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

/* Один тёплый источник, который едет по странице вместе со скроллом.
   Он и связывает секции: в герое это лампа на столе, у стадий — свечение
   активной метки, у регистрации — блик на панели. Повод разный, свет один. */
/* Слой лежит ПОД контентом (z-index: -1), а не поверх. Первая версия шла
   поверх с mix-blend-mode и роняла контраст текста по всей странице.
   Снизу свет виден там, где фоновые планы растворяются, — то есть ровно
   на стыках, которые и надо связать. */
.page-light {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}
.page-light::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  width: 160vw;
  aspect-ratio: 2 / 1;
  background: radial-gradient(closest-side, rgba(255, 178, 82, 0.16), transparent 70%);
  transform: translate(-50%, -18vh);
}

/* Засветка на смене сцены. Проезжает по кадру при скролле, на пике
   выбивает картинку и открывает следующую секцию. Ровно один раз
   на страницу: пять одинаковых засветок — дешёвый глитч-эффект. */
.leak {
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--cam-bleed) * -0.8);
  height: calc(var(--cam-bleed) * 1.6);
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  background: linear-gradient(180deg,
    transparent 0%,
    rgba(255, 186, 96, 0.10) 28%,
    rgba(255, 214, 150, 0.42) 50%,
    rgba(255, 186, 96, 0.10) 72%,
    transparent 100%);
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .page-light::before {
      animation: camLight linear both;
      animation-timeline: scroll(root block);
      will-change: transform;
    }
    @keyframes camLight {
      from { transform: translate(-50%, -34vh); }
      to   { transform: translate(-50%, 88vh); }
    }

    .leak {
      animation: leakSweep linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 72%;
      will-change: transform, opacity;
    }
    @keyframes leakSweep {
      0%   { opacity: 0; transform: translateY(-14vh) scaleY(0.7); }
      46%  { opacity: 1; transform: translateY(0) scaleY(1); }
      100% { opacity: 0; transform: translateY(12vh) scaleY(1.25); }
    }
  }
}

/* Без поддержки scroll-driven анимаций свет стоит в верхней трети,
   а засветка не показывается: статика вместо поломки. */
@media (prefers-reduced-motion: reduce) {
  .page-light::before { animation: none; }
  .leak { display: none; }
}


@media (max-width: 768px) {
  /* На телефоне 32svh растворения съедали монитор — главный объект кадра.
     Ход короче: экран меньше, и стык всё равно не виден. */
  :root { --cam-bleed: 20svh; }
}

/* Лестница слоёв: ранняя секция лежит ВЫШЕ поздней. Иначе фоновый план
   следующей секции, вынесенный за её границы, ложится поверх контента
   предыдущей и гасит его — на первом экране так замывался индикатор
   «листай ниже». Теперь план проявляется снизу, сквозь прозрачную
   нижнюю кромку предыдущей секции. */
.hero      { z-index: 8; }
#stages    { z-index: 7; }
#why       { z-index: 6; }
#client    { z-index: 5; }
#history   { z-index: 4; }
#proof     { z-index: 3; }
.registration-scene  { z-index: 2; }
#orest     { z-index: 1; }
#final     { z-index: 0; }


/* ============================================================
   ПОЧЕМУ RETRO X7 — ТЗ §20–23

   Секция не про карточки. Одна мысль на акт, крупная типографика
   и настоящая игровая сцена, которая меняется по ходу прокрутки.
   ============================================================ */

/* overflow не ставим: он обрезал бы вынос фонового плана за границы секции
   и вернул бы шов на стыке — та же ошибка, что была у блока стадий. */
/* Переход к соседним секциям. Секция обрезана по своим границам, иначе
   закреплённая сцена уезжает на блок клиента, — а обрезка давала жёсткий
   шов: тёмный фон страницы встык с кадром. Полосы растворяют обе кромки
   в фон, поэтому границы не видно. */
/* --- Туман на стыке секций ---
   Градиентом фиксированного цвета стык не закрыть: фон блока стадий меняется
   вместе с выбранной стадией. Размытие фона тоже не подошло — оно заводит
   собственную кромку: выше полосы картинка размыта, ниже резкая.

   Саму кромку сцены гасит маска на .why__stage. Здесь — атмосфера поверх
   стыка: узкая полоса тумана на фрактальном шуме. Мягкие радиальные пятна
   на этом месте читались молочным пятном — у тумана должна быть клочковатая
   структура, а не ровный градиент. Два слоя с разным зерном идут в противоход
   с разной скоростью: дальний медленно и широко, ближний быстрее и мельче. */
.seam {
  --seam-h: clamp(150px, 21svh, 280px);
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--seam-h) * -0.5);
  height: var(--seam-h);
  z-index: 1;
  pointer-events: none;
  /* Общая маска полосы: к обеим кромкам всё уходит в ноль, поэтому границы
     самого тумана не видно. */
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0%, #000 26%, #000 74%, transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0%, #000 26%, #000 74%, transparent 100%);
}
/* Ровная подложка снимает перепад тона до того, как за дело берётся туман. */
.seam__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    rgba(9, 10, 14, 0) 0%, rgba(9, 10, 14, 0.34) 42%,
    rgba(9, 10, 14, 0.34) 58%, rgba(9, 10, 14, 0) 100%);
}
.seam__fog {
  /* Слой ровно по полосе. Двигается не он, а облако внутри него: у svg
     процентная ширина не разрешалась, слой оставался по ширине окна, и сдвиг
     анимации оголял правый край — на кадре была видна вертикальная кромка.
     Облако шире вьюбокса и обрезается его границами, поэтому края нет. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
/* Ход кратен запасу по бокам, поэтому цикл замыкается без рывка. */
.seam__cloud { will-change: transform; }
.seam__fog--far {
  opacity: 0.44;
}
.seam__fog--far .seam__cloud { animation: fogFar 68s linear infinite; }
.seam__fog--near {
  opacity: 0.3;
}
.seam__fog--near .seam__cloud { animation: fogNear 43s linear infinite reverse; }
@keyframes fogFar {
  from { transform: translate(0, 0) scaleY(1); }
  50%  { transform: translate(-280px, 10px) scaleY(1.06); }
  to   { transform: translate(-560px, 0) scaleY(1); }
}
@keyframes fogNear {
  from { transform: translate(0, 0) scaleY(1); }
  50%  { transform: translate(-220px, -14px) scaleY(1.09); }
  to   { transform: translate(-440px, 0) scaleY(1); }
}
/* Тёплый отсвет — единственная точка цвета: связывает туман с акцентом
   проекта и не даёт полосе уйти в холодную серость. */
.seam__ember {
  position: absolute;
  inset: 0;
  background: radial-gradient(34% 62% at 72% 52%, rgba(247, 145, 30, 0.10), transparent 72%);
  animation: emberBreath 23s ease-in-out infinite alternate;
}
@keyframes emberBreath {
  from { opacity: 0.55; transform: translate3d(-2%, 0, 0); }
  to   { opacity: 1;    transform: translate3d(3%, -1.5%, 0); }
}

/* Туман ведёт прокрутка, а не таймер. Автономный ход выше остаётся там,
   где scroll-driven анимаций нет: туман просто дышит сам по себе.
   Где они есть — облака расходятся и набирают плотность ровно на подходе
   к стыку и отпускают его после. Секция страницу не удерживает, никакого
   закрепления: это обычная прокрутка, просто слои знают её фазу. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .seam__fog--far .seam__cloud,
    .seam__fog--near .seam__cloud,
    .seam__ember {
      animation-duration: 1ms;      /* длительность не важна: время ведёт прокрутка */
      animation-iteration-count: 1;
      animation-fill-mode: both;
      animation-timing-function: linear;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    .seam__fog--far .seam__cloud  { animation-name: seamFarPass; }
    .seam__fog--near .seam__cloud { animation-name: seamNearPass; }
    .seam__ember { animation-name: seamEmberPass; }

    @keyframes seamFarPass {
      from { transform: translate(-260px, 12px) scaleY(1.02); }
      to   { transform: translate(260px, -8px) scaleY(1.08); }
    }
    @keyframes seamNearPass {
      from { transform: translate(200px, -14px) scaleY(1.06); }
      to   { transform: translate(-200px, 10px) scaleY(1); }
    }
    /* Отсвет разгорается на самом стыке и гаснет к краям диапазона. */
    @keyframes seamEmberPass {
      from { opacity: 0.35; transform: translate3d(-3%, 0, 0); }
      50%  { opacity: 1;    transform: translate3d(0, -1%, 0); }
      to   { opacity: 0.35; transform: translate3d(3%, 0, 0); }
    }
    /* Плотность полосы тоже ведёт прокрутка: у краёв её почти нет. */
    .seam {
      animation: seamDensity 1ms linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes seamDensity {
      from { opacity: 0.55; }
      50%  { opacity: 1; }
      to   { opacity: 0.55; }
    }
  }
}
@media (prefers-reduced-motion: reduce) {
  .seam__cloud, .seam__ember { animation: none; transform: none; }
}
/* Снизу отдельной полосы нет намеренно: фон блока клиента сам выносится вверх за свою
   границу и там затухает. Полоса перекрывала этот вынос и рисовала ровно тот
   шов, который должна была убрать. */

.why {
  position: relative;
}

/* Сцена: пять кадров стопкой, каждый проявляется в свою четверть прокрутки.
   Смена эпохи показана сменой места, а не сменой брони — рендеров
   персонажа по грейдам у проекта нет, а выдумывать их нельзя (ТЗ §54). */
.why__stage {
  /* Сцена закреплена на высоту экрана, а не растянута на всю секцию.
     Растянутый кадр приходилось увеличивать в три раза, и от игры
     оставалось однотонное пятно — тот самый «бессмысленный размытый фон»,
     который ТЗ §19 требует убрать. Закреплённая сцена держит масштаб
     кадра и заодно даёт эффект «герой остаётся на месте, меняется мир». */
  position: sticky;
  top: 0;
  height: 100svh;
  /* Закреплённый слой остаётся в потоке и занимал бы целый экран, отодвигая
     текст вниз: после карты шёл пустой кадр во весь экран и только потом
     заголовок. Отрицательный отступ снимает его вклад в поток — контент
     ложится прямо на сцену, как и задумано (ТЗ §19). */
  z-index: 0;
  overflow: hidden;
  /* Верхняя кромка сцены гасится в ноль. Раньше кадр начинался обрывом:
     выше тёмный фон, ниже сразу освещённая сцена — контраст такой, что
     никакая дымка сверху его не закрывала. Растворять нужно саму кромку,
     а не прятать её.

     Пока сцена не закреплена, маска съедает её начало — ровно то, что
     нужно на стыке. Когда закрепится, тем же градиентом уходит верх окна:
     мягкая виньетка под шапкой, она не мешает.

     Нижняя кромка гасится так же. Раньше маска доводила кадр до 100%
     непрозрачности и обрывала его вплотную: когда сцена перестаёт быть
     закреплённой, её низ оказывается посреди страницы и стыкуется с
     кадром соседней секции — два кадра встык и дают шов. Пока сцена
     закреплена, это просто симметричная виньетка снизу экрана. */
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0%, #000 20%, #000 84%, transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0%, #000 20%, #000 84%, transparent 100%);
}
.why__frame {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  /* ТЗ §19: за прогрессией должна жить настоящая игра, а не размытая
     темнота. Кадр поднят до читаемого; текстовую колонку держит вуаль. */
  filter: saturate(0.7) brightness(0.62);
  transform: scale(1.06);
}
.why__frame:first-child { opacity: 1; }

/* Вуаль выносится за границы секции и гасится маской — иначе обрывается
   на кромке и даёт ступеньку по тону. Ровно та же ошибка, что была
   у атмосферы стадий. */
.why__veil {
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--cam-bleed) * -1);
  bottom: calc(var(--cam-bleed) * -1);
  z-index: 1;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--cam-bleed), #000 calc(100% - var(--cam-bleed)), transparent 100%);
  /* Диагональ в 100° на высокой секции уходила мимо текстовой колонки:
     0.94 оказывалось в верхнем левом углу, а не там, где текст. Разделяем
     на горизонтальную (текст слева) и вертикальную составляющие. */
  background:
    radial-gradient(70% 45% at 24% 30%, rgba(247, 145, 30, 0.10), transparent 70%),
    /* Плавнее по горизонтали: на трёх стопах переход от тёмной колонки
       к открытому кадру читался вертикальным швом. */
    linear-gradient(90deg,
      rgba(6, 8, 12, 0.88) 0%, rgba(6, 8, 12, 0.84) 22%, rgba(6, 8, 12, 0.72) 42%,
      rgba(6, 8, 12, 0.52) 62%, rgba(6, 8, 12, 0.36) 82%, rgba(6, 8, 12, 0.3) 100%),
    linear-gradient(180deg, rgba(6, 8, 12, 0.35) 0%, transparent 22%, transparent 74%, rgba(6, 8, 12, 0.4) 100%);
}
/* Контент поднимается на сцену. Отрицательный отступ висел на самой сцене,
   и та переставала упираться в низ секции — уезжала на блок клиента, из-за
   чего секцию пришлось обрезать, а обрезка давала жёсткий шов. Теперь
   закрепление ограничено секцией само, обрезка не нужна. */
.why > .wrap { position: relative; z-index: 2; margin-top: -100svh; }

.why__title { white-space: normal; max-width: none; }

.why__act { margin-top: var(--s-9); display: grid; gap: var(--s-5); }
/* Одна мысль — одна крупная строка. Ради этого блок и существует:
   очередной ряд карточек здесь ничего бы не добавил. */
.why__claim {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: clamp(1.75rem, 3.4vw, 3rem);
  line-height: 1.1;
  letter-spacing: var(--tr-display);
  max-width: 20ch;
}
.why__note { color: var(--c-text-2); max-width: 52ch; }

/* --- Лестница грейдов --- */
/* Обойма прокрутки. Семь ступеней в ряд на 1024px давали по 120px на
   ступень: набор экипировки рвался на три строки, «Vesper / Vorpal»
   переносился, и ряд читался кашей. Теперь ступень держит свой минимум,
   а лишнее уезжает вбок — цепочку можно пролистать. */
.ladder-rail {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--c-line-strong) transparent;
  /* Место под полосу прокрутки, иначе она наезжает на фразы ступеней. */
  padding-bottom: var(--s-2);
}
/* Правый край растворяется, когда ряд длиннее обоймы: на macOS полосы
   прокрутки не видно, пока её не тронешь, и о том, что цепочка едет
   дальше, сказать было нечем. Порог — по ширине самой колонки, а не
   окна: он совпадает с той шириной, на которой ряд перестаёт помещаться. */
.why__act { container-type: inline-size; }
@container (max-width: 1250px) {
  .ladder-rail {
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent 100%);
    mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent 100%);
  }
}
@media (max-width: 900px) {
  .ladder-rail { -webkit-mask-image: none; mask-image: none; }
}
.ladder {
  display: grid;
  grid-template-columns: repeat(var(--n, 5), minmax(0, 1fr));
  gap: var(--s-4);
  counter-reset: none;
  position: relative;
  padding-top: var(--s-5);
  /* Ширина ряда считается по числу ступеней (--n приходит из скрипта):
     пока сумма минимумов помещается — ряд тянется на всю ширину, дальше
     обойма прокручивается. */
  min-inline-size: calc(var(--n, 5) * var(--ladder-step) + (var(--n, 5) - 1) * var(--s-4));
}
/* Нить, на которой сидят ступени */
.ladder::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: calc(var(--s-5) + 5px);
  /* Два пикселя нейтрального света: однопиксельная нить с акцентным
     началом терялась на фотофоне, и за движением реки было не уследить. */
  height: var(--river-line-h);
  background: var(--river-base);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
}
.ladder__step {
  display: grid;
  gap: 8px;
  justify-items: start;
  opacity: 0.4;
  transition: opacity var(--d-base) var(--e-cam);
}
.ladder__dot {
  width: 11px; height: 11px;
  rotate: 45deg;
  border: 1.5px solid var(--c-line-strong);
  background: var(--c-void);
  transition: all var(--d-base) var(--e-cam);
}
.ladder__grade {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: var(--t-lg);
  line-height: 1.1;
}
.ladder__stage {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
}
/* Экипировка эпохи — ряд названий сетов, вторичный по отношению к грейду. */
.ladder__gear {
  font-size: var(--t-caption);
  color: var(--c-text-2);
  line-height: 1.4;
}
/* Фраза ступени: ради неё экран и существует — это память, а не спецификация.
   Ступень раскрывает её только когда активна, иначе семь фраз читаются
   сплошной стеной (ТЗ §18: экран 2 — эмоция, а не повтор механики). */
.ladder__phrase {
  font-family: var(--f-display);
  /* 14-й кегль антиквой поверх фотографии читался мылом: у засечек на
     этом размере не остаётся ни одного целого пикселя, а текст на
     полупрозрачном фоне рисуется без субпиксельного сглаживания.
     Кегль поднят до основного, интерлиньяж — под него. */
  font-size: var(--t-base);
  line-height: 1.4;
  color: var(--c-text-1);
  max-width: 26ch;
  opacity: 0;
  translate: 0 4px;
  transition: opacity var(--d-base) var(--e-out), translate var(--d-base) var(--e-out);
}
.ladder__step.is-on .ladder__phrase,
.ladder:hover .ladder__step:hover .ladder__phrase { opacity: 1; translate: 0 0; }
/* В режиме покоя прокруточные анимации не запускаются, а стартовая
   прозрачность у ступени и фразы нулевая — без этого правила половина
   экрана осталась бы пустой. ТЗ §23: движение убираем, контент оставляем
   весь. */
@media (prefers-reduced-motion: reduce) {
  .ladder__phrase { transition: none; translate: 0 0; opacity: 1; }
  .ladder__step { opacity: 1; }
}


/* --- Мозаика узнаваемых локаций ---
   Не шесть одинаковых прямоугольников: размеры разные, ритм неровный. */
.why__mosaic {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--s-3);
}
.why__tile {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  border: 1px solid var(--c-line);
  aspect-ratio: 16 / 10;
}
.why__tile img { width: 100%; height: 100%; object-fit: cover; }
.why__tile::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 40%, rgba(6, 8, 12, 0.55) 100%);
}
.why__tile:nth-child(1) { grid-column: span 3; aspect-ratio: 16 / 8; }
.why__tile:nth-child(2) { grid-column: span 3; aspect-ratio: 16 / 8; }
.why__tile:nth-child(3) { grid-column: span 2; }
.why__tile:nth-child(4) { grid-column: span 2; }
.why__tile:nth-child(5) { grid-column: span 2; }

.why__bridge {
  margin-top: var(--s-9);
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: clamp(1.5rem, 2.6vw, 2.25rem);
  line-height: 1.15;
  color: var(--c-text-2);
  max-width: 24ch;
}

@media (max-width: 900px) {
  /* На узком экране цепочка не едет вбок, а складывается в колонки:
     боковая прокрутка внутри вертикальной ленты на телефоне только мешает.
     Минимум ступени снимаем, иначе обойма осталась бы прокручиваемой. */
  .ladder { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-5); min-inline-size: 0; }
  .ladder-rail { overflow-x: visible; padding-bottom: 0; }
  .ladder::before { display: none; }
  .why__mosaic { grid-template-columns: repeat(2, 1fr); }
  .why__tile:nth-child(n) { grid-column: span 1; aspect-ratio: 16 / 10; }
  .why__tile:nth-child(1) { grid-column: span 2; aspect-ratio: 16 / 9; }
}

/* На телефоне две колонки дают 157px на ступень: набор экипировки
   («Blue Wolf · Doom · Avadon · Zubei») рвётся там на четыре строки,
   и ступень перестаёт читаться списком. Ниже 560px — одна колонка. */
@media (max-width: 560px) {
  .ladder { grid-template-columns: minmax(0, 1fr); gap: var(--s-4); }
}

/* Сцена и ступени ведутся прокруткой: кадр эпохи и её ступень
   загораются одновременно. Без поддержки остаётся первый кадр. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .why__frame {
      animation: whyFrame linear both;
      animation-timeline: view();
      animation-range: cover calc(12% + var(--i) * var(--lit-gap, 17%)) cover calc(30% + var(--i) * var(--lit-gap, 17%));
      will-change: opacity;
    }
    @keyframes whyFrame { to { opacity: 1; } }

    /* Лестница грейдов прокруткой больше не ведётся — см. «Река по нити
       грейдов» ниже. */

    .why__tile {
      animation: tileIn linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 62%;
      animation-delay: calc(var(--i) * -1ms);
    }
    @keyframes tileIn {
      from { opacity: 0; transform: translateY(18px) scale(0.98); }
      to   { opacity: 1; transform: none; }
    }
  }
}


/* --- Река по нити грейдов ---
   Подсветку ступеней раньше вела прокрутка. Это сломалось, когда лестница
   получила прокручиваемую обойму: view() стал считаться от неё, а она по
   вертикали не двигается. Да и по смыслу это не прокрутка: прогрессия
   мира идёт сама, от B-grade к Elegia, — как течёт река.

   Класс .is-live ставит скрипт, когда лестница в кадре, и снимает на
   выходе; при возвращении ход идёт с начала. Хореография одного цикла
   (--river-run, гребень ходит бесконечно):
     0 … lead        гребень стоит у первой ступени и проявляется
     lead + i·β      гребень доходит до ступени i (β = (run − lead)/n,
                     скрипт кладёт момент в --t ступени): ромб заливается
                     акцентом, подпись диапазона — акцентом, ступень
                     выходит из полутона; через --river-copy-delay
                     проявляется фраза. Всё это — один раз, навсегда.
     run             гребень у последней ступени и гаснет; заливка нити
                     остаётся, всё горит. Повторов нет: один проход. */
.ladder__stage { transition: color var(--d-base) var(--e-out); }

/* Контейнер реки лежит на нити; сама нить — .ladder::before. */
.ladder__river {
  position: absolute;
  left: 0; right: 0;
  top: calc(var(--s-5) + 5px);
  height: var(--river-line-h);
  pointer-events: none;
}
/* Заливка за гребнем: тёплая нить поверх серой. */
.ladder__river::before {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(90deg, var(--c-accent), var(--c-accent-hi));
  opacity: var(--river-active-opacity);
  transform: scaleX(0);
  transform-origin: left center;
}
/* Хвост гребня: размытый след позади ядра. Правый край стоит на позиции. */
.ladder__river::after {
  content: '';
  position: absolute;
  left: 0; top: 50%;
  width: var(--river-tail-w); height: var(--river-tail-h);
  translate: -100% -50%;
  background: linear-gradient(90deg, transparent 0%, rgba(247, 145, 30, 0.10) 34%, rgba(255, 192, 30, 0.42) 100%);
  filter: blur(5px);
  opacity: 0;
}
/* Ядро гребня: мягкое пятно света, а не штрих. Центр — на позиции. */
.ladder__crest {
  position: absolute;
  left: 0; top: 50%;
  width: var(--river-core-w); height: var(--river-core-h);
  translate: -50% -50%;
  border-radius: 999px;
  background: radial-gradient(ellipse at center, var(--c-accent-hi) 0%, var(--c-accent) 42%, transparent 72%);
  filter: drop-shadow(0 0 12px rgba(255, 192, 30, 0.58));
  opacity: 0;
}

/* Первый проход — один раз: заливка нити и зажигание ступеней. */
.ladder.is-live .ladder__river::before {
  animation: riverFill calc(var(--river-run) - var(--river-lead)) linear both;
  animation-delay: var(--river-lead);
}
@keyframes riverFill { to { transform: scaleX(1); } }

/* Гребень проходит нить один раз и гаснет у последней ступени. Цикл равен
   --river-run целиком, чтобы совпадать с ромбами: первые 4.9% (0.4s из
   8.2s) — стоянка у старта. Бесконечный ход был и раздражал: свет бегал
   по одному и тому же пути. Дошёл до конца — всё горит, и хватит. */
.ladder.is-live .ladder__crest,
.ladder.is-live .ladder__river::after {
  animation: riverCrest var(--river-run) linear both;
}
@keyframes riverCrest {
  0%     { left: 0%;   opacity: 0; }
  4.88%  { left: 0%;   opacity: 1; }
  97%    { opacity: 1; }
  100%   { left: 100%; opacity: 0; }
}
.ladder.is-live .ladder__crest { animation-name: riverCrestCore; }
@keyframes riverCrestCore {
  0%     { left: 0%;   opacity: 0; }
  4.88%  { left: 0%;   opacity: var(--river-core-opacity); }
  97%    { opacity: var(--river-core-opacity); }
  100%   { left: 100%; opacity: 0; }
}
.ladder.is-live .ladder__river::after { animation-name: riverCrestTail; }
@keyframes riverCrestTail {
  0%     { left: 0%;   opacity: 0; }
  4.88%  { left: 0%;   opacity: var(--river-tail-opacity); }
  97%    { opacity: var(--river-tail-opacity); }
  100%   { left: 100%; opacity: 0; }
}

/* Ступень: из полутона в полный — один раз, когда гребень до неё дошёл. */
.ladder.is-live .ladder__step {
  animation: ladderLit var(--river-on) var(--e-out) both;
  animation-delay: var(--t, 0s);
}
@keyframes ladderLit { to { opacity: 1; } }
/* Ромб: залился акцентом один раз (и остался чуть приглушённым — пройден),
   а в момент, когда гребень его проходит, даёт короткий импульс со свечением. */
.ladder.is-live .ladder__dot {
  animation:
    ladderDotOn var(--river-on) var(--e-out) both,
    ladderDotPulse var(--river-run) linear both;
  animation-delay: var(--t, 0s), var(--t, 0s);
}
@keyframes ladderDotOn {
  to { border-color: var(--c-accent); background: var(--c-accent); opacity: var(--dot-settled-opacity); }
}
@keyframes ladderDotPulse {
  0%    { transform: scale(1);    box-shadow: 0 0 0 0 rgba(247, 145, 30, 0); }
  1.3%  { transform: scale(1.22); box-shadow: 0 0 14px 3px rgba(247, 145, 30, 0.45); }
  2.7%  { transform: scale(1);    box-shadow: 0 0 0 0 rgba(247, 145, 30, 0); }
  100%  { transform: scale(1);    box-shadow: 0 0 0 0 rgba(247, 145, 30, 0); }
}
.ladder.is-live .ladder__stage {
  animation: ladderStage var(--river-on) var(--e-out) both;
  animation-delay: var(--t, 0s);
}
@keyframes ladderStage { to { color: var(--c-accent); } }
/* Фраза — чуть после своей ступени: на ней держится смысл экрана (ТЗ §18). */
.ladder.is-live .ladder__phrase {
  animation: ladderPhrase var(--river-copy-in) var(--e-out) both;
  animation-delay: calc(var(--t, 0s) + var(--river-copy-delay));
}
@keyframes ladderPhrase { to { opacity: 1; translate: 0 0; } }

/* На телефоне ступени стоят колонками и нити нет — река не рисуется,
   ступени всё равно загораются по очереди. */
@media (max-width: 900px) {
  .ladder__river { display: none; }
}

/* Без движения и без скрипта лестница стоит зажжённой целиком: контент
   остаётся весь (ТЗ §23). Гребень в покое не ходит. */
@media (prefers-reduced-motion: reduce) {
  .ladder__dot { border-color: var(--c-accent); background: var(--c-accent); opacity: var(--dot-settled-opacity); }
  .ladder__stage { color: var(--c-accent); }
  .ladder__river::before { transform: none; }
  .ladder__crest, .ladder__river::after { display: none; }
}
html:not(.js) .ladder__step { opacity: 1; }
html:not(.js) .ladder__phrase { opacity: 1; translate: 0 0; }
html:not(.js) .ladder__dot { border-color: var(--c-accent); background: var(--c-accent); }


/* ============================================================
   ФИНАЛЬНЫЙ ПРИЗЫВ — ТЗ §43

   Последний экран не блок с функциями, а сцена. Интерфейса почти нет:
   по §46 к этому моменту он растворяется, остаётся игровой мир.
   Поэтому здесь нет ни рамок, ни карточек, ни сетки — только кадр,
   три строки и одна кнопка.
   ============================================================ */

.final {
  position: relative;
  min-height: clamp(400px, 56svh, 560px);
  display: grid;
  align-items: center;
  padding-block: var(--s-8);
}
/* Кадр светлее остальных секций: это эмоциональная выплата, и чёрный
   здесь должен быть глубиной, а не содержанием (ТЗ §13). */
.scene--final::after {
  background:
    radial-gradient(65% 55% at 30% 52%, rgba(6, 8, 12, 0.86) 0%, rgba(6, 8, 12, 0.5) 60%, rgba(6, 8, 12, 0.3) 100%),
    linear-gradient(180deg, rgba(6, 8, 12, 0.55) 0%, transparent 24%, transparent 68%, rgba(6, 8, 12, 0.8) 100%);
}
.scene--final .scene__img { filter: brightness(1.06) saturate(0.94); }

/* max-width сюда ставить нельзя: этот элемент — сам .wrap, и вместе
   с его margin-inline: auto блок уезжал в центр экрана. Ширину
   ограничиваем на строках. */
.final__inner {
  position: relative;
  z-index: var(--z-content);
  display: grid;
  justify-items: start;
  gap: var(--s-3);
}
.final__lead {
  font-size: var(--t-md);
  color: var(--c-text-2);
  letter-spacing: 0.01em;
  max-width: 30ch;
}
/* Знак, а не заголовок: это подпись сцены, поэтому у неё нет уровня
   в структуре документа и она скрыта от скринридера. */
.final__mark {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: clamp(3rem, 7.5vw, 6.5rem);
  line-height: 0.94;
  letter-spacing: var(--tr-display);
  white-space: nowrap;
  text-shadow: 0 6px 40px rgba(0, 0, 0, 0.85);
}
.final__call {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: clamp(1.5rem, 2.8vw, 2.25rem);
  line-height: 1.15;
  color: var(--c-text-2);
  max-width: 24ch;
}
.final__cta {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  flex-wrap: wrap;
  margin-top: var(--s-4);
}
/* Вторая ссылка намеренно маленькая: один главный CTA на экран (§53). */
.final__link {
  font-size: var(--t-sm);
  color: var(--c-text-2);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-color: var(--c-line-strong);
  transition: color var(--d-fast) var(--e-out);
}
.final__link:hover { color: var(--c-accent); }

@media (max-width: 768px) {
  .final { min-height: 0; }
  .final__lead, .final__call { max-width: 20ch; }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Камера успокаивается: кадр приезжает чуть крупнее и садится на место. */
    .scene--final .scene__img {
      animation: finalSettle linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 90%;
      will-change: transform;
    }
    @keyframes finalSettle {
      from { transform: scale(1.14) translateY(2%); }
      to   { transform: scale(1) translateY(0); }
    }

    .final__inner > * {
      animation: finalRise linear both;
      animation-timeline: view();
      will-change: transform, opacity;
    }
    @keyframes finalRise {
      from { opacity: 0; transform: translateY(26px); }
      to   { opacity: 1; transform: none; }
    }
    .final__inner > *:nth-child(1) { animation-range: entry 14% entry 52%; }
    .final__inner > *:nth-child(2) { animation-range: entry 20% entry 60%; }
    .final__inner > *:nth-child(3) { animation-range: entry 26% entry 66%; }
    .final__inner > *:nth-child(4) { animation-range: entry 32% entry 72%; }

    /* §46: интерфейс Ореста растворяется на выходе, и остаётся только мир. */
    .orest__grid {
      animation: uiDissolve linear both;
      animation-timeline: view();
      animation-range: exit 10% exit 90%;
      will-change: transform, opacity;
    }
    @keyframes uiDissolve {
      from { opacity: 1; transform: none; filter: blur(0); }
      to   { opacity: 0; transform: translateY(-24px) scale(0.97); filter: blur(6px); }
    }
  }
}


/* ============================================================
   ВХОД В МИР — ТЗ §11

   Две строки проявляются поверх первого экрана, пока камера входит
   в монитор. Отдельной секцией они не становятся: это подпись
   к движению, а не самостоятельный экран.
   ============================================================ */

.dive {
  /* Без scroll-driven анимаций перехода не существует, и строки не должны
     висеть поверх первого экрана — поэтому по умолчанию слоя нет вовсе. */
  display: none;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .dive {
      display: grid;
      position: absolute;
      inset: 0;
      z-index: calc(var(--z-content) + 1);
      place-items: center;
      padding-inline: var(--s-5);
      pointer-events: none;
    }
    .dive__line {
      grid-area: 1 / 1;
      max-width: 24ch;
      text-align: center;
      font-family: var(--f-display);
      font-variant-numeric: var(--fvn-display);
      font-weight: 500;
      font-size: clamp(1.5rem, 3.4vw, 2.75rem);
      line-height: 1.15;
      letter-spacing: var(--tr-display);
      text-shadow: 0 4px 28px rgba(0, 0, 0, 0.9);
      opacity: 0;
      animation: diveLine linear both;
      animation-timeline: view();
      will-change: transform, opacity;
    }
    /* Первая строка ведёт вход, вторая догоняет и уходит вместе с кадром. */
    .dive__line:nth-child(1) { animation-range: exit 10% exit 52%; }
    .dive__line:nth-child(2) { animation-range: exit 46% exit 94%; }
    @keyframes diveLine {
      0%   { opacity: 0; transform: translateY(16px) scale(0.98); }
      28%  { opacity: 1; transform: none; }
      68%  { opacity: 1; transform: none; }
      100% { opacity: 0; transform: translateY(-12px) scale(1.03); }
    }
  }
}


/* Холодный рефлекс от мокрого стекла внутрь комнаты — ТЗ §8 разрешает.
   Очень слабый: это отсвет, а не источник света. */
.hero__wetglow {
  position: absolute;
  left: 62%; top: -4%;
  width: 26%; height: 62%;
  z-index: 2;
  pointer-events: none;
  background: radial-gradient(closest-side, rgba(126, 176, 224, 0.13), transparent 72%);
  filter: blur(18px);
}
@media (max-aspect-ratio: 1 / 1) {
  /* В портретной сцене окно одно и стоит выше — рефлекс переезжает за ним. */
  .hero__wetglow { left: 58%; top: 0%; width: 40%; height: 34%; }
}


/* ============================================================
   ОРЕСТ — ЖИВАЯ ДЕМОНСТРАЦИЯ, ТЗ §41–42

   Семь одинаковых карточек делали из экрана админ-панель. Теперь слева
   компактный список функций, справа — окно Ореста, и это главный элемент:
   в нём по очереди приходят сообщения, а слева загорается та функция,
   которая его прислала. Пользователь не читает обещание, а видит работу.
   ============================================================ */

.orest__list {
  display: grid;
  gap: 2px;
  margin: var(--s-6) 0 var(--s-6);
}
.orest-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-3);
  align-items: start;
  padding: 10px 12px;
  border-radius: var(--r-md);
  border: 1px solid transparent;
  transition: background var(--d-base) var(--e-out), border-color var(--d-base) var(--e-out);
}
.orest-item__icon {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  border-radius: var(--r-sm);
  border: 1px solid var(--c-line);
  color: var(--c-text-3);
  background: rgba(10, 12, 16, 0.5);
  transition: all var(--d-base) var(--e-out);
}
.orest-item__icon svg { width: 17px; height: 17px; }
.orest-item__body { display: grid; gap: 2px; }
.orest-item__body b { font-size: var(--t-sm); font-weight: 500; color: var(--c-text-2); }
/* Пояснение приглушено и раскрывается только у активной функции:
   семь описаний разом — это снова стена текста. */
.orest-item__body i {
  font-style: normal;
  font-size: var(--t-caption);
  color: var(--c-text-3);
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  transition: opacity var(--d-base) var(--e-out), max-height var(--d-base) var(--e-out);
}

/* Активная функция — та, что прислала сообщение */
.orest-item.is-live {
  background: rgba(247, 145, 30, 0.07);
  border-color: var(--c-accent-soft);
}
.orest-item.is-live .orest-item__icon {
  border-color: var(--c-accent);
  color: var(--c-accent-hi);
  box-shadow: 0 0 16px rgba(247, 145, 30, 0.3);
}
.orest-item.is-live .orest-item__body b { color: var(--c-text); }
.orest-item.is-live .orest-item__body i { opacity: 1; max-height: 4em; }

@media (prefers-reduced-motion: reduce) {
  .orest-item__body i { opacity: 1; max-height: none; }
}

/* --- Персонаж: ТЗ §42 ---
   Раньше висел отдельным PNG над окном. Теперь стоит ЗА окном чата,
   частично им перекрыт, с контактной тенью и мягким ambient-светом —
   он часть сцены, а не наклейка поверх фона. */
.orest__aside { position: relative; }
/* Персонаж поднят над окном чата: при bottom: 0 он целиком уходил
   за непрозрачную панель, и его просто не было видно. */
.orest__character {
  position: absolute;
  right: -7%;
  top: -26%;
  z-index: 0;
  width: min(270px, 56%);
  height: auto;
  pointer-events: none;
  filter: drop-shadow(0 26px 34px rgba(0, 0, 0, 0.75)) brightness(0.86) saturate(0.9);
}
/* Ambient: тёплое пятно за персонажем — он перестаёт быть вырезкой */
.orest__aside::before {
  content: '';
  position: absolute;
  right: -2%; top: -14%;
  width: 46%; aspect-ratio: 1;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(247, 145, 30, 0.16), transparent 72%);
  filter: blur(26px);
  pointer-events: none;
}
.orest__aside .tg { position: relative; z-index: 1; }

@media (max-width: 900px) {
  /* ТЗ §49: на телефоне окно Ореста — главный элемент, персонаж вторичен. */
  .orest__character { width: 38%; right: -2%; top: -14%; opacity: 0.65; }
}


/* ============================================================
   ИСТОРИЯ THEBATTLE.CLUB — ТЗ §28–31

   Не корпоративная презентация, а лента воспоминаний: кадры едут
   медленнее контента, годы идут от первого к RETRO X7. Кадры —
   настоящие материалы TheBattleClub, состаренные фильтром; генерировать
   «архив» реального проекта нельзя, это подделка записи.
   ============================================================ */

/* В секции много текста, а кадр stage-6 яркий и лиловый — общей вуали
   .scene::after не хватало, подписи вех тонули. Своя, заметно плотнее. */
.history { position: relative; }
.history .scene__img { filter: brightness(0.42) saturate(0.5) contrast(0.95); }
.history .scene::after {
  background:
    linear-gradient(90deg, rgba(6, 8, 12, 0.95) 0%, rgba(6, 8, 12, 0.9) 52%, rgba(6, 8, 12, 0.66) 100%),
    linear-gradient(0deg, rgba(6, 8, 12, 0.9) 0%, rgba(6, 8, 12, 0.55) 46%, rgba(6, 8, 12, 0.8) 100%);
}
.history__title { white-space: normal; max-width: none; }
.history__intro { color: var(--c-text-2); max-width: 62ch; margin-top: var(--s-3); }
.history__outro {
  margin-top: var(--s-8);
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-weight: 500;
  font-size: clamp(1.25rem, 2.2vw, 1.75rem);
  line-height: 1.25;
  color: var(--c-text-2);
  max-width: 34ch;
}


/* --- Слои игрового кадра ---
   Общие для всех секций со сценой. Порядок фиксирован: тон секции даёт
   температуру, локальный свет возвращает кадру объём, scrim держит контраст
   текста, виньетка собирает края, зерно снимает цифровую гладкость.
   Каждый слой — отдельный элемент, а не тень на одном: иначе их нельзя
   гасить по отдельности и настраивать под секцию. */
.grain-def { position: absolute; width: 0; height: 0; overflow: hidden; }

.scene-tone,
.scene-light,
.scene-vignette,
.scene-grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.scene-tone {
  z-index: 1;
  background: linear-gradient(180deg,
    var(--c-backdrop-tone) 0%, rgba(8, 8, 10, 0.16) 46%, var(--c-backdrop-tone) 100%);
}
/* Тёплое пятно у фокуса кадра и холодный отсвет в противоположном углу:
   один источник света читается плоско. */
.scene-light {
  z-index: 2;
  background:
    radial-gradient(circle var(--backdrop-light-size)
      at var(--backdrop-light-x) var(--backdrop-light-y),
      var(--c-backdrop-amber) 0%, transparent 68%),
    radial-gradient(circle calc(var(--backdrop-light-size) * 0.72) at 18% 72%,
      var(--c-backdrop-cold) 0%, transparent 72%);
}
.scene-vignette {
  z-index: 4;
  background:
    radial-gradient(ellipse at center,
      transparent 0%, transparent var(--backdrop-vignette-stop),
      var(--c-backdrop-vignette) 100%),
    linear-gradient(180deg,
      rgba(0, 0, 0, 0.18) 0%, transparent 18%,
      transparent 78%, rgba(0, 0, 0, 0.44) 100%);
}
.scene-grain {
  z-index: 5;
  opacity: var(--backdrop-grain-opacity);
  mix-blend-mode: soft-light;
}
.scene-grain rect { width: 100%; height: 100%; filter: url(#rx7-grain); }

/* --- История: монтаж вех ---
   Кадры перелистываются как страницы комикса: следующий наезжает на
   предыдущий вытеснением слева направо, край подчёркнут тенью. Шкалы лет
   и рассказов по годам нет — блок работает как одна эмоция, а не справочник.

   Монтаж идёт по времени, а не по прокрутке: киновступление узнаётся именно
   ритмом, которого прокрутка не даёт — она зависит от того, как быстро
   листает пользователь. Страницу блок при этом не удерживает. */
.saga {
  position: relative;
  isolation: isolate;
  overflow: clip;
  /* Монтажу нужна высота: иначе кадры прижимаются к нижней кромке и
     читаются обрезком фона, а не сценой. */
  min-height: clamp(560px, 76svh, 820px);
  display: grid;
  align-items: center;
}

.saga__video {
  /* Сцена занимает весь блок и выносится за боковые кромки.
     Задаём левый край и размеры, а не inset со всех сторон: у заменяемого
     элемента ширина auto берётся из самого файла, и кадр вставал по своим
     размерам вместо блока. Правую границу через inset задавать нельзя —
     вместе с шириной она сдвигала кадр. */
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  /* Вынос за боковые кромки убран, и это не косметика. Секция и так во всю
     ширину, а вынос делал контейнер шире кадра — 2.95 против 2.10, — и
     object-fit: cover срезал почти треть высоты, попутно растягивая
     картинку в 1.4 раза. Отсюда шли сразу две претензии: «слишком
     приближено» и мягкость финального плана. Теперь пропорции контейнера
     и монтажа совпадают, кадр виден целиком и почти не масштабируется. */
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  filter: saturate(0.9) contrast(1.02);
  /* Кромка гасится маской, а не закрашивается непрозрачным фоном.
     Красить нельзя: задник следующей секции по системе страницы
     поднимается сюда на --cam-bleed и должен проступать сквозь низ
     истории. Непрозрачная заливка его глушила, и на границе оставалась
     ступенька по тону — снизу секция была темнее соседа.
     Длина растворения короче --cam-bleed: полная съедала середину
     монтажа, а сосед всё равно проявляется на своей длине. */
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 calc(var(--cam-bleed) * 0.7),
    #000 calc(100% - var(--cam-bleed) * 0.7), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 calc(var(--cam-bleed) * 0.7),
    #000 calc(100% - var(--cam-bleed) * 0.7), transparent 100%);
}

/* Вуаль: сцена остаётся фоном, текст держит контраст на любом кадре. */
.saga__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Затемнение идёт непрерывно, без плато. Ровная плотная зона слева
     читалась прямоугольной подложкой под текст и возвращала блок к схеме
     «текст слева, картинка справа» — ровно к той простоте, от которой мы
     уходим. Теперь это глубина кадра: плотно у левой кромки, к середине
     мягче, к 74% кадр открыт. */
  background:
    linear-gradient(90deg, var(--overlay-strong) 0%, var(--overlay-strong) 34%,
      var(--overlay-soft) 58%, var(--overlay-faint) 78%, transparent 94%);
  /* Затемнение тоже нельзя обрывать по границе: слева оно плотное, и его
     кромка читалась бы полосой. Растворяем короче, чем кадр, — вуаль
     плоская, её край почти не виден, а тексту нужен контраст подольше. */
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 calc(var(--cam-bleed) * 0.45),
    #000 calc(100% - var(--cam-bleed) * 0.45), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 calc(var(--cam-bleed) * 0.45),
    #000 calc(100% - var(--cam-bleed) * 0.45), transparent 100%);
}

.saga__copy {
  /* Текст стоит одним кластером в нижней левой зоне: раньше финальная
     строка отрывалась от лида пустотой и читалась второй остановкой. */
  position: relative;
  z-index: 2;
  align-self: end;
  /* padding-block здесь не нужен: .section уже держит --section-y с обеих
     сторон, и второй отступ отбрасывал текст на 128px вверх — под ним
     оставалась четверть высоты пустой темноты без функции. */
  display: grid;
  gap: var(--s-5);
  justify-items: start;
  /* max-width здесь стоять не может: он перебивал ограничение .wrap, блок
     растягивался во всю ширину окна, и текст садился на 46px вместо общей
     левой линии страницы в 86px. Переполнение держит min-width: 0. */
  min-width: 0;
}
.saga__title { white-space: normal; max-width: none; }
/* pre-line: разбивка лида задана в тексте, как и у заголовка. При
   автопереносе вторая фраза цеплялась хвостом к первой и лид читался
   одним потоком вместо двух утверждений. */
.saga__lede { max-width: 52ch; overflow-wrap: normal; white-space: normal; }

/* Шкала десятилетия — неподвижная смысловая опора под текстом. Ни счётчика
   кадров, ни номеров сцен, ни прогресса ролика: у блока уже есть живой ритм
   от смены планов, второй слой данных превратил бы киноэкран в интерфейс.
   Акцент здесь запрещён — только линия и третичный текст. */
.saga__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s-4);
  margin-block-start: var(--s-7);
  padding-block-start: var(--s-3);
  border-block-start: 1px solid var(--c-line);
  width: 100%;
  /* Шире текстовой колонки намеренно: короткая линия заканчивалась вместе
     с лидом и работала нижней границей случайного прямоугольника. Теперь
     она уходит вправо и физически связывает текст с кадром. */
  max-width: min(64ch, 100%);
  list-style: none;
  font-size: var(--t-caption);
  letter-spacing: var(--tr-eyebrow);
  font-variant-numeric: tabular-nums;
  color: var(--c-text-3);
}

/* Единственный сценарий на прокрутке в блоке. Соседние секции живут —
   стадии переключаются, клиент листается, лестница грейдов растёт, — а
   история стояла неподвижно, и на их фоне читалась недоделанной. Текст
   собирается снизу вверх, шкала приходит последней.

   Только view() — обработчиков на scroll в проекте нет и быть не должно:
   такие анимации идут в композиторе и физически не могут задержать
   прокрутку. Таймкод ролика к прокрутке НЕ привязан: темп чтения у каждого
   свой, скраббер дал бы рваный ритм вместо киноритма. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .saga__copy > * {
      animation: sagaIn linear both;
      animation-timeline: view();
      animation-range: entry 14% entry 64%;
    }
    .saga__rail { animation-range: entry 30% entry 82%; }
    @keyframes sagaIn {
      from { opacity: 0; transform: translateY(18px); }
      to   { opacity: 1; transform: none; }
    }
  }
}

/* Покой: ролик не запускается, остаётся постер — контент не теряется. */

/* ============================================================
   КЛИЕНТ — СЕЛЕКТОР ВОЗМОЖНОСТЕЙ, ТЗ §24–27

   Слева не список обещаний, а переключатель: выбор пункта запускает
   соответствующий игровой фрагмент справа. Пользователь сразу видит
   подтверждение, а не читает про него.
   ============================================================ */

.picker { display: grid; gap: 2px; margin-bottom: var(--s-6); }
.picker__btn {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s-3);
  align-items: start;
  width: 100%;
  padding: 14px 16px;
  text-align: left;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  transition: background var(--d-base) var(--e-out), border-color var(--d-base) var(--e-out);
}
/* Метка-ромб слева — тот же знак, что в навигаторе стадий и в углах панели */
.picker__mark {
  width: 9px; height: 9px;
  margin-top: 6px;
  rotate: 45deg;
  border: 1.5px solid var(--c-line-strong);
  background: var(--c-void);
  transition: all var(--d-base) var(--e-out);
}
.picker__body { display: grid; gap: 3px; }
.picker__body b { font-size: var(--t-md); font-weight: 500; color: var(--c-text-2); }
/* Пояснение раскрывается только у выбранного пункта: четыре описания разом
   возвращают ту же стену текста, ради ухода от которой всё и делалось. */
.picker__body i {
  font-style: normal;
  font-size: var(--t-sm);
  color: var(--c-text-3);
  line-height: 1.45;
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  transition: opacity var(--d-base) var(--e-out), max-height var(--d-base) var(--e-out);
}
.picker__pending {
  align-self: center;
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
  border: 1px dashed var(--c-line-strong);
  border-radius: 999px;
  padding: 3px 9px;
  white-space: nowrap;
}

.picker__btn:hover .picker__mark { border-color: var(--c-accent); }
.picker__btn:hover .picker__body b { color: var(--c-text); }

.picker__btn[aria-selected='true'] {
  background: rgba(247, 145, 30, 0.07);
  border-color: var(--c-accent-soft);
}
.picker__btn[aria-selected='true'] .picker__mark {
  border-color: var(--c-accent);
  background: var(--c-accent);
  box-shadow: 0 0 14px rgba(247, 145, 30, 0.45);
}
.picker__btn[aria-selected='true'] .picker__body b { color: var(--c-text); }
.picker__btn[aria-selected='true'] .picker__body i { opacity: 1; max-height: 6em; }

@media (prefers-reduced-motion: reduce) {
  .picker__body i { opacity: 1; max-height: none; }
}

/* Подмена источника не должна давать чёрный кадр (§27): на время загрузки
   видео плавно гаснет и возвращается, а не мигает пустотой. */
.client-video video { transition: opacity var(--d-fast) var(--e-out); }
.client-video video.is-swapping { opacity: 0.25; }


@media (max-width: 768px) {
  /* Плашка «фрагмент готовится» не помещается в строку с названием
     и вылезала за правый край — переносим её под текст. */
  .picker__btn { grid-template-columns: auto minmax(0, 1fr); }
  .picker__pending {
    grid-column: 2;
    justify-self: start;
    margin-top: 6px;
    align-self: start;
  }
}


/* ============================================================
   МИР УЖЕ СОБИРАЕТСЯ — ТЗ §32

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

/* Блок подведён вплотную к регистрации: снизу почти нет воздуха, и «мир уже
   собирается» читается разгоном к «создай аккаунт», а не отдельным экраном
   между историей и формой. */
.proof { padding-block: var(--s-9) var(--s-5); }

/* Вуаль и линии полосы — псевдослоем, а не фоном секции. Фон секции лежит
   НИЖЕ её позиционированных детей, и .scene с игровым кадром закрыла бы и
   заливку, и границы. */
.proof::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: var(--overlay-soft);
  border-block: 1px solid var(--c-line);
}
.registration-scene { padding-block-start: var(--s-7); }

.proof__inner {
  display: grid;
  /* Один кластер слева, а не «тезис слева — факты справа». Показатель
     остался один, и прежний строй растягивал его к правой кромке: между
     концом фразы и числом получалось полторы трети пустоты, взгляд делал
     длинный прыжок и читал число главнее заголовка. */
  grid-template-columns: max-content max-content minmax(0, 1fr);
  grid-template-areas: "lead counters clans";
  column-gap: var(--s-8);
  row-gap: var(--s-6);
  /* По центру, а не по нижней кромке: список кланов занимает четыре строки,
     и при выключке по низу заголовок с числом проваливались под него —
     три колонки читались лесенкой, а не одной строкой утверждения. */
  align-items: center;
  padding-block: var(--s-7);
}
.proof__lead     { grid-area: lead; }
.proof__counters { grid-area: counters; }
.proof__clans    { grid-area: clans; align-self: center; }
/* Одной строкой: в две «Мир уже / собирается.» читалось обрывком фразы,
   а не утверждением. Меры не ставим — строка короткая и должна держаться
   целой; переносить её будет только очень узкий экран. */
.proof__title {
  font-size: clamp(1.75rem, 3.2vw, 2.75rem);
  text-wrap: nowrap;
}

.proof__counters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s-7);
  justify-self: start;
}
.proof__counters dt {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display) tabular-nums;
  /* Ниже заголовка на ступень: число доказывает тезис, а не спорит с ним.
     Вровень с заголовком оно читалось вторым заголовком секции. */
  font-size: var(--t-2xl);
  line-height: 1;
  /* Показатель здесь один, и он же — доказательство фразы «мир уже
     собирается»: число кланов выделено цветом, чтобы читаться раньше
     списка имён справа. Правило §1.1 про точечный акцент не нарушено —
     акцентная точка в секции ровно одна. */
  color: var(--c-accent);
}
/* Подпись — строка со знаком клана. Знак один на весь блок и намеренно
   нейтральный: ряд пиктограмм или золотой герб превратили бы спокойную
   доказательную полосу в промоблок с достижениями. */
.proof__counters dd {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: 6px;
  font-size: var(--t-caption);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
}

.proof__clans-unused {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  align-items: center;
  padding-top: var(--s-5);
  border-top: 1px solid var(--c-line);
}
.proof__clans[hidden] { display: none; }
.proof__clan img { height: 32px; width: auto; opacity: 0.7; transition: opacity var(--d-fast) var(--e-out); }
.proof__clan img:hover { opacity: 1; }

@media (max-width: 900px) {
  .proof__inner { grid-template-columns: 1fr; }
  .proof__counters { gap: var(--s-5) var(--s-6); }
}

@media (max-width: 768px) {
  /* Ряд из четырёх метрик на узком экране не помещался: подписи вроде
     «КОНТЕНТ-МЕЙКЕРОВ» — одно длинное слово, его минимальная ширина не
     сжимается, поэтому последняя колонка уезжала за правый край, а
     «7 стран» оставалась одна во второй строке и читалась остатком.
     Двумя колонками все четыре равноправны. */
  .proof { padding-block: var(--s-7); }
  .proof__inner { grid-template-columns: 1fr; row-gap: var(--s-4); }
  .proof__counters { gap: var(--s-5); }
  .proof__counters dt { font-size: var(--t-xl); }
  .proof__counters > * { min-width: 0; }
  /* Подписи разной высоты рассыпали нижний ряд: «КОНТЕНТ-МЕЙКЕРОВ» шло в
     две строки, соседние в одну, и четыре метрики читались независимыми
     блоками, а не набором. Держим место под две строки у всех. */
  .proof__counters dd {
    overflow-wrap: normal;
    min-block-size: 2lh;
    /* Длинную подпись делим на две соразмерные строки, а не «почти всё
       в первой и хвост во второй» — иначе ячейка тяжелее соседних. */
    text-wrap: balance;
  }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .proof__counters > div {
      animation: proofIn linear both;
      animation-timeline: view();
      animation-range: entry 10% entry 62%;
    }
    @keyframes proofIn {
      from { opacity: 0; transform: translateY(14px); }
      to   { opacity: 1; transform: none; }
    }
  }
}


/* --- Превью локации при наведении, ТЗ §14 ---
   Карточка появляется НАД медальоном, подпись активной метки остаётся под
   ним — так они не сталкиваются. Превью — настоящий игровой кадр стадии. */
/* Карточка стоит СБОКУ от метки. Раньше она открывалась вверх и упиралась
   в верхнюю кромку окна карты: активная метка стоит у точки фокуса, а над
   ней места меньше, чем высота карточки, — кадр стадии срезало. Сбоку
   места хватает всегда; сторону и вертикальный сдвиг считает скрипт, ему
   видно, куда отъехала карта. */
.mappin__card {
  /* Отступ от центра метки. Считается не от медали (±34px), а от самого
     широкого, что у метки есть, — подписи «Стадия · День» (±72px): она
     стоит под медалью и попадала под карточку. 88 = 72 + воздух. */
  --card-gap: 88px;
  position: absolute;
  left: 50%; top: 50%;
  translate: calc(var(--card-gap) + var(--card-nudge, 0px)) calc(-50% + var(--card-shift, 0px));
  z-index: 6;
  /* Кадр сверху во всю ширину, подписи под ним. Прежняя раскладка в две
     колонки давала превью шириной 96px — на нём от игровой сцены не
     оставалось ничего, а теперь у каждой стадии свой кадр со снаряжением,
     и его надо видеть. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  width: 260px;
  /* Высота — по содержимому, и явно: коробка висит на кнопке-якоре
     нулевого размера, и обычная auto-высота там разрешалась в одну строку.
     Заодно это снимает прыжок в момент загрузки превью: высота кадра
     задана числом ниже, поэтому размер известен ещё до картинки. */
  block-size: max-content;
  padding: 8px;
  border: 1px solid var(--c-accent-soft);
  border-radius: var(--r-md);
  background: rgba(8, 10, 14, 0.97);
  box-shadow: var(--sh-card);
  text-align: left;
  opacity: 0;
  transform: translateX(-6px);
  transition: opacity var(--d-fast) var(--e-out), transform var(--d-fast) var(--e-out);
  pointer-events: none;
}
/* У правого края окна карточка раскрывается влево. */
.mappin.is-card-left .mappin__card {
  translate: calc(-100% - var(--card-gap) + var(--card-nudge, 0px)) calc(-50% + var(--card-shift, 0px));
  transform: translateX(6px);
}
.mappin:hover .mappin__card,
.mappin:focus .mappin__card { opacity: 1; transform: none; pointer-events: auto; cursor: text; }


/* Высота задана числом, а не только пропорцией. Одной aspect-ratio коробка
   карточки не набирала: она оставалась высотой в одну строку, и центрирование
   по вертикали считалось от неверного размера. Ширина карточки фиксирована
   (260px минус 8px полей с двух сторон), поэтому 244 / 16 × 9 — точное
   значение, а не подгонка. */
.mappin__thumb {
  display: block;
  block-size: 137px;
  aspect-ratio: var(--ratio-cinematic);
  border-radius: var(--r-sm);
  overflow: hidden;
  background: rgba(244, 242, 239, 0.05);
}
.mappin__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.mappin__info { display: grid; gap: 2px; padding-inline: 2px; }
.mappin__info i {
  font-style: normal;
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-accent);
}
.mappin__info b { font-size: var(--t-sm); color: var(--c-text); line-height: 1.2; }
.mappin__info u { text-decoration: none; font-size: var(--t-micro); color: var(--c-text-2); }
.mappin__info em {
  font-style: normal;
  font-size: var(--t-micro);
  color: var(--c-text-3);
}

/* Правило `bottom: 44px` у активной метки осталось от прежней раскладки,
   когда карточка открывалась вверх. Теперь она открывается сбоку по
   `top: 50%`, и заданный вместе с ним `bottom` растягивал абсолютную
   коробку между двумя краями нулевой кнопки-якоря: высота схлопывалась в
   одну строку, содержимое вываливалось за подложку. Держим низ свободным. */
.mappin.is-active .mappin__card { bottom: auto; }

@media (max-width: 768px) {
  /* На телефоне наведения нет, а место дорого: превью не показываем.
     Это же снимает вопрос с боковым раскрытием: 260px карточки плюс
     отступ в узкую карту не поместились бы ни с одной стороны. */
  .mappin__card { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .mappin__card { transition: none; }
}


@media (max-width: 1200px) {
  /* На узком десктопе колонка с чатом прижата к краю обёртки, и вынос
     персонажа на -7% давал горизонтальный скролл всей страницы. */
  .orest__character { right: 0; }
  .orest__aside::before { right: 2%; }
}




/* Ростер: наборный список имён, а не каталог. Лента и карусель заставили бы
   читать доказательство в движении, сетка из равных ячеек дала бы ровно те
   одинаковые карточки, которые ТЗ §1.1 запрещает. Разделитель — точка, а не
   рамка: имена должны читаться участниками, а не элементами интерфейса.
   Авторское написание сохранено намеренно — разнобой регистров и языков и
   есть след живых команд. */
.proof__clans {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2) var(--s-3);
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--c-text-2);
  text-transform: none;
}
/* Точка принадлежит ПРЕДЫДУЩЕМУ имени, а не следующему: при переносе она
   иначе повисала в начале строки, и список читался маркированным. */
.proof__clan:not(:last-of-type)::after {
  content: '·';
  margin-inline-start: var(--s-3);
  color: var(--c-text-3);
}
.proof__clans-more { color: var(--c-text-3); font-size: var(--t-micro); display: none; }

@media (max-width: 900px) {
  /* Полный ростер перед формой перетянул бы на себя первый экран
     регистрации, поэтому на узком показываем начало списка. */
  .proof__inner {
    grid-template-columns: 1fr;
    grid-template-areas: "lead" "counters" "clans";
    row-gap: var(--s-5);
  }
  .proof__clans { align-self: start; }
  /* Восемь имён — столько же, сколько считает PROOF_CLANS_MOBILE в main.js. */
  .proof__clan:nth-child(n + 9) { display: none; }
  .proof__clans-more { display: inline; }
}


/* Один кадр на всю лестницу грейдов. Посменная стопка сменялась на
   прокрутке, и в момент смены два кадра одновременно были полупрозрачными:
   на игровых рендерах с персонажами это читалось не переходом, а двумя
   наложенными друг на друга картинками. Статичный кадр такой возможности
   не оставляет — правило идёт последним и перекрывает анимацию стопки. */
.why__frame--static {
  opacity: 1;
  animation: none;
}

/* ============================================================
   ЧТО НОВОГО

   Один трек, шесть слайдов, единый формат. Разделение на «основные» и
   «минорные» снято: второй трек читался необязательным хвостом.
   ============================================================ */

.updates__deck-head {
  scroll-margin-top: clamp(32px, 4vw, 64px);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4) var(--s-7);
  margin-top: var(--updates-deck-gap);
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--c-line);
}
/* Счётчик отвечает на вопрос «сколько их всего»: без него названия в
   навигации читаются вкладками, а не последовательностью материалов. */
.updates__counter {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
}

.updates__nav-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--updates-nav-gap);
  list-style: none;
}
/* Названия материалов — единственный способ листать помимо самого жеста,
   поэтому они выглядят кнопками, а не строкой текста: у каждой кромка,
   фон и своя площадь нажатия. Отдельной полосы прокрутки под треком нет —
   она дублировала бы эти же шесть переходов. */
.updates__nav-button {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  min-height: var(--tap);
  border: 1px solid var(--c-line);
  border-radius: var(--r-pill);
  background: var(--c-surface);
  font-size: var(--t-sm);
  color: var(--c-text-2);
  text-decoration: none;
  transition: color var(--d-fast) var(--e-out), border-color var(--d-fast) var(--e-out),
              background var(--d-fast) var(--e-out);
}
.updates__nav-button:hover {
  color: var(--c-text);
  border-color: var(--c-line-strong);
  background: var(--c-surface-2);
}
/* Активный пункт: точка, цвет и кромка сразу. Одного цвета мало —
   состояние обязано читаться не только им (ТЗ §13.3). */
.updates__nav-button::before {
  content: '';
  inline-size: var(--s-2);
  block-size: var(--s-2);
  border-radius: var(--r-pill);
  background: var(--c-line-strong);
  flex: none;
  transition: background var(--d-fast) var(--e-out);
}
.updates__nav-button[aria-current='true'] {
  color: var(--c-text);
  border-color: var(--c-accent);
  background: var(--c-surface-2);
}
.updates__nav-button[aria-current='true']::before { background: var(--c-accent); }

/* Область прокрутки трека называет свой отсчёт на всю секцию: указатель
   позиции лежит уже снаружи вьюпорта и должен его видеть. */
.updates {
  timeline-scope: --upd-track;
  /* Anchor at the content rather than the section’s decorative top spacing. */
  scroll-margin-top: calc(-1 * var(--section-y));
}

/* ---- Фон секции: кадр текущего материала ----
   Раздел стоял на голой темноте и читался технической вкладкой. Кадр
   слайда, размытый и приглушённый, возвращает ему место в мире игры и
   меняется вместе с материалом — связь фона с содержанием буквальная.
   Размытие сильное намеренно: узнаваться должна атмосфера, а не сцена,
   иначе фон начнёт спорить с кадром в рамке. */
/* Стык с соседями держится на ПЕРЕКРЫТИИ, а не на встречных кромках.

   Сначала фон выносился на всю --cam-bleed и ложился плотным слоем поверх
   сцены истории — на границе выходила мутная полоса. Потом я запер его
   внутри секции, и стало хуже иначе: обе маски гасли в ноль ровно на
   границе, между ними оставалось около двухсот пикселей чистого чёрного,
   и переход читался провалом.

   Правильно — короткий хвост за границу. Там, где сосед только начинает
   проявляться, этот фон ещё держится вполсилы; сумма двух слоёв нигде не
   падает в ноль, и линии стыка не существует. Хвост короткий намеренно:
   на всю длину выноса слой был бы слишком плотным над чужим кадром. */
.updates__scene {
  /* Ровно столько, сколько сосед тратит на собственное проявление
     (.saga__video гаснет на той же длине). Кривые получаются зеркальными:
     где один слой на половине, другой тоже — суммарная плотность идёт
     ровно, без провала и без наложения двух плотных слоёв. */
  --upd-tail: calc(var(--cam-bleed) * 0.7);
  position: absolute;
  inset: 0 0 calc(var(--upd-tail) * -1) 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 12%,
    #000 calc(100% - var(--upd-tail) * 2.2), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 12%,
    #000 calc(100% - var(--upd-tail) * 2.2), transparent 100%);
}
/* Вуаль держит контраст текста. Раньше она обрывалась ровно на границе
   секции жёстким краем — на стыке была видна горизонтальная линия. Теперь
   она идёт по всей коробке и гаснет вместе с хвостом: к низу и вуаль, и
   сам кадр сходят в ноль, никакой кромки не остаётся.
   Сверху плотнее — там заголовок секции; в середине, где стоит рамка,
   кадру дано дышать. 84% — это как раз граница секции: дальше идёт хвост. */
.updates__scene::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg,
      rgba(11,11,14,0.90) 0%,
      rgba(11,11,14,0.66) 44%,
      rgba(11,11,14,0.80) 84%,
      rgba(11,11,14,0) 100%);
}
/* Два слоя: уходящий кадр гасится, приходящий проявляется. Один слой дал
   бы подмену картинки рывком. */
.updates__scene img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Запас масштаба съедает светлую кромку, которую оставляет размытие. */
  scale: 1.08;
  filter: blur(26px) saturate(0.9);
  opacity: 0;
  transition: opacity var(--d-slow) var(--e-out);
}
.updates__scene img.is-on { opacity: 1; }
.updates__viewport { position: relative; margin-top: var(--s-7); }
.updates__track::-webkit-scrollbar { display: none; }

/* Края трека растворяются МАСКОЙ, а не заливкой цвета фона. Заливка
   казалась равнозначной — фон секции того же тона, — но под секцией лежит
   световой слой страницы, и плоский прямоугольник пробивал в нём дыру:
   на странице это читалось чёрной полосой. Маска убирает содержимое, и
   всё, что за ним, остаётся нетронутым.

   Справа гасим срез следующего слайда: он выглядывает на 136px своей
   текстовой колонкой, и жёсткая обрезка резала заголовок посреди слова.
   Слева — только на последнем слайде: он не может встать вплотную к
   краю (ширина слайда это край минус peek), и из-под него торчит хвост
   предыдущего. На остальных положениях левое растворение съело бы начало
   заголовка, поэтому его ширина там нулевая.

   Ширины считает скрипт через --upd-edge-*: ноль означает «растворения
   нет», и обе границы схлопываются в край. */
.updates__viewport {
  --upd-fade-end: calc((var(--updates-peek) + var(--updates-deck-gap)) * var(--upd-edge-end, 1));
  --upd-fade-start: calc(var(--updates-peek) * var(--upd-edge-start, 0));
  -webkit-mask-image: linear-gradient(90deg,
    transparent 0,
    #000 var(--upd-fade-start),
    #000 calc(100% - var(--upd-fade-end)),
    transparent 100%);
  mask-image: linear-gradient(90deg,
    transparent 0,
    #000 var(--upd-fade-start),
    #000 calc(100% - var(--upd-fade-end)),
    transparent 100%);
}

.updates__track {
  display: flex;
  gap: var(--s-8);
  overflow-x: auto;
  scroll-snap-type: inline mandatory;
  scroll-snap-stop: always;
  overscroll-behavior-inline: contain;
  padding-inline-end: var(--updates-peek);
  padding-bottom: var(--s-3);
  /* Системная полоса убрана: она была тонкой, чёрной и всплывала только по
     наведению — по неподвижному кадру человек не понимал, что карусель
     листается. Вместо неё своя, всегда видимая, ниже трека. */
  scrollbar-width: none;
  scroll-timeline: --upd-track inline;
  /* Перетаскивания мышью нет намеренно: оно съедало бы выделение текста.
     Листают прокруткой, названиями в навигации и клавиатурой. */
  touch-action: pan-x pan-y;
}

/* Лента ростом с открытый слайд. Высоту считает initDeck и кладёт в
   --upd-deck-h; без неё лента равнялась бы по самому высокому слайду и
   вокруг коротких зияли бы сотни пустых пикселей. Пока переменной нет
   (скрипт не отработал) — auto, то есть прежнее поведение. */
.updates__viewport { block-size: var(--upd-deck-h, auto); }
.updates__track { block-size: 100%; align-items: start; overflow-y: clip; }

.updates__slide {
  flex: 0 0 calc(100% - var(--updates-peek));
  scroll-snap-align: start;
  align-self: start;
  min-width: 0;
  min-block-size: var(--updates-slide-min);
  display: grid;
  grid-template-columns:
    minmax(0, calc(100% - var(--updates-media-basis) - var(--s-8)))
    minmax(0, var(--updates-media-basis));
  gap: var(--s-8);
  align-items: center;
}
/* Соседние слайды приглушены и чуть отодвинуты, кадр внутри рамки идёт с
   отставанием от самого слайда. Оба движения привязаны к прокрутке трека,
   а не к таймеру: они идут в композиторе, следуют за пальцем и не могут
   заблокировать жест. Шесть отдельных экранов от этого начинают читаться
   одной лентой — видно, что слева и справа есть продолжение. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Отсчёт один на весь слайд: и приглушение, и масштаб, и отставание
       кадра идут по нему. Если бы каждый элемент считал свою видимость
       сам, колонки слайда съезжали бы по фазе друг относительно друга. */
    .updates__slide {
      view-timeline: --upd-slide inline;
      animation: upd-fade linear both;
      animation-timeline: --upd-slide;
      animation-range: cover 0% cover 100%;
    }
    @keyframes upd-fade {
      0%   { opacity: 0.3; }
      42%, 58% { opacity: 1; }
      100% { opacity: 0.3; }
    }

    /* Масштаб живёт на содержимом, а НЕ на самом слайде. Слайд — цель
       прилипания, а прилипание выравнивает трансформированную рамку: у
       уменьшенного слайда она уже layout-рамки, прокрутка встаёт со
       сдвигом, и текст уезжает за край трека под обрезку. Хуже того,
       сдвиг меняет фазу той же анимации — получается замкнутая петля.
       Прозрачность геометрию не трогает, поэтому она осталась на слайде. */
    .updates__slide > .update__text,
    .updates__slide > .update__media {
      animation: upd-lift linear both;
      animation-timeline: --upd-slide;
      animation-range: cover 0% cover 100%;
    }
    @keyframes upd-lift {
      0%   { scale: 0.965; }
      42%, 58% { scale: 1; }
      100% { scale: 0.965; }
    }

    /* Отставание кадра даёт глубину: рамка едет со слайдом, картинка
       внутри — медленнее. Запас масштаба нужен, чтобы сдвиг не обнажил
       край кадра. */
    .updates__slide .media-frame img {
      scale: 1.14;
      animation: upd-parallax linear both;
      animation-timeline: --upd-slide;
      animation-range: cover 0% cover 100%;
    }
    @keyframes upd-parallax {
      from { translate: 6% 0; }
      to   { translate: -6% 0; }
    }
  }
}

/* Кадр во всю ширину слайда, текст — плашкой поверх его низа.
   Прежде графика занимала 58% ширины и стояла иллюстрацией при тексте, а в
   этом разделе она и есть содержание: шторка «до и после», сцена Сада,
   монстры локаций. Текст поверх, а не под кадром, — чтобы слайд не вырос
   на высоту абзаца: лента и так ростом с самый высокий слайд.

   Слайд «Умения» остаётся в две колонки: там рядом с кадром живёт список
   умений, и без колонки он превращается в ленту под картинкой.
   Правило только для широкого экрана: на телефоне слайды уже сложены
   в колонку (медиа, затем текст), и наложение там лишнее — плашка
   съедала бы половину и без того небольшого кадра. */
@media (min-width: 901px) {
  .updates__slide:not(.update--skills) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    align-items: stretch;
    gap: 0;
    /* Высоту задаёт сам кадр. Общий минимум слайда оставлял под подписью
       полосу пустоты, и плашка отрывалась от низа картинки. */
    min-block-size: 0;
  }
  .updates__slide:not(.update--skills):not(.update--dx11) > .update__media,
  .updates__slide:not(.update--skills):not(.update--dx11) > .update__text { grid-area: 1 / 1; }

  /* «Новый клиент» — исключение из наложения: под кадром там живут ручка
     шторки и подпись к сравнению, и плашка текста накрывала именно их.
     Здесь текст идёт под кадром — второй разрешённый вариант раскладки. */
  .update--dx11 {
    grid-template-rows: auto auto;
    align-items: start;
    gap: var(--s-5);
  }
  .update--dx11 > .update__media { order: -1; }
  /* Рамка кадра перестаёт держать пропорцию и берёт всю ширину: пропорция
     при такой ширине дала бы кадр в 800px высотой — слайд не помещался бы
     на экран ноутбука целиком. */
  .updates__slide:not(.update--skills) .media-frame,
  .update--dx11 .compare {
    aspect-ratio: auto;
    block-size: clamp(22rem, 62svh, 34rem);
  }
  .updates__slide:not(.update--skills):not(.update--dx11) > .update__text {
    align-self: end;
    z-index: 2;
    padding: var(--s-9) var(--s-6) var(--s-6);
    border-end-start-radius: var(--r-lg);
    border-end-end-radius: var(--r-lg);
    /* Затемнение снизу вверх: подпись читается на любом кадре, а верх
       картинки остаётся нетронутым. */
    background: linear-gradient(180deg,
      transparent 0%, rgba(6, 8, 12, 0.62) 42%, rgba(6, 8, 12, 0.93) 100%);
  }
  .updates__slide:not(.update--skills) .update__title { max-inline-size: 24ch; }
  .updates__slide:not(.update--skills) .update__body { max-inline-size: 64ch; }
  .update--dx11 > .update__text { padding-inline: var(--s-1); }
}

/* Текст выделяется и копируется. Курсор текстовый — иначе внутри
   прокручиваемого трека рука тянется листать, а не читать. */
.update__text {
  user-select: text;
  -webkit-user-select: text;
  cursor: text;
}
.update__title {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-size: var(--t-2xl);
  line-height: 1.12;
  letter-spacing: var(--tr-display);
  max-width: 20ch;
}
.update__body {
  margin-top: var(--s-4);
  color: var(--c-text-2);
  max-width: 46ch;
}

/* Рамка под кадр одна и та же у всех шести и крупная: графика в этом
   разделе главная, а не служебная иллюстрация при тексте. */
.update__media { min-width: 0; }
.media-frame {
  position: relative;
  aspect-ratio: var(--ratio-landscape);
  overflow: clip;
  border-radius: var(--r-lg);
  border: 1px solid var(--c-line);
  background: var(--c-surface);
}
.media-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Абстрактный рельеф вместо пустоты. Игровой кадр он не изображает —
   генерация игровых материалов в проекте запрещена. */
.media-frame__grid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--c-line-strong);
  stroke-width: 0.4;
  opacity: 0.5;
  pointer-events: none;
}

/* Состояние — текстом, формой точки и цветом сразу (ТЗ §13.3).
   Точка квадратная: «ожидание» против круглой «активно». */
.media-status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* Трекинг у подписи большой, и на узком экране строка рвалась посреди
     слова. Разрешаем перенос между словами и сжимаем разрядку. */
  gap: var(--s-1) var(--s-3);
  margin-top: var(--s-3);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-text-3);
}
.media-status__dot {
  inline-size: 7px;
  block-size: 7px;
  border-radius: var(--r-none);
  background: var(--c-text-3);
  flex: none;
}
.media-status b { font-weight: 600; color: var(--c-text-2); }
.media-status[hidden] { display: none; }

/* --- Шторка сравнения DX11 --- */
.compare {
  position: relative;
  aspect-ratio: var(--ratio-landscape);
  overflow: clip;
  border-radius: var(--r-lg);
  border: 1px solid var(--c-line);
  background: var(--c-surface);
  touch-action: pan-y;
}
.compare__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Новый клиент открывается СПРАВА от ручки, старый остаётся слева.
   Было наоборот, и подписи врали: «Старый клиент» стояло слева, а
   показывался там как раз DX11. */
.compare__img--after { clip-path: inset(0 0 0 var(--compare-position, 50%)); }
.compare__handle {
  position: absolute;
  top: 0; bottom: 0;
  left: var(--compare-position, 50%);
  width: 1px;
  background: var(--c-line-strong);
  pointer-events: none;
}
/* Запись и её ступенчатая копия лежат в одной рамке. Левая половина —
   canvas, он перекрывает видео до линии шторки. */
.compare__video,
.compare__canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.compare__canvas { clip-path: inset(0 calc(100% - var(--compare-position, 50%)) 0 0); }
/* Пара роликов: правый (DX11) лежит поверх левого и режется шторкой так же,
   как кадр «после» в паре картинок. */
.compare__video--after { clip-path: inset(0 0 0 var(--compare-position, 50%)); }

/* Показатели частоты — на плашке цвета фона в противоположных верхних
   углах рамки, а не поверх геймплея: внутри записи есть собственный
   интерфейс игры, и цифры на нём читались бы поддельным игровым
   счётчиком. Цвет здесь не единственный носитель смысла — рядом стоит
   само число (ТЗ §13.3), поэтому красный и зелёный допустимы. */
.compare__fps {
  position: absolute;
  top: var(--s-4);
  padding: var(--s-2) var(--s-3);
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  background: var(--c-bg);
  border: 1px solid currentColor;
  border-radius: var(--r-sm);
  pointer-events: none;
  z-index: 2;
}
.compare__fps--before { left: var(--s-4); color: var(--c-error); }
.compare__fps--after { right: var(--s-4); color: var(--c-success); }

/* Оговорка о способе демонстрации стоит рядом со шторкой, а не в сноске
   внизу страницы: она объясняет ровно то, что человек видит сейчас. */
.compare-note {
  margin-top: var(--s-3);
  font-size: var(--t-micro);
  line-height: 1.45;
  color: var(--c-text-3);
  max-width: 62ch;
}

.compare__label {
  position: absolute;
  bottom: var(--s-3);
  padding: var(--s-2) var(--s-3);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-2);
  background: var(--overlay-strong);
  border-radius: var(--r-sm);
  pointer-events: none;
}
.compare__label--before { left: var(--s-3); }
.compare__label--after  { right: var(--s-3); }
/* Ручка отдельной видимой строкой под кадром: с подписью, доступна с
   клавиатуры и не спорит с жестом листания. */
.compare-control {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-3);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
}
.compare-control input { flex: 1 1 auto; min-width: 0; accent-color: var(--c-accent); }

.dx11-video { margin-top: var(--s-4); }
.dx11-video video {
  width: 100%;
  aspect-ratio: var(--ratio-cinematic);
  border-radius: var(--r-md);
  border: 1px solid var(--c-line);
  background: var(--c-void);
}

/* --- Умения: вертикальный выбор класса, кадр справа ---
   Список умений будет расти, поэтому он колонкой: горизонтальная лента
   здесь означала бы второй боковой жест внутри трека. */
.skills__list {
  display: grid;
  gap: var(--s-1);
  list-style: none;
  margin-top: var(--s-5);
}
.skill > button {
  appearance: none;
  inline-size: 100%;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
  min-block-size: var(--tap);
  transition: background var(--d-fast) var(--e-out), border-color var(--d-fast) var(--e-out);
}
.skill > button:hover { background: var(--c-surface); }
.skill.is-active > button {
  background: var(--c-surface-2);
  border-color: var(--c-line-strong);
}
.skill__icon {
  display: grid;
  place-items: center;
  inline-size: 34px;
  block-size: 34px;
  flex: none;
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  background: var(--c-surface-2);
  color: var(--c-text-3);
}
.skill.is-active .skill__icon { border-color: var(--c-accent); color: var(--c-accent); }
.skill__icon svg { inline-size: 56%; block-size: 56%; }
.skill__icon img { inline-size: 100%; block-size: 100%; object-fit: contain; border-radius: var(--r-sm); }
.skill__body { display: grid; gap: 1px; min-inline-size: 0; }
.skill__role { font-size: var(--t-sm); color: var(--c-text); font-weight: 400; }
/* Список классов у умения бывает длинным (четыре класса у Пробуждения
   Слуги) — переносим на вторую строку, а не режем многоточием. */
.skill__name {
  font-style: normal;
  font-size: var(--t-micro);
  line-height: 1.3;
  color: var(--c-text-3);
}
.skill.is-active .skill__name { color: var(--c-text-2); }

/* Подпись под кадром: название умения и описание. Место под них
   зарезервировано с самого начала — подстановка настоящих текстов не
   сдвинет composition. */
/* Описания у умений разной длины — от двух строк до семи. Место под
   подпись резервируется по самому длинному, иначе рамка с кадром прыгала
   бы вверх-вниз при каждом переключении. */
.skills__caption { display: grid; gap: var(--s-2); margin-top: var(--s-4); min-block-size: 9.5rem; align-content: start; }
.skills__name {
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-size: var(--t-lg);
  font-weight: 400;
  color: var(--c-text);
}
.skills__class {
  margin-top: calc(var(--s-2) * -1);
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-accent);
}
.skills__text { color: var(--c-text-2); font-size: var(--t-sm); max-width: 60ch; }

@media (max-width: 900px) {
  /* Одна колонка: медиа перед текстом. Механическое сжатие десктопной
     раскладки дало бы слишком узкую шторку и спорящие подписи. */
  :root { --updates-slide-min: 0; --updates-peek: var(--s-5); }

  .updates__slide { grid-template-columns: 1fr; gap: var(--s-5); align-items: start; align-content: start; }
  .updates__slide > .update__media { order: 1; }
  .updates__slide > .update__text { order: 2; }
  /* У умений порядок обратный: сначала список классов, потом кадр. Иначе
     нажатие на класс меняло бы кадр, уехавший вверх за пределы экрана. */
  .update--skills > .update__media { order: 2; }
  .update--skills > .update__text { order: 1; }
  .updates__deck-head { flex-direction: column; align-items: flex-start; }
}

@media (prefers-reduced-motion: no-preference) {
  /* Смена высоты ленты — такой же переход, как и сама перелистка. */
  .updates__viewport { transition: block-size var(--d-slow) var(--e-out); }
}

/* Полоса стыка обрезает себя сама. Уголёк внутри дышит сдвигом на 3%
   ширины и на широком экране выезжал за правый край страницы — документ
   получал горизонтальную прокрутку на полсотни пикселей. На вид клип не
   влияет: края полосы и так гасит маска.
   Правило стоит здесь, а не в самом блоке .seam, потому что определение
   .seam в этом файле продублировано (см. строки ~2300 и ~3298) — стоит
   свести их в одно, это отдельная уборка. */
.seam { overflow: clip; }

/* ============================================================
   ПОДАРКИ К СТАРТУ

   Движение взято у барабана: лента едет всегда и по нажатию разгоняется,
   чтобы длинным выкатом встать на подарке. Взята именно моторика — не
   оформление. Ни рычага, ни трёх барабанов, ни 777, ни фруктов, ни монет,
   ни красно-золотого казино здесь нет: по ТЗ это запрещено, да и механики
   случайности в результате не существует. Образ прежний — горизонтальный
   поток предметов сквозь металлический створ, как артефакт из мира игры.
   ============================================================ */

.gifts__hint { margin-top: var(--s-3); color: var(--c-text-3); font-size: var(--t-sm); }
.gifts__hint[hidden] { display: none; }

/* Вся механика — один объект. Лента, выпавшие подарки и текущий шаг лежат
   в общей оправе: связь между барабаном и полученным набором держится
   именно ею. */
.gifts__machine {
  margin-top: var(--s-7);
  margin-inline: auto;
  max-inline-size: var(--gift-machine-max);
  /* Колонка обязана быть minmax(0,…). `overflow: clip` не делает рамку
     скролл-контейнером, поэтому её вклад в max-content — это вся лента,
     а она в разы шире экрана: без этого вся страница уезжала вбок. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-5);
  padding: var(--s-5);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-lg);
  /* Оправа стоит на освещённой сцене: слева от неё кадр тёмный, справа —
     тёплый и светлый, и на этой половине плита читалась насквозь
     прозрачной. Поверхность на ступень выше фона, а кромку держит тень —
     теперь это предмет на сцене, а не затемнение поверх неё. */
  background: var(--c-surface-2);
  box-shadow: var(--sh-card);
}

/* Рамка ленты: камень и тёмный металл, тёплая кромка. Это главный предмет
   блока, поэтому он светлее окружения и стоит на собственном свету — иначе
   тонет в тёмной сцене. Свечение при этом точечное: подсвечена кромка и
   столб под маркером, заливки акцентом по площади нет (ТЗ §1.1). */
.gifts__reel {
  position: relative;
  width: 100%;
  /* Ширину держит оправа: своя мера оставляла по бокам ленты пустые поля,
     когда оправу расширили, и окно переставало быть её содержимым. */
  overflow: clip;
  padding: var(--s-6) 0;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-lg);
  /* Окно, прорезанное в корпусе: внутри рамки лента должна лежать ГЛУБЖЕ
     оправы, иначе это рамка в рамке и обе спорят за внимание. */
  background:
    /* тёплый столб света под маркером — он же смысловой центр рамки */
    radial-gradient(60% 130% at 50% 50%, rgba(247,145,30,0.13), transparent 70%),
    var(--c-void);
  box-shadow:
    inset 0 2px 6px rgba(0,0,0,0.65),
    inset 0 -1px 0 rgba(244,242,239,0.05);
}
/* Края ленты растворяются: предметы приходят и уходят, а не обрываются
   по кромке рамки. */
.gifts__reel::before,
.gifts__reel::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  width: 14%;
  z-index: 2;
  pointer-events: none;
}
.gifts__reel::before { left: 0; background: linear-gradient(90deg, var(--c-void) 12%, transparent); }
.gifts__reel::after { right: 0; background: linear-gradient(270deg, var(--c-void) 12%, transparent); }

/* Шага между ячейками нет: воздух даёт сама ячейка. Так шаг ленты равен
   ширине ячейки — вся арифметика остановки становится точной. */
.gifts__strip {
  display: flex;
  gap: 0;
  list-style: none;
  will-change: transform;
}
/* Смаз по скорости живёт только на разгоне: фильтр в покое заставлял бы
   браузер держать лишний слой всё время. */
.gifts__reel.is-spinning .gifts__strip { filter: blur(var(--gift-blur, 0px)); }

.gift-item {
  flex: 0 0 auto;
  inline-size: var(--gift-cell);
  display: grid;
  justify-items: center;
  gap: var(--s-2);
  text-align: center;
}
.gift-item__frame {
  display: grid;
  place-items: center;
  inline-size: var(--gift-frame);
  block-size: var(--gift-frame);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-md);
  background: linear-gradient(180deg, var(--c-surface-3), var(--c-surface));
  box-shadow: inset 0 1px 0 rgba(244,242,239,0.08);
  color: var(--c-text-2);
  transition: border-color var(--d-base) var(--e-out), box-shadow var(--d-base) var(--e-out);
}
/* Три нейтральных оттенка по номеру ячейки. Лента без них — ряд
   одинаковых серых квадратов; с ними — материал, у которого есть фактура.
   С ценностью предмета оттенок не связан НИКАК: цвет здесь не имеет права
   предсказывать результат. */
.gift-item--t1 .gift-item__frame { background: linear-gradient(180deg, var(--c-surface-2), var(--c-void)); }
.gift-item--t2 .gift-item__frame { background: linear-gradient(200deg, var(--c-surface-3), var(--c-surface-2)); }
.gift-item--t1 .gift-item__rune { opacity: 0.72; }
.gift-item--t2 .gift-item__rune { opacity: 0.88; }
/* Иконка показывается в своём размере — 32×32, как приходит из игры.
   Раньше она растягивалась на 72% рамки, то есть втрое: пиксельный
   рисунок расплывался, и предмет переставал узнаваться. Объём теперь
   даёт не масштаб, а подложка под ней. */
.gift-item__frame img {
  inline-size: var(--gift-icon);
  block-size: var(--gift-icon);
  object-fit: none;
  image-rendering: pixelated;
  /* Тонкая тень отделяет иконку от подложки: у игровых иконок нет
     собственного контура, и на тёмном они «висят» без опоры. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.65));
}
/* Подложка под иконкой — та самая рамка. Кольцо в один пиксель, лёгкий
   внутренний блик сверху и провал снизу: так плитка читается вдавленной
   в ячейку, а не наклеенной на неё. */
.gift-item__frame::before {
  content: '';
  position: absolute;
  inline-size: var(--gift-plate);
  block-size: var(--gift-plate);
  border-radius: var(--r-sm);
  border: 1px solid var(--c-line-strong);
  background: radial-gradient(120% 120% at 50% 0%,
    rgba(244, 242, 239, 0.10) 0%, rgba(8, 8, 10, 0.55) 70%);
  box-shadow: inset 0 1px 0 rgba(244, 242, 239, 0.12),
              inset 0 -8px 14px rgba(0, 0, 0, 0.45);
}
.gift-item__frame { position: relative; }
.gift-item__frame img, .gift-item__rune { position: relative; }
.gift-item__rune { inline-size: 30px; block-size: 30px; }

/* Заточка и количество — на углах плитки, как в игре: число принадлежит
   предмету, а не подписи под ним. */
.gift-item__ench,
.gift-item__qty {
  position: absolute;
  z-index: 1;
  padding: 1px 5px;
  border-radius: var(--r-sm);
  background: rgba(8, 8, 10, 0.86);
  font-size: var(--t-micro);
  font-weight: 600;
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
}
.gift-item__ench {
  inset-block-start: calc(50% - var(--gift-plate) / 2 - 6px);
  inset-inline-end: calc(50% - var(--gift-plate) / 2 - 6px);
  color: var(--c-accent-hi);
  border: 1px solid var(--c-accent-soft);
}
.gift-item__qty {
  inset-block-end: calc(50% - var(--gift-plate) / 2 - 6px);
  inset-inline-end: calc(50% - var(--gift-plate) / 2 - 6px);
  color: var(--c-text);
  border: 1px solid var(--c-line-strong);
}
.gift-item__ench[hidden], .gift-item__qty[hidden] { display: none; }

/* В итоговом наборе плитка меньше, поэтому заточка садится в самый угол. */
.gift-slot__frame { position: relative; }
.gift-slot__frame img {
  inline-size: var(--gift-icon);
  block-size: var(--gift-icon);
  object-fit: none;
  image-rendering: pixelated;
}
.gift-slot__frame .gift-item__ench {
  inset-block-start: -7px;
  inset-inline-end: -7px;
}
/* Две строки резервируются всегда. Иначе ячейка с длинным названием выше
   соседних, и на ходу — когда такая ячейка въезжает в рамку и выезжает —
   высота всей ленты дёргается. */
.gift-item__name {
  font-size: var(--t-micro);
  color: var(--c-text-2);
  line-height: 1.25;
  block-size: 2.5em;
  overflow-wrap: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Единственная ячейка, которой позволено носить акцент, — та, на которой
   лента фактически остановилась. Пока лента идёт, акцента нет ни у кого:
   иначе цвет выдавал бы результат заранее — по неподвижному кадру было
   видно, что именно выпадет. */
.gift-item.is-selected .gift-item__frame {
  border-color: var(--c-accent);
  color: var(--c-accent-hi);
  box-shadow: inset 0 0 0 1px var(--c-accent-soft), 0 0 26px rgba(247,145,30,0.34);
}
.gift-item.is-selected .gift-item__name { color: var(--c-text); }

/* Маркер — не рамка, а указатель линии остановки. Рамка обещала бы слот,
   в который предмет встаёт всегда; лента же едет непрерывно, и пустая
   рамка посреди хода читалась бы поломкой. Указатель честнее: он
   показывает место, а створ загорается только в момент остановки. */
.gifts__marker {
  position: absolute;
  top: 0; bottom: 0;
  left: 50%;
  translate: -50% 0;
  inline-size: var(--gift-cell);
  z-index: 3;
  pointer-events: none;
  transition: box-shadow var(--d-base) var(--e-out), opacity var(--d-base) var(--e-out);
}
/* Стрелки сверху и снизу. Они же — единственное, что видно на ходу. */
.gifts__marker::before,
.gifts__marker::after {
  content: '';
  position: absolute;
  left: 50%;
  translate: -50% 0;
  inline-size: 15px;
  block-size: 9px;
  background: var(--c-accent);
}
.gifts__marker::before { top: 0; clip-path: polygon(0 0, 100% 0, 50% 100%); }
.gifts__marker::after { bottom: 0; clip-path: polygon(50% 0, 0 100%, 100% 100%); }

/* На разгоне указатель уходит на второй план: смотреть надо на ленту. */
.gifts__reel.is-spinning .gifts__marker { opacity: 0.45; }
/* Остановка — единственный момент, когда створ действительно смыкается. */
.gifts__reel.is-hit .gifts__marker {
  box-shadow:
    inset 1px 0 0 var(--c-accent), inset -1px 0 0 var(--c-accent),
    0 0 38px rgba(247,145,30,0.40), inset 0 0 38px rgba(247,145,30,0.16);
}

/* ---- Три места под подарки ----
   Стоят пустыми с начала игры. Это счётчик прогресса, который читается
   раньше текста, и одновременно фиксатор высоты: рамка не растёт по ходу
   открытий. Выпавшее показывается здесь, а не отдельным блоком ниже. */
.gifts__slots {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-3);
  list-style: none;
}
.gift-slot {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-block-size: var(--gift-slot);
  padding: var(--s-3);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  background: var(--c-surface);
  min-inline-size: 0;
}
/* Пустое место — тихий контур со знаком по центру. Пунктир внутри пунктира
   читался не «будущим подарком», а незагрузившимся содержимым или зоной
   для перетаскивания файла: это устойчивый паттерн загрузки, и здесь он
   сбивает с толку. */
.gift-slot--empty {
  justify-content: center;
  background: none;
}
.gift-slot--empty .gift-slot__frame {
  border: 0;
  background: none;
  color: var(--c-text-3);
  opacity: 0.45;
}
.gift-slot--hero { border-color: var(--c-accent-soft); box-shadow: var(--sh-accent); }
.gift-slot__frame {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  flex: none;
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  background: var(--c-surface-2);
  color: var(--c-accent);
}
.gift-slot__frame img { width: 74%; height: 74%; object-fit: contain; }
.gift-slot__frame .gift-item__rune { inline-size: 52%; block-size: 52%; }
.gift-slot__body { display: grid; gap: 2px; min-inline-size: 0; }
.gift-slot__body b {
  font-size: var(--t-sm);
  color: var(--c-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gift-slot__body i { font-style: normal; font-size: var(--t-micro); color: var(--c-text-3); }

/* ---- Нижняя строка рамки ----
   Три шага — открытие, ник, бот — живут в одном месте и сменяют друг
   друга. Раньше это были три отдельных блока вниз по странице, и между
   ними было больше воздуха, чем содержания. */
.gifts__state { border-top: 1px solid var(--c-line); padding-top: var(--s-5); }
.gifts__row {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex-wrap: wrap;
}
.gifts__row[hidden] { display: none; }
.gifts__row .btn { flex: none; }
.gifts__status {
  flex: 1 1 18rem;
  min-inline-size: 0;
  margin: 0;
  color: var(--c-text-2);
  font-size: var(--t-sm);
}
/* Счётчик открытий держит левую половину строки против крупной кнопки.
   Тусклым он превращал её в пустое техническое поле. */
.gifts__row [data-gift-left] {
  color: var(--c-text);
  font-variant-numeric: tabular-nums;
}
/* Шаг закрепления. Единственное место в блоке, где предупреждение несёт
   последствия: набор живёт в браузере, пока его не забрал бот. Поэтому у
   строки своя рамка и тёплая подложка — обычной серой строкой статуса это
   читалось как сноска, мимо которой уходят. */
.gifts__row--claim {
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--c-accent-soft);
  border-radius: var(--r-md);
  background: linear-gradient(160deg,
    rgba(247, 145, 30, 0.10), rgba(10, 12, 16, 0.35));
}
.gifts__row--claim .gifts__status { display: grid; gap: 4px; }
.gifts__claim-title {
  font-family: var(--f-display);
  font-size: var(--t-md);
  font-weight: 500;
  color: var(--c-accent-hi);
}
.gifts__row .field__error { flex: 1 1 100%; }
.link-btn {
  appearance: none; background: none; border: 0; padding: 0;
  font: inherit; font-size: var(--t-sm);
  color: var(--c-accent); cursor: pointer; text-decoration: underline;
}

/* ---- Финал ----
   Две даты в строку и одна кнопка. Раньше здесь стояли два display-
   заголовка, две центрированные даты друг под другом, прощание и вторая
   кнопка — четверть экрана, не дающая ни одного нового действия. */
.gifts__launch {
  /* Одинаковая ширина и радиус с оправой — признак одного сценария;
     поэтому и дистанция короткая, иначе полоса читается второй случайной
     карточкой на странице. */
  margin-top: var(--s-4);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-5) var(--s-7);
  padding: var(--s-5) var(--s-6);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
  max-inline-size: var(--gift-machine-max);
  margin-inline: auto;
}
.gifts__launch[hidden] { display: none; }
.gifts__dates {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4) var(--s-6);
  flex: 1 1 auto;
}
/* Разделитель между событиями — тонкая стойка, а не второй заголовок. */
.gifts__date + .gifts__date {
  padding-inline-start: var(--s-6);
  border-inline-start: 1px solid var(--c-line);
}
/* Даты — две главные цифры финала, ради них человек сюда и шёл. В строку
   их ставит компактность, но по весу они обязаны читаться раньше подписей:
   подпись сверху мелкой строкой, число под ней в display-шрифте. */
.gifts__date {
  display: grid;
  gap: var(--s-1);
}
.gifts__date dt {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-3);
}
.gifts__date dd {
  margin: 0;
  font-family: var(--f-display);
  font-variant-numeric: var(--fvn-display);
  font-size: var(--t-xl);
  line-height: 1.05;
  color: var(--c-text-2);
  white-space: nowrap;
}
.gifts__date dd span { font-size: var(--t-sm); color: var(--c-text-2); margin-inline-start: var(--s-2); }
/* Старт весомее ОБТ: это и есть событие, ради которого всё остальное.
   Акцент точечный — он на подписи, число несёт размер и контраст. */
.gifts__date--main dt { color: var(--c-accent); }
.gifts__date--main dd { font-size: var(--t-2xl); color: var(--c-text); }

@media (max-width: 768px) {
  /* Лента помещается целиком, кнопка во всю ширину, места под подарки —
     в столбец. Ячейка мельче, но не настолько, чтобы подпись перестала
     читаться на ходу. */
  /* Ячейка мельче не ради экономии места: при трёх целых ячейках без
     обрезанных соседей лента читается статичным выбором из трёх карточек,
     а не куском бесконечной ленты. По краям обязаны быть половинки. */
  :root { --gift-cell: 92px; --gift-frame: 72px; }
  .gifts__machine { padding: var(--s-4); gap: var(--s-3); }
  .gifts__reel { padding-block: var(--s-5); }
  .gifts__slots { grid-template-columns: 1fr; gap: var(--s-2); }
  .gifts__row { flex-direction: column; align-items: stretch; }
  .gifts__row .btn, .gifts__row--form .field__control { width: 100%; }
  .gifts__status { flex: none; }
  .gifts__launch { flex-direction: column; align-items: stretch; text-align: center; }
  .gifts__dates { flex-direction: column; align-items: center; }
  .gifts__date + .gifts__date { padding-inline-start: 0; border-inline-start: 0; }
  .gifts__launch .btn { width: 100%; }
}

/* Фон регистрации. Кадр здесь работает частью композиции, а не тёмной
   подложкой: панель компактная и полупрозрачная, вокруг неё сцена должна
   читаться. Общий scrim секций для этого кадра слишком плотный. */
.registration-scene .scene::after {
  /* Вуаль ослаблена: обе панели секции теперь со своим фоном и кромкой,
     и читаемость текста больше не держится на затемнении кадра. */
  background:
    linear-gradient(90deg, rgba(6,6,8,0.42) 0%, rgba(6,6,8,0.56) 38%, rgba(6,6,8,0.50) 70%, rgba(6,6,8,0.28) 100%),
    linear-gradient(0deg, rgba(6,6,8,0.84) 0%, rgba(6,6,8,0.10) 46%, rgba(6,6,8,0.46) 100%);
}
/* Кадр был слишком приближен. Причина не в самом кадре, а в выносе сцены:
   слой уходит на --cam-bleed вверх и вниз, коробка получается заметно выше
   пропорции 16:9, и `cover` съедает больше половины ширины. Уменьшаем вынос
   именно здесь — стык остаётся мягким, а кадра видно вдвое больше. */
.registration-scene { --cam-bleed: 12svh; }
/* Смысловой центр кадра — фигура с луком, в исходнике она левее середины.
   Панель стоит слева, поэтому фигуру держим правее её края. */
.registration-scene .scene__img { object-position: 38% 46%; }

@media (max-width: 900px) {
  /* На узком экране панель занимает почти всё, и кадр остаётся только
     по краям — сдвигаем фокус к фигуре, иначе видно один песок. */
  .registration-scene .scene__img { object-position: 34% 40%; }
}

/* Видимые подписи доступны для выделения; декоративные слои по-прежнему
   пропускают указатель. Скрытые подписи не перекрывают карту. */
.dive__line,
.mappin.is-active .mappin__caption,
.mappin:hover .mappin__caption,
.mappin:focus-visible .mappin__caption,
.timeline__btn:hover .timeline__tip,
.timeline__btn:focus-visible .timeline__tip,
.compare__fps, .compare__label {
  pointer-events: auto;
  cursor: text;
}
.compare-note, .compare-control { font-size: var(--t-caption); }

/* Keep the entire gameplay frame visible, including skill effects and targets. */
.media-frame .feature-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--c-void);
}

@media (min-width: 901px) {
  .updates__slide.update--motion:not(.update--skills) {
    grid-template-rows: auto auto;
    align-items: start;
    gap: var(--s-5);
  }
  .updates__slide.update--motion:not(.update--skills) > .update__media { grid-area: auto; order: -1; }
  .updates__slide.update--motion:not(.update--skills) > .update__text {
    grid-area: auto;
    align-self: start;
    padding: 0;
    background: none;
  }
}

/* Animation clips have their baked-in letterbox removed. The frame follows
   the footage ratio, so neither sidebars nor extra cropping are needed. */
.updates__slide.update--motion .media-frame {
  position: relative;
  aspect-ratio: 160 / 81;
  block-size: auto;
}
.media-frame .feature-video {
  position: absolute;
  inset: 0;
  object-fit: cover;
  pointer-events: none;
}

/* One continuous scene behind the closing invitation and footer. */
.closing {
  position: relative;
  isolation: isolate;
  background: var(--c-void);
}
.closing__scene {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.closing__scene .scene__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: min(100%, var(--closing-scene-height));
  object-fit: cover;
  object-position: center top;
  -webkit-mask-image: linear-gradient(transparent, #000 var(--s-8), #000 55%, transparent 100%);
  mask-image: linear-gradient(transparent, #000 var(--s-8), #000 55%, transparent 100%);
}
.closing__scene::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--closing-scene-shade);
}
.closing > .footer {
  border-top: 0;
  background: transparent;
}

.closing__scene .scene__img { animation: none; transform: none; }
.nostalgia { position: relative; min-width: 0; max-width: 100%; }
.nostalgia__track {
  position: relative;
  display: flex;
  gap: var(--s-4);
  padding-inline: 8%;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-inline: contain;
  scrollbar-width: none;
}
.nostalgia__track::-webkit-scrollbar { display: none; }
.nostalgia__slide {
  flex: 0 0 100%;
  min-width: 0;
  aspect-ratio: 16 / 9;
  scroll-snap-align: center;
  overflow: hidden;
  border-radius: var(--r-md);
  opacity: .45;
  transition: opacity var(--d-base) var(--e-out);
}
.nostalgia__slide.is-active { opacity: 1; }
.nostalgia__slide img { display: block; width: 100%; height: 100%; object-fit: cover; }
.nostalgia__controls { display: flex; justify-content: center; align-items: center; gap: var(--s-4); margin-top: var(--s-5); }
.nostalgia__controls button { min-width: var(--tap); min-height: var(--tap); display: inline-flex; justify-content: center; align-items: center; color: var(--c-text); }
.nostalgia__count { color: var(--c-text-2); font-size: var(--t-sm); font-variant-numeric: tabular-nums; min-width: 5ch; text-align: center; }
.nostalgia__autoplay { padding-inline: var(--s-3); font-size: var(--t-sm); border: 1px solid var(--c-line-strong); border-radius: var(--r-pill); }
@media (max-width: 768px) {
  .nostalgia__track { padding-inline: 4%; gap: var(--s-2); }
  .nostalgia__controls { gap: var(--s-2); flex-wrap: wrap; }
}
@media (prefers-reduced-motion: reduce) { .nostalgia__slide { transition: none; } }

/* Кланы и регистрация — одна сцена, от сообщества к присоединению. */
.registration-scene { padding-block-start: var(--s-8); }
.registration-scene .proof { padding: 0; margin: 0 auto var(--s-9); max-width: var(--page-narrow); }
.registration-scene .proof::before { content: none; }
.registration-scene .proof__inner {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "lead counters" "clans clans";
  gap: var(--s-6) var(--s-7);
  padding: 0 0 var(--s-8);
  border-bottom: 1px solid var(--c-line-strong);
}
.registration-scene .proof__title { font-size: var(--t-section); text-wrap: balance; }
.registration-scene .proof__counters { justify-self: end; }
.registration-scene .proof__counters > div { animation: none; }
.registration-scene .proof__counters dt { font-size: var(--t-3xl); }
.registration-scene .proof__counters dd { min-block-size: 0; margin-top: var(--s-2); }
.registration-scene .proof__clans { font-size: var(--t-base); color: var(--c-text-2); gap: var(--s-3) var(--s-4); }
.registration-scene .proof__clan::after { margin-inline-start: var(--s-4); }
@media (max-width: 768px) {
  .registration-scene .proof { margin-bottom: var(--s-8); }
  .registration-scene .proof__inner { gap: var(--s-5); padding-bottom: var(--s-6); }
  .registration-scene .proof__title { font-size: var(--t-xl); }
  .registration-scene .proof__counters dt { font-size: var(--t-2xl); }
  .registration-scene .proof__counters dd { font-size: var(--t-micro); letter-spacing: normal; }
  .registration-scene .proof__clans { font-size: var(--t-sm); gap: var(--s-2) var(--s-3); }
  .registration-scene .proof__clan::after { margin-inline-start: var(--s-3); }
}

/* У ростера нет висячей точки перед скрытым продолжением. */
.proof__clan:nth-last-child(2)::after { content: none; }
.registration-scene .scene::after { background: var(--recruitment-shade); }
@media (max-width: 900px) { .proof__clan:nth-child(8)::after { content: none; } }

/* После карусели сразу начинается приглашение в собирающийся мир. */
.why { padding-block-end: var(--s-8); }
@media (max-width: 768px) { .why { padding-block-end: var(--s-6); } }

/* Кланы — узнаваемые имена и прямой вход в их темы. */
.registration-scene .proof__title { color: var(--c-text); }
.registration-scene .proof .eyebrow { color: var(--c-text-2); }
.registration-scene .proof__counters dt { font-size: var(--t-clan-count); color: var(--c-accent-hi); }
.registration-scene .proof__counters dd { color: var(--c-text); font-size: var(--t-sm); }
.registration-scene .proof__clans { gap: var(--s-2) var(--s-6); }
.registration-scene .proof__clan {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-size: var(--t-lg);
  font-weight: var(--w-emphasis);
  color: var(--c-text);
}
.registration-scene .proof__clan::after { content: none; }
.registration-scene a.proof__clan {
  text-decoration: underline;
  text-decoration-color: var(--c-line-strong);
  text-underline-offset: var(--s-1);
  transition: color var(--d-fast) var(--e-out), text-decoration-color var(--d-fast) var(--e-out);
}
.registration-scene a.proof__clan:hover,
.registration-scene a.proof__clan:focus-visible { color: var(--c-accent-hi); text-decoration-color: currentColor; }
.registration-scene .proof__clans-more { min-height: var(--tap); font-size: var(--t-base); color: var(--c-text-2); text-decoration: underline; text-underline-offset: var(--s-1); }
.registration-scene .proof__clans.is-expanded .proof__clans-more { display: none; }
@media (max-width: 900px) {
  .registration-scene .proof__clan:nth-child(n + 9) { display: none; }
  .registration-scene .proof__clans.is-expanded .proof__clan { display: inline-flex; }
}
@media (max-width: 768px) {
  .registration-scene .proof__clan { font-size: var(--t-md); }
  .registration-scene .proof__clans { gap: var(--s-2) var(--s-5); }
}

/* Вуаль ностальгии не затемняет заголовок и число следующего блока. */
.why__veil { bottom: 0; }

/* Точки отделяют клановые ссылки, не попадая в их подчёркивание. */
.registration-scene .proof__clans { column-gap: var(--s-4); }
.registration-scene .proof__clan:has(+ .proof__clan)::after {
  content: '';
  flex: 0 0 var(--s-1);
  width: var(--s-1);
  height: var(--s-1);
  margin-inline-start: var(--s-4);
  border-radius: var(--r-pill);
  background: var(--c-text-3);
}
@media (max-width: 768px) {
  .registration-scene .proof__clans { column-gap: var(--s-3); }
  .registration-scene .proof__clan:has(+ .proof__clan)::after { margin-inline-start: var(--s-3); }
}

.registration-scene .proof__clan { font-size: var(--t-clan-name); }
@media (max-width: 768px) { .registration-scene .proof__clan { font-size: var(--t-clan-name-mobile); } }
.compare__video, .compare__canvas, .compare__img {
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}
