/* =========================================================================
   TMN Talents — композиция посадочника

   Поверх кита Frame & Field (css/system.css) и палитры (css/palette.css);
   кит не трогаем. Здесь только то, чего в ките нет: фулскрин-полотно с
   автоскейлом, раскладка секций, полноэкранный герой с шейдерным фоном,
   плитка ростера с инициалами вместо портрета и появление блоков.

   Цвет приходит из palette.css: страница переехала на тёмное полотно и
   многоцветную палитру с референса, поэтому от кита здесь остались
   типографика, ритм и геометрия компонентов — не цвет.

   Правила кита, которые держатся сознательно:
   — заголовки с точкой на конце, набраны Anton;
   — вертикальный ритм секций и волосяные границы вместо теней;
   — левое выравнивание везде, кроме героя и плиты призыва.

   Правила, от которых пришлось отказаться:
   — «один акцент на страницу» — референс многоцветный, круг акцентов
     раздаёт .accent-cycle;
   — «блок не касается края окна» — герой полноэкранный, как на образце,
     и плиты в нём больше нет.
   ========================================================================= */
/* Кит тянет Inter весами 400/500/600 — жирного там нет, а кнопке героя он
   нужен настоящий: синтетический bold браузер размазывает. Добираем один
   недостающий вес отдельным запросом, не трогая system.css.
   @import обязан стоять до всех правил — комментарии выше ему не мешают.

   Второй потребитель у этого веса был — счётчик шторки, — но он переехал
   на кегль и вес пунктов меню. Запрос остаётся ради .hero__cta. */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@700&display=swap");


/* ============================================================
   Фулскрин и автоскейл
   Тот же приём, что в посадочнике Frogge: контейнер тянется от края до
   края (max-width снят), а корневой кегль начинает расти с ширины окна
   от 1440px. Все размеры кита переписаны из пикселей в rem — на 1440
   они совпадают с исходными до пикселя, дальше едут за корнем
   пропорционально: отступы, радиусы, кегли, высоты кнопок.

   Почему именно так, а не «просто увеличить шрифты»: кит собран в
   абсолютных пикселях, и на мониторе 2560 страница иначе превращается в
   узкую колонку посреди бумаги — ровно то, от чего editorial-система и
   должна спасать.
   ============================================================ */
:root {
  /* Полотно */
  --container-max: none;
  --container-tight: 60rem; /* 960 — на бейзлайне то же самое */
  --space-gutter: clamp(20px, 4vw, 3.5rem);

  /* Кегли. Средний член в vw оставлен от кита — он и держит масштаб
     ниже 1440; потолок в rem подхватывает эстафету выше. */
  --fs-display-xxl: clamp(4rem, 12vw, 10rem); /* 64 / 160 */
  --fs-display-xl: clamp(3.5rem, 9vw, 7rem); /* 56 / 112 */
  --fs-headline-lg: clamp(3rem, 7vw, 5.5rem); /* 48 / 88 */
  --fs-headline-md: clamp(2rem, 4.4vw, 3.5rem); /* 32 / 56 */
  --fs-headline-sm: clamp(1.375rem, 2.4vw, 2rem); /* 22 / 32 */
  --fs-body-lg: 1.0625rem; /* 17 */
  --fs-body-md: 0.9375rem; /* 15 */
  --fs-body-sm: 0.8125rem; /* 13 */
  --fs-label-sm: 0.75rem; /* 12 */
  --fs-stat: clamp(3.5rem, 7vw, 5.5rem); /* 56 / 88 */
  --fs-meta: 0.75rem; /* 12 */
  /* Кегль блока «Кто мы»: заявление и продолжение под роликом (у второго
     0.75 от этого числа). Метка блока сюда больше не смотрит — она набрана
     пунктом меню.

     У образца заявление набрано скромно, около 1.55vw, но здесь оно вырасло
     до 1.95vw — примерно 37px на окне 1920 против прежних 30.

     Выше не пускает не вкус, а колонка. Заявление занимает правую половину
     сетки, на 1920 это около 860px чистых, и самая длинная строка при 37px
     съедает от неё уже девять десятых. Прежние 30px были у того же потолка:
     чтобы кегль сдвинулся вообще, пришлось перебить вторую строку надвое в
     разметке. Растить дальше можно только вместе с колонкой. */
  --fs-about: clamp(1.25rem, 1.95vw, 2.4rem); /* 20 / 37 на 1920 */

  /* Ритм */
  --space-xxs: 0.25rem;
  --space-xs: 0.5rem;
  --space-sm: 0.75rem;
  --space-md: 1.25rem;
  --space-lg: 2rem;
  --space-xl: 3.5rem;
  --space-xxl: 6rem;
  --space-section: 8rem;

  /* Скругления. --radius-full остаётся в 999px: пилюля есть пилюля. */
  --radius-sm: 0.375rem;
  --radius-md: 0.75rem;
  --radius-lg: 1.125rem;
  --radius-xl: 1.5rem;

  /* Тень плиты тоже в rem, иначе на широком экране она бы «худела». */
  --shadow-plate: 0 1.5rem 3.75rem -2rem rgba(17, 17, 17, 0.35);

  /* Шапка. Высота ряда равна высоте пилюли — она в нём самый высокий
     элемент. Из этих же величин считается высота героя, поэтому
     .nav ниже берёт отступ отсюда, а не из кита: разъехаться нельзя. */
  --btn-h: 2.75rem; /* 44 — высота кнопки из кита */
  --nav-h: var(--btn-h);
  --nav-pad-y: var(--space-lg);
  --nav-block: calc(var(--nav-h) + 2 * var(--nav-pad-y));

  /* ---- Зерно (СЕЙЧАС НЕ ПОДКЛЮЧЕНО) ----------------------------------
     Ни одно полотно шум больше не берёт: чёрный на странице чистый, а зерно
     поднимало его примерно до #060606, то есть ровно тем и мешало, ради
     чего чёрный делали нулевым.

     Токены оставлены рабочими, а не вычищены: тайлы ничего не грузят и
     ничего не стоят, пока на них никто не ссылается, — это две строки в
     :root. Вернуть зерно куда-либо — дописать туда пару

         background-image: var(--grain-tile);
         background-size: var(--grain-size) var(--grain-size);

     Было оно на восьми полотнах: полосы шторки, мобильное меню, страница,
     герой (псевдоэлементом вуали), блок услуг, панель карты, призыв и
     подвал. Светлым четырём доставался --grain-tile-ink, тёмным —
     --grain-tile. Подробности и почему тайла два — в «Зерне» в README.

     Рисует его feTurbulence, вшитый в CSS как data-URI: никакой картинки на
     диске, ничего не грузится — на file:// это важнее всего.

     Матрица делает из шума не серую пелену, а редкие светлые крупинки:
     альфа = 2R − 1, то есть половина пикселей отсекается в ноль, и видно
     только гребни. Сила — opacity у прямоугольника внутри URI, сейчас 10%;
     это единственное число, которое там имеет смысл трогать.

     --grain-size — крупность на экране. Турбулентность считается в системе
     координат самого SVG (тайл 120 единиц, частота 0.6, крупинка ~1.67
     единицы), а на экран тайл раскладывается на заданный размер: 150 / 120
     * 1.67 ≈ 2px на крупинку. Больше значение — крупнее зерно. */
  --grain-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='g' x='0' y='0' width='120' height='120' filterUnits='userSpaceOnUse'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.6' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 2 0 0 0 -1'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23g)' opacity='0.1'/%3E%3C/svg%3E");
  /* Тот же шум, но крупа чёрная — для светлых полотен: кораллового блока
     услуг и оранжевого призыва. Раньше их оставляли без зерна, потому что
     белая крупа на светлом не видна, а второй тайл заводить «ради одного
     блока» не хотелось. Блоков оказалось не один, а вместе они держат
     заметную часть прокрутки, и без фактуры страница разваливалась на
     зернистые куски и гладкие.

     Отличие от тайла выше ровно в одной строке матрицы: три первых ряда
     дают не единицу, а ноль, то есть чёрный вместо белого. Альфа считается
     так же (2R − 1), поэтому рисунок крупы совпадает — это то же поле шума,
     перекрашенное, а не второе, живущее своей жизнью.

     Сила меньше — 0.07 против 0.1. На равной силе светлый блок выглядел бы
     грязным рядом с полотном страницы, а вдвое слабее (0.05) крупа уходила
     за порог заметности: размах по каналу выходил 8 единиц из 255 против 17
     у белой крупы на чёрном. Это ЕДИНСТВЕННОЕ число здесь, которое стоит
     подбирать глазами, — остальная механика тайла общая с белым. */
  --grain-tile-ink: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='g' x='0' y='0' width='120' height='120' filterUnits='userSpaceOnUse'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.6' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 -1'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23g)' opacity='0.07'/%3E%3C/svg%3E");
  --grain-size: 150px;
}

/* Ниже 1440 корень обычный, 16px: размеры кита там и рассчитаны.
   Выше — растёт линейно с шириной. Кривая взята из посадочника Frogge,
   чтобы два сайта на соседних вкладках не выглядели разного кегля. */
@media (min-width: 1440px) {
  html {
    font-size: clamp(16px, 1.111vw, 32px);
  }
}

/* Выше 1920 наклон роста уменьшен — иначе на 2560 страница раздувается. */
@media (min-width: 1920px) {
  html {
    font-size: clamp(21.33px, calc(0.4445vw + 12.8px), 27.2px);
  }
}

/* --------------------------------------------------------
   Компоненты кита, у которых размеры зашиты в правилах, а не
   в токенах. Значения те же самые, просто в rem — иначе кнопки,
   табы и подписи остались бы островками фиксированного размера
   посреди отмасштабированной страницы.
   Кит при этом не трогаем: правила живут здесь и перекрывают его
   порядком подключения.
   -------------------------------------------------------- */
.btn {
  gap: 0.5rem;
  height: var(--btn-h);
  padding-inline: 1.375rem;
  font-size: 0.75rem;
}

.btn--ghost {
  height: auto;
  padding: 0.375rem 0.625rem;
}

.btn__icon {
  width: 1rem;
  height: 1rem;
}

.pill {
  gap: 0.5rem;
  height: 2.5rem;
  padding-inline: 1rem;
  font-size: 0.75rem;
}

.nav {
  padding-block: var(--nav-pad-y);
  min-height: var(--nav-block);
}

.nav__link {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.tabs {
  gap: 0.375rem;
  padding: 0.25rem;
}

.tabs__tab {
  height: 2rem;
  padding-inline: 0.875rem;
  font-size: 0.75rem;
}

/* Углы у медиа прямые. Кит скругляет рамку кадра на --radius-lg, но на этой
   странице кадры стоят квадратными: и плитки кампаний, и плитки ростера, и
   широкий кадр в блоке «Кто мы» — одно правило на все три места.
   overflow: hidden у рамки оставлен: скруглять больше нечего, но обрезать
   содержимое по коробке он продолжает. */
.card-frame {
  border-radius: 0;
}

/* Слои страницы, снизу вверх:

     дорожка героя и .page-body — без чисел, порядком разметки
     1   — вертикальная сетка страницы (внутри .page-body)
     100 — шторка загрузки
     120 — медиа: рамки кадров и ролик

   Верхних этажей больше нет: их занимали мобильная панель меню (130) и
   гамбургер (135), оба сняты вместе со всем мобильным меню — см. запрос
   max-width: 720px ниже по файлу.

   Сетки в этом списке одной строкой нет намеренно: она разнесена на три
   экземпляра по областям, и каждый решает порядок внутри своей. Здесь стоит
   только тот, что лежит в .page-body; экземпляры героя и шторки живут в их
   собственных контекстах наложения и снаружи ни с чем не сравниваются.
   Подробнее — в секции «Вертикальная сетка».

   Медиа подняты над сеткой намеренно: волосяная линия, проходящая сквозь
   фотографию, читается не разметкой полотна, а грязью на кадре. Над всем
   остальным набором линии по-прежнему идут.

   Слой .page-body этому не мешает, хотя раньше здесь стояло обратное. И
   сетка, и медиа лежат внутри неё, а контекст наложения меняет только то,
   как сама страница спорит с соседями снаружи. Подробнее — в комментарии
   к .page-body.

   position: relative у рамки уже есть от кита, у ролика ставим сами: без
   позиционирования z-index не действует. */
.card-frame,
.about__clip {
  position: relative;
  z-index: 120;
}

/* ============================================================
   Неоновый след за курсором (js/neon-cursor.js)
   Холст во весь экран, поверх страницы. Ниже кадров (120),
   шторки (100) и мобильного меню (130): лента — добавка к
   странице, а не слой над ней.

   screen на почти чёрном полотне работает сложением, то есть
   светится; поверх светлого кадра тот же режим сам собой
   сводит ленту в ничто — и подписи под кадрами она не мажет.

   События проходят насквозь: курсор слушает документ, а холст
   иначе перехватывал бы наведение на всю страницу разом.
   ============================================================ */
.neon-cursor {
  position: fixed;
  inset: 0;
  z-index: 99;
  pointer-events: none;
  mix-blend-mode: screen;
}

/* ============================================================
   Курсор-кружок (js/blob-cursor.js)
   Пришёл на место ленты — она отцеплена от index.html, стили
   выше оставлены рабочими на случай возврата.

   Холст размером с сам кружок, а не во весь экран: слой с
   difference заставляет пересчитывать наложение всюду, где под
   ним что-то шевелится, а под ним постоянно крутится шейдер
   героя. Ездит трансформом, размеры ставит скрипт.

   z-index 150 — выше всего: сейчас это кадры (120), а раньше над
   ними стояли ещё мобильное меню (130) и гамбургер (140). Курсор
   обязан быть поверх всего; лежи он ниже кадров, как лежала
   лента, — пропадал бы ровно над фотографиями, где difference и
   вывозит. Число оставлено с запасом: снижать его вслед за
   снятым меню незачем, выше курсора всё равно ничего нет.

   difference не красит собой, а выворачивает то, что под ним:
   на почти чёрном полотне кружок светится мелом, поверх
   светлого кадра сам собой уходит в чёрный. Подбирать цвет под
   каждую секцию не нужно.

   Прозрачность ведёт скрипт классом is-live: до первого
   движения мыши кружка нет, ушёл курсор из окна — гаснет.

   События проходят насквозь: курсор слушает документ, а холст
   иначе перехватывал бы наведение на всё под собой.
   ============================================================ */
.blob-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 150;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
  mix-blend-mode: difference;
}

.blob-cursor.is-live {
  opacity: 1;
}

.project-tile {
  gap: 0.875rem;
}

.project-tile__caption {
  font-size: 0.875rem;
}

.stat {
  gap: 0.75rem;
}

.stat__label {
  font-size: 0.75rem;
}

.plate__inner {
  padding: 3.5rem 3rem 4rem;
}

.footer__link,
.footer__copy {
  font-size: 0.8125rem;
}

.icon {
  width: 1.125rem;
  height: 1.125rem;
}

.icon-sm {
  width: 0.875rem;
  height: 0.875rem;
}

.icon-lg {
  width: 1.5rem;
  height: 1.5rem;
}

/* ============================================================
   Полотно
   ============================================================ */
/* Полосы прокрутки нет.

   Это не украшательство, а починка: пока висит шторка, на <html> стоит
   overflow: hidden, полосы нет и окно шире; шторка уходит — полоса
   появляется, окно сужается на её ширину, и вся страница пересчитывается.
   Заметнее всего по вертикальной сетке: линии стоят на четвертях окна и на
   этом кадре прыгают вбок. То же самое происходило и при открытии мобильной
   панели, у неё тот же overflow: hidden.

   Резервировать место (scrollbar-gutter: stable) не годится: полоса тогда
   остаётся видимой, а полотно теряет её ширину справа — на фулскрин-герое и
   на полноширинных панелях услуг это читается как поле, которого в макете
   нет.

   Две записи, потому что механизма два и общего у них нет: scrollbar-width
   — стандартное свойство (Firefox, свежие Safari), ::-webkit-scrollbar —
   всё на WebKit и Blink. Прокрутка при этом никуда не девается: колесо,
   клавиатура и тач работают как работали, пропадает только сама полоса. */
html {
  scroll-behavior: smooth;
  scrollbar-width: none;
}

html::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* Якорь не должен упирать секцию в самый верх окна. */
section[id] {
  scroll-margin-top: var(--space-lg);
}

/* ============================================================
   Знак
   ============================================================ */
/* Знак — вытянутый вордмарк 2037x238, поэтому меряем его высотой, а не
   шириной: иначе на узком экране он съедает весь ряд.

   В файле знак залит чистым белым, поэтому выворачивать его нечем и не
   надо — прежняя инверсия (файл был чёрный) снята. Осталось только
   brightness: чистый #FFF светил бы ярче соседних ссылок, а так знак
   садится на уровень набора. */
.brand__logo {
  height: 1.375rem;
  width: auto;
  filter: brightness(0.96);
}

/* ============================================================
   Лоадер
   Полноэкранная шторка: квадратное окошко с кадрами по центру, счётчик
   снизу. Знака нет — страница здоровается кадром, а не вордмарком.

   Маска здесь буквальная. Шторка — не сплошное полотно, а рамка из
   четырёх чёрных полос вокруг квадратной дырки. Страница уже стоит под
   ней, поэтому в дырке всё время лежит настоящий фон героя, а квадрат
   показывает его центральный кусочек. На выходе полосы расходятся, дырка
   растёт до краёв, и фон героя открывается целиком.

   Отсюда два свойства, которых не было у прежних попыток:
   — увеличивать нечего. Открывается сам герой в своём размере, а не копия
     картинки, растянутая на весь экран;
   — стыка нет. Раскрытие заканчивается ровно на живом герое, а не на
     статичном кадре, который пришлось бы в него перекрещивать.

   Кадры загрузки лежат в дырке сверху и гаснут перед раскрытием. В окошке
   они уменьшены (560px исходника на сотню с небольшим пикселей стороны), а
   не растянуты. Сторону задаёт --loader-window ниже.

   Фонового видео на полотне шторки больше нет: какое-то время поверх полос
   играл video/loader.webm с вырезом под окошко, теперь полотно снова
   чистое чёрное. Сам файл на диске оставлен.

   Показывается только под классом is-loading на <html>. Класс ставит
   синхронный скрипт в <head>, снимает js/loader.js. Отсюда же следует
   поведение без скриптов: класса не будет — не будет и шторки.
   ============================================================ */
.loader {
  display: none;
  /* Сторона окошка. Кадр в нём — метка, а не картинка: место держит
     полоса загрузки, идущая через весь экран.

     Множитель 0.75 — просьба уменьшить кадры на четверть. Стоит отдельным
     сомножителем, а не вписан в три числа внутри min(): те подобраны под
     три ограничения разом (высота окна, ширина окна, потолок в rem), и
     пересчитывать каждое значило бы потерять, откуда они взялись. */
  --loader-window: calc(min(14vh, 19vw, 8rem) * 0.75);
  /* Толщина полосы загрузки — та же волосяная линия, что у сетки. */
  --loader-line: 1px;
  /* Мерка ряда на средней линии: кегль цифр. Значение — 0.8125rem, ровно как
     у .nav__link: счётчик набран пунктом меню.

     От неё же считается коробка звезды напротив, но НЕ приравниванием — см.
     --loader-star ниже. */
  --loader-mark: 0.8125rem;

  /* Коробка звезды. Считается от целевой высоты САМОГО ШТРИХА, а не
     приравнивается к кеглю цифр: в этом и была ошибка первой версии. Коробка
     у звезды 320x320, а штрих в ней 292x252 и стоит по центру (поля 14/14 по
     горизонтали и 34/34 по вертикали — замерено по кривым, а не по
     контрольным точкам). Равные коробки давали видимый штрих 10.2px против
     капители цифр 9.5px — формально даже крупнее, а читалось мельче.

     Читалось мельче потому, что звезда РАЗРЕЖЕНА. Заливки в ней 25% коробки
     и 35% собственного габарита: это лучевая форма, у которой между концами
     лучей почти всё пусто, тогда как цифра — плотный штрих почти во весь
     свой габарит. Габаритом такие метки не равняют.

     Отсюда формула. 1.1 — целевая высота штриха в кеглях цифр, надбавка на
     разреженность; 0.7875 — доля штриха в коробке по высоте (252 из 320).
     На 13px это коробка 18.2px и видимый штрих 14.3px, то есть в полтора
     раза выше капители цифр. Крутить, если понадобится, надо ПЕРВОЕ число:
     второе — свойство файла, и меняется оно только вместе со звездой. */
  --loader-star: calc(var(--loader-mark) * 1.1 / 0.7875);
  /* Крупность зерна на полотне шторки. Зерна на шторке сейчас нет, и
     переменную никто не читает — держим её вместе с самим зерном, на случай
     возврата: это она стояла в background-size у .loader__bar. Сдвиги в
     @keyframes loader-grain заданы числами и от неё не зависят, там она
     упомянута только в пояснении. Само число — в --grain-size у корня. */
  --loader-grain: var(--grain-size);
  /* Толщина полос: от кромки экрана до кромки окошка. Проценты каждая
     сторона берёт по своей оси, поэтому одно выражение даёт квадрат на
     любом экране. */
  --loader-inset: calc(50% - var(--loader-window) / 2);
  --loader-open: 700ms cubic-bezier(0.76, 0, 0.24, 1) 120ms;
}

html.is-loading {
  overflow: hidden;
}

/* Фона у шторки нет: чёрное дают полосы, всё остальное — дырка. */
html.is-loading .loader {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 100;
}

.loader__mask {
  position: absolute;
  inset: 0;
}

/* Чёрное полотно шторки. Зерна на нём больше нет — вместе со всей страницей,
   см. «Зерно» в README. Ушло и то, что его обслуживало: привязка фона к окну
   (нужна была, чтобы четыре полосы тайлили шум от одного угла) и шевеление
   через @keyframes loader-grain. Сами @keyframes оставлены ниже нетронутыми:
   вернуть — вернуть сюда три строки. */
.loader__bar {
  position: absolute;
  background-color: var(--c-void);
}

/* СЕЙЧАС НЕ ПОДКЛЮЧЕНО: зерна на шторке нет, и запускать эту анимацию
   некому — ссылка на неё снята с .loader__bar выше. Оставлено целиком, как
   и тайлы: вернуть зерно на шторку значит вернуть туда три строки.

   Зерно живёт, а не лежит: поле шума на каждом шаге прыгает на новое место,
   и крупинки пересыпаются, как в киноплёнке.

   Шагов восемь на 720мс — примерно одиннадцать кадров в секунду. Плавно
   двигать шум нельзя: он бы поплыл цельным полотном, и вместо зерна вышел
   бы ползущий узор. Отсюда steps(1, end): значение держится всю долю и
   меняется скачком.

   Сдвиги заданы в пикселях и нарочно не кратны тайлу (--loader-grain):
   попади шаг в кратность — поле встало бы на прежнее место и кадр
   повторился бы. Полосы при этом остаются в такт: фон у них привязан к
   окну, а не к своим коробкам, поэтому позиция у всех четырёх одна. */
@keyframes loader-grain {
  0% {
    background-position: 0 0;
  }
  12.5% {
    background-position: -47px 23px;
  }
  25% {
    background-position: 31px -59px;
  }
  37.5% {
    background-position: -83px -17px;
  }
  50% {
    background-position: 67px 41px;
  }
  62.5% {
    background-position: -29px 71px;
  }
  75% {
    background-position: 53px -37px;
  }
  87.5% {
    background-position: -71px -83px;
  }
}

.loader__bar--top,
.loader__bar--bottom {
  left: 0;
  right: 0;
  height: var(--loader-inset);
  transition: height var(--loader-open);
}

.loader__bar--top {
  top: 0;
}
.loader__bar--bottom {
  bottom: 0;
}

/* Боковым полосам мало ужаться по ширине: пока верхняя и нижняя уезжают,
   их собственные кромки должны ехать вместе с ними, иначе в углах раньше
   времени открылись бы просветы. */
.loader__bar--left,
.loader__bar--right {
  top: var(--loader-inset);
  bottom: var(--loader-inset);
  width: var(--loader-inset);
  transition:
    width var(--loader-open),
    top var(--loader-open),
    bottom var(--loader-open);
}

.loader__bar--left {
  left: 0;
}
.loader__bar--right {
  right: 0;
}

.loader.is-out .loader__bar--top,
.loader.is-out .loader__bar--bottom {
  height: 0;
}

.loader.is-out .loader__bar--left,
.loader.is-out .loader__bar--right {
  width: 0;
  top: 0;
  bottom: 0;
}

/* Слои внутри шторки. Раньше порядок держался разметкой — полосы, сетка,
   окошко, полоса загрузки, счётчик, — и по правилам покраски позиционированные
   соседи без чисел ложатся в этом же порядке. Держалось, но не для всех пар:
   полоса загрузки идёт в разметке ПОСЛЕ окошка и потому проходила прямо по
   кадру, стоило заливке дойти до середины экрана.

   Теперь порядок задан числами и разметке больше не подчиняется:

     полосы шторки   — без числа, самый низ
     полоса загрузки — без числа, но выше полос по разметке
     сетка           — 1
     окошко с кадрами— 2
     счётчик         — 3

   Линии сетки при этом ушли под кадр, а не наоборот: волосок сквозь
   фотографию читается грязью — ровно та же причина, по которой на самой
   странице медиа подняты над сеткой на 120. */
.loader .grid-lines {
  z-index: 1;
}

/* Окошко с кадрами — ровно та же квадратная область, что и дырка. */
.loader__frame {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  width: var(--loader-window);
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  transition: opacity 220ms ease;
}

.loader.is-out .loader__frame {
  opacity: 0;
}

/* Все десять лежат стопкой, видимый помечен классом. Подменять src у одной
   картинки нельзя: неподгруженный кадр давал бы просвет. */
.loader__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

.loader__shot.is-on {
  opacity: 1;
}

/* Полоса загрузки идёт по центральной вертикальной линии страницы — по той
   самой, что рисует .grid-lines. Совпадение точное и считать нечего: у
   линий контейнер отбит маржином симметрично, поэтому их середина
   приходится ровно на середину окна, а 50% полноэкранной шторки — то же
   место.

   Идёт до самого низа, сквозь счётчик. Раньше обрывалась над ним (bottom:
   4rem) — тот стоит на той же оси, и линия шла бы прямо через цифры. Пока
   счётчик был крупным, зазор перестал спасать: цифры выросли выше него и
   линия заходила в них на четыре десятка пикселей. Развели слоями, а не
   зазором — счётчик идёт в разметке следом и красится поверх, поэтому линия
   уходит ему за спину и выныривает под ним.

   Счётчик с тех пор снова уменьшился до кегля пунктов меню, и зазором его
   опять можно было бы обойти. Не возвращаем: слоями надёжнее — они не
   зависят от кегля, и следующая правка размера ничего не сломает. */
.loader__track {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: var(--loader-line);
  transition: opacity 180ms ease;
}

.loader.is-out .loader__track {
  opacity: 0;
}

/* Высоту двигает js/loader.js — тем же значением, что и цифры, поэтому
   полоса и счётчик всегда говорят одно и то же. Перехода здесь нет
   намеренно: значение меняется каждый кадр, и переход только добавил бы
   отставание. */
.loader__fill {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0;
  background-color: var(--c-chalk);
}

/* Ряд на средней линии: звезда у левого поля, счётчик у правого. Устройство
   то же, что у образца (experiment.obys.agency): там на середине высоты
   стоит ряд с подписью слева и процентом справа, разведённые space-between.

   Раньше счётчик стоял внизу экрана, справа от центральной линии, а звезды
   не было вовсе.

   ПОЛЯ — ТЕ ЖЕ, ЧТО У СТРАНИЦЫ. Отбивка от краёв берётся --space-gutter,
   тем же числом, которым отбит контейнер и вертикальная сетка. Метки поэтому
   стоят ровно на полях страницы, а не на своих собственных: шторка и герой
   под ней должны говорить одним ритмом.

   ЦЕНТР ДЕРЖИТ transform, А НЕ ПАРА top/bottom. Ряду хватает высоты одной
   метки, и его середину надо совместить с серединой экрана — растягивать
   его на весь экран ради align-items: center значило бы накрыть прозрачной
   коробкой и окошко с кадрами, и полосу загрузки.

   С ОКОШКОМ РЯД НЕ СТАЛКИВАЕТСЯ: оно стоит в середине и занимает около сотни
   пикселей, а метки прижаты к полям. Полосу загрузки ряд пересекает, но
   между метками он пуст и ничего не закрывает — счётчик выше её слоем (см.
   слои у окошка), и в этом месте линия уходит ему за спину.

   Гаснет ряд вместе с кадрами: ехать вместе с полосами ему незачем. */
.loader__foot {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: var(--space-gutter);
  transform: translateY(-50%);
  transition: opacity 250ms ease;
}

/* Звезда. Коробка квадратная, ростом в --loader-star — оно считается от
   высоты видимого штриха, а не от кегля цифр; почему именно так, расписано
   там же.

   Цвет задан здесь и явно. В файле звезда залита чистым белым; путь
   переписан на currentColor, чтобы она брала мел ряда — тот же самый, что у
   цифр напротив. Две метки одного ряда не должны спорить оттенком, а чистый
   #FFF светил бы ярче соседа: ровно та же история, что со знаком в шапке, —
   только там оттенок правится фильтром, потому что знак приходит внешним
   файлом и currentColor до него не достаёт.

   Появляется вместе со счётчиком и той же анимацией — они одна пара, и
   проявляться порознь им нечего. */
.loader__star {
  display: block;
  flex: none;
  width: var(--loader-star);
  height: var(--loader-star);
  color: var(--c-chalk);
  animation: loader-count-in 800ms cubic-bezier(0.33, 1, 0.68, 1) both;
}

.loader.is-out .loader__foot {
  opacity: 0;
}

/* Набран РОВНО как пункт меню: Inter 500, 0.8125rem, трекинг 0.06em — те же
   три числа, что у .nav__link. Раньше счётчик был вдвое ниже заголовка
   героя и жирным (700), и это была самая крупная надпись на шторке; теперь
   он читается служебной строкой, как «WHO WE ARE» в шапке.

   Кегль поэтому и записан числом, а не выражением от --fs-display-xxl:
   привязка была к заголовку героя, а теперь — к пункту меню, и повторять
   её выражением не за что. Совпадать они обязаны, и держится это тем, что
   значение одно и то же на обоих правилах.

   Вес 700 из счётчика ушёл, но @import в начале файла остаётся: жирный
   Inter носит кнопка героя.

   Ширина не заперта, и ведущего нуля нет — оба держались за то, что счётчик
   стоял по центру: там любая смена числа знаков двигала бы обе кромки сразу.
   Теперь он прижат к правому полю, коробка сжата по содержимому, и число
   растёт влево, в пустую середину ряда: правая кромка стоит от «0» до «100».
   Выравнивать набор внутри поэтому нечем и незачем — коробка ровно по нему.
   tabular-nums остаётся: моноширинных цифр у Inter нет по умолчанию, а без
   них строка играла бы шириной на каждом шаге.

   Кегль — мерка ряда, 0.8125rem, ровно как у .nav__link. Звезда напротив от
   неё же и считается, но своим выражением: приравнять её коробку к кеглю
   нельзя, штрих внутри коробки заметно меньше её самой (см. --loader-star).

   Появляется счётчик проявлением, как у образца: 800мс от нуля к единице.
   Анимацией, а не переходом, — переход нужно чем-то запустить, а тут
   элемент просто возникает вместе со шторкой. Гашение при этом осталось
   переходом на .loader__foot: у него есть чем щёлкнуть, класс is-out. */
.loader__count {
  animation: loader-count-in 800ms cubic-bezier(0.33, 1, 0.68, 1) both;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--loader-mark);
  /* Межстрочный от набора добавил бы под цифрами пустую полосу, и отбивка
     от нижней кромки перестала бы совпадать с заданной. */
  line-height: 1;
  letter-spacing: 0.06em;
  /* Тот же белый, что у полосы загрузки: полоса и цифры — одно и то же
     значение, и цветом они спорить не должны. Раньше он был только запасным
     — поверх ложилась заливка по глифам; теперь заливки нет, и это
     единственный цвет счётчика. */
  color: var(--c-chalk);
  font-variant-numeric: tabular-nums;
}

/* Проявление счётчика. Единственное, что с ним происходит помимо самого
   счёта: у образца (experiment.obys.agency) цифры приходят прозрачностью за
   0.8с и уходят за 0.25с перед раскрытием шторки — уход у нас на
   .loader__foot выше.

   ЗАЛИВКИ ПО ГЛИФАМ ЗДЕСЬ БОЛЬШЕ НЕТ. Раньше цифры наливались мелом снизу
   вверх — градиент, обрезанный по тексту (background-clip: text), с резкой
   границей на --fill, долю писал js/loader.js тем же значением, что и высоту
   полосы. Само число было шкалой. Снято вместе с переездом счётчика на
   поведение образца: там цифры просто белые, а шкалу держит полоса. Вместе с
   заливкой ушли @supports-обёртка (без обрезки по тексту прозрачный color
   оставил бы на месте счётчика пустоту), -webkit-text-fill-color и строка
   --fill в paint(). */
@keyframes loader-count-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* В покое шторка просто исчезает: ни раскрытия, ни задержек. Зерно при этом
   остаётся, но перестаёт пересыпаться — текстура полотна не анимация. */
@media (prefers-reduced-motion: reduce) {
  .loader__bar,
  .loader__frame,
  .loader__track,
  .loader__foot {
    transition: none;
  }

  .loader__bar {
    animation: none;
  }

  /* Проявления у ряда тоже нет — звезда и цифры сразу на месте. both в
     базовом правиле держит начальный кадр до старта, поэтому обнулять
     анимацию мало: без явной единицы обе метки остались бы прозрачными. */
  .loader__count,
  .loader__star {
    animation: none;
    opacity: 1;
  }
}

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

   Экземпляров четыре, по одному на область: в шторке, в герое, в .page-body
   и в подвале. Подвал добавлен последним и по простой причине: он лежит
   отдельной плитой этажом ниже страницы (выкатывается из-под неё), сетка
   .page-body до него не достаёт, и линии обрывались на его верхней кромке.
   Раньше был один фиксированный слой поверх всей страницы, и из этого
   следовало, что поднять над ним что бы то ни было из героя невозможно:
   герой залипший и вдобавок isolate, то есть собственный контекст
   наложения, и любой z-index внутри него считается только относительно
   соседей внутри героя. Поднять сам герой тоже нельзя — под ним стоит
   .page-body, которая лежит сверху намеренно (см. комментарий к ней), а
   шторка обязана быть выше героя, при том что линии обязаны быть выше
   шторки: круг не разрывается.

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

   Отступ по краям задан маржином, а не паддингом: проценты у абсолютных
   детей считаются от паддинг-бокса, и с паддингом линии встали бы не на
   четверти набора, а на четверти набора вместе с полями. Совпадение с
   .container точное, потому что у него --container-max: none — ширина та
   же, а гаттер одинаковый.
   ============================================================ */
.grid-lines {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* В герое: над вуалью, под набором. Число ровно то же, что у вуали, — при
   равенстве спор решает порядок разметки, а линии идут после неё. Оставить
   auto нельзя: вуаль с положительным числом легла бы сверху. Рамка с
   набором стоит на 2 и остаётся выше обеих.

   Клиппинг героя линии обрезает, поэтому в переходе они уезжают вместе с
   ним и за его кромку не вылезают. */
.hero .grid-lines {
  z-index: 1;
}

/* На странице: над полотном, под медиа (у тех 120). Число нужно только
   чтобы обойти фон .page-body — сама она слоем не становится, и медиа
   по-прежнему поднимаются над линиями своим числом. */
.page-body .grid-lines {
  z-index: 1;
}

/* ИСКЛЮЧЕНИЕ НА ВРЕМЯ ВЫХОДА БЛОКА «КТО МЫ».

   Задача. Пока блок проигрывает выход, js/about-exit.js держит на
   .about-section transform, а он делает её собственным контекстом
   наложения. Медиа блока подняты над сеткой числом 120, но в запертом слое
   это число спорит уже только с соседями по блоку: сама сетка живёт этажом
   выше, у .page-body, и выходит поверх кадра волосяной линией — ровно та
   грязь на фотографии, ради которой всё это поднимали. Изнутри блока не
   лечится никак: из запертого слоя не выбраться никаким числом.

   КАК БЫЛО И ЧЕМ ПЛОХО. На время хода роняли саму сетку — z-index: 0, под
   обе дорожки. Кадр от линии это спасало, но сетка пропадала при этом НА
   ВСЁЙ странице, и в первую очередь на приходящем блоке «Что мы делаем»: он
   занимает почти весь экран все полторы секунды хода, и линии на нём
   дорисовывались только в тот кадр, когда блок вставал на место. Читалось
   ровно как поломка — сетка страницы обязана идти непрерывно.

   КАК СТАЛО. Роняем не сетку, а наоборот — поднимаем над ней обе дорожки.
   Линии никуда не деваются: дорожки прозрачны, своего полотна у них нет, и
   сквозь них видно всё, что нарисовано ниже. Меняется только порядок над
   кадром: медиа блока «Кто мы» заперты в его слое, но сам слой теперь выше
   линии, и она по фотографии не идёт.

   Число одно: 121 у секции — на единицу выше её же медиа (120), чтобы
   поднять слой целиком, а не влезть внутрь его спора.

   Поднимаем секцию, а не её дорожку: transform висит на секции, слой создаёт
   она, ей и подниматься. Дорожка при этом остаётся с z-index: auto и своего
   контекста не заводит — число секции считается этажом .page-body, там же,
   где живёт сетка.

   ПОДНИМАТЬ ЗАОДНО ДОРОЖКУ «ЧТО МЫ ДЕЛАЕМ» НЕЛЬЗЯ, и здесь ровно та же
   ловушка, что и с уронённой сеткой. Полторы секунды тут стояло второе
   правило — .services-track на 122, «чтобы входящий блок остался выше
   уходящего», — и линии на нём пропадали ТОЧНО ТАК ЖЕ: полотно у этого
   блока своё и непрозрачное (--sv-paper, фиолетовое), и стоит ему оказаться
   выше сетки, как он её собой и закрывает. Все три способа спрятать линии
   на фиолетовом — уронить сетку, поднять дорожку, поднять секцию — это одно
   и то же действие с разных сторон.

   А выше уходящего блока входящему быть и не нужно: на экране они не
   перекрываются вовсе. Уходящий залипает и уезжает вверх на свою высоту,
   входящий на столько же поднят отрицательным маржином — их кромки идут
   встык, кадр в кадр, и накрывать нечего. Спор о порядке возник бы только
   если бы скрипт отстал от прокрутки на кадр; но transform он пишет в том же
   кадре, в котором двигает scrollY.

   Класс ставит сам скрипт и только на ходу (0 < e < 1 с точностью до
   полтысячных): на единице блок уже за экраном, и порядок возвращается к
   обычному. Без скриптов и в покое (prefers-reduced-motion) хода нет,
   класса тоже, и правило не действует. */
html.is-about-exiting .about-section {
  z-index: 121;
}

.grid-lines__inner {
  position: relative;
  height: 100%;
  margin-inline: var(--space-gutter);
}

.grid-lines__inner span {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background-color: var(--c-chalk);
  opacity: 0.09;
}

/* Линии набраны цветом набора, а не мелом наглухо. На чёрном полотне это
   одно и то же, а на белом блоке «Что мы делаем» мел по белому — ничего:
   линии там были, но их не было видно.

   --ink-rgb ведёт js/paper.js вместе с полотном, поэтому линии темнеют
   ровно тогда же, когда темнеет набор, и на переходе между блоками
   перекрашиваются вместе с ним, а не ступенькой.

   Под тем же классом, что и остальное перетекание: без скрипта в силе
   остаётся мел выше. */
html.is-paper-live .grid-lines__inner span {
  background-color: rgb(var(--ink-rgb));
}

.grid-lines__inner span:nth-child(1) {
  left: 25%;
}
.grid-lines__inner span:nth-child(2) {
  left: 50%;
}
.grid-lines__inner span:nth-child(3) {
  left: 75%;
}

/* ============================================================
   Навигация
   ============================================================ */

/* Подчёркивания в меню нет. Здесь была линия, въезжавшая на наведении слева
   и уходившая вправо; вместе с ней уходят и подпорки, которые она требовала:
   position: relative с inline-block у пункта (абсолютному ::after не на что
   было опереться по ширине) и снятие китового гашения прозрачностью — с ним
   линия проявлялась бы на выцветающем тексте, и два приёма мешали бы друг
   другу.

   Отклика на наведение у пунктов нет вовсе — ни линии, ни гашения. Китовый
   opacity: 0.6 из system.css снят намеренно: признак наводимости на странице
   уже есть, и он общий для всех ссылок — курсор-кружок разбухает втрое,
   наехав на любую (js/blob-cursor.js, в его целях стоит a[href]). Второй
   признак, работающий только у меню, был бы не подсказкой, а шумом.

   Гасим и переход тоже: кит вешает на пункт transition по opacity, и без
   его снятия остался бы висеть переход, которому нечего анимировать. */
.nav__link:hover,
.nav__link:focus-visible {
  opacity: 1;
}

.nav__link {
  transition: none;
}

/* ============================================================
   Мобильного меню здесь больше нет
   Стояли гамбургер (.nav__burger с двумя полосами, сходившимися в крест) и
   полноэкранная панель (.nav-panel, z-index 130, ссылки дисплейным кеглем
   по кругу акцентов, призыв внизу). Оба жили в конце <body>, а не в герое:
   у .hero isolation: isolate, и число гамбургера считалось только против
   соседей по этому контексту — панель этажом выше накрывала весь герой
   вместе с ним.

   Убрано целиком по просьбе: два пункта меню помещаются в ряд навигации и
   на телефоне. Как ряд там перестраивается — в запросе max-width: 720px
   ниже по файлу.
   ============================================================ */

/* ============================================================
   Шапка секции
   Заголовок и подводка. Никаких надзаголовков — кит их запрещает.
   ============================================================ */
.section-head__lede {
  max-width: 56ch;
}

/* ============================================================
   Герой
   Фулскрин во всю ширину окна, как у референса: шейдерный фон на всю
   площадь, навигация оверлеем сверху, крупный центрированный заголовок
   посередине, мета и подсказка о прокрутке снизу.

   Это единственное место на странице, где набор центрирован, и
   единственный блок, доходящий до края окна: кит запрещал плите касаться
   вьюпорта, но плиты тут больше нет — фон занимает весь экран.

   Секция объявлена контейнером по обеим осям, и весь набор внутри
   меряется её высотой (cqh). Приём тот же, что был у плиты: при любой
   высоте окна заголовок, подводка и поля ужимаются одной пропорцией.
   ============================================================ */
/* Дорожка героя — перенос перехода из посадочника Frogge (у них .hero-track
   со .stage, а туда он попал из демо Codrops FullscreenScroll).

   Устройство: дорожка выше экрана ровно на --hero-exit, герой внутри неё
   залипает у верхней кромки, а вся страница ниже поднята отрицательным
   маржином на ту же величину и наезжает на героя сверху. Высота документа
   от этого не меняется: сколько дорожка добавила, столько маржин и забрал.

   Проигрывание — js/hero-exit.js. Здесь только геометрия и слои. */
:root {
  --hero-exit: 100svh;
}

.hero-track {
  position: relative;
  height: calc(100svh + var(--hero-exit));
}

/* Слой у страницы теперь есть, и он нужен подвалу. Тот идёт в разметке после
   неё, а по правилам покраски из двух позиционированных соседей с z-index:
   auto верхним оказывается тот, что ниже в разметке. Подвалу же положено
   лежать ПОД страницей и выезжать из-под её нижней кромки (см. .footer и
   js/footer-reveal.js), а трансформ, которым он выезжает, сам поднял бы его
   в ту же фазу покраски. Единица тут — то, чем страница его прижимает.

   Раньше на этом месте стояло предупреждение, что числа тут быть не должно:
   мол, .page-body станет отдельным слоем и запрёт содержимое, а медиа не
   поднимутся над сеткой. Проверено — не запирает: и сетка (z-index: 1), и
   медиа (120) лежат ВНУТРИ .page-body, а новый контекст наложения меняет
   только то, как сама страница спорит с соседями, и не трогает порядок
   потомков между собой. (Тем более что меню и гамбургер, стоявшие на 130 и
   140 в конце <body>, сняты — выше .page-body теперь вообще ничего нет.)

   Дорожку героя это не ломает: у неё z-index: auto, единица над ней, — то
   есть ровно то же, что было по порядку разметки.

   Фон обязателен: секции прозрачные, и сквозь них просвечивал бы уходящий
   кадр героя. Он же и накрывает подвал, пока тот не выехал.

   Зерно то же, что на полосах шторки, и стоит здесь ровно ради этого: под
   ним #000000 читается примерно как #060606, и пока фактура была только на
   шторке, два чёрных не сходились.

   Привязку оставляем обычной, не fixed. У шторки fixed вынужден — полос
   четыре, и без общего окна каждая тайлила бы шум от своего угла. Здесь
   поверхность одна, а тайл в 150px при прокрутке всё равно неразличим;
   fixed же заставлял бы браузер перерисовывать полноэкранное поле на каждом
   кадре прокрутки.

   Шевеления тоже нет намеренно: живое зерно на шторке идёт полторы секунды,
   а на странице это была бы вечная анимация во весь экран — ровно то, от
   чего страницу однажды уже избавили, убрав бегущий штрих из героя. */
.page-body {
  position: relative;
  z-index: 1;
  margin-top: calc(-1 * var(--hero-exit));
  background-color: var(--c-void);
}

/* ============================================================
   Перетекание полотна
   Блок «Что мы делаем» стоит на фиолетовом, вся остальная
   страница — на чёрном; набор и там и там светлый. Раньше это
   была ступенька: кромка секции и есть граница цвета. Теперь цвет
   переливается на подходе к блоку и обратно на выходе — считает
   долю js/paper.js и пишет каналы в --paper-rgb и --ink-rgb у
   корня.

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

   Набор при этом никуда не идёт: у блока он тот же мел, что и на
   странице, и --ink-rgb на всём пути остаётся мелом. Пару
   полотно/набор скрипт по-прежнему ведёт вдвоём — это его
   устройство, а не настройка блока, — просто вторая половина пары
   сейчас стоит на месте. Заведётся блок с тёмным набором — поедет
   снова.

   ПОЛОТНО БЛОКА НЕ СТОИТ НА МЕСТЕ. Пока идёт стопка, оно едет от
   фиолетового к оранжевому (--c-ember): пять карт — пять шагов
   цвета. Ведёт это тот же js/paper.js по второму концу, объявленному
   в разметке (data-paper-end), и берёт долю из прокрутки дорожки
   блока — то есть ровно из смены карт. Порядок у него такой: сперва
   «какого зона цвета сейчас», потом «насколько она на экране».

   Провала читаемости на всём этом пути нет, и это стоило проверить
   дважды. От чёрного к фиолетовому контраст мела падает с 18.6:1 до
   6.8:1 монотонно; дальше, от фиолетового к оранжевому, он идёт
   6.8 → 7.3 (середина) → 6.0 и ниже шести не опускается. Ямы нет ни
   там, ни там.

   Ради этой ямы пара полотно/набор и ведётся вдвоём — и ровно она
   задала глубину оранжевого: на исходной яркости заказанного тона мел
   давал бы 2.4:1, а перекрасить набор в чёрный по дороге значило бы
   выкопать яму ровно посередине пути, где он совпал бы с полотном по
   тону. Подробности — у --c-ember в css/palette.css.

   Всё под классом is-paper-live, который ставит сам скрипт.
   Без него в силе остаются статичные значения ниже: фиолетовый блок
   на чёрной странице, ровно как было. Страница без скриптов
   обязана выглядеть законченной, а не серой. */
html.is-paper-live .page-body {
  background-color: rgb(var(--paper-rgb));
}

/* Доли те же, что в статическом объявлении блока ниже, и менять их врозь
   нельзя: пока полотно переливается, набор обязан идти к тем же значениям,
   к которым он придёт по прибытии, — иначе на конце перехода цвет прыгнет. */
html.is-paper-live .services-section {
  --sv-paper: rgb(var(--paper-rgb));
  --sv-ink: rgb(var(--ink-rgb));
  --sv-ink-soft: rgba(var(--ink-rgb), 0.94);
  --sv-ink-muted: rgba(var(--ink-rgb), 0.86);
}

/* Без анимаций переход выключается целиком: дорожка схлопывается до экрана,
   наезда нет, страница ведёт себя ровно как до правки. Скрипт при этом и
   сам не запускается. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --hero-exit: 0px;
  }
}

/* sticky, а не relative: на дистанции дорожки герой залипает у верхней
   кромки, пока страница наезжает сверху. Ни container-type, ни overflow
   этому не мешают, а transform, которым герой уезжает, накладывается уже
   поверх залипания. */
.hero {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  container-type: size;
  overflow: hidden;
  isolation: isolate;
  /* Фон кадра до того, как холст нарисует первый кадр: тот же коралловый,
     что и в самом кадре, — переход не читается как вспышка. */
  background-color: var(--c-coral);
  /* Смена полотна на загрузке (правило ниже) — переходом, а не встык: если
     шейдер к моменту раскрытия ещё не нарисовал первый кадр, коралловый
     проступает мягко, а не хлопком. Успел — холст всё равно закрывает фон
     целиком, и перехода никто не увидит. */
  transition: background-color 400ms ease;
}

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

   Класс is-loading держится до конца раскрытия (js/loader.js снимает его в
   finish(), уже после того как полосы уехали), поэтому чёрным кроется вся
   загрузка целиком, а не только её начало. Коралловый возвращается на
   готовой странице — и почти всегда прямо под уже рисующим холстом. */
html.is-loading .hero {
  background-color: var(--c-void);
}

/* Смаз при уходе: кадр тянется вниз от ближней кромки, как scaleY: 2 в
   оригинале. Холст и вуаль тянутся одним и тем же преобразованием — иначе
   затемнение разъехалось бы с кадром, который оно затемняет.

   Общей обёртки у них нет намеренно: js/hero-flow.js берёт за host родителя
   холста — вешает на него is-live, слушает pointermove и меряет размер.
   Появись между холстом и секцией лишний слой, курсор перестал бы работать
   (обёртка была бы не для событий), а класс лёг бы не на ту коробку. */
.hero__canvas,
.hero__veil {
  transform-origin: 50% 0%;
}

.hero__canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* События слушает сама секция: заголовок и кнопки лежат выше холста и
     иначе перехватывали бы pointermove. */
  pointer-events: none;
  opacity: 0;
  transition: opacity 900ms ease;
}

.hero.is-live .hero__canvas {
  opacity: 1;
}

/* Вуаль — не украшение, а условие читаемости: кадр несёт и коралловый фон,
   и почти белое лицо, и чёрные провалы, и светлый набор на нём местами
   пропадал бы.

   Составная, а не плоская заливка. Плоская решала задачу тупо: гасила кадр
   везде одинаково, в том числе там, где поверх ничего не лежит. Здесь
   затемнение уходит туда, где сидит набор:

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

   Слои складываются по альфе, поэтому итог на верхней кромке — около 0.74,
   на боковых — 0.49, в углах — 0.89, в центре — 0.22. Ручки вынесены в
   переменные ниже: править там, а не в самих градиентах. */
.hero {
  --veil-base: 0.22; /* ровная подложка на всю площадь */
  --veil-edge: 0.92; /* виньетка на кромках и в углах */
  --veil-band: 0.5; /* полосы под навигацией и нижним рядом */
}

.hero__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-color: rgba(var(--c-void-rgb), var(--veil-base));
  background-image:
    /* Полоса держит почти полную силу первые 8% высоты — ровно столько
       занимают отступ рамки и ряд навигации, — и только потом спадает.
       Без этой ступени затемнение начинало таять уже под знаком. */
    linear-gradient(
        to bottom,
        rgba(var(--c-void-rgb), var(--veil-band)) 0%,
        rgba(var(--c-void-rgb), calc(var(--veil-band) * 0.78)) 8%,
        rgba(var(--c-void-rgb), calc(var(--veil-band) * 0.34)) 18%,
        rgba(var(--c-void-rgb), 0) 34%
      ),
    linear-gradient(
      to top,
      rgba(var(--c-void-rgb), var(--veil-band)) 0%,
      rgba(var(--c-void-rgb), calc(var(--veil-band) * 0.74)) 7%,
      rgba(var(--c-void-rgb), calc(var(--veil-band) * 0.32)) 16%,
      rgba(var(--c-void-rgb), 0) 30%
    ),
    /* Радиусы в процентах, поэтому эллипс следует пропорциям секции и на
       сверхшироком мониторе не превращается в круг посреди кадра.

       Ровно 72%, а не «с запасом»: угол бокса отстоит от центра на
       sqrt(0.5² + 0.5²) ≈ 0.707 его половины, и чтобы он попал на конец
       градиента, радиус должен быть 50 / 0.707 ≈ 71%. При прежних 120%
       угол приходился лишь на 59% пути, а середины кромок — на 42%, то
       есть до полной силы виньетка не доходила нигде и читалась как
       лёгкая грязь по периметру. */
      radial-gradient(
        72% 72% at 50% 50%,
        rgba(var(--c-void-rgb), 0) 35%,
        rgba(var(--c-void-rgb), calc(var(--veil-edge) * 0.5)) 70%,
        rgba(var(--c-void-rgb), var(--veil-edge)) 100%
      );
}

/* Блюровая виньетка: к кромкам кадр не только темнеет, но и уходит из
   фокуса. Второй слой поверх той же геометрии — тёмная виньетка гасит, эта
   размывает, и вместе они уводят периферию сильнее, чем каждая порознь.

   Псевдоэлементом вуали, а не отдельной коробкой в разметке: место
   свободно, лишний слой между холстом и секцией заводить не нужно (там
   слушается курсор), и на выходе героя размытие едет вместе с кадром —
   смаз задан на .hero__veil, а трансформ родителя тянет псевдоэлемент.

   ГЕОМЕТРИЯ ОБЩАЯ С ТЁМНОЙ ВИНЬЕТКОЙ: тот же эллипс 72% 72% от центра и
   тот же старт на 35%. Числа повторены, а не вынесены в переменную, — там
   они стопы градиента, здесь стопы маски, и совпадение это смысловое, а не
   техническое: разойдись они, читалось бы как две разные виньетки.

   Маска, а не прозрачность: backdrop-filter считается по альфе элемента,
   поэтому прозрачный центр маски означает нетронутый кадр, а плотный край —
   полную силу размытия. Между ними альфа ведёт плавную долю между
   размытым и резким.

   ЦЕНА. Слой во весь экран, а под ним каждый кадр перерисовывается шейдер,
   значит фон пересобирается и размывается тоже каждый кадр. Это самая
   дорогая строка в герое. Если на слабой машине первый экран станет
   тяжёлым — снимать надо отсюда. */
.hero__veil::after {
  content: "";
  position: absolute;
  inset: 0;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  mask-image: radial-gradient(
    72% 72% at 50% 50%,
    transparent 35%,
    #000 100%
  );
  -webkit-mask-image: radial-gradient(
    72% 72% at 50% 50%,
    transparent 35%,
    #000 100%
  );
}

.hero__frame {
  position: relative;
  z-index: 2;
  height: 100%;
  display: flex;
  flex-direction: column;
  padding-block: clamp(1.25rem, 4cqh, 2.5rem);
}

/* Ряд навигации садится на ту же четырёхколоночную сетку, что и линии на
   странице: знак в первой колонке, два пункта во второй и третьей, часы
   в четвёртой. Ширина ряда совпадает с .grid-lines__inner (у контейнера
   паддинг, у линий маржин той же величины), поэтому колонки сходятся
   с линиями пиксель в пиксель.

   gap обязателен нулевой: китовый .nav ставит его от флекса, а в гриде он
   съел бы ширину колонок, и они разъехались бы с линиями.

   Выравнивание по базовой линии, а не по центру: знак должен стоять на
   нижней границе пунктов меню. Работает это само, без подгонки числами.
   У картинки базовая линия — её нижняя кромка (так CSS считает у всех
   замещаемых элементов), а .brand вокруг неё отдаёт наружу базовую линию
   своего единственного ребёнка. У пунктов меню базовая линия — та, на
   которой стоят прописные. Совместив их, получаем ровно то, что нужно:
   низ вордмарка на низу букв. Заодно на ту же линию встаёт и время в
   четвёртой колонке.

   align-content: center обязателен в паре. Ряду задана min-height, и по
   умолчанию единственная строка растянулась бы на неё целиком, а группа
   выровненных по базовой линии внутри прижалась бы к её верху — весь ряд
   уехал бы вверх. Так строка остаётся по содержимому и центрируется в
   ряду, как раньше.

   Отступы ряду задаёт рамка героя, у самой навигации они лишние. */
.hero__nav {
  padding-block: 0;
  min-height: var(--nav-h);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
  align-items: baseline;
  align-content: center;
}

/* Список раскрывается в сетку ряда, чтобы каждый пункт встал в свою
   колонку. Только выше мобильной ступени: ниже кит гасит .nav__links через
   display: none, а landing.css грузится позже и вернул бы пункты на
   телефон. */
@media (min-width: 721px) {
  .nav__links {
    display: contents;
  }
}

/* Пункт начинается в 20px вправо от своей линии сетки (1.25rem — те же
   20px на бейзлайне, но едут вместе с автоскейлом страницы). */
.nav__links > li {
  padding-left: 1.25rem;
}

/* Часы Центральной Европы в четвёртой колонке. От пунктов меню отличаются
   ровно тем, что это не ссылка: семейство, кегль, капс, трекинг и тот же
   отступ 1.25rem от своей линии сетки — ряд читается четырьмя однородными
   подписями, а не тремя и кнопкой.

   Колонку не назначаем автоматике: до часов в ряду ровно три видимых
   элемента, и авторазмещение попало бы в четвёртую само, но любой будущий
   сосед сдвинул бы их с линии. Пилюли в ряду больше нет — в одну колонку
   она с часами не помещается, а призыв остался в плите, в подвале и в
   мобильной панели.

   tabular-nums обязателен: в Inter цифры пропорциональные, и на каждой
   секунде строка меняла бы ширину — часы дёргались бы на месте. */
.nav__clock {
  /* Отступ от линии нужен и самим часам, и метке под ними, а метка лежит
     вне потока и своего паддинга не унаследует — держим число в одном
     месте. */
  --clock-indent: 1.25rem;

  position: relative;
  grid-column: 4;
  justify-self: start;
  padding-left: var(--clock-indent);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.8125rem;
  line-height: var(--lh-tight);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--color-ink);
}

/* Метка пояса — вторая строка, но вне потока. В потоке она сделала бы
   часы вдвое выше, ряд навигации вырос бы под них, и первая строка съехала
   бы с линии пунктов меню: ряд выравнивает детей по центру. Так время
   стоит там же, где стояло, а метка просто висит под ним. */
.nav__clock-zone {
  position: absolute;
  left: var(--clock-indent);
  top: calc(100% + 0.25rem);
  color: var(--color-muted);
}
.hero__center {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: clamp(0.75rem, 2.6cqh, 1.75rem);
}

/* Кегль меряется высотой секции: три строки Anton при line-height 0.94
   съедают 2.82 своих кегля, и на невысоком экране это и есть то, что не
   влезает. min() берёт меньшее из двух — на широком и высоком экране
   правит ширина, на низком высота.

   Коэффициент поднят с 15cqh до 20cqh после того, как из героя убрали
   подводку: её четыре строки занимали заметную долю бюджета, и без них
   заголовок мог вырасти. На 1440x900 верх теперь упирается в китовые
   160px, то есть правит ширина; высота вступает от ~800px и ниже.

   max-width: 18ch вместо китовых 12ch: в узкой колонке фраза рвалась на
   четыре строки, а на восемнадцати знаках «CREATOR MARKETING» и «BUILT TO
   PERFORM» ложатся по строке. Меняя текст, проверяйте число строк. */
.hero__headline {
  font-family: var(--font-display);
  font-size: min(var(--fs-display-xxl), 20cqh);
  line-height: var(--lh-display);
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--c-chalk);
  max-width: 18ch;
}

/* Нижняя граница героя. В ряду осталась одна кнопка, и она стоит по центру
   экрана — то есть ровно на средней линии сетки, под серединой заголовка.
   Подсказки о прокрутке здесь больше нет: её рельс со штрихом был
   единственной бесконечной анимацией на странице, и вместе с ней ушло
   правило, которое глушило её при prefers-reduced-motion. */
.hero__foot {
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

/* Магнитной кнопке нужен запас вокруг: зона притяжения выходит за неё на
   56px, и на этот же порядок она уезжает за курсором. Без нижнего поля
   кнопка на возврате пружиной вылезала бы за край героя. */
/* Единственная кнопка на первом экране, и держать её в китовом размере
   ряда навигации незачем: у кита 44px, здесь 56 в высоту, вдвое шире
   отбивка по бокам и кегль на две ступени крупнее. Токен --btn-h при этом
   не трогаем — на нём сидит высота ряда навигации (--nav-h), и правка
   развалила бы шапку.

   Вес 700 против китовых 500. Он настоящий: кит тянет Inter только до 600,
   жирный добран отдельным @import в начале файла. Синтетический bold
   браузер размазывает, а на цветной заливке это видно вдвойне.

   Кнопка теперь единственное место, где этот вес нужен: счётчик шторки,
   ради которого его когда-то добирали вместе с ней, переехал на вес
   пунктов меню. */
.hero__cta {
  margin-bottom: clamp(0.25rem, 1.5cqh, 1rem);
  height: 3.5rem;
  padding-inline: 2.75rem;
  /* 18px против китовых 12. Высота пилюли при этом та же, 56px: капитель
     Inter на таком кегле около 13px, и воздуха сверху и снизу остаётся по
     двадцать с лишним — тесно не становится, а растянутая под набор кнопка
     перестала бы попадать в нижний ряд героя, где она стоит по центру. */
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: 0.06em;
}

/* Заливка CTA — СТЕКЛО. Пилюля не красится своим цветом, а замутняет то,
   что под ней: кадр героя проходит сквозь неё размытым и подкрашивает её
   собой. Двигается кнопка магнитом, поэтому стекло ещё и ездит по кадру —
   backdrop-filter пересчитывается на каждом кадре сам, и подложка под
   подписью живёт вместе с шейдером.

   ЧТО БЫЛО ДО. Тут стоял --grad-cta: три слоя поверх линейного хода
   orange → magenta → orchid — блик сверху-слева, холодный голубой подсвет
   снизу-справа, — плюс цветное свечение под пилюлей. Набор был чёрный
   (--fg-on-accent). Приём был снят с primary-кнопки посадочника Frogge,
   угол 114.41deg оттуда же, а цвета шли поперёк круга акцентов: три верхних
   цвета референса по доле в кадре, выложенные в одну диагональ. Смысл был
   в том, что кнопка одна на весь первый экран и ей можно показать всю
   палитру разом, пока карточки берут из неё по одному цвету за раз.

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

   НАБОР ПЕРЕВЁРНУТ НА МЕЛ. На градиенте он был чёрным, и это было
   единственно возможное; на стекле подложка тёмная, и чёрный на ней
   пропал бы. Контраст считан по худшему случаю кадра: под нижним рядом
   вуаль гасит кадр на 61% (ровная подложка 0.22 плюс нижняя полоса 0.5 в
   полную силу), поэтому даже белое лицо в кадре приходит под кнопку как
   99 из 255. Со стеклянной тенью 0.22 и бликом мел даёт к нему 5.8:1, а на
   обычном коралловом кадре — 9.1:1. Тень поэтому и оставлена лёгкой: за
   читаемость тут работает вуаль, а не сама кнопка, и утемнять пилюлю до
   тёмной плашки не пришлось.

   Смену цвета на ховере глушим по-прежнему: обратную связь даёт магнитное
   притяжение (js/magnetic.js), а перекрас поверх него читался как рябь.
   Все четыре состояния поэтому и перечислены вместе — переходу кита по
   background-color нечего анимировать, значения во всех одинаковые. */
.hero__cta,
.hero__cta:hover,
.hero__cta:focus-visible,
.hero__cta:active {
  /* Тень стекла. Не «цвет кнопки», а сколько кадра она съедает: чем больше,
     тем ближе к тёмной плашке и дальше от стекла. 0.22 — на этом числе
     худший случай кадра даёт 5.8:1 под подписью. */
  background-color: rgba(var(--c-void-rgb), 0.22);
  /* Блик по стеклу: светлее у верхней кромки, глуше в середине, чуть
     подхватывает у нижней — так стекло читается изогнутым, а не плоской
     плёнкой. Белый, а не мел: это свет, а не краска, и тёплый мел увёл бы
     блик в желтизну. Та же причина, по которой белым был блик градиента. */
  background-image: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.16) 0%,
    rgba(255, 255, 255, 0.04) 48%,
    rgba(255, 255, 255, 0.09) 100%
  );
  /* Само замутнение. В rem, а не в px: страница вся на автоскейле, и на
     широком мониторе стекло обязано мутнеть вместе с ней. saturate добавлен
     не для красоты — размытие усредняет кадр и обесцвечивает его, а
     подкрашиваться стекло должно именно кадром. */
  -webkit-backdrop-filter: blur(1rem) saturate(1.4);
  backdrop-filter: blur(1rem) saturate(1.4);
  color: var(--c-chalk);
  /* Кромка стекла. Без неё пилюля на светлом куске кадра теряет границу:
     замутнение видно, а где оно кончается — нет. */
  border-color: rgba(var(--c-chalk-rgb), 0.3);
  /* Первые две — толщина стекла по кромке: сверху волосок света, снизу
     волосок тени. Третья сажает пилюлю на кадр. Цветного свечения больше
     нет: оно красило кадр под кнопкой, а стекло, наоборот, обязано этот
     кадр показывать. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.38),
    inset 0 -1px 0 rgba(var(--c-void-rgb), 0.3),
    0 0.5rem 1.5rem rgba(var(--c-void-rgb), 0.35);
}

/* Без backdrop-filter стекла нет, и полупрозрачная пилюля осталась бы
   просто мутным пятном на кадре — подпись легла бы прямо на шейдер. Тень
   поэтому поднимается до плотной: кнопка становится тёмной плашкой со
   светлым набором, что некрасивее, но читается.

   Проверяем оба написания: Safari до 18 знает только префиксное. */
@supports not (
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
) {
  .hero__cta,
  .hero__cta:hover,
  .hero__cta:focus-visible,
  .hero__cta:active {
    background-color: rgba(var(--c-void-rgb), 0.62);
  }
}

/* Магнитная кнопка (js/magnetic.js). Смещение скрипт пишет в отдельное
   свойство translate, а не в transform, — иначе его подхватил бы переход
   появления и каждый кадр тянулся бы 700 мс.
   will-change держим только на том, что действительно едет каждый кадр. */
[data-magnetic] {
  will-change: translate;
}

/* Обёртку подписи ставит сам скрипт: без него лишнего span в кнопке нет. */
[data-magnetic] .btn__label {
  display: inline-flex;
  align-items: center;
  pointer-events: none;
  will-change: translate;
}

/* ============================================================
   Кто мы
   Раскладка снята с блока «( The Studio )» на noth.in: две колонки с
   большим воздухом между ними.

   Верхний ряд: слева метка, справа — от середины — заявление. Нижний ряд:
   справа широкий кадр, слева квадратный ролик с двумя строками продолжения
   под ним, и вся эта левая пара прижата низом к нижней кромке кадра. Именно
   выравнивание по низу и держит композицию: воздух сверху над роликом
   получается сам, ровно столько, сколько остаётся.

   Колонок четыре, и это те же четверти, по которым идут линии страницы:
   .about лежит в .container, а .grid-lines__inner отбит маржином той же
   величины, что даёт контейнеру паддинг, — значит 25/50/75% здесь и там это
   одно и то же место. Как и в ряду навигации, каждый кусок отбит от своей
   линии на 1.25rem.
   ============================================================ */
/* Секция занимает экран целиком. min-height, а не height: на невысоком окне
   набор должен уметь вырасти за экран, а не обрезаться — здесь, в отличие от
   героя, ничего в экран влезать не обязано.

   svh, а не vh: на телефоне динамическая панель браузера иначе то съедала бы
   низ секции, то оставляла бы под ней полосу. Строка с vh стоит первой как
   запасная — её подхватят браузеры, которые про svh ещё не знают.

   Набор при этом остаётся прижатым к верху: лишняя высота уходит вниз, как и
   у образца, где под кадром остаётся заметный воздух.

   Отбивка сверху и снизу меряется окном, а не китовыми 8rem. Причина в том
   же «влезать»: на окне 1280x720 фиксированные 8rem + 8rem съедали больше
   трети экрана, набор не помещался, секция вырастала за экран — и фулскрин
   переставал быть фулскрином ровно там, где он и нужен. Доля от svh
   отдаёт секции ту же долю воздуха на любой высоте окна. Кап в 8rem
   оставлен: выше него отбивка начала бы разъезжаться с остальной страницей.

   Якорь у секции свой, без общего отступа: страница ниже отбивает секции от
   верхнего края на --space-lg, чтобы заголовок не упирался в кромку, но эта
   секция как раз обязана вставать вровень с окном. Иначе переход из меню
   ставил бы её на 2rem ниже, и сверху торчала бы полоса уходящего героя.
   Специфичность поднята атрибутом: у общего правила селектор section[id]. */
/* Отбивка набора от кромок экрана в незалипшем состоянии — на невысоком
   окне, на телефоне и в покое. В залипших блоках она не работает: там поля
   секций обнулены, а набор центруется по высоте экрана. */
:root {
  --block-top: clamp(2rem, 7svh, var(--space-section));

  /* ЛИНИЯ МЕТОК ЗАЛИПШИХ БЛОКОВ. Одна на «( Who we are )» и
     «( What we do )»: обе метки прибиты к верху своего экрана этим числом и
     потому стоят ровно друг на друге — при смене блоков прокруткой одна
     встаёт на место другой, а не подпрыгивает.

     Иначе их не свести. Метка — первая строка своего блока, а блоки
     центруются по высоте экрана: ряды набора у них разной высоты, значит и
     верхние линии на разной глубине. Пока метки ехали вместе со своим
     набором, совпасть они могли только случайно. Теперь они из потока
     вынуты (position: absolute в запросах залипания) и набор больше не
     двигают — он центруется без них.

     Число просили «примерно 200px». Стоит оно в долях высоты окна, а не
     пикселями: 22svh дают ровно 198 на экране в 900 и держат ту же долю на
     любом другом. Пол в 6rem — на низком окне, где пятая часть высоты
     уводила бы метку в набор; потолок в 12.5rem — на высоком, где она
     иначе уползала бы к середине. */
  --label-top: clamp(6rem, 22svh, 12.5rem);
}

/* Набор блока стоит по центру высоты экрана, а не прижат к верху.

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

   Флекс-колонка ради одного justify-content: у секции ровно один ребёнок,
   .about, и центровать нечего кроме него. Паддинг при этом остаётся и работает
   минимальным полем: на невысоком окне набор перестаёт помещаться, min-height
   отпускает секцию в её собственную высоту, и центровать становится нечего —
   тогда паддинг и есть то, что отделяет набор от кромок.

   ВНИМАНИЕ НА СТЫК С БЛОКОМ «ЧТО МЫ ДЕЛАЕМ». Метки двух блоков стояли на одной
   высоте намеренно: --block-top у обоих, и при смене блоков прокруткой одна
   метка вставала ровно на место другой. Центровка это рассинхронизировала —
   метка «( Who we are )» опустилась на половину свободной высоты, а метка
   «( What we do )» осталась на --block-top.

   Теперь центрован и сосед, только другим способом: у него центруется ряд
   карты внутри залипшего экрана (align-content в запросе стопки —
   подробности там). Вычитать свободную высоту, как здесь, ему нельзя:
   высоту ряда карты CSS не знает, а мерить её скриптом пробовали и
   отказались.

   А МЕТКИ ОБОИХ БЛОКОВ ЦЕНТРОВКА БОЛЬШЕ НЕ КАСАЕТСЯ. Пока они ехали вместе
   со своим набором, совпасть на одной линии они могли только случайно: ряды
   у блоков разной высоты. Теперь обе вынуты из потока и прибиты к верхней
   кромке своего экрана одним числом (--label-top) — набор от этого не
   двигается, а метки стоят ровно друг на друге и при смене блоков не
   подпрыгивают. */
.about-section {
  min-height: 100vh;
  min-height: 100svh;
  padding-block: var(--block-top);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* ============================================================
   Дорожка блока «Кто мы»
   Тот же переход, что между героем и этим блоком, только
   этажом ниже: дорожка выше экрана на --about-exit, секция
   внутри залипает у верхней кромки, а блок «Что мы делаем»
   поднят отрицательным маржином на ту же величину и наезжает
   сверху. Проигрывание — js/about-exit.js, геометрия здесь.

   Высота документа от этого не меняется: сколько дорожка
   добавила, столько маржин и забрал.

   Всё внутри одного запроса, и в нём три условия. Узкий экран
   и низкое окно отсекаются потому, что залипшей секции нужна
   ровно высота экрана: блок в одну колонку туда не помещается,
   а обрезать его ради перехода нельзя. no-preference — потому
   что в покое переходов на странице нет вовсе.

   Вне запроса --about-exit остаётся нулём, дорожка выходит
   ростом с секцию, и блок ведёт себя как обычная секция. Скрипт
   это видит по измерению и молча выключается.
   ============================================================ */
:root {
  --about-exit: 0px;
}

.about-track {
  position: relative;
}

@media (min-width: 721px) and (min-height: 641px) and (prefers-reduced-motion: no-preference) {
  :root {
    --about-exit: 100svh;
  }

  .about-track {
    height: calc(100svh + var(--about-exit));
  }

  /* Ровно высота экрана, а не минимум: секция залипает, и всё, что
     выступило бы за экран, ушло бы под наезжающий блок. overflow
     страхует от переполнения на промежуточных высотах окна. */
  .about-section {
    position: sticky;
    top: 0;
    height: 100svh;
    min-height: 0;
    overflow: hidden;
    /* Полей у залипшего экрана нет: от его верхней кромки отсчитывается
       линия метки, и любое поле сдвинуло бы её вниз на свою величину.
       Минимальным полем они работают только в незалипшем состоянии — там
       правило выше в силе. */
    padding-block: 0;
  }

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

     ЦЕНТРУЕТСЯ ИМЕННО .about__grid, А НЕ .about, И ЭТО НЕСУЩЕЕ РАЗЛИЧИЕ.

     Метка вынута из потока и прибита к --label-top от верхней кромки
     экрана — на ту же линию, где стоит «( What we do )» у соседа. Пока
     метка была абсолютной ЯЧЕЙКОЙ СЕТКИ, отсчёт шёл не от экрана: у
     абсолютного ребёнка грида содержащим блоком служит полоса его рядов,
     а она тут центрована, то есть ходит вместе с высотой набора — а высота
     зависит и от ширины окна (заявление переливается, кадр меняет
     пропорцию вместе с колонкой).

     Числа с живой страницы: метка соседнего блока вставала ровно на
     --label-top — 176 при окне 800, 198 при 900, 238 при 1080. Метка этого
     блока при тех же окнах — 341, 387, 431. То есть общей линии не было
     вовсе, а сама метка при смене размера прыгала.

     И въезжала в гуся. Ролик висит в той же первой колонке, только
     центрован по высоте кадра справа: зазор между ними уходил в минус на
     1280×700, 1280×800, 1440×800/900/1000, 1600×900, 1280×1024, 1024×768 —
     до −64 пикселей, то есть метка целиком уходила под ролик. Мимо
     проносило только на широких экранах, где первая колонка шире ролика.

     Сетка в своей коробке — и содержащим блоком метки становится .about,
     обычный блок, чья верхняя кромка совпадает с кромкой экрана. --label-top
     наконец означает то, что написано, метка стоит на линии соседа и
     пересечься с рядами не может: она выше их всех.

     Высота 100% нужна обоим. У .about — чтобы коробка сетки было от чего
     считать, у самой сетки — чтобы центровать ряды в экране, а не в высоте
     набора. */
  .about {
    height: 100%;
  }

  .about__grid {
    height: 100%;
    align-content: center;
  }

  /* Метка на общей линии залипших блоков — той же, где стоит
     «( What we do )» у соседа.

     Из потока вынута по двум причинам сразу. Первая: в потоке её высота
     входила бы в центруемый набор, и подвинуть метку значило бы подвинуть
     весь блок. Вторая: линия у неё своя, от кромки экрана, а набор
     центрован — законы разные, и в одном потоке их не свести.

     Отсчёт идёт от .about: это .container блока, поэтому left в гаттер
     ставит метку ровно на левую кромку колонок, а top — от верхней кромки
     экрана, полей у залипшей секции нет. Работает это ровно потому, что
     .about теперь обычный блок, а сетка живёт в .about__grid внутри него:
     у абсолютного ребёнка ГРИДА содержащим блоком служит полоса рядов, и
     отсчёт поехал бы вместе с центровкой. Разбор — выше.

     grid-area здесь больше не нужен: метка не ячейка сетки. Раньше он стоял
     обязательным — с явным grid-column отсчёт шёл бы от области ячейки, то
     есть от левой кромки первой колонки плюс ещё один гаттер вбок и от
     верха центрованного ряда.

     Нижнее поле снимаем: вне потока оно ничего не отбивает, а в правиле
     базового вида оно есть и читалось бы обещанием, которого тут нет. */
  .about__label {
    position: absolute;
    top: var(--label-top);
    left: var(--space-gutter);
    margin-bottom: 0;
  }

  /* Наезжающего блока здесь больше нет: подъём на --about-exit переехал к
     самой .services-track и объявлен вне запроса — там эта переменная и так
     ноль по умолчанию, а маржин обязан висеть на дорожке, а не на секции
     внутри неё, иначе он сбил бы отсчёт её залипания. */
}

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

   Общее section[id] отбивает якорь на --space-lg, чтобы секция не упиралась
   в самую кромку окна. Залипшему блоку это вредно: он приезжает не к своей
   верхней кромке, а к началу своей дорожки — той точке, где кончается
   переход предыдущего блока и начинается залипание этого. Отступ ставит
   страницу на два рема ВЫШЕ этой точки, то есть на последний процент чужого
   перехода, — и блок открывается недоигранным. */
.about-section[id],
.services-section[id] {
  scroll-margin-top: 0;
}

/* Зацепка контр-параллакса из js/hero-exit.js. Скрипт двигает этот блок
   через top, а не через transform, — значит блок обязан быть позиционирован.
   Ровно поэтому и через top: преобразование сделало бы блок собственным
   слоем и заперло бы внутри него медиа, которым положено подниматься над
   вертикальной сеткой. Относительный сдвиг слоя не заводит. */
[data-hero-next] {
  position: relative;
}

/* Сетка блока лежит в своей коробке, а метка — снаружи неё, прямо в .about.
   Разбор, почему так, — в комментарии к разметке; коротко: у абсолютного
   ребёнка грида содержащим блоком служит полоса его рядов, а она здесь
   центрована по высоте экрана и ходит вместе с высотой набора. Метка,
   прибитая к --label-top, ездила вместе с ней и въезжала в ролик с гусём.
   Снаружи сетки содержащим блоком становится сам .about — обычный блок, чья
   верхняя кромка совпадает с кромкой экрана. */
.about {
  position: relative;
}

.about__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  column-gap: 0;
  row-gap: var(--space-xl);
}

/* Метка блока. Заголовок секции по разметке, обычный набор по виду — как у
   образца, где это такая же строка, что и остальные.

   Набрана пунктом ряда навигации целиком: тот же кегль (0.8125rem), тот же
   вес (500), тот же капс и тот же трекинг 0.06em. Она называет блок и
   работает подписью, а не частью набора рядом, — и с пунктом меню, который
   на неё ведёт, совпадает теперь во всём.

   Вес был 400 — единственное, чем метка отличалась от пункта. Поднят до 500
   по просьбе. Класс один на обе метки страницы, поэтому вместе с
   «( Who we are )» потяжелела и «( What we do )»: это одна и та же подпись
   в одной и той же роли, и разойтись по весу они не должны.

   Трекинг при капсе обязателен: прописные набраны плотнее строчных, и без
   разрядки «( WHO WE ARE )» слиплось бы в один брусок. Это то же число,
   что кит держит на пунктах меню и пилюлях.

   Раскладку класс не трогает: где метке стоять, решает сетка своего блока. */
.block-label {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.8125rem;
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-chalk);
}

/* Заявление блока. Набрано телесным шрифтом, а не Anton: это фраза, а не
   заголовок, и в китовом капсе она читалась бы вывеской.

   Меры в ch нет намеренно — строки ломают переносы в разметке. У образца
   они тоже расставлены по смыслу, а не отданы ширине колонки.

   Интерлиньяж 1.12, а не 1.3: при кегле около 37px прежнее число оставляло
   между строками под сорок пикселей воздуха, и фраза распадалась на
   отдельные строки вместо блока. Чем крупнее набор, тем меньше
   относительный интерлиньяж ему нужен — то же правило, по которому у
   дисплейных заголовков кита стоит 0.94. */
.block-statement {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--fs-about);
  line-height: 1.12;
  letter-spacing: -0.005em;
  /* Страховка на те случаи, когда переносы расставляет не разметка, а
     браузер: на телефоне ручные <br> погашены, а на узком десктопе (от 721
     до ~1030px кегль упирается в свой пол и строки перестают влезать) они
     доламываются по мере. pretty не даёт последней строке остаться из
     одного слова — единственная разновидность рванья, которую ручная
     разбивка предотвратить не может.

     Именно pretty, а не balance: строки тут завёрнуты каждая в свой
     inline-block (.reveal-line), и balance выравнивал бы их по отдельности,
     каждую внутри своей коробки, — то есть не то, чем кажется. */
  text-wrap: pretty;
  color: var(--c-chalk);
}

/* Метка блока «Кто мы» стоит от левого края контейнера, без отбивки: слева
   от неё линии нет, отступать не от чего.

   В сетку блока она больше не входит (см. .about__grid выше), поэтому
   отбивку от набора держит собственным нижним полем, а не row-gap сетки.
   Число то же, что у гэпа, — на широком экране метку из потока всё равно
   вынимают, и поле там ни на что не влияет; работает оно на телефоне и на
   низком окне, где метка идёт обычной строкой над набором. */
.about__label {
  margin-bottom: var(--space-xl);
}

/* Заявление блока «Кто мы» занимает правую половину. Типографика общая —
   в .block-statement выше; здесь только место на сетке. */
.about__statement {
  grid-column: 3 / span 2;
  grid-row: 1;
  padding-left: 1.25rem;
}

/* Левая пара: ролик и продолжение фразы. Стоит по центру высоты кадра справа
   — align-self: center. Высоту ряда задаёт кадр (он заметно выше пары),
   поэтому центр ряда и центр кадра это одно и то же место, и считать ничего
   не надо.

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

   ПАРА ВИСИТ НА ЛИНИИ 25% СЛЕВА. Стояла она справа от той же линии, в
   колонке 2, и композиция собиралась в правые три четверти: колонки 2, 3 и 4
   заняты, а в первой висела одна короткая метка блока. Теперь пара занимает
   колонку 1 и прижата к её правому краю — то есть к линии 25%, но с другой
   её стороны.

   Отбивка от линии та же самая, 1.25rem, только зеркальная: у заявления и
   кадра это padding-left от линии 50%, здесь — padding-right до линии 25%.
   Правило страницы «кусок отбит от своей линии на 1.25rem» тем самым не
   нарушено, просто пара читает линию с левой стороны.

   justify-self: end обязателен. Без него коробка растянулась бы на всю
   колонку, и padding-right отодвигал бы от линии пустоту, а не сам ролик:
   набор внутри выровнен по левому краю и остался бы у края контейнера. С
   ним коробка сжимается по содержимому (её ширину задаёт ролик, 240px — он
   шире двух строк продолжения) и встаёт правым краем к отбивке.

   Если колонка уже 260px, коробка садится по ней, и отбивка съедает ширину
   у ролика — тот ужимается, но от линии не отлипает. Ровные 240 держатся
   примерно от 1130px по ширине окна.

   Нижнее поле поднимает пару над общей кромкой. Прижим к низу при этом
   остаётся точкой отсчёта: пара по-прежнему висит на нижней кромке кадра,
   просто отступив от неё, — и на любой высоте ряда поднимается ровно на
   это число, а не на долю чужой высоты. Поле в rem, поэтому едет вместе с
   автоскейлом страницы. */
.about__aside {
  grid-column: 1;
  grid-row: 2;
  align-self: center;
  justify-self: end;
  padding-right: 1.25rem;
}


/* Ролик квадратный (у файла свои 800x800), поэтому высоту не задаём — её
   держит intrinsic-пропорция, и 240 по ширине дают 240 по высоте сами.

   Размер в пикселях, а не в rem, — единственное место на странице, где так.
   Просили ровно 240x240, а rem на широком мониторе уехал бы вместе с
   автоскейлом: 15rem на 1920 это уже 320px. Хотите, чтобы ролик ехал со
   страницей, — меняйте на 15rem.

   max-width: 100% всё равно оставляем. Коробка пары — четверть контейнера
   минус отбивка до линии, и ровные 240 получаются примерно от 1130px по
   ширине окна; ниже ролик садится по коробке, а не перелезает линию 25% —
   за ней начинается пустая вторая четверть, но лезть туда ему незачем.

   Размер теперь на обёртке, а не на самом ролике: обёртка держит маску,
   ролик внутри неё тянется на всю её ширину. */
.about__clip {
  display: block;
  width: 240px;
  max-width: 100%;

  /* Пропорция задана явно, хотя её держал и сам ролик: холст гуся лежит
     поверх абсолютно, а сам ролик после съёмки убирается из потока — и
     без этой строки коробка в тот момент сложилась бы в ноль. */
  aspect-ratio: 1 / 1;

  /* Точка отсчёта для холста. */
  position: relative;
}

.about__clip-media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Холст гуся (js/about-scrub.js) лежит поверх ролика с самого начала, а не
   встаёт на его место после съёмки. Причина простая: кадры снимаются с
   проигрывания, другого способа получить их дёшево нет, — и это
   проигрывание не должно быть видно. Холст закрывает ролик и замирает на
   первом же снятом кадре, а ролик доигрывает под ним.

   Прозрачным холст остаётся ровно до первого кадра, то есть доли секунды.

   Ролик после съёмки снимаем из потока совсем: он отыграл своё, а лишний
   декодируемый слой под холстом ни к чему. */
.about__clip-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* Курсор слушает секция, а не холст. Событий он не ловит — иначе перекрыл
     бы ролик и для тех, у кого слежение не заводится вовсе. */
  pointer-events: none;
}

.about__clip.is-scrub .about__clip-media {
  display: none;
}

/* Продолжение фразы мельче заявления — так у образца. Левым краем оно
   совпадает с роликом: обоих держит одна колонка. */
.about__tail {
  margin: var(--space-sm) 0 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: calc(var(--fs-about) * 0.75);
  line-height: 1.12;
  color: var(--c-chalk);
}

/* Широкий кадр — под заявлением и по той же линии. Рамка китовая, та же,
   что у плиток кампаний; меняется только пропорция: у исходника 16:9, а
   кит по умолчанию даёт 4:3. */
.about__frame {
  grid-column: 3 / span 2;
  grid-row: 2;
  margin: 0;
  padding-left: 1.25rem;
}

.about__media {
  position: relative;
  aspect-ratio: 16 / 9;
}

/* Живое поле шума на кадре — тот же шейдер, что на фоне героя (js/hero-flow.js
   держит оба хоста), и идёт он так же всегда.

   Холст накрывает картинку целиком, но сама она остаётся в разметке: ради alt
   и ради тех, у кого шейдер не заведётся — без скриптов, без WebGL или в
   покое. Тогда виден обычный кадр, и блок ничего не теряет.

   Событий холст не ловит: указатель слушает сама .card-frame__media, а холст —
   её собственный ребёнок и накрывает её целиком, так что перехватывал бы
   pointermove у своего же хоста.

   Проявление ждёт is-live: класс вешает скрипт, когда доехала текстура. Без
   него мигнул бы чёрный холст — рендерер заводится раньше, чем в него
   приходит картинка. Длительность та же, что у фона героя. */
.about__flow {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
  transition: opacity 900ms ease;
}

.about__media.is-live .about__flow {
  opacity: 1;
}

/* Рамка кита подсвечивает границу на наведении — здесь это не карточка и
   никуда не ведёт, поэтому граница стоит на месте. */
.about__frame .card-frame:hover {
  border-color: var(--color-hairline);
}

/* ============================================================
   Что мы делаем
   Порт блока подразделений с silent-house.com. Разобран по их разметке, а
   не срисован на глаз, поэтому переносится устройство:

     li  — position: sticky, общий top, растущий сверху паддинг
     div — панель ростом 78lvh, непрозрачная, с волосяной линией сверху,
           разложенная на их 12 колонок (у нас на наши четыре)

   Отсюда главное свойство блока: карты залипают и складываются в стопку.
   Следующая встаёт на ряд ниже предыдущей и накрывает её, а видно ровно
   кромки стопки. Это и есть дизайн — не список строк.

   Секция при этом выходит высокой: пять карт по 78svh это около четырёх с
   половиной экранов прокрутки. У образца карт три; число экранов и есть
   единственное, чем мы от него отличаемся. Крутить, если много, — высоту
   панели и шаг стопки ниже.

   ПОЛОТНО У БЛОКА СВОЁ И ЦВЕТНОЕ. У образца тут белые панели, у нас
   фиолетовый разворот — #5b2cd1, свой цвет, заведённый в палитре ради
   этого блока.

   НАБОР НА НЁМ СВЕТЛЫЙ, А НЕ ТЁМНЫЙ. Пока полотно было бумажным, а потом
   коралловым, роли переворачивались обратно к киту: светлое полотно,
   тёмный набор. Фиолетовый так не умеет — по светлоте он 8.6%, и чистый
   чёрный даёт к нему 2.7:1, то есть не читается вовсе. Набор остался
   мелом, и блок теперь не «светлый разворот на чёрной странице», а
   цветной: меняется полотно, а не полярность.

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

   Зерна тут нет, но по общей причине: оно сейчас отключено на всей странице
   (см. «Зерно» в :root). Вернётся — блоку теперь полагается --grain-tile,
   белая крупа для тёмных полотен, а не --grain-tile-ink, который заводили
   специально под светлые: коралловый блок и оранжевый призыв. На фиолетовом
   чёрная крупа читалась бы грязью.

   Вертикальная сетка сюда почти не доходит: карты подняты выше неё (см.
   z-index у .service). Над шапкой блока её линии теперь видно: набор здесь
   светлый, значит и они остаются мелом на 9%, тогда как на коралле темнели
   в чёрный вместе с набором. На фиолетовом это ровно тот же волосок, что и
   на всей остальной странице, — правки не требует. Колонки блок держит
   собственной раскладкой.
   ============================================================ */
.services-section {
  /* Полотно и набор. Полотно фиолетовое — --c-violet, единственный цвет
     палитры, заведённый ради одного блока. Коралл, стоявший тут раньше, был
     взят у героя, и разворот получался с ним одного цвета — второе за
     страницу появление того же тона, а не собственное событие.

     Бумаги тут больше нет. Блок был белым разворотом посреди чёрной
     страницы; теперь это цветной разворот, и перелив полотна из чёрного в
     фиолетовый — самое яркое событие прокрутки.

     Набор мелом, а не чёрным. Фиолетовый по светлоте 8.6%: чёрный даёт к
     нему 2.7:1 и пропадает, мел — 6.8:1. Полярность блока тем самым не
     переворачивается, и это не потеря: событием остаётся цвет, а светлый
     набор идёт через всю страницу без разрыва. */
  --sv-paper: var(--c-violet);
  --sv-ink: var(--c-chalk);
  /* Приглушённый набор на полотне. Берём каналами от мела, а не готовым
     --color-muted: тот подобран под чёрное полотно, а на фиолетовом гаснет.

     Доли пересчитаны под ВЕСЬ ПУТЬ полотна, а не под его начало. Полотно
     блока едет от фиолетового к оранжевому (--c-ember), и светлеет по
     дороге: то, что с запасом проходило на фиолетовом, к последней карте
     упиралось в порог. Считать надо по худшей точке, а она в конце.

     Мягкий поднят с 0.86 до 0.94: описанию это даёт 6.2:1 на фиолетовом и
     5.6:1 на оранжевом. Приглушённый — с 0.78 до 0.86: 5.4:1 и 4.9:1. На
     прежних долях номер к концу пути давал 4.3:1, то есть проваливал AA
     ровно там, где его носит мелкий капс в 13px.

     Порядок между ними сохранён: приглушённый по-прежнему тише мягкого, и
     номер читается подписью к описанию, а не вровень с ним. */
  --sv-ink-soft: rgba(var(--c-chalk-rgb), 0.94);
  --sv-ink-muted: rgba(var(--c-chalk-rgb), 0.86);

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

     Лесенки здесь больше нет. У образца следующая карта встаёт на пару
     ритмов ниже предыдущей, и сверху копятся их кромки — приём перенесли
     вместе с остальным, но он тянет за собой две вещи, которых на этой
     странице быть не должно. Первая: карты стоят на разной высоте, значит
     и набор в них — тоже, и на каждой смене он перескакивал на шаг. Второе
     лечится компенсацией паддингом, но тогда у первой карты над набором
     остаётся пустая полоса во всю глубину стопки, а сама стопка кромок
     ползёт по экрану, пока карты приходят. Ни того, ни другого не нужно:
     блок должен стоять, двигаться должна одна карта.

     ЧИСЛА, ЗАДАЮЩЕГО ВЕРХНЮЮ ЛИНИЮ НАБОРА, ЗДЕСЬ БОЛЬШЕ НЕТ. Стояло
     --service-stack-top: верхний паддинг сетки, который отбивал сразу оба
     столбца — метку слева и кромку набора справа. Пока набор жался к верху,
     это работало; центровать им же не вышло. Свободную высоту надо знать
     вычитанием, а высоту ряда CSS о карте не знает (в ряду соперничают
     кадр, имя и описание, и кто выше — зависит от ширины окна), поэтому её
     мерил и писал в переменную скрипт. Мера не доезжала, паддинг оставался
     общей отбивкой, и набор стоял у верхней кромки — то есть центровки не
     было вовсе.

     Теперь её делает сама раскладка, и мерить ничего не надо: панель
     держит экран и центрует свой единственный ряд (align-content: center в
     запросе стопки ниже), метка центруется в своей ячейке тем же способом.
     Обе центровки идут от одной и той же высоты — залипшего экрана, — и
     потому сходятся сами, без общего числа между ними.

     ЧТО ИЗ ЭТОГО СЛЕДУЕТ ДЛЯ МЕТКИ. Метка и верхняя кромка набора больше не
     на одной линии: набор центруется рядом (его середина на середине
     экрана, а верх — выше неё на пол-ряда), метка центруется строкой. На
     одной линии они стояли, пока обе отбивались одним паддингом сверху.
     Вернуть совпадение можно только измерением ряда — тем самым, которое
     отсюда и убрали.

     ПОДЪЁМ КАДРА. --service-lift — на сколько кадр карты стоит ВЫШЕ верхней
     линии набора. Просьба была сперва «не меньше 60px», потом «ещё на
     столько же»: 7.5rem это ровно 120 на 1440, и едет он дальше вместе с
     автоскейлом страницы, а не остаётся ста двадцатью пикселями на любом
     мониторе.

     Метка блока поднималась этим же числом, пока стояла в ряду набора.
     Теперь у неё своя линия, общая с меткой блока «Кто мы» (--label-top), и
     подъём её больше не касается.

     Подъём относительный (position: relative, top), а не полем. Полем он
     менял бы высоту ряда, а от неё центруется весь блок: кадр, поднятый
     маржином, укоротил бы ряд на те же 60px, и набор поехал бы вниз на
     тридцать — то есть подъём частью съел бы сам себя. Относительный сдвиг
     раскладки не трогает: ряд стоит на месте, по центру экрана, а метка с
     кадром просто нарисованы выше своей линии.

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

     ЛИНИИ ПОЛОТНА. Карта теперь во весь экран, а не от линии набора вниз,
     и накрывает их в блоке целиком — прежней полосы с видимыми линиями над
     шапкой блока больше нет. Это цена того, что коробка карты равна экрану:
     на её высоте держится ход прихода (карта уведена вниз ровно на свою
     высоту, то есть за нижнюю кромку экрана). */
  --service-lift: 7.5rem;

  background-color: var(--sv-paper);
}

/* Сетка блока: слева метка, справа стопка. Ростом в залипший экран — это
   ниже, в запросе стопки, — и от её верхней кромки отсчитываются обе
   колонки сразу.

   Скрипт выхода блока «Кто мы» двигает эту сетку через top (js/about-exit.js)
   — на ней держится отставание входящего блока, — значит она обязана быть
   позиционирована. */
.services__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  column-gap: 0;
  position: relative;
}

/* Метка. align-self обязателен: без него ячейка сетки растянулась бы на всю
   высоту колонки, а метка вместе с ней.

   Залипания у метки больше нет, и не нужно: блок под ней сам стоит экраном,
   а линию с верхней кромкой карты держит верхний паддинг сетки — он
   отбивает обе одинаково. Пока залипал список карт внутри проезжающей мимо
   секции, метке приходилось залипать отдельно и своим числом, чтобы за ним
   угнаться. */
.services__label {
  grid-column: 1;
  align-self: start;
  color: var(--sv-ink);
}

/* Правая часть: одна дорожка карт. Три колонки из четырёх, и левой кромкой
   она садится РОВНО на линию 25% — своей отбивки у неё нет.

   Отбивка была и переехала внутрь, к кускам набора. Пока она стояла здесь,
   на неё съезжала вся панель, а с панелью и её доли: они выходили по 25%
   минус доля рема, кромки промахивались мимо линий 50% и 75% на семь
   пикселей, и «отбито на 1.25rem» переставало быть правдой — куски стояли
   то на 1.25, то на 1.67 от своей линии.

   Теперь доли панели и колонки полотна — одно и то же, а базовый отступ
   каждый кусок держит сам: колонка набора паддингом, кадр полем, номер —
   своей отрицательной отбивкой наружу. */
.services__body {
  grid-column: 2 / span 3;
}

/* Подводки у блока нет намеренно: имена сервисов на картах говорят то же
   самое, а вместе с ней стопка начиналась на экран ниже метки. Теперь
   верхняя кромка первой карты и метка держат одну линию сразу, а не после
   прокрутки — ровно то, ради чего метка и залипает.

   Метка перекрашена здесь, а не в общей .block-label: та набрана для
   чёрного полотна и служит ещё блоку «Кто мы». */


/* ---- Дорожка блока ------------------------------------------------
   Та же конструкция, что у дорожки блока «Кто мы» этажом выше: коробка выше
   экрана, секция внутри залипает у верхней кромки и держит ровно экран, а
   вся прокрутка дорожки уходит на смену карт внутри неё.

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

   Высота дорожки и есть вся прокрутка блока: экран на сам блок плюс по шагу
   на каждую смену карты. Шаг меньше экрана намеренно — карта успевает прийти
   и постоять, но целого экрана прокрутки на неё жалко: пять карт по экрану
   превратили бы блок в шесть экранов подряд.
   ------------------------------------------------------------------- */
.services-track {
  --service-scroll: 85svh;

  position: relative;
  /* Наезд на блок «Кто мы»: тот залипает, а этот идёт сверху и накрывает
     его. Раньше маржин стоял на самой секции — теперь на дорожке, иначе
     он смещал бы отсчёт залипания секции внутри неё.

     Правило вне запроса намеренно: за пределами своего запроса --about-exit
     равен нулю, и маржин схлопывается сам. */
  margin-top: calc(-1 * var(--about-exit));
}

/* Список карт. Контейнера у него нет: панели держат свои колонки
   собственной раскладкой, а не сеткой страницы. */
.services__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Панель непрозрачна — она накрывает предыдущую, и на этом держится вся
   смена карт.

   Границы у неё больше нет. У образца сверху идёт волосяная линия, и с ней
   приём был перенесён: линия и была видимой кромкой приходящей плиты. Убрана
   как некрасивая, а работу её теперь делает уход накрытой карты — гашение с
   подсжатием, в запросе стопки ниже.

   Долей у панели три, и это не четвёрка страницы в миниатюре, а её же
   колонки: панель занимает три четверти контейнера и стоит левой кромкой
   ровно на линии 25%, значит на три части она и делится — каждая доля
   выходит колонкой полотна, а кромки долей садятся на линии 50% и 75%
   без промаха. Ровно ради этого с .services__body сняли отбивку: пока она
   была, панель съезжала на 1.25rem и уводила с собой обе кромки.

   Четвёрка стояла тут раньше и именно этим была плоха: доли выходили по
   18.75% страницы, ни одна кромка на линию не попадала, и имя карты,
   стоявшее во второй доле, начиналось на 45% и переваливало через линию
   50% серединой слова.

   Мера долей: две левые — набор (имя и описание под ним), правая — кадр.
   То есть набор занимает половину страницы, от линии 25% до линии 75%, а за
   крайнюю линию уходит один кадр. Порядок сложился в три приёма: сперва
   кадр слева и набор вразбивку справа, потом набор одной колонкой слева от
   линии 50% и кадр за ней, теперь — набор в две доли и кадр за линией 75%.
   Первый вариант ломал разбивку имени, второй жал описание в четверть
   страницы, а описание с тех пор набрано кеглем заявления соседнего блока и
   такой колонки не терпит. */
.service__panel {
  /* Отбивка набора от верхней кромки карты. Здесь, в потоке, она нужна:
     карты идут подряд и кромок столько же, сколько карт. В залипшем экране
     ниже переменная обнуляется — там кромка одна и совпадает с линией метки
     блока.

     Переменной, а не числом, ещё и потому, что от неё отсчитывается номер,
     вынесенный за левую кромку панели: он на той же линии, и порознь их
     двигать нельзя. */
  --service-pad-top: var(--space-xl);

  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: 0;
  align-items: start;
  min-height: 78svh;
  padding-top: var(--service-pad-top);
  padding-bottom: var(--space-xl);
  /* Полотно у панели своё и непрозрачное — на нём держится вся смена карт:
     приходящая накрывает предыдущую. Волосяной линии сверху больше нет,
     поэтому плита теперь невидима, и смену показывает не её кромка, а уход
     накрытой карты — гашение с подсжатием ниже, в запросе стопки. */
  background-color: var(--sv-paper);
}

/* Номер в первой доле панели. В потоке — обычным элементом: слева от карты
   тут ничего нет, вынести его туда некуда. Наружу, за линию, он выходит
   только в залипшем экране, и всё это описано ниже, в его запросе. */
.service__index {
  grid-column: 1;
  margin: 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.8125rem;
  line-height: 1.3;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--sv-ink-muted);
  transition: color var(--motion-base);
}

/* Кадр сервиса. Стоит в третьей доле панели — справа от линии 75%, то есть
   за крайней линией полотна, а весь набор карты остаётся левее. Так у блока
   появляется своя крайняя колонка, занятая одним кадром: на странице это
   единственное место, где за последнюю линию уходит медиа.

   Долями он двигался дважды: стоял первым (слева от всего), потом вторым
   (за линией 50%). По разметке он при этом всё время идёт первым — на
   телефоне колонка одна, и там кадр обязан приходить раньше имени.

   Отбит от своей линии на общие 1.25rem, и не паддингом, а полем: паддинг
   вошёл бы внутрь квадрата (box-sizing у страницы border-box), aspect-ratio
   держит квадратной внешнюю коробку, и картинка внутри вышла бы уже своей
   высоты.

   Квадрат — правило страницы: на ней все медиа квадратные, и кадры ростера,
   и широкий кадр в «Кто мы» с ним не спорят, он там один. Углы прямые по той
   же причине.

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

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

   В коробке теперь ролик, а не картинка. Раскладке всё равно: и <img>, и
   <video> тут одинаковый блочный элемент во всю коробку с object-fit: cover,
   и оба одинаково масштабируются приходом карты. Прежние JPEG лежат в
   images/ (service-01…05) — на странице они больше не участвуют.

   Файлы по мегабайту, 640x640, пять секунд, зациклены. Квадрат исходника
   совпадает с квадратом коробки, поэтому cover ничего не срезает — он тут
   на случай, если файл однажды подменят неквадратным. Тянутся они лениво, по
   подходу блока (js/ui.js): пять источников с обычным src браузер потянул бы
   с первой секунды и отобрал бы канал у героя. */
/* Отбивка от линии 75% съедается из ширины, а не добавляется к ней. Пока
   стояли просто width: 100% и margin-left, кадр выходил за свою долю ровно
   на эту отбивку. На широком экране не видно: доля там шире 12rem, кадр
   упирается в свой максимум и до правой кромки не достаёт. А на 721–900,
   где доля уже 12rem, он вылезал за панель на те же 20px — то есть за поле
   страницы: на 768 правая кромка кадра вставала в 13px от края окна при
   поле в 20. Единственное место на странице, где что-то заходило в поле. */
.service__shot {
  grid-column: 3;
  width: calc(100% - 1.25rem);
  max-width: 12rem;
  margin-left: 1.25rem;
  aspect-ratio: 1;
  overflow: hidden;
}

/* Одинаково для картинки и ролика: коробку заполняет собой, лишнее срезает.
   Масштаб по приходу карты пишется сюда же, ниже, в запросе стопки. */
.service__shot-media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Имя — единственное дисплейное место в секции. Anton, капс, ОДНА строка.

   Ручной перенос из разметки снят вместе с двухстрочностью: имена
   двусоставные («Campaigns Management»), и пока колонка была в четверть
   страницы, второе слово в неё не влезало — перенос ставился руками, чтобы
   ломалось по составу, а не по мере. Теперь под именем половина страницы, и
   ломать нечего: самое длинное из пяти идёт в строку с запасом.

   Места на сетке у него больше нет: имя лежит внутри .service__col, первой
   строкой колонки, и стоит там же, где описание под ним. Разметкой, а не
   двумя соседними ячейками, — иначе имя и абзац были бы двумя рядами сетки
   панели, кадр рядом растягивал бы первый ряд на свои двенадцать ремов, и
   описание отъезжало бы от имени на всю высоту кадра.

   Нижнее поле и есть отбивка описания от имени. Число то же, что держит
   ряды панели на телефоне.

   КЕГЛЬ ПОДОБРАН ПОД ОПИСАНИЕ, а не под колонку. Абзац под именем набран
   заявлением блока «Кто мы» (--fs-about: 28px на 1440), и имя обязано
   встать к нему в отношение, а не просто занять всю доступную ширину:
   3vw дают 43px на 1440 — примерно полтора описания. Это слышно как
   заголовок и не орёт: на дисплейной гарнитуре в капсе полутора хватает
   с избытком.

   Пол и потолок сторожат края. 1.375rem — чтобы «CAMPAIGNS MANAGEMENT»
   (около 9.2em в Anton) осталось в одну строку на 721px, где колонка самая
   узкая: там она даёт под набор ~245px, а 22px кегля просят 202. Пол был
   1.5rem, и запаса хватало с лихвой, пока колонка не сузилась на пятую
   часть; после этого 24px оставляли от силы десятую — на глаз незаметно, но
   одного промаха в ширине глифа хватило бы, чтобы имя сорвалось во вторую
   строку. 3.5rem подхватывает выше 2560, где 3vw уже перерастают страницу.
   Между краями кегль едет ровно по vw, как и весь дисплейный набор кита.

   Прежний кап 2.7vw заводился под колонку в 18.75% страницы и под перенос
   в две строки — с одной строкой в половину страницы он больше ни от чего
   не защищал. */
.service__name {
  margin: 0 0 var(--space-md);
  font-family: var(--font-display);
  font-size: clamp(1.375rem, 3vw, 3.5rem);
  line-height: var(--lh-display);
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--sv-ink);
  transition: color var(--motion-base);
}

/* Колонка набора: имя и описание под ним. Две доли панели из трёх — от
   линии 25% до линии 75%, то есть половина страницы. Одной доли ей было
   мало: описание набрано кеглем заявления из блока «Кто мы» (см. ниже), и в
   четверти страницы такой набор шёл бы колодцем в пять-шесть строк.

   Правой половиной колонка была ещё раньше: имя стояло отдельной ячейкой,
   описание — двумя последними долями, вразбивку. Теперь это одна коробка, и
   отбивку внутри неё держит нижнее поле имени.

   Отбивка от линии 25% своя, паддингом: с .services__body её сняли ради
   того, чтобы доли панели легли на линии полотна ровно, и теперь каждый
   кусок отбивается от своей линии сам.

   ШИРИНА — ЧЕТЫРЕ ПЯТЫХ ОТ ЭТИХ ДВУХ ДОЛЕЙ. Просьба была сузить набор на
   20%, и считается она от места на сетке, а не от меры в знаках: колонка
   по-прежнему начинается на линии 25%, но правым краем не доходит до линии
   75% на пятую часть своей ширины. На 1440 это 538px вместо 672.

   Процентом, а не мерой в ch: доля панели едет с шириной окна, и процент
   едет вместе с ней — набор сужается ровно на пятую часть на любом экране,
   а не на пятую часть от одной конкретной ширины.

   width, а не max-width: ячейка грида по умолчанию растянута по своей
   области (stretch), и потолок ей не указ, пока она не переросла его сама. */
.service__col {
  grid-column: 1 / span 2;
  width: 80%;
  padding-left: 1.25rem;
}

/* Описание набрано заявлением блока «Кто мы» — тем же кеглем, тем же
   интерлиньяжем, тем же весом (см. .block-statement выше). Не «похоже», а
   ровно: --fs-about, 1.12 и 400 стоят здесь теми же числами, и трекинг у
   них и так был общий.

   Что это меняет. Было clamp(1rem, 1.35vw, 1.5rem) при интерлиньяже 1.35 —
   обычный мелкий набор подписи, вдвое мельче имени над ним. Стало 28px на
   1440 против прежних 19: описание перестало быть подписью к имени и
   встало с ним в один голос, а страница получила один и тот же тон
   изложения в обоих блоках-экранах.

   Интерлиньяж 1.12 — не самоволие, а следствие кегля: чем крупнее набор,
   тем меньше относительный интерлиньяж ему нужен, и при 28px прежние 1.35
   растащили бы абзац на отдельные строки. Число взято у соседа, где оно
   стоит по той же причине.

   Меры в ch больше нет, и снята она не за компанию. 34ch держали прежний
   мелкий набор, но ch едет вместе с кеглем: на новом это около 495px при
   колонке в 650, то есть мера съела бы ровно ту ширину, которую колонке
   только что добавили, — набор остался бы в трети страницы вместо половины.
   Мерой теперь работает сама колонка: две доли панели, и больше ничего.

   text-wrap: pretty — оттуда же, где и всё остальное: не даёт последней
   строке остаться из одного слова. На крупном наборе такой хвост виден
   вдвое сильнее, чем на мелком.

   Гашение по --op при этом никуда не делось: долю на каждом кадре пишет
   js/ui.js, ноль — панель открыта, единица — закрыта следующей. У образца
   это lg:opacity-[calc(1-var(--op))] на том же абзаце. */
.service__text {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--fs-about);
  line-height: 1.12;
  letter-spacing: -0.005em;
  text-wrap: pretty;
  color: var(--sv-ink-soft);
  opacity: calc(1 - var(--op, 0));
}

/* ---- Залипший экран -----------------------------------------------
   Всё, что делает блок стопкой, живёт только здесь. Выше по файлу карты
   размечены обычным потоком — одна под другой, каждая со своей кромкой и
   своей отбивкой, — и это же и есть поведение за порогом и в покое.
   Сбрасывать там нечего: правил нет.

   Раньше стопка была объявлена базово, а три режима её разбирали — телефон,
   prefers-reduced-motion и <noscript>, — и каждый сброс был отдельным поводом
   разъехаться с остальными. Из трёх остался один: без скриптов запрос тоже
   действует, а --enter писать некому, и стопку приходится разбирать вручную.
   ------------------------------------------------------------------- */
@media (min-width: 721px) and (prefers-reduced-motion: no-preference) {
  .services-track {
    /* Шагов на один меньше, чем карт. Первая стоит на месте с самого начала
       — её приход считается ещё до входа в дорожку и прокрутки не занимает,
       — а сменяются за блок только четыре остальные. Пока здесь стояло число
       карт, лишний шаг оставался в хвосте: последняя карта приходила на 3.85
       шага из пяти, и почти целый экран после неё колесо крутило блок
       вхолостую. Тот же множитель стоит в js/ui.js, и врозь их менять
       нельзя — от него считается доля прокрутки. */
    height: calc(100svh + (var(--service-count, 5) - 1) * var(--service-scroll));
  }

  .services-section {
    position: sticky;
    top: 0;
    height: 100svh;
    /* Непришедшие карты уведены вниз за собственную высоту — обрезать их
       обязана секция, иначе они висели бы поверх следующего блока. */
    overflow: hidden;
    /* Поля секции внутри залипшего экрана съедали бы его высоту и гнали блок
       по экрану, пока проходят: и на входе, и на выходе. Отбивку сверху
       задаёт сетка, снизу карта доходит до самой кромки. */
    padding-block: 0;
  }

  /* ВОТ ЗДЕСЬ ЦЕНТРУЕТСЯ БЛОК. Сетка ровно в залипший экран и без полей, а
     ряд у неё один: слева метка, справа коробка стопки. Коробка теперь
     ростом с набор карты (см. .services__list ниже), значит и ряд ростом с
     набор, — и грид центрует его в высоте экрана сам, ничего не измеряя.

     Метка от этого встаёт на верхнюю линию набора: она в том же ряду и
     прижата к его началу (align-self: start в базовом правиле). Ровно так же
     стоит «( Who we are )» этажом выше — первой строкой своего ряда, а ряд
     целиком по центру экрана.

     Полей у сетки нет намеренно. Любое поле отняло бы часть высоты, от
     которой считается середина, и ряд встал бы ниже центра на его половину.

     Так это и должно было работать с самого начала. Стояло иначе: ряд
     центровался внутри панели, метка — отдельно, каждый от своей высоты, и
     на одну линию они не попадали; а до того верхняя линия считалась
     вычитанием, для которого высоту ряда мерил скрипт. */
  .services__grid {
    height: 100%;
    align-content: center;
  }

  /* Стопка — грид в одну клетку. Все пять карт лежат в ней друг на друге, и
     коробка выходит ростом с самую высокую из них, то есть с НАБОР, а не с
     экран.

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

     Обрезки у коробки больше нет. Резать ей нечего: карта за экраном
     уводится вниз на целый экран, и всё лишнее снимает overflow самой
     секции. А была она вредна — срезала бы и номер, вынесенный за левую
     кромку, и хвост панели, которым та накрывает полотно под собой. Вместе
     с обрезкой ушло и поле под номер (--service-index-gutter): оно
     существовало только затем, чтобы номер уцелел под этой обрезкой. */
  /* Метка на той же линии, что и «( Who we are )» этажом выше, — и по той
     же причине вынута из потока: пока она стояла ячейкой сетки, её высота
     входила в центруемый ряд.

     Подъёма на --service-lift у неё больше нет: он поднимал её над линией
     набора, а линия у неё теперь своя, общая с соседним блоком. Кадр свой
     подъём сохранил — он часть карты и центруется вместе с ней.

     Отсчёт от .services__grid: она же и есть .container блока, стоит ровно
     в залипший экран и полей по вертикали не имеет. */
  .services__label {
    position: absolute;
    /* grid-area в auto — не для порядка. У абсолютной ячейки грида
       контейнером служит её ОБЛАСТЬ СЕТКИ, если позиция задана явно: с
       оставленным grid-column: 1 отсчёт пошёл бы от левой кромки первой
       колонки (плюс ещё один гаттер вбок) и от верха ряда — а ряд
       центрован, то есть линия метки уехала бы к середине экрана. С auto
       позиция перестаёт быть определённой, и контейнером становится сама
       сетка. */
    grid-area: auto;
    top: var(--label-top);
    left: var(--space-gutter);
  }

  .services__list {
    display: grid;
    /* Колонка объявлена явно, и нулём в минимуме не для красоты: у ячейки
       грида собственный минимум — min-content, и длинное слово набора могло
       бы распереть колонку шире коробки. С minmax(0, 1fr) колонка держится
       ширины стопки, а перенос остаётся заботой набора. */
    grid-template-columns: minmax(0, 1fr);
  }

  /* Все карты в одной клетке: карта не отбивается от предыдущей, а встаёт
     ровно на её место и накрывает целиком. Порядок наложения решает
     разметка — каждая следующая идёт после предыдущей и потому выше.

     Растянуты по клетке (стандартный stretch грида), значит высота у всех
     одна — самой высокой. Набор во всех картах от этого стоит на одной
     линии, и при смене карт он не прыгает.

     --enter ведёт js/ui.js: ноль — карта ещё внизу, единица — встала на
     место. Ход теперь в экранах, а не в процентах собственной высоты:
     коробка карты сжалась до набора, и прежние 100% увели бы её вниз
     всего на высоту ряда — карта выезжала бы из воздуха посреди экрана
     вместо того, чтобы приходить снизу. */
  .service {
    grid-area: 1 / 1;
    transform: translateY(calc((1 - var(--enter, 1)) * 100svh));

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

       Долго держать нельзя — на быстрой прокрутке карта заметно отстала бы
       от пальца. Четверти секунды хватает, чтобы съесть ступеньку, и мало,
       чтобы это читалось задержкой. */
    transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
    /* Выше сетки страницы (у неё 1 внутри .page-body). Линии полотна панелям
       не нужны: они и не набор, размеченный сеткой, а непрозрачные слои
       поверх страницы. Число у всех карт одно: спор равных решает порядок
       разметки, и каждая следующая накрывает предыдущую сама. */
    z-index: 2;
  }

  .service__panel {
    /* Опора для хвоста (::after ниже). */
    position: relative;
    /* Во всю клетку стопки — то есть в высоту самой высокой карты. Набор
       внутри прижат к верху (align-items: start в базовом правиле), поэтому
       во всех пяти он стоит на одной линии. */
    height: 100%;
    min-height: 0;

    /* Поля обнулены оба, и заодно с ними --service-pad-top: в потоке карта
       отбивается ими от соседних карт, а в стопке соседей нет — карты лежат
       друг на друге, и любое поле съело бы высоту коробки, от которой
       центруется весь блок. */
    padding-block: 0;
  }

  /* Хвост панели: непрозрачное поле от нижней кромки набора вниз на экран.

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

     Целый экран с запасом: секция всё лишнее срежет своим overflow, а
     меньшего числа не хватило бы на невысоком окне.

     Цвет — полотно блока, то же самое, что у панели: хвост и есть её
     продолжение, просто пустое. */
  .service__panel::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: 100svh;
    background-color: var(--sv-paper);
  }

  /* Номер выходит наружу, влево за линию 25%, — имя и описание остаются
     справа от неё. Единственная подпись на странице, отбитая от своей линии
     наружу.

     ЯЧЕЙКА, А НЕ АБСОЛЮТ. Стоял абсолют — right: 100% от панели и top в ноль,
     — и держался он на том, что набор прижат к верхней кромке: ноль и был
     его линией. Ряд теперь стоит по центру высоты, и абсолют этого не знает:
     номер остался бы у верхней кромки экрана один, в двух сотнях пикселей
     над своим набором. Ячейка знает — она в том же ряду и едет вместе с ним.

     Наружу за линию его выводит отрицательное поле. Первая доля начинается
     ровно на линии 25% (отбивку с .services__body сняли ради того, чтобы
     доли легли на линии полотна), значит от кромки доли номеру надо уехать
     влево на свою ширину плюс базовый отступ: 2.5 + 1.25 = 3.75rem, и от
     линии до него остаётся ровно один отступ.

     Ширина фиксированная, 2.5rem, и без неё нельзя: у ячейки грида ширина
     иначе тянется по колонке, а отрицательное поле обязано знать, сколько
     отступать. В 2.5rem две цифры тринадцатого кегля помещаются с запасом
     вдвое, а прижаты они к правому краю коробки — то есть к линии.

     Ряд номера тот же, первый: без явного номера ряда автораскладка увела бы
     колонку набора вниз, во второй, — первая доля первого ряда была бы уже
     занята. Обе ячейки стоят в одной клетке и не спорят: номер из неё
     полностью выехал влево.

     По верху ряда номер встаёт сам, вместе с кадром и набором: у панели
     align-items: start, и своей отбивки сверху ему больше не нужно.

     Резать его больше нечем: обрезки у коробки стопки нет, а секция срезает
     только то, что вышло за экран.

     Гашение по --op вынужденное: внутри панели номер накрывался бы следующей
     картой, снаружи накрывать его нечем — без него все пять висели бы в поле
     разом. Переменную js/ui.js пишет на панель, значит номер обязан остаться
     её потомком, а не уехать в карту. */
  .service__index {
    grid-column: 1;
    grid-row: 1;
    width: 2.5rem;
    margin-left: -3.75rem;
    text-align: right;
    opacity: calc(1 - var(--op, 0));
  }

  /* Колонка набора — в том же первом ряду, что и номер с кадром. Явно, а не
     автораскладкой: первая доля первого ряда занята номером, и без указания
     колонка уехала бы во второй ряд. */
  .service__col {
    grid-row: 1;
  }

  /* Кадр поднят над линией набора на тот же общий подъём, что и метка блока:
     просьба была двигать их вместе.

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

     С transform и scale, которые кадру раздаёт приход карты, сдвиг не
     спорит: top живёт отдельно от них и складывается сверху. */
  .service__shot {
    position: relative;
    top: calc(-1 * var(--service-lift));
  }
}

/* ---- Наведение ----------------------------------------------------
   Зона — вся панель, как у образца, где карта работает целиком. Цвет
   раздаёт .accent-cycle по кругу палитры: карта читает готовый --accent и
   не знает, какой ей достался.

   Но чистый акцент тут не годится, и на фиолетовом тем более. Круг подобран
   под чёрное полотно; на белой бумаге половина его выцветала — оранжевый
   давал 2.4:1, голубой столько же, зелёный 2.6, — и акцент приходилось
   подмешивать к чёрному на 74%. На коралле не работало и это (сам коралл
   лежал в том же тёплом диапазоне), и доля упала до 30%.

   Фиолетовое полотно перевернуло направление смеси: мешать теперь надо не к
   чёрному, а к МЕЛУ. Полотно тёмное, и любая тёмная смесь на нём тонет —
   чистые цвета круга дают к #5b2cd1 от 1.78:1 (пурпурный, он же почти тот
   же тон) до 2.94:1 (оранжевый), то есть не проходит ни один.

   Доля — 28% акцента к мелу. Оттенок карты остаётся узнаваем — тёплый у
   оранжевой, холодный у голубой, — но звучит он высветленным тоном, а не
   заливкой. Контраст к фиолетовому у всех шести цветов круга выходит от
   4.61:1 (малиновый) до 5.43:1 (зелёный): проходит AA даже мелкий капс
   номера, а имя набрано дисплейным кеглем и имеет запас втрое. Долю тянуть
   вверх нельзя из-за малинового и пурпурного — они ближе всех к полотну по
   тону и упираются в порог первыми.

   Наведение при этом слегка ГАСИТ набор, а не разжигает: мел даёт 6.8:1,
   смесь — от 4.6 до 5.4. Так же было и на коралле (чёрный 7.6:1 против смеси
   4.8–5.7), поэтому поведение блока не изменилось — имя окрашивается, а не
   вспыхивает.

   Смесь объявлена на карте, а не на секции: --accent живёт на ней, и
   посчитайся значение выше — все пять карт получили бы первый цвет круга.

   Гасить прозрачностью, как это делает кит со своим списком услуг, нельзя:
   это читается как «строка выключилась». Вместо этого имя загорается. */
.service {
  --sv-accent: color-mix(in srgb, var(--accent) 28%, var(--c-chalk));
  /* Та же смесь, но вдвое слабее — для номера. Он набран мелким капсом в
     13px, а значит держит порог 4.5:1, тогда как имени, набранному
     дисплейным кеглем, довольно трёх. На фиолетовом хватало и общей смеси,
     но полотно теперь едет к оранжевому и к последней карте светлеет: там
     28% малинового дают номеру 4.1:1 — имени этого с запасом хватает,
     номеру нет. На 18% у него 4.7:1 в худшей точке пути и 5.3:1 в начале.

     Двумя смесями, а не одной ослабленной на всех: имя — то место, где
     наведение и должно быть видно, и гасить его ради чужого порога незачем. */
  --sv-accent-fine: color-mix(in srgb, var(--accent) 18%, var(--c-chalk));
}

/* Границы у панели больше нет, красить нечего: на наведении остаётся только
   набор. */
.service__panel:hover .service__name {
  color: var(--sv-accent);
}

.service__panel:hover .service__index {
  color: var(--sv-accent-fine);
}

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

   Возвращаем движение, но уже собственное: набор отстаёт от своей панели и
   догоняет её к посадке. Ровно тот же приём, которым в этом посадочнике
   входит первая секция из-под героя (js/hero-exit.js) — там содержимое
   тоже отстаёт от рамки. Разница в том, что здесь отставание не общее:
   куски трогаются по очереди, номер первым, описание последним, и карта
   собирается на глазах.

   Само движение — китовое проявление [data-reveal]: подъём, проступание и
   сход размытия одним жестом. Размытие и связывает приход с фоном — набор
   наводится на резкость поверх приехавшей плиты, а не просто выезжает.

   Вся доля берётся из --enter, которую на карту и так пишет js/ui.js на
   каждом кадре прокрутки. Своего таймера у движения нет: оно целиком
   привязано к положению карты, поэтому не отстаёт и не забегает при любой
   скорости колеса, а на обратной прокрутке разбирается само.

   За брейкпоинтом и в покое правила не действуют вовсе — там стопки нет,
   скрипт --enter не пишет, и набор обязан просто стоять на месте. Отсюда и
   запрос вокруг: без него пришлось бы сбрасывать движение в трёх местах
   (телефон, покой, отключённые скрипты), и каждый сброс — новый повод
   разъехаться.
   ------------------------------------------------------------------- */
@media (min-width: 721px) and (prefers-reduced-motion: no-preference) {
  .service__panel {
    /* Доля прихода, пересчитанная в долю выхода набора, четырьмя ступенями
       в порядке чтения карты: номер, кадр, имя, описание.

       Шаг 0.12, а не 0.08. Шире шаг — заметнее очередь: куски приходят
       по одному, а не почти вместе, и карта читается собирающейся. Заодно
       каждому достаётся меньше хода на себя (0.64 вместо 0.76), то есть
       своё движение он проделывает быстрее и приходит решительнее.

       Знаменатель — это 1 минус последняя задержка: так последняя ступень
       доходит до единицы ровно в тот кадр, когда карта садится на место, а
       не после него. */
    --in-1: clamp(0, calc(var(--enter, 1) / 0.64), 1);
    --in-2: clamp(0, calc((var(--enter, 1) - 0.12) / 0.64), 1);
    --in-3: clamp(0, calc((var(--enter, 1) - 0.24) / 0.64), 1);
    --in-4: clamp(0, calc((var(--enter, 1) - 0.36) / 0.64), 1);
  }

  .service__index {
    --in: var(--in-1);
  }
  .service__shot {
    --in: var(--in-2);
  }
  .service__name {
    --in: var(--in-3);
  }
  .service__text {
    --in: var(--in-4);
  }

  /* Подъём у каждого куска свой, а не общий на всех. Номер — мелкая
     подпись, ему хватает малого хода; кадру достаётся обычный. Имя и
     описание едут не отсюда, а словами, ниже — и там ход свой у каждого.

     Число берётся из --rise, чтобы правило осталось одно на двоих. */
  .service__index {
    --rise: 1.25rem;
  }

  .service__index,
  .service__shot {
    transform: translate3d(0, calc((1 - var(--in, 1)) * var(--rise, 2rem)), 0);
  }

  /* ---- Набор карты приходит словами ------------------------------
     Имя и описание ехали каждое одним куском, и на карте, где кадр
     раскрывается щелью, а номер выезжает своим ходом, текст оставался
     единственным местом без движения внутри себя.

     Режем на слова, а не на строки: строки тут не размечены руками, как в
     блоке «Кто мы», — описание течёт по мере, и где оно переломится, зависит
     от ширины окна. Разбор делает js/ui.js тем же splitWords, которым он
     разбирает заголовок героя, и кладёт каждому слову номер в --word-i.

     Очередь считается здесь и от --enter карты — то есть от прокрутки, а не
     по таймеру, как у героя: слова приходят, пока карта едет, и на обратной
     прокрутке разбираются сами.

     Считается доля слова не от --enter напрямую, а от --in — доли СВОЕГО
     куска, той самой третьей или четвёртой ступени очереди. Так пословный
     каскад ложится внутрь общего, а не рядом с ним: имя по-прежнему
     трогается третьим, описание четвёртым, и переставить их местами можно
     по-прежнему одной строкой выше.

     Формула одна на всех, а числа у имени и описания свои:
       --step — насколько отстаёт каждое следующее слово, в долях хода куска;
       --span — сколько того же хода достаётся слову на своё движение.
     Последнее слово обязано доехать до конца хода куска, отсюда и связь
     между ними: step * (слов - 1) + span ≈ 1.

     Маски с обрезкой здесь нет намеренно — по той же причине, что и у
     заголовка героя (см. .hero__word): Anton посажен с line-height 0.94, и
     коробка слова срезала бы хвосты. Подъём, проявление и сход размытия
     читаются не хуже, а ломаться им нечем. */
  /* Числа по умолчанию — описания: слов там под три десятка, шаг обязан быть
     мелким, иначе хвост очереди не успеет до конца хода. Имя ниже
     переопределяет все три. Своего размытия у слов описания нет, оно осталось
     на абзаце целиком: три десятка отдельных фильтров на кадр прокрутки — та
     цена, ради которой эффект не стоит затевать. */
  .service__word {
    --step: 0.018;
    --span: 0.5;
    --rise: 1.5rem;

    --w: clamp(
      0,
      calc((var(--in, 1) - var(--word-i, 0) * var(--step)) / var(--span)),
      1
    );

    display: inline-block;
    opacity: var(--w);
    transform: translate3d(0, calc((1 - var(--w)) * var(--rise)), 0);
  }

  /* Имя: слов два-три, поэтому шаг крупный и ход длинный — очередь должна
     читаться на такой короткой строке. Размытие тут по слову: их мало, и
     фильтр на трёх узлах ничего не стоит. */
  .service__name .service__word {
    --step: 0.09;
    --span: 0.6;
    --rise: 3.5rem;

    filter: blur(calc((1 - var(--w)) * 8px));
  }

  /* Размытие только на наборе. Кадру его не даём намеренно: фильтровать
     картинку на каждом кадре прокрутки — та цена, ради которой эффект не
     стоит затевать, и размытие всё равно читается на буквах, а не на
     фотографии. Кадру достаётся своё движение, ниже. */
  .service__index,
  .service__text {
    filter: blur(calc((1 - var(--in, 1)) * 6px));
  }

  /* Проступание осталось одному номеру.

     Кадру его не даём: он и так раскрывается срезом, и два проявления разом
     (щель раскрывается, и она же проступает) размазывают событие — а весь
     смысл правки был в обратном.

     Имя и описание проявляются словами, каждое своей долей. Абзацу при этом
     возвращается его собственная прозрачность — гашение по --op, пока карту
     накрывают: раньше правило прихода стояло ниже и затирало его, и накрытое
     описание не гасло вовсе. */
  .service__index {
    opacity: var(--in, 1);
  }

  /* КАДР: ОКНО СЖАТО, РОЛИК ВНУТРИ УВЕЛИЧЕН.

     Приём тот же, что у кадров блока «Кто мы», но числа здесь свои и куда
     крупнее. Там пара коробка/медиа работает по прокрутке через весь экран
     и должна быть еле заметной (MASK_SQUEEZE 0.03, MEDIA_ZOOM 0.09 в
     js/ui.js); здесь всё событие укладывается в приход одной карты, и на
     таком коротком ходу три процента не читаются вовсе — ровно то, из-за
     чего у прихода «не было импакта».

     ОКНО режется clip-path'ом, а не сжимается масштабом. Разница
     принципиальная: масштаб сплющил бы и сам ролик — картинка поехала бы
     по пропорциям, — а срез оставляет её нетронутой и просто показывает
     меньше. Кадр приходит горизонтальной щелью в треть высоты (сверху и
     снизу отрезано по 34%, с боков по 8%) и раскрывается до полного
     квадрата.

     Раскрытие идёт по ease-out, а не ровно: --open это доля, у которой
     съеден разгон (1 - (1-t)²). Окно раскрывается решительно и потом
     дотягивает — так рывок приходится на начало прихода, а не размазан по
     всему ходу.

     РОЛИК внутри садится из увеличения в 1.32 к единице — линейно, то есть
     доезжает ровно к посадке карты. Пара тем и живёт: окно уже раскрылось, а
     ролик всё ещё едет, и последние проценты хода читаются как оседание
     кадра, а не как остановка.

     Тридцать два процента увеличения кадр выдерживает: файл 640x640, а
     показывается он максимум в 12rem, то есть даже в увеличении картинке
     хватает своих пикселей.

     scale и clip-path, а не transform: у коробки transform занят подъёмом
     каскада, а отдельные свойства накладываются поверх, не затирая его. */
  .service__shot {
    --open: calc(1 - (1 - var(--in, 1)) * (1 - var(--in, 1)));

    clip-path: inset(
      calc((1 - var(--open)) * 34%) calc((1 - var(--open)) * 8%)
    );
  }

  .service__shot-media {
    scale: calc(1 + 0.32 * (1 - var(--in, 1)));
  }

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

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

     Доля --op своя у каждой панели, её пишет js/ui.js: ноль — карта открыта,
     единица — закрыта следующей. Последней накрывать нечем, она остаётся на
     нуле и не гаснет никогда. */
  .service__panel {
    opacity: calc(1 - var(--op, 0));
    scale: calc(1 - 0.04 * var(--op, 0));
  }
}

/* ============================================================
   Цифры
   Список носит .accent-cycle: цвет каждой строке раздаётся по кругу из
   палитры, элемент читает готовый --accent и не знает, какой именно ему
   достался. Переставить блок в разметке — цвет переедет сам. Тем же
   приёмом покрашены строки услуг выше.
   ============================================================ */
.stat__value {
  color: var(--accent);
}

/* ============================================================
   Ростер
   Заголовок слева, пилюльные табы справа — прижаты к базовой линии
   подводки, чтобы ряд читался как одна строка над сеткой.
   ============================================================ */
.roster__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-lg);
  flex-wrap: wrap;
}

.roster__tabs {
  flex: 0 0 auto;
}

/* Плитка таланта: вместо фотографии — инициалы в Anton на цветном поле.
   Квадрат, а не портрет: инициалы в портретной рамке висят в пустоте.
   Цвет плитке раздаёт .accent-cycle по кругу — восемь карточек набирают
   ровно тот коллаж из флэтовых блоков, что на референсе. */
.talent__mark {
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: clamp(3rem, 6vw, 5.5rem);
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--fg-on-accent);
  background-color: var(--accent);
  user-select: none;
}

.talent__meta {
  margin-top: -0.375rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* Скрытие по фильтру: сетка сама переукладывается, анимировать нечего. */
.talent[hidden] {
  display: none;
}

/* ============================================================
   Призыв
   Вторая тёмная плита. Единственное место, где кит разрешает
   центровку: заголовок и одна пилюля.
   ============================================================ */
/* Единственный большой флэтовый блок на странице — оранжевый, первый цвет
   референса. Набор на нём чёрный: это уже не тёмная плита, и китовый
   бумажный текст с неё не читался бы. */
.cta {
  background-color: var(--c-orange);
  color: var(--fg-on-accent);
}

.cta__inner {
  padding-block: var(--space-xxl);
  justify-items: center;
  text-align: center;
  gap: var(--space-md);
}

.cta__headline {
  font-family: var(--font-display);
  font-size: var(--fs-headline-md);
  line-height: var(--lh-headline);
  text-transform: uppercase;
  color: var(--fg-on-accent);
  max-width: 18ch;
}

.cta__paragraph {
  font-family: var(--font-body);
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body);
  color: var(--fg-on-accent-soft);
  max-width: 46ch;
}

.cta__btn {
  margin-top: var(--space-sm);
}

/* ============================================================
   Появление блоков
   Один общий приём на всю страницу: сдвиг на 20px и проявление,
   очередь задаётся в js/ui.js через --reveal-delay.
   ============================================================ */
[data-reveal] {
  opacity: 0;
  transform: translateY(20px);
}

[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  transition:
    opacity 700ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms),
    transform 700ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms);
}

/* ============================================================
   Кадры блока «Кто мы»: маска и масштаб
   Два эффекта на одной паре коробок, и разведены они намеренно.

   ПОЯВЛЕНИЕ. Маска раскрывается сверху вниз (clip-path из inset с полной
   нижней отсечкой в ноль) за 1100мс, а кадр внутри в это же время садится с
   1.06 до единицы за 1600 и подаётся снизу на два процента. Приём
   известный: кадр как будто уже жил своей жизнью до того, как его
   показали, — и это ровно то, чего не даёт простое проявление.
   Маска короче кадра: окно открывается быстро, а масштаб внутри доезжает
   чуть позже, уже в открытом окне. Порядок важнее длительностей — если
   уравнять, эффект развалится в обычное проявление.

   Кривая здесь своя, не общая. Вся страница построена на easeOutExpo
   (0.16, 1, 0.3, 1) — та проходит около восьмидесяти процентов пути за
   первую четверть времени. Для строки это хорошо: она стреляет и садится.
   Для кадра в пол-экрана тот же старт читается щелчком, и удлинять его
   бесполезно — резкой остаётся именно первая четверть.

   Кадрам отдан (0.4, 0, 0.2, 1) — с разгоном на входе, а не мгновенным
   стартом. Это и есть разница между «быстро» и «плавно»: у всех кривых
   семейства easeOut движение начинается на полной скорости, и сколько
   времени ни добавляй, первый кадр остаётся скачком. Разгон убирает
   именно его.

   И маска идёт не одна: вместе с ней кадр проявляется. Одна голая шторка
   даёт жёсткую кромку, которая едет по изображению, — проявление её
   размывает, и раскрытие перестаёт быть заметным само по себе.

   ПРОКРУТКА. Ведёт js/ui.js: чем дальше кадр от середины экрана, тем
   заметнее сжимается маска и тем сильнее увеличено изображение внутри.
   В середине экрана оба стоят на своих местах — кадр показан честным
   размером ровно тогда, когда на него и смотрят.

   Почему это не дерётся с появлением: скрипт пишет в отдельное свойство
   scale, а не в transform. Тот же приём, что у магнитной кнопки — в списке
   перехода scale не значится, поэтому идёт мгновенно и складывается с
   масштабом появления сам.

   И почему у ролика появилась обёртка: clip-path считается в координатах
   самого элемента, то есть уехал бы вместе с его масштабом. Обрезать и
   масштабировать обязаны разные коробки.
   ============================================================ */
[data-shot] {
  /* Очереди у кадров нет вовсе. Наблюдатель раздаёт пришедшей пачке шаг в
     70мс по порядку разметки, и кадры в блоке «Кто мы» стоят в её хвосте —
     ролик третьим, широкий кадр пятым, то есть ждали бы 140 и 280мс. Но
     кадр и есть то, ради чего блок открывают: набор дочитывают, а картинку
     ждут. Поэтому кадры стартуют вместе с блоком, а очередь остаётся
     метке, заявлению и подписи. */
  --shot-delay: 0ms;

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

     --shot-ease здесь же. Общестраничный easeOutExpo кадру не подошёл —
     почему, написано в шапке блока. */
  --shot-open: 1100ms; /* маска */
  --shot-fade: 820ms; /* проявление */
  --shot-zoom: 1600ms; /* масштаб внутри */
  --shot-ease: cubic-bezier(0.4, 0, 0.2, 1);

  overflow: hidden;
  clip-path: inset(0 0 100% 0);
}

[data-shot].is-revealed {
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--shot-open) var(--shot-ease) var(--shot-delay);
}

/* Подъём общего приёма коробке гасим: она занята маской, и translateY
   сдвинул бы окно вместе с кадром. А вот проявление оставляем — но своё,
   короче маски: оно снимает жёсткость кромки, не отменяя раскрытия. */
[data-shot][data-reveal] {
  opacity: 0;
  transform: none;
}

[data-shot][data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  transition: opacity var(--shot-fade) var(--shot-ease) var(--shot-delay);
}

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

   Всё одним transform: скрипт прокрутки пишет в отдельное свойство scale
   (js/ui.js), поэтому здесь можно распоряжаться transform целиком. */
[data-shot-media] {
  transform: scale(1.06) translateY(2%);
  transform-origin: 50% 50%;
}

[data-shot].is-revealed [data-shot-media] {
  transform: none;
  transition: transform var(--shot-zoom) var(--shot-ease) var(--shot-delay);
}

/* Построчный каскад. Ставится на [data-lines]: js/ui.js режет абзац по
   ручным переносам и заворачивает каждую строку в .reveal-line. Пускает
   каскад ближайший предок с data-reveal — тот самый класс is-revealed, что
   вешает наблюдатель, поэтому отдельного механизма запуска здесь нет.

   Само движение то же, что у героя: подъём, проявление и сход размытия. Шаг
   между строками крупнее, чем между словами в заголовке (110 против 85), —
   строка и читается дольше слова.

   Сам абзац при этом стоит: is-split снимает с него подъём общего приёма,
   иначе блок ехал бы целиком, а строки внутри него — ещё раз по отдельности. */
[data-reveal].is-split.is-revealed {
  opacity: 1;
  transform: none;
}

.reveal-line {
  --line-delay: calc(var(--reveal-delay, 0ms) + var(--line-i, 0) * 110ms);

  display: inline-block;
  opacity: 0;
  transform: translateY(0.4em);
  filter: blur(10px);
}

[data-reveal].is-revealed .reveal-line {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition:
    opacity 820ms cubic-bezier(0.16, 1, 0.3, 1) var(--line-delay, 0ms),
    transform 820ms cubic-bezier(0.16, 1, 0.3, 1) var(--line-delay, 0ms),
    filter 820ms cubic-bezier(0.16, 1, 0.3, 1) var(--line-delay, 0ms);
}

/* ============================================================
   Появление героя
   Первый экран из общего приёма выведен: наблюдатель ему не
   подходил. Его нижняя кромка обрезана на 12% высоты окна, чтобы
   блок считался пришедшим, только когда он заметно вошёл в кадр,
   — а кнопка героя живёт как раз в этой полосе и не пересекалась
   с ней никогда. Показывала её страховка на три секунды, отсюда и
   была задержка. Теперь герой играет по расписанию из js/ui.js
   сразу после того, как уходит шторка, а --reveal-delay каждому
   элементу проставляется числом.

   Движение общее для всех троих: подъём, проявление и сход
   размытия. Размытие и есть то, что связывает появление с фоном —
   элементы наводятся на резкость поверх текущего поля шума, а не
   просто выезжают.
   ============================================================ */

/* Ряд навигации: знак, пункты меню, часы. */
.hero__nav [data-reveal] {
  opacity: 0;
  transform: translateY(0.75rem);
  filter: blur(8px);
}

.hero__nav [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition:
    opacity 640ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms),
    transform 640ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms),
    filter 640ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms);
}

/* Заголовок едет не целиком, а словами: js/ui.js разбирает строку и
   заворачивает каждое слово в .hero__word (класс is-split — признак,
   что разбор состоялся; без него заголовок появляется как раньше).

   Маски с обрезкой здесь намеренно нет. Anton посажен с line-height
   0.94, строчные «g» и «p» уходят ниже базовой линии, и коробка слова
   срезала бы им хвосты в покое. Каскад из подъёма, проявления и схода
   размытия читается не хуже, а сломаться ему нечем.

   Очередь считается прямо в задержке: общий старт заголовка плюс
   порядковый номер слова, который скрипт кладёт в --word-i. */
.hero__headline.is-split[data-reveal] {
  opacity: 1;
  transform: none;
}

.hero__word {
  --word-delay: calc(var(--reveal-delay, 0ms) + var(--word-i, 0) * 85ms);

  display: inline-block;
  opacity: 0;
  transform: translateY(0.34em);
  filter: blur(16px);
}

.hero__headline.is-revealed .hero__word {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition:
    opacity 900ms cubic-bezier(0.16, 1, 0.3, 1) var(--word-delay, 0ms),
    transform 900ms cubic-bezier(0.16, 1, 0.3, 1) var(--word-delay, 0ms),
    filter 900ms cubic-bezier(0.16, 1, 0.3, 1) var(--word-delay, 0ms);
}

/* Кнопка. К общему движению добавлен масштаб: пилюля чуть подрастает
   до своего размера, а не подъезжает снизу как строка текста, — и
   собственная кривая с более коротким хвостом дотягивает её до места
   без долгого доползания. Магниту это не мешает: смещение он пишет в
   отдельное свойство translate.

   В покое у кнопки filter: none, а НЕ blur(0), и это не косметика. Кнопка
   стеклянная, то есть носит backdrop-filter, а любой filter кроме none
   заводит на элементе отдельный контекст наложения и в части движков
   отрезает подложку, которую стекло должно замутнять. Так фильтр живёт
   ровно те 700мс, что идёт появление, и дальше кнопки не касается.

   Переход при этом работает: интерполируя к none, браузер подставляет
   нейтральный список функций, то есть тот же blur(0). */
.hero__cta[data-reveal] {
  opacity: 0;
  transform: translateY(0.75rem) scale(0.94);
  filter: blur(6px);
}

.hero__cta[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  filter: none;
  transition:
    opacity 700ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms),
    transform 700ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms),
    filter 700ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms);
}

/* ============================================================
   Подвал
   Раскладка перенесена с noth.in (разбор — в комментарии над
   разметкой), а выезд — из посадочника Frogge.

   Три яруса: ряд призыва, знак во всю ширину, мелкая строка.
   ============================================================ */

/* Раскладку кита снимаем целиком. У него подвал — одна строка с волосяной
   линией сверху: ссылки слева, копирайт справа. Здесь он разворачивается в
   полосу из трёх ярусов, и flex со space-between поставил бы их в ряд.

   Линия сверху тоже уходит. Границей подвала работает сам знак: он занимает
   всю ширину контейнера, и отделять его от страницы ещё и линией — то же
   самое, что подчёркивать заголовок.

   ---- Выезд из-под страницы ----------------------------------------
   Механика посадочника Frogge (js/footer-reveal.js там, у нас файл того же
   имени), а туда она попала со studiodado.com.

   Подвал лежит НИЖЕ страницы слоем: .page-body непрозрачна и поднята на
   z-index: 1, подвал идёт следующим за ней и остаётся на нуле. Поднятый на
   четверть своей высоты, он спрятан за её нижней кромкой и выезжает из-под
   неё, пока садится на место. По ходу он идёт медленнее страницы — отсюда
   и ощущение, что контент уползает вверх, открывая плиту под собой.

   Своё позиционирование подвалу нужно ровно для этого: без него плита
   красилась бы в фазе обычных блоков, а с трансформом — наоборот, поверх
   .page-body, потому что идёт в разметке после неё. Нулём мы её туда и
   прижимаем.

   Плита ЧЁРНАЯ, тем же --c-void, что и полотно страницы. Раньше тут стояла
   --c-raise, приподнятая поверхность палитры: она на пару процентов светлее
   полотна, и ход читался ещё и по тону — из-под страницы выезжала заметно
   другая поверхность.

   Тона больше нет, и это осознанный размен. Выезд теперь держится на одном
   движении: страница уходит вверх быстрее подвала, и из-под её кромки
   выкатывается содержимое плиты — кнопка, знак во всю ширину, мелкая
   строка. Кромка полотна при этом не видна вовсе, то есть подвал не
   «выезжает плитой», а проявляется. На чёрном это и требовалось: страница
   заканчивается не другой поверхностью, а тем же провалом, в котором
   остаётся один знак.

   Зерна тут нет — как и нигде на странице сейчас (см. «Зерно» в :root):
   оба полотна чистые, поэтому чёрный подвала и чёрный страницы совпадают
   не приблизительно, а ровно. Вернётся зерно на страницу — плите оно
   полагается тем же тайлом и по той же причине: без него стык проступит
   гладким пятном там, где всё остальное в крупе. */
.footer {
  position: relative;
  z-index: 0;
  border-top: 0;
  background-color: var(--c-void);
  padding-block: var(--space-section) var(--space-lg);

  display: flex;
  flex-direction: column;

  /* Четыре строки ниже гасят кит. У него .footer — горизонтальный ряд
     (display: flex, align-items: center, justify-content: space-between,
     gap, flex-wrap: wrap), и раньше здесь стоял display: block, который их
     обезвреживал: на не-флексе они просто не действуют. Колонка их
     оживляет, поэтому каждую снимаем вручную.

     Опаснее всех flex-wrap: в колонке с заданной высотой он разложил бы
     содержимое во второй столбец, как только оно перестало помещаться. */
  align-items: stretch;
  justify-content: flex-start;
  gap: 0;
  flex-wrap: nowrap;
}

/* Свободную высоту забирает разрыв над знаком (см. .footer__mark): ряд
   призыва встаёт по верхней кромке, знак с мелкой строкой — по нижней.
   Без свободной высоты auto схлопывается в ноль, и подвал ведёт себя как
   раньше — поэтому правило общее, а не в запросе. */
.footer > .container {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  /* Над сеткой подвала. Позиционирование тут только ради числа: без него
     абсолютная сетка вышла бы поверх непозиционированного контейнера и
     прошла бы линией по знаку. */
  position: relative;
  z-index: 1;
}

/* Сетка подвала — под содержимым. Нуль, а не отрицательное число: полотно
   плиты непрозрачно и лежит на самом подвале, отрицательное утопило бы
   линии под него. */
.footer .grid-lines {
  z-index: 0;
}

/* ============================================================
   Шлейф звёзд за курсором
   Механика — js/footer-flair.js, перенос пена GreenSock «flair cursor
   follower». Здесь только слой и габарит звезды.
   ============================================================ */

/* Поверх всего содержимого подвала: сетка на нуле, контейнер на единице,
   слой на двойке. Выше него на странице остаётся только курсор-кружок
   (150) — шлейф идёт ЗА курсором и накрывать его не должен.

   Событий слой не ловит вовсе. Без этого он накрыл бы собой весь подвал, и
   ни кнопка, ни пункты, ни почта не нажимались бы; заодно pointermove
   продолжает всплывать от содержимого к .footer, где его и слушает скрипт.

   overflow: hidden — это конец падения. Звезда уходит на 1.2 высоты плиты
   вниз, то есть заведомо за её нижнюю кромку; без обрезки она удлинила бы
   страницу и на секунду завела бы полосу прокрутки. */
.footer__flair {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
}

/* Габарит звезды меряется шириной окна: на 1440 выходит около 72px, на
   узком — 56. Пен показывает свои по 50px, но там они летают по пустой
   странице, а тут под ними знак во всю ширину и ряд подписей — слишком
   мелкая читалась бы сором, слишком крупная перекрывала бы подвал.

   Позицию, поворот, масштаб и прозрачность ведёт скрипт. В покое звезда
   прозрачна и стоит в левом верхнем углу слоя: до первого взмаха мышью
   ей там и место, а нулевая прозрачность держит её невидимой, не выводя
   из раскладки (display: none снял бы и ленивую загрузку).

   will-change не ставим. Их дюжина, и держать дюжину постоянных слоёв
   ради движения, которое идёт секунду и от случая к случаю, дороже, чем
   отдать браузеру решать самому: трансформ он и так поднимает в слой на
   время анимации, а по её концу скрипт стиль снимает начисто. */
.footer__flair-shape {
  position: absolute;
  left: 0;
  top: 0;
  width: clamp(3.5rem, 5vw, 4.5rem);
  height: auto;
  opacity: 0;
  user-select: none;
  -webkit-user-drag: none;
}

/* Плита ровно в экран — и это не про вид, а про сам ход. Выезд поднимает
   подвал на четверть его высоты (js/footer-reveal.js), поэтому высота плиты
   и есть амплитуда хода. По высоте контента выходило около 520px, то есть
   ход в 130: движение заметное, но мелкое. Плита в экран даёт те же четверть
   уже от вьюпорта, и из-под фиолетового блока выкатывается полотно во весь
   экран, а не полоса. Столько же и у Frogge, откуда взята механика.

   min-, а не фиксированная высота: на узком экране содержимое бывает выше
   экрана, и подвал должен доращиваться, а не резаться.

   Запрос ровно тот же, которым закрыт сам выезд в js/footer-reveal.js:
   721px и не в покое. Высота эта нужна ради хода и без хода бессмысленна —
   на телефоне она дала бы пустой экран с кнопкой наверху и знаком внизу,
   а выезда всё равно нет. Слой трансформа просим здесь же и по той же
   причине: где ход не идёт, там и слой ни к чему. */
@media (min-width: 721px) and (prefers-reduced-motion: no-preference) {
  .footer {
    min-height: 100svh;
    will-change: transform;
  }
}

/* Ряд призыва — ЗЕРКАЛО РЯДА НАВИГАЦИИ. Раньше это был флекс со
   space-between: кнопка слева, пункты справа, и посередине пустота во
   полстраницы. Теперь ряд садится на ту же четырёхколоночную сетку, что и
   линии подвала: кнопка в первой колонке, два пункта во второй и третьей,
   часы в четвёртой — ровно расклад .hero__nav.

   Колонки сходятся с линиями пиксель в пиксель, и это не совпадение:
   .grid-lines__inner отбит от краёв тем же --space-gutter, что стоит
   паддингом у .container, а линии лежат на 25/50/75%. Ряд внутри
   контейнера получает ту же ширину — значит, и те же четверти.

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

   Первый и последний экран страницы читаются одним кадром: сверху знак и
   те же три подписи, снизу кнопка и они же. Ради этого ряд и переписан. */
.footer__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  flex-wrap: wrap;
}

@media (min-width: 721px) {
  .footer__row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    /* Нулевой обязателен: китовый gap здесь съел бы ширину колонок, и они
       разъехались бы с линиями. Та же причина, что у .hero__nav. */
    gap: 0;
    /* По базовой линии, как в шапке: подпись кнопки, оба пункта и время
       стоят на одной строке, хотя пилюля выше их всех. */
    align-items: baseline;
  }

  /* Пункты раскрываются в сетку ряда — каждый в свою колонку. */
  .footer__links {
    display: contents;
  }

  /* Пункт начинается в 20px вправо от своей линии (1.25rem — те же 20px на
     бейзлайне, но едут вместе с автоскейлом). Число из шапки. */
  .footer__links > li {
    padding-left: 1.25rem;
  }

  /* Пилюля — грид-элемент, а грид приводит inline-flex к обычному flex и
     тянет его на всю колонку. Прижимаем к началу: колонка ~330px, кнопка
     ~180px, растянутая читалась бы плашкой, а не пилюлей. */
  .footer__cta {
    justify-self: start;
  }
}

/* Пункты подвала набраны ПУНКТАМИ МЕНЮ. У кита .footer__link — 13px
   пепельным нижним регистром, у .nav__link — капс 0.8125rem с трекингом
   0.06em цветом набора; берём второе, иначе «те же подписи» в двух рядах
   держались бы только на словах.

   Кегль в rem, а не в китовых 13px: страница на автоскейле, и подписи
   обязаны расти вместе с ней — в шапке ровно поэтому же.

   Отклика на наведение нет, как и в шапке: признак наводимости на странице
   общий и один — курсор-кружок разбухает втрое на любой ссылке
   (js/blob-cursor.js). Китовый перекрас в ink стал пустым сам собой (базовый
   цвет теперь он и есть), а переход снимаем явно, чтобы не висел тот,
   которому нечего играть.

   Скоупим рядом: тем же классом набрана почта в мелкой строке под знаком, и
   ей полагается ровно китовое — 13px пепельным. Она не пункт меню. */
.footer__row .footer__link,
.footer__row .footer__link:hover,
.footer__row .footer__link:focus-visible {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-ink);
  transition: none;
}

/* Кнопка магнитится, как и в герое: на странице их две, и вести себя они
   обязаны одинаково. Запас вокруг нужен по той же причине — зона
   притяжения выходит за кнопку, и на возврате пружиной она заходит за свои
   габариты. */
.footer__cta {
  margin: 0.5rem 0;
}

/* Заливка — ТО ЖЕ СТЕКЛО, ЧТО У КНОПКИ ГЕРОЯ, значение в значение (разбор
   каждого слоя — там, над .hero__cta). На странице этих кнопок две, они
   первое и последнее действие на ней, и раз уж обе магнитятся одинаково,
   выглядеть они обязаны одним предметом.

   До этого тут стоял .btn--primary из кита: мел заливкой, чёрный набор.
   На чёрной плите он читался единственным светлым пятном и перебивал знак,
   ради которого подвал и собран.

   ЧТО СТЕКЛО ДЕЛАЕТ ЗДЕСЬ, ГДЕ ЗА НИМ ЧЁРНОЕ. В герое оно замутняет кадр
   и красится им; тут замутнять нечего — под кнопкой ровное полотно и пара
   волосяных линий сетки. Работает поэтому не backdrop-filter, а два верхних
   слоя: блик и кромка. Поверх нуля блик даёт полосу от 41 до 10 по каналу
   (пилюля читается изогнутой, а не плоской), кромка мелом на 0.3 — 73,
   верхний внутренний волосок света — 97. Этого хватает: на чёрном светлая
   кромка и есть вся граница, а сама пилюля остаётся тёмной и знак не
   перебивает.

   backdrop-filter при этом оставлен, и не «за компанию»: сетка подвала
   лежит под содержимым (см. .footer .grid-lines), кнопка на неё наезжает,
   и на магнитном ходу линия под стеклом обязана размываться вместе с ним —
   иначе видно, что стекло ненастоящее. По той же причине сюда перенесён и
   запасной вариант из @supports ниже.

   Смена цвета на ховере погашена, как и в герое: обратную связь даёт
   магнит, перекрас поверх него читается рябью. Четыре состояния поэтому и
   перечислены вместе — переходу кита по background-color нечего играть. */
.footer__cta,
.footer__cta:hover,
.footer__cta:focus-visible,
.footer__cta:active {
  background-color: rgba(var(--c-void-rgb), 0.22);
  background-image: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.16) 0%,
    rgba(255, 255, 255, 0.04) 48%,
    rgba(255, 255, 255, 0.09) 100%
  );
  -webkit-backdrop-filter: blur(1rem) saturate(1.4);
  backdrop-filter: blur(1rem) saturate(1.4);
  color: var(--c-chalk);
  border-color: rgba(var(--c-chalk-rgb), 0.3);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.38),
    inset 0 -1px 0 rgba(var(--c-void-rgb), 0.3),
    0 0.5rem 1.5rem rgba(var(--c-void-rgb), 0.35);
}

/* Без backdrop-filter теряется только размытие сетки под пилюлей — блик и
   кромка на месте, и кнопка остаётся собой. Плотность тени поэтому НЕ
   поднимаем, в отличие от героя: там за стеклом кадр, и без замутнения
   подпись легла бы прямо на шейдер. Здесь за ним чёрное, поднимать нечего.
   Гасим только сам фильтр, чтобы Safari до 18 не держал слой впустую.

   Проверяем оба написания: до 18-й версии Safari знает лишь префиксное. */
@supports not (
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
) {
  .footer__cta,
  .footer__cta:hover,
  .footer__cta:focus-visible,
  .footer__cta:active {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* Знак во всю ширину контейнера — то, ради чего подвал и переносился.

   Разрыв над ним двойной, и каждая половина отвечает за своё. Отбивка
   меряется окном, а не числом: на невысоком экране полтораста пикселей
   воздуха между рядом призыва и знаком съели бы сам знак. Она же —
   гарантированный минимум. Всё, что остаётся сверх неё до высоты плиты,
   забирает auto: знак с мелкой строкой садятся по нижней кромке, ряд
   призыва остаётся у верхней.

   Порядок именно такой — auto маржином, минимум подложкой. Наоборот не
   выйдет: auto схлопывается в ноль, когда свободной высоты нет, и вместе с
   ним пропал бы минимум. */
.footer__mark {
  margin-top: auto;
  padding-top: clamp(4rem, 12svh, 9.5rem);
}

/* width: 100% и height: auto — вся раскладка знака. Высота набегает из его
   пропорции, и подгонять её нельзя: знак длинный (2037×238), на ширине
   контейнера выходит около 133px и читается полосой. Атрибуты width/height
   в разметке держат место до загрузки файла. */
.footer__logo {
  display: block;
  width: 100%;
  height: auto;
}

/* Мелкая строка под знаком. Набрана теми же 13px, что пункты подвала у кита,
   и по базовой линии: слева копирайт, справа почта.

   Отбивка от знака выросла с --space-md (20px). Знак на ширине контейнера
   набирает около 133px высоты и читается сплошной полосой; двадцать
   пикселей под ней вставали вплотную к её нижней кромке, и мелкая строка
   выглядела нижним краем знака, а не отдельным ярусом. Тут же и ответ на
   «поднять знак выше»: ярус прижат к нижней кромке плиты целиком (margin-
   top: auto у .footer__mark), поэтому воздух, добавленный ПОД знаком,
   поднимает именно знак — они расходятся, а не съезжают вместе.

   Меряется окном по той же причине, что и разрыв над знаком: на невысоком
   экране фиксированные полсотни пикселей забирались бы у самого знака. */
.footer__info {
  margin-top: clamp(1.75rem, 4svh, 3.25rem);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}

/* Мелкая строка встаёт на ту же четвёрку колонок, что и ряд призыва, — ради
   третьей подписи посередине. Флексом её было не поставить: space-between
   развесил бы троих по краям и по центру ряда, то есть по случайным точкам,
   а нужна конкретная — правее первой линии сетки.

   Копирайт садится в первую колонку сам (авторазмещение), подпись места
   назначена во вторую явно, почта — в четвёртую. Так под кнопкой стоит
   копирайт, под первым пунктом меню — «Cyprus based», и оба яруса подвала
   читаются одной сеткой, а не двумя разными раскладками.

   ПОЧТА ОСТАЛАСЬ РОВНО ГДЕ БЫЛА: justify-self: end прижимает её к правому
   краю контейнера, как раньше это делал space-between. Часы этажом выше
   стоят по началу своей колонки, и соблазн выровнять почту под них есть, но
   тогда справа от неё повисла бы четверть пустоты на самом низу страницы. */
@media (min-width: 721px) {
  .footer__info {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
    align-items: baseline;
  }

  /* Тот же отступ от своей линии, что у пунктов меню и у часов. */
  .footer__where {
    grid-column: 2;
    padding-left: 1.25rem;
  }

  .footer__info > .footer__link {
    grid-column: 4;
    justify-self: end;
  }
}

/* На телефоне — колонка, по той же причине, что и ряд призыва: переносом
   трое коротких строк раскладываются от ширины экрана как придётся. Шаг
   мельче, чем в ряду: это одна мелкая строка, разложенная в три, а не три
   самостоятельных яруса. */
@media (max-width: 720px) {
  .footer__info {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-xs);
  }
}

/* ============================================================
   Отклик на ширину
   Широкие ступени кита (1024 / 720 / 480) остаются в силе — здесь
   только то, что появилось вместе с фулскрином.
   ============================================================ */

@media (max-width: 900px) {
  .roster__head {
    align-items: flex-start;
    flex-direction: column;
  }
}

@media (max-width: 720px) {
  /* Ступень кита по вертикальному ритму: наши :root перекрыли его,
     поэтому повторяем здесь. Поля контейнера уже фулскрин-clamp,
     его нижняя граница — те же 20px. */
  :root {
    --space-section: 4.5rem;
  }

  .plate__inner {
    padding: 2.25rem 1.5rem 2.5rem;
  }

  .brand__logo {
    height: 1.125rem;
  }

  /* Блок «Кто мы» разворачивается в одну колонку: на четверть экрана ни
     ролик, ни строки уже не садятся, а выравнивание по низу теряет смысл,
     когда колонка одна.

     Переносы гасим: они расставлены под широкую раскладку и здесь рвали бы
     фразу посреди узкой строки. display: none на <br> — единственный способ
     их выключить, не трогая разметку. */
  .about__grid {
    grid-template-columns: 1fr;
    row-gap: var(--space-lg);
  }

  /* Метка в этот список больше не входит: она лежит вне сетки, обычной
     строкой над ней, и отбивку держит собственным полем. Приводим его к
     телефонному шагу — на широком экране гэп сетки --space-xl, здесь
     --space-lg, и поле метки обязано идти с ними в ногу. */
  .about__label {
    margin-bottom: var(--space-lg);
  }

  .about__statement,
  .about__aside,
  .about__frame {
    grid-column: 1;
    grid-row: auto;
    padding-left: 0;
  }

  /* Прижим пары к линии 25% на телефоне разбираем: колонка одна, линий под
     блоком нет, и сжатая по содержимому коробка у правого края читалась бы
     съехавшей. Оба свойства возвращаем к тому, что у грид-элемента по
     умолчанию. */
  .about__aside {
    justify-self: stretch;
    padding-right: 0;
  }

  /* Правило по атрибуту, а не по именам блоков: [data-lines] и означает
     «строки этого абзаца расставлены руками», а на телефоне такие переносы
     гасятся везде одинаково. */
  [data-lines] br {
    display: none;
  }

  /* Раз переносы погашены, строки должны течь одна за другой и переноситься
     по мере. Инлайн-блоком они бы этого не сделали — каждая осталась бы
     отдельной коробкой. Подъём при этом отваливается: к строчным элементам
     transform не применяется. Остаются проявление и сход размытия, и этого
     на телефоне достаточно. */
  [data-lines] .reveal-line {
    display: inline;
  }

  /* Услуги. Стопка на телефоне не работает: карта ростом в экран и залипший
     блок превратили бы его в пять полноэкранных слоёв, между которыми не
     прокрутиться. У образца ровно так же — всё залипание там под lg:. Свой
     запрос стопка и не выполняет: он начинается на 721px.

     Здесь остаётся только раскладка. Колонка одна и во всю ширину: метка
     встаёт над картами обычным заголовком блока, номер уходит в узкую первую
     долю панели, остальное — под именем. */
  .services__grid {
    grid-template-columns: 1fr;
    row-gap: var(--space-md);
  }

  .services__body {
    grid-column: 1;
  }

  /* Разбирать стопку тут больше нечего: залипание, дорожка, поле под номер
     и увод карт вниз объявлены в своём запросе, а он до телефона не
     доходит. Карты и так идут потоком, одна за другой.

     Остаётся собственно телефонная раскладка панели: колонка номера узкая
     и фиксированная — четверть узкого контейнера ушла бы под две цифры. */
  .service__panel {
    grid-template-columns: 2.5rem 1fr;
    row-gap: var(--space-md);
    min-height: 0;
    padding-block: var(--space-lg);
  }

  /* Кадр возвращается из третьей доли во вторую. На телефоне долей всего
     две, и третьей у панели нет вовсе — оставь мы её номер, кадр завёл бы
     себе неявную колонку справа и утащил бы туда всю раскладку. Первая же
     доля узкая и занята номером (2.5rem): в ней кадр сплющило бы в полоску.
     Встаёт он второй долей первой строкой — то есть по-прежнему перед
     именем, только сверху, а не слева.

     Поле от линии снято: линий тут нет вовсе, колонка одна, и отбивать кадр
     не от чего — он должен стоять по одному краю с именем и описанием. */
  .service__shot {
    grid-column: 2;
    max-width: 9rem;
    margin-left: 0;
  }

  /* Имя своей ячейки не имеет: оно лежит внутри колонки описания и едет
     вместе с ней. Отбивка от линии снята по той же причине, что и у кадра:
     линий на телефоне нет, колонка одна, и набор должен стоять по одному
     краю с кадром над ним. */
  .service__col {
    grid-column: 2;
    padding-left: 0;
    /* Сужение на пятую часть — приём десктопной композиции: там колонка
       набора не доходит до линии 75%. На телефоне линий нет, колонка одна и
       узкая, и отнимать у неё ещё пятую часть значило бы гнать описание
       колодцем. */
    width: auto;
  }

  /* Гашение описания завязано на стопку, а без неё панель ничем не
     накрывается: скрипт --op здесь не пишет, но подстрахуемся. Номер
     гасить незачем — его гашение объявлено в запросе стопки. */
  .service__text {
    opacity: 1;
  }

  /* На телефоне колонка одна и во всю ширину, поэтому 240px там заняли бы
     почти на весь экран. Возвращаем долю. */
  .about__clip {
    width: 55%;
  }

  /* Поля рамки героя фиксированные, а не в cqh. Число досталось от
     гамбургера: тот висел в конце <body>, и совпасть с рядом навигации по
     вертикали они могли только на общем значении. Гамбургера больше нет, но
     число оставлено как есть — менять поля героя на телефоне стоит отдельно
     и глазами, а не заодно с меню. */
  .hero__frame {
    padding-block: 1.5rem;
  }

  /* ---- Ряд навигации на телефоне -------------------------------------
     Четыре колонки тут не нужны и не помещаются: сетка страницы на телефоне
     всё равно не читается, а два пункта прекрасно встают в простой ряд.
     Знак слева, пункты справа.

     Это замена гамбургеру с панелью: те убраны целиком, и пункты вернулись
     в сам ряд. Кит гасит .nav__links на этой же ступени через display: none
     — правило ниже сильнее только порядком подключения, специфичность у них
     одинаковая, и переставлять файлы местами нельзя.

     МЕСТО ДЕЛИТСЯ НЕ ПОРОВНУ, И ЭТО ГЛАВНОЕ ЗДЕСЬ. Пункты — набор, они не
     умеют сжиматься иначе как переносом строки, поэтому им отдано ровно
     столько, сколько они просят (flex: none). Уступает знак: у него база
     9rem и разрешено ужиматься до 4.5rem. Картинка при этом не искажается —
     ширину ей задаёт коробка, а высота считается из пропорции файла. На
     360px по ширине ряд собирается с запасом, на 320px знак поджимается
     сам. Ни одного замера ширины набора в этой раскладке нет намеренно:
     считать advance-ширины Inter и надеяться, что не промахнулся, — плохая
     опора, а flex решает это без чисел.

     Выравнивание по базовой линии то же, что на десктопе: у картинки
     базовая линия — её нижняя кромка (правило для замещаемых элементов),
     у списка — базовая линия первой строки. Знак садится на строку пунктов
     сам. */
  .hero__nav {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: clamp(0.75rem, 3vw, 1.5rem);
  }

  .brand {
    flex: 0 1 9rem;
    min-width: 4.5rem;
  }

  /* Ширина от коробки, высота из пропорции: так знак ужимается без искажения
     и без max-width поверх заданной высоты (та бы его сплющила). */
  .brand__logo {
    width: 100%;
    height: auto;
  }

  .nav__links {
    display: flex;
    flex: none;
    gap: clamp(0.75rem, 3.5vw, 1.25rem);
  }

  /* Отбивка от линии сетки — десктопное правило: на телефоне линий под
     пунктами нет, а лишний паддинг съедал бы место, которого и так в обрез.
     Расстояние между пунктами держит gap ряда. */
  .nav__links > li {
    padding-left: 0;
  }

  /* Кегль отпущен вниз: на узком экране 13px в двух пунктах подряд не
     оставляют места знаку. Нижняя граница 11px — ниже капс с трекингом
     0.06em начинает читаться шумом. */
  .nav__link {
    font-size: clamp(0.6875rem, 3vw, 0.8125rem);
  }

  /* Часами жертвуем — так и было решено. Вчетвером набор в ряд не влезает
     даже без гамбургера: колонка на телефоне ~84px, а время с меткой пояса
     просит больше. Пилюля переехала в плиту призыва и подвал ещё раньше. */
  .nav__clock {
    display: none;
  }

  /* А в подвале часы остаются. Жертва выше — про ряд шапки: там четверо в
     строку не влезают, колонка на телефоне ~84px. В подвале ряд разложен в
     колонку и упирается не в ширину, а в высоту, которой на плите в целый
     экран с избытком.

     Правило идёт после китового display: none и той же весомости — спор
     решает порядок в файле, и он здесь наш. */
  .footer__clock {
    display: block;
    /* Отбивка от линии сетки — десктопное правило: на телефоне линий под
       рядом нет, а в колонке часы с отступом стояли бы уступом относительно
       кнопки и пунктов. Через переменную, а не padding-left: её же читает
       метка пояса, лежащая вне потока. */
    --clock-indent: 0px;
  }

  /* Ряд подвала на телефоне — колонка, а не флекс с переносом. Переносом
     трое (кнопка, пункты, часы) раскладывались бы как придётся: на 360px
     пункты уходят на вторую строку, а space-between растаскивает оставшуюся
     пару по краям — ряд каждый раз собирается по-новому от ширины экрана.
     Колонкой он собран одинаково на любой. */
  .footer__row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-md);
  }

  .cta__inner {
    padding-block: var(--space-xl);
  }
}

/* Экран ниже 520px в высоту (телефон в альбоме, ноутбучная «щель»):
   пропорция уже не спасает — у полей, кегля подписи и высоты кнопки есть
   свой пол, и ниже него набор перестаёт помещаться. Здесь честнее отпустить
   герой в собственную высоту и дать странице прокрутиться.
   container-type снимаем на inline-size: без него cqh резолвился бы против
   высоты, которой у секции больше нет. */
@media (max-height: 520px) {
  .hero {
    height: auto;
    min-height: 26rem;
    container-type: inline-size;
  }
}

/* Браузер без контейнерных запросов роняет все cqh-объявления, и набор
   вернётся к китовым размерам — а высота у секции при этом осталась бы
   жёсткой, и низ обрезало бы вместе с кнопкой (у героя стоит overflow:
   hidden ради холста). Возвращаем ему прежнее поведение: экран как пол,
   дальше секция растёт. */
@supports not (container-type: size) {
  .hero {
    height: auto;
    min-height: 100vh;
    min-height: 100svh;
  }
}

/* Пользователю, попросившему покой, страница отдаётся собранной:
   ни сдвигов, ни бегущего штриха, ни живого холста (последний гасит и
   js/hero-flow.js). */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  [data-reveal] {
    opacity: 1;
    transform: none;
  }

  /* Правила героя и кадров перечислены отдельно: они специфичнее общего
     [data-reveal] и без этого пересилили бы его прямо здесь. */
  .hero__nav [data-reveal],
  .hero__cta[data-reveal],
  .hero__word,
  .reveal-line,
  [data-shot][data-reveal],
  [data-shot-media] {
    opacity: 1;
    transform: none;
    filter: none;
    will-change: auto;
  }

  /* Маска раскрыта сразу, и скрипт по прокрутке её не трогает — он в покое
     не запускается. */
  [data-shot] {
    clip-path: none;
  }

  /* Стопку карт разбирать не нужно: её запрос сам требует
     prefers-reduced-motion: no-preference и сюда не доходит. Карты идут
     потоком, одна за другой, — ровно то, что здесь и нужно. */

  .hero__canvas {
    transition: none;
  }
}
