/* ═══════════════════════════════════════════════════════════════
   ПОЛИС — маркетинговый лендинг polis2050.com
   SITE-79, этап 3: скелет. Дизайн Кости Муравьёва (VisDev_04).

   Макет: 1920 × 5962.84. Сверялся с VisDev_04.png — у SVG сломан
   viewBox (объявлено 4494.33), нижняя треть при рендере обрезается.
   ═══════════════════════════════════════════════════════════════ */

/* ─────────────────────────── шрифты ─────────────────────────── */

/* Три гарнитуры Кости. Сабсеттинг не делали: файлы по 5–13 КБ,
   урезание рисковало выбросить нужный символ. */

@font-face {
  font-family: 'POLIS Display';
  src: url('fonts/polis-display-371465.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}

@font-face {
  font-family: 'POLIS Text Head';
  src: url('fonts/polis-text-head-a3e358.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}

@font-face {
  font-family: 'POLIS Text';
  src: url('fonts/polis-text-6e9402.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

/* Символов, которых в гарнитурах Кости нет, очень много: у Display
   отсутствует 80 из 95 ходовых знаков, у Text_Head 78, у Text 67 —
   нет ни одного тире, ни многоточия, ни №, ни %, ни валют, ни
   математики, ни неразрывного пробела.

   Перечислять их в unicode-range бессмысленно и хрупко: любой не
   учтённый заранее символ дал бы .notdef при первой же правке текста.
   Вместо этого полагаемся на штатный фолбэк по font-family — браузер
   сам берёт недостающий глиф из следующего шрифта списка. Проверено
   на 56 знаках во всех трёх гарнитурах: квадратов нет ни одного.

   Стек подобран так, чтобы подмена нашлась на любой системе:
   Helvetica Neue — macOS/iOS, Segoe UI — Windows, Roboto и Noto Sans —
   Android и Linux.

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

/* ─────────────────────────── токены ─────────────────────────── */

:root {
  /* цвета — вынуты из CSS внутри VisDev_04.svg */
  --bg:        #0a0c0b;   /* фон страницы, пилюля кнопки тизера */
  --header-bg: #020202;   /* плашка шапки (Maket_2.svg, класс st25) — темнее --bg */
  /* Правка арт-директора, круг 4: --stroke/--accent сверены заново по
     Maket_2.svg (класс .st1 обводки #556484, текст кнопки «Доступ…»
     #a8b7de) — расхождение с прежними значениями на 1 в младшем байте,
     но раз замер, значит замер. */
  --stroke:    #556484;   /* обводка всех рамок */
  --fg:        #ffffff;   /* заголовки, имена */
  --text:      #b3b3b3;   /* основной текст */
  --menu:      #c6c6c6;   /* пункты меню */
  --accent:    #a8b7de;   /* кнопка закрытого раздела */

  --font-fallback: 'Helvetica Neue', Arial, 'Segoe UI', Roboto, 'Noto Sans', sans-serif;

  /* Дизайн-ширина макета 1920, контейнер капнут на 1600 (плотность
     картинок — см. ниже). До правок (пакет 03) шапка масштабировалась
     от ширины ОКНА через vw без потолка, а секции («о проекте»,
     «контакты») — от ширины КОНТЕНТ-БОКСА контейнера через cqw:
     на любом окне шире ~1650px это две разные шкалы, съезжающиеся
     только случайно. У Кости в макете шапка и секции соразмерны —
     значит шкала должна быть одна.

     --scale-w — эта общая шкала, взятая от уже существующей: точная
     ширина, которую .about-stage/.contacts-stage видят через cqw
     (100% контент-бокса .container — т.е. капнутая ширина минус
     оба поля --gutter). Всё, что раньше было в vw без потолка (кегль
     меню) или в фиксированных px (толщина линий, радиус скругления),
     пересчитано от нёе тем же коэффициентом (px-в-макете / 1920),
     что уже используют секции через cqw — теперь буквально та же
     величина, не только тот же коэффициент. */
  --cap-w: min(100vw, 1600px);
  --gutter: max(20px, 6.51vw);
  --scale-w: calc(var(--cap-w) - 2 * var(--gutter));

  /* Толщина обводки и радиус скругления рамок — раньше фиксированные
     px независимо от размера окна, из-за чего на узких десктопных
     окнах линии выглядели непропорционально толстыми (макет:
     3px и 15.71px имеют смысл только при дизайн-ширине 1920). */
  --stroke-w: clamp(2px, calc(var(--scale-w) * 0.0015625), 3px);
  --radius:   clamp(8px, calc(var(--scale-w) * 0.00818229), 15.71px);

  /* Дизайн-ширина 1920, контейнер капнут на 1600: при нём исходники
     дают ~1.8x плотности, до 2x не дотягивают нигде (запас 1.44–1.79). */
  --container: 1600px;

  --header-h: 56px;

  /* Высота кнопки тизера (Web_Materials_03: измерено по встроенному
     <image> в Maket_2.svg — 108.79px при ширине 1920, а не 84px,
     как было в предыдущем пакете; коэффициент 108.79/1920). Теперь
     от --scale-w — та же шкала, что и у остальной страницы. Живёт
     в :root, а не в .hero__cta-wrap — переменную использует и кнопка
     закрытого раздела (у неё свой коэффициент, см. .contacts-stage —
     макетная высота там другая, 99.31px). */
  --btn-h: clamp(44px, calc(var(--scale-w) * 0.05666), 108.79px);

  /* Кегль меню — как у заголовка «Философия проекта…» (.about-title):
     тот же коэффициент 1.563cqw/30px, но через --scale-w, а не через
     container query — раньше это была отдельная vw-формула без
     потолка, из-за неё меню на широких окнах оказывалось крупнее,
     чем должно быть при пропорциях макета. */
  --fs-menu: clamp(17px, calc(var(--scale-w) * 0.015625), 30px);

  /* Кегль текста на кнопках-«пилюлях» (низ страницы, «Доступ в закрытый
     раздел») и подписи поверх растровой кнопки тизера — та же общая
     шкала. На мобильном (<768px) переопределяется отдельной vw-формулой
     ниже — вертикальную версию в этом заходе не трогаем. */
  --fs-btn: clamp(23px, calc(var(--scale-w) * 0.01927), 37px);

  --fs-contacts:  clamp(32px, 1.4px + 2.583vw, 51px);
  --fs-h2:        clamp(20px, 0.7px + 1.484vw, 30px);
  --fs-body:      clamp(18px, -8.0px + 2.031vw, 31px);
  --fs-role:      clamp(17px, -0.3px + 1.484vw, 30px);

  /* Интерлиньяж по измерению макета: шаг строк 38px при кегле 31px.
     Число «14,4» из ТЗ не сходится ни в px, ни в pt — взяли факт. */
  --lh-body: 1.226;
}

/* ─────────────────────────── база ─────────────────────────── */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* Отступ якорей задаём только через scroll-margin у секций.
     Раньше здесь дублировался scroll-padding, и два отступа
     складывались: секция тизера вставала на 112px вместо 56, а
     её нижняя грань уезжала за окно. */

  /* Костя просил перехват колеса (Главная → Тизер одним небольшим
     движением, «как в тиктоке»). Snap только на первых двух секциях
     (см. .hero/#teaser ниже) — дальше секций с scroll-snap-align нет.

     Правка арт-директора, круг 4: proximity не давал нужного эффекта —
     чтобы перебросило на тизер, приходилось тащить почти пол-экрана.
     mandatory это чинит, но открылось (playwright-отладка этого
     захода): mandatory — не «снап срабатывает только рядом с точкой»,
     а инвариант «точка ПОКОЯ обязана совпасть со снап-точкой, если
     хоть одна есть в контейнере» — даже если сейчас проверяется точка
     покоя далеко внизу, в «Контактах». Из этого следуют два бага:

     1) клик по «Контакты»/«О проекте» из меню — смус-скролл идёт
        НАСКВОЗЬ через mandatory-точку «Тизер» по пути к цели, и
        Chromium прерывает его прямо там.
     2) если просто ПОЗВОЛИТЬ скроллу доехать до контактов, а потом
        (даже через полсекунды) снова стать mandatory — браузер видит,
        что текущая точка покоя не снап-точка, и откатывает страницу
        НАЗАД к тизеру. Это не гонка таймингов (никакой дебаунс не
        помогает) — это поведение спецификации, воспроизведено и
        подтверждено отдельно от бага 1.

     Чинится не в CSS, а в JS (см. app.js, wireScrollSnapZone):
     mandatory реально активен только пока точка покоя физически НЕ
     ниже тизера — атрибут data-no-snap ниже держит snap выключенным
     и на время самого клика по меню (защита от бага 1, транзит), и
     постоянно, пока проскроллено ниже тизера (защита от бага 2,
     переоценивается на каждый scroll, не только в момент клика).
     Клавиатура (ArrowDown/PageDown от самого верха до конца страницы,
     playwright) проверена в этой схеме — стрелки проходят тизер
     насквозь без залипания. */
  scroll-snap-type: y mandatory;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; scroll-snap-type: none; }
}

/* См. комментарий у scroll-snap-type выше — атрибут ставит/снимает
   app.js (wireScrollSnapZone), по клику МЕНЮ и постоянно по позиции
   скролла (не только на время клика). */
html[data-no-snap] { scroll-snap-type: none; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img,
video { display: block; max-width: 100%; }

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Виден только скринридеру — для заголовков секций-заглушек,
   чтобы структура страницы читалась до наполнения. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ─────────────────────────── шапка ─────────────────────────── */

/* Правка арт-директора, круг 4: отменяем прозрачную шапку-оверлей —
   у Кости в макете это сплошная чёрная плашка, кадр заставки
   начинается ПОД ней, а не под ней просвечивает. sticky вместо
   fixed — не только чтобы плашка занимала настоящее место в потоке
   (тогда .hero/#teaser сами считают себя без хаков под неё, см.
   ниже), но и обходит задокументированный баг WebKit: fixed-элемент
   рядом с документом на scroll-snap (см. html ниже) на iOS Safari
   норовит "уехать" вместе с прокруткой вместо того чтобы остаться
   прибитым — это и есть пункт «меню фиксированное» из ТЗ. sticky
   этому баку не подвержен. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  background: var(--header-bg);
}

.site-nav {
  width: 100%;
  /* Правка арт-директора, круг 4 — отменяет предыдущую (пакет 03,
     см. старый комментарий ниже в истории git): по факту в
     Maket_2.svg пункты меню разнесены НЕ в капнутом на 1600
     .container, а по всей ширине плашки (окна) — ГЛАВНАЯ начинается
     на 2.02% от левого края холста, КОНТАКТЫ кончается на 2.29% от
     правого. Замер: x=38.79 (ГЛАВНАЯ) … 1875.97 (конец текста
     КОНТАКТЫ) при холсте 1920. max-width снят, паддинги — эти
     проценты. */
  padding-inline: 2.02% 2.29%;
  display: flex;
  align-items: center;
  /* Зазоры между пунктами в файле — 164.15/168.28/167.46/171.67px
     при 1920 (8.55–8.94%), то есть почти равны при РАЗНОЙ ширине
     слов — это и есть характерная подпись space-between (лишнее
     место делится поровну между промежутками), а не индивидуально
     подобранный gap. */
  justify-content: space-between;
}

/* На десктопе — прозрачная обёртка: все пять пунктов остаются
   прямыми flex-детьми .site-nav и распределяются одной строкой.
   В две строки .nav-row превращается только на узких экранах,
   см. медиа-запрос ниже. */
.nav-row { display: contents; }

.site-nav a {
  position: relative;
  font-family: 'POLIS Display', var(--font-fallback);
  font-size: var(--fs-menu);
  /* Web_Materials_03: в самом Maket_2.svg у меню letter-spacing:1 —
     буквально 1 юнит на холсте 1920 (не трекинг-конвенция 1/1000em,
     та используется у других классов для растяжки justify-текста).
     1/31 = .032em. Старое значение .05em шло из текстового ТЗ, а не
     из файла — при сверке источник расходится с ним, беру файл. */
  letter-spacing: .032em;
  text-transform: uppercase;      /* в шрифте нет строчных вообще */
  color: var(--menu);
  text-decoration: none;
  white-space: nowrap;
  padding-block: 4px;
  transition: color .2s ease;
}

/* Правка арт-директора (пакет 03): выделение только цветом. Полоса
   под надписью и подсветка активного раздела при скролле — убраны
   совсем, включая подсветку «Главной» при загрузке (см. app.js —
   там больше нет ни aria-current, ни обсервера секций). Ховер —
   единственное состояние, и оно только на одном пункте одновременно
   (это уже даёт :hover сам по себе — курсор физически на одной
   ссылке). */
.site-nav a:hover,
.site-nav a:focus-visible { color: var(--fg); }

/* Активный пункт на отдельной странице (сейчас — только «Посетить
   станцию», SITE-80): та же подсветка, что у ховера, но статичная —
   на лендинге такой подсветки по-прежнему нет (см. комментарий выше,
   решение осталось в силе для одностраничного скролла). */
.site-nav a[aria-current="page"] { color: var(--fg); }

/* ─────────────────────── главный экран ─────────────────────── */

/* Заставка во всё окно. dvh, а не vh: на мобильном адресная строка
   то появляется, то прячется, и vh считается по большему размеру —
   кнопка уезжала бы за нижнюю кромку. */
.hero {
  position: relative;
  width: 100%;
  /* Шапка теперь в потоке (sticky, см. выше) и занимает своё
     var(--header-h) над заставкой — вычитаем его, чтобы шапка+кадр
     вместе по-прежнему заполняли ровно один экран (это держит
     инвариант снапа: каждая снап-секция = один viewport). */
  height: calc(100vh - var(--header-h));          /* фолбэк для браузеров без dvh */
  height: calc(100dvh - var(--header-h));
  background: var(--bg);
  /* scroll-margin-top — как у .section (тизер/о проекте/контакты), но
     .hero в этот класс не входит, задаём отдельно. Без него всплыл
     баг: шапка sticky теперь занимает свои var(--header-h) в потоке
     ДО .hero, значит собственная документная позиция .hero — не 0, а
     var(--header-h). scroll-snap-align:start снапает на эту позицию
     буквально — а страница при обычной загрузке стоит на scrollY:0,
     это НЕ валидная снап-точка. Результат: сразу после загрузки
     mandatory заметно доскролливал страницу на var(--header-h) —
     видимый рывок (проверено playwright: 0→15→51→56px за ~120мс).
     scroll-margin-top сдвигает эффективную снап-точку на ту же
     величину назад — 0 снова совпадает с «.hero снапнут». */
  scroll-margin-top: var(--header-h);
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.hero__media {
  width: 100%;
  height: 100%;
  object-fit: cover;

  /* Пересчитано 02.09.2026 (Web_Materials_02): у нового горизонтального
     кадра Костя встроил градиентную шторку в самый верх — непрозрачно
     чёрная 0–2.8% высоты кадра, дальше плавный градиент. Раньше её не
     было, и 38% выбирались только под один критерий — не срезать
     надпись «ПОЛИС» (0.232–0.391 высоты кадра) на низком окне.

     top (0%) держит оба условия разом: верх видимого окна остаётся
     версией кадра y=0 независимо от соотношения сторон — значит шапка
     меню всегда сидит на шторке, а не на голом небе. Надпись при этом
     не срезается вплоть до высоты 1920×422 (нужно 39.1% кадра видимыми,
     а видимая доля кадра при top не может быть меньше этого без второго
     ограничения) — с запасом ниже проверенных 1920×500.

     Проверено рендером (playwright, тот же ролик): 1920×1080, 2560×1080,
     3440×1440, 1920×700, 1920×500 — шторка везде под меню, надпись
     цела везде. На 1920×500 у прежних 38% меню накладывалось прямо на
     «ПОЛИС» — теперь исправлено. */
  object-position: center top;
}

/* В макете пилюля сидела центром ровно на стыке заставки и тизера.
   Теперь стык — нижняя грань окна, и посадить кнопку на неё центром
   значит срезать половину: проверено на 1920×700, видна только
   верхушка. Поэтому кнопка целиком внутри, с отступом от кромки —
   требование «видна всегда» важнее буквального повторения стыка. */
.hero__cta-wrap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: clamp(20px, 3.5vh, 48px);
  z-index: 2;
  display: flex;
  justify-content: center;
}

/* Кнопка «Смотреть тизер» — растровая (Button_1.png, пакет 03),
   вместо CSS-пилюли. Знаки »/« убраны, текст — HTML-слоем поверх
   картинки, тем же приёмом, что и остальная типографика: высота
   картинки держится на --btn-h (общая шкала --cap-w), ширина следует
   из её пропорции (2379×473). */
.btn-image {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--btn-h);
  text-decoration: none;
  cursor: pointer;
}

.btn-image picture,
.btn-image img {
  display: block;
  height: 100%;
  width: auto;
}

.btn-image__label {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;   /* не inset — надёжнее в старых WebKit */
  display: flex;
  align-items: center;
  justify-content: center;
  /* Правка арт-директора, круг 4: третья попытка центровки, и в
     первый раз меняется не только transform. В Maket_2.svg Костя сам
     положил текст «СМОТРЕТЬ ТИЗЕР» поверх встроенной картинки кнопки —
     это его собственная разметка, надёжнее скана альфа-канала PNG
     (которым центровали дважды раньше и оба раза мимо). У его текста
     font-family 'POLIS_Display' — НЕ 'POLIS Text Head', как было
     здесь. Два разных шрифта дают разную высоту строки при одном и
     том же align-items:center, и это давало видимый вертикальный
     сдвиг КРУПНЕЕ, чем сама translate-поправка ниже — вероятная
     причина, почему предыдущие две попытки поправить только dx/dy не
     сработали. */
  font-family: 'POLIS Display', var(--font-fallback);
  /* НЕ var(--fs-btn): на мобильном --btn-h и --fs-btn считаются двумя
     независимыми vw-формулами (--btn-h упирается в свой clamp-пол
     раньше, чем --fs-btn — унаследовано от старой резиновой пилюли,
     где рассинхрон был не виден). У растровой кнопки ширина жёстко
     завязана на высоту через пропорцию картинки — при рассинхроне
     подпись перестаёт помещаться на части мобильных ширин (проверено:
     на 360px «Смотреть тизер» вылезала на ~19px). calc(var(--btn-h) * .34)
     держит кегль ПРИВязанным к реальной высоте кнопки везде — то же
     соотношение 37/108.79, измеренное по Maket_2.svg на 1920, работает
     на любой ширине, а не только там, где обе vw-формулы случайно
     совпадали. */
  font-size: calc(var(--btn-h) * .34);
  /* Трекинг из файла Кости — 1 юнит холста на 37px кегль = .027em
     (та же конвенция «letter-spacing:1» на 1920, что и у меню, но
     свой коэффициент — у меню кегль 31px даёт .032em). */
  letter-spacing: .027em;
  text-transform: uppercase;
  color: var(--menu);
  transition: color .2s ease;
  white-space: nowrap;

  /* Замер положения СОБСТВЕННОГО текста Кости в Maket_2.svg
     (font-family, letter-spacing и координаты — из самого файла, не
     скана растра): относительно центра всего канваса 2379×473 текст
     смещён на dx=-0.365%, dy=-5.815% (было +0.3153%/-7.0825% —
     старые числа держались на скане альфа-канала кнопки без текста
     и не совпадали с тем, что Костя реально нарисовал сверху).
     translateX/Y — доля от размеров label (=размеры картинки). */
  transform: translateX(-0.365%) translateY(-5.815%);
}

.btn-image:hover .btn-image__label,
.btn-image:focus-visible .btn-image__label { color: var(--fg); }

.btn-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--btn-h);
  /* max-width обязателен: без него nowrap-строка на узком экране
     распирает страницу шире вьюпорта и утаскивает вбок всё остальное. */
  max-width: 100%;
  padding-inline: clamp(28px, 3vw, 56px);
  background: var(--bg);
  border: none;
  border-radius: var(--radius);
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-size: var(--fs-btn);
  letter-spacing: .05em;
  text-transform: uppercase;   /* капслок-онли шрифт */
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: color .2s ease;
}

.btn-pill:hover,
.btn-pill:focus-visible { color: var(--accent); }

/* ──────────────────── заглушки секций ──────────────────── */

/* Этап 3 — только каркас с якорями. Наполнение следующим этапом:
   тизер-плеер (Kinescope), три секции описания, контакты. */

.section {
  scroll-margin-top: var(--header-h);
}

.placeholder {
  margin-block: clamp(48px, 5vw, 96px);
  border: var(--stroke-w) solid var(--stroke);
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  justify-content: center;
  color: color-mix(in srgb, var(--stroke) 80%, var(--fg));
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: clamp(13px, 1vw, 18px);
  letter-spacing: .18em;
  text-transform: uppercase;
  text-align: center;
  padding: 24px;
}

/* Тизер занимает всё окно: при переходе по кнопке в кадре не должно
   быть ничего, кроме плеера и меню. */
#teaser {
  --teaser-pad: clamp(24px, 3vw, 56px);
  /* Шапка sticky и занимает своё место в потоке (см. .site-header) —
     секция больше не обязана резервировать под неё отдельный
     padding-top, scroll-margin-top у .section (общее правило) уже
     держит зазор при переходе по якорю. Высота — как у .hero, минус
     шапка, чтобы вместе с ней тизер тоже закрывал ровно один экран. */
  min-height: calc(100vh - var(--header-h));   /* фолбэк */
  min-height: calc(100dvh - var(--header-h));
  display: flex;
  align-items: center;
  padding-block: var(--teaser-pad);
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* Правка арт-директора: окно тизера должно занимать максимум ширины
   страницы (небольшие поля по бокам), а не капнутый на 1600
   .container — на широких окнах между рамкой и краями оставалось
   слишком много пустоты. .wide-container — та же коробочная модель,
   что у .container (100%, свои поля), но без max-width. */
.wide-container {
  width: 100%;
  padding-inline: clamp(16px, 3vw, 56px);
}

#teaser > .wide-container { width: 100%; }

/* Рамка вписывается и по ширине, и по высоте. Ширину считаем сами:
   с width:100% и max-height высота обрезалась, а ширина оставалась
   прежней — на 1920×700 пропорция уезжала в 2.54 вместо 1.78.
   min() берёт меньшее из ширины обёртки и той ширины, которая
   влезает по высоте; высоту дальше досчитывает aspect-ratio.

   Правка арт-директора: не border + border-radius на iframe отдельно
   (оба скругления рисуются независимо, и на границе между ними
   оставалась щель — subpixel-несовпадение двух радиусов на разных
   элементах). Вместо этого рамка — сплошная ПОДЛОЖКА цвета --stroke:
   padding ровно в толщину линии, iframe лежит поверх с радиусом
   меньше на ту же толщину. Просвета в углу нет вообще, потому что
   нет двух рисуемых границ — только один слитный прямоугольник и
   вырез под видео поверх него. */
/* Правка арт-директора, круг 5 — третий заход на щели в углах,
   гипотеза про WebKit/overflow из круга 4 не объясняла всей картины.
   Разведка инспектором (playwright, frame API — доступ к DOM ВНУТРИ
   кросс-доменного kinescope.io iframe, обычной странице такое
   недоступно, у автоматизации браузера свои права): у САМОГО
   Kinescope-плеера (классы kin-pl-*, включая <video>, постер и все
   обёртки) border-radius: 20px — жёстко зашит их собственным CSS,
   не связан ни с нашим --radius, ни с чем настраиваемым через
   IframePlayer.create() или query-параметры embed-URL. По документации
   Kinescope радиус плеера меняется только через «плеер-темплейт» в их
   личном кабинете — доступа к которому у нас в этом заходе нет.

   Правка арт-директора (05.09): паспарту читалось как широкая серая
   рама, не как рамка макета (там 3px). Возврат к тонкой обводке —
   но раз мы теперь ЗНАЕМ зашитый радиус плеера (20px), задачу можно
   решить точно, а не маскировкой: толщина рамки — обычная линия,
   как везде на странице (--stroke-w, 2–3px по --scale-w, не vw),
   а внешний радиус контейнера подгоняется ПОД плеер, а не наоборот —
   internal = external − pad должно быть ровно 20px при любой толщине
   линии, отсюда external = 20px + var(--stroke-w). Щели в углах не
   возникает: линия и видео делят одну и ту же геометрию скругления. */
:root {
  --teaser-frame-pad:    var(--stroke-w);
  --teaser-frame-radius: calc(20px + var(--stroke-w));
}

.teaser-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  width: min(
    100%,
    calc((100dvh - var(--header-h) - var(--teaser-pad) * 2) * 16 / 9)
  );
  margin-inline: auto;
  padding: var(--teaser-frame-pad);
  background: var(--stroke);
  border-radius: var(--teaser-frame-radius);
  box-sizing: border-box;

  /* overflow:hidden остаётся: клипует iframe как обычный div, не
     полагаясь на то, будет ли border-radius самого iframe-тега
     клипован движком (в WebKit это ненадёжно) — независимая, более
     дешёвая подстраховка на случай, если плеер когда-нибудь станет
     шире своего фактического кадра. */
  overflow: hidden;
}

.teaser-frame iframe {
  position: absolute;
  inset: var(--teaser-frame-pad);
  width: calc(100% - var(--teaser-frame-pad) * 2);
  height: calc(100% - var(--teaser-frame-pad) * 2);
  border: 0;
  display: block;
  border-radius: calc(var(--teaser-frame-radius) - var(--teaser-frame-pad));
}

.placeholder--contacts { aspect-ratio: 1758 / 760; }

/* ──────────── о проекте: секции 5–7 (десктоп) ────────────

   Круг 7 — нахлёст убран совсем, на всех ширинах, а не только ниже
   порога. Круги 5–6 держали пересечение текста и картинки (приём
   макета, отрицательный margin-top у картинки) живым выше 1150px,
   подпирая его z-index'ом как гарантией «буква не под графикой» —
   но это лечило только ЧТЕНИЕ буквы, не сам нахлёст: на широких
   окнах (1920) визуально осталось наложение картинки на текстовый
   блок, четвёртый круг починки одного и того же места. Значит
   пересечение как приём в вёрстке не годится в принципе — по
   требованию не должно быть НИ абсолютных координат, НИ отрицательных
   margin, НИ z-index для разведения слоёв. Пересекаться нечему по
   построению, на любой ширине.

   Схема: .about-col--left/--right — две независимые колонки (обычный
   flex-row) выше 1150px, одна колонка в потоке ниже порога (тот же
   медиа-запрос, что и раньше, см. ниже — порог обоснован читаемостью
   текста, не нахлёстом, и остаётся в силе). Внутри колонки все блоки
   идут друг за другом с обычным положительным отступом — то, что было
   в макете нахлёстом (числом из координат), взято как расстояние:
   плотное соседство вместо пересечения. */

/* SITE-85 (14.09), смена схемы: две колонки — НЕ flex-row с общей
   внутренней границей, а grid с ОДНОЙ ячейкой на всю ширину сцены,
   где обе группы стоят друг НАД другом и прижаты каждая к своему
   полю (left/right group — см. .about-col--left/--right ниже).
   В макете общей границы между колонками нет: левая группа у левого
   поля сцены, правая — у правого, ширины не делят сцену пополам,
   между ними зона перекрытия по X ~102px (в проде не воспроизводим
   один в один — при живых ширинах текста overlap не обязателен,
   но правило «колонки не пересекаются» снято: раньше flex не
   позволял им встать ближе друг к другу, чем сумма их ширин). */
/* SITE-85 (15.09), своя шкала сцены: --cap-w/--scale-w общестраничные
   (капнуты на 1600px ради плотности картинок в шапке/тизере/контактах
   — к этому разделу отношения не имеют, разведка 14.09 показала, что
   единой шкалы на странице и так нет — у тизера свой левый край,
   94.99, не 124.99, как здесь). Рабочая область секций 5-7 в
   VisDev_04.svg — 1728.14px от 1920 = 90.01% холста (124.99 → 1853.13,
   левый край левой группы — правый край правой). .about-stage — 90.01vw
   напрямую, БЕЗ --cap-w/--scale-w, выведена из-под ограничения
   родителя .container через margin-left:50%+translateX(-50%)
   (стандартный приём full-bleed), иначе её ширина обрезалась бы
   паддингом .container на широких окнах.

   ⚠️ ВСЕ cqw внутри — доля от РАБОЧЕЙ ОБЛАСТИ (1728.14), НЕ от 1920.
   Первая версия этой правки (15.09, до пересчёта) по инерции оставила
   старые коэффициенты (px-в-макете / 1920) нетронутыми — они были
   правильными, пока сцена сама была долей от 1920 (0.703), но когда
   сцена стала РАВНА рабочей области (0.9001 = 1728.14/1920), тот же
   cqw-расчёт от 1920 стал давать Pic_1 шириной 728px вместо макетных
   809px: содержимое масштабировалось ДВАЖДЫ (один раз — шириной сцены,
   второй раз — старым коэффициентом внутри неё). Раз .about-stage при
   1920 viewport и есть рабочая область 1:1, всё внутри должно at 1920
   рендериться в СВОЙ макетный px без остатка — отсюда новая база:
   coeff = macket_px / 1728.14 × 100, не / 1920 × 100. */
.about-stage {
  position: relative;
  display: grid;
  grid-template-columns: 100%;
  align-items: start;
  margin-block: clamp(48px, 5vw, 96px);

  width: 90.01vw;
  margin-left: 50%;
  transform: translateX(-50%);

  container-type: inline-size;

  --stroke-w: clamp(2px, 0.173597cqw, 3px);
  --radius:   clamp(8px, 0.909070cqw, 15.71px);
  --line-r:   clamp(10px, 1.015543cqw, 17.55px);
  --bottom-gap-58:     clamp(20px, 3.770528cqw, 65.16px);
  --pic3-drop-jog-58:  clamp(15px, 2.676288cqw, 46.25px);
  --a-spine-offset-58: clamp(20px, 4.010092cqw, 69.30px);
  --b-margin-inset-58: clamp(20px, 3.653061cqw, 63.13px);

  /* SITE-85 (15.09), смена приоритета: картинки и линии — из макета,
     фиксированы, текст — вторичен, встаёт где придётся и может не
     поместиться. Раньше позиции картинок были следствием потока
     (высота текста над ними) — длинный синопсис (22 строки против
     макетных 16) утаскивал Pic_3 вниз, спица приходила в текст блока
     "сильные стороны". Теперь top каждой картинки и каждого
     текст-блока — координата ИЗ VisDev_04.svg (Y верхнего края
     рамки/первой строки заголовка минус Y верха секции = верх Pic_1,
     2269.25), той же cqw-базой (1728.14), БЕЗ clamp-потолка — как и
     у widths выше по файлу: если ширины картинок растут без предела
     за 1920 (макетный кэп только у кегля), то и Y-дистанции между
     ними должны расти в том же темпе, иначе на 2560 раскладка
     «сожмётся» по вертикали относительно горизонтали. */
  --pic2-top-58:       19.992015cqw;   /* 345.49px от верха Pic_1 */
  --pic3-top-58:       84.660386cqw;   /* 1463.05px от верха Pic_1 — МАКЕТНАЯ позиция, см. ⚠️ ниже */
  --phil-title-top-58:  0.165444cqw;   /* 2.86px — почти вровень с Pic_1 */
  --syn-title-top-58:  37.577401cqw;   /* 649.39px от верха Pic_1 */
  --str-title-top-58:  96.159067cqw;   /* 1661.76px от верха Pic_1 */
  /* Высота секции — от верха Pic_1 до нижней горизонтали обводки Б
     (макетная низкая точка, 4889.09), задана, не следствие текста.
     Реальная высота может вырасти дальше этого — см. ⚠️ ниже и
     stage.style.minHeight в app.js. */
  --about-height-58:  151.598829cqw;   /* 2619.84px */

  /* ⚠️ ВРЕМЕННОЕ ОТСТУПЛЕНИЕ ОТ МАКЕТА (SITE-85, 15.09, решение
     Андрея — инвесторский показ, наезд текста на картинку
     недопустим). Синопсис в html на 6 строк длиннее макетного
     (22 против 16, текст финальный, не будет короче сам по себе).
     --pic3-top-58 выше — это МАКЕТНАЯ позиция Pic_3, но app.js
     (wireAboutLines) её не использует напрямую: перед расчётом линий
     он меряет фактический низ последней строки синопсиса
     (Range.getClientRects) и опускает Pic_3 ниже макетной позиции
     ровно настолько, чтобы между ними остался этот зазор — если
     синопсис короче макетного, Pic_3 остаётся на --pic3-top-58 без
     изменений (макет — приоритет, отступление — только по нужде).
     --pic3-gap-58 — тот самый макетный зазор синопсис→Pic_3 (был
     margin-top у .about-pic--3 до перехода на фиксированные
     позиции), 66.30px при 1920, та же cqw-база.
     ВЕРНУТЬ Pic_3 на --pic3-top-58 без замера текста, когда Костя
     решит вопрос с длиной синопсиса — тогда этот блок и связанный
     код в app.js можно убрать целиком.

     ⚠️ ВТОРОЕ ВРЕМЕННОЕ ОТСТУПЛЕНИЕ (та же дата, следствие первого):
     точка входа спицы А в Pic_3 считается долей высоты Pic_3
     (PIC3_SPOKE_ANCHOR_FROM_BOTTOM в app.js) — когда Pic_3 опущена
     по правилу выше, эта точка уезжает вниз вместе с ней и утыкается
     в заголовок «сильные стороны». app.js поднимает её, если нужно,
     новой CSS-переменной для этого не заведено — переиспользован
     --b-margin-inset-58 (63.13px при 1920: тот же зазор, что держит
     левую вертикаль обводки Б от Pic_2/текста). Правило (Андрей,
     15.09 вечер) — спицу не прятать, если целевой зазор не набирается;
     точку входа не поднимать выше нижней границы из третьего
     отступления ниже.

     ⚠️ ТРЕТЬЕ ВРЕМЕННОЕ ОТСТУПЛЕНИЕ (та же дата, тот же вечер,
     следствие второго): пин к самому верху Pic_3 сводил хвост спицы
     ровно в её верхний левый угол — дуга скругления линии сливалась
     со скруглением рамки, выглядело как ошибка стыка (скриншот
     Андрея 23:49). Нижняя граница для точки входа — не верх Pic_3,
     а верх Pic_3 + 2×--line-r (CORNER_CLEARANCE в app.js): при
     меньшем отступе дуги визуально соприкасаются. На 1920/1440 после
     этого отступа зазор до заголовка «сильные стороны» — 2.8px и
     1.9px (был 37.9/28.2 до этой правки): по-прежнему не касание, но
     вплотную к нулю — если синопсис станет ещё длиннее, уйдёт в
     минус, и app.js спрячет спицу с числом в консоли (см. проверку у
     spokeTitleClearanceFinal). Подробности и точные числа — у
     CORNER_CLEARANCE в app.js. Убрать вместе с первым и вторым
     отступлением. */
  --pic3-gap-58: clamp(24px, 3.836495cqw, 66.30px);
}

.about-col {
  position: relative;    /* контекст для position:absolute у картинок/текста ниже */
  min-width: 0;   /* иначе grid-элемент не сжимается уже текстового контента — переполнение вместо переноса */
  height: var(--about-height-58); /* задана из макета — не следствие содержимого, см. .about-stage выше */
  grid-column: 1;
  grid-row: 1;    /* обе группы в одной ячейке — могут перекрываться по X, см. .about-stage выше */
}

/* Ширины групп — по самому широкому блоку в каждой: слева «сильные
   стороны» (914/1728.14 = 52.889), справа «синопсис» (916/1728.14 =
   53.005). Более узкие блоки внутри группы (философия, pic1–3) держат
   свою собственную ширину ниже и просто не заполняют группу целиком —
   как и в макете, где они не были одной ширины. justify-self держит
   каждую группу у СВОЕГО поля сцены (не у общей границы), align-items
   держит узкие блоки внутри правой группы у её правого края (в
   макете Pic_1 и текст синопсиса прижаты к правому полю сцены). */
.about-col--left  { width: 52.889234cqw; justify-self: start; }
.about-col--right { width: 53.004965cqw; justify-self: end; }

.about-title {
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-weight: 700;
  font-size: clamp(17px, 1.735970cqw, 30px);
  letter-spacing: .04em;        /* ТЗ: трекинг 40 ÷ 1000 */
  text-transform: uppercase;    /* капслок-онли гарнитура */
  color: var(--fg);
  line-height: 1.2;
}

/* Отступ заголовок→абзац — в Web_Materials_03 он РАЗНЫЙ у трёх блоков
   (25.51 / 37.17 / 28.33 при 1920, измерено по расстоянию между
   text-box'ами в Maket_2.svg), а не один общий, как было. cqw — та же
   единица, что уже держит кегль заголовка выше (доля от рабочей
   области сцены, не от 1920). */
.about-text--philosophy .about-title { margin: 0 0 clamp(10px, 1.476154cqw, 25.51px); }
.about-text--synopsis   .about-title { margin: 0 0 clamp(10px, 2.150867cqw, 37.17px); }
.about-text--strengths  .about-title { margin: 0 0 clamp(10px, 1.639335cqw, 28.33px); }

.about-text p {
  font-size: clamp(16px, 1.793836cqw, 31px);
  line-height: var(--lh-body);
  margin: 0 0 var(--lh-body-em);
  text-align: justify;          /* в макете набор выключен по формату */
  hyphens: auto;                /* иначе justify рвёт строки пробелами */
  color: var(--text);
}

.about-text p:last-child { margin-bottom: 0; }

/* Абзацы в макете разделены двойным шагом строки (76px при 38px) —
   то есть пустой строкой, а не произвольным отступом. */
:root { --lh-body-em: 1.226em; }

/* Ширины — доля от РАБОЧЕЙ ОБЛАСТИ (1728.14), не от 1920 (см. правку
   у .about-stage выше). Огибающая самой широкой строки текущего
   текста в файле — 853/916/914px при 1920 — единственная доступная
   мера, в файле нет сохранённого text-frame (Illustrator развернул
   area-type в построчные tspan при экспорте). Раздельного max-width
   под абзац нет: огибающая мерена по блоку целиком (заголовок+абзац),
   как и у двух других блоков ниже. */
.about-text--philosophy { width: 49.359427cqw; }
.about-text--synopsis   { width: 53.004965cqw; }
.about-text--strengths  { width: 52.889234cqw; }

.about-pic { margin: 0; }

.about-pic img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: var(--stroke-w) solid var(--stroke);
  border-radius: var(--radius);
}

/* Пропорции слотов совпадают с исходниками до третьего знака —
   Костя резал картинки под макет. Ширины — доля от рабочей области,
   не от 1920 (см. правку у .about-stage). */
.about-pic--1 { width: 46.831275cqw; aspect-ratio: 809.31 / 585.65; }
.about-pic--2 { width: 40.231694cqw; aspect-ratio: 695.26 / 1212; position: relative; }
.about-pic--3 { width: 40.949229cqw; aspect-ratio: 707.66 / 1091.63; }

/* Позиции — фиксированы из макета (SITE-85, 15.09, см. --*-top-58 и
   --about-height-58 у .about-stage выше), не следствие текста над
   ними или порядка в потоке. Левая группа прижата к левому полю
   (left:0), правая — к правому (right:0), Pic_3 — единственный
   элемент правой группы НЕ вровень с правым полем (в макете его
   правый край на 55.58px левее, см. прежний margin-right — то же
   число, тот же смысл, просто теперь абсолютный отступ, а не сдвиг
   внутри flex). */
.about-text--philosophy { position: absolute; left: 0; top: var(--phil-title-top-58); }
.about-pic--2           { position: absolute; left: 0; top: var(--pic2-top-58); }
.about-text--strengths  { position: absolute; left: 0; top: var(--str-title-top-58); }

.about-pic--1           { position: absolute; right: 0; top: 0; }
.about-text--synopsis   { position: absolute; right: 0; top: var(--syn-title-top-58); }
/* top здесь — только CSS-дефолт (макетная позиция, --pic3-top-58).
   app.js (wireAboutLines) обычно ПЕРЕБИВАЕТ его инлайн-стилем — см.
   ⚠️ ВРЕМЕННОЕ ОТСТУПЛЕНИЕ у .about-stage выше. */
.about-pic--3           { position: absolute; right: clamp(0px, 3.216175cqw, 55.58px); top: var(--pic3-top-58); }

/* ──────────── о проекте: линии между блоками (SITE-84, заход 2) ────────────

   Только десктоп (>1150px) — see .about-line ниже медиа-запроса; порог тот
   же, что уже делит раскладку на колонки/поток, отдельного не заводим.
   Геометрия — из VisDev_04.svg (viewBox сломан, координаты путей целы),
   разведка в SITE-84.md раздел 2. Три связи:

   А. Спица Pic_1(левый край)↔Pic_3(левый край), через зазор колонок.
      Обе точки входа — фиксированная доля высоты СВОЕЙ рамки
      (aspect-ratio делает высоту постоянной), устойчиво к любой высоте
      синопсиса между ними. В макете Pic_1/Pic_3 на разных x
      (54.288%/56.746% в старой абсолютной схеме) — во flex-раскладке
      оба уже на одном левом крае колонки, спица симметрична и сидит по
      центру зазора между .about-col--left/--right. SITE-84, круг 6:
      маршрут (2 прямых + 2 дуги) собран ОДНИМ SVG-путём,
      .about-line--a-path, прямо в .about-stage — тот же фикс и та же
      причина, что у обводки Б на круге 4 (див-прямая + отдельная
      SVG-дуга давали на глаз подобранный стык, см. разбор у
      .about-line-arc). d/viewBox целиком строит app.js от реальных
      боксов Pic_1/Pic_2/Pic_3/.about-col--left/.about-col--right —
      ни одного числа в cqw для этого маршрута в CSS больше нет.

   Б. Обводка Pic_3(правый край)↔Pic_2(левый край), вокруг низа и левого
      поля сцены. «Низ секции» для линии — max(низ левой колонки, низ
      Pic_3) плюс отступ (SITE-85, 14.09): держит маршрут ниже ОБОИХ,
      какая бы колонка ни оказалась длиннее при живом тексте — раньше
      брали только низ левой колонки, и при более длинной правой
      (Pic_3 ниже) маршрут заходил в границы картинки (проверка по
      VisDev_04.svg показала, что в макете так не бывает: там низ
      обводки всегда ниже Pic_3). Этот низ зависит от текста в ОБЕИХ
      колонках и от высоты Pic_3 — чистым CSS не выражается ни один
      конец участка. Поэтому весь маршрут от Pic_3 до Pic_2 — на app.js
      (ResizeObserver на всех трёх текстовых блоках, wireAboutLines).
      SITE-84, круг 4: после четырёх заходов точечных стыков (див-
      прямая + отдельная SVG-дуга, см. разбор у .about-line-arc) —
      маршрут собран ОДНИМ SVG-путём, .about-line--b-path, прямо в
      .about-stage: d/viewBox целиком строит JS, в путь входят и обе
      прямые у Pic_3 («drop»/«bottom»), и вход в Pic_2 («margin»,
      бывший «nub»), и все три скругления между ними — своей коробки
      и своего стыка не осталось ни у одного куска. Без успешного
      измерения — d пуст, элемент невидим (visibility:hidden по
      умолчанию), ошибка — в консоль; никогда не остаётся висеть
      с длиной, посчитанной для другого текста.

   В. Перемычка Pic_2(правый край)↔спица А — короткий отрезок. SITE-84,
      круг 6: второй подпуть (свой M) внутри .about-line--a-path — Т-стык
      на теле спицы, третье направление, не продолжение линии, а не
      отдельный элемент в .about-pic--2, как раньше; ширина считается
      от реальных боксов Pic_1/Pic_2/Pic_3 в app.js, не от cqw-долей
      ширины колонок.

   Итого 8 прямых отрезков (3 + 4 + 1) и 5 скруглений (2 + 3 + 0) — оба
   маршрута с круга 6 (А) и круга 4 (Б) целиком свои SVG-пути, ни один
   угол не отдельный элемент, — r≈17.55 при 1920 (--line-r), на один
   отрезок и одно скругление меньше
   макетных 9/6: там, где в макете Pic_3 не на одной линии с Pic_1
   (см. выше) и вход в обводку Б был микро-джогом (46.25px) в сторону от
   рамки, здесь оба примыкания к Pic_3 идут перпендикулярно кромке без
   обхода — на экране лишний отрезок ничего не добавлял. */

:root {
  /* Радиус скругления линий — свой у Кости для этих кривых (17.55 при
     1920), отличается от --radius рамок (15.71) на доли пикселя;
     не объединяю с --radius, чтобы не подгонять один токен под два
     источника, но и не завожу больше одной новой переменной. */
  --line-r: clamp(10px, calc(var(--scale-w) * 0.0091406), 17.55px);
}

.about-line { background: var(--stroke); }

/* Пробники для app.js (wireAboutLines, спица А и обводка Б) —
   getComputedStyle на custom property (--line-r/--gutter/--stroke-w)
   возвращает СЫРУЮ строку формулы (`clamp(...)`), не посчитанное
   число: единственный способ достать реальные px — прочитать
   getBoundingClientRect() у бокса, чья ширина буквально равна нужной
   переменной. Нулевой высоты, position:absolute — не участвуют в
   flex-раскладке .about-stage и ничего не рисуют (visibility:hidden).
   --stroke-w нужен не для толщины штриха самого пути (её централизованно
   держит .about-line-arc path ниже) — он нужен, чтобы посчитать, где
   именно проходит ВИДИМЫЙ край этого штриха при зазоре до чужой рамки
   (SITE-84, круг 6), см. вывод у dropX в app.js. */
.js-metric {
  position: absolute;
  visibility: hidden;
  height: 0;
  pointer-events: none;
}
.js-metric--line-r { width: var(--line-r); }
.js-metric--gutter { width: var(--gutter); }
.js-metric--stroke-w { width: var(--stroke-w); }
.js-metric--bottom-gap { width: var(--bottom-gap-58); }
.js-metric--pic3-drop-jog { width: var(--pic3-drop-jog-58); }
.js-metric--a-spine-offset { width: var(--a-spine-offset-58); }
.js-metric--b-margin-inset { width: var(--b-margin-inset-58); }
.js-metric--pic3-gap { width: var(--pic3-gap-58); }
/* Макетная позиция Pic_3 как чистое число — нужна app.js, чтобы
   сравнивать с ней НЕЗАВИСИМО от текущего инлайн-стиля top (который
   сам app.js мог уже выставить на прошлом вызове update()), см. ⚠️
   ВРЕМЕННОЕ ОТСТУПЛЕНИЕ выше и вывод у macketPic3Top в app.js. */
.js-metric--pic3-top { width: var(--pic3-top-58); }

/* Все скругления — SVG (path+stroke), не div+border-radius; с круга 4
   (Б) и круга 6 (А) форма дуги встроена прямо в единый путь каждого
   маршрута, см. .about-line--a-path и .about-line--b-path ниже.
   SITE-84, круг 2: div-подход разъезжался на 300-400% zoom —
   border рисуется от КРАЯ бокса, а соединяемая прямая (тоже div) была
   центрирована на
   своей линии; расхождение в половину --stroke-w, невидимое на 100%,
   видное вплотную.

   viewBox="0 0 1 1" — единичный квадрат, не px и не R: R (--line-r)
   задаёт только CSS width/height самого <svg>, дуга внутри отрисована
   один раз в относительных координатах и просто масштабируется вместе
   с боксом на любой ширине. Толщина обводки — vector-effect:
   non-scaling-stroke гасит собственное масштабирование SVG, stroke-width
   остаётся --stroke-w в настоящих CSS-пикселях на любой ширине сцены,
   тот же токен, что у прямых.

   Три типа дуги (местные имена — не классы, просто ярлык формы):
   "верх-лево" — путь `M 1 0 A 1 1 0 0 0 0 1`; "низ-лево" —
   `M 1 1 A 1 1 0 0 1 0 0`; "низ-право" — `M 0 1 A 1 1 0 0 0 1 0`.
   Раньше (до SITE-84, круг 4 у Б и круг 6 у А) каждый угол был
   отдельным <svg viewBox="0 0 1 1"> с таким path — сейчас ни одного
   такого элемента не осталось: обе спицы (.about-line--a-path,
   .about-line--b-path) строят те же три формы дуги прямо внутри
   своего d-атрибута, уже в настоящих px, не в единичном квадрате
   (см. buildPath в app.js). .about-line-arc ниже теперь общий базовый
   стиль обоих путей — имя осталось историческим, не про "один угол —
   один элемент".

   Круг 3 (12.09) и круг 6 (13.09), оба раза найдено измерением, не
   глазом: пока дуга и соседняя прямая были РАЗНЫМИ элементами (div —
   прямая, отдельный SVG — дуга), стык между ними требовал отдельной
   численной поправки на каждом углу — div центрирует обводку на
   заданной толщине, а path кладёт stroke ЦЕНТРОМ РОВНО на координате
   пути, и если оба брали одно и то же число за исходную точку, центр
   прямой оказывался на --stroke-w/2 дальше от точки касания дуги, чем
   нужно (не антиалиасинг — два разных способа читать одну координату,
   замерено на a-corner-top до починки: 1.039px против ожидаемых
   --stroke-w/2 = 1.055px). Первый фикс (круг 3, только для Б) был
   такой поправкой, общей на все углы Б, но именно то, что она общая и
   ручная, а не встроенная в геометрию, оставляло тот же риск на
   каждом новом углу — что и подтвердилось на спице А (круг 6):
   идентичная поправка не была перенесена на a-top/a-spine, потому что
   про неё просто не вспомнили в другом месте файла, и дуга годами
   стояла с тем же на глаз подобранным стыком, что и Б до круга 4.

   Настоящий фикс — не формула поправки, а её отсутствие как класса:
   один SVG-путь на весь маршрут, прямая и дуга — одна система
   координат одного элемента, стыковать в принципе нечего (см.
   .about-line--a-path, .about-line--b-path и wireAboutLines в app.js).
   Только это гарантирует, что новый угол не унаследует старый риск —
   поправку неоткуда забыть, потому что её не существует.

   Находка 9 (историческая, сама находка не про поправку): в прежней
   редакции этого комментария список классов был записан через дефис и
   слэш подряд — случайно получился закрывающий токен комментария
   посреди фразы, тот же класс бага, что уже ловили в этом файле
   раньше (закрывающий токен внутри русской прозы). Следствие — весь
   текст ниже до следующего такого же токена читался движком как
   невалидный CSS и отбрасывался целиком, включая правило про фон дуги
   ниже — отсюда квадрат-заливка вместо дуги у всех пяти углов.
   Проверка после фикса — не глазами: getComputedStyle(svg).backgroundColor
   должен быть прозрачным, не var(--stroke). Урок: избегать звёздки
   вплотную перед слэшем в любом будущем комментарии этого файла, не
   только в этом месте. */
.about-line-arc {
  display: block;
  overflow: visible;
  background: none;
}
.about-line-arc path {
  fill: none;
  stroke: var(--stroke);
  stroke-width: var(--stroke-w);
  vector-effect: non-scaling-stroke;
}

/* А — один SVG-путь, ребёнок .about-stage (как и Б ниже), целиком
   строит app.js (wireAboutLines): спица Pic_1↔Pic_3 плюс перемычка В
   вторым подпутём (свой M — Т-стык на спине спицы, третье направление,
   не продолжение линии). SITE-84, круг 6 — тот же класс бага и то же
   лекарство, что у Б на круге 4 (см. разбор выше и у .about-line-arc):
   раньше div-прямые (a-top/a-spine/a-bottom) плюс отдельные SVG-дуги
   (a-corner-top/a-corner-bottom), теперь один .about-line--a-path. */
.about-line--a-path {
  position: absolute;
  visibility: hidden;
}

/* Б — один SVG-путь, ребёнок .about-stage, целиком строит app.js
   (wireAboutLines), см. комментарий у блока "о проекте: линии" выше.
   SITE-84, круг 4 — четыре захода (div-прямая + SVG-дуга раздельно,
   стык через --line-fit + магическая константа отступа от Pic_3)
   ловили баг за багом, каждый раз в НОВОМ месте: последний —
   отступ от рамки Pic_3 был зашит фиксированным px (`--b-drop-offset:
   9px`), а бокс дуги рядом с ним шириной `--line-r` — clamp(10px, …,
   17.55px), т.е. ВСЕГДА больше отступа. Бокс дуги отступает от точки
   привязки на свою полную ширину НАЗАД, к Pic_3 — при r > 9px это
   утапливало дальний край бокса (а значит и саму кривую) на (r − 9)px
   ОБРАТНО в рамку Pic_3, на любой ширине экрана, потому что r никогда
   не бывает меньше 10px. Числовая проверка стыка прямая↔дуга
   (--line-fit, см. вывод у .about-line-arc выше) при этом СХОДИЛАСЬ —
   она сверяет только эту одну границу, а не то, что бокс дуги вообще
   не заходит в чужую геометрию. Отсюда и приказ обхода на будущее
   (см. app.js): зазор от чужой рамки считается ОТ КРИВОЙ (r + видимый
   зазор), не отдельной константой, и обе геометрии (стык и clearance)
   проверяются числом, не одна.

   Один <svg>+<path> убирает весь класс: прямая и дуга — одна и та же
   система координат одного элемента, стыковать в принципе нечего.
   viewBox здесь НЕ единичный квадрат — app.js ставит его в настоящих
   px 1:1 с CSS-размером самого <svg> (viewBox="0 0 W H" = ширина/высота
   бокса), поэтому d — сразу реальные координаты, без масштабирования;
   vector-effect: non-scaling-stroke (общий с .about-line-arc path) тут
   просто на всякий случай, не несёт нагрузки при масштабе 1:1.

   Clearance (уточнено 13.09) — правило и живой пример (путь ожидаемо
   ныряет под нижний край рамки Pic_3, это не дефект) см. в комментарии
   у wireAboutLines в app.js, коротко: нельзя перекрывать собой текст
   и рамки, можно законно скрываться под тем, что рисуется позже в DOM
   и потому ложится поверх.

   Зазор до рамки Pic_3 (SITE-84, круг 6) — dropX в app.js учитывает не
   только r (ширину дуги), но и половину --stroke-w СВОЕГО же штриха:
   путь рисует stroke, центрированный на координате, значит видимый
   край линии стоит на r + strokeW/2 от точки привязки, а не только на
   r. Без этой поправки зазор МЕЖДУ БОКСАМИ сходился числом (r + 9px от
   рамки), а зазор МЕЖДУ ШТРИХАМИ на экране был на strokeW/2 меньше —
   не пересечение (то, что чинил круг 4), а именно нечитаемость: два
   мазка почти впритык, воспринимаются как один двойной, а не как два
   раздельных.

   Одна точка касания остаётся и это не баг: в самом низу дуги corner1
   (где она становится строго горизонтальной) она физически доходит до
   высоты «низа секции», а та и так уходит под рамку Pic_3 — см. вывод
   у wireAboutLines в app.js (SIDE_GAP там не трогать, подробности там). */
.about-line--b-path {
  position: absolute;
  visibility: hidden;
}

/* В. Перемычка Pic_2→спица А — с SITE-84 круга 6 второй подпуть
   (свой M) внутри .about-line--a-path выше, T-стык на спице спицы А,
   не отдельный элемент. Вход обводки Б (левый край Pic_2) с круга 4 —
   хвост .about-line--b-path, тоже не отдельный элемент. В .about-pic--2
   не осталось ни одной собственной линии — обе перемычки строит app.js
   от реальных боксов Pic_2/Pic_1/Pic_3, не от cqw-долей ширины колонок. */

/* ──────────── о проекте: одна колонка ниже 1150px ────────────

   Порог не про нахлёст (его теперь нет ни выше, ни ниже — см.
   комментарий у .about-stage) — про читаемость: двухколоночная
   раскладка при узкой колонке даёт слишком короткую строку. Ниже
   порога переключаемся на поток целиком, одна колонка на всю ширину.

   Порог найден числом, не на глаз — по characters-per-line (CPL),
   стандартной типографской мере читаемости. Кегль абзацев завязан на
   ширину сцены через cqw (`clamp(16px, 1.615cqw, 31px)`, см. выше):
   пока кегль масштабируется пропорционально, CPL держится постоянным
   (замерено playwright по реальным line-box через Range.getClientRects —
   не оценка на глаз): 34.8–41.6 на 1150–1600px, СТАБИЛЬНО. Как только
   кегль упирается в свой пол 16px (дальше сужать шрифт некуда), CPL
   начинает падать при каждом следующем px сужения окна — на 1100px
   кегль уже точно 16.00px (было 16.15 на 1150), и минимальный CPL
   проседает с 34.8 до 31.5.

   Точка, где кегль-формула равна полу, считается аналитически из тех
   же переменных, что и сама формула (--gutter, --container): при
   ширине окна W (без капа контейнера, W<1600) ширина сцены
   ≈0.8698·W (учтён gutter=max(20,6.51vw) с обеих сторон); кегль
   абзаца = 1.615cqw = 0.01615·0.8698·W; это равно 16px при
   W≈1139px. Между запрошенными кандидатами 1150 и 1100 — то есть
   именно там, где и остановилось падение CPL в замере. 1150px —
   ближайшее к 1139 круглое число из кандидатов, гарантирует, что пол
   кегля НИКОГДА не включается, пока активна двухколоночная раскладка.

   Ниже 1150px — тот же поток, что уже был отработан на мобильном
   (одна колонка, картинка над текстом, order по макетной
   последовательности) — просто применяется в более широком диапазоне
   виджета вместо начинавшегося исторически с 767px. */

@media (max-width: 1150px) {
  /* Линии между блоками (SITE-84, заход 2) — только десктоп, решение
     арт-директора 12.09. Тот же порог, что переключает раскладку на
     поток — отдельного под линии не заводим. .about-line--a-path и
     .about-line--b-path сами по себе всё равно не появились бы
     (visibility:hidden без app.js — wireAboutLines проверяет тот же
     matchMedia порог, см. app.js) — display:none безусловно и для них
     тоже, второй независимый слой защиты, не полагаемся на один. */
  .about-line { display: none; }

  /* Колонки (круг 5, десктоп) исчезают из дерева боксов — их дети
     становятся прямыми детьми .about-stage для .about-stage > *
     ниже, тот же приём, что уже есть у .nav-row в шапке. */
  .about-col { display: contents; }

  /* Правка 05.09: колонка была прижата к левому краю, справа пустело
     поле — .about-stage > * ниже никогда не попадал в цель (см.
     комментарий у него), поэтому реальные блоки держали свою старую
     desktop-ширину в cqw (~45-48% широкой сцены) вместо 100% — узкие
     И не по центру одновременно. Чинится по отдельности: центровка —
     на .about-stage целиком (не на тексте внутри — иначе картинки
     остались бы слева), ширина блоков — правильным селектором ниже.

     max-width подобран числом, не на глаз — по CPL (characters per
     line), той же мере, что уже держит двухколоночную раскладку выше
     порога (34.8–41.6, см. комментарий у медиа-запроса). При кегле
     19px (потолок clamp(16px,4.3vw,19px) ниже — активен весь этот
     диапазон ширин, до ~442px) замер дал ~13.3px на символ; 520px по
     ширине блока — это ~39 символов в строке, середина целевого
     диапазона (проверено Range.getClientRects по всем трём текстовым
     блокам на 1149-600px: 37-43 CPL). Ниже ~600px (когда доступная
     ширина сцены падает ниже самого max-width) колонка идёт в 100%
     контейнера как обычно — CPL там ниже (24-35 на 500-360px), это
     уже не про эту правку: та же кегля-формула упирается в пол 16px,
     а поля сцены на таких экранах и так узкие. */
  .about-stage {
    display: flex;
    flex-direction: column;
    gap: clamp(20px, 5vw, 36px);
    max-width: 520px;
    /* SITE-85 (15.09): width/margin-left:50%/transform:translateX(-50%)
       у десктопного .about-stage (своя шкала сцены, 90.01vw, full-bleed
       приём — см. правило выше медиа-запроса) сюда не наследуются как
       единое целое: margin-left:50% всё ещё считался бы от НЕпадженной
       .container (widths ниже 1150px совсем другие), а translateX(-50%)
       — от НОВОЙ ширины после max-width:520px, не от 90.01vw, и вместе
       давали сдвиг на пол-ширины НЕ туда (было измерено: .about-stage
       съезжала на 260px влево от центра при 1100px). Сброс всех трёх —
       возврат к простому auto-центрированию, как было до сегодня. */
    width: auto;
    margin-left: 0;
    transform: none;
    margin-inline: auto;
  }

  /* .about-col — обёртки с display:contents (см. выше) — не имеют
     собственного бокса, поэтому .about-stage > * их НЕ обходит: этот
     комбинатор смотрит на DOM-дерево, а не на дерево флекс-раскладки,
     где дети .about-col подняты наверх. Реальные блоки берём по их
     базовым классам напрямую — это и есть недостающая половина
     починки (без неё центрировался бы пустой контейнер, а блоки
     внутри остались бы той же старой ширины и слева). */
  /* Десктопные position:absolute/top/left/right (SITE-85, 15.09,
     фиксированные из макета позиции картинок/текста) — приём ИМЕННО
     для раскладки колонками; в потоке ниже порога интервал целиком
     держит gap у .about-stage выше, а порядок — order ниже. Без
     сброса все шесть блоков остались бы наложены друг на друга по
     старым абсолютным top/left/right. */
  .about-pic,
  .about-text {
    width: 100%;
    position: static;
    top: auto;
    left: auto;
    right: auto;
  }

  .about-pic--1 { order: 1; }
  .about-text--philosophy { order: 2; }
  .about-pic--2 { order: 3; }
  .about-text--synopsis { order: 4; }
  .about-pic--3 { order: 5; }
  .about-text--strengths { order: 6; }

  /* Выключка по формату на узкой колонке рвёт строки дырами —
     в потоке текст идёт по левому краю. */
  .about-text p {
    text-align: left;
    font-size: clamp(16px, 4.3vw, 19px);
  }

  /* Специфичность как у .about-text--philosophy p (двухколоночная
     раскладка, выше по файлу) — иначе здесь она бы не перебивалась и
     абзац философии зажимало в ~150px. */
  .about-stage .about-text--philosophy p { max-width: none; }

  /* Заголовок был всего на пиксель крупнее текста и переставал
     читаться как заголовок — в узкой колонке контраст нужен явный. */
  .about-title {
    font-size: clamp(19px, 5vw, 23px);
  }

  /* Специфичность выше, чем у desktop-правил (.about-text--N .about-title,
     см. выше) — иначе здесь подставлялись бы десктопные px вместо
     этого узкого vw-диапазона. Один общий отступ достаточен, разные
     по блокам macket-значения — только в двухколоночной раскладке. */
  .about-stage .about-title {
    margin-bottom: clamp(10px, 2.5vw, 16px);
  }

  .about-stage { gap: clamp(24px, 6vw, 40px); }
}

/* ──────────── контакты: секция 8 (десктоп) ────────────

   Сцена была 1920 × 1012.84 — от верха заголовка «КОНТАКТЫ» (canvas
   y=4950) до «конца полотна макета» (5962.84). Эта нижняя цифра
   оказалась неверной: она сверялась с VisDev_04.png (более ранний
   референс), а не с Maket_2.svg. Прямой замер в Maket_2.svg (круг 4):
   baseline кнопки «ДОСТУП В ЗАКРЫТЫЙ РАЗДЕЛ» — canvas y=6042.77, то
   есть на ~80px НИЖЕ старого «конца полотна». Реальная сцена выше.

   Правка: высота сцены увеличена до 1244.47 (тот же приём инженерного
   запаса, что уже применялся в about-stage выше — компенсация
   1012.84/1244.47 = 0.813872 к top%/height% ВСЕХ блоков ДО кнопки
   (рамка, фото, роли, лид, почта/телеграм) отменяет побочный сдвиг от
   увеличения знаменателя — их пиксельная позиция не меняется. Двигается
   только низ сцены и top у .gate-cta — вниз, на измеренное расстояние
   телеграм→кнопка (baseline-baseline 304.94px design-1920, замер
   Maket_2.svg) плюс собственная высота кнопки (99.31px) плюс запас
   ~45px фона под ней (тот же «конец скролла = низ кнопки», см. .gate-cta
   ниже). Кегли по-прежнему считаются от ШИРИНЫ сцены (контейнер капнут
   на 1600, макет — под 1920), высота сцены их не касается. */

/* --btn-h/--fs-btn кнопки «Доступ в закрытый раздел» — через
   var(--scale-w), не cqw, так значение не зависит от container-type
   конкретного элемента. Высота рамки кнопки в Web_Materials_03
   измерена по Maket_2.svg — 99.31px при 1920 (99.31/1920 = 0.051724),
   кегль подтверждён явным font-size:37px в самом файле
   (37/1920 = 0.01927). */
#contacts {
  --btn-h: clamp(40px, calc(var(--scale-w) * 0.051724), 99.31px);
  --fs-btn: clamp(15px, calc(var(--scale-w) * 0.01927), 37px);
}

.contacts-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 1920 / 1244.47;
  container-type: inline-size;
  margin-block: clamp(48px, 5vw, 96px);
}

.contacts-stage > * { position: absolute; }

/* Заголовок стоит на верхней грани рамки и разрывает её. Фон под
   ним — цвет страницы, поэтому линия «расступается», а по бокам
   остаются усы. Именно так это нарисовано у Кости. */
.contacts-title {
  left: 0; right: 0; top: 0;
  margin: 0;
  z-index: 2;
  text-align: center;
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-weight: 700;
  font-size: clamp(28px, 2.656cqw, 51px);   /* 51/1920 */
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--fg);
  line-height: 1;
}

.contacts-title::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Правка арт-директора, круг 4: расстояние от текста до усов
     меньше, замерено. Invisible text-box заголовка в Maket_2.svg —
     472.62/1920 = 24.6% (было 32%, маска гасила линию заметно шире
     самого текста). */
  width: calc(100% * 0.246);
  height: 1.6em;
  background: var(--bg);
  z-index: -1;
}

/* left/width уточнены по Web_Materials_03 (было 4.947%/91.551% —
   калибровка предыдущего пакета): рамка контактов в новом макете шире
   и сдвинута левее — left 59.57/1920 = 3.102%, width 1807.43/1920 =
   94.137% (мерено по крайним точкам двух половин рамки вокруг
   заголовка «КОНТАКТЫ»). */
.contacts-frame {
  left: 3.102%;
  top: 3.214%;      /* 3.949% × 0.813872 — компенсация роста сцены, см. выше */
  width: 94.137%;
  height: 18.834%;  /* 23.141% × 0.813872 */
  border: var(--stroke-w) solid var(--stroke);
  border-radius: var(--radius);
}

/* Правка арт-директора, третий заход — оба спейсера (vh-формулы,
   гонявшиеся за высотой окна) были неверным путём: они добавляли
   в документ лишнюю высоту, чтобы подогнать конец скролла под
   заголовок, и на широких/невысоких по контенту окнах эта лишняя
   высота сама превращалась в черноту под кнопкой (было ~314px на
   1920×1080).

   Правильный инвариант проще: если ПОСЛЕ кнопки в документе ничего
   нет (кроме её собственного margin-block-end у .contacts-stage —
   тех самых «~45px фона», см. комментарий у .gate-cta), то конец
   документа физически совпадает с низом кнопки на любом экране без
   вычислений от vh — scrollY не долистает дальше, потому что дальше
   просто нет контента. Спейсер убран, секция стоит по высоте своего
   содержимого (aspect-ratio сцены + margin-block).

   Заголовок при переходе по якорю по-прежнему прибивается к верху
   через scroll-margin-top у .section (см. выше) — это её штатное
   поведение, спейсер тут был костылём для другой проблемы и его
   отсутствие её не трогает. На окнах, где контента ВЫШЕ секции не
   хватает, чтобы докрутить ровно до «заголовок под шапкой», браузер
   просто останавливается на максимуме — ниже кнопки в это время
   виден только фон страницы, а не пустая прокручиваемая область. */

.person {
  top: 8.132%;      /* 9.992% × 0.813872 — компенсация роста сцены, см. выше */
  width: 18.614%;
  text-align: center;
}

.person--1 { left: 12.670%; }
.person--2 { left: 41.470%; }
.person--3 { left: 70.480%; }

.person__photo {
  margin: 0;
  /* Слот в макете 357.38 × 349.18 — не квадрат, при квадратных
     исходниках 640×640 это дало бы обрез или растяжение на 8px.
     Ошибка макета, а не замысел: приводим слот к квадрату. */
  aspect-ratio: 1 / 1;
}

.person__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: var(--stroke-w) solid var(--stroke);
  border-radius: var(--radius);
}

.person__name {
  margin: clamp(10px, 1.5cqw, 30px) 0 0;
  /* Имена держим в одну строку. «Константин Муравьёв» на 8px шире
     слота, но промежуток между фото 137px — выход незаметен, а
     перенос ломал бы ровный ряд подписей. Роль при этом может
     переноситься: в макете у Кости она тоже в две строки. */
  white-space: nowrap;
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-weight: 700;
  font-size: clamp(15px, 1.563cqw, 30px);   /* 30/1920 */
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--fg);
  line-height: 1.2;
}

.person__role {
  margin: clamp(6px, .6cqw, 12px) 0 0;
  font-size: clamp(14px, 1.563cqw, 30px);
  line-height: 1.25;
  color: var(--text);
}

.contacts-lead {
  left: 0; right: 0;
  top: 52.769%;      /* 64.837% × 0.813872 — компенсация роста сцены, см. выше */
  margin: 0;
  text-align: center;
  font-size: clamp(14px, 1.615cqw, 31px);   /* 31/1920 */
  color: var(--text);
}

/* Правка арт-директора, круг 4: отменяет предыдущую (прижатие к
   левому краю .person--1) — в макете блок почта/телеграм стоит по
   центру сцены вместе с иконками. left:0/right:0 + justify-content:
   center вместо фиксированного left держит центровку при любой
   ширине блока (иконка+текст), не завязываясь на измеренную один раз
   ширину текста, которая зависит от системного фолбэк-шрифта. */
.contacts-mail,
.contacts-tg {
  left: 0; right: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6em;
  font-size: clamp(14px, 1.615cqw, 31px);
}

/* Координаты иконок в макете: почта на 5665.04, телеграм на 5713.46 —
   шаг 48.42px, то есть 4.78% старой высоты сцены (компенсация роста
   ниже применена к обеим строкам одинаково, шаг между ними не
   меняется). */
.contacts-mail { top: 56.956%; }  /* 69.981% × 0.813872 */
.contacts-tg   { top: 60.846%; }  /* 74.761% × 0.813872 */

/* Подписи «Email:»/«Telegram:» — дефолтным цветом, сам адрес/ник —
   белым по умолчанию (как в макете: часть слов белым, часть
   дефолтным), не только на ховере. */
.contacts-label { color: var(--text); }

.contacts-mail a,
.contacts-tg a {
  color: var(--fg);
  text-decoration: none;
  transition: color .2s ease;
}

.contacts-mail a:hover,
.contacts-mail a:focus-visible,
.contacts-tg a:hover,
.contacts-tg a:focus-visible { color: var(--accent); }

/* Иконки — inline SVG, кривые дословно из Maket_2.svg (см. index.html).
   viewBox уже в координатах исходника — масштаб задаём только через
   width/height, цвет заливки фиксирован (значки не перекрашиваются
   по ховеру, так в макете). */
.icon {
  flex: none;
  width: 1.05em;
  height: 1.05em;
}

/* ─────────────────── кнопка закрытого раздела ─────────────────── */

/* Правка арт-директора, круг 4: расстояние от контактов до кнопки
   больше — замерено. Старое "y 5917.9" было приблизительным; прямой
   замер в Maket_2.svg даёт baseline кнопки на canvas y=6042.77 (см.
   комментарий у .contacts-stage — оттуда же и выросшая высота сцены).
   Ниже кнопки остаётся ~45px фона и страница кончается — футера у
   Кости нет. */
.gate-cta {
  left: 0; right: 0;
  top: 88.404%;
  display: flex;
  justify-content: center;
}

.gate-cta .btn-pill {
  background: transparent;
  color: var(--accent);
}

.gate-cta .btn-pill:hover,
.gate-cta .btn-pill:focus-visible { color: var(--fg); }

/* Правка арт-директора: рамка со скруглением, как в макете (Maket_2.svg) —
   раньше кнопки-«ссылки» */
.btn-pill--framed {
  border: var(--stroke-w) solid currentColor;
}

/* ─────────────────────── мобильная версия ─────────────────────── */

/* Меню — раньше сокращённая прокручиваемая строка (Костя бургер
   отверг осознанно, но макета для мобильного не было — состав
   сжимали сами). Web_Materials_02 привёз вертикальный макет: меню
   в две строки, полный состав из пяти пунктов, 3 + 2. Числа —
   из Vertical_v1.svg (доля от ширины/высоты кадра, кадр = .hero,
   он на весь экран, поэтому шапка поверх него мерится в dvh):

     кегль меню      3.96% ширины  (~15.4px на 390)
     поля по бокам   3.30% ширины
     строка 1        2.50% высоты от верха кадра
     строка 2        6.98% высоты
     кнопка тизера    кегль 5.51% ширины (позиция — своя, см. .hero__cta-wrap) */

@media (max-width: 767px) {
  :root {
    --header-h: clamp(72px, 9.6dvh, 92px);
    /* Кегль по макету; clamp страхует край диапазона (макет считан
       на 390 — за пределами узкого мобильного окна пропорция не
       заявлена). */
    --fs-menu: clamp(13px, 3.96vw, 19px);
    --fs-btn: clamp(16px, 5.51vw, 26px);
    /* Правка арт-директора, круг 4: высота кнопки тизера — по замеру
       пилюли в Vertical_v1.svg (было 12vw — не макетное число, а
       эмпирика прошлого захода). rect x=3.61 y=944.41 w=588.22
       h=79.12 rx=21.31 в кадре 783×1305.6 → высота 79.12/783 =
       10.105% ширины кадра. */
    --btn-h: clamp(44px, 10.105vw, 56px);

    /* --stroke-w/--radius (пакет 03) теперь считаются от --cap-w —
       без этого возврата на узких экранах (<768px, --cap-w=100vw)
       линии становились бы тоньше 3px. Вертикальную версию в этом
       заходе не трогаем — держим прежние фиксированные значения. */
    --stroke-w: 3px;
    --radius: 15.71px;
  }

  /* Правка арт-директора, круг 4: горизонтальный отступ текст↔рамка
     должен быть постоянным на любой ширине окна. Раньше паддинг
     (4vw) и кегль --fs-btn (5.51vw) скейлились РАЗНЫМИ
     vw-коэффициентами — на части ширин текст утыкался в рамку. em —
     не vw: паддинг считается от font-size элемента, значит следует
     за --fs-btn один в один при любой ширине, рассинхрон невозможен
     в принципе (Vertical_v1.svg этой кнопки не содержит — макетного
     числа для мобильного отступа нет, взят типовой комфортный отступ
     пилюли, близкий к прежнему на референсной ширине 390). */
  .btn-pill { padding-inline: 1em; }

  /* Правка арт-директора, круг 4: кнопка «Смотреть тизер» на
     мобильном — крупнее, шире, текст по центру. Раньше это была
     растровая Button_1.png (десктопная, пропорция 2379/473=5.029) —
     на мобильном макете своя пилюля с ДРУГОЙ пропорцией (588.22/79.12
     =7.434, Vertical_v1.svg), и картинка её не воспроизводила: на
     той же высоте кнопка получалась заметно ýже нужного. Здесь она —
     CSS-пилюля (та же техника, что у .btn-pill), не растр: текст
     Кости в самом Vertical_v1.svg уже отцентрован (проверено рендером
     реальных метрик шрифта, расхождение <0.1px по обеим осям) — в
     отличие от десктопной кнопки, никаких dx/dy-поправок не нужно,
     обычная flex-центровка. */
  .hero__cta-wrap .btn-image picture { display: none; }

  .hero__cta-wrap .btn-image {
    width: auto;
    /* На очень узких окнах (320px, iPhone SE 1) высота упирается в
       44px-пол --btn-h раньше, чем ширина вписывается в экран —
       7.434 при 44px даёт 327px, шире вьюпорта. max-width страхует:
       пропорция чуть просядет на этой одной ширине, зато без
       горизонтального оverflow страницы. */
    max-width: calc(100vw - 48px);
    aspect-ratio: 7.434;   /* 588.22 / 79.12, Vertical_v1.svg */
    background: var(--bg);
    border-radius: var(--radius);
    /* Тень — тот же filter drop-shadow-1 из Vertical_v1.svg (feOffset
       dx -2 dy 8, feGaussianBlur stdDeviation 7, feFlood #010101 .34),
       что оборачивает именно ЭТУ пилюлю в файле (класс st7) — раньше
       была по ошибке навешена на .btn-pill (кнопку «Доступ…», у
       которой в файле такого фильтра нет вообще). Радиус растяжки в
       box-shadow — примерно 2× stdDeviation. */
    box-shadow: -2px 8px 14px rgba(1, 1, 1, .34);
  }

  .hero__cta-wrap .btn-image__label {
    position: static;
    transform: none;   /* макетный вектор уже отцентрован, без поправок */
  }

  /* Правка арт-директора, круг 4: nav растягивается на всю высоту
     шапки (не центрируется по контенту) — иначе dvh-координаты
     строк меню ниже отсчитывались бы не от верха «кадра», а от
     произвольно сдвинутого центрированием верха .site-nav. */
  .site-header { align-items: stretch; }

  .site-nav {
    position: relative;   /* база для абсолютной .nav-row--2 ниже */
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    padding-inline: 3.30vw;
    padding-top: 2.50dvh;
    gap: 0;
  }

  .nav-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 4vw;
  }

  /* Межстрочный шаг замерен в Vertical_v1.svg: baseline строки 1 на
     2.503% высоты кадра, baseline строки 2 на 6.98% — шаг 4.478dvh.
     Прежний подход (margin-top после потока) плыл, потому что
     реальная line-box строки 1 в браузере выше макетного шага — и
     плюс margin-top СВЕРХ него давал ещё бо́льший интервал, чем
     макетный. Абсолютная позиция от верха .site-nav держит ровно
     макетные 6.98dvh независимо от того, что рендерит браузер для
     строки 1. */
  .nav-row--2 {
    position: absolute;
    left: 3.30vw;
    right: 3.30vw;
    top: 6.98dvh;
  }

  /* Полоса ховера на тач-экранах бесполезна, но активный пункт
     подсвечивать надо — он остаётся. */
  .site-nav a { padding-block: 2px; }

  /* Заставка на мобильном теперь настоящее портретное видео Кости
     (Web_Materials_02, MainScreen_Vertical_v1) — снято под телефонный
     кадр, а не горизонтальное 16:9 с обрезкой. object-fit: cover
     больше не должен жертвовать сторонами ради вписанной надписи:
     полоса, которую раньше держал object-position: 38%, ушла вместе
     с горизонтальным кадром на этой ширине.

     У вертикального ролика та же встроенная шторка сверху (опаково
     чёрная 0–2% высоты кадра), что и у горизонтального — см. разбор
     у .hero__media выше. При портретном соотношении экрана (высота
     ≥ 0.7645 × ширины — то есть буквально любой телефон в портретной
     ориентации, 390×844 и уже) видео вообще не обрезается по
     вертикали, cover режет только по бокам, и шторка с надписью
     видны целиком независимо от object-position — так на 390 и на
     360, до 767px width.

     top — та же причина, что у горизонтального ролика: при top
     верх видимого окна = верх кадра всегда, значит меню на шторке
     гарантированно, а не только пока повезло с фоном.

     ⚠️ Компромисс, который НЕ снят: при высоте окна < 0.7645 × ширины
     (это только альбомная ориентация телефона — портретные экраны
     сюда не попадают) видео обрезается уже и по вертикали. Тогда
     top обрезает низ надписи «ПОЛИС» (нужна видимая доля кадра
     ≥ 43.0% высоты, у альбомного iPhone SE 667×375 она 31.6% —
     не хватает). center вместо обрезки кладёт меню поверх надписи
     — нечитаемо и то, и другое. Ни одно значение object-position
     не даёт одновременно чистое меню и целую надпись на такой высоте
     — выбран top, потому что меню важнее для навигации, а обрезанный
     логотип не ломает функциональность. Численно: конфликт начинается
     ниже высоты 0.7645 × ширины (~586px при 767px, ~298px при 390px
     — реальных портretных телефонов это не касается, только поворот
     в альбомную). */
  .hero__media { object-position: center top; }
}

@media (max-width: 767px) {
  /* Контакты: сцена разбирается в поток. Рамку убираем — она держится
     на том, что фото пересекают её грань, а в одну колонку это не
     читается. */
  .contacts-stage {
    position: static;
    aspect-ratio: auto;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: clamp(18px, 4vw, 28px);
  }

  .contacts-stage > * {
    position: static;
    left: auto; right: auto; top: auto;
    width: 100%;
  }

  .contacts-frame { display: none; }

  /* Правка арт-директора, круг 4: усы по бокам заголовка «КОНТАКТЫ»
     на мобильном раньше отсутствовали совсем (десктопная ::before-
     маска здесь не работает — рамки нет, гасить нечего). Vertical_v1.svg
     контактов не содержит (только главный экран) — макетного числа
     нет, толщина линий берётся от --stroke-w, тот же приём
     «текст-между-линиями», что и на десктопе визуально. */
  .contacts-title {
    display: flex;
    align-items: center;
    gap: 3vw;
    margin-bottom: clamp(6px, 2vw, 14px);
  }

  .contacts-title::before,
  .contacts-title::after {
    content: '';
    /* Сброс десктопного ::before (position:absolute/inset-центровка,
       см. выше) — без этого ус слева оставался абсолютно
       спозиционированной невидимой маской вместо строчного флекс-
       элемента, ус был только справа (у ::after, десктоп его не
       трогал). */
    position: static;
    inset: auto;
    transform: none;
    z-index: auto;
    display: block;
    flex: 1 1 auto;
    height: var(--stroke-w);
    background: var(--stroke);
  }

  /* Человек — строкой: фото слева, подпись справа. Вертикальными
     карточками по 320px три портрета растягивали экран на добрую
     тысячу пикселей, а лица всё равно рассматривать незачем — важнее
     имя и роль. */
  /* Три элемента в двух колонках раскладывались сами и уводили роль
     под фото — размещаем явно: фото слева на обе строки, имя и роль
     справа друг под другом. */
  .person {
    display: grid;
    grid-template-columns: clamp(96px, 27vw, 132px) 1fr;
    grid-template-rows: auto auto;
    column-gap: clamp(14px, 4vw, 22px);
    row-gap: 4px;
    align-content: center;
    text-align: left;
  }

  .person__photo { grid-column: 1; grid-row: 1 / 3; }

  .person__name {
    grid-column: 2;
    grid-row: 1;
    align-self: end;
    margin: 0;
    white-space: normal;      /* в узкой колонке перенос уместен */
    font-size: clamp(16px, 4.4vw, 19px);
  }

  .person__role {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    margin: 0;
    font-size: clamp(14px, 3.9vw, 17px);
  }

  /* Правка арт-директора, круг 4: расстояние от последнего фото до
     этой строки — больше (Vertical_v1.svg контактов не содержит,
     макетного числа нет — инженерная оценка, не замер). */
  .contacts-lead {
    margin-top: clamp(20px, 7vw, 36px);
    font-size: clamp(15px, 4vw, 18px);
    text-align: left;
  }

  /* Почта — полноценная тач-цель: 170x17 пальцем не взять. */
  .contacts-mail,
  .contacts-tg {
    justify-content: flex-start;
    font-size: clamp(15px, 4.2vw, 19px);
  }

  /* Правка арт-директора, круг 4: Email/Telegram — единый плотный
     блок, а не два пункта с тем же интервалом, что между остальными
     блоками сцены (тот же общий flex-gap у .contacts-stage). Тянем
     строку теlegram ближе к почте отрицательным margin-top поверх
     общего gap (макетного числа нет — см. выше). */
  .contacts-tg { margin-top: -12px; }

  .contacts-mail a,
  .contacts-tg a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: var(--fg);
  }

  /* Правка арт-директора, круг 4: иконки почты/телеграма крупнее
     (макетного числа нет, см. выше — инженерная оценка). */
  .icon { width: 1.4em; height: 1.4em; }

  .gate-cta { margin-top: clamp(10px, 3vw, 24px); }

  /* Кнопки: 44px — нижняя граница уверенного попадания пальцем. */
  .btn-pill { min-height: 44px; }
}

/* ═══════════ гейт пароля + чтение сценария (SITE-81) ═══════════

   Перенесено из колоды (index.html до SITE-79) — механика (SHA-256
   хэш, localStorage-разблокировка на 30 дней, один скролл-ридер с
   музыкой и регулировкой кегля) не менялась, только оформление:
   токены лендинга вместо палитры/шрифтов колоды (Disket Mono, Space
   Grotesk, IBM Plex Mono, #D8583F) — везде --bg/--stroke/--fg/--text/
   --accent/--radius/--stroke-w и POLIS Text/Text Head. Острые
   ГОСТ-засечки колоды не переносим — не язык этой страницы. */

#accessGate {
  position: fixed;
  inset: 0;
  z-index: 10000;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

#accessGate[data-open] { display: flex; }

.access-gate-panel {
  position: relative;
  width: min(420px, 100%);
  background: var(--bg);
  border: var(--stroke-w) solid var(--stroke);
  border-radius: var(--radius);
  padding: clamp(28px, 4vw, 40px) clamp(24px, 3.5vw, 36px);
  box-sizing: border-box;
}

.access-gate-cancel {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 28px;
  height: 28px;
  background: transparent;
  border: none;
  color: var(--menu);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: color .2s ease;
}

.access-gate-cancel:hover { color: var(--fg); }

.access-gate-eyebrow {
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 12px;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 10px;
}

.access-gate-title {
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-size: clamp(18px, 2.2vw, 22px);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--fg);
  line-height: 1.4;
}

#accessGateInput {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: var(--stroke-w) solid var(--stroke);
  border-radius: calc(var(--radius) * .35);
  color: var(--fg);
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 15px;
  letter-spacing: .08em;
  padding: 14px 16px;
  text-transform: uppercase;
  transition: border-color .2s ease;
  margin-top: 24px;
}

#accessGateInput:focus { outline: none; border-color: var(--accent); }

.access-gate-submit { width: 100%; margin-top: 14px; }

.access-gate-error {
  margin-top: 14px;
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--accent);
  text-transform: uppercase;
  opacity: 0;
  transition: opacity .2s ease;
}

.access-gate-error[data-visible] { opacity: 1; }

/* ──────────── чтение сценария ──────────── */

.script-reading {
  --reading-font-scale: 1;
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: var(--bg);
  display: none;
  opacity: 0;
  transition: opacity .22s ease;
}

.script-reading[data-open] { opacity: 1; }

.script-reading-topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: clamp(16px, 2.5vw, 22px) clamp(20px, 3vw, 32px);
  background: linear-gradient(180deg, var(--bg) 0%, transparent 100%);
}

.script-reading-eyebrow {
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 11px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--menu);
}

.script-reading-actions { display: flex; align-items: center; gap: 10px; }

.script-sound-btn,
.script-close-btn {
  background: var(--bg);
  border: var(--stroke-w) solid var(--stroke);
  border-radius: calc(var(--radius) * .35);
  color: var(--fg);
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  cursor: pointer;
  padding: 10px 14px;
  display: flex;
  align-items: center;
  gap: 8px;
  transition: border-color .2s ease, color .2s ease;
}

.script-sound-btn:hover,
.script-close-btn:hover { border-color: var(--accent); color: var(--accent); }

.script-text-size {
  display: flex;
  align-items: stretch;
  background: var(--bg);
  border: var(--stroke-w) solid var(--stroke);
  border-radius: calc(var(--radius) * .35);
  overflow: hidden;
}

.script-text-btn {
  background: transparent;
  border: none;
  color: var(--fg);
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-size: 13px;
  cursor: pointer;
  padding: 10px 13px;
  transition: color .2s ease, background .2s ease;
}

.script-text-btn:hover { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
.script-text-btn[disabled] { opacity: .35; cursor: default; pointer-events: none; background: none; color: var(--fg); }

.script-text-label {
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 10px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--menu);
  padding: 0 10px;
  border-left: var(--stroke-w) solid var(--stroke);
  border-right: var(--stroke-w) solid var(--stroke);
  display: flex;
  align-items: center;
}

.script-reading-scroll { position: absolute; inset: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }

.script-reading-inner {
  max-width: 760px;
  margin: 0 auto;
  padding: clamp(96px, 13vw, 130px) clamp(20px, 4vw, 32px) 120px;
  box-sizing: border-box;
}

.script-reading-hero { text-align: center; margin-bottom: clamp(40px, 7vw, 64px); }

.script-reading-kicker {
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 12px;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--accent);
}

.script-reading-title {
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-size: clamp(40px, 8vw, 72px);
  line-height: 1;
  text-transform: uppercase;
  color: var(--fg);
  margin: 16px 0;
}

.script-reading-format {
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: 13px;
  letter-spacing: .08em;
  color: var(--menu);
}

.script-section { margin-bottom: clamp(40px, 6vw, 56px); }

.script-section-label {
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-size: 11px;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 18px;
  padding-bottom: 10px;
  border-bottom: var(--stroke-w) solid var(--stroke);
}

.script-body {
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: calc(15px * var(--reading-font-scale));
  line-height: 1.85;
  color: var(--text);
  margin: 0 0 18px;
  text-align: left;
}

.script-body strong { color: var(--fg); font-weight: 700; }

.script-character { margin-bottom: 36px; }
.script-character:last-child { margin-bottom: 0; }

.script-character-name {
  font-family: 'POLIS Text Head', var(--font-fallback);
  font-size: calc(24px * var(--reading-font-scale));
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 14px;
}

.script-act { margin-bottom: 32px; }
.script-act:last-child { margin-bottom: 0; }

.script-act-title {
  font-family: 'POLIS Text', var(--font-fallback);
  font-size: calc(13px * var(--reading-font-scale));
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--fg);
  margin-bottom: 14px;
}

@media (max-width: 767px) {
  .script-reading-eyebrow { display: none; }
  .script-reading-inner { padding: 90px 20px 90px; }
  .script-sound-btn .btn-label,
  .script-close-btn .btn-label { display: none; }
  .script-text-label { display: none; }
  .script-text-size { border-left: none; border-right: none; }
  .script-text-btn { padding: 10px 9px; }
  .script-reading-actions { gap: 6px; }

  /* Развели управление по краям: тянуться по диагонали от крестика
     к музыке/кеглю неудобно. Крестик выходит из общего flex-ряда
     топбара в левый верхний угол, звук+кегль остаются группой и
     прижимаются к правому краю — оба через position:fixed, поэтому
     порядок в разметке (см. index.html) на раскладку не влияет.
     На десктопе (эта секция не действует) всё как было. */
  .script-close-btn {
    position: fixed;
    top: clamp(12px, 3vw, 18px);
    left: clamp(12px, 3vw, 18px);
    z-index: 3;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    justify-content: center;
  }
  .script-reading-actions {
    position: fixed;
    top: clamp(12px, 3vw, 18px);
    right: clamp(12px, 3vw, 18px);
    z-index: 3;
  }
  .script-sound-btn {
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    justify-content: center;
  }
  .script-text-btn { min-width: 44px; min-height: 44px; }
}
