﻿/* 01-page.css — топбар, hero, сцена фильма, плашки, факты, языки, контакты. */

@layer hero {
  /* ─── СТЕКЛЯННАЯ ПАНЕЛЬ ───────────────────────────────────────────────
   * Матовое стекло в духе Apple: размытие подложки + подъём насыщенности.
   * saturate(180%) здесь несущий — именно он даёт «дорогое» стекло: цвет
   * под панелью проступает живым, а не сереет, как под обычным blur.
   *
   * ПЕРЕХОД ОБЪЯВЛЕН В БАЗОВОМ ПРАВИЛЕ, а не в .is-on. Раньше он стоял на
   * .is-on: класс приносил переход с собой при появлении, но при снятии
   * переходить было уже нечему — панель пропадала мгновенно. Это ровно то,
   * что заказчик описал как «назад скроллю — просто пропадает».
   * Уход намеренно чуть быстрее прихода: панель не должна тянуться следом. */
  .topbar {
    /* ПЛАВАЮЩАЯ КАРТОЧКА, а не полоса встык с краем экрана.
     * Отступ сверху и по бокам + скругление: панель читается отдельным
     * предметом, лежащим НАД страницей, — ради этого и затевалось стекло.
     * Полоса во всю ширину прижималась к кромке экрана, и никакое размытие
     * не спасало: у предмета не было границ, значит не было и толщины.
     * Боковой отступ через clamp — на телефоне 12 px, на большом мониторе 40:
     * фиксированное число на узком экране съело бы половину строки. */
    position: fixed; z-index: 40;
    inset: clamp(8px, 1.2vw, 14px) clamp(12px, 3vw, 40px) auto;
    border-radius: 22px;
    display: flex; align-items: center; justify-content: space-between;
    gap: 16px; padding: 10px clamp(14px, 2vw, 24px);
    /* Уход НЕ на -102%: при отступе сверху этого уже не хватает, и над экраном
     * осталась бы видна кромка. Считаем от собственной высоты плюс запас. */
    translate: 0 calc(-100% - 24px); opacity: 0;
    /* ДВА СЛОЯ ФОНА. Верхний — БЛИК: косая полоса света по поверхности.
     * Именно он отличает стекло от матовой плёнки: стекло свет ЛОВИТ, а не
     * просто пропускает. Нижний слой — сама заливка, и она намеренно жиже
     * прежней (было .74/.60): при такой плотности панель читалась плотной
     * бумагой, а не стеклом. */
    background:
      linear-gradient(104deg,
        rgb(255 255 255 / .30) 0%, rgb(255 255 255 / 0) 30%,
        rgb(255 255 255 / 0) 64%, rgb(255 255 255 / .18) 100%),
      linear-gradient(to bottom,
        rgb(255 255 255 / .52), rgb(255 253 248 / .34));
    /* ПОЧЕМУ БЛЮР ТАКОЙ БОЛЬШОЙ И ОТКУДА CONTRAST.
     * Панель высотой ~60 px и шириной больше 1200 висит над фильмом, и под ней
     * проезжают яркие объекты — телефон, светлые плашки. При blur(30px) такой
     * объект не усредняется, а остаётся под стеклом светлым пятном: панель
     * «резко меняет цвет местами». Лечится это только материалом — transition
     * здесь бесполезен, потому что меняется не состояние элемента, а живое
     * содержимое под ним.
     * Два средства, и оба сохраняют прозрачность (заливка НЕ уплотнялась):
     *   blur(64px)   — ГЛАВНОЕ средство: радиус больше высоты панели (60 px),
     *                  поэтому любое пятно размазывается по всей её толщине и
     *                  перестаёт быть пятном, становясь тоном;
     *   contrast(.78) — добавка: сжимает разброс подложки к середине до 78 %,
     *                  то есть убирает ещё примерно пятую часть оставшейся
     *                  разницы между светлым и тёмным участком. Цвет остаётся.
     * brightness чуть выше 1 — подложка светлеет, и тёмный текст держит
     * контраст даже когда снизу проезжает тёмный кадр. */
    -webkit-backdrop-filter: blur(64px) saturate(200%) contrast(.78) brightness(1.06);
    backdrop-filter: blur(64px) saturate(200%) contrast(.78) brightness(1.06);
    /* ТОЛЩИНА: яркая фаска сверху, тёмная кромка снизу, кольцо по контуру.
     * Без нижней кромки стекло выглядит наклейкой — у него нет ребра. */
    box-shadow: inset 0 1px 0 rgb(255 255 255 / .9),
                inset 0 -1px 0 rgb(42 28 16 / .05),
                inset 0 0 0 1px rgb(255 255 255 / .22),
                0 10px 34px rgb(11 11 18 / .12);
    /* backdrop-filter ОБЯЗАН быть в списке. Между светлым и тёмным вариантом
     * он меняется сильнее всего (brightness 1.06 -> .9, saturate 200 -> 180,
     * contrast .78 -> .75), и без строки перехода эта часть переключалась
     * мгновенно, пока фон и текст плавно перетекали. Глаз читает такую
     * рассинхронизацию как рывок цвета — заказчик описал это как «резко
     * меняет цвет». Списки функций у обоих вариантов совпадают по составу и
     * порядку, поэтому браузер интерполирует их покомпонентно. */
    transition: translate .42s cubic-bezier(.32, .72, 0, 1),
                opacity .28s ease,
                background .5s var(--ease-soft),
                box-shadow .5s var(--ease-soft),
                color .5s var(--ease-soft),
                -webkit-backdrop-filter .5s var(--ease-soft),
                backdrop-filter .5s var(--ease-soft);
    will-change: translate, opacity;
  }

  /* ─── ТЁМНОЕ СТЕКЛО НАД ТЁМНЫМИ ПОЛОСАМИ ─────────────────────────────────
   * Класс ставит page.js, когда под шапкой оказывается фильм или сцена с
   * пультом. Без этого прозрачное светлое стекло положило бы ТЁМНЫЙ текст на
   * тёмное видео — то есть за стеклянность заплатили бы читаемостью.
   * Здесь же стекло впервые работает по-настоящему: под ним живой цветной
   * кадр, а не белая бумага, и сквозь панель действительно что-то видно. */
  .topbar--dark {
    background:
      linear-gradient(104deg,
        rgb(255 255 255 / .16) 0%, rgb(255 255 255 / 0) 30%,
        rgb(255 255 255 / 0) 64%, rgb(255 255 255 / .10) 100%),
      linear-gradient(to bottom,
        rgb(18 18 26 / .44), rgb(11 11 18 / .30));
    /* Те же два средства, что у светлого варианта: радиус больше высоты панели
     * плюс сжатие диапазона. Здесь это нужнее всего — именно тёмное стекло
     * висит над фильмом, где под ним и проезжают яркие объекты. */
    -webkit-backdrop-filter: blur(64px) saturate(180%) contrast(.75) brightness(.9);
    backdrop-filter: blur(64px) saturate(180%) contrast(.75) brightness(.9);
    color: #F3F4F8;
    box-shadow: inset 0 1px 0 rgb(255 255 255 / .22),
                inset 0 -1px 0 rgb(0 0 0 / .30),
                inset 0 0 0 1px rgb(255 255 255 / .10),
                0 10px 34px rgb(0 0 0 / .35);
  }
  /* Кнопка инвертируется теми же значениями, что в тёмном hero, включая
   * бирюзовую кромку ВНИЗ (поправка 18): на тёмном свет идёт снизу. */
  /* :not(.btn--rainbow) — радужная кнопка несёт СВОЮ заливку и свою рамку.
   * Без исключения этот селектор (0,2,0) перебил бы её (0,1,0), и от
   * MagicUI-кнопки осталась бы обычная светлая пилюля. */
  .topbar--dark .btn:not(.btn--rainbow) {
    background: #F3F4F8; color: #0B0B12;
    box-shadow: inset 0 -1px 0 rgb(111 211 234 / .5),
                0 0 0 0 rgb(111 211 234 / 0),
                0 8px 24px rgb(0 0 0 / .45);
  }
  .topbar--dark .btn:not(.btn--rainbow):hover {
    box-shadow: inset 0 -1px 0 rgb(111 211 234 / .8),
                0 0 0 4px rgb(111 211 234 / .26),
                0 12px 30px rgb(0 0 0 / .55);
  }
  .topbar--dark a:focus-visible { outline-color: #F3F4F8; }
  /* ЗДЕСЬ ПЕРЕЧИСЛЕН ВЕСЬ НАБОР, А НЕ ТОЛЬКО ВЪЕЗД, И ЭТО НЕ ИЗБЫТОЧНОСТЬ.
   * transition — шорткат: указав в нём две строки, правило СТИРАЛО переходы
   * материала, объявленные у .topbar. Селектор (0,2,0) перебивал (0,1,0), а
   * класс is-on висит на панели всё время, пока она видна, — то есть плавная
   * смена тона работала ровно тогда, когда панель спрятана, и никогда тогда,
   * когда на неё смотрят. Отсюда и рывок при въезде в тёмную полосу.
   * Если правишь длительности въезда — не забудь оставить материал. */
  .topbar.is-on {
    translate: 0 0; opacity: 1;
    transition: translate .58s var(--ease-out-expo),
                opacity .42s ease,
                background .5s var(--ease-soft),
                box-shadow .5s var(--ease-soft),
                color .5s var(--ease-soft),
                -webkit-backdrop-filter .5s var(--ease-soft),
                backdrop-filter .5s var(--ease-soft);
  }

  /* ПОД ВУАЛЬЮ ШАПКА МЕНЯЕТ СОСТОЯНИЕ МГНОВЕННО. Класс ставит page.js на время
   * прыжка по якорю.
   * Зачем: прыжок мгновенный, а состояние шапки пересчитывает
   * IntersectionObserver — асинхронно, на следующем кадре. К этому моменту
   * вуаль уже начинает подниматься, и гость видит не результат перехода, а
   * САМ УХОД шапки: она доигрывает translate 0.42 с и смену материала 0.5 с
   * из-под редеющей вуали. Заметно это только на кнопке-логотипе (#top) — это
   * единственная цель, где шапка обязана исчезнуть. На #contacts hero остаётся
   * за экраном, шапка нужна, уходить ей не надо, и глюка там нет.
   * Смысл вуали в том, чтобы перестановки были не видны; переходы внутри неё
   * этот смысл отменяют. */
  html.is-jumping .topbar { transition: none; }
  /* Нижней линии-кромки здесь БОЛЬШЕ НЕТ, и это следствие скругления.
   * Она отделяла прямоугольную полосу от страницы под ней. У плавающей
   * карточки отделять нечего — её границы задают кольцо и тень, — а сама
   * линия шла бы поперёк закруглённых углов и торчала бы из них двумя
   * хвостами. Если панель когда-нибудь вернут к полной ширине, линию надо
   * вернуть вместе с ней. */
  /* Без backdrop-filter стекло не читается: подложка не размыта, и
   * полупрозрачный фон превращается в грязь. Тогда — плотная бумага. */
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .topbar { background: color-mix(in oklab, var(--paper) 96%, transparent); }
  }
  /* Предохранитель: класс ставит page.js, когда проба понизила тир видео.
   * Стекло — самый дорогой композитинг на странице, поэтому на слабой машине
   * оно уходит ПЕРВЫМ, раньше качества плёнки. Панель остаётся плотной
   * бумагой: читается так же, стоит почти ноль. */
  html.no-glass .topbar {
    -webkit-backdrop-filter: none; backdrop-filter: none;
    background: color-mix(in oklab, var(--paper) 96%, transparent);
  }
  .topbar__brand { display: flex; align-items: center; gap: 10px; font-weight: 500; }
  .topbar__brand span { font-size: .95rem; letter-spacing: -.01em; }


  .hero { position: relative; min-height: 130svh; background: var(--paper); }

  /* ─── HERO на Neon Swarm (shaders.com, пресет заказчика) ──────────────────
   * Тёмный hero на светлой странице — сознательное решение заказчика
   * («поставь это вместо нашего hero»). Фон под canvas повторяет RadialGradient
   * пресета, поэтому до загрузки рантайма (2.5 МБ, грузится после load) и без
   * WebGPU hero выглядит тем же кадром, только без роя.
   *
   * ГЕОМЕТРИЯ ЗДЕСЬ И В ШЕЙДЕРЕ — ОДИН КОНТРАКТ, НЕ ПОДБИРАТЬ НА ГЛАЗ.
   * `120% 110% at 50% 100%` = эллипс rx=1.2W, ry=1.1H из нижнего центра.
   * Тот же эллипс в шейдере задаётся парой (radius, aspect):
   *     rx = radius*H/aspect,  ry = radius*H
   *  →  radius = 1.1,  aspect = 1.1 / (1.2 * W/H)          ← js/hero-shader.js
   * Менять размер здесь — значит пересчитать aspect там, иначе фон и шейдер
   * разъедутся и на переключении opacity будет виден подмен кадра.
   *
   * ПОЧЕМУ 110%, А НЕ 80%. При ry=0.8H градиент упирается в свой последний
   * стоп ВНУТРИ экрана, дальше идёт плоский #000. На этой границе рвётся
   * первая производная — глаз видит полосу Маха, то есть чёткую кромку между
   * «чуть светлее чёрного» и чистым чёрным. В шейдере было хуже: radius и
   * aspect не задавались вовсе, брались умолчания (1 и 1), и плоским чёрным
   * оказывалось 22% кадра на 1920×950 и 34% на 21:9 — две почти вертикальные
   * дуги слева и справа, тот самый «квадрат вокруг шейдера» со скрина
   * заказчика. 110% выбрано так, чтобы стоп #000 приходился РОВНО на верхние
   * углы: клампа внутри кадра нет ни на одном вьюпорте, рваться нечему. */
  .hero--shader {
    background: radial-gradient(120% 110% at 50% 100%, #FFF 0%, #ECEEF3 100%);
    min-height: 100svh;
  }
  .hero--shader[data-theme="dark"] {
    background: radial-gradient(120% 110% at 50% 100%, #212130 0%, #000 100%);
    color: #F3F4F8;
  }
  /* ОДНА ВЕРТИКАЛЬНАЯ ОСЬ. Прежняя раскладка ставила текст слева, а знак —
   * справа по центру высоты: два объекта без общей оси и без общего центра,
   * плюс пустая треть экрана сверху. Читалось как «криво и мелко».
   * Теперь всё на одной оси: тег, знак, имя, подпись, факты, кнопки.
   * Знак — самый крупный элемент, имя под ним работает как вордмарк. */
  .hero--shader .hero__inner {
    display: flex; flex-direction: column;
    align-items: center; justify-content: center; text-align: center;
    height: 100svh; padding: 4svh var(--pad);
    max-width: none;                    /* имя дышит на широких экранах */
  }
  /* Резерв под знак: hero-shader.js меряет ЭТОТ элемент и ставит знак ровно
   * сюда, поэтому вёрстка и шейдер не могут разъехаться ни на одном вьюпорте.
   * 52svh — знак должен ДОМИНИРОВАТЬ: он и есть главный объект первого экрана.
   * Вертикальный бюджет посчитан под 900 px: 36 (поля) + 41 (тег) + 468 (знак)
   * + 126 (имя) + 47 (подпись) + 37 (факты) + 80 (кнопки) = 835 из 900. */
  .hero__markspace {
    width: 100%; height: clamp(190px, 52svh, 520px);
    /* СЖИМАЕМЫЙ, а не фиксированный: при длинном тексте или низком окне
     * лишнее отдаёт знак, а не выезжает стек. align() в hero-shader.js
     * меряет фактический прямоугольник, поэтому шейдер следует за слотом
     * автоматически — подгонять числа под вьюпорт не требуется. */
    flex: 0 1 auto; min-height: 150px;
  }

  .hero--shader .hero__kicker { margin: 0 0 clamp(12px, 2.2svh, 24px); }
  /* Имя — в одну строку: это вордмарк, а не заголовок в три этажа.
   * 5.6vw вместо 6.6: кегль отдан ЗНАКУ. В паре «крупный объект + имя под
   * ним» доминировать должен объект, иначе они спорят и композиция рассыпается
   * — ровно это и было на скрине заказчика. */
  .hero--shader .hero__name {
    font-size: clamp(1.8rem, 5.6vw, 7.5rem);
    line-height: 1; letter-spacing: -.04em; margin-top: clamp(14px, 2.6svh, 32px);
  }
  .hero--shader .hero__sub {
    max-width: 44ch; margin-top: clamp(10px, 1.8svh, 20px);
    font-size: clamp(1rem, 1.3vw, 1.28rem);
  }
  .hero--shader .hero__facts { margin-top: clamp(10px, 1.6svh, 20px); }
  .hero--shader .hero__cta {
    margin-top: clamp(18px, 2.8svh, 32px); justify-content: center;
  }
  .hero__shader {
    position: absolute; inset: 0; width: 100%; height: 100%; display: block;
    opacity: 0; transition: opacity .6s ease; z-index: 0;
  }
  .hero--live .hero__shader { opacity: 1; }

  /* ГАШЕНИЕ НИЗА HERO В ЧИСТЫЙ ЧЁРНЫЙ.
   * Радиальный градиент стоит `at 50% 100%`, то есть у нижней кромки по
   * центру он САМЫЙ СВЕТЛЫЙ (#212130), а фильм под ним начинается с чёрной
   * вуали — на стыке получалась отчётливая горизонтальная линия. Кладём
   * поверх canvas длинную растяжку в #000: к низу и шейдер, и фон приходят
   * ровно в тот же чёрный, с которого стартует плёнка, — шва не остаётся.
   * Много промежуточных стопов вместо двух: на тёмных тонах градиент из двух
   * точек полосит на 8-битных экранах. */
  .hero--shader[data-theme="dark"]::after {
    content: ''; position: absolute; inset: auto 0 0 0; height: 42svh;
    z-index: 1; pointer-events: none;
    background: linear-gradient(to bottom,
      rgb(0 0 0 / 0) 0%,
      rgb(0 0 0 / .10) 18%,
      rgb(0 0 0 / .26) 34%,
      rgb(0 0 0 / .46) 48%,
      rgb(0 0 0 / .66) 62%,
      rgb(0 0 0 / .84) 76%,
      rgb(0 0 0 / .95) 88%,
      #000 100%);
  }
  /* Текст остаётся над растяжкой */
  .hero--shader .hero__inner { position: relative; z-index: 2; }
  /* КУРСОР ДОЛЖЕН ДОХОДИТЬ ДО ШЕЙДЕРА. У пресета есть mouseInfluence и
   * mouseRadius — рой реагирует на указатель, — но текстовый слой лежал
   * поверх canvas и съедал pointermove на всей своей площади. Отключаем
   * события на всём стеке и возвращаем их только ссылкам. */
  /* none на САМОМ контейнере, а не только на детях: .hero__inner растянут на
   * весь экран и иначе продолжает съедать pointermove мимо текста. */
  .hero--shader .hero__inner { pointer-events: none; }
  .hero--shader .hero__cta a { pointer-events: auto; }
  /* Параллакс: сдвиг ведёт JS в --px/--py, здесь только сглаживание.
   * Заголовок и знак идут с разной амплитудой, отсюда ощущение глубины. */
  .hero--shader .hero__kicker,
  .hero--shader .hero__name,
  .hero--shader .hero__sub,
  .hero--shader .hero__facts,
  .hero--shader .hero__cta {
    /* --px/--py приходят из JS УЖЕ в px: длина × число в calc() надёжнее,
     * чем число × число × 1px — вторая форма молча схлопывалась в 0. */
    translate: calc(var(--px, 0px) * var(--depth, 1))
               calc(var(--py, 0px) * var(--depth, 1));
    transition: translate .5s cubic-bezier(.22, .61, .36, 1);
  }
  .hero--shader .hero__kicker { --depth: .35; }
  .hero--shader .hero__name   { --depth: 1; }
  .hero--shader .hero__sub    { --depth: .7; }
  .hero--shader .hero__facts  { --depth: .5; }
  .hero--shader .hero__cta    { --depth: .3; }
  @media (prefers-reduced-motion: reduce) {
    .hero--shader .hero__inner > * { translate: none; }
  }

  .hero--shader[data-theme="dark"] .hero__kicker,
  .hero--shader[data-theme="dark"] .hero__sub,
  .hero--shader[data-theme="dark"] .hero__facts { color: rgb(243 244 248 / .78); }
  /* Кнопка на тёмном. Свечение знака перевёрнуто: кромка уходит ВНИЗ, потому
   * что на первом экране источник света снизу — из шейдера, — и блик обязан
   * следовать за ним, иначе кнопка выглядит подсвеченной ниоткуда.
   * box-shadow приходится повторять целиком: этот селектор специфичнее
   * базового `.btn`, а частичного наследования у box-shadow нет. */
  /* :not(.btn--rainbow) — у радужной кнопки своя заливка и своя рамка. Без
   * исключения этот селектор (0,3,0) перебил бы её (0,1,0) и превратил бы
   * MagicUI-кнопку обратно в светлую пилюлю. */
  .hero--shader[data-theme="dark"] .btn:not(.btn--rainbow) {
    background: #F3F4F8; color: #0B0B12;
    box-shadow: inset 0 -1px 0 rgb(111 211 234 / .5),
                0 0 0 0 rgb(111 211 234 / 0),
                0 8px 24px rgb(0 0 0 / .45);
  }
  .hero--shader[data-theme="dark"] .btn:not(.btn--rainbow):hover {
    box-shadow: inset 0 -1px 0 rgb(111 211 234 / .8),
                0 0 0 4px rgb(111 211 234 / .26),
                0 12px 30px rgb(0 0 0 / .55);
  }
  .hero--shader[data-theme="dark"] .btn--ghost {
    background: transparent; color: #F3F4F8;
    box-shadow: inset 0 0 0 1.5px rgb(243 244 248 / .38);
  }
  /* Та же ловушка специфичности, что в 00-base.css: `… .btn:hover` (0,4,0)
   * перебивает `… .btn--ghost` (0,3,0), и без этой строки «Смотреть →»
   * по ховеру получал бы бирюзовое кольцо главной кнопки. */
  .hero--shader[data-theme="dark"] .btn--ghost:hover {
    box-shadow: inset 0 0 0 1.5px rgb(243 244 248 / .55);
  }
  .hero--shader[data-theme="dark"] a:focus-visible { outline-color: #F3F4F8; }

  @media (max-width: 759px) {
    .hero--shader .hero__inner { grid-template-columns: 1fr; padding-bottom: 14svh; }
  }
  .hero__inner {
    position: sticky; top: 0; height: 100svh;
    display: grid; align-content: center;
    grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: 24px;
    max-width: var(--wrap); margin: 0 auto; padding: 0 var(--pad);
    transform-origin: left center;
  }
  .hero__kicker { font-size: 12.5px; color: var(--ink-soft); margin-bottom: 22px; }
  .hero__name {
    font-size: clamp(2.6rem, 6.2vw, 6.2rem); line-height: .94;
    letter-spacing: -.035em; font-weight: 620;
  }
  .hero__sub {
    margin-top: 20px; max-width: 26ch; color: var(--ink-soft);
    font-size: clamp(1.05rem, 1.5vw, 1.35rem); font-weight: 450; line-height: 1.3;
  }
  .hero__facts { margin-top: 26px; font-size: 13.5px; color: var(--ink-soft); }
  .hero__cta { margin-top: 34px; display: flex; flex-wrap: wrap; gap: 12px; }

  .hero__mark { justify-self: end; align-self: center; }
  /* Жёсткий потолок: без него длинные заголовки на узких вьюпортах заезжают
   * на знак — это ловили в старой сборке. */
  .hero__mark img { width: min(28vw, 380px); height: auto; }

  @media (max-width: 1199px) {
    .hero__inner { grid-template-columns: 1fr; justify-items: start; }
    .hero__mark { order: -1; justify-self: start; }
    .hero__mark img { width: min(30vw, 240px); }
  }
  @media (max-width: 759px) {
    .hero__inner { height: auto; min-height: 100svh; padding-top: 88px; }
    .hero__mark { position: absolute; top: 76px; right: var(--pad); order: 0; }
    .hero__mark img { width: 128px; }
    .hero__name { font-size: clamp(2.2rem, 11vw, 3.4rem); }
    .hero__facts { line-height: 2; }
    .hero__cta .btn--ghost { display: none; }
  }
}

@layer film {
  .film { position: relative; }
  /* sticky, а не fixed: у fixed-элемента нет view()-таймлайна, и он висел бы
   * под hero и просвечивал под контактами (§5.6). */
  .film__stage {
    position: sticky; top: 0; height: 100svh;
    margin-bottom: -100svh;            /* не занимает высоту потока */
    z-index: 1; overflow: clip; background: var(--bg);
  }
  .film__stage > * {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; object-position: center 46%;
  }
  /* Постер лежит ПОД видеослоями (z 0 против 1-3). Гасить его можно только
   * после того, как видео уже непрозрачно, — тогда его растворение невидимо
   * и фон сквозь него не просвечивает. Раньше видео само проявлялось 0.2 c,
   * и в это окно постер уже уходил: сумма альф падала ниже 1, фон --bg
   * пробивался — «цвет меняется перед стартом». Теперь видео включается
   * мгновенно, а постер тихо уходит следом. */
  .film__poster { transition: opacity .25s linear .05s; }
  .film__poster img { width: 100%; height: 100%; object-fit: cover; object-position: center 46%; }
  /* НИКАКИХ transition на видеослоях. Стык частей — жёсткая склейка на
   * точном кадре (§2.0 №3): это байтовая нарезка одного непрерывного рендера,
   * кадр 179 -> 180 такой же шаг, как любой внутри части. Растворение здесь
   * стояло 0.2 c, и в середине перехода оба слоя были на 0.5 — сквозь них на
   * 25 % просвечивал фон --bg, тёмная виньетка. Заказчик видел это как
   * «в каждом обрезе меняется цвет». */
  .film__tape, .film__part { opacity: 0; }

  .film__beats { position: relative; z-index: 10; pointer-events: none; }
  .beat { height: var(--len); display: flex; padding: 0 var(--pad); }
  .beat[data-side="right"] { justify-content: flex-end; }

  /* ─── ПЛАШКА-РЕПЛИКА — главный приём (§8.3) ──────────────────────────────
   * Нижний левый угол срезан до 8 px: это хвост речевого пузыря из знака.
   * Не декор — мы применяем существующий компонент бренда. Остальные три
   * угла подняты с 22 до 26 px: у плотной бумаги радиус мельче, у стекла
   * крупнее, иначе оно читается наклейкой.
   *
   * КРЕМОВАЯ БУМАГА ЗАМЕНЕНА ТЁМНЫМ ДЫМЧАТЫМ СТЕКЛОМ (просьба заказчика).
   * Формально это отход от §9.3 п.19, где glassmorphism в списке запретов;
   * фактически стекло на странице УЖЕ живёт — топбар сделан ровно этим
   * приёмом, — и плашка теперь говорит с ним на одном языке, а не спорит.
   * Два слоя фона те же, что у .topbar--dark: косой блик сверху (стекло
   * свет ЛОВИТ) и заливка под ним.
   *
   * КОНТРАСТ. Раньше он был фиксированный, потому что подложка была
   * непрозрачной; теперь сквозь плашку проходит кадр, и держать AA
   * приходится расчётом — от ЗАМЕРЕННОГО худшего кадра (R244 G242 B244,
   * бит «арена»; см. 00-base.css и tools/glass.py). Держим тремя вещами:
   *   1) плотностью заливки (--plate-a1/a2 — доля тёмного в верхней и
   *      нижней точке градиента);
   *   2) brightness(.62) в самом backdrop-filter: подложка ГАСИТСЯ до
   *      того, как через неё что-то просвечивает (у топбара тот же приём
   *      с .86);
   *   3) парой чернил --glass-ink / --glass-ink-soft, подобранной по
   *      худшему случаю (см. 00-base.css).
   * На замеренном худшем кадре под самым светлым углом блика: заголовок
   * 7.84:1, абзац и шаги 5.80:1, бирюзовый акцент 6.22:1 — все выше AA.
   *
   * СКРАБ. backdrop-filter пересчитывает размытие подложки на КАЖДОМ кадре,
   * и план запрещал его именно поэтому. Поэтому он стоит НЕ здесь, а ниже,
   * за тремя предохранителями (@supports, десктоп, html.no-glass), а
   * page.js вдобавок прячет неактивные плашки через visibility: без этого
   * браузер считал бы размытие для всех семи разом. База — без размытия и
   * почти непрозрачная: она обязана быть читаемой сама по себе. */
  .plate {
    /* доля тёмного в заливке: верх / низ. Ниже, под стеклом, разжижается */
    --plate-a1: .88; --plate-a2: .93;
    position: sticky; top: var(--y, 50%); translate: 0 -50%;
    align-self: flex-start; pointer-events: auto;
    color: var(--glass-ink);
    /* Блик тише прежнего (.06 против .10) и уже: широкая белая полоса по
     * диагонали читалась как засвет на панели, а не как отражение в стекле. */
    background:
      linear-gradient(104deg,
        rgb(255 255 255 / .06) 0%, rgb(255 255 255 / 0) 26%,
        rgb(255 255 255 / 0) 72%, rgb(255 255 255 / .04) 100%),
      linear-gradient(to bottom,
        rgb(22 24 32 / var(--plate-a1)), rgb(11 11 18 / var(--plate-a2)));
    border-radius: 20px 20px 20px 6px;
    padding: clamp(16px, 1.45vw, 21px) clamp(18px, 1.7vw, 24px);
    max-width: 30ch; min-width: 230px;
    /* ТОЛЩИНА. Кромка ярче, чем была (.18 против .09), а тёмное ребро снизу
     * почти снято: у прозрачного стекла ребро подсвечено, а не залито
     * чернотой — именно чёрное ребро делало плашку панелью. */
    box-shadow: inset 0 1px 0 rgb(255 255 255 / .30),
                inset 0 -1px 0 rgb(0 0 0 / .18),
                inset 0 0 0 1px rgb(255 255 255 / .18),
                var(--shadow-glass);
  }
  /* Размытие подложки — только там, где за него нечем платить.
   * Три предохранителя: браузер умеет; экран не телефонный (на мобильном
   * плашка и так во всю ширину, размывать под ней полкадра — самый дорогой
   * из возможных вариантов); проба не понизила тир видео. Последний —
   * тот же класс, которым гасится стекло топбара: на слабой машине оба
   * эффекта уходят вместе, раньше качества плёнки. */
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    @media (min-width: 760px) {
      html:not(.no-glass) .plate {
        /* Радиус размытия ВДВОЕ больше прежнего, гашение почти снято.
         * Так работают материалы iOS: подложку не топят в темноте, её
         * размазывают до цветового пятна и поднимают насыщенность. Кадр под
         * плашкой остаётся узнаваемым по цвету и движению — это и читается
         * как стекло. Прежние blur(26) + brightness(.62) пропускали к глазу
         * 15 % кадра, эти — 33 %. */
        --plate-a1: .62; --plate-a2: .66;
        -webkit-backdrop-filter: blur(44px) saturate(180%) brightness(.86);
        backdrop-filter: blur(44px) saturate(180%) brightness(.86);
      }
    }
  }
  /* Кегль опущен почти вдвое (было 1.5-2.2rem), вес с 620 до 560, интерлиньяж
   * с 1.06 до 1.16. Плашка — реплика, а не заголовок секции: на кадре она
   * должна читаться подписью, а не перекрикивать кино. balance выравнивает
   * длину строк, чтобы в двухстрочном заголовке не висело одно слово. */
  .plate h2 {
    font-size: clamp(1.18rem, 1.52vw, 1.48rem); font-weight: 560;
    line-height: 1.16; letter-spacing: -.018em; text-wrap: balance;
  }
  .plate p {
    margin-top: 7px; color: var(--glass-ink-soft);
    font-size: clamp(.875rem, .95vw, .98rem); line-height: 1.5; text-wrap: pretty;
  }
  /* Нижняя строка плашки отделена волосяной линией, а не просто отступом:
   * факты — не продолжение мысли, а её подпись. */
  .plate .chip {
    margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--glass-line);
    display: flex; flex-wrap: wrap; gap: 6px;
  }
  /* Пилюли вместо строки с разделителем «·»: каждый факт — свой объект.
   * Точка-разделитель из разметки убрана, поэтому старого правила
   * `.chip span { opacity: .45 }` больше нет — оно гасило именно её. */
  /* Пилюля НЕ подсвечивается изнутри. Любая светлая заливка поднимает фон
   * ровно под своим же текстом: бирюзовая в 7 % роняла его до 4.30:1, белая
   * в 10 % — до 4.40:1. Обе ниже AA. Осталась кромка и тень в 6 % — пилюля
   * очерчена, но фона под текстом не меняет. Цвет бренда живёт в кромке:
   * там он ничего не читает и никому не мешает. */
  .plate .chip span {
    padding: 4px 9px; border-radius: 999px;
    font-size: 11px; line-height: 1.3; letter-spacing: .005em;
    color: var(--glass-accent);
    background: rgb(0 0 0 / .06);
    box-shadow: inset 0 0 0 1px rgb(111 211 234 / .34);
  }
  .plate__cta { margin-top: 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }
  .plate__cta .tel { font-weight: 500; color: var(--glass-accent); }

  .steps { margin-top: 10px; display: grid; gap: 5px; counter-reset: s; }
  .steps li {
    counter-increment: s; display: flex; gap: 8px; align-items: baseline;
    font-size: .875rem; color: var(--glass-ink-soft);
    opacity: 1;                        /* база = конечное видимое состояние */
  }
  /* Номер шага белый, а не бирюзовый: на светлом стекле бирюзовый давал
   * 3.6:1 и первым уходил в фон. Он выделен весом и моношириной, а не цветом. */
  .steps li::before {
    content: counter(s); flex: none;
    font-family: 'Geist Mono', monospace; font-size: 10.5px; font-weight: 500;
    color: var(--glass-accent);
  }
  .steps[start="4"] { counter-reset: s 3; }

  /* оверлей экрана телефона — только на холде (кадры 216-252) */
  .phone { display: grid; place-items: center; pointer-events: none; }
  .phone[hidden] { display: none; }
  .phone__screen {
    width: clamp(150px, 15vw, 210px); aspect-ratio: 9 / 19.5;
    border-radius: 14% / 6.5%; background: linear-gradient(#12212B, #0C161D);
    display: grid; place-items: center; align-content: center; gap: 14px;
    box-shadow: 0 24px 60px rgb(0 0 0 / .38);
  }
  .phone__mark { width: 34%; height: auto; }
  .phone__name { color: #E8F6FB; font-size: 11px; letter-spacing: .02em; opacity: .82; }
  .phone__ring {
    width: 26px; height: 26px; border-radius: 50%;
    border: 2px solid rgb(232 246 251 / .22); border-top-color: var(--cyan);
    animation: spin 1s linear infinite;
  }
  @keyframes spin { to { rotate: 360deg; } }

  /* Мобильная раскладка: полоса 16:9 в верхних ~58 % экрана, остальное залито
   * тем же интерполированным --bg. Фоны в фильме плоские, стык читается не
   * как леттербокс, а как композиция. Плашка живёт в нижней трети (§7.7 путь B). */
  @media (max-width: 759px) {
    /* ПОЛОСА, а не обрезка. Коробка ровно 16:9, поэтому cover ничего не
     * режет и композиция видна целиком. Раньше стояло inset: 0 0 42% 0 —
     * коробка получалась вертикальной (390x490), и cover показывал лишь
     * ~44 % ширины кадра: спикер у микрофона и пульт уезжали за край.
     * Заодно втрое падает требование к источнику: полосе шириной 390 px
     * при dpr 3 нужен кадр 1170 px, а вертикальной коробке был нужен
     * 2613-й, которого в мастере нет и быть не может. */
    .film__stage > * {
      inset: 0 0 auto 0; height: auto; aspect-ratio: 16 / 9;
      object-position: center center;
    }
    .plate { top: auto; bottom: 4%; translate: 0 0; align-self: flex-end; max-width: none; }
    .beat { align-items: flex-end; }
    .phone { inset: 0 0 auto 0; height: auto; aspect-ratio: 16 / 9; }
  }

  /* Вуаль сцены. Чистый чёрный слой поверх видео; opacity ведёт page.js по
   * номеру кадра. Работает двумя концами:
   *   кадры 0..24    — вход ИЗ ЧЁРНОГО, чтобы плёнка проявлялась из темноты
   *                    hero, а не подставлялась встык;
   *   кадры 600..720 — медленное затемнение финала. Видео при этом
   *                    ПРОДОЛЖАЕТ скрабиться: темнеет картинка, а не встаёт
   *                    плёнка. Взяты все 5 последних секунд — «с запасом»,
   *                    а не последний кадр.
   * pointer-events: none — вуаль не перехватывает курсор у сцены. */
  .film__veil {
    z-index: 6; background: #000; opacity: 0; pointer-events: none;
    will-change: opacity;
  }

  /* ─── ТЁМНЫЙ ЭКРАН МЕЖДУ ФИЛЬМОМ И СВЕТЛОЙ ЧАСТЬЮ ──────────────────────
   * Здесь ТОЛЬКО затемнение. Обратный ход — подъём вуали над лентой фактов,
   * см. .reel__night в слое outro.
   *
   * ПОЧЕМУ ПЕРЕГОН УЕХАЛ ОТСЮДА (поправка 24). Раньше низ секции был
   * крашеной рампой чёрное -> бумага. Заказчик: «ты просто поставил цветовое
   * изменение, я хочу из дарка в лайт переход как пред сцене». Пред сцена —
   * это вуаль поверх живого кадра: темнеет ВЕСЬ экран разом, одним тоном.
   * У рампы всё устроено наоборот — это пространственный градиент, у каждой
   * строки пикселей свой цвет, и глаз читает его как ползущую поперёк экрана
   * полосу, сколько стопов в неё ни добавляй. Никакой подбор оттенков этого
   * не лечит, потому что дело не в оттенках. Поэтому обратный ход сделан тем
   * же приёмом, что прямой, — вуалью, и выполняет его уже следующая секция,
   * над своим живым содержимым.
   *
   * Секции остался ровно экран копи. Стопы печатает tools/ramp.py.
   * ПОЗИЦИИ В svh, А НЕ В ПРОЦЕНТАХ: в ступени отказа секция выше (см. ниже),
   * и проценты уехали бы вместе с высотой — а вход из фильма обязан добирать
   * до чёрного на одной и той же отметке, иначе на 100svh покажется нижний
   * край кадра. */
  .handoff {
    position: relative; z-index: 12; min-height: 100svh;
    padding: 0 var(--pad); text-align: center;
    /* по умолчанию хвоста нет: секция кончается чёрной */
    --tail: #0B0B12 100%;
    /* Хвост ступени отказа. Смягчённые плечи: f(u) = u - sin(2pi*u)/(2pi),
     * производная на обоих концах ноль, поэтому ни на стыке с площадкой, ни
     * на стыке с бумагой нет излома скорости — а излом глаз читает как линию
     * поперёк экрана (поправка 23). */
    --ramp:
      #0B0B12 70svh, #0B0B12 72.38svh, #0C0C13 74.77svh,
      #0D0D14 77.15svh, #0F0F16 79.54svh, #13131A 81.92svh,
      #19191F 84.31svh, #202027 86.69svh, #2A2A30 89.08svh,
      #36363C 91.46svh, #444449 93.85svh, #545458 96.23svh,
      #666569 98.62svh, #79787B 101svh, #8C8B8D 103.38svh,
      #9F9E9F 105.77svh, #B2B0B0 108.15svh, #C3C2C0 110.54svh,
      #D2D1CE 112.92svh, #E0DEDB 115.31svh, #EAE9E5 117.69svh,
      #F3F1EC 120.08svh, #F8F7F2 122.46svh, #FCFAF5 124.85svh,
      #FEFCF7 127.23svh, #FFFDF8 129.62svh, #FFFDF8 132svh;
    background: linear-gradient(to bottom,
      rgb(11 11 18 / 0)      0svh,
      rgb(11 11 18 / 0)      6.18svh,
      rgb(11 11 18 / .04)    12.36svh,
      rgb(11 11 18 / .12)    18.55svh,
      rgb(11 11 18 / .24)    24.73svh,
      rgb(11 11 18 / .41)    30.91svh,
      rgb(11 11 18 / .59)    37.09svh,
      rgb(11 11 18 / .76)    43.27svh,
      rgb(11 11 18 / .88)    49.45svh,
      rgb(11 11 18 / .96)    55.64svh,
      rgb(11 11 18 / 1)      61.82svh,
      rgb(11 11 18 / 1)      68svh,
      var(--tail));
  }

  /* Дизеринг — ТОЛЬКО под хвост ступени отказа, поэтому в базе его нет.
   * У смягчённых плеч один код цвета держится до 11 px, и на 8-битной панели
   * это видно поперёк экрана. Тонкий монохромный шум (~1 код СКО при
   * opacity .05 — классическая амплитуда в один младший разряд) размывает
   * границу ступени в случайную, и контур перестаёт читаться. Маска гасит шум
   * к обоим концам: у стыка с бумагой он сдвинул бы средний тон на 2-3 кода,
   * и вместо полосы мы получили бы шов с секцией фактов. */
  .handoff::after {
    content: ''; display: none;
    position: absolute; left: 0; right: 0; top: 68svh; bottom: 0;
    pointer-events: none; opacity: .05;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
    background-size: 140px 140px;
    -webkit-mask-image: linear-gradient(to bottom, #0000 0, #000 10%, #000 88%, #0000 100%);
            mask-image: linear-gradient(to bottom, #0000 0, #000 10%, #000 88%, #0000 100%);
  }

  /* ─── СТУПЕНЬ ОТКАЗА ──────────────────────────────────────────────────
   * Вуаль над лентой едет на scroll-driven анимации. Там, где их нет, не
   * едет и сама лента — в базе она обычный вертикальный список, — а чёрное
   * упёрлось бы в бумагу встык. Значит только для этой ветки секция
   * удлиняется и получает крашеный перегон, и только здесь он дизерится.
   * Две ветки, а список стопов один: он лежит в --ramp выше. */
  @supports not (animation-timeline: view()) {
    .handoff { min-height: 132svh; --tail: var(--ramp); }
    .handoff::after { display: block; }
  }
  @media (prefers-reduced-motion: reduce) {
    .handoff { min-height: 132svh; --tail: var(--ramp); }
    .handoff::after { display: block; }
  }

  /* Схема hero: одна вертикальная ось, экран целиком, заголовок в оптическом
   * центре. margin-block: auto у карточки съедает весь свободный остаток,
   * поэтому нижняя строка садится ровно на низ экрана, а не болтается под
   * заголовком на случайном расстоянии. */
  .handoff__stage {
    position: relative;                  /* якорь для .grid */
    min-height: 100svh; display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    padding: clamp(72px, 12svh, 150px) 0 clamp(40px, 7svh, 90px);
  }
  .handoff__card, .handoff__lead { position: relative; z-index: 1; }
  .handoff__card { color: #F3F4F8; margin-block: auto; }
  /* max-width в em, а не в ch. Раньше стояло 34ch НА КАРТОЧКЕ, а ch считается
   * от её собственных 16 px — выходило ~300 px на любом экране, и заголовок
   * в 59 px рубился в пять строк даже на десктопе. Теперь предел живёт на
   * самом h2 и растёт вместе с кеглем: 13em — это ровно две строки. */
  .handoff__card h2 {
    max-width: 13em; margin-inline: auto;
    font-size: clamp(2.6rem, 6.6vw, 5.6rem);
    line-height: .98; letter-spacing: -.04em; text-wrap: balance;
  }
  .handoff__lead {
    max-width: 46ch; margin-top: clamp(36px, 7svh, 96px);
    color: rgb(243 244 248 / .70); line-height: 1.5;
    font-size: clamp(1rem, 1.3vw, 1.22rem);
  }
}

@layer outro {
  /* ─── ФАКТЫ: ЛЕНТА ВБОК ────────────────────────────────────────────────
   * Заказчик: «после второго экрана следующая сцена идёт вправо, потом
   * дальше по умолчанию». Здесь — раскладка и цвет; сам пин и ход поперёк
   * экрана живут в 02-effect.css, потому что это scroll-driven эффект и он
   * обязан иметь ступень отказа.
   *
   * ЦВЕТ СЧИТАЕТСЯ, А НЕ ВЫПИСЫВАЕТСЯ. Тон каждого факта — точка на дуге
   * oklch от --start до --end, шаг выводится из --count. Поэтому пятый факт
   * не потребует подбирать пятый hex: палитра перестроится сама и останется
   * равномерной. Светлота и цветность одни на всю ленту — цвета отличаются
   * ТОЛЬКО тоном, и ни один не выпрыгивает вперёд остальных. */
  .reel {
    position: relative; z-index: 12; background: var(--paper);
    --start: 18; --end: 288; --lightness: 62%; --base-chroma: .26;
    --step: calc((var(--end) - var(--start)) / (var(--count) - 1));
  }
  /* БАЗА — обычный вертикальный список (§6.2). Горизонталь включается только
   * там, где есть scroll-driven анимации: без них лента застыла бы на первом
   * кадре и три четверти фактов не прочитал бы никто. */
  .reel__stage { position: relative; padding: 110px 0 130px; }
  /* Вуаль — часть залипающей сцены, а сцены в базе нет вовсе: там лента
   * обычный вертикальный список. Значит и вуали в базе быть не должно,
   * иначе она осталась бы чёрным блоком поперёк страницы. */
  .reel__night { display: none; }
  @keyframes night-lift { from { opacity: 1; } to { opacity: 0; } }

  .reel__list {
    position: relative; z-index: 1; list-style: none; margin: 0;
    padding: clamp(36px, 6vh, 72px) var(--pad) 0;
    display: grid; gap: clamp(40px, 8vh, 90px);
  }
  .reel__list b {
    display: block; font-size: 13px; font-weight: 500; letter-spacing: .18em;
    color: oklch(var(--lightness) var(--base-chroma)
                 calc(var(--start) + var(--step) * var(--i)));
  }
  .reel__list em {
    display: block; font-style: normal; font-weight: 620;
    margin-top: clamp(6px, .9vh, 14px);
    font-size: clamp(3rem, 8.2vw, 8.5rem);
    line-height: .92; letter-spacing: -.04em;
    /* Тон берётся ЗДЕСЬ, а не заранее в --hue на .reel: там ещё нет --i,
     * var() в нём был бы невалиден, и oklch() падала целиком — все факты
     * выходили цветом --ink. Ловится только глазами. */
    color: oklch(var(--lightness) var(--base-chroma)
                 calc(var(--start) + var(--step) * var(--i)));
  }
  .reel__list span {
    display: block; margin-top: clamp(10px, 1.3vh, 20px);
    color: var(--ink-soft); font-size: clamp(1rem, 1.25vw, 1.2rem);
    line-height: 1.4; max-width: 34ch;
  }

  /* ─── КОЛОДА: ЯЗЫКИ+ФОТО -> КОНТАКТЫ ──────────────────────────────────
   * Две ЦЕЛЫЕ страницы в одной залипающей сцене. Языки и кадр пульта —
   * ОДНА страница: кадр лежит внутри той же панели и уезжает влево вместе
   * с языками, а не остаётся неподвижным полом.
   * База (нет scroll-driven анимаций) — обычная вертикальная стопка; порядок
   * в ней уже правильный по разметке, поэтому order здесь не нужен. */
  .deck { position: relative; z-index: 12; background: var(--paper); }
  .deck__stage { position: relative; display: flex; flex-direction: column; }
  .deck__track { display: contents; }
  .deck__panel { position: relative; z-index: 1; }
  /* ─── ЛЕНТА ВБОК: сам ход ──────────────────────────────────────────────
   * Пин на высоту экрана + сдвиг списка по X по таймлайну секции. Без JS и
   * без единой замеренной константы: диапазон `contain` у view-timeline —
   * это ровно то время, пока секция закрывает экран целиком, то есть ровно
   * время залипания sticky. Совпадение не случайное, поэтому лента не может
   * разъехаться с пином ни на одном вьюпорте и ни при какой длине списка.
   *
   * --screens задаёт ХОД ПРОКРУТКИ, а не длину ленты: длина у списка
   * max-content и берётся из содержимого. Больше фактов — лента станет
   * длиннее сама, вырастет только скорость; станет резкой — поднимать надо
   * --screens, а не трогать раскладку.
   *
   * Блок стоит ЗДЕСЬ, а не в 02-effect.css, хотя это scroll-driven эффект:
   * слой outro объявлен после effect (см. 00-base.css), и оттуда sticky
   * молча проигрывал бы базовому relative. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .reel {
        /* 3.5, а не 4: заголовок слева сняли, лента укоротилась на его
         * поле в 46vw, и при прежнем запасе она ползла (0.85 px вбок на px
         * вниз). Здесь — 1.02, один к одному. */
        height: calc(100svh * var(--screens, 4));
        view-timeline-name: --reel;
      }
      /* ─── ПОДЪЁМ ВУАЛИ: ИЗ ТЁМНОГО В СВЕТ ──────────────────────────
       * Зеркало финала фильма. Там чёрная вуаль ЗАКРЫВАЕТСЯ поверх живого
       * кадра; здесь она ОТКРЫВАЕТСЯ поверх живой светлой сцены. Отличие от
       * прежней крашеной рампы принципиальное: вуаль — ОДИН ТОН на весь
       * экран. У неё нет пространственного градиента, поэтому нечему
       * полосить и нечего читать как линию поперёк кадра; экран просто
       * светлеет целиком, как в кино.
       *
       * sticky + margin-bottom:-100svh — тот же приём, что у .film__stage:
       * слой пришпилен к экрану и не занимает высоту потока. Пока лента
       * только ВЪЕЗЖАЕТ, вуаль стоит на её верхнем крае и кроет низ экрана,
       * а верх экрана в этот момент — чёрный низ .handoff. Оба чёрные,
       * граница не видна. Как только лента пришпилилась, вуаль накрывает
       * экран целиком — и вот тогда поднимается.
       *
       * `both` обязателен: до начала диапазона анимация держит ПЕРВЫЙ кадр,
       * то есть вуаль непрозрачна всё время въезда. Без него она была бы
       * прозрачна, и лента выехала бы на свет раньше времени. */
      .reel__night {
        display: block;
        position: sticky; top: 0; height: 100svh; margin-bottom: -100svh;
        z-index: 4; background: #0B0B12; pointer-events: none;
        /* ease-in-out, а НЕ linear. Скорость на обоих концах ноль, и это то
         * же требование, что у стопов градиента (поправка 23), только во
         * времени: слева вуаль стыкуется с чёрной площадкой, справа — с
         * бумагой. При linear подъём трогался бы и вставал рывком, и оба
         * рывка читались бы как «щёлкнуло», а заказчик просил «аккуратно». */
        animation: night-lift ease-in-out both;
        animation-timeline: --reel;
        animation-range: contain 0% contain 20%;
        will-change: opacity;
      }
      .reel__stage {
        position: sticky; top: 0; height: 100svh; padding: 0;
        overflow-x: clip;              /* clip, а не hidden: ось Y не станет скроллером */
        display: flex; align-items: center;
      }

      .reel__list {
        display: flex; align-items: flex-start; width: max-content;
        padding: 0 14vw 0 12vw; gap: clamp(56px, 7vw, 140px);
        animation: reel-x linear both;
        animation-timeline: --reel;
        /* Старт сдвинут на 20 % — ровно на длину подъёма вуали. Без сдвига
         * первый факт успевал уехать почти на экран влево, пока его ещё не
         * видно, и читать его было нечем. Плотность хода почти не изменилась:
         * диапазон короче на 20 %, но сцена длиннее (--screens 3.5 -> 4), и
         * выходит 1.06 px вбок на px вниз вместо прежних 1.02. */
        animation-range: contain 20% contain 100%;
      }
      .reel__list em { white-space: nowrap; }
      .reel__list span { max-width: 32ch; }

      /* ─── КОЛОДА ───────────────────────────────────────────────────────
       * Тот же приём, что у ленты фактов: `contain` у view-timeline — это
       * ровно время залипания sticky, поэтому ход вбок не может разъехаться
       * с пином. Но едет здесь НЕ вся сцена: трек занимает верхнюю строку
       * грида, полоса с пультом — нижнюю и стоит на месте.
       *
       * --screens 2.5: ход ровно один экран (100vw), запас прокрутки
       * 1.5 экрана — на 1280x832 это 1.03 px вбок на px вниз, один к одному. */
      .deck {
        --screens: 3;
        height: calc(100svh * var(--screens));
        view-timeline-name: --deck;
      }
      .deck__stage {
        position: sticky; top: 0; height: 100svh;
        display: grid;
        /* Колонка ЗАКРЕПЛЕНА в экран. Без неё неявная колонка растягивается
         * по самому широкому элементу — треку в 200vw, — и всё внутри едет
         * по двойной ширине: кадр пульта масштабировался вдвое и показывал
         * только свою левую половину. Трек в эту колонку не влезает
         * намеренно и торчит вправо, а обрезает его overflow-x. */
        grid-template-columns: 100vw;
        overflow-x: clip;
      }
      .deck__track {
        grid-row: 1; justify-self: start; align-self: stretch;
        display: flex; width: 200vw;
        animation: deck-x linear both;
        animation-timeline: --deck;
        /* Ход кончается на 68 % пина, дальше — МЁРТВАЯ ВЫДЕРЖКА: страница
         * контактов стоит целиком и не двигается до самого конца документа.
         * Заказчик: «последняя сцена опускается вниз после скроллинга
         * направо, пусть замерзает».
         * На 1280x832 запас после пина замерен как ровно 0, то есть у меня
         * сцена не уезжала. Значит расхождение приходит от другой высоты
         * окна, и чинить надо не число, а схему: с выдержкой последний кадр
         * — всегда собранная страница, чем бы ни кончился пин.
         * 68 % от 2 экранов запаса даёт 1132 px хода на 1280 px пути —
         * 1.13 px вбок на px вниз, тот же темп, что у ленты фактов. */
        animation-range: contain 0% contain 68%;
      }
      /* Панель = целая страница: свой экран по ширине и по высоте.
       * Обе панели устроены одинаково — копи прижата к ОДНОЙ верхней линии,
       * низ отдан широкой полосе (слева кадр пульта, справа юрстрока).
       * Заказчик просил «на одной линии»: линия одна и та же переменная,
       * поэтому панели не могут разъехаться при правке одной из них. */
      .deck { --deck-top: clamp(84px, 11svh, 132px); }
      .deck__panel {
        width: 100vw; height: 100svh; min-width: 0; padding: 0;
        display: grid; grid-template-rows: minmax(0, 1fr) auto;
      }
      .deck__panel > :first-child { align-self: start; padding-top: var(--deck-top); }
      /* Двойной класс — НЕ украшение. Базовые .langs и .outro объявлены ниже
       * в этом же слое и с тем же весом (0,1,0), их padding-bottom в 120 px
       * выигрывал по порядку строк и съедал у страницы низ: строка 1fr
       * получала 229 px вместо 349, копи вылезала из своей строки и
       * наезжала на кадр пульта. Двойной класс снимает зависимость от
       * порядка строк в файле раз и навсегда. */
      .deck__panel.langs, .deck__panel.outro { padding: 0; }

      /* СТРОКА КОПИ У ОБЕИХ СТРАНИЦ ОДНА И ТА ЖЕ. Раньше каждая брала свою
       * естественную высоту, и полосы стартовали на разных уровнях: копи
       * контактов выше на 70 px из-за стопятидесятого кегля телефона —
       * заказчик увидел это на стыке страниц. Теперь высоту задаёт общий
       * токен, поэтому кадр пульта и кадр Trilliant начинаются на одной
       * линии по построению, а не по совпадению.
       * Побочная и нужная выгода: у языков появляется воздух между лентой
       * флагов и кадром — они больше не касаются.
       * minmax(..., auto), а не жёсткая высота: если копи где-то перерастёт
       * токен (три строки лида, крупный системный шрифт), строка вырастет и
       * текст не ляжет на фото. Разъедутся — но это безопасный отказ.
       * Высота кадра auto: клампы из базового .pult нужны обычному потоку,
       * а внутри страницы кадр обязан добирать ровно остаток. */
      .deck { --deck-copy: clamp(300px, 46svh, 420px); }
      .deck__panel { grid-template-rows: minmax(var(--deck-copy), auto) minmax(0, 1fr); }
      .deck .pult { margin: 0; height: auto; }
    }
  }


  .langs { background: var(--paper); padding: 0 0 120px; }
  .langs .wrap { position: relative; z-index: 1; }
  .langs__title { font-size: clamp(1.8rem, 3.2vw, 2.8rem); }
  .langs__lead { margin-top: 14px; max-width: 46ch; color: var(--ink-soft); }
  .langs__list {
    margin-top: 34px; color: var(--ink-soft);
    font-size: clamp(1rem, 1.35vw, 1.22rem); line-height: 1.85;
    column-count: 1; letter-spacing: -.005em;
  }
  .langs__tail { margin-top: 28px; font-size: 13px; color: var(--ink-peach); }

  /* ─── СЦЕНА С ПУЛЬТОМ ───────────────────────────────────────────────────
   * Полоса во всю ширину и заведомо меньшей высоты, чем экран: 58svh с
   * зажимом, чтобы на низком ноутбуке она не съедала весь кадр, а на большом
   * мониторе не растягивалась в баннер.
   *
   * overflow: clip, а НЕ hidden — критерий 6.8. Здесь нужен клип по ОБЕИМ
   * осям (параллакс двигает картинку по вертикали), и `clip` это даёт, не
   * делая элемент скролл-контейнером. `hidden` уронил бы приёмку.
   *
   * Фон под картинкой — почти чёрный, в тон самому кадру: пока webp не
   * декодирован, полоса уже нужного цвета и на её месте не мигает бумага. */
  .pult {
    margin: 64px 0 0; padding: 0;
    position: relative; overflow: clip;
    height: clamp(300px, 58svh, 660px);
    background: #07070B;
  }
  /* ДЫМКА ПО БОКАМ — та же картинка, размытая и притушенная.
   * Нужна из-за геометрии: полоса во всю ширину и низкая, а кадр 16:9. При
   * object-fit: cover он масштабируется по ШИРИНЕ, и на полосе 1920x620
   * срезается треть высоты — ровно верх и низ планшета. Заказчик это и
   * увидел: «видно планшет частично».
   * Лечится одним: кадр надо вписывать по ВЫСОТЕ (contain), а тогда по бокам
   * остаётся пустое место. Заливать его плоским #07070B нельзя — вышла бы
   * видимая рамка вокруг фото. Поэтому бока продолжает сам кадр:
   * увеличенная, размытая и притушенная его копия. Края мастера и так почти
   * чёрные с боке, поэтому стык не читается.
   * Берётся версия 1024 (26 КБ): под blur(46px) разрешение не значит ничего,
   * а трафик значит. */
  .pult::before {
    content: ''; position: absolute; inset: -8%;
    background: url('../assets/pult_scene_1024.webp') center / cover no-repeat;
    filter: blur(46px) brightness(.42) saturate(.9);
  }
  /* Кадр вписан ЦЕЛИКОМ, и КОРОБКА КАРТИНКИ РАВНА САМОМУ КАДРУ.
   * Это принципиально, а не стилистика. При object-fit: contain коробка
   * осталась бы во всю ширину полосы, а кадр стоял бы внутри неё; тогда
   * маску не к чему привязать — она гасила бы пустые поля, а не края
   * снимка, и стык резкого кадра с дымкой читался бы вертикальной линией
   * (на первой пробе он был виден в левом углу, где у кадра оранжевый блик).
   * Здесь ширину задаёт aspect-ratio от высоты, поэтому границы элемента и
   * границы снимка совпадают, и маска гасит именно их.
   * Центрируется автополями, а НЕ translate: translate занят параллаксом
   * (@keyframes par), и центровка им же была бы затёрта на первом кадре.
   * max-width: none снимает потолок из нормализации (img { max-width: 100% }),
   * иначе на узком экране ширина упёрлась бы в полосу и кадр сплющило.
   * Запас хода 26 px против хода 22.5 px при --par: .25 (02-effect.css). */
  .pult__img {
    position: absolute; top: -26px; left: 0; right: 0; margin-inline: auto;
    width: auto; max-width: none; height: calc(100% + 52px);
    aspect-ratio: 16 / 9; object-fit: cover; display: block;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
            mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
  }
  /* Подпись лежит НА кадре: угол там почти чёрный и пустой, а отдельная
   * строка под полосой разрывала бы ритм секции. */
  .pult__cap {
    position: absolute; left: var(--pad); bottom: 20px; margin: 0;
    font-size: 13px; letter-spacing: .01em; color: rgb(255 253 248 / .66);
    text-shadow: 0 1px 12px rgb(0 0 0 / .8);
  }
  @media (max-width: 760px) {
    .pult { height: clamp(240px, 42svh, 420px); }
    .pult__cap { left: 20px; bottom: 14px; font-size: 12px; }
  }

  /* ─── ПОЛОСА УЖЕ СВОЕГО КАДРА: САДИМ ПО ШИРИНЕ ──────────────────────────
   * Правило выше сажает кадр по ВЫСОТЕ и растворяет боковые края. Это верно,
   * пока полоса шире 16:9. На телефоне она не шире: высоту полосе внутри
   * колоды задаёт грид панели (остаток после строки копи), и на 390x844
   * выходит примерно 390x456 — почти квадрат. Кадр, посаженный по высоте,
   * стал бы 810 px шириной и планшет срезало бы по бокам — та же болезнь,
   * от которой лечились, только по другой оси.
   *
   * Порог по ПРОПОРЦИЯМ ЭКРАНА, а не по ширине, и это не педантизм: высота
   * полосы считается от высоты экрана (46svh уходит под копи), поэтому
   * полоса переворачивается из горизонтальной в вертикальную примерно там
   * же, где сам экран, — расчётный переход на 0.96, берём 1:1.
   * Ось меняется целиком: посадка, центровка автополями и направление
   * маски. */
  @media (max-aspect-ratio: 1 / 1) {
    .pult__img {
      top: 0; bottom: 0; margin-block: auto;
      width: 100%; height: auto;
      -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 7%, #000 93%, transparent 100%);
              mask-image: linear-gradient(to bottom, transparent 0, #000 7%, #000 93%, transparent 100%);
    }
  }

  /* ─── БЕГУЩАЯ ЛЕНТА ФЛАГОВ (порт LogoLoop на ваниль) ────────────────────
   * Разметку строит js/langs.js, там же считается --dur по фактической
   * ширине трека, чтобы скорость держалась на 100 px/с при любом составе.
   *
   * Анимация — на композиторе, БЕЗ rAF: на странице уже идёт скраб видео,
   * и лента не имеет права отнимать тики главного потока.
   *
   * Зазор задан margin-right у элемента, а НЕ flex-gap. С gap между двумя
   * копиями появляется лишний зазор, и сдвиг на -50% попадает в его
   * середину — на стыке виден рывок. С margin обе копии равны по построению.
   *
   * Растушёвка краёв — mask-image (fadeOut у оригинала). Она честнее
   * градиента-накладки: работает на любом фоне и ничего не закрашивает. */
  .loop {
    --h: 45px; --gap: 60px; --dur: 60s;
    /* overflow-x: clip, а НЕ overflow: hidden — так требует критерий 6.8.
     * hidden сделал бы ленту скролл-контейнером, а на этой странице от
     * поведения прокрутки зависит движок плёнки. clip только обрезает.
     * Побочная польза: по вертикали остаётся visible, и увеличение элемента
     * под курсором (scale 1.14) не срезается сверху и снизу. */
    margin-top: 40px; overflow-x: clip;
    -webkit-mask-image: linear-gradient(to right,
      transparent 0, #000 7%, #000 93%, transparent 100%);
    mask-image: linear-gradient(to right,
      transparent 0, #000 7%, #000 93%, transparent 100%);
  }
  .loop__track {
    display: flex; width: max-content;
    animation: loop-run var(--dur) linear infinite;
  }
  /* hoverSpeed={0} у оригинала — то есть остановка, а не замедление. */
  .loop:hover .loop__track { animation-play-state: paused; }
  @keyframes loop-run { to { translate: -50% 0; } }

  .loop__item {
    display: flex; align-items: center; gap: 12px; flex: none;
    margin-right: var(--gap);
    font-size: 15px; color: var(--ink-soft); white-space: nowrap;
    transition: scale 260ms var(--ease-soft), color 260ms ease;
  }
  .loop__item:hover { scale: 1.14; color: var(--ink); }

  /* Один спрайт вместо 64 файлов: 8x8 ячеек по 60x45 CSS px (в файле 2x).
   * SVG-исходники весили 425 КБ, из них 324 — четыре герба, неразличимых на
   * этой высоте. WebP-спрайт весит 60.6 КБ и стоит один запрос. */
  .loop__flag {
    flex: none; width: 60px; height: var(--h); border-radius: 6px;
    background-image: url("../assets/flags.webp");
    background-size: 480px 360px;
    background-position: calc(var(--c) * -60px) calc(var(--r) * -45px);
    box-shadow: 0 1px 3px rgb(42 28 16 / .2);
  }

  @media (prefers-reduced-motion: reduce) {
    .loop__track { animation: none; }
    .loop__item { transition: none; }
    .loop__item:hover { scale: 1; }
  }

  .outro { position: relative; background: var(--paper); padding: 0 0 120px; }
  .deck__panel.outro { z-index: 1; }
  .outro__grid {
    display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
    gap: 40px; align-items: start; padding-top: 40px;
  }
  .outro__main { grid-column: 1; }
  /* Приписка переехала из правой колонки под почту: правая колонка целиком
   * отдана фото, а сама приписка относится к разговору, а не к адресу. */
  .outro__note {
    margin-top: 26px; max-width: 34ch;
    color: var(--ink-soft); font-size: .95rem; opacity: .8;
  }

  /* ─── КАРТОЧКА ОФИСА ───────────────────────────────────────────────────
   * Заняла место большого знака: тот дублировал логотип из подвала, а правая
   * треть секции пустовала. Фото закрывает пустоту и подтверждает адрес.
   *
   * ГЛУБИНА, А НЕ СВЕЧЕНИЕ — сознательный выбор. Светящаяся рамка на бумажном
   * фоне читается дёшево и, главное, забрала бы единственный акцент страницы
   * у кнопки «Обсудить мероприятие» (бирюза знака, см. поправку 18).
   * Работают три слоя: волосяное кольцо задаёт кромку, короткая тень сажает
   * карточку на бумагу, длинная даёт объём. Ни одного нового слоя
   * композитинга: всё это один box-shadow. */
  /* Без --par: параллакса внутри колоды нет по построению, см. правило
   * `.deck [data-par]` в 02-effect.css. Заводить переменную «на будущее»
   * здесь нельзя — она бы выглядела работающей настройкой, каковой не является. */
  .office { margin: 0; }   /* колонок в панели больше нет */
  .office__img {
    display: block; width: 100%; height: auto;
    aspect-ratio: 4 / 3; object-fit: cover; border-radius: 14px;
    box-shadow: 0 0 0 1px rgb(42 28 16 / .07),
                0 2px 4px rgb(42 28 16 / .05),
                0 22px 48px rgb(42 28 16 / .17);
  }
  .office__cap {
    margin-top: 14px; font-size: .95rem; color: var(--ink-soft);
  }
  .outro__h { font-size: clamp(1.8rem, 3.2vw, 2.8rem); }
  .outro__lead { margin-top: 14px; max-width: 40ch; color: var(--ink-soft); }
  .outro__tel {
    display: inline-block; margin-top: 30px; font-weight: 600;
    font-size: clamp(1.9rem, 3.4vw, 3.1rem); letter-spacing: -.03em;
  }
  .outro__mail { display: block; margin-top: 8px; font-size: 1.25rem; color: var(--ink-soft); }

  /* Юрстрока живёт ВНУТРИ страницы контактов: сайт обязан закончиться этой
   * панелью, отдельного экрана под подвал больше нет. */
  .legal { background: var(--paper); border-top: 1px solid rgb(42 28 16 / .12); padding: 26px 0 40px; }
  .deck .legal { background: none; padding: 22px 0 clamp(20px, 3svh, 34px); }
  .legal .wrap { display: flex; align-items: center; gap: 12px; font-size: 13.5px; color: var(--ink-soft); }

  /* Ссылки на политику, условия и поддержку. Прижаты вправо и переносятся
   * на свою строку на узком экране: в 350 px юрстрока и три ссылки одной
   * строкой не помещаются, а обрезать нельзя — магазины требуют, чтобы
   * ссылки были доступны. Цвет наследуется от .legal .wrap, поэтому
   * над фотографией в колоде они белеют вместе с подписью. */
  .legal .wrap { flex-wrap: wrap; }
  /* Ссылки стоят ОТДЕЛЬНОЙ строкой НАД юрстрокой (order: -1 + width: 100%),
   * а не справа от неё. Справа они не помещаются: на странице контактов
   * юрстрока делит горизонталь с подписью к фотографии, и три ссылки делали
   * её достаточно широкой, чтобы наехать на подпись. Своя строка растит
   * подвал вверх — он прижат align-self: end, — и нижняя строка остаётся
   * той же, на которую рассчитано смещение .office__cap. */
  .legal__nav { display: flex; flex-wrap: wrap; gap: 6px 18px; order: -1; width: 100%; margin-bottom: 10px; }
  .legal__nav a { color: inherit; text-underline-offset: 3px; opacity: .85; }
  .legal__nav a:hover { opacity: 1; text-decoration-thickness: 2px; }

  @media (min-width: 760px) { .langs__list { column-count: 2; column-gap: 48px; } }
  @media (min-width: 1100px) { .langs__list { column-count: 3; } }

  /* ─── ПОСЛЕДНЯЯ СТРАНИЦА: НОМЕР ВО ВЕСЬ ЭКРАН ───────────────────────────
   * Отзыв заказчика дословно: «тусклое, мелко фото, мёртвые шрифты, это не
   * наш стиль». Все четыре претензии — про ОДНО: на панели не было ни одного
   * перепада. Замер старой сборки на 1280x832: h2 40.96 px, телефон 43.52 px,
   * почта 20 px. Заголовок крупнее телефона в 1.06 раза — иерархии нет вовсе,
   * поэтому шрифты и читаются мёртвыми.
   *
   * Лечение — не украшения, а пересадка схемы соседней ленты фактов дословно:
   * там крошечная разрядка (b, 13px/500/.18em), под ней объект кеглем до
   * 8.5rem (em, 620/-.04em/.92), под ним тихая подпись (span, --ink-soft).
   * Здесь роль объекта играет ТЕЛЕФОН — целевое действие страницы.
   * Разрыв кеглей становится 13.4 -> 108.2 px (x8) при весах 500/450/620.
   */
  .deck__panel.outro {
    /* Тон 235 — лазурь, по просьбе заказчика («поменяй цвет с красного на
     * голубой»). Прежние 18 замыкали страницу на цвет факта «01» ленты
     * (там --start: 18); это кольцо сознательно разорвано — заказчик выбрал
     * холодный контакт вместо тёплого. Тон родня --cyan, знаку и стеклу
     * плашек, так что страница не осталась без опоры.
     *
     * СВЕТЛОТА У ЧИСЛА И НАДЗАГОЛОВКА РАЗНАЯ, и это не небрежность: на
     * бумаге #FFFDF8 голубой держит контраст хуже красного при той же
     * светлоте. Замерено на пикселях рендера (снимок swatch-полос):
     * 58%/.18 -> #0086D4, 3.85:1 — годится ЧИСЛУ (крупный текст, порог 3);
     * 49%/.16 -> #006AAC, 5.65:1 — надзаголовку 13-15 px (порог 4.5).
     * Подставлять 58 % в надзаголовок нельзя: там выйдет 3.85 при нужных
     * 4.5. Светлее 58 % не берём и числу: на 62%/.26 (плотность прежнего
     * красного) голубой даёт 2.68:1 и проваливает даже крупный порог. */
    --tel-h: 235;
    /* Левая линия подписей на кадре = левая кромка .wrap. Считается, а не
     * подбирается: на 1280 даёт ровно 144 px, на 1920 — 464 px, на телефоне
     * схлопывается в --pad. */
    --gut: max(var(--pad), calc(50vw - var(--wrap) / 2 + var(--pad)));
    --foot: clamp(14px, 2.4svh, 26px);
    /* Бумага снимается: под колодой всё это время лежит .grid--diag (клетка
     * 45 px с диагональной маской), и её не видно ни разу за весь сайт —
     * обе панели закрашены. На последней странице штора отдёргивается:
     * маска -20deg держит клетку в верхне-левом углу, ровно под копи, и
     * растворяет её к фотополосе. Конфликта с кадром нет по построению. */
    background: none;
  }

  /* Колонок в .wrap больше нет: фото уехало из него в отдельную полосу.
   * padding-top НЕ трогаем — верхнюю линию обеих страниц колоды задаёт
   * существующее правило .deck__panel > :first-child. */
  .deck__panel.outro .outro__grid { display: block; }

  /* Почта и приписка встают в одну строку на общей базовой линии: это
   * экономит ~34 px высоты и даёт под числом ровно одну тихую строку —
   * то же место, что занимает span под em в ленте фактов. */
  .deck__panel.outro .outro__main {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: clamp(24px, 4vw, 64px);
    align-items: baseline;
  }

  /* 1. НАДЗАГОЛОВОК — спецификация .reel__list b дословно. Регистр даёт CSS,
   *    текст в разметке остаётся прежним. */
  .deck__panel.outro .outro__h {
    grid-column: 1 / -1;
    font-size: clamp(13px, 1.05vw, 15px);
    font-weight: 500; line-height: 1.2; letter-spacing: .18em;
    text-transform: uppercase;
    /* Не 58%/.18, как у числа: на 13 px тот тон даёт 3.85:1 и не проходит
     * порог 4.5:1 для мелкого текста. 49%/.16 — тот же тон, но плотнее,
     * 5.65:1. Мелкому нужна плотность — та же логика, что у кегля. */
    color: oklch(49% .16 var(--tel-h));
  }

  /* 2. ЛИД */
  .deck__panel.outro .outro__lead {
    grid-column: 1 / -1; margin-top: 12px; max-width: 44ch;
    font-size: clamp(1.05rem, 1.35vw, 1.3rem);
    line-height: 1.35; letter-spacing: -.005em;
    color: var(--ink-soft);
  }

  /* 3. ТЕЛЕФОН — спецификация .reel__list em.
   * Потолок 7rem, а не 8.5rem как у фактов: мера .wrap — 992 px на любом
   * экране шире 1120, а строка «+998 97 722 20 60» при весе 620 занимает
   * 8.33em (замерено по hmtx самого файла шрифта, в браузере легло в 8.28em).
   * 8.5rem = 136 px дали бы 1133 px и вылет. При этом на 1280 факты ленты
   * рендерятся в 8.2vw = 105 px, а число — в 108 px: оно КРУПНЕЕ ленты.
   * Третий член min() — 13svh — держит число на низких окнах.
   * white-space: nowrap намеренно: при ошибке в метриках будет видимый
   * вылет, а не тихий перенос в две строки, который никто не заметит. */
  .deck__panel.outro .outro__tel {
    grid-column: 1 / -1;
    display: block; width: max-content; max-width: 100%;
    margin-top: clamp(18px, 3.4svh, 34px);
    margin-inline-start: -.035em;          /* оптический вынос плюса */
    font-size: min(10vw, 13svh, 7rem);
    font-weight: 620; line-height: .92; letter-spacing: -.04em;
    white-space: nowrap;
    color: oklch(58% .18 var(--tel-h));
    /* Подчёркивания НЕТ НИ В ОДНОМ состоянии, включая наведение. Раньше оно
     * приходило по ховеру, и именно его заказчик увидел на скриншоте как
     * «нижнюю линию»: под числом в 108 px черта идёт во всю ширину экрана.
     * Ответ на курсор остаётся подъёмом на 3 px, фокус-кольцо из
     * 00-base.css не тронуто — с клавиатуры ссылка по-прежнему видна. */
    text-decoration: none;
    transition: translate 260ms var(--ease-out-expo);
  }
  .deck__panel.outro .outro__tel:hover { translate: 0 -3px; }
  .deck__panel.outro .outro__tel:active { translate: 0 -1px; transition-duration: 90ms; }

  /* 4. ПОЧТА + ПРИПИСКА — одна тихая строка под числом. */
  .deck__panel.outro .outro__mail {
    grid-column: 1; margin-top: clamp(14px, 2.2svh, 26px);
    font-size: clamp(1.15rem, 1.6vw, 1.5rem);
    font-weight: 500; line-height: 1.3; letter-spacing: -.01em;
    color: var(--ink);
    /* Подчёркивание снято по просьбе заказчика вместе с линией под числом.
     * Ссылочность держится иначе: сам адрес — уже знак ссылки, а по
     * наведению строка уходит в тот же голубой, что и число. Это ответ на
     * курсор без единой лежачей черты на странице. */
    text-decoration: none;
    transition: color 180ms var(--ease-soft);
  }
  .deck__panel.outro .outro__mail:hover { color: oklch(49% .16 var(--tel-h)); }

  /* opacity: .8 СНЯТА намеренно: полупрозрачный текст и есть половина
   * ощущения «тускло». Вместо неё честный тон --ink-soft (15:1) — тот же,
   * которым набрана подпись span в ленте фактов. */
  .deck__panel.outro .outro__note {
    grid-column: 2; margin-top: 0; max-width: 40ch; opacity: 1;
    font-size: clamp(12.5px, .95vw, 14px); line-height: 1.5;
    color: var(--ink-soft);
  }

  /* 5. ФОТОПОЛОСА — ПОЛ СТРАНИЦЫ, А НЕ КАРТОЧКА.
   * Заказчик сказал «мелко фото»: было 461x346 в колонке 36vw. Стало
   * 1280x475 во всю ширину — площадь x3.8. Ни скругления, ни тени: это
   * прямая рифма к полосе с пультом, которой заканчивается ЛЕВАЯ страница
   * колоды. Две страницы становятся близнецами — копи на бумаге сверху,
   * тёмная полоса снизу, в ней мелкая светлая подпись.
   * Высота здесь фиксированная — это БАЗА (нет scroll-driven анимаций,
   * панель не 100svh и растягивать нечего); в @supports ниже она станет
   * auto и полоса заберёт всю высоту, не занятую копи.
   * grid-column: 1 обязателен: базовое .office { grid-column: 2 } завело бы
   * второй, несуществующий столбец панели. */
  .deck__panel.outro .office {
    grid-row: 2; grid-column: 1; margin: 0;
    position: relative; overflow: clip;      /* clip, не hidden — критерий 6.8 */
    height: clamp(240px, 46svh, 460px);
    background: #141210;                     /* до декода webp не мигает бумага */
  }
  .deck__panel.outro .office__img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    aspect-ratio: auto; object-fit: cover;
    /* 50% 38%: при полосе 1280x475 в кадр попадают строки исходника 171..617
     * из 896 — решётка стеклянного навеса и вывеска «trilliant» (она на
     * y ~ 360). Ниже — асфальт, выше — пустое небо. */
    object-position: 50% 38%;
    border-radius: 0; box-shadow: none;
  }
  /* Вуаль ровно под строку подписей, а не на пол-кадра: затемнять половину
   * фотографии, которую просили сделать КРУПНЕЕ, — значит отвечать на
   * претензию её же причиной. Нижний предел 64 px — на телефоне лёжа полоса
   * ужимается до 112 px, и процент перестал бы накрывать строку. */
  .deck__panel.outro .office::after {
    content: ''; position: absolute; inset: auto 0 0 0; z-index: 1;
    height: clamp(64px, 38%, 150px); pointer-events: none;
    background: linear-gradient(to top,
      rgb(10 8 6 / .82) 0%, rgb(10 8 6 / .50) 38%,
      rgb(10 8 6 / .16) 70%, rgb(10 8 6 / 0) 100%);
  }
  /* Подпись лежит НА кадре — тот же приём, что у .pult__cap на соседней
   * странице. Отдельная строка под полосой съела бы 34 px высоты и разорвала
   * бы ритм. Смещение снизу считается так, чтобы подпись встала по центру
   * плитки логотипа в юрстроке (28x29) и обе строки читались одной линией. */
  .deck__panel.outro .office__cap {
    position: absolute; z-index: 2; left: 0; right: 0;
    bottom: calc(var(--foot) + (29px - 1.5em) / 2);
    margin: 0; padding-inline: var(--gut);
    font-size: 13px; line-height: 1.5; letter-spacing: .01em;
    color: rgb(255 253 248 / .82); text-shadow: 0 1px 14px rgb(0 0 0 / .75);
  }

  @media (max-width: 760px) {
    .deck__panel.outro .outro__main { display: block; }
    .deck__panel.outro .outro__note { margin-top: 14px; max-width: 46ch; }
    /* Полоса упирается в высоту, кроп идёт по бокам: видны столбцы
     * исходника 225..975 из 1200 — навес и вывеска остаются в кадре. */
    .deck__panel.outro .office__img { object-position: 50% 50%; }
    .deck__panel.outro .office__cap { font-size: 12px; }
  }

  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      /* Панель = ровно один экран. Строка 1 — копи по содержимому, строка 2 —
       * фотополоса, которая забирает ВСЮ оставшуюся высоту. Переполниться
       * по построению не может: minmax(0, 1fr) сжимается до нуля, если копи
       * вдруг вырастет (третья строка лида, крупный системный шрифт).
       * padding панели НЕ трогаем: .deck__panel > :first-child уже вешает
       * padding-top: var(--deck-top) на .wrap, и второй отступ на самой
       * панели сложился бы с ним — обе страницы разъехались бы по верхней
       * линии ровно на --deck-top. Именно на этом сгорело исходное
       * предложение: оно поднимало токен вдвое и роняло страницу языков
       * на кадр пульта. */
      /* Строки задаёт общее правило .deck__panel выше: обе страницы обязаны
       * держать полосы на одной линии, поэтому своей раскладки у контактов
       * здесь больше нет. */
      .deck__panel.outro .office { height: auto; }

      /* Юрстрока ложится на низ кадра, в ту же ячейку, что фотополоса:
       * подпись слева, знак и юрстрока справа, обе на одной линии.
       * align-self: end означает, что при схлопывании строки в ноль подвал
       * уйдёт ВВЕРХ, внутрь панели, а не вывалится за её нижнюю кромку.
       * Волосяная линия border-top снимается: поперёк фотографии она
       * читалась бы как царапина.
       * Правила стоят внутри @supports намеренно: в отказной ветке подвал
       * остаётся под полосой на бумаге, и белый текст был бы невидим. */
      .deck__panel.outro .legal {
        grid-row: 2; grid-column: 1; z-index: 2;
        position: relative; align-self: end;
        background: none; border-top: 0; padding: 0 0 var(--foot);
      }
      .deck__panel.outro .legal .wrap {
        max-width: none; padding-inline: var(--gut);
        justify-content: flex-end;
        font-size: 13px; line-height: 1.5; letter-spacing: .01em;
        color: rgb(255 253 248 / .82); text-shadow: 0 1px 14px rgb(0 0 0 / .75);
      }
      @media (max-width: 760px) {
        /* На телефоне подпись и юрстрока перестают быть одной строкой и
         * встают в две: 191 + 266 px в 350 не помещаются. */
        .deck__panel.outro .legal .wrap { justify-content: flex-start; font-size: 12px; }
        /* Ссылки ужимаются, чтобы три штуки гарантированно легли в одну
         * строку даже на 320 px: перенос на вторую строку сдвинул бы всё
         * выше и снова уронил подпись под фото. */
        .deck__panel.outro .legal__nav { font-size: 11px; gap: 4px 13px; margin-bottom: 8px; }
        /* 29px — плитка логотипа, 10px — воздух, 34px — строка ссылок
         * вместе с её отступом. Подпись встаёт над всем этим. */
        .deck__panel.outro .office__cap { bottom: calc(var(--foot) + 29px + 10px + 34px); }
      }
    }
  }
}
