/* ===========================================================================
 * Стили главной и /site/
 * ---------------------------------------------------------------------------
 * Приехали из bench.css 21.08. Пока они лежали там, обе продовые страницы
 * тянули весь стендовый лист - 202 КБ, из которых им нужно меньше половины.
 *
 * Почему ОТДЕЛЬНЫЙ файл, а не main.css: порядок подключения
 * main -> screen -> paper -> pages -> bench. Уехав в main.css, эти правила
 * начали проигрывать paper.css. ЗАМЕРЕНО отпечатком страницы: .case__o > *
 * переставал убирать margin-top 44.8px, и отчёт на главной уезжал вниз
 * ровно на 45. Здесь у них тот же вес, что был в bench.css.
 *
 * Состав определён замером, а не на глаз: на обеих страницах перебраны все
 * 894 правила bench.css и проверено, совпадает ли селектор хоть с одним
 * элементом. Псевдоклассы и значения атрибутов при проверке срезались,
 * поэтому :hover и [data-week-on="off"] не потерялись.
 *
 * Стенды ничего не потеряли: pages.css подключается на всех страницах.
 * ========================================================================= */

/* ==========================================================================
   Home bench, /_home/ : the display case
   --------------------------------------------------------------------------
   Ось A, ратифицирована PO. Показать конечное состояние ПРЕДМЕТАМИ, а не
   аргументом. Каждый выходит на сцену крупно и потом занимает место на полке.
   К финалу полка полна, и над ней одна строка.

   Раскладка задана ОДИН РАЗ и в жёстких единицах. До этого она собиралась из
   заплаток: размер выводился процентом от соседа, сосед от контейнера,
   контейнер от содержимого, и каждая следующая правка ломала предыдущую.
   Единственная вычисляемая величина здесь это --fit, множитель «во сколько раз
   уменьшить предмет, чтобы он поместился в свою клетку»; его считает скрипт по
   ЗАМЕРУ, а не по пропорции.

   Экраны чужих продуктов (выдача, карточка в картах, лента, чат) говорят своим
   языком, а не нашей палитрой: #fff бумага, #08080b тушь, родные для этих
   интерфейсов синий и зелёный. Тот же приём, что на /money-trail/. Наши токены
   держат ОБСТАНОВКУ вокруг них: поле секции, тип, полка, ритм.
   ========================================================================== */

.case-sect {
  background: var(--panel-dark);
  color: var(--on-dark);
  /* Секция помечена [data-dark-top], значит шапка плывёт над ней. Без
     подтяжки под шапкой остаётся дыра в её высоту. */
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}

/* --- Композиция экрана ----------------------------------------------------
 *
 * Слева колонка чтения: заголовок, реплика акта, выбор ремесла, полка и
 * оговорка. Справа предмет во всю высоту сцены. Три блока подряд сверху вниз
 * оставляли справа дыру во весь экран, а предмет выдавливали в нижнюю треть.
 */

.case-pin { grid-template-rows: minmax(0, 1fr); padding-bottom: var(--sp-l); }

/* --- Пространство за предметом --------------------------------------------
 *
 * Тёмное поле было плоским листом: предмет лежал на нём, а не стоял в глубине.
 * Глубину дают два слоя, ни один из которых не рисует объектов: свет бренда
 * позади и затемнение к краям.
 *
 * Свет нарисован ГРАДИЕНТОМ, а не компонентом .bloom с главной, хотя цвета
 * взяты у него же (#0130F9, #00FFFF, #74FF80). Причина в цене: .bloom это SVG
 * с feGaussianBlur 103 и mix-blend-mode: hard-light. Режим наложения запрещает
 * браузеру закешировать слой, и вся эта площадь пересчитывается каждый кадр
 * прокрутки, потому что поверх неё каждый кадр меняется прозрачность
 * предметов. Градиент композитится даром и выглядит на 0.17 так же.
 */
.case-pin::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(46% 40% at 60% 40%, rgb(1 48 249 / 0.30), transparent 70%),
    radial-gradient(38% 34% at 70% 60%, rgb(0 255 255 / 0.12), transparent 72%),
    radial-gradient(30% 30% at 54% 70%, rgb(116 255 128 / 0.08), transparent 74%);
}
.case-pin::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background:
    radial-gradient(70% 55% at 63% 47%, rgb(255 255 255 / 0.055), transparent 70%),
    radial-gradient(120% 90% at 63% 47%, transparent 30%, rgb(0 0 0 / 0.5) 100%),
    linear-gradient(180deg, rgb(0 0 0 / 0.35) 0%, transparent 22%, transparent 66%, rgb(0 0 0 / 0.45) 100%);
}
/* Содержимое стоит НАД светом, иначе оно тонет в нём. */
.case__wrap { position: relative; z-index: 1; }

.case__wrap {
  display: grid;
  /* Четыре ребёнка, четыре строки. С тремя четвёртый уезжал в неявную строку,
     сцена получала auto и схлопывалась в ноль: на узком экране предметы
     оставались в натуральную величину и вылезали за кадр, потому что --fit
     считается от клетки, а клетка была нулевой. */
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  gap: var(--sp-m);
  min-height: 0;
}

@media (min-width: 62rem) {
  .case__wrap {
    /* 42rem, не 30: на --step-5 «Five places he decides.» в 30rem шло ЧЕТЫРЬМЯ
       строками и выбор ремесла ложился на подпись акта (замерено, наложение
       439 точек). В 42rem заголовок в две строки, столбец сходится. */
    grid-template-columns: minmax(0, 42rem) minmax(0, 1fr);
    /* Строки объявляются заново: в одну колонку их четыре, здесь полка стоит
       в одной строке с выбором ремесла, а сцена тянется через все три. Без
       этого от базовой раскладки оставалась пустая четвёртая строка. */
    grid-template-rows: auto auto minmax(0, 1fr);
    grid-template-areas:
      "head  stage"
      "pick  stage"
      "foot  stage";
    column-gap: var(--sp-xl);
    align-content: stretch;
  }
  .case__head  { grid-area: head; }
  /* Выбор был приклеен к заголовку, а под ним стояла пустота во весь столбец.
     Отступ отделяет его от заголовка и отдаёт часть пустоты вверх. */
  .case__pick  { grid-area: pick; align-self: start; margin-top: var(--sp-l); }
  .case__foot  { grid-area: foot; align-self: end; }
  .case__stage { grid-area: stage; }
}

/* --- Голова: заголовок стоит, реплики сменяются ---------------------------
 *
 * Заголовок берёт шкалу сайта и не пишет свой кегль: h2 сам получает --title,
 * как на любой другой секции. Меняется он один раз, в финале, и второй
 * экземпляр лежит поверх первого, чтобы смена шла на месте.
 */

.case__titles { position: relative; }
/* Оба титула витрины - герой (первый h1, второй его смена в финале),
   поэтому ступень героя стоит на классе, а не на теге: иначе h2-двойник
   остался бы на --title и смена читалась бы скачком кегля. */
.case__title { font-size: var(--hero); transition: opacity 0.45s ease; }
.case__title + .case__title { position: absolute; inset: 0 auto auto 0; }
.case__title[data-on="no"] { opacity: 0; }

/* Реплика акта живёт под полкой: горящая ячейка называет акт, строка под ней
   его рассказывает. Наверху она несла кикер с тем же словом, что и полка. */
/* Высота под САМУЮ ВЫСОКУЮ реплику, замерено: две строки по 30px = 60px.
   Меньше, и последняя строка налезала бы на оговорку под ней. */
.case__lines { position: relative; min-height: 3.9rem; }
.case__line {
  position: absolute;
  inset: 0 auto auto 0;
  max-width: 32ch;
  font-size: var(--step-1);
  line-height: 1.25;
  color: var(--on-dark-muted);
  opacity: 0;
  transform: translateY(0.5rem);
  transition: opacity 0.4s ease, transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
  pointer-events: none;
}
.case__line[data-on="yes"] { opacity: 1; transform: none; }
.case__line b { color: var(--white); font-weight: 500; }
/* --- Выбор ремесла --------------------------------------------------------
 *
 * Тот же приём, что на /digital-inventory/: радиокнопка спрятана под своей
 * подписью. Без выбора нишу героя выбираю я, и узнавание срабатывает у одного
 * посетителя из шести. Видно его всю секцию: в прошлой версии он показывался
 * только в нулевом акте, а нулевой акт не наступал ни разу.
 */

.case__pick { display: grid; gap: 0.6rem; }
.case__pick-key { color: var(--grey); }
.case__pills { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.case__p input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.case__p span {
  display: block;
  padding: 0.34rem 0.8rem;
  border: 1px solid var(--line-dark);
  border-radius: 999px;
  font-size: var(--step--1);
  color: var(--on-dark-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}
.case__p:hover span { color: var(--white); border-color: var(--grey); }
.case__p input:checked + span {
  background: var(--blue);
  border-color: var(--blue);
  color: #fff;
}
.case__p input:focus-visible + span { outline: 2px solid var(--blue-lo); outline-offset: 2px; }

/* --- Полка и оговорка ----------------------------------------------------- */

.case__foot { display: grid; gap: var(--sp-m); }

.case__shelf {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.case__slot {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.34rem 0.7rem;
  border: 1px solid var(--line-dark);
  border-radius: 999px;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey);
  transition: color 0.3s ease, border-color 0.3s ease, background 0.3s ease;
}
.case__slot::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.45;
}
.case__slot[data-on="yes"] {
  color: var(--white);
  border-color: rgb(74 108 255 / 0.5);
  background: rgb(74 108 255 / 0.1);
}
.case__slot[data-on="yes"]::before { background: var(--blue-lo); opacity: 1; }

.case__note {
  font-size: var(--step--1);
  color: var(--grey);
  max-width: 46ch;
}

/* --- Сцена и часы предмета ------------------------------------------------
 *
 * Предмет живёт своим временем u: -1 «ещё летит», 0 «на сцене», +1 «уже ушёл».
 * Скрипт публикует u и непрозрачность на каждом кадре скролла, поэтому
 * движение принадлежит предмету, а не переключателю акта. Переходов здесь нет
 * НАМЕРЕННО: значение и так меняется каждый кадр, а transition поверх скролла
 * рвёт картинку. Это уже проходили на /money-trail/.
 *
 * Только сдвиг и прозрачность. Поворот и дробный масштаб на движении мылили
 * текст: он растрируется на нецелой сетке. Масштаб есть только у --fit, и он
 * постоянен внутри акта.
 */

.case__stage { position: relative; min-width: 0; }

.case__o {
  --u: -2;
  --o: 0;
  --fit: 1;
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Не на сцене значит НЕ РИСУЕТСЯ. Прозрачность нулём браузер всё равно
     обсчитывает, а предметов шесть и внутри у них двенадцать плиток, таблица
     отчёта и два устройства. Граница видимости совпала с переключением акта
     специально: кривая доходит до нуля ровно на |u| = 0.5. */
  visibility: hidden;
  opacity: var(--o);
  transform: translate(calc(var(--u) * -6%), calc(var(--u) * -2.5%));
}
.case__o[data-phase="stage"] { visibility: visible; }

/* Лента: сам аппарат НЕ ЕДЕТ. Внутри него идёт прокрутка (--pan и --scroll),
   и если одновременно двигать корпус, читается тряска устройства, а не лента.
   Появление и уход остаются на прозрачности, как у остальных предметов. */
.case__o.o-feed { transform: none; }

/* Предмет свёрстан в жёстких rem, а в клетку садится одним замеренным
   множителем.

   Ставится он абсолютно от центра клетки, а НЕ выравниванием грида. ЗАМЕРЕНО:
   у грид-элемента шире своей дорожки Chrome заменяет center на start, чтобы не
   срезать содержимое, а масштаб потом считается от центра уже сдвинутого
   блока. В финале из-за этого три предмета из шести уезжали на пол-клетки
   вправо и налезали на соседей, а отчёт вылезал за сцену.

   Радиус здесь ДО масштаба: на сцене множитель около 1.3, и 14px печатаются
   примерно двадцатью, то есть радиусом карточки с главной. */
.case__o > * {
  /* Предмет, взятый с другой страницы, приносит с собой её отступы. ЗАМЕРЕНО:
     .dash несёт margin-top 44.8px из paper.css, и отчёт уезжал вниз на эти
     44.8, вылезая из сцены на 33, притом что размер был посчитан верно. */
  margin: 0;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(var(--fit));
  transform-origin: center;
  border-radius: 14px;
  box-shadow: var(--edge-light), var(--lift-3);
}

/* Ширина у каждого своя, высота из содержимого. Одна ширина на всех не
   работает: лента это три квадрата в столбик, а отчёт таблица. */
.o-search > * { width: 34rem; }
.o-maps   > * { width: 21rem; }
.o-feed   > * { width: 19rem; }
.o-ai     > * { width: 32rem; }
.o-report > * { width: 46rem; }

/* --- Финал: витрина целиком ------------------------------------------------
 *
 * Последний акт был пустой сценой под строкой «шесть мест, одна компания», то
 * есть обещанием без предмета. Раскладка переключается в тот момент, когда на
 * сцене уже пусто (кривая ухода доводит предмет до нуля раньше), а видимость
 * набирается из --fin, поэтому переключения не видно.
 */

/* ⚠️ Здесь стояло `@media (min-width: 60.01rem)` с доводом «ниже 60rem сайт
   распинивает рельсовые секции, финала как акта не существует». Витрина на
   телефоне теперь припинена, финал наступает, и без этой раскладки последний
   такт был ПУСТОЙ СЦЕНОЙ под строкой «шесть мест, одна компания» - ровно тем
   обещанием без предмета, которое эта раскладка и чинила на десктопе.
   Замерено: сцена 390x360 без единого видимого предмета.

   Колонок на телефоне две, а не три: шесть марок в три колонки на 390 точках
   действительно нечитаемы, в две - узнаются. */
.case-pin[data-act="7"] .case__stage {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: min(var(--sp-s), 1.4svh);
}
@media (min-width: 60.01rem) {
  .case-pin[data-act="7"] .case__stage {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: repeat(2, minmax(0, 1fr));
    gap: var(--sp-s);
  }
}
.case-pin[data-act="7"] .case__o {
  visibility: visible;
  /* relative, а не static: клетка обязана оставаться системой координат для
     предмета внутри неё, иначе он центрируется по всей сцене. */
  position: relative;
  inset: auto;
  opacity: var(--fin, 1);
  transform: none;
}
/* Предметы стоят на общей линии в каждом ряду, а не висят по центру своих
   клеток. Это и есть полка: шесть разных по росту вещей, поставленных рядом. */
.case-pin[data-act="7"] .case__o > * {
  top: auto;
  bottom: 0.6rem;
  transform: translateX(-50%) scale(var(--fit));
  transform-origin: bottom center;
}

/* ==========================================================================
   1. Выдача
   ========================================================================== */

.sr { background: #fff; color: #08080b; padding: 1.3rem 1.5rem; text-align: left; }
.sr__url { font-size: 0.78rem; color: #4d5156; }
.sr__url i { font-style: normal; color: #9aa0a6; }
.sr__t { font-size: 1.28rem; color: #1a0dab; line-height: 1.3; margin-top: 0.15rem; }
.sr__d { font-size: 0.86rem; color: #4d5156; margin-top: 0.3rem; max-width: 62ch; }
.sr__m { font-size: 0.82rem; color: #4d5156; margin-top: 0.35rem; }
.sr__m b { color: #202124; }
.sr__links {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.1rem;
  margin: 0.55rem 0 0;
  padding: 0;
}
.sr__links li { font-size: 0.82rem; color: #1a0dab; }

/* ==========================================================================
   2. Карточка в картах
   ========================================================================== */

.gb { background: #fff; color: #08080b; padding: 1rem 1.1rem 1.1rem; text-align: left; }

/* Карта настоящая, тайлами OpenStreetMap. Нарисованная сетка серых
   прямоугольников не похожа ни на одну карту на свете, и это было видно
   сразу. Источник указан в углу, как требует лицензия. */
.gb__map { position: relative; aspect-ratio: 5 / 2; border-radius: 8px; overflow: hidden; margin-bottom: 0.8rem; }
.gb__map img { display: block; width: 100%; height: 100%; object-fit: cover; }
.gb__osm {
  position: absolute;
  right: 4px;
  bottom: 3px;
  font-size: 0.46rem;
  color: rgb(0 0 0 / 0.55);
  background: rgb(255 255 255 / 0.72);
  padding: 0 3px;
  border-radius: 3px;
}
.gb__pin {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 15px;
  height: 15px;
  margin: -17px 0 0 -7.5px;
  border-radius: 50% 50% 50% 0;
  transform: rotate(-45deg);
  background: #ea4335;
  box-shadow: 0 2px 5px rgb(0 0 0 / 0.45);
}
.gb__pin::after { content: ""; position: absolute; inset: 4.5px; border-radius: 50%; background: #fff; }

.gb__n { display: flex; align-items: center; gap: 0.5rem; font-size: 1.05rem; font-weight: 500; }
.gb__r { font-size: 0.84rem; color: #4d5156; margin-top: 0.15rem; }
.gb__r span { color: #f5a623; letter-spacing: -1px; }
.gb__c, .gb__o { font-size: 0.82rem; color: #4d5156; }
.gb__o { color: #1e8e3e; }
.gb__btns { display: flex; gap: 0.4rem; margin-top: 0.7rem; }
.gb__b { padding: 0.3rem 0.85rem; border: 1px solid #dadce0; border-radius: 999px; font-size: 0.78rem; color: #1a73e8; }
.gb__b--go { background: #1a73e8; color: #fff; border-color: #1a73e8; }

/* ==========================================================================
   3. Лента, нарисованная как конечное состояние
   --------------------------------------------------------------------------
   Серые плитки были подделкой: они не показывали ни жанра, ни марки. Лента в
   этих ремёслах состоит из двух жанров и ровно двух: до и после, и текстовая
   обложка в фирменном шаблоне. Второе это буквально то, что производит Brand
   Kit, поэтому предмет показывает Brand Kit в работе, а не рассказывает о нём.

   Два фирменных цвета приходят от выбора ремесла, поэтому шесть лент читаются
   как шесть компаний, а не как одна в шести шляпах.
   ========================================================================== */

/* Аппарат. Пропорция настоящая: 19 на 41 рем это 9:19.4, как у телефона в
   руке. Прошлая версия высоты не задавала вовсе, брала её из содержимого и
   выходила приземистой, как планшет: девять плиток 4:5 просто не набирают
   высоту телефона. Плитки теперь 9:16, потому что открыт таб REELS, и три ряда
   таких плиток ровно заполняют экран, а нижний ряд режется его краем, как на
   настоящем аппарате.

   Белая карточка посреди тёмного экрана переставала читаться как приложение,
   поэтому лента живёт в телефоне, а не на карточке. */
.fone {
  width: 19rem;
  height: 41rem;
  background: #0c0e12;
  border-radius: 2.5rem;
  padding: 0.5rem;
  display: grid;
}
.fone {
  box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / 0.18), var(--lift-3);
}

/* Предмет это аппарат ПЛЮС точки под ним, поэтому карточную оправу с общего
   правила снимаем: рамку и тень несёт сам аппарат. */
.case__o > .feed { border-radius: 0; box-shadow: none; background: none; }
.feed { display: grid; justify-items: center; gap: 0.9rem; }

/* Четыре точки: сколько площадок и на какой сейчас. Активная это не кружок
   покрупнее, а полоска: различаем формой, а не оттенком, как и везде. */
.feed__dots { list-style: none; margin: 0; padding: 0; display: flex; gap: 0.4rem; }
.feed__dots li {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 999px;
  background: var(--grey);
  transition: width 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.28s ease;
}
.feed__dots li[data-on="yes"] { width: 1.5rem; background: var(--blue-lo); }
.fone__screen {
  position: relative;
  background: #fff;
  border-radius: 2.1rem;
  overflow: hidden;
  display: grid;
  /* Колонка ЗАДАНА. Неявная колонка грида меряется по max-content, а внутри
     лежит дорожка из четырёх экранов: панель выходила 473px вместо 289 и
     торчала за края аппарата. Третий случай той же ошибки в этой секции:
     неявная дорожка всегда меряется содержимым. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
}
/* Островок камеры. Одна деталь, без которой это не телефон. */
.fone__screen::before {
  content: "";
  position: absolute;
  z-index: 3;
  top: 0.3rem;
  left: 50%;
  transform: translateX(-50%);
  width: 4.2rem;
  height: 1.05rem;
  border-radius: 999px;
  background: #0c0e12;
}
.fone__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.45rem 1rem 0.3rem;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.56rem;
  font-weight: 600;
  color: #262626;
}
.fone__bar i { width: 1.5rem; height: 0.38rem; border-radius: 2px; background: #262626; opacity: 0.55; }

/* --- Четыре площадки в одном аппарате --------------------------------------
 *
 * Дорожка из четырёх панелей шириной в экран каждая. Перелистывание считает
 * скрипт из времени предмета: панель СТОИТ две трети своего отрезка и едет
 * последнюю треть, а пока стоит, внутри неё едет сама лента. Непрерывно, без
 * переходов: значение меняется каждый кадр скролла.
 */
.pan {
  display: flex;
  width: 100%;
  min-width: 0;
  min-height: 0;
  transform: translateX(calc(var(--pan, 0) * -100%));
  will-change: transform;
}
.pan__p {
  flex: 0 0 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: #fff;
  color: #08080b;
  text-align: left;
  display: grid;
  /* Колонка ЗАДАНА у каждой панели. Третий случай одной и той же ошибки в этой
     секции: неявная дорожка грида меряется по max-content, и сетка ленты
     раздувала колонку шире аппарата, оставляя видимой только левую треть. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
}
/* Окно прокрутки. Содержимое выше окна и сдвигается ровно на ту долю, которую
   панель уже отстояла. */
.pan__win { --scroll: 0px; overflow: hidden; min-height: 0; }
.pan__win > * { display: block; transform: translateY(var(--scroll)); will-change: transform; }

.ig {
  --ig-a: #14213d;
  --ig-b: #5b8def;
  padding: 0.7rem 0.7rem 0;
  grid-template-rows: auto auto minmax(0, 1fr);
}

.ig__top { display: flex; gap: 0.9rem; align-items: flex-start; }
.ig__who { min-width: 0; }
.ig__h { display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; }
.ig__h i { font-style: normal; font-size: 0.7rem; background: #0095f6; color: #fff; padding: 0.16rem 0.6rem; border-radius: 7px; }
.ig__st { font-size: 0.78rem; color: #262626; margin-top: 0.3rem; }
.ig__bio { font-size: 0.76rem; color: #262626; margin-top: 0.3rem; line-height: 1.35; }

.ig__tabs {
  display: flex;
  justify-content: center;
  gap: 2.2rem;
  margin-top: 0.9rem;
  border-top: 1px solid #dbdbdb;
  padding-top: 0.55rem;
}
.ig__tabs span {
  font-size: 0.56rem;
  letter-spacing: 0.14em;
  color: #8e8e8e;
  padding-bottom: 0.5rem;
  border-top: 1px solid transparent;
  margin-top: -0.56rem;
  padding-top: 0.5rem;
}
.ig__tabs span[data-on="yes"] { color: #262626; border-top-color: #262626; }

/* Лента едет вместе с проходом предмета по сцене: статичная сетка это
   скриншот, а не лента. Сетка выше своего окна и сдвигается ровно на ту долю,
   которую предмет уже прошёл. */
.ig__grid { --scroll: 0px; overflow: hidden; min-height: 0; margin: 0.3rem -0.7rem 0; position: relative; }
/* Двенадцать плиток ехали ДВЕНАДЦАТЬЮ трансформами, то есть двенадцатью
   слоями композитора. Едет одна обёртка. */
.ig__roll {
  transform: translateY(var(--scroll));
  will-change: transform;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-content: start;
  /* Высота строки задана числом, а не выведена из aspect-ratio плитки.
     ЗАМЕРЕНО: у растянутого грид-элемента aspect-ratio в расчёт строки не
     попадает, строка выходила 110.6px при высоте плитки 168px, и плитки
     налезали друг на друга на треть. Арифметика здесь: экран внутри рамки
     18rem, минус два зазора по 2px, делить на три = 5.92rem колонка, на 16/9
     = 10.5rem строка. Меняется ширина телефона - меняется и это число. */
  grid-auto-rows: 10.5rem;
  gap: 2px;
}

/* Плитка: плоская заливка, крупный конденсированный тип во всю плитку, номер
   и счётчик по углам. Кегль ЗАДАН, а не выведен из вьюпорта: предмет теперь
   фиксированной ширины, и вьюпортные единицы внутри него означали бы, что на
   широком экране тип вылезает из плитки. */
.ig__c {
  position: relative;
  display: grid;
  align-content: space-between;
  height: 10.5rem;
  overflow: hidden;
  padding: 0.42rem 0.45rem;
  background: var(--ig-a);
  color: #fff;
}

/* Маркер reels: это видео, а не фотографии. */
.ig__c::before {
  content: "";
  position: absolute;
  top: 0.45rem;
  right: 0.45rem;
  border: 5.5px solid transparent;
  border-left-color: currentColor;
  border-right-width: 0;
  opacity: 0.85;
  z-index: 2;
}

.ig__n {
  font-style: normal;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.5rem;
  letter-spacing: 0.16em;
  opacity: 0.6;
}
/* Кегль обложки посажен по САМОМУ ДЛИННОМУ слову, а не на глаз: в плитке
   81px чистой ширины, и при 0.86rem «STOREFRONT» набирало 95px и уезжало за
   край. Слова длиннее девяти знаков из копии убраны, кегль опущен до 0.82rem
   (самое длинное теперь «PUBLISHED», 76px), а overflow-wrap стоит страховкой
   на случай, если кто-то впишет слово длиннее. */
.ig__say {
  font-style: normal;
  font-family: Asgard, Impact, sans-serif;
  font-size: 0.82rem;
  overflow-wrap: anywhere;
  line-height: 0.92;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  align-self: end;
}
/* Счётчик стоит наверху, у номера. Внизу он ложился прямо на строку обложки:
   «104K» печаталось поверх «STAYS ON». Тип владеет низом плитки целиком. */
.ig__v {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.5rem;
  opacity: 0.62;
}
.ig__head { display: flex; gap: 0.4rem; align-items: baseline; position: relative; z-index: 1; }

/* Снятое: один ролик играет, четыре кадра стоят. В настоящей ленте играет
   плитка в фокусе, остальные стоят; девять одновременно дёргающихся видео это
   не лента, это припадок.

   Съёмка принадлежит одному ремеслу (мойка окон, BAWC), поэтому и показывается
   только в нём: подставить её кровельщику значит соврать первым же кадром. */
.ig__c--live, .ig__c--pic { background: #0e0f13; }
.ig__vid, .ig__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ig__c--live::after, .ig__c--pic::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgb(0 0 0 / 0.45) 0 26%, transparent 46%, rgb(0 0 0 / 0.62) 100%);
}
.ig__c--live .ig__say, .ig__c--pic .ig__say { position: relative; z-index: 1; text-shadow: 0 1px 12px rgb(0 0 0 / 0.75); }
.ig__c--live .ig__head, .ig__c--pic .ig__head { text-shadow: 0 1px 8px rgb(0 0 0 / 0.7); }
/* Треугольник стоит на КАЖДОЙ плитке: открыт таб REELS, и каждая плитка там
   это ролик, а снимок под ней его обложка. Именно это мы и продаём: не фото с
   объекта, а снятый материал. */

@media (prefers-reduced-motion: reduce) { .ig__vid { display: none; } }

/* --- Facebook ------------------------------------------------------------ */

.fbx { background: #f0f2f5; }
.fbx__bar {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.4rem 0.6rem 0.35rem;
  background: #fff;
}
.fbx__bar b {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: -0.04em;
  color: #1877f2;
  margin-right: auto;
}
.fbx__bar i { width: 0.85rem; height: 0.85rem; border-radius: 50%; background: #e4e6eb; }

.fbx__post { background: #fff; margin-bottom: 0.4rem; padding: 0.45rem 0 0; }
.fbx__post header { display: flex; align-items: center; gap: 0.4rem; padding: 0 0.55rem 0.35rem; }
.fbx__who { display: grid; }
.fbx__who b { font-size: 0.52rem; font-weight: 600; }
.fbx__who em { font-style: normal; font-size: 0.4rem; color: #65676b; }

.fbx__img, .lix__img { display: block; width: 100%; aspect-ratio: 4 / 5; object-fit: cover; background: #0e0f13; }
.fbx__post > p.fbx__say { font-size: 0.46rem; line-height: 1.4; padding: 0 0.55rem 0.4rem; }
.fbx__meta {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.35rem 0.55rem;
  font-size: 0.4rem;
  color: #65676b;
}
.fbx__meta i {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: #1877f2;
  flex: none;
}
.fbx__meta span { margin-left: auto; }
.fbx__acts {
  display: flex;
  justify-content: space-around;
  border-top: 1px solid #ced0d4;
  padding: 0.3rem 0;
  font-size: 0.44rem;
  font-weight: 600;
  color: #65676b;
}

/* --- YouTube: Shorts ------------------------------------------------------
 *
 * Классический плеер горизонтальный, а вся съёмка клиента вертикальная: в
 * 16:9 она садится либо чёрными полями, либо обрезкой всего, ради чего кадр
 * снят. Shorts это та же площадка и та же аудитория, только в родной для
 * материала пропорции, и местный сервис действительно живёт там.
 * ------------------------------------------------------------------------ */

.ytx { background: #0f0f0f; grid-template-rows: minmax(0, 1fr); }
.ytx__short { position: relative; overflow: hidden; }
.ytx__vid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #0f0f0f;
}

.ytx__mark {
  position: absolute;
  left: 0.55rem;
  top: 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.56rem;
  font-weight: 600;
  color: #fff;
  text-shadow: 0 1px 6px rgb(0 0 0 / 0.6);
}
.ytx__mark i {
  width: 0.62rem;
  height: 0.44rem;
  border-radius: 0.12rem;
  background: #ff0000;
  position: relative;
}
.ytx__mark i::after {
  content: "";
  position: absolute;
  left: 53%;
  top: 50%;
  transform: translate(-50%, -50%);
  border: 0.11rem solid transparent;
  border-left-color: #fff;
  border-right-width: 0;
}

/* Правая колонка действий. Иконки нарисованы формой, а не шрифтом: на 12
   пикселях глиф не читается, а силуэт читается. */
.ytx__rail {
  position: absolute;
  right: 0.4rem;
  bottom: 3.4rem;
  display: grid;
  justify-items: center;
  gap: 0.75rem;
}
.ytx__act {
  display: grid;
  justify-items: center;
  gap: 0.15rem;
  color: #fff;
  text-shadow: 0 1px 6px rgb(0 0 0 / 0.7);
}
.ytx__act b { font-size: 0.38rem; font-weight: 500; }
.ytx__act::before {
  content: "";
  width: 1.1rem;
  height: 1.1rem;
  background: rgb(255 255 255 / 0.92);
}
.ytx__act--up::before { clip-path: polygon(38% 0, 62% 0, 58% 34%, 100% 34%, 88% 100%, 20% 100%, 20% 34%, 42% 34%); }
.ytx__act--say::before { border-radius: 45% 45% 45% 8%; }
.ytx__act--out::before { clip-path: polygon(0 20%, 55% 20%, 55% 0, 100% 42%, 55% 84%, 55% 62%, 0 62%); }
.yc--yt { width: 1.5rem; height: 1.5rem; border-radius: 50%; box-shadow: 0 0 0 1.5px #fff; }
.yc--yt i { font-size: 0.52rem; }

.ytx__foot {
  position: absolute;
  left: 0.55rem;
  right: 3rem;
  bottom: 0.7rem;
  display: grid;
  gap: 0.24rem;
  color: #fff;
  text-shadow: 0 1px 8px rgb(0 0 0 / 0.75);
}
.ytx__chan { display: flex; align-items: center; gap: 0.4rem; }
.ytx__chan b { font-size: 0.46rem; font-weight: 600; }
.ytx__sub {
  background: #fff;
  color: #0f0f0f;
  font-size: 0.36rem;
  font-weight: 600;
  padding: 0.16rem 0.42rem;
  border-radius: 999px;
  text-shadow: none;
  white-space: nowrap;
}
.ytx__cap { font-size: 0.44rem; line-height: 1.3; }
.ytx__snd {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.36rem;
  color: rgb(255 255 255 / 0.85);
}
.ytx__snd::before {
  content: "";
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 50%;
  border: 1.5px solid currentColor;
}
.ytx__prog {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 38%;
  background: #fff;
}

/* --- LinkedIn ------------------------------------------------------------- */

.lix { background: #f4f2ee; }
.lix__bar { display: flex; align-items: center; gap: 0.4rem; padding: 0.4rem 0.6rem 0.35rem; background: #fff; }
.lix__bar i {
  font-style: normal;
  font-family: Asgard, Impact, sans-serif;
  font-size: 0.5rem;
  color: #fff;
  background: #0a66c2;
  border-radius: 0.15rem;
  padding: 0.05rem 0.18rem 0.1rem;
}
.lix__find {
  flex: 1;
  padding: 0.16rem 0.4rem;
  border-radius: 0.15rem;
  background: #edf3f8;
  font-size: 0.38rem;
  color: #8a8a8a;
}
.lix__msg { width: 0.8rem; height: 0.8rem; border-radius: 0.1rem; background: #e7e7e7; flex: none; }

.lix__post { background: #fff; margin-bottom: 0.4rem; padding: 0.45rem 0 0; }
.lix__post header { display: flex; align-items: flex-start; gap: 0.4rem; padding: 0 0.55rem 0.35rem; }
.lix__who { display: grid; }
.lix__who b { font-size: 0.5rem; font-weight: 600; }
.lix__who em { font-style: normal; font-size: 0.38rem; color: #666; }
.lix__post > p.lix__say { font-size: 0.45rem; line-height: 1.42; padding: 0 0.55rem 0.4rem; }
.lix__say em { font-style: normal; color: #8a8a8a; }

.lix__meta {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.3rem 0.55rem;
  font-size: 0.38rem;
  color: #666;
}
.lix__meta i { width: 0.55rem; height: 0.55rem; border-radius: 50%; background: #0a66c2; flex: none; }
.lix__meta span { margin-left: auto; }
.lix__acts {
  display: flex;
  justify-content: space-between;
  border-top: 1px solid #e8e8e8;
  padding: 0.3rem 0.55rem;
  font-size: 0.4rem;
  font-weight: 600;
  color: #666;
}

/* ==========================================================================
   4. Ответ нейросети
   --------------------------------------------------------------------------
   Был абзац на белом листе, и он не читался как ответ нейросети: отличают её
   не слова, а обстановка вокруг них. Вопрос пузырём справа, марка ассистента
   слева, курсор на конце строки, источники плашками, строка ввода внизу.
   ========================================================================== */

.chat {
  background: #fff;
  color: #08080b;
  text-align: left;
  display: grid;
  gap: 0.85rem;
  padding: 0.9rem 1.1rem 1rem;
}

.chat__bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid #ececf0;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #8b8b93;
}
.chat__dot { width: 8px; height: 8px; border-radius: 50%; background: #10a37f; }
.chat__now { margin-left: auto; }

/* Вопрос: пузырь справа, как в любом чате. Без него ответ читается абзацем. */
.chat__ask {
  justify-self: end;
  max-width: 34ch;
  background: #f1f1f4;
  border-radius: 16px 16px 4px 16px;
  padding: 0.55rem 0.85rem;
  font-size: 0.92rem;
  line-height: 1.35;
  color: #22232a;
}

.chat__say { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.7rem; }
.chat__who {
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: #10a37f;
  position: relative;
}
.chat__who::after {
  content: "";
  position: absolute;
  inset: 0.42rem;
  border-radius: 50%;
  border: 2px solid #fff;
  border-right-color: transparent;
}
.chat__body { min-width: 0; }
.chat__a { font-size: 1rem; line-height: 1.55; }
.chat__a b { color: var(--blue); font-weight: 500; }

/* Курсор набора. Одна деталь, по которой видно, что текст пишется сейчас. */
.chat__caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  vertical-align: -0.15em;
  margin-left: 2px;
  background: #08080b;
  animation: chatblink 1.1s steps(2, start) infinite;
}
@keyframes chatblink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .chat__caret { animation: none; } }

.chat__cite { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-top: 0.8rem; }
.chat__cite em {
  font-style: normal;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.58rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #a0a0a8;
  margin-right: 0.15rem;
}
.chat__cite span {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.66rem;
  color: #6b6b74;
  border: 1px solid #e6e6ec;
  border-radius: 999px;
  padding: 0.15rem 0.55rem;
}
.chat__cite span:first-of-type { color: var(--blue); border-color: rgb(1 48 249 / 0.3); }

.chat__field {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  border: 1px solid #e6e6ec;
  border-radius: 999px;
  padding: 0.5rem 0.5rem 0.5rem 0.95rem;
  font-size: 0.86rem;
  color: #a0a0a8;
}
.chat__field i {
  margin-left: auto;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  background: #08080b;
  position: relative;
}
.chat__field i::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 52%;
  width: 6px;
  height: 6px;
  border: 2px solid #fff;
  border-left: 0;
  border-bottom: 0;
  transform: translate(-50%, -50%) rotate(-45deg);
}

/* ==========================================================================
   5. Сайт: ноутбук и телефон
   --------------------------------------------------------------------------
   Семь попыток свести их в один кадр ломались не в CSS. Телефон стоял внутри
   ЛЕНТЫ, в чужой статье, потому что `.fone` не был закрыт, а лишний `</div>`
   в этой статье закрывал сцену и выбрасывал из неё отчёт и полку. Ни одно
   правило размера этого починить не могло.

   Здесь раскладка задана целиком и один раз: два устройства стоят РЯДОМ на
   одной линии основания. Наложение всегда режет то, что под ним, а на столе
   они и так стоят рядом.
   ========================================================================== */

.case__o > .gear { border-radius: 0; box-shadow: none; background: none; }
.gear {
  display: grid;
  grid-auto-flow: column;
  align-items: end;
  justify-content: center;
  gap: 1.6rem;
}

/* --- Ноутбук --------------------------------------------------------------- */

.gear__lap { display: grid; justify-items: center; }
.gear__screen {
  width: 31rem;
  height: 19.4rem;                    /* 16:10 плюс рамка */
  padding: 0.4rem 0.4rem 1rem;
  background: #16181f;
  border-radius: 0.8rem 0.8rem 0.2rem 0.2rem;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
}
/* Петля и подставка. Без них экран висит в воздухе и читается как картинка, а
   не как устройство. */
.gear__hinge {
  width: 32.4rem;
  height: 0.45rem;
  background: linear-gradient(180deg, #23262f, #0e1015);
  border-radius: 0 0 0.25rem 0.25rem;
}
.gear__base {
  width: 34rem;
  height: 0.7rem;
  background: linear-gradient(180deg, #2b2f3a, #14161c);
  clip-path: polygon(0.9% 0, 99.1% 0, 100% 100%, 0 100%);
  border-radius: 0 0 0.5rem 0.5rem;
  box-shadow: 0 1.6rem 3rem -1.2rem rgb(0 0 0 / 0.7);
}

/* --- Десктопный сайт внутри ------------------------------------------------
 *
 * Было: шапка и герой в две колонки, и всё. Это баннер, а не сайт: страница
 * ничем не продолжалась, ни одной цены кроме одной плашки, ни отзыва, ни зоны
 * работы. В превью такое читается как шаблон.
 *
 * Стало четыре полосы: служебная (часы, район, телефон), шапка с ОДНИМ
 * действием, герой и полоса услуг с ценами. Полоса услуг несёт главное
 * обещание всей линейки: цены опубликованы, а не «звоните узнавать».
 *
 * САЙТ СВЕТЛЫЙ. Пять предметов витрины из шести это белые интерфейсы, и
 * тёмный сайт выпадал из ряда: на тёмном поле сцены он терял края и переставал
 * читаться как отдельный предмет. Плюс местные сервисные компании в США держат
 * светлые сайты, а «точнее» здесь значит именно это.
 *
 * МАСШТАБ. Внутри рамы всё набрано мельче, чем кажется правильным на глаз:
 * рама шириной 30rem изображает окно браузера примерно в 1440px, то есть всё
 * внутри уменьшено примерно втрое. Прошлая версия была набрана вдвое крупнее
 * нужного и читалась как страница, увеличенная до 200%. Здесь компромисс:
 * мельче настолько, чтобы читалось как снимок экрана, и не мельче того, что
 * ещё можно прочесть при масштабе сцены.
 */

.dt {
  height: 100%;
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  background: #fff;
  color: #0c0e12;
  text-align: left;
  border-radius: 0.3rem;
  overflow: hidden;
}

/* Служебная полоса. Часы, район и телефон это первое, что ищет местный
   заказчик, и первое, чего нет на устаревших сайтах. */
.dt__util {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.2rem 0.8rem;
  background: #f2f3f6;
  border-bottom: 1px solid #e5e7ec;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.36rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #71747f;
}
.dt__open { display: flex; align-items: center; gap: 0.26rem; color: #1e8e3e; }
.dt__open::before { content: ""; width: 3px; height: 3px; border-radius: 50%; background: currentColor; }
.dt__tel { margin-left: auto; color: #0c0e12; letter-spacing: 0.06em; }

.dt__nav {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.4rem 0.8rem;
  flex-wrap: nowrap;
  border-bottom: 1px solid #eceef2;
}
.dt__brand {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-family: Asgard, Impact, sans-serif;
  font-size: 0.62rem;
  white-space: nowrap;
}
.yc--dt { width: 1.1rem; height: 1.1rem; }
.yc--dt i { font-size: 0.4rem; }
.dt__nav nav { display: flex; gap: 0.62rem; margin-left: auto; flex-wrap: nowrap; }
.dt__nav a { font-size: 0.44rem; color: #5a5d69; white-space: nowrap; }
.dt__nav a:first-child { color: #0c0e12; }
.dt__call {
  font-size: 0.44rem;
  font-weight: 500;
  background: var(--ig-b, #5b8def);
  color: #0c0e12;
  padding: 0.26rem 0.6rem;
  border-radius: 999px;
  white-space: nowrap;
}

/* Снимок берёт больше половины: работа это то, ради чего сюда пришли. Плотное
   поле фирменного цвета РЯДОМ со снимком, а не вуаль поверх него: текст поверх
   фотографии всегда спорит с ней, и именно эта пара (вуаль плюс белый капслок
   плюс синяя кнопка) была названа шаблоном.

   Строка героя ЗАДАНА. Без неё она неявная и берёт высоту от содержимого, а
   содержимое это снимок с `height: 100%` от неопределённой строки, то есть от
   своей натуральной высоты: 1400x1050 на ширине 277 давали 208px против 167
   отведённых, и низ поля с кнопками срезался. У героя position: relative, и
   этот вылет рисовался прямо поверх полосы услуг. */
.dt__hero {
  position: relative;
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  grid-template-rows: minmax(0, 1fr);
  min-height: 0;
  overflow: hidden;
}
.dt__img { width: 100%; height: 100%; object-fit: cover; }

.dt__field {
  background: var(--ig-b, #5b8def);
  color: #0c0e12;
  min-height: 0;
  padding: 0.7rem 0.75rem;
  display: grid;
  align-content: center;
  gap: 0.26rem;
}
.dt__brow {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.34rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgb(12 14 18 / 0.6);
}
.dt__h {
  font-family: Asgard, Impact, sans-serif;
  font-size: 1.1rem;
  line-height: 0.94;
  letter-spacing: -0.03em;
}
.dt__s { font-size: 0.44rem; line-height: 1.42; color: rgb(12 14 18 / 0.76); max-width: 32ch; }

.dt__acts { display: flex; gap: 0.3rem; margin-top: 0.2rem; }
.dt__go, .dt__ghost {
  font-size: 0.44rem;
  font-weight: 500;
  padding: 0.28rem 0.62rem;
  border-radius: 999px;
  white-space: nowrap;
}
.dt__go { background: #0c0e12; color: #fff; }
.dt__ghost { border: 1px solid rgb(12 14 18 / 0.38); color: #0c0e12; }

/* Плашка с оценкой на снимке: в поле она отнимала строку у заголовка, а на
   снимке занимает пустое небо. */
.dt__rate {
  position: absolute;
  left: 0.5rem;
  bottom: 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.24rem;
  padding: 0.2rem 0.42rem;
  border-radius: 999px;
  background: rgb(255 255 255 / 0.95);
  box-shadow: 0 3px 12px rgb(0 0 0 / 0.28);
  font-size: 0.38rem;
  color: rgb(12 14 18 / 0.68);
}
.dt__rate b { color: #0c0e12; font-size: 0.46rem; }
.dt__rate i { font-style: normal; letter-spacing: -1px; color: #f5a623; }

/* Полоса услуг. Цена под каждой, потому что «цены опубликованы» это не
   прилагательное, а четыре числа на экране. */
.dt__svc {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-top: 1px solid #e5e7ec;
}
.dt__svc li {
  display: grid;
  gap: 0.1rem;
  padding: 0.4rem 0.55rem 0.45rem;
  border-left: 1px solid #eceef2;
}
.dt__svc li:first-child { border-left: 0; }
.dt__svc b { font-size: 0.46rem; font-weight: 500; }
.dt__svc em {
  font-style: normal;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.42rem;
  color: #0c0e12;
}
.dt__svc--say { background: #f7f8fa; }
.dt__svc--say b { color: #4a4d59; }
.dt__svc--say em { color: #8b8e99; }

/* --- Телефон рядом ---------------------------------------------------------
 *
 * Первая версия сайта в телефоне была фото под тёмным градиентом с белым
 * капслоком и синей кнопкой. Это шаблон, который генерит любая нейросеть, и PO
 * назвал его AI слопом справедливо. Не такой её делают: плотное поле
 * фирменного цвета вместо вуали, чёрный тип по цвету, ценник повёрнутым
 * стикером внахлёст, бегущая строка услуг.
 */

.gear__phone { display: grid; }
.ph {
  --ph-hot: var(--ig-b, #5b8def);
  position: relative;
  width: 10.5rem;
  /* Тот же светлый сайт, что на ноутбуке. Тёмный он был только здесь, и два
     устройства показывали два разных сайта. */
  background: #fff;
  color: #0c0e12;
  overflow: hidden;
  text-align: left;
  border-radius: 22px;
  box-shadow: inset 0 0 0 1.5px rgb(12 14 18 / 0.12);
}

/* Тот же островок камеры, что у телефона с лентой: два аппарата на витрине
   обязаны быть одной семьёй. */
.ph::before {
  content: "";
  position: absolute;
  z-index: 3;
  top: 0.3rem;
  left: 50%;
  transform: translateX(-50%);
  width: 2.6rem;
  height: 0.66rem;
  border-radius: 999px;
  background: #0c0e12;
}
.ph__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.4rem 0.75rem 0.2rem;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.44rem;
  font-weight: 600;
  color: #0c0e12;
}
.ph__sig { width: 1.5rem; height: 0.3rem; border-radius: 2px; background: rgb(12 14 18 / 0.55); }

.ph__url {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin: 0 0.45rem 0.35rem;
  padding: 0.22rem 0.5rem;
  border-radius: 999px;
  background: #f2f3f6;
  font-size: 0.44rem;
  color: #5a5d69;
}
.ph__url i { font-style: normal; font-size: 0.46rem; opacity: 0.7; }

.ph__shot { position: relative; aspect-ratio: 5 / 4; }
.ph__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.ph__nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0.6rem;
  text-shadow: 0 1px 10px rgb(0 0 0 / 0.6);
}
.ph__brand { display: flex; align-items: center; gap: 0.3rem; font-family: Asgard, Impact, sans-serif; font-size: 0.54rem; }
.ph__burger { display: grid; gap: 2.4px; padding: 0.2rem; }
.ph__burger i { display: block; width: 10px; height: 1.3px; background: #fff; border-radius: 2px; }

/* Стикер. Повёрнут и заходит на границу двух блоков, поэтому читается как
   наклейка, а не как ещё одна плашка интерфейса. */
.ph__sticker {
  position: absolute;
  right: 0.5rem;
  /* Наклейка заходит на границу снимка и поля, но НЕ на заголовок: она
     печаталась поверх «CLEAN GLASS». Поле добирает верхний отступ ровно на ту
     долю, которой стикер в него заходит. */
  bottom: -0.8rem;
  z-index: 2;
  display: grid;
  place-items: center;
  gap: 0.05rem;
  padding: 0.32rem 0.5rem;
  background: #0c0e12;
  color: #fff;
  border-radius: 8px;
  transform: rotate(-6deg);
  box-shadow: 0 6px 18px rgb(0 0 0 / 0.45);
}
.ph__sticker b { font-family: Asgard, Impact, sans-serif; font-size: 0.72rem; line-height: 1; letter-spacing: -0.03em; }
.ph__sticker em {
  font-style: normal;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.42rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ph-hot);
}

/* Поле. Плотный цвет, чёрный тип, без единого градиента. */
.ph__field { background: var(--ph-hot); color: #0c0e12; padding: 1.5rem 0.6rem 0.6rem; }
/* Тот же набор, что на ноутбуке: это ОДИН сайт, у него не может быть двух
   типографик. Капслоком заголовок был плакатом, в предложении он сайт. */
.ph__h {
  font-family: Asgard, Impact, sans-serif;
  font-size: 0.98rem;
  line-height: 0.94;
  letter-spacing: -0.03em;
}
.ph__rate { display: flex; align-items: center; gap: 0.26rem; margin-top: 0.4rem; font-size: 0.42rem; color: rgb(12 14 18 / 0.72); }
.ph__rate b { color: #0c0e12; }
.ph__rate i { font-style: normal; letter-spacing: -1px; }

/* Бегущая строка услуг. Одна из немногих вещей, которую невозможно спутать с
   генератором лендингов. */
.ph__run { overflow: hidden; background: #0c0e12; padding: 0.26rem 0; }
.ph__run span {
  display: inline-block;
  white-space: nowrap;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.42rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ph-hot);
  animation: phrun 16s linear infinite;
}
@keyframes phrun { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .ph__run span { animation: none; } }

/* Док: главное действие плюс звонок, оба видимы. */
.ph__dock { display: flex; gap: 0.28rem; align-items: center; padding: 0.5rem 0.55rem 0.6rem; }
.ph__dock span {
  flex: 1;
  text-align: center;
  background: #0c0e12;
  color: #fff;
  font-weight: 500;
  font-size: 0.54rem;
  padding: 0.42rem;
  border-radius: 10px;
}
.ph__dock em {
  font-style: normal;
  font-size: 0.5rem;
  font-weight: 500;
  color: #0c0e12;
  border: 1px solid rgb(12 14 18 / 0.3);
  border-radius: 10px;
  padding: 0.42rem 0.5rem;
}

/* ==========================================================================
   6. Отчёт
   --------------------------------------------------------------------------
   Настоящий .dash с /money-trail/, а не перерисованная сводка. Данные те же
   самые, site.dash, поэтому витрина и продуктовая страница не могут разойтись.
   Здесь только посадка по размеру предмета.
   ========================================================================== */

.o-report .dash__table { font-size: 0.74rem; }
.o-report .dstat b { font-size: 1.8rem; }

/* ==========================================================================
   Марка
   --------------------------------------------------------------------------
   «Your Company» без марки читается как незаполненное поле. Скруглённый
   квадрат в цвете ремесла, монограмма в дисплейной гарнитуре. Цвет приходит от
   выбора, поэтому в шести ремёслах это шесть разных компаний.
   ========================================================================== */

.yc {
  display: grid;
  place-items: center;
  flex: none;
  border-radius: 26%;
  background: var(--ig-a, #14213d);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.14);
}
.yc i {
  font-style: normal;
  font-family: Asgard, Impact, sans-serif;
  font-weight: 500;
  letter-spacing: -0.04em;
  color: #fff;
  line-height: 1;
}
.yc--av { width: 4rem; height: 4rem; border-radius: 50%; }
.yc--av i { font-size: 1.35rem; }
.yc--gb { width: 2.6rem; height: 2.6rem; }
.yc--gb i { font-size: 0.92rem; }
.yc--sr { width: 1.05rem; height: 1.05rem; display: inline-grid; vertical-align: -2px; margin-right: 0.35rem; }
.yc--sr i { font-size: 0.42rem; }
.yc--dt { width: 1.35rem; height: 1.35rem; }
.yc--dt i { font-size: 0.5rem; }
.yc--ph { width: 1rem; height: 1rem; }
.yc--ph i { font-size: 0.36rem; }

/* ==========================================================================
   Узкий экран
   --------------------------------------------------------------------------
   Ниже 62rem колонки нет: голова, выбор, сцена и полка идут сверху вниз, а
   предмет садится в остаток высоты тем же множителем --fit.
   ========================================================================== */

/* ⚠️ ЗДЕСЬ СТОЯЛО РАСПИНИВАНИЕ, И ЭТО БЫЛА ОШИБКА.
 *
 * Сайт ниже 60rem глушил рельсовые секции целиком, а витрина главной
 * превращалась в столбик из шести предметов по 62svh. Довод был такой:
 * хореографии на телефоне быть не может, потому что у сцены нет определённой
 * высоты и `--fit` считать не от чего. Довод верен наполовину: высоты
 * действительно не было - ПОТОМУ ЧТО ЕЁ НЕ ДАЛИ. Дать её стоит одной строки
 * сетки, и всё остальное работает как на большом экране, включая `fit()`,
 * который считает от коробки предмета, а не от ширины окна.
 *
 * PO: «все первые экраны иммерсивный опыт при скролле, а не просто скролл».
 * Первый экран сайта - главный из них.
 *
 * Раскладка та же, что на десктопе, только в одну колонку: заголовок, выбор
 * ремесла, сцена во весь остаток, полка с репликой внизу. Пин липкий, его
 * ставит общее правило первых экранов в `main.css`. */
@media (max-width: 60rem) {
  .case-pin {
    padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-m), 2.6svh));
    padding-bottom: min(var(--sp-m), 2.4svh);
  }
  .case__wrap {
    grid-template-columns: minmax(0, 1fr);
    /* Сцена - ЕДИНСТВЕННАЯ тянущаяся строка. Остальные три по содержимому:
       им нельзя растягиваться, иначе сцена схлопнется и `--fit` снова будет
       считать от нуля. */
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas: "head" "pick" "stage" "foot";
    row-gap: min(var(--sp-s), 1.8svh);
    column-gap: 0;
    align-content: stretch;
  }
  .case__pick { margin-top: 0; }
  /* Заголовок и реплики отдают телефону высоту: три роли по кеглю сайта
     съедали сцену целиком. Роль не меняется, меняется её потолок. */
  .case__title { font-size: min(var(--step-2), 5.4svh); }
  .case__lines { min-height: 2.8rem; }
  .case__line { font-size: min(var(--step-0), 2.0svh); max-width: none; }
  .case__foot { gap: min(var(--sp-s), 1.6svh); }
  .case__note { max-width: none; font-size: var(--step-label); }
}

/* Число закрытия, которое НЕ потеря. Красный на сайте значит «деньги уходят»,
   и бесплатный первый шаг, напечатанный им, врал цветом. */
.close__figure--own b { color: var(--white); }

/* --- Передача первого экрана второму ---------------------------------------
 *
 * Не следующая секция, а второй лист в том же припиненном кадре. Приём взят у
 * первого экрана живой главной: там полёт растворяется с уже лежащего под ним
 * калькулятора, и это описано там же словами «полёт ПРЕВРАЩАЕТСЯ в
 * калькулятор, а не заканчивается рядом с ним».
 */
.case__sheet {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  align-content: center;
  /* Непрерывно, от --hand, а не переключателем: то же правило, что у предметов
     витрины. Переход держится там, где читатель остановил колесо. */
  opacity: var(--hand, 0);
  transform: translateY(calc((1 - var(--hand, 0)) * 1.1rem));
  /* Кадр целиком меняет грунт: тёмное это аренда, светлое это своё. Тот же
     ход, что у первого экрана живой главной, где полёт заканчивается на
     бумаге. Токены сбрасываются вместе с фоном, иначе заголовок листа
     останется белым на белом. */
  background: var(--white);
  color: var(--text);
  --heading-color: var(--text);
  --lede-color: var(--text-muted);
  pointer-events: none;
}
.case-pin[data-done="true"] .case__sheet { pointer-events: auto; }

/* Витрина уходит из-под листа, а не просвечивает через него. */
.case__wrap {
  opacity: calc(1 - var(--hand, 0));
  transform: translateY(calc(var(--hand, 0) * -1.1rem));
}
.case-pin[data-done="true"] .case__wrap { pointer-events: none; }



/* Метка двери: какие из шести предметов первого экрана она производит. Это и
   есть связь развилки с витриной, и она читается формой, а не объяснением. */
/* Развилка использует крупный `h3` вместо мелкой метки: голос клиента это
   то, за что цепляется глаз, а моношрифтовая строка над ним уже названа
   `.door__mark`. Живая дверь красит `h3` мелким моно, поэтому переопределяем
   ТОЛЬКО внутри листа, не трогая двери на остальных страницах. */
.case__sheet .door h3 {
  font-family: Asgard, Impact, sans-serif;
  font-weight: 400;
  font-size: var(--step-2);
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-transform: none;
  color: #fff;
}

/* --- Сколько стоит ----------------------------------------------------------
 *
 * Напечатанный прейскурант, лежащий на тёмном столе. Не таблица, оформленная
 * аккуратнее: предмет. Отточия, шрифт пишущей машинки и бумажный лист
 * узнаются формой раньше, чем читается хоть одно слово, и это ровно тот
 * аргумент, который блок должен донести: цены НАПЕЧАТАНЫ.
 */
/* Полосе нужен воздух. `--sp-l` тут стоял с тех пор, когда заголовок был НАД
   листом и блок ужимали в один экран; теперь заголовок сбоку, ужимать нечего, а
   тёмная полоса в обтяжку читается щелью между двумя светлыми секциями. */
/* ⚠️ Было на `#prices`: тот же класс ловушки, см. `.sect--airy` ниже. */
.sect--sheet { padding-block: clamp(4rem, 8vw, 7rem); }
/* ⚠️ Двойной класс, а не один: `.tariff-lay .sect__head { margin-bottom: 0 }`
   лежит ниже в файле и при равной специфичности выигрывал, обнуляя отступ.
   Прежний селектор был на идентификаторе и выигрывал всегда. Замерено:
   0px вместо 22.4px сразу после замены. */
.sect.sect--sheet .sect__head { margin-bottom: var(--sp-m); }

/* Заголовок слева, лист справа. Над листом заголовок оставлял по пятьсот
   пикселей пустоты по бокам; сбоку та же ширина работает колонкой. */
@media (min-width: 64rem) {
  .tariff-lay {
    display: grid;
    /* Левая колонка от левого поля страницы, правая до правого: блок стоит в
       сетке страницы, как все остальные. Потолок на ширину висит на тексте
       внутри колонки, а не на самой колонке. */
    grid-template-columns: minmax(0, 1fr) minmax(26rem, 46rem);
    align-items: center;
    column-gap: var(--sp-l);
  }
  /* Ключ на `.tariff-lay`, а не на `#prices`: вариант выбран и будет стоять не
     только в этой секции, а правило, привязанное к идентификатору, переехать
     вместе с ним не может. */
  .tariff-lay .sect__head { margin-bottom: 0; max-width: none; }
  /* Потолок стоит на ТЕКСТЕ, а не на колонке. Колонка обязана начинаться от
     левого поля страницы, иначе блок не встаёт в её сетку; а строка в 1120px
     не читается. Воздух между текстом и листом это не дыра, а поле. */
  .tariff-lay .sect__head h2 { max-width: 15ch; }
  .tariff-lay .sect__head .lede { max-width: 40ch; }

  /* Шапка секции сама двухколоночная: на полноэкранной странице заголовок
     слева, подпись справа. Внутри узкой колонки эти две колонки схлопываются
     в две ленты по девяносто пикселей. Здесь она снова одна колонка. */
  .tariff-lay .sect__head { display: block; }
  .tariff-lay .sect__head > * + * { margin-top: var(--sp-s); }
}

.tariff {
  perspective: 1400px;
  /* Блок, а не гибкий контейнер: лист просто заполняет колонку.

     ⚠️ Ловушка при замере. Лист наклонён на 14° при `perspective: 1400px`, и
     `getBoundingClientRect()` возвращает габарит ПРОЕКЦИИ: 806px вместо 736,
     то есть по 35px наружу с каждой стороны. Три правки подряд были сделаны
     против этой цифры, и ни одна ничего не чинила, потому что чинить было
     нечего. Ширину этого листа мерить `offsetWidth`, а не rect. */
}

.tariff__p {
  /* Ширину задаёт колонка, лист её заполняет. */
  box-sizing: border-box;
  /* Поля листа подобраны от ВЫСОТЫ секции, а не от вкуса: с 2.6rem лист вместе
     с заголовком не помещался в экран 800px, ради которого весь блок и
     собирался в одну картину. */
  padding: clamp(1.3rem, 2.6vw, 2rem) clamp(1.4rem, 3vw, 2.4rem);
  background: #f7f5ef;
  color: #14161c;
  border-radius: 3px;
  /* Тень от листа, лежащего на столе, а не от карточки в интерфейсе: длинная,
     мягкая, со смещением вниз. */
  box-shadow: 0 2px 4px rgb(0 0 0 / 0.3), 0 30px 60px rgb(0 0 0 / 0.5);
  transform-origin: 50% 0;
  transition: transform 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Лист въезжает под углом и ложится. Один раз, на въезде. */
.tariff[data-tilt="true"] .tariff__p { transform: rotateX(14deg) translateY(1.2rem); }
.tariff[data-tilt="false"] .tariff__p { transform: rotateX(0) translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .tariff__p { transition: none; }
  .tariff[data-tilt="true"] .tariff__p { transform: none; }
}

/* ⚠️ На узком экране лист наклоняется вокруг НИЖНЕГО края, а не верхнего.
 *
 * Причина - те самые 5 точек горизонтальной прокрутки на главной, которые
 * долго не находились. Ловушка замера про проекцию наклонённого листа была
 * записана прямо здесь, выше, и я всё равно искал виновника среди коробок:
 * НИ ОДИН элемент за край не выходил, а `scrollWidth` был 395 при 390.
 * Виновата проекция. Петля на верхнем крае бросает низ листа НА ЗРИТЕЛЯ, и
 * проекция становится шире коробки во столько раз, во сколько высота листа
 * приблизилась к камере:
 *   главная  лист 711  -> [-5..395] при экране 390
 *   /pricing/ лист 1551 -> [-45..435]
 * То есть вылет растёт с высотой листа, и постоянной поправкой перспективы он
 * не лечится: при 2600px `/pricing/` всё равно даёт [-10..400].
 *
 * Петля на нижнем крае уводит лист ОТ зрителя, и проекция может только
 * сузиться. Замерено на обеих страницах при `tilt=true`: [19..371], sw=390,
 * при высотах 711 и 1551. То есть починка не зависит от содержимого.
 *
 * На большом экране жест остаётся прежним: там запаса хватало, и PO этот
 * кадр принял. */
@media (width < 60rem) {
  .tariff__p { transform-origin: 50% 100%; }
}

.tariff__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-s);
  padding-bottom: 0.7rem;
  border-bottom: 2px solid #14161c;
}

.tariff__mark {
  font-family: Asgard, Impact, sans-serif;
  font-size: var(--step-0);
  letter-spacing: -0.01em;
}

.tariff__mark sup { font-size: 0.5em; vertical-align: super; }

.tariff__t {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgb(20 22 28 / 0.55);
}

.tariff__rows { list-style: none; margin: 0.7rem 0 0; padding: 0; }

/* Строка прейскуранта: имя, отточие во всю оставшуюся ширину, срок, цена.
   Отточие это не декор, а то, чем печатный тариф отличается от таблицы: оно
   ведёт глаз от названия к числу и не даёт строке распасться. */
.tar {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.24rem 0;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step--1);
}

.tar__n { white-space: nowrap; }

.tar__dots {
  flex: 1 1 auto;
  min-width: 1.5rem;
  height: 0.55em;
  border-bottom: 1px dotted rgb(20 22 28 / 0.42);
}

.tar__w {
  color: rgb(20 22 28 / 0.48);
  font-size: var(--step-label);
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* ⚠️ Узкий экран: строка прейскуранта складывается в две. Имя, отточие и
   цена в один ряд не влезают, и лист давал горизонтальную прокрутку:
   замерено на 390px, документ 549 вместо 390, цена уходила на 549. */
@media (width < 34rem) {
  .tar { flex-wrap: wrap; row-gap: 0.15rem; }
  .tar__n { white-space: normal; flex: 1 1 100%; }
  .tar__dots { display: none; }
  .tar__w { order: 1; }
  .tar__p { order: 2; min-width: 0; margin-left: auto; }
}

.tar__p {
  min-width: 7.5rem;
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- Кто уже работает -------------------------------------------------------
 *
 * Сетка со сдвигом. Приём взят у студийных индексов работ (cuberto,
 * instrument, build in amsterdam, basic/dept): там дело не в рамках, не в
 * наведении и не в хитрой механике, а в РАЗМЕРЕ снимка и в воздухе вокруг
 * него. Подпись стоит прямо под кадром одной строкой, ремесло мелким моно.
 *
 * Плита это единственная добавка к снимку. Голый скриншот на белом читается
 * вырезкой; цвет самого клиента под ним делает из вырезки предмет.
 */
.works {
  display: grid;
  gap: clamp(2.4rem, 4vw, 4.2rem) clamp(1.6rem, 3vw, 3.4rem);
}

@media (min-width: 60rem) {
  .works { grid-template-columns: 1fr 1fr; }
  /* Правая колонка опущена: ряд перестаёт читаться рядом одинаковых карточек
     и начинает читаться потоком. */
  .work:nth-child(even) { margin-top: clamp(3rem, 7vw, 7.5rem); }
}

.work {
  display: grid;
  gap: 0.7rem;
  align-content: start;
  text-decoration: none;
  color: inherit;
}

/* Кадр стоит НА плите: снизу отступа нет, и снимок упирается в её край, как
   экран в стол. С равными отступами со всех сторон это была бы просто
   картинка в цветной рамке. */
.work__plate {
  display: block;
  padding: clamp(1.4rem, 3.2vw, 3rem) clamp(1.4rem, 3.2vw, 3rem) 0;
  border-radius: var(--radius-card);
  background: var(--plate, var(--panel-dark));
  overflow: hidden;
}

/* Окно, в котором едет страница. `container-type: size` нужен не для запросов,
   а ради единицы `cqh`: путь ленты это её собственная высота минус высота
   окна, и в CSS это выражается только через высоту контейнера. Иначе на
   каждого клиента пришлось бы вписывать в разметку пиксели его снимка. */
.work__win {
  position: relative;
  display: block;
  container-type: size;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: 8px 8px 0 0;
  box-shadow: 0 24px 50px rgb(0 0 0 / 0.34);
}

.work__win img {
  position: absolute;
  inset: 0 0 auto;
  display: block;
  width: 100%;
  height: auto;
  animation: roll 26s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
  /* Стоит, пока не видно, и пока на карточку не навели. Смещение старта по
     номеру карточки: пять лент, едущих в ногу, читаются как заставка. */
  animation-delay: calc(var(--i, 0) * -4.5s);
  animation-play-state: paused;
}

@keyframes roll {
  from { transform: translateY(0); }
  to { transform: translateY(calc(100cqh - 100%)); }
}

.work[data-seen="true"] .work__win img { animation-play-state: running; }
.work:hover .work__win img,
.work:focus-visible .work__win img { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .work__win img { animation: none; }
}

/* Окно приподнимается над плитой. Двигается кадр, а не карточка: плита
   остаётся на месте, и сетка не дышит. */
.work__plate .work__win {
  transform: translateY(0);
  transition: transform 0.42s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.work:hover .work__win,
.work:focus-visible .work__win { transform: translateY(-0.7rem); }

.work__cap {
  max-width: 44ch;
  font-size: var(--step-0);
  line-height: 1.42;
  color: var(--lede-color);
}

.work__cap b { color: var(--heading-color); font-weight: 600; }
.work:hover .work__cap b { color: var(--blue); }

.work__tag {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--tag-color);
}

/* --- Развилка: чище и с глубиной --------------------------------------------
 *
 * Живая дверь склеена в одну плиту: общий фон, зазор в один пиксель, скругление
 * только по краям группы. На листе развилки это читается таблицей из трёх
 * ячеек. Здесь три ОТДЕЛЬНЫХ предмета: свой зазор, своё скругление, своя тень.
 *
 * Имена свои (`--fork`, `.fork-arrow`), а не переопределение `.door`: гейт
 * коллизий запрещает править чужой компонент со стенда, и он прав. Всё
 * остальное поведение двери берётся у компонента без изменений.
 */
.doors--fork {
  gap: 0.7rem;
  background: none;
  border-radius: 0;
  overflow: visible;
  box-shadow: none;
}

.door--fork {
  border-radius: var(--radius-card);
  box-shadow: var(--lift-2);
  transition: background 0.22s ease, transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1),
              box-shadow 0.28s ease;
}

.door--fork:hover,
.door--fork:focus-visible { transform: translateY(-5px); box-shadow: var(--lift-3); }

/* Номер двери. Он же и есть счёт: посетитель видит, что путей ровно три, до
   того как прочтёт хоть одну карточку. */
.door__no {
  margin-right: 0.5rem;
  color: rgb(255 255 255 / 0.32);
}

/* На синей двери номер светлее: на `--blue` та же прозрачность тонет.
   Селектор ведёт от СВОЕГО имени, иначе гейт видит правку чужого `.is-you`. */
.door--fork.is-you .door__no { color: rgb(255 255 255 / 0.55); }

/* Стрелка выезжает при наведении. Место под неё занято всегда, меняются только
   сдвиг и прозрачность: иначе соседний текст дёргается на каждом наведении. */
.fork-arrow {
  display: inline-block;
  width: 1em;
  font-style: normal;
  opacity: 0;
  transform: translateX(-0.3rem);
  transition: opacity 0.22s ease, transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.door--fork:hover .fork-arrow,
.door--fork:focus-visible .fork-arrow { opacity: 1; transform: translateX(0.15rem); }

/* --- Живая рамка -----------------------------------------------------------
 * Общая часть первого экрана и примеров: полоса адреса, окно с ходом страницы
 * и подпись. `container-type: size` нужен ради единицы `cqh`: путь ленты это
 * её высота минус высота окна, и в CSS это выражается только так.
 */
.lap,
.frame {
  display: grid;
  align-content: start;
  background: var(--panel-dark);
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: var(--lift-3);
  text-decoration: none;
  color: inherit;
}

.lap__bar,
.frame__bar {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.45rem 0.7rem;
  background: rgb(255 255 255 / 0.06);
  border-bottom: 1px solid rgb(255 255 255 / 0.1);
}

.lap__bar i,
.frame__bar i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgb(255 255 255 / 0.22);
}

.lap__url,
.frame__url {
  margin-left: 0.5rem;
  padding: 0.1rem 0.6rem;
  border-radius: 999px;
  background: rgb(0 0 0 / 0.35);
  font-family: PlexMono, ui-monospace, monospace;
  font-size: 0.6rem;
  letter-spacing: 0.04em;
  color: rgb(255 255 255 / 0.7);
}

.lap__win,
.frame__win {
  position: relative;
  display: block;
  container-type: size;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: #fff;
}

.lap__win img,
.frame__win img {
  position: absolute;
  inset: 0 0 auto;
  display: block;
  width: 100%;
  height: auto;
  animation: roll 30s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
  animation-delay: calc(var(--i, 0) * -6s);
  animation-play-state: paused;
}

[data-seen="true"] .lap__win img,
[data-seen="true"] .frame__win img { animation-play-state: running; }

.lap:hover .lap__win img,
.frame:hover .frame__win img { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .lap__win img,
  .frame__win img { animation: none; }
}

/* --- Примеры --------------------------------------------------------------- */
.frames {
  display: grid;
  gap: var(--sp-m);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}

.frame { transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); }
.frame:hover { transform: translateY(-5px); }

.frame__cap {
  display: grid;
  gap: 0.2rem;
  padding: var(--sp-s) var(--sp-m) var(--sp-m);
}

.frame__cap b { color: #fff; }
.frame__did { font-size: var(--step--1); color: rgb(255 255 255 / 0.62); }

.frame__trade {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 0.4);
}

/* --- Неделя ----------------------------------------------------------------
 * Семь дней одной лентой. Номер отдельной колонкой: расписание узнаётся по ней
 * раньше, чем прочитана первая строка.
 */
/* --- Гарантия как БЛАНК ------------------------------------------------------
 *
 * Тот же печатный язык, что у брифа в сцене: бумага с подсветкой, водяной
 * знак, строки формы с чередующейся подложкой, отрывной корешок со штрихкодом
 * и штамп. Блок перестаёт быть полотном прозы и становится вещью, которую
 * подписывают.
 */
.pact {
  position: relative;
  overflow: hidden;
  max-width: 84rem;
  margin-inline: auto;
  padding: clamp(1.6rem, 3.4vw, 3.4rem);
  border-radius: 4px;
  color: var(--ink);
  background:
    radial-gradient(120% 90% at 34% 10%, rgb(255 255 255 / 0.75), transparent 62%),
    #fbfaf6;
  box-shadow: var(--lift-3);
}

.pact__head {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-s);
  margin: 0 0 0.7rem;
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  color: #4e525c;
  border-bottom: 1.5px solid rgb(11 13 20 / 0.14);
  padding-bottom: 0.7rem;
}

/* Водяной знак. Тот же приём и тот же наклон, что на бланке в сцене. */
.pact__mark {
  position: absolute;
  right: -2%;
  top: 26%;
  font-family: Asgard, Impact, sans-serif;
  font-size: clamp(4rem, 9vw, 8.5rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: rgb(11 13 20 / 0.022);
  transform: rotate(-20deg);
  pointer-events: none;
  user-select: none;
}

.pact__title {
  position: relative;
  margin: 0 0 0.7rem;
  color: var(--ink);
  max-width: 22ch;
  /* Ниже секционного: рядом стоят строки формы, и заголовок не должен их
     задавить. */
  font-size: var(--step-3);
}

.pact__sub {
  position: relative;
  max-width: 56ch;
  margin: 0 0 var(--sp-l);
  font-size: var(--step-0);
  color: #4e525c;
}

.pact__rows {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid rgb(11 13 20 / 0.12);
}

.pact__row {
  display: grid;
  grid-template-columns: 2.4rem minmax(0, 1fr);
  gap: 0.25rem 1rem;
  padding: 0.85rem 0.9rem;
  border-bottom: 1px solid rgb(11 13 20 / 0.09);
}

/* Чередование подложки: строку видно как строку формы, а не как абзац. */
.pact__row:nth-child(odd) { background: rgb(11 13 20 / 0.028); }

.pact__n {
  grid-column: 1;
  font-size: var(--step--2);
  color: #8c8f9a;
  padding-top: 0.3rem;
}
.pact__label {
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  color: #4e525c;
  align-self: start;
  padding-top: 0.3rem;
}
.pact__value {
  grid-column: 2;
  font-size: var(--step-0);
  line-height: 1.45;
  color: var(--ink);
}

@media (min-width: 62rem) {
  .pact__row { grid-template-columns: 2.4rem 19rem minmax(0, 1fr); gap: 0 1.6rem; }
  .pact__n { grid-column: 1; }
  .pact__label { grid-column: 2; }
  .pact__value { grid-column: 3; }
}

/* Корешок: перфорация, подпись, штрихкод, штамп. */
.pact__stub {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-m);
  margin-top: var(--sp-l);
  padding-top: var(--sp-m);
  border-top: 1.5px dashed rgb(11 13 20 / 0.28);
}

.pact__sign p { margin: 0; }
.pact__sign .mono { font-size: var(--step--2); letter-spacing: 0.12em; color: #4e525c; }
.pact__by { font-size: var(--step-0); color: var(--ink); margin-top: 0.2rem !important; }

/* Штрихкод полосами, а не картинкой: он обязан быть одинаковым в каждом
   показе и ничего не весить. */
.pact__code {
  display: block;
  width: min(15rem, 42vw);
  height: 2.2rem;
  margin-top: 0.7rem;
  background-image: repeating-linear-gradient(90deg,
    var(--ink) 0 2px, transparent 2px 5px, var(--ink) 5px 8px, transparent 8px 12px,
    var(--ink) 12px 13px, transparent 13px 17px, var(--ink) 17px 21px, transparent 21px 24px);
}

/* Штамп: тот же приём, что у отметки приоритета на бланке в сцене. */
.pact__stamp {
  flex: none;
  margin: 0;
  padding: 0.5rem 1.1rem;
  font-family: Asgard, Impact, sans-serif;
  font-size: var(--step-2);
  letter-spacing: 0.02em;
  color: var(--red);
  border: 3px solid var(--red);
  border-radius: 6px;
  transform: rotate(-7deg);
  opacity: 0.85;
}

@media (max-width: 48rem) {
  .pact__stub { flex-direction: column; align-items: flex-start; }
}

/* --- «Что покрывает цена»: язык синих блоков с главной ----------------------
 *
 * Взят блок «дверей» с главной (.doors / .door в main.css, секция #doors):
 * общий контейнер со скруглением, ячейки разделены зазором в 1px по фону
 * контейнера, одна залита сплошным синим. Ничего не придумано: те же приёмы,
 * те же величины, наши токены.
 *
 * Синей залита ячейка про счёт за хостинг: на главной синим залита дверь, где
 * у читателя забирают расход, и здесь забирают его же.
 */
.cov {
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  background: rgb(255 255 255 / 0.1);
  border-radius: clamp(14px, 1.4vw, 20px);
  overflow: hidden;
  box-shadow: var(--lift-2);
}

.cov__cell {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  gap: var(--sp-s);
  padding: var(--sp-m);
  background: var(--panel-dark);
}

.cov__mark {
  margin: 0;
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blue-lo);
}

.cov__voice {
  margin: 0;
  font-family: Asgard, Impact, sans-serif;
  font-size: var(--step-2);
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: #fff;
}

.cov__text {
  margin: 0;
  font-size: var(--step--1);
  line-height: 1.45;
  color: rgb(255 255 255 / 0.72);
}

.cov__foot {
  margin: 0;
  padding-top: var(--sp-s);
  border-top: 1px solid rgb(255 255 255 / 0.16);
}

.cov__foot b {
  font-weight: 400;
  font-size: var(--step-1);
  color: var(--blue-lo);
}

/* Наведение. Взято у дверей с главной: заливка синим и подъём на 3px.
   Так карточка ведёт себя как дверь, а не как плитка. */
.cov__cell {
  transition: background 0.22s ease, transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.cov__cell:hover { background: var(--blue); transform: translateY(-3px); }
.cov__cell:hover .cov__mark { color: rgb(255 255 255 / 0.9); }
.cov__cell:hover .cov__text { color: rgb(255 255 255 / 0.86); }
.cov__cell:hover .cov__foot { border-top-color: rgb(255 255 255 / 0.35); }
.cov__cell:hover .cov__foot b { color: #fff; }

/* Уже залитой перекрашивать нечего, но подъём ей положен тот же. */
.cov__cell.is-blue:hover { transform: translateY(-3px); }

@media (prefers-reduced-motion: reduce) {
  .cov__cell, .cov__cell:hover { transition: none; transform: none; }
}

/* Одна ячейка залита. Та же заливка и та же логика, что у .door.is-you. */
.cov__cell.is-blue { background: var(--blue); }
.cov__cell.is-blue .cov__mark { color: rgb(255 255 255 / 0.9); }
.cov__cell.is-blue .cov__text { color: rgb(255 255 255 / 0.86); }
.cov__cell.is-blue .cov__foot { border-top-color: rgb(255 255 255 / 0.35); }
.cov__cell.is-blue .cov__foot b { color: #fff; }

/* --- Дизайн-проход по странице «сайт» ----------------------------------------
 *
 * До этого страница была верной по смыслу и никакой на вид: типографика по
 * умолчанию, ровные отступы, плоские секции. Здесь она получает ритм.
 *
 * Правил ровно три, и они держат всю страницу:
 *   1. Каждая секция пронумерована. Страница это последовательность, а не
 *      набор разделов, и номер говорит об этом раньше заголовка.
 *   2. Заголовки крупные и в две строки. Мелкий заголовок над крупным
 *      предметом читается подписью к нему, а не разделом.
 *   3. Синий значит «вот что ты получаешь». Больше он не значит ничего, и
 *      поэтому его мало.
 */
/* #included выведен из общего ритма намеренно: он обязан уместиться в один
   экран, а 120px сверху и снизу выносили его за край. #meter удалён вместе
   со своей секцией.

   ⚠️ БЫЛО НА ИДЕНТИФИКАТОРАХ: `#top, #examples, #guarantee` и `#top`.
   Специфичность идентификатора выше любого класса, поэтому правило,
   написанное под `/site/`, молча било по КАЖДОЙ странице сайта с таким
   `id`. Восемь страниц несут `id="top"`, и на `/pricing/` это обнулило
   верхнее поле секции: заголовок ушёл под шапку, а то же значение инлайном
   давало 148px. `#examples` не существует ни на одной странице.
   Правило `#top { padding-block: 0 }` было ЛИШНИМ: секции с этим id несут
   `.rail-sect`, `.week`, `.stage`, `.paper`, у которых своего паддинга нет,
   а там, где он был (`/content/`), правило и так не срабатывало. Замерено
   до и после на десяти страницах, значения совпали. */
.sect--airy { padding-block: clamp(4rem, 8vw, 7.5rem); }

/* Кегль здесь БОЛЬШЕ не переопределяется.

   Правило обещало «заголовки крупнее обычного» и давало 57.6px на 1900.
   Штатный кегль заголовка секции с тех пор вырос до 70.4px, то есть
   правило стало делать их МЕЛЬЧЕ обычного, ровно наоборот своему
   комментарию. Замер 25.08: 57.6px на `/site/`, `/_migration/` и
   `/ai-seo/`, при 70.4px у всех прочих секций тех же страниц. Одна роль
   двумя размерами это то, что правило в `CLAUDE.md` запрещает.

   Ловушка была в идентификаторах: правило висит на `#included` и подобных,
   а любая новая страница, взявшая тот же идентификатор, молча получает
   чужую настройку. Остальные строки блока (отступ снизу) роль не трогают
   и остаются. */
.sect--brief .sect__head h2 { max-width: 15ch; }
.sect--brief .sect__head { margin-bottom: var(--sp-l); }

/* --- Примеры ---------------------------------------------------------------
 * Кадры крупнее и с воздухом: сайт это предмет страницы, а не иллюстрация к
 * абзацу. Подпись под кадром дышит, а не жмётся.
 */
.frames { gap: var(--sp-l) var(--sp-m); }

.frame__cap { padding: var(--sp-m) var(--sp-m) calc(var(--sp-m) + 0.2rem); gap: 0.3rem; }

.frame__cap b {
  font-family: Asgard, Impact, sans-serif;
  font-size: var(--step-1);
  letter-spacing: -0.01em;
}

.frame__did { line-height: 1.45; }

/* --- Первый экран /site/: неделя одним проездом -----------------------------
 *
 * Секция высокая, внутри неё припинен один экран с канвасом. Устройство то же,
 * что у полёта на главной и `/un-saas/`: `position: sticky` внутри длинной
 * секции, прокрутка ведёт время сцены. Разница только в том, что рисует
 * сцену three, а не 2D-дизер.
 *
 * Десять актов на восьми станциях: пятая держит три слоя, поэтому проезд
 * считается по станциям, а не по актам.
 */
.week {
  position: relative;
  /* По экрану с небольшим запасом на акт: меньше и акт не успевает
     проиграться, больше и между предметами появляется пустая прокрутка. */
  /* Десять актов плюс хвост: последний обязан доиграть до того, как пин
     отпустит, иначе конец сцены попадает в один кадр со следующей секцией. */
  /* Тринадцать актов плюс хвост последнему. Число обязано совпадать с
     длиной ACTS в week.js: короче и последние акты не успевают играть. */
  /* Девять актов: дизайн-фаза это ОДИН слайд с четырьмя бетами внутри, а не
     четыре акта. Слайду отдан двойной перегон, иначе беты не успевают. */
  /* Восемь актов: build и deploy слиты в один, отдельного Live больше нет. */
  /* Хвост короткий: последний акт доигрывает к концу полосы, и дальше
     страница сразу уходит в блок кейсов. */
  height: calc(8 * 130svh + 30svh);
  background: var(--ink);
  color: var(--on-dark);
  /* Шапка sticky и занимает место в потоке: без подтяжки над тёмным первым
     экраном ложится полоса фона страницы, а на ней белый логотип. */
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}

.week__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.week__canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* Шапка и подпись акта НЕ идут через рендер: это единственное в кадре, что
   уже принадлежит читателю, и оно обязано быть резким. */
.week__head {
  /* Титром управляет прокрутка: значение приходит числом из сцены, а не
     переключателем, поэтому уход непрерывный. */
  opacity: var(--titr, 1);
  transition: opacity 0.2s linear;
  position: absolute;
  /* Не под шапкой: заголовок стоит в той же колонке, что подпись акта, и
     держит верхнюю треть кадра. Прижатый к самому верху, он читался
     техническим титром, а не заголовком страницы. */
  top: 26svh;
  left: 0;
  right: 0;
  z-index: 2;
  pointer-events: none;
}

/* Узкая колонка: заголовок обязан стоять слева и НЕ доходить до предмета.
   При 15ch вторая строка упиралась в бланк, и оба читались наложением. */
/* Две строки, как задумано разметкой: при 11ch они ломались на четыре, и
   заголовок съедал половину экрана. Кегль ниже общего титульного, потому что
   рядом стоит предмет и они не должны спорить. */
/* 42rem: правый край текста обязан стоять левее бланка (его левый край около
   805px на 1900, бланк в canvas, DOM его не видит, замерено по пикселям). При
   24ch на --hero строка «A website you own,» кончалась на 872 и заходила на
   бланк; PO 18.09: «заголовок наехал на контент». */
.week__head h1 { max-width: min(24ch, 42rem); }

/* Посадочный лист. Свойства взяты у .stage__sheet, который сажает полёт на
   /un-saas/: тот же подъём, та же длительность, то же состояние в
   data-done. Один приём на сайт, а не второй такой же. */
.week__sheet {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  align-content: center;
  opacity: 0;
  transform: translateY(1.1rem);
  transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1);
  pointer-events: none;
  /* Земля листа светлая, а секция несёт .on-dark: он задаёт ТОКЕНЫ, и без
     этого заголовок кейсов встал бы белым по белому. */
  background: var(--white);
  color: var(--ink);
  --heading-color: var(--text);
  --lede-color: var(--text-muted);
  --tag-color: var(--text-muted);
  --panel-line: var(--line);
  overflow: hidden;
}

/* Внутри листа кейсы обязаны уместиться в ОДИН экран: он высотой ровно с
   закреплённый кадр, и прокрутки у него нет. Рамки ниже обычного, шапка
   блока теснее. */
/* ⚠️ ПОСАДКА. Эти три правила лежали в `bench.css` и уехали туда вместе с
   переносом страницы из стенда в прод. Живая страница `bench.css` НЕ
   ПОДКЛЮЧАЕТ: в `dist/site/index.html` четыре таблицы стилей, и его среди них
   нет. Замерено развёрткой прокрутки: `data-done` встаёт в `true` на 0.97, а
   прозрачность листа остаётся 0.00 - и на 390, и на 1900. То есть блок кейсов
   «Sites that are running» не показывался на проде вообще никому. */
.week[data-done="true"] .week__sheet {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
/* Подпись акта и заголовок принадлежат проезду: на посадке они уходят. */
.week[data-done="true"] .week__hud,
.week[data-done="true"] .week__head {
  opacity: 0;
  transition: opacity 0.35s ease;
}

.week__sheet .sect__head { margin-bottom: var(--sp-l); }
.week__sheet .frame__win { aspect-ratio: 16 / 11; }
.week__sheet .frame__cap { padding: 0.6rem 0.8rem 0.8rem; }
.week__sheet .frame__did { display: none; }

.week__hud {
  position: absolute;
  left: clamp(1.25rem, 5vw, 5rem);
  bottom: var(--sp-l);
  z-index: 2;
  display: grid;
  gap: 0.4rem;
  max-width: min(46rem, 56vw);
  pointer-events: none;
}

.week__act {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
  margin: 0;
}

.week__act b {
  font-family: Asgard, Impact, sans-serif;
  font-size: var(--step-3);
  line-height: 1;
  color: #fff;
}

.week__note {
  margin: 0;
  max-width: 46ch;
  font-size: var(--step-0);
  line-height: 1.5;
  color: rgb(255 255 255 / 0.7);
}

/* Документ под сценой. Он и есть страница, когда сцены нет: без WebGL, с
   заблокированным скриптом и при prefers-reduced-motion читатель получает
   список этапов, а не пустое чёрное поле. */
.week__doc {
  position: relative;
  z-index: 1;
  padding: var(--sp-xl) max(1.25rem, 5vw) var(--sp-xxl);
}

/* Сцена не запустилась: секция распинывается, холст убирается, заголовок и
   документ встают в поток. ВАЖНО: отступ задаётся ТОЛЬКО по вертикали -
   .week__head несёт ещё и класс .wrap, и сокращённый padding убивал его
   горизонтальные поля, отчего заголовок уезжал в самый край. */
.week[data-week-on="off"] { height: auto; }
.week[data-week-on="off"] .week__pin { position: static; height: auto; }
.week[data-week-on="off"] .week__canvas { display: none; }
.week[data-week-on="off"] .week__hud { display: none; }

.week[data-week-on="off"] .week__head {
  position: static;
  opacity: 1;
  padding-block: var(--sp-xxl) 0;
}

.js .week[data-week-on="off"] .week__doc {
  position: relative;
  width: auto;
  height: auto;
  overflow: visible;
  clip-path: none;
  white-space: normal;
  padding: var(--sp-l) max(1.25rem, 5vw) var(--sp-xxl);
}

/* Без WebGL проезда нет, и запасной экран это ЕДИНСТВЕННОЕ, что видит
   посетитель. Значит, он обязан быть свёрстан, а не вывален списком:
   заголовок раздела моношрифтом, этапы двумя колонками, номер моношрифтом,
   хайрлайн под каждой строкой. Тот же язык, что у остальных перечней сайта. */
.week[data-week-on="off"] .week__doc h2 {
  font-size: var(--step-2);
  margin-bottom: var(--sp-m);
}

.week[data-week-on="off"] .week__doc ol {
  max-width: none;
  display: grid;
  gap: 0;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
  column-gap: var(--sp-l);
  list-style: none;
  counter-reset: stage;
  padding: 0;
  border-top: 1px solid var(--panel-line);
}

.week[data-week-on="off"] .week__doc li {
  counter-increment: stage;
  position: relative;
  padding: 0.85rem 0 0.85rem 2.8rem;
  border-bottom: 1px solid var(--panel-line);
  font-size: var(--step--1);
  line-height: 1.45;
  color: var(--lede-color);
}

.week[data-week-on="off"] .week__doc li::before {
  content: counter(stage, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 0.95rem;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step--2);
  color: var(--tag-color);
}

.week[data-week-on="off"] .week__doc li b {
  display: block;
  font-size: var(--step-0);
  color: var(--heading-color);
}


/* Пока JS есть, документ скрыт с самого начала: иначе он висит поверх
   заголовка всё время, пока сцена поднимается. */
.js .week__doc,
.week[data-week-on="true"] .week__doc {
  /* Остаётся в дереве для скринридера и поиска. */
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  padding: 0;
}

.week__doc ol { max-width: 60ch; display: grid; gap: var(--sp-s); }

/* ⚠️ ЗДЕСЬ СТОЯЛО «узкому экрану проезд не отдаём».
 *
 * Секция распинивалась, холст, заголовок и подпись акта снимались, а на их
 * место вставал служебный документ - список из девяти шагов. Итог: у первого
 * экрана страницы, которая продаёт сайт за $1,500, НА ТЕЛЕФОНЕ НЕ БЫЛО ДАЖЕ
 * ЗАГОЛОВКА. Довод «предметы там не читаются» был про кадрирование, а
 * кадрирование чинится в камере: `week.js` теперь считает отход и прицел от
 * пропорции окна.
 *
 * PO: «все первые экраны иммерсивный опыт при скролле, а не просто скролл».
 *
 * Документ остаётся тем, чем был на большом экране: скрытым запасным путём.
 * Если сцена не поднялась (нет WebGL, reduce-motion), его показывает ветка
 * `[data-week-on="off"]` выше - она общая, и на телефоне работает так же. */
@media (max-width: 60rem) {
  .week__head {
    /* Заголовок уходит ВВЕРХ кадра: предмет на телефоне стоит по центру
       колонки, и на 26svh заголовок ложился бы прямо на белый бланк. Камера
       со своей стороны опускает предмет в нижние две трети. */
    top: calc(var(--chrome-cover, var(--header-h)) + 2svh);
  }
  .week__head h1 { font-size: min(var(--step-4), 7.4vw); line-height: 1.04; max-width: none; }
  /* ⚠️ ЧТО НА ДЕСКТОПЕ СЛЕВА НАПРАВО, НА ТЕЛЕФОНЕ СВЕРХУ ВНИЗ. Правило PO.
     На большом экране слева стоит колонка слов (заголовок сверху, подпись
     акта снизу), справа предмет. Значит на телефоне сначала слова, потом
     предмет, а не подпись, загнанная под него. Предмет опускает камера. */
  .week__hud {
    top: 31svh; bottom: auto; left: 0; right: 0;
    padding-inline: max(1.25rem, 5vw);
    max-width: none;
  }
  .week__act b { font-size: min(var(--step-3), 6.4vw); }
  .week__note { font-size: min(var(--step-0), 3.6vw); max-width: none; }
  /* Лист кейсов внутри кадра: рамки в два столбца, шапка теснее. */
  .week__sheet .sect__head { margin-bottom: min(var(--sp-m), 2.2svh); }
  .week__sheet .sect__head h2 { font-size: min(var(--step-3), 6.2vw); line-height: 1.04; }
  .week__sheet .sect__head .lede { font-size: min(var(--step-0), 3.4vw); }
  /* ⚠️ Кадры идут В ОДИН СТОЛБЕЦ. Правило PO: что на десктопе слева направо,
     на телефоне сверху вниз. В две колонки они не только нарушали его, но и
     резались краем: карточка уже 18rem ломает своё содержимое.
     Три сайта в столбец в экран не влезают, поэтому лист получает прокрутку
     внутри себя - тем же приёмом, что чек на `/brand-platform/`. Цепочку
     прокрутки не обрываем: дойдя до конца, палец листает страницу дальше. */
  .week__sheet { align-content: start; }
  .week__sheet > .wrap {
    overflow-y: auto;
    max-height: 100%;
    padding-block: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.4svh)) var(--sp-l);
  }
  .week__sheet .frames { grid-template-columns: minmax(0, 1fr); gap: 0.6rem; }
  .week__sheet .frame__cap { padding: 0.5rem 0.7rem 0.7rem; }
  .week__sheet .frame__cap b { font-size: min(var(--step-1), 4.6vw); }
}

/* ==========================================================================
   /ai-seo/, первый экран: один интерфейс и лента реплик

   Витрина главной строит сцену из АКТОВ: предметы сменяют друг друга.
   Разговор так не идёт, реплики КОПЯТСЯ. Поэтому движок актов с этой
   страницы снят, и осталось общее: рельса даёт такт прокрутки, `data-read`
   печатает ответ по словам, `ask.js` держит поле ввода.

   Панель тёмная по решению PO. Это отход от `.chat` главной, где она белая:
   там она снимок чужого интерфейса среди шести мест, здесь она предмет
   всего экрана и стоит на тёмном грунте.
   ========================================================================== */

.ai-sect {
  height: calc(var(--n, 3.4) * 92svh);
  /* Подтяжка под липкую шапку. Без неё над тёмной секцией остаётся белая
     полоса в её высоту: замерено, секция начиналась на 68px, а фон страницы
     белый. Тот же приём стоит у витрины главной. */
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}
.ai-pin {
  background: var(--ink);
  color: var(--on-dark);
  --tp: min(calc(var(--p, 0) * var(--n, 3.4)), calc(var(--n, 3.4) - 0.02));
  /* Равные свободные поля сверху и снизу. Верхний паддинг несёт ещё и
     высоту шапки, поэтому одинаковой делается ДОБАВКА к ней, а не сам
     паддинг. Было 94 сверху против 114 снизу, экран читался задранным. */
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m));
  padding-bottom: var(--sp-m);
  /* ОДНА дорожка. У общего пина их три, а ребёнок здесь один: он садился
     в первую, две пустые съедали низ, и экран читался задранным. Замерено:
     дорожки 496 / 133.6 / 0 при одном ребёнке. */
  grid-template-rows: minmax(0, 1fr);
  align-content: center;
  overflow: hidden;
}
.ai-lay { align-self: center; }

/* Три колонки, как на странице цифровой инвентаризации: чтение, предмет,
   органы управления. Управление НАСТОЯЩЕЙ колонкой, а не абсолютом: у
   абсолютного элемента внутри сетки координаты считаются от его клетки, и
   первая попытка оставила выбор слева, замерено. */
.ai-lay {
  display: grid;
  /* Колонка чтения шире кегля героя: 112px не влезали в 22rem, и заголовок
     уезжал ПОД панель. Чинится раскладкой, а не размером: кегль тут по роли,
     а не по месту. */
  /* Колонка чтения ПОД КЕГЛЬ ГЕРОЯ, а не под глазомер: самая длинная
     строка заголовка занимает 605px на 1900, замерено диапазоном. При 34rem
     она обрезалась на 61px и уходила под панель. Кегль тут по роли, поэтому
     подстраивается колонка. */
  /* 30rem, не 26: на --hero (80px, 18.09) «Somebody.» шире 26rem и резался
     краем панели, замерено. */
  grid-template-columns: minmax(0, 30rem) minmax(0, 1fr) min(17rem, 26vw);
  gap: var(--sp-l);
  align-items: center;
  min-height: 0;
}
/* ⚠️ Здесь стоял комментарий, спорящий с каноном: «это заголовок секции, а
   не герой страницы, ступень --step-4». Токен в main.css говорит обратное:
   герой страницы это --step-5, и h1 первого экрана есть герой. Своего кегля
   у этого h1 больше нет, он берёт базовый (05.09, аудит по 21 странице). */
.ai-read h1 { line-height: 0.98; }
.ai-read .lede { margin-top: var(--sp-m); color: var(--on-dark-muted); }

/* --- Панель ---------------------------------------------------------- */
.ai-panel {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: min(62svh, 32rem);
  max-width: 44rem;
  justify-self: center;
  background: #0a0a0f;
  border: 1px solid rgb(255 255 255 / 0.09);
  border-radius: 16px;
  box-shadow: 0 40px 80px -40px rgb(0 0 0 / 0.9);
  overflow: hidden;
}
.ai-bar {
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.85rem 1.1rem;
  border-bottom: 1px solid rgb(255 255 255 / 0.07);
  font-size: var(--step--1);
}
.ai-dot { width: 8px; height: 8px; border-radius: 50%; background: #10a37f; }
.ai-name { color: var(--on-dark); }
.ai-tag {
  margin-left: auto;
  font-size: var(--step-label); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--on-dark-muted);
  border: 1px solid rgb(255 255 255 / 0.12); border-radius: 999px;
  padding: 0.1rem 0.5rem;
}

/* Лента едет вверх по такту: новая реплика приходит снизу, старые
   поднимаются. Сдвиг считается от такта, а не от высоты содержимого:
   содержимое меняется вместе с нишей, а такт нет. */
.ai-feed {
  padding: var(--sp-m) 1.05rem;
  display: grid; align-content: start; gap: 0.85rem;
  /* Сдвига ленты трансформом НЕТ: он двигает саму коробку вместе с её
     обрезкой, и первая реплика налезала на шапку панели, а последняя
     обрезалась снизу. Лента просто прокручивается внутри себя, как в
     настоящем чате, и новая реплика подкручивается скриптом. */
  scroll-behavior: smooth;
}
/* Реплика приходит на своём такте. Пара «вопрос и ответ» появляется
   целиком: вопрос без ответа читается как обрыв. */
.ai-turn {
  display: grid; gap: 0.6rem;
  /* Первая пара реплик стоит УЖЕ НА ВХОДЕ: без запаса 0.6 читатель
     приземлялся в пустое окно, замерено, видимых реплик ноль. Разговор
     начинается, а не ждёт прокрутки.

     Остальные приходят по такту, по одной. */
  opacity: clamp(0, calc((var(--tp) + 0.6 - var(--i) * 0.75) * 2.6), 1);
  transform: translateY(calc(clamp(0, calc(1 - (var(--tp) + 0.6 - var(--i) * 0.75) * 2.6), 1) * 0.8rem));
}
.ai-q {
  justify-self: end; max-width: 80%;
  padding: 0.6rem 0.9rem;
  background: rgb(255 255 255 / 0.07);
  border-radius: 14px 14px 3px 14px;
  font-size: var(--step--1); color: var(--on-dark);
}
.ai-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.7rem; align-items: start; }
.ai-who {
  width: 1.55rem; height: 1.55rem; border-radius: 50%;
  background: #10a37f; position: relative; flex: 0 0 auto;
}
.ai-who::after {
  content: "C"; position: absolute; inset: 0;
  display: grid; place-items: center;
  font-family: PlexMono, ui-monospace, monospace; font-size: 0.7rem; color: #041a13;
}
.ai-body { font-size: var(--step--1); line-height: 1.6; color: var(--on-dark); }
/* Названы ТРИ, и все три выделены: подпись слева обещает именно три, а
   выделен был один. Синим только наш клиент: «названный» и «наш» это
   разные вещи, и различать их надо формой, а не невидимостью остальных. */
.ai-body b { color: var(--on-dark); font-weight: 500; }
.ai-body b.is-ours { color: var(--blue-lo); }

.ai-field {
  display: flex; align-items: center; gap: 0.6rem;
  margin: 0 1.1rem 1.1rem;
  padding: 0.5rem 0.5rem 0.5rem 0.95rem;
  border: 1px solid rgb(255 255 255 / 0.12); border-radius: 999px;
}
.ai-in {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: none; outline: none;
  font: inherit; font-size: var(--step--1); color: var(--on-dark);
}
.ai-in::placeholder { color: var(--on-dark-muted); }
.ai-go {
  flex: 0 0 auto; width: 1.7rem; height: 1.7rem; padding: 0;
  border: 0; border-radius: 50%; background: var(--on-dark);
  position: relative; cursor: pointer;
}
.ai-go i { position: absolute; inset: 0; }
.ai-go i::after {
  content: ""; position: absolute; left: 50%; top: 52%;
  width: 6px; height: 6px; border: 2px solid #0a0a0f;
  border-left: 0; border-bottom: 0;
  transform: translate(-50%, -50%) rotate(-45deg);
}
.ai-go:focus-visible { outline: 2px solid var(--blue-lo); outline-offset: 2px; }

/* --- Выбор ниши ------------------------------------------------------- */
.ai-pick__key {
  display: block; margin-bottom: 0.6rem;
  color: var(--on-dark-muted);
}
.ai-pills { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.ai-p input { position: absolute; opacity: 0; pointer-events: none; }
.ai-p span {
  display: inline-block; cursor: pointer;
  padding: 0.26rem 0.6rem;
  border: 1px solid rgb(255 255 255 / 0.14); border-radius: 999px;
  font-size: var(--step--1); color: var(--on-dark-muted);
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.ai-p span:hover { color: var(--on-dark); }
.ai-p input:checked + span { background: var(--blue); border-color: var(--blue); color: #fff; }
.ai-p input:focus-visible + span { outline: 2px solid var(--blue-lo); outline-offset: 2px; }

/* ⚠️ Здесь стояло распинивание: секция `height: auto`, пин `static`, лента
   без сдвига и все реплики видны разом. Разговор при этом переставал БЫТЬ
   разговором - три готовых ответа списком. Секция несёт `data-rail-phone`,
   пин остаётся липким, реплики копятся по такту, как на большом экране.

   Колонка одна: чтение сверху по содержимому, панель во весь остаток, выбор
   ремесла под ней строкой пилюль. Тянущаяся строка ровно одна. */
@media (width < 60rem) {
  .ai-pin {
    padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.8svh));
    padding-bottom: min(var(--sp-s), 1.8svh);
  }
  .ai-lay {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
    gap: min(var(--sp-s), 1.8svh);
    align-self: stretch;
    align-items: stretch;
  }
  .ai-read h1 { font-size: min(var(--step-4), 7.6vw); line-height: 1.04; }
  .ai-read .lede { margin-top: min(var(--sp-s), 1.4svh); font-size: min(var(--step-0), 3.8vw); }
  /* Смыслы под чтением на телефоне съедали панель: три роли по кеглю сайта
     плюс сама панель не встают в один экран. Строка остаётся, кегль течёт. */
  .ai-lines { margin-top: min(var(--sp-s), 1.4svh); min-height: 3.4rem; }
  .ai-line { font-size: min(var(--step-0), 2.0svh); max-width: none; }
  .ai-panel { height: 100%; max-width: none; }
  .ai-pick { align-self: end; }
  /* ⚠️ Восемнадцать ремёсел в перенос занимали ПЯТЬ строк и оставляли панели
     двести точек: предмет экрана становился полоской, а ответ обрывался на
     середине фразы. Замерено на 390x844. Пилюли идут одной строкой с
     прокруткой вбок - экран отдан предмету, а выбор никуда не делся. */
  .ai-pills {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 0.2rem;
    margin-inline: calc(max(1.25rem, 5vw) * -1);
    padding-inline: max(1.25rem, 5vw);
  }
  .ai-pills::-webkit-scrollbar { display: none; }
  /* Без этого «Windows and gutters» ломался на три строки внутри своей
     пилюли: `nowrap` у контейнера не запрещает переносить подпись. */
  .ai-p span { white-space: nowrap; }
}
html:not(.js) .ai-turn { opacity: 1; transform: none; }
html:not(.js) .ai-feed { transform: none; }

/* Дописанная читателем реплика видна ВСЕГДА: её вызвал человек, а не такт
   прокрутки, и прятать её по такту значит съесть ответ на его же вопрос. */
.ai-turn.is-typed { opacity: 1; transform: none; }

/* Лента прокручивается внутри панели, когда реплик становится больше, чем
   влезает. Без этого дописанное уходит за нижний край. */
.ai-feed { overflow-y: auto; scrollbar-width: thin; }

/* Смыслы под прокруткой. Были в подвале витрины и ушли вместе с ним.
   Форма та же: строка стоит НА МЕСТЕ и сменяется, а не съезжает вниз,
   поэтому позиция абсолютная и коробке задана высота. Такты те же, что у
   реплик, чтобы смысл приходил вместе со своей парой. */
.ai-lines { position: relative; min-height: 4.4rem; margin-top: var(--sp-l); }
.ai-line {
  position: absolute; inset: 0 auto auto 0;
  max-width: 32ch;
  font-size: var(--step-1); line-height: 1.25;
  color: var(--on-dark-muted);
  /* Приход и уход ДВУМЯ множителями: одним выражением строка только
     копилась и все три горели разом, замерено. Последняя не гаснет, у неё
     нет следующей. */
  --in: clamp(0, calc((var(--tp) + 0.6 - var(--i) * 0.75) * 3), 1);
  --out: clamp(0, calc(((var(--i) + 1) * 0.75 - 0.45 - var(--tp)) * 3), 1);
  opacity: calc(var(--in) * var(--out));
  transition: opacity 0.35s ease;
}
.ai-line:last-child { --out: 1; }
.ai-line b { color: var(--on-dark); font-weight: 400; }
html:not(.js) .ai-line { position: static; opacity: 1; }
html:not(.js) .ai-lines { min-height: 0; }

/* Переход в следующий экран ЭФФЕКТОМ. Одно число на всю передачу, как на
   `/migration/`: непрерывно, а не переключателем, чтобы переход держался
   там, где читатель остановил колесо.

   Сцена уходит ДО прихода листа: перекрытие читается как две картинки
   одна поверх другой. */
.ai-pin { --hand: clamp(0, calc((var(--tp) - 2.35) * 1.9), 1); }

/* ⚠️ `--done-at` ОБЯЗАН равняться такту, на котором строка ниже доводит
   `.ai-lay` до нуля, иначе слой пропадёт раньше, чем догорит. Считается
   из неё же: непрозрачность обнуляется при `--hand` = 0.52, а `--hand`
   доходит до 0.52 при `--tp` = 2.35 + 0.52 / 1.9 = 2.624.

   Число читает рельса в `main.js` и ставит `[data-done]`. Пока лист
   поднят, невидимая раскладка под ним продолжала ловить клики и Tab:
   замерено, клик в центр листа попадал в `.ai-turn`. Приём и имя
   атрибута общие, та же пара стоит на `/site/`. */
.ai-pin { --done-at: 2.624; }
.ai-pin[data-done="true"] .ai-lay { pointer-events: none; visibility: hidden; }
.ai-lay { opacity: clamp(0, calc((0.52 - var(--hand, 0)) * 2.4), 1); }
.ai-sheet {
  position: absolute; inset: 0; z-index: 2;
  display: grid; align-content: center;
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m));
  padding-bottom: var(--sp-m);
  padding-inline: max(1.25rem, 5vw);
  overflow: clip;
  background: var(--white);
  color: var(--text);
  --heading-color: var(--text);
  --lede-color: var(--text-muted);
  opacity: clamp(0, calc((var(--hand, 0) - 0.5) * 2.4), 1);
  transform: translateY(calc(clamp(0, calc(1.2 - var(--hand, 0) * 1.6), 1) * 1.1rem));
  pointer-events: none;
}
.ai-sheet .wrap { padding-inline: 0; }
html:not(.js) .ai-sheet { position: static; opacity: 1; transform: none; }
html:not(.js) .ai-lay { opacity: 1; }
/* ⚠️ Лист передачи ОСТАЁТСЯ листом и на телефоне: `position: static` здесь
   превращал его в следующий блок, а страница припинена. Влезть в экран ему
   помогает кегль, а не отмена механики. */
@media (width < 60rem) {
  /* ⚠️ Тёмный лист на телефоне разъезжается на всю ширину, и СВЕТЛАЯ панель
     `.pact` внутри него доходила до самого верха: белый логотип оказывался
     белым по светлому. Замерено на 390x844. Лечится полосой тёмного листа под
     шапкой, а не перекраской шапки: шапка над этой секцией остаётся
     прозрачной, потому что сама секция тёмная. */
  .ai-sheet .wrap { padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.4svh)); }
  .pact__title { font-size: min(var(--step-3), 6.6vw); }
  .pact__sub { font-size: min(var(--step-0), 3.7vw); }
  .pact__row { gap: 0.3rem; padding-block: 0.4rem; }
  .pact__label { font-size: min(var(--step-label), 3.1vw); }
  .pact__value { font-size: min(var(--step--1), 3.3vw); }
}

/* --- Пространство за предметом --------------------------------------------

   Тёмное поле было плоской заливкой: панель лежала на нём, а не стояла в
   глубине. Приём взят у витрины главной вместе с объяснением: два слоя, ни
   один не рисует объектов, свет бренда позади и затемнение к краям.

   Свет ГРАДИЕНТОМ, а не компонентом `.bloom`: у того `mix-blend-mode`
   запрещает кешировать слой, и площадь пересчитывается каждый кадр
   прокрутки, а поверх неё и так каждый кадр меняется прозрачность реплик.

   Центры пятен сдвинуты к середине: предмет здесь по центру, а не справа. */
.ai-pin::before {
  content: "";
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(46% 40% at 52% 42%, rgb(1 48 249 / 0.30), transparent 70%),
    radial-gradient(38% 34% at 62% 62%, rgb(0 255 255 / 0.12), transparent 72%),
    radial-gradient(30% 30% at 44% 70%, rgb(116 255 128 / 0.08), transparent 74%);
}
.ai-pin::after {
  content: "";
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(70% 55% at 52% 47%, rgb(255 255 255 / 0.055), transparent 70%),
    radial-gradient(120% 90% at 52% 47%, transparent 30%, rgb(0 0 0 / 0.5) 100%),
    linear-gradient(180deg, rgb(0 0 0 / 0.35) 0%, transparent 22%, transparent 66%, rgb(0 0 0 / 0.45) 100%);
}
/* Содержимое стоит НАД светом, иначе тонет в нём. Лист перехода тоже: он
   светлый и обязан перекрывать свечение целиком. */
.ai-lay { position: relative; z-index: 1; }


/* Лист перехода несёт документ отказов, а он живёт на тёмном грунте. Без
   этого лист оставался белым, а документ на нём читался чужим куском. */
.ai-sheet--ink {
  background: var(--ink);
  color: var(--on-dark);
  --heading-color: var(--on-dark);
  --lede-color: var(--on-dark-muted);
}

/* Источник рядом с утверждением, а не сноской внизу страницы: цифра тут
   аргумент карточки, и проверить её надо не отходя. */
/* Примечание под тремя шагами: почему строки с ценой на странице нет.
   Имя своё, а не `.cite__src`: то досталось от снятого блока и говорило
   про источник, тогда как здесь стоит оговорка. */
.ai-note {
  margin-top: var(--sp-l); max-width: 62ch;
  font-size: var(--step--1); letter-spacing: 0.06em;
  color: var(--text-muted);
}

.cov__src {
  display: block; margin-top: 0.5rem;
  font-size: var(--step-label); letter-spacing: 0.1em;
  text-transform: uppercase; opacity: 0.7;
}

/* ==========================================================================
   /brand-platform/, первый экран: шесть поверхностей одной компании

   Приём ратифицирован в `docs/PAGE-CONCEPTS.md` и подтверждён в
   `docs/ICP-OWNER.md` §8: сломанный trust stack против собранного. Шесть
   поверхностей видны СРАЗУ и не листаются, потому что предмет тут не цитата,
   а противоречие между ними, а оно читается только целиком.

   Прокрутка добавляет не карточки, а вывод под каждой: какая это компания,
   если читать только её. Механизм не свой: такты, два множителя на смысловых
   строках и передача листом взяты у `/ai-seo/` без изменений.
   ========================================================================== */

.bp-sect {
  height: calc(var(--n, 4.6) * 92svh);
  /* Подтяжка под липкую шапку, иначе над тёмной секцией белая полоса в её
     высоту. Тот же приём стоит у витрины главной и у первого экрана
     `/ai-seo/`. */
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}
.bp-pin {
  background: var(--ink);
  color: var(--on-dark);
  --tp: min(calc(var(--p, 0) * var(--n, 4.6)), calc(var(--n, 4.6) - 0.02));
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m));
  padding-bottom: var(--sp-m);
  /* ОДНА дорожка: у общего пина их три, а ребёнок здесь один, и две пустые
     съедали низ. Замерено на `/ai-seo/`, повторяю вывод, а не ошибку. */
  grid-template-rows: minmax(0, 1fr);
  align-content: center;
  overflow: hidden;
}

/* Три ряда: шапка, стена, полоса расхождений. Ряды, а не колонки: при
   боковой колонке чтения «One company.» не вставало в строку при кегле
   70.4 и заголовок шёл в четыре строки. Кегль тут по РОЛИ и не трогается,
   значит неверна была раскладка.

   Приём привычный и на сайте уже стоит: заголовок в две строки, описание
   справа от него. */
.bp-lay {
  position: relative; z-index: 1;
  align-self: center;
  display: grid;
  grid-template-rows: auto auto auto;
  gap: var(--sp-m);
}
.bp-read {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 30rem);
  gap: 0.6rem var(--sp-xl);
  align-items: end;
}
.bp-read h1 { line-height: 0.96; margin: 0; }
.bp-read .lede { margin: 0; max-width: 34ch; }

/* Смыслы под прокруткой. Строка стоит НА МЕСТЕ и сменяется, поэтому позиция
   абсолютная и коробке задана высота. Приход и уход ДВУМЯ множителями: одним
   выражением строки только копились бы и горели разом. */
/* Смыслы стоят ПОД описанием, в той же колонке: у них одна роль, они
   объясняют предмет словами, и разносить их по разным местам нечестно. */
.bp-lines { position: relative; min-height: 4.2rem; margin-top: 0.7rem; }
.bp-line {
  position: absolute; inset: 0 auto auto 0;
  max-width: 38ch;
  font-size: var(--step-1); line-height: 1.25;
  color: var(--on-dark-muted);
  /* ⚠️ Окно строки задаётся ТАКТАМИ в разметке, а не номером.
     Прежняя формула нарезала по 0.95 такта подряд от `--i`, и строки
     разъезжались с кадрами при любой правке раскадровки: числа жили в двух
     местах и об этом никто не узнавал. Теперь отрезок стоит рядом с самой
     строкой, и сверить его с `CUT` можно глазами. */
  --in: clamp(0, calc((var(--tp) - var(--from, 0)) * 5), 1);
  --out: clamp(0, calc((var(--to, 9) - var(--tp)) * 5), 1);
  opacity: calc(var(--in) * var(--out));
  transition: opacity 0.35s ease;
}
/* Последняя строка держится до конца сама: у неё `--to` за пределом такта. */
.bp-line b { color: var(--on-dark); font-weight: 400; }

/* Стена. Три колонки на две строки: шесть штук в решётку 2x3 читаются как
   набор, а в один ряд как лента, и тогда пропадает то самое «все сразу». */
.bp-wall {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.7rem;
}

/* Карточка это ПРЕДМЕТ, а не строка таблицы: у неё своя рамка, своя шапка с
   местом и своя подпись источника. Место важнее цитаты, потому что вся суть
   в том, ГДЕ это написано. */
.bp-card {
  display: grid;
  grid-template-rows: auto 1fr auto auto;
  gap: 0.5rem;
  margin: 0;
  padding: 0.85rem 0.9rem 0.75rem;
  border: 1px solid rgb(255 255 255 / 0.1);
  border-radius: clamp(10px, 0.9vw, 14px);
  background: rgb(255 255 255 / 0.03);
  /* Вывод под цитатой приходит по такту, по одной карточке. Окно своё у
     каждой, шаг 0.38 такта: шесть штук укладываются к 2.2 из 4.6, и до
     передачи листом остаётся время просто посмотреть на стену. */
  --in: clamp(0, calc((var(--tp) - 0.3 - var(--i) * 0.38) * 4), 1);
}
.bp-card__where {
  padding-bottom: 0.5rem;
  border-bottom: 1px solid rgb(255 255 255 / 0.08);
  font-size: var(--step-label); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--on-dark-muted);
}
.bp-card__says {
  margin: 0;
  font-size: var(--step-0); line-height: 1.28;
  color: var(--on-dark);
}
/* Вывод: единственное синее на карточке, потому что ради него всё и стоит. */
.bp-card__reads {
  font-size: var(--step--1); line-height: 1.3;
  color: var(--blue-lo, #7f9dff);
  opacity: var(--in);
  transition: opacity 0.3s ease;
}
.bp-card__reads span::before { content: "reads as "; color: var(--on-dark-muted); }
.bp-card__src {
  font-size: var(--step-label); letter-spacing: 0.08em;
  color: var(--on-dark-muted); opacity: 0.65;
  overflow-wrap: anywhere;
}

/* Пары чисел. НЕ «ошибки»: которое из двух верное, знает только владелец, и
   в этом ровно всё дело, поэтому обе половины набраны одинаково. */
.bp-splits {
  display: grid; gap: 0.35rem;
  list-style: none; margin: 0; padding: 0;
}
.bp-split {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1rem;
  padding-top: 0.5rem;
  border-top: 1px solid rgb(255 255 255 / 0.08);
  opacity: clamp(0, calc((var(--tp) - 2.4 - var(--i) * 0.2) * 4), 1);
  transition: opacity 0.3s ease;
}
.bp-split__what {
  flex: 0 0 11rem;
  font-size: var(--step-label); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--on-dark-muted);
}
.bp-split__pair { display: flex; align-items: baseline; gap: 0.5rem; }
.bp-split__pair b {
  font-family: PlexMono, ui-monospace, monospace;
  font-weight: 500; font-size: var(--step-1);
  font-variant-numeric: tabular-nums; color: var(--on-dark);
}
.bp-split__w { font-size: var(--step-label); color: var(--on-dark-muted); }
.bp-split__vs { font-size: var(--step-label); color: var(--on-dark-muted); opacity: 0.6; }

/* Передача листом. Одно число на всю передачу, непрерывно, чтобы переход
   держался там, где читатель остановил колесо.

   ⚠️ `--done-at` ОБЯЗАН равняться такту, на котором строка ниже доводит
   `.bp-lay` до нуля: непрозрачность обнуляется при `--hand` = 0.52, а `--hand`
   доходит до 0.52 при `--tp` = 3.3 + 0.52 / 1.9 = 3.574. Число читает рельса
   в `main.js` и ставит `[data-done]`, иначе невидимый слой продолжает ловить
   клики и Tab. */
/* Передача начинается ПОСЛЕ того, как тело собралось. Складывание занимает
   первые 78% секции (это записано в `fold.js`), то есть до такта 3.59 из
   4.6; на собранном теле остаётся пауза, и только потом поднимается лист. */
.bp-pin { --hand: clamp(0, calc((var(--tp) - 3.95) * 3.1), 1); }
/* ⚠️ 3.95 + 0.52 / 3.1 = 4.118, такт, на котором строка ниже доводит
   `.bp-lay` до нуля. Числа обязаны сходиться, иначе слой пропадает раньше,
   чем догорит, или продолжает ловить клики после. */
.bp-pin { --done-at: 4.118; }
.bp-pin[data-done="true"] .bp-lay { pointer-events: none; visibility: hidden; }
.bp-lay { opacity: clamp(0, calc((0.52 - var(--hand, 0)) * 2.4), 1); }
.bp-sheet {
  position: absolute; inset: 0; z-index: 2;
  display: grid; align-content: center;
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m));
  padding-bottom: var(--sp-m);
  padding-inline: max(1.25rem, 5vw);
  overflow: clip;
  /* ⚠️ Лист СВЕТЛЫЙ, и шапка переворачивается вместе с ним.

     Он был тёмным по одной причине: белый лист закрывал полосу под липкой
     шапкой, и белый логотип оказывался на белом. Причина верная, но лечили её
     не там. Теперь секция сообщает шапке, что грунт стал светлым
     (`data-sheet-light` плюс `[data-done]` у рельсы), шапка садится на плиту и
     логотип темнеет. Грунт светлый, документ на нём белой бумагой. */
  background: var(--off);
  color: var(--text);
  --heading-color: var(--text);
  --lede-color: var(--text-muted);
  opacity: clamp(0, calc((var(--hand, 0) - 0.50) * 2.6), 1);
  transform: translateY(calc(clamp(0, calc(1.2 - var(--hand, 0) * 1.6), 1) * 1.1rem));
  pointer-events: none;
}
.bp-sheet .wrap { padding-inline: 0; }

/* Документ. Это буквально то, что покупают: одна страница, на которой
   решение записано. Поэтому он и выглядит листом бумаги, а не секцией
   страницы. Бумага своя, потому что грунт под ней тёмный.

   ⚠️ Здесь лежали ещё три комментария от блоков, которые я удалил, и два из
   них были ОБОРВАНЫ на полуслове, без `*/`. Незакрытый комментарий съедал
   правило ниже: `.bp-bill { align-self: start; }` в сборку не попадало.
   Поймано ревью, подтверждено счётом: 224 открытия против 222 закрытий. */
.bp-bill { align-self: start; }
/* Без скрипта такта нет, и всё обязано читаться как обычная страница. */
html:not(.js) .bp-sect { height: auto; }
html:not(.js) .bp-pin { position: static; height: auto; overflow: visible; }
html:not(.js) .bp-line { position: static; opacity: 1; }
html:not(.js) .bp-card__reads,
html:not(.js) .bp-split { opacity: 1; }
html:not(.js) .bp-sheet { position: static; opacity: 1; transform: none; }
html:not(.js) .bp-lay { opacity: 1; }

@media (width < 68rem) {
  .bp-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* ⚠️ Здесь стояло распинивание со ВСЕМ содержимым разом: шесть строк смыслов
   списком, все карточки и обе стороны сплита с `opacity: 1`, лист следующим
   блоком. Довод «на узком экране сцены не будет» был верен, пока сцена была
   живым WebGL: `room.js` выключает себя ниже 60rem. Но кадры теперь играет
   `roomplay.js`, а у него порога по ширине НЕТ - проверено по файлу. Сцена на
   телефоне доступна, и первый экран может быть опытом.

   Колонка одна, стена в две колонки вместо трёх, кегли отдают высоту. */
@media (width < 60rem) {
  .bp-pin {
    padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.6svh));
    padding-bottom: min(var(--sp-s), 1.6svh);
  }
  .bp-read { grid-template-columns: minmax(0, 1fr); gap: 0.4rem; align-items: start; }
  /* Замерено: набор первого экрана выходил 899 при пине 844, и заголовок
     уезжал под шапку. Кегли и отбивки сведены так, чтобы набор влезал. */
  .bp-lay { gap: min(var(--sp-s), 1.4svh); }
  .bp-read h1 { font-size: min(var(--step-4), 6.9vw); line-height: 1.02; }
  .bp-read .lede { max-width: none; font-size: min(var(--step-0), 3.5vw); }
  .bp-lines { min-height: 2.9rem; margin-top: 0.25rem; }
  /* ⚠️ Разбор в списке РОВНО ОДИН (`site.js`, `bs.splits`), и гасить ради
     него стену нельзя: экран остаётся почти пустым - проверено кадром.
     Он остаётся своей строкой под стеной, а место под него освобождает
     СТЕНА: на телефоне у карточек снимается адрес страницы. Место названо
     словами («THE ROOF PAGE»), а мелкий URL под цитатой на 390 точках
     занимает по две строки на карточку, шесть карточек = 58 точек среза,
     ровно тех, которых не хватало. */
  .bp-card__src { display: none; }
  .bp-splits { gap: 0.35rem; align-content: start; }
  .bp-split { padding-top: 0.42rem; }
  .bp-split__what { flex: 0 0 100%; font-size: min(var(--step-label), 2.9vw); }
  .bp-split__pair b { font-size: min(var(--step-0), 3.8vw); }
  .bp-line { font-size: min(var(--step-0), 1.9svh); max-width: none; }
  .bp-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.35rem; }
  .bp-card { padding: 0.42rem 0.5rem; gap: 0.25rem; }
  .bp-card__says { font-size: min(var(--step--1), 3.2vw); line-height: 1.2; }
  .bp-card__where, .bp-card__reads { font-size: 0.52rem; }
  /* ⚠️ Чек в листе - ВОСЕМНАДЦАТЬ строк, и на телефоне он не влезает в экран
     ни при каком кегле: замерено, содержимое 1465 при листе 844. Лист сам
     получает прокрутку внутри себя. Цепочка прокрутки НЕ обрывается
     (`overscroll-behavior` не трогаем): дойдя до конца чека, палец продолжает
     листать страницу, а не упирается в стену. */
  .bp-sheet .wrap {
    padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.4svh));
    padding-bottom: var(--sp-l);
    overflow-y: auto;
    max-height: 100%;
  }
  .bp-sheet { grid-template-rows: minmax(0, 1fr); }
}

/* --- Развёртка: холст и путь без него --------------------------------------

   Холст стоит ПОД чтением и правее его: развёртка широкая, а под колонкой
   заголовка ей делать нечего. Ширина колонки повторяет ту, что задана
   раскладке шапки, одним числом, потому что расхождение читалось бы как
   съехавшая сцена.

   ⚠️ Ничего не прячется заранее. Стена карточек стоит на экране до тех пор,
   пока сцена не нарисовала ПЕРВЫЙ кадр и не поставила `data-drawn`. Без
   модулей, без WebGL и на узком экране этого не случится никогда, и секция
   остаётся тем, чем была: серверной и читаемой. */
/* ⚠️ Коробку держит ОБЁРТКА, а холст занимает её процентами. Замерено:
   холст был 300x150, свой внутренний размер. У абсолютно позиционированного
   ЗАМЕЩАЕМОГО элемента ширина при `auto` не растягивается между left и
   right, в отличие от обычного блока, и сцена рисовалась в марку. */
.bp-stage {
  position: absolute;
  inset: 0 0 0 clamp(0px, 26vw, 30rem);
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s ease;
  /* Кромки растворяются, а не обрезаются. Кадр снят на своём фоне, и без
     маски слой читается вклеенным прямоугольником: видна ровная вертикаль
     слева, где начинается холст, и ровная горизонталь сверху. Растворение
     идёт со стороны текста и сверху, где граница и заметна.

     ⚠️ Растворение ШИРОКОЕ, и это замер, а не вкус.

     При 14% слева перепад на шве уже был мягким: по строке пикселей живого
     сайта самый резкий скачок в полосе 400..800 составлял 1.7 из 255. Но
     блоком читалась не кромка, а УРОВЕНЬ: страница стоит на яркости 3, а
     пустое пространство сцены стояло на 20. Стену в сцене притушили до 11, а
     здесь растянули выход, чтобы сцена появлялась через треть своей ширины, а
     не начиналась сразу. Добавлен и правый край: раньше кадр обрывался о край
     окна отвесно. */
  -webkit-mask-image:
    linear-gradient(to right, transparent, #000 34%, #000 94%, transparent),
    linear-gradient(to bottom, transparent, #000 14%, #000 82%, transparent);
  mask-image:
    linear-gradient(to right, transparent, #000 34%, #000 94%, transparent),
    linear-gradient(to bottom, transparent, #000 14%, #000 82%, transparent);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.bp-canvas { display: block; width: 100%; height: 100%; }

/* Запасной кадр. Виден ДО скрипта и остаётся единственным, если скрипт не
   выполнился: без него на месте сцены была бы пустота. Прячется только когда
   проигрыватель нарисовал первый кадр по-настоящему. */
.bp-poster {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* ⚠️ Прячется по ЛЮБОМУ из двух признаков.
   Раньше стоял только признак проигрывателя. В сборке с живой сценой
   проигрыватель не подключается, признак не появлялся никогда, и запасной кадр
   навсегда оставался поверх холста: у него `position: absolute`, у холста
   `static`, а позиционированный рисуется выше независимо от порядка в разметке.
   Живая сцена при этом исправно работала снизу, и её не было видно. Замерено:
   буфер холста 1420x800 и `roomBrand` рапортует «кузов оклеен», а в кадр шёл
   постер, снятый ещё до оклейки. */
.bp-stage[data-roomplay-drawn="true"] .bp-poster,
[data-room][data-drawn="true"] .bp-poster { opacity: 0; }
.bp-poster { transition: opacity 0.3s ease; }

/* Режим съёмки: на экране остаётся ТОЛЬКО сцена. Кадры обязаны содержать её
   одну, иначе запечённый в них текст рисуется поверх живого второй раз. */
/* ⚠️ Прячется ПОИМЕННО, а не «всё кроме секции»: у страницы под body лежат
   шапка, полоса аренды, подвал и служебные ссылки, и правило «кроме
   .rail-sect» их не покрывало. Поймано на кадре: шапка и меню остались. */
html[data-bare="true"] .header,
html[data-bare="true"] .meter,
html[data-bare="true"] .skip,
html[data-bare="true"] footer,
html[data-bare="true"] .sect:not(.rail-sect) { display: none; }
html[data-bare="true"] .bp-lay,
html[data-bare="true"] .bp-credit,
html[data-bare="true"] .bp-sheet { display: none; }
/* ⚠️ Запасной кадр прячется тоже. Он показывает СТАРЫЙ снимок, и на кадре я
   принял его за живую шапку: текст в нём запечён с прошлого прогона. Оставь
   его тут, и каждый новый рендер запекал бы предыдущий. */
html[data-bare="true"] .bp-poster { display: none; }
html[data-bare="true"] .bp-stage { inset: 0; }
html[data-bare="true"] .bp-sect { margin-top: 0; }
html[data-bare="true"] .bp-pin { padding: 0; }

/* Атрибуция моделей. Тихая, но НЕ спрятанная: у CC-BY условие выполняется
   тем, что читатель может её увидеть. Поэтому не `display: none` и не нулевая
   непрозрачность, а мелкий кегль в углу сцены. */
.bp-credit {
  position: absolute; z-index: 2;
  right: max(1.25rem, 3vw); bottom: 1rem;
  max-width: 52ch;
  margin: 0;
  font-size: var(--step-label); letter-spacing: 0.06em;
  color: var(--on-dark-muted); opacity: 0.5;
  text-align: right;
}
.bp-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.bp-credit:hover { opacity: 0.9; }
@media (max-width: 60rem) {
  .bp-credit { position: static; text-align: left; margin-top: var(--sp-m); }
}
/* ⚠️ Сцена ГАСНЕТ ПЕРЕД листом, а не остаётся под ним.

   Гасла только колонка текста (`.bp-lay`), а холст сцены ей не родня: он
   сосед, и его непрозрачность вела только готовность. Из-за этого на такте
   0.90 второй экран читался ПОВЕРХ ещё видимой сцены, два слоя разом.

   Числа разведены так, чтобы между слоями был зазор, а не нахлёст: сцена
   доходит до нуля при `--hand` 0.45, лист начинает проявляться с 0.50. */
.bp-sect[data-drawn="true"] .bp-stage {
  opacity: clamp(0, calc((0.45 - var(--hand, 0)) * 3.4), 1);
}

/* Стена и полоса расхождений это запасной путь. Как только сцена ожила, они
   уходят из потока: держать их рядом значило бы напечатать цитаты дважды. */
.bp-sect[data-drawn="true"] .bp-wall,
.bp-sect[data-drawn="true"] .bp-splits { display: none; }

/* Когда сцена жива, чтение занимает свою колонку и не растягивается на всю
   ширину: справа стоит предмет. */
.bp-sect[data-drawn="true"] .bp-read {
  grid-template-columns: minmax(0, 1fr);
  /* 44rem, не 26: потолок ставился под 70px, на --step-5 заголовок в 26rem
     шёл ДЕВЯТЬЮ строками и уходил за низ экрана (848 при 800). Слева от стола
     в сцене пусто, колонке есть куда расти. Замерено на 1900x800. */
  max-width: 44rem;
}
.bp-sect[data-drawn="true"] .bp-lay { align-content: center; min-height: 0; }

@media (max-width: 60rem) {
  /* ⚠️ Довод здесь был УСТАРЕВШИЙ: «тот же брейкпоинт, что зашит в `fold.js`,
     скрипт на этой ширине three вообще не запрашивает». Живого WebGL тут
     давно нет, кадры играет `roomplay.js`, и порога по ширине у него НЕТ -
     проверено по файлу. Сцена на телефоне технически доступна.

     Настоящая причина другая и она в весе: последовательность это 120 кадров,
     8.7 МБ. Открывать её человеку по мобильной сети нельзя. Поэтому на
     телефоне предметом первого экрана остаётся СТЕНА, и теперь она живая:
     секция припинена, карточки и разбор приходят по такту.

     Разбудить сцену на телефоне = сначала сделать лёгкую раскадровку, а не
     снять эту строку. */
  .bp-stage { display: none; }
  /* ⚠️ Подпись к моделям УХОДИТ ВМЕСТЕ СО СЦЕНОЙ.
     Здесь сцены нет вовсе, а подпись оставалась и стояла НАД заголовком:
     первое, что читает человек с телефона, это перечень авторов 3D-моделей,
     которых он не увидит. У CC-BY условие выполняется там, где работу видят;
     работы тут нет, значит и условия нет. */
  .bp-credit { display: none; }
  .bp-sect[data-drawn="true"] .bp-wall,
  .bp-sect[data-drawn="true"] .bp-splits { display: grid; }
  .bp-sect[data-drawn="true"] .bp-read { max-width: none; }
  /* ⚠️ Строки СНОВА СМЕНЯЮТСЯ. Здесь стояло «остаётся одна», и довод был
     верен: прокрутка их не вела, потому что секция была распинена, и все
     четыре зажигались разом. Секция припинена, такт есть, довод отпал. */
  /* Пара имён в столбик: в строку она ломалась, и `vs` уезжал в конец. */
  .bp-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.2rem; }
  .bp-split__vs { display: none; }
}

/* ⚠️ Лист поставки ПОВТОРЯЕТ `.tariff` с главной, числами, а не на глаз.

   PO поставил два кадра рядом: «почему не выглядит так же минималистично».
   Разница была измеримой, а не вкусовой. На главной 12 строк, каждая ровно в
   одну строку, имена по 2-3 слова, ни одного группового заголовка, слева
   заголовок и одна строка в восемь слов. У меня было 16 позиций плюс пять
   подзаголовков, четыре строки переносились на вторую, и слева стояли абзац на
   пять строк и оговорка на шесть.

   Поэтому здесь взяты те же значения, что у `.tar`: кегль `--step--1`, отбивка
   0.24rem, отточие 1px точками, столбец количества 7.5rem с табличными цифрами.
   Бумага та же, `#f7f5ef` на тёмной подложке карточки. */
.bp-bill { perspective: 1400px; align-self: start; }
.bp-bill__p {
  box-sizing: border-box;
  padding: clamp(1.3rem, 2.6vw, 2rem) clamp(1.4rem, 3vw, 2.4rem);
  background: #f7f5ef;
  color: #14161c;
  border-radius: 3px;
  box-shadow: 0 2px 4px rgb(0 0 0 / 0.18), 0 30px 60px rgb(0 0 0 / 0.22);
  transform-origin: 50% 0;
}
.bp-bill__top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-s);
  padding-bottom: 0.7rem;
  border-bottom: 2px solid #14161c;
}
.bp-bill__mark {
  font-family: Asgard, Impact, sans-serif;
  font-size: var(--step-0); letter-spacing: -0.01em;
}
.bp-bill__mark sup { font-size: 0.5em; vertical-align: super; }
.bp-bill__t {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label); letter-spacing: 0.22em;
  text-transform: uppercase; color: rgb(20 22 28 / 0.55);
}
/* Отбивка шапки чека тоже течёт по высоте окна.

   ⚠️ Работает она НЕ так, как поля бумаги, и разница считается, а не
   угадывается. Лист центрируется: уменьшение отступа ВНУТРИ чека на d
   поднимает нижнюю строку на d/2, а уменьшение полей самой бумаги на d
   поднимает верх на d/2 и опускает низ на d/2, то есть в сумме на ноль.
   Поэтому поля откачены, а этот отступ оставлен.

   Прямая подобрана так, чтобы при высоте от 800 значение упиралось в прежние
   0.7rem и на экране пользователя не менялось ничего. */
.bp-bill__rows {
  list-style: none; padding: 0;
  margin: clamp(0.2rem, 3vh - 12.8px, 0.7rem) 0 0;
}
.bpr {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.24rem 0;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step--1);
}
.bpr__n { white-space: nowrap; }
.bpr__dots {
  flex: 1 1 auto; min-width: 1.5rem; height: 0.55em;
  border-bottom: 1px dotted rgb(20 22 28 / 0.42);
}
.bpr__w {
  color: rgb(20 22 28 / 0.48);
  font-size: var(--step-label); letter-spacing: 0.06em; white-space: nowrap;
}
.bpr__q {
  min-width: 9.5rem; text-align: right; font-weight: 600;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Главный артефакт выделен весом, а не цветом: на бумаге цвет читается
   ссылкой, а тут ничего не нажимается. */
.bpr--lead .bpr__n { font-weight: 600; }

/* ⚠️ Высота строки считается от ВЫСОТЫ ОКНА, одним правилом.

   Лист режет содержимое (`overflow: clip`), а чек в 18 строк выше короткого
   окна. Сперва здесь стояли три порога по `max-height`, и это была подгонка
   под те размеры, что я успел померить: между порогами дефект возвращался.
   Замерено на 1440x700, где срезало 4 точки ровно потому, что размер попал
   в щель между 780 и 680.

   Теперь отступ строки линеен по vh и упирается в потолок 0.24rem: 3.0
   точки при окне 800 и ноль при 600.

   ⚠️ Отступ строки это ЕДИНСТВЕННЫЙ рычаг, который тут работает. Поля
   самой бумаги пробовались и откачены: лист центрируется по вертикали,
   поэтому уменьшение полей на N смещает верх и низ на N/2 каждый, и нижняя
   строка не поднимается ни на точку. Замерено: срез не сдвинулся с 6.

   ⚠️ Дефект НЕ мой: на проде до ступеней цены срезало 4 точки на 1366x768,
   27 на 1280x720 и 67 на 1280x640. Это правило чинит и его. */
.bpr { padding: clamp(0px, 1.6vh - 9.8px, 0.24rem) 0; }
.bp-bill__adds { margin-top: clamp(0.1rem, 1.2vh - 5px, 0.5rem); }
.bp-bill__foot {
  display: flex; justify-content: space-between; gap: 1rem;
  /* ⚠️ Отбивка итога тоже течёт по высоте окна, и по той же причине, что у
     `.bp-bill__rows`: она стоит ВЫШЕ ступеней, поэтому её уменьшение на d
     поднимает нижнюю строку на d/2.

     ⚠️ ЛОВУШКА ДЛЯ СЛЕДУЮЩЕЙ ПРАВКИ: каждая новая строка чека съедает запас
     на коротком окне, и подобранные тут прямые перестают спасать. Ступень
     «сайт» добавила одну строку, и срез на 1280x600 вернулся с нуля на 8
     точек. После ЛЮБОЙ новой строки замерять заново, иначе уедет молча. */
  margin: clamp(0.3rem, 4.8vh - 24px, 0.9rem) 0 0;
  padding-top: clamp(0.25rem, 3.6vh - 17.6px, 0.7rem);
  border-top: 2px solid #14161c;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label); letter-spacing: 0.14em; text-transform: uppercase;
}

/* Ступени цены как опции в накладной.

   Под итогом и мельче его, цифры в тот же правый край. Не ряд крупных
   цифр: `docs/PAGE-CONCEPTS.md` запрещает служебной странице заканчиваться
   крупной цифрой, а этой отдельно запрещает делать цену темой.

   Регистр НЕ капсом, в отличие от итога: это фразы, а не метки. */
.bp-bill__adds {
  display: grid; gap: 0.12rem;
  list-style: none; margin: 0; padding: 0;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label); letter-spacing: 0.06em;
  color: rgb(20 22 28 / 0.48);
}
.bp-bill__adds li { display: flex; justify-content: space-between; gap: 1rem; }
.bp-bill__adds span + span { font-variant-numeric: tabular-nums; white-space: nowrap; }


/* ===== Стена брендов на `/brand-platform/` ================================

   Знаки клиентов, каждый на своём грунте. Геометрия карточки взята у эталона
   системы (`docs/DESIGN-SYSTEM.md` §4, `.doors`): шов 1px, радиус и тень
   оттуда же, ничего своего.

   ⚠️ Грунт у каждой панели СВОЙ, и это не прихоть: замерено на стенде из
   пяти знаков рядом, что чёрный скрипт The View пропадает на тёмном, а белые
   знаки Tim's и Aersoma пропадают на светлом.  */
.bwall {
  /* ⚠️ Имя своё, не `.marks`: тот класс УЖЕ ЗАНЯТ в `main.css` под ряд
     чипов, `display: inline-flex`. Моя сетка молча не работала, пятая панель
     уезжала на второй ряд, а `grid-template-columns` считался и был не при
     делах. Ровно от этого стоит гейт `collide`.

     ⚠️ Пять колонок ЯВНО, а не `auto-fit`. Наследованный от `.doors`
     `auto-fit` увёл пятую панель на второй ряд: замерено, четыре встали в
     95..1471, пятая в 95..889 ниже. Брендов ровно пять, и ряд у них один. */
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  margin-top: var(--sp-xl);
}
@media (max-width: 68rem) { .bwall { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 34rem) { .bwall { grid-template-columns: 1fr; } }
.bcard {
  display: grid; grid-template-rows: auto 1fr auto auto;
  gap: var(--sp-s);
  min-height: clamp(19rem, 26vw, 25rem);
  padding: var(--sp-m);
  background: var(--plate); color: #fff;
  text-decoration: none;
  /* Цвета текста заданы ОДИН раз тут и переворачиваются одной строкой ниже,
     чтобы светлая панель не потребовала своей копии всех правил. */
  --bc-dim: rgb(255 255 255 / 0.52);
  --bc-line: rgb(255 255 255 / 0.16);
}
.bcard.is-light {
  color: var(--ink);
  --bc-dim: rgb(20 22 28 / 0.52);
  --bc-line: rgb(20 22 28 / 0.14);
}
.bcard__who {
  font-size: var(--step-label); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--bc-dim);
}
/* Знак это ПРЕДМЕТ панели: он занимает её, а не подписывает. */
.bcard__sign { display: grid; place-items: center; min-height: 0; }
.bcard__sign img {
  width: auto; max-width: 84%; max-height: clamp(4.5rem, 7vw, 6.5rem);
  object-fit: contain;
}
/* Чёрный знак выбивается в белое. Инверсия, а не подмена файла: белой версии
   у нас нет, а знак одноцветный, и инверсия его не искажает. */
.bcard__sign img.is-knockout { filter: invert(1); }
/* Что получил: токенами, а не фразой через запятую. Каждый токен читается
   отдельно, и глазу есть за что зацепиться. */
.bcard__gets { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.bcard__gets i {
  font-style: normal;
  padding: 0.18rem 0.5rem;
  border: 1px solid var(--bc-line); border-radius: 999px;
  font-size: var(--step-label); letter-spacing: 0.06em;
}
/* Повод нажать: адрес со стрелкой. Стрелка уезжает под курсором, потому что
   это единственное движение, которое тут что-то значит. */
.bcard__go {
  display: flex; align-items: baseline; gap: 0.35rem;
  padding-top: var(--sp-s); border-top: 1px solid var(--bc-line);
  font-size: var(--step-label); letter-spacing: 0.08em; color: var(--bc-dim);
}
.bcard__go i { font-style: normal; transition: transform 0.24s ease; }
/* ⚠️ При наведении карточка синеет, и это НЕ моя правка: так устроен
   `.door:hover` в системе. Значит весь текст обязан перевернуться в белый,
   включая светлую панель BAWC, где он тёмный. Иначе на синем остаётся чёрный,
   и карточка читается сломанной: ровно это увидел PO.

   Переворот идёт через те же две переменные, поэтому светлой панели не нужна
   своя копия правил. */
.bcard:hover, .bcard:focus-visible,
.bcard.is-light:hover, .bcard.is-light:focus-visible {
  color: #fff;
  --bc-dim: rgb(255 255 255 / 0.74);
  --bc-line: rgb(255 255 255 / 0.42);
}
/* Знак светлой панели выбивается в белый силуэт: серо-красный логотип на
   синем тускнеет так же, как тускнел бы на тёмно-синем. */
.bcard.is-light:hover .bcard__sign img,
.bcard.is-light:focus-visible .bcard__sign img { filter: brightness(0) invert(1); }
.bcard:hover .bcard__go i { transform: translate(0.15rem, -0.15rem); }
@media (prefers-reduced-motion: reduce) { .bcard__go i { transition: none; } }

/* ===== Страница услуги Knowledge OS, `/knowledge-os/` ======================

   Выросла из стенда `/_arrive/` и переехала сюда целиком 02.09 по указанию PO
   («выкатывай в прод»). Классы `.arv-` оставлены: имя стенда, а не страницы,
   ровно как `.bp-` и `.ai-` у соседей. Ниже прежняя шапка стенда. */
/* =====

   «Подъехал к адресу, и оно уже знает». Строки записи всплывают над улицей по
   такту рельсы, каждая со своим штампом.

   ⚠️ Механика ВЗЯТА, а не написана: `--p` пишет общая рельса, `--tp` считает
   пин, строка сравнивает его со своими `--from` / `--to`. Ровно как строки на
   `/brand-platform/`.  */

.arv-sect {
  --n: 4;
  /* Высота такта 92svh, как у `.ai-sect` и `.bp-sect`, а не 105svh общей
     рельсы: герой отпускает пин раньше, и следующий блок подходит без
     лишнего пустого хода. */
  height: calc(var(--n, 4) * 92svh);
  /* ⚠️ Подтяжка под липкую шапку, ровно как у `.ai-sect`. Без неё над тёмным
     героем остаётся светлая полоса в высоту шапки, и БЕЛЫЙ ЛОГОТИП ложится на
     белое: в кадре его просто нет. Видно только глазами, разметка при этом
     совершенно исправна. */
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}
.arv-pin {
  /* ⚠️ position НЕ ЗАДАЁТСЯ.  уже липкий, и своё
     `position: relative` тут перебивало sticky: секция не припинивалась,
     кадры выходили пустыми, а разметка при этом выглядела рабочей. */
  /* ⚠️ FLEX, а не grid. На сетке `align-content: center` тут не центрировал
     ничего: замерено дважды, содержимое упрямо стояло у верхнего края коробки
     (сверху 40, снизу 171). Колонка с `justify-content: center` центрирует
     детерминированно. */
  display: flex; flex-direction: column; justify-content: center;
  /* ⚠️ Грунт пина ЧЕРНИЛА, как у `.ai-pin`. Без него передача листом шла
     поверх белой страницы: сцена уже погасла, лист ещё на четверти, и кадр
     читался серой плитой. Замечено кадром при `--hand` ≈ 0.6. */
  background: var(--ink);
  /* ⚠️ Равные поля сверху и снизу. Верхний паддинг несёт ЕЩЁ И высоту шапки,
     поэтому одинаковой делается ДОБАВКА к ней, а не сам паддинг. Без этого
     сверху оставалось 56 точек, снизу 155. */
  --arv-pad: clamp(2rem, 5vh, 4rem);
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--arv-pad));
  padding-bottom: var(--arv-pad);
  /* Такт: доля прокрутки, умноженная на число тактов секции. */
  --tp: min(calc(var(--p, 0) * var(--n, 4)), calc(var(--n, 4) - 0.02));
  overflow: clip;
}

/* --- Вид ---------------------------------------------------------------- */

.arv-view { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.arv-view video, .arv-view img {
  width: 100%; height: 100%; object-fit: cover; object-position: 50% 42%;
  /* Наплыва больше нет: движение даёт само видео, перематываемое прокруткой.
     Масштаб оставлен постоянным, чтобы кадр перекрывал края. */
  transform: scale(1.02);
}
/* Затемнение слева, где ложится запись: без него текст лёг бы на листву. */
.arv-view__wash {
  position: absolute; inset: 0;
  background:
    /* Свет первого экрана (`--first-glow`, тот же, что `data-glow` на
       остальных страницах). Здесь он в вуали, а не псевдоэлементом пина:
       под видео его не видно, а над текстом ему не место. */
    var(--first-glow),
    linear-gradient(100deg, rgb(0 1 8 / 0.82) 0 30%, rgb(0 1 8 / 0.44) 56%, rgb(0 1 8 / 0.08) 100%),
    linear-gradient(to top, rgb(0 1 8 / 0.6), transparent 38%),
    /* ⚠️ Полоса под шапкой. Меню и логотип белые, а верх кадра это небо:
       без неё пункты меню ложатся на светлое и не читаются. Замерено глазами
       в кадре, разметка при этом исправна. */
    linear-gradient(to bottom, rgb(0 1 8 / 0.72), transparent 14%);
}


/* --- Раскладка первого экрана. Образец: `/ai-seo/`. --------------------- */

/* Три колонки: чтение, панель, чипы. Ровно так стоит первый экран на
   `/ai-seo/`, и своей раскладки тут заводить незачем. */
.arv-lay {
  position: relative; z-index: 1;
  display: grid; gap: var(--sp-l);
  align-items: start;
}
@media (min-width: 68rem) {
  .arv-lay {
    /* ⚠️ Левая колонка держит `h1`. На 27rem он ломался на ШЕСТЬ строк и
       лез под шапку: 34rem дают три, как на остальных страницах. */
    /* Ширины подогнаны под рамки, которые PO нарисовал на кадре: панель
       по центру шире, колонка чипов уходит правее. */
    /* 40rem, не 31: на --step-5 слово «remembers.» шире 31rem и вылезало
       из колонки на 169-248 точек (замерено на трёх окнах). Панель отдаёт
       разницу, у неё запас. */
    grid-template-columns: minmax(0, 42rem) minmax(0, 39rem) minmax(0, 17rem);
    gap: var(--sp-xl);
    /* ⚠️ Колонки начинаются С ОДНОЙ ЛИНИИ. При `center` каждая центрировалась
       сама по себе, и под коротким левым столбцом оставалась дыра, пока
       панель справа тянулась ниже. PO отметил её на кадре. */
    align-items: start;
  }
}

.arv-read { display: grid; gap: var(--sp-s); }
@media (min-width: 68rem) {
  /* Описание уходит ВНИЗ столбца, как отметил PO стрелкой на кадре. Столбец
     тянется на высоту ряда, которую задаёт панель, и строка прижимается
     `margin-top: auto`, а не отступом на глаз. */
  .arv-read { align-self: stretch; }
  .arv-lines { margin-top: auto; }
}
/* Потолок кеглем от ширины СВОЕЙ КОЛОНКИ, а не окна: колонка в трёхколонной
   сетке сжимается быстрее окна (654 на 1900, 438 на 1440, 371 на 1280), и
   потолок в vw всё равно давал вылет 11/74/84. Самое длинное слово
   «remembers.» это 5.95 кегля в ширину, отсюда 16.6cqw: заголовок физически
   не может стать шире колонки. На 1900 это 108px, на волос ниже --step-5. */
.arv-read { container-type: inline-size; }
.arv-read h1 { color: #fff; font-size: min(var(--hero), 16.6cqw); }
.arv-read .lede { color: rgb(255 255 255 / 0.74); max-width: 34ch; }

/* Строки смысла: стоят на одном месте и сменяются по такту. */
/* ⚠️ Место под строку РЕЗЕРВИРУЕТСЯ. Строки абсолютные, и на 5.4rem
   двухстрочная реплика вылезала на лид: в кадре они наезжали друг на друга. */
.arv-lines { position: relative; min-height: 4.6em; margin-top: var(--sp-m); }
.arv-line {
  position: absolute; inset: 0 auto auto 0; max-width: 34ch;
  color: rgb(255 255 255 / 0.6);
  --in: clamp(0, calc((var(--tp) + 0.7 - var(--i) * 1.25) * 3), 1);
  --out: clamp(0, calc(((var(--i) + 1) * 1.25 - 0.4 - var(--tp)) * 3), 1);
  opacity: calc(var(--in) * var(--out));
}
.arv-line b { color: #fff; font-weight: 400; }

/* --- Панель записи. Предмет с рамкой и шапкой. ------------------------- */

.arv-panel {
  background: rgb(0 1 8 / 0.62);
  backdrop-filter: blur(14px);
  border: 1px solid rgb(255 255 255 / 0.14);
  border-radius: clamp(14px, 1.4vw, 20px);
  overflow: hidden;
  box-shadow: var(--lift-2, 0 19px 42px -16px rgb(0 2 24 / 0.32));
}
.arv-bar {
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.8rem var(--sp-m);
  border-bottom: 1px solid rgb(255 255 255 / 0.12);
}
.arv-dot {
  width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: #3ddc97; flex: 0 0 auto;
}
.arv-name { color: #fff; font-size: var(--step-0); }
.arv-tag {
  margin-left: auto;
  padding: 0.15rem 0.45rem;
  border: 1px solid rgb(255 255 255 / 0.2); border-radius: 3px;
  font-size: var(--step-label); letter-spacing: 0.14em; text-transform: uppercase;
  color: rgb(255 255 255 / 0.55);
}
/* ⚠️ Высота НЕ фиксируется. Жёсткие 17rem держали пустую панель, пока строки
   только начинали копиться: в кадре это читалось дырой, а не ожиданием. */
.arv-feed {
  display: grid; gap: var(--sp-s); padding: var(--sp-m); align-content: start;
  /* Панель занимает место, отмеченное PO на кадре: она предмет экрана, а не
     карточка сбоку. Пустоты внизу нет, потому что лента копится. */
  min-height: clamp(18rem, 46vh, 30rem);
}
.arv-where { color: rgb(255 255 255 / 0.5); }

/* Строки записи КОПЯТСЯ по мере подъезда, а не сменяют друг друга. */
.arv-item {
  display: grid; gap: 0.15rem;
  --in: clamp(0, calc((var(--tp) - var(--from, 0)) * 4), 1);
  opacity: var(--in);
  transform: translateY(calc((1 - var(--in)) * 0.7rem));
}
.arv-item__v { color: #fff; }
.arv-item__by { color: rgb(255 255 255 / 0.5); }

/* --- Чипы: своя колонка справа, как на `/ai-seo/`. --------------------- */

.arv-pick { display: grid; gap: 0.5rem; align-content: start; }
@media (min-width: 68rem) {
  /* Чипы встают ПОД текстом, в дыру, которую PO отметил. Управление и то, чем
     управляют, остаются в одном взгляде: панель рядом, слева от них. */
  .arv-lay { grid-template-areas: "read panel pick"; }
  .arv-read { grid-area: read; }
  .arv-panel { grid-area: panel; }
  .arv-pick { grid-area: pick; align-self: center; justify-self: end; width: 100%; }
}
.arv-pick__key { margin: 0 0 0.2rem; color: rgb(255 255 255 / 0.72); }
/* ⚠️ У чипов СВОЙ грунт, а не полупрозрачная плёнка. Они стоят над листвой,
   и на светлых пятнах кадра прозрачные пилюли переставали читаться. */
.arv-chip {
  padding: 0.45rem 0.9rem;
  background: rgb(0 1 8 / 0.62); backdrop-filter: blur(10px); color: #fff;
  border: 1px solid rgb(255 255 255 / 0.22); border-radius: 999px;
  font: inherit; font-size: var(--step-label); letter-spacing: 0.1em;
  text-transform: uppercase; cursor: pointer; text-align: left;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.arv-chip:hover { background: rgb(255 255 255 / 0.16); }
.arv-chip[aria-pressed="true"] { background: var(--blue); border-color: var(--blue); }
.arv-chip:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.arv-own input {
  width: 100%; padding: 0.45rem 0.9rem;
  background: rgb(0 1 8 / 0.62); backdrop-filter: blur(10px); color: #fff;
  border: 1px solid rgb(255 255 255 / 0.22); border-radius: 999px;
  font: inherit; font-size: var(--step-label); letter-spacing: 0.06em;
}
.arv-own input::placeholder { color: rgb(255 255 255 / 0.45); }
.arv-own input:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* Показывается только выбранный адрес. Состояний два: записанный адрес и
   чужой. Третье, `grand`, снято вместе со вторым адресом 07.09. */
.arv-name, .arv-where, .arv-item { display: none; }
.arv-sect[data-addr="willow"] [data-for="willow"],
.arv-sect[data-addr="none"] [data-for="none"] { display: grid; }
.arv-sect[data-addr="willow"] span[data-for="willow"],
.arv-sect[data-addr="none"] span[data-for="none"],
.arv-sect[data-addr="willow"] p[data-for="willow"],
.arv-sect[data-addr="none"] p[data-for="none"] { display: block; }

/* .sr-only не объявляется: она уже есть в main.css. */

/* --- Переход во второй экран листом --------------------------------------

   ВЗЯТО с `/ai-seo/` (`.ai-sheet`), не написано. Одно число `--hand` на всю
   передачу: сцена гаснет до 0 при 0.52, лист проявляется с 0.5 и приподнят на
   1.1rem, пока не пришёл. Лист тёмный (`--ink`), как `.ai-sheet--ink`: видео
   уходит в чернила, а не в белое.

   Пороги: строки героя кончаются на такте 1.6 (данные `addrs`), передача
   начинается на 2.35, как на `/ai-seo/`. */
.arv-pin { --hand: clamp(0, calc((var(--tp) - 2.35) * 1.9), 1); }

/* ⚠️ `--done-at` ОБЯЗАН равняться такту, где `.arv-lay` доходит до нуля:
   2.35 + 0.52 / 1.9 = 2.624. Рельса в `main.js` читает это число и ставит
   `[data-done]`, иначе невидимая раскладка под листом ловит клики и Tab. */
.arv-pin { --done-at: 2.624; }
.arv-pin[data-done="true"] .arv-lay { pointer-events: none; visibility: hidden; }
.arv-lay { opacity: clamp(0, calc((0.52 - var(--hand, 0)) * 2.4), 1); }
/* Чипы и вид гаснут вместе с раскладкой: они её часть. */
.arv-view { opacity: clamp(0, calc((0.52 - var(--hand, 0)) * 2.4), 1); }

.arv-sheet {
  position: absolute; inset: 0; z-index: 2;
  display: grid; align-content: center;
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m));
  padding-bottom: var(--sp-m);
  padding-inline: max(1.25rem, 5vw);
  overflow: clip;
  background: var(--ink);
  color: var(--on-dark);
  --heading-color: var(--on-dark);
  --lede-color: var(--on-dark-muted);
  opacity: clamp(0, calc((var(--hand, 0) - 0.5) * 2.4), 1);
  transform: translateY(calc(clamp(0, calc(1.2 - var(--hand, 0) * 1.6), 1) * 1.1rem));
  pointer-events: none;
}
.arv-pin[data-done="true"] .arv-sheet { pointer-events: auto; }
.arv-sheet .wrap { padding-inline: 0; }
.arv-sheet .sect__head { margin-bottom: var(--sp-l); }
html:not(.js) .arv-sheet { position: static; opacity: 1; transform: none; }
html:not(.js) .arv-lay, html:not(.js) .arv-view { opacity: 1; }
/* ⚠️ Здесь стояло распинивание листа и `opacity: 1` у вида: видео переставало
   перематываться прокруткой, подъезд к дому исчезал, и от первого экрана
   оставалась картинка с текстом. Секция несёт `data-rail-phone`, пин липкий,
   плёнка снова идёт по такту.

   Колонок на телефоне и так одна: трёхколонная раскладка живёт с 68rem. Всё,
   что нужно, - свести роли по высоте и дать листу влезть в экран. */
@media (width < 60rem) {
  .arv-pin {
    padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.6svh));
    padding-bottom: min(var(--sp-s), 1.6svh);
  }
  .arv-lay { gap: min(var(--sp-m), 2.2svh); }
  .arv-read h1 { font-size: min(var(--step-4), 7.4vw); line-height: 1.04; }
  .arv-read .lede { max-width: none; font-size: min(var(--step-0), 3.6vw); }
  .arv-lines { min-height: 3.8em; margin-top: min(var(--sp-s), 1.4svh); }
  .arv-line { font-size: min(var(--step-0), 2.0svh); max-width: none; }
  .arv-feed { min-height: 0; padding: min(var(--sp-s), 1.6svh); gap: min(var(--sp-s), 1.4svh); }
  /* Чипы одной строкой с прокруткой вбок, как на `/ai-seo/`: в перенос они
     съедали высоту у предмета экрана. */
  .arv-pick { grid-auto-flow: column; grid-auto-columns: max-content; overflow-x: auto; scrollbar-width: none; }
  .arv-pick::-webkit-scrollbar { display: none; }
  .arv-pick__key { grid-row: 1; align-self: center; white-space: nowrap; }
  .arv-chip { white-space: nowrap; }
  .arv-sheet .wrap { padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.4svh)); }
  /* Замерено: четвёртая ячейка обрезалась на 38 точек, то есть подпись
     «the work, not the words» читателю телефона недоступна вовсе. */
  .arv-sheet .sect__head { margin-bottom: min(var(--sp-m), 2.2svh); }
  .arv-sheet .sect__head h2 { font-size: min(var(--step-3), 6.0vw); line-height: 1.04; }
  .arv-sheet .sect__head .lede { font-size: min(var(--step-0), 3.3vw); }
  .arv-sheet .cov { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.3rem; }
  .arv-sheet .cov__cell { padding: 0.4rem 0.5rem; gap: 0.3rem; }
  .arv-sheet .cov__foot b { font-size: min(var(--step--1), 3.1vw); }
  .arv-sheet .cov__voice { font-size: min(var(--step--1), 3.3vw); line-height: 1.15; }
  .arv-sheet .cov__text { font-size: min(var(--step--1), 2.9vw); }
  .arv-sheet .cov__foot { margin-top: 0.4rem; padding-top: 0.35rem; }
  .arv-sheet .cov__mark, .arv-sheet .cov__foot { font-size: 0.52rem; }
}

/* --- Чек в свободной секции ---------------------------------------------

   Отбивки `.bp-bill` на `/brand-platform/` сжаты по высоте окна, потому что
   там чек стоит в припиненном листе и обязан влезть в экран. Здесь секция
   обычная, и те же прямые дают 6 строк слипшейся мелочью: замерено кадром.
   Отбивки возвращены к прежним постоянным значениям того же компонента. */
.arv-bill .bp-bill__rows { margin-top: 0.9rem; }
.arv-bill .bpr { padding: 0.42rem 0; }
.arv-bill .bp-bill__foot { margin-top: 0.9rem; padding-top: 0.7rem; }
.arv-bill .bp-bill__adds { margin-top: 0.6rem; gap: 0.3rem; }

/* --- Два экрана помощника ------------------------------------------------

   Пара «стол и телефон» взята у рельсы `/un-saas/` вместе с классами
   `.rail__pair / .rail__desk / .rail__phone`: те же тени, тот же сдвиг
   телефона к правому краю стола. У рельсы высота приходит от пина, здесь
   секция обычная, поэтому высота объявлена явно, и это ЕДИНСТВЕННОЕ своё
   правило. Телефон стоит на `--p: 0`, то есть без хода. */
.arv-shots {
  margin: 0 0 var(--sp-xl);
  height: clamp(22rem, 34vw, 40rem);
  display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: var(--sp-m);
}
.arv-shots .rail__pair { min-height: 0; }
.arv-shots__cap {
  display: grid; gap: 0.3rem; justify-items: center; text-align: center;
  font-size: var(--step--1); color: var(--on-dark-muted);
}
.arv-shots__cap .mono { font-size: var(--step-label); letter-spacing: 0.14em; color: var(--blue-lo); }

/* Без скрипта такта нет: страница обязана читаться как обычная. */
html:not(.js) .arv-sect { height: auto; }
html:not(.js) .arv-pin { position: static; height: auto; overflow: visible; }
html:not(.js) .arv-item, html:not(.js) .arv-line { display: grid; opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .arv-item, .arv-line { transform: none; }
}

/* ===== Страница услуги Web-site Migration, `/migration/` ===================

   Переехало из bench.css целиком 02.09, в день, когда страница стала
   прод-страницей, ровно как обещал прежний комментарий стенда. Имена
   `.mig-*` и `.admin__*` уникальны, конфликтовать не с чем (гейт `collide`
   на это смотрит). Ниже прежняя шапка стенда без изменений. */
/* =========================================================================
   /migration/ : первый экран, пятая композиция.

   Правки PO по третьей: «текст должен быть так же, как на digital inventory:
   там правильные размеры, композиция, иерархия» и «переход плохо выглядит,
   прямо миграция не вау».

   1. ТИПОГРАФИКА СНЯТА ЗАМЕРОМ с живой `/digital-inventory/` на 1900, а не
      подобрана на глаз. Эталон в `paper.css`:

        .paper__kicker  9.6px  PlexMono  .12em  #6b6f7d   margin-bottom 1rem
        .paper__head   70.4px  Asgard 500  lh .98  ls -.01em  #fcfcfc
        .paper__line   18.1px  PlexMono  lh 1.6  #8c8f9a  max-width 26rem
        .paper__run    18.1px  PlexMono 600  margin-top 1rem
        .paper__hint   18.1px  PlexMono  #8c8f9a  margin-top 1.5rem

      У меня заголовок был 33.6px, то есть ВДВОЕ мельче, тело шло сансовым
      лидом, а цифра стояла отдельным гигантским числом. Иерархия была своя,
      а не сайта.

      Красный берётся токеном `--red`: у системы он значит уходящие деньги и
      декоративно запрещён. `/digital-inventory/` печатает свой оттенок мимо
      токенов, и повторять это я не стал.

   2. ПЕРЕХОД. Карточка больше не переворачивается целиком: поверхность
      перебирается СТРОКА ЗА СТРОКОЙ, волной сверху вниз. Приём снят с живого
      `vestaboard.com` (замер 24.08: фон #171818, 2 краски, 39 слов, предмет
      занимает экран целиком). Там перещёлк это то, что предмет ДЕЛАЕТ.

      Почему не переворот: переворот говорит «другая сторона», а продукт
      делает не это. Он забирает контент и пересобирает его на новом
      фундаменте. Волна по строкам и есть пересборка, а адресная строка стоит
      НАД поверхностью и в волне не участвует.

   ⚠️ `overflow: hidden` на предке с `preserve-3d` расплющивает сцену. Поэтому
   `preserve-3d` живёт на СТРОКЕ, а `.mig-flap` держит только `perspective` и
   обрезку: клип соседа по дереву 3D строк не убивает.

   ⚠️ Габарит наклонённого элемента `getBoundingClientRect()` отдаёт по
   ПРОЕКЦИИ. Ширину карточки мерить `offsetWidth`.

   Такты (--n: 7):
     0   .. 1     вопрос, карточки ещё нет
     1   .. 1.6   карточка приезжает справа
     1.6 .. 2.8   старый сайт едет вниз
     2.8 .. 3.9   волна перещёлка сверху вниз
     3.9 .. 5.5   новый сайт едет вниз
     5.5 .. 6.4   держится, грунт горит синим
     6.4 .. 7     лист с картой адресов ПОЯВЛЯЕТСЯ поверх сцены
   ========================================================================= */

.mig-sect {
  height: calc(var(--n, 6) * 62svh);
  /* Тёмный полноэкранный верх подтягивается под шапку, иначе она встаёт
     сплошной и кладёт светлую полосу поверх чёрного. Ловушка в CLAUDE.md. */
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}

.mig-pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  /* 30rem под колонку: заголовок в 70.4px должен ложиться в четыре или пять
     строк, как на `/digital-inventory/`, а строка текста имеет свой потолок
     в 26rem и колонку не распирает. */
  grid-template-columns: minmax(0, 30rem) minmax(0, 1fr);
  /* Строка обязана иметь ОПРЕДЕЛЁННУЮ высоту, иначе height:100% у сцены
     зацикливается на авторазмерной строке. */
  grid-template-rows: minmax(0, 1fr);
  gap: var(--sp-xl);
  align-items: stretch;
  overflow: hidden;
  background: var(--ink);
  color: var(--on-dark);
  --tp: min(calc(var(--p, 0) * var(--n, 6)), calc(var(--n, 6) - 0.02));
  /* Одно число на всю передачу. Непрерывно, а не переключателем: переход
     держится там, где читатель остановил колесо. */
  --hand: clamp(0, calc((var(--tp) - 5.4) * 1.7), 1);
  /* Одно число на всю волну. */
  --wave: clamp(0, calc((var(--tp) - 2.8) * 0.91), 1);
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-l));
  padding-bottom: var(--sp-l);
  padding-inline: max(1.25rem, 5vw);
}

/* Комната, в которой стоит предмет. Синий тёмнофоновый, третьего у сайта нет. */
.mig-pin::before {
  content: "";
  position: absolute;
  inset: -20%;
  pointer-events: none;
  background:
    radial-gradient(48rem 44rem at 66% 44%, rgb(74 108 255 / 0.11), transparent 68%),
    radial-gradient(34rem 34rem at 8% 70%, rgb(252 252 252 / 0.03), transparent 72%);
}
/* Второй источник света приходит С ВОЛНОЙ. Порядок в дереве: ::after идёт
   последним и по умолчанию лёг бы ПОВЕРХ карточки, залив снимки сайта
   синим. Поэтому у него z-index 0, а у содержимого 1. */
.mig-pin::after {
  content: "";
  position: absolute;
  inset: -20%;
  z-index: 0;
  pointer-events: none;
  opacity: var(--wave, 0);
  /* Плоская вуаль и второй источник в ОДНОМ слое: альфа со своим calc внутри
     rgb() браузером отбрасывается целиком, поэтому значения статичные, а
     меняется только прозрачность слоя. */
  background:
    radial-gradient(58rem 50rem at 62% 46%, rgb(74 108 255 / 0.24), transparent 70%),
    linear-gradient(rgb(74 108 255 / 0.11), rgb(74 108 255 / 0.11));
}
.mig-pin > * { position: relative; z-index: 1; }

/* --- Смыслы. Четыре элемента на состояние, размеры с /digital-inventory/ -- */

.mig-say {
  grid-row: 1;
  grid-column: 1;
  display: grid;
  align-self: center;
  min-height: 0;
}
.mig-say__b {
  grid-area: 1 / 1;
  align-content: center;
  display: block;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
/* Состояния сменяют друг друга встык, без перекрытия: две копии текста, одна
   поверх другой, читаются как брак печати. */
/* Вопрос во весь экран, через обе колонки: это отдельный такт, а не одно из
   состояний левой колонки. Иерархия та же, что у остальных, размер шире. */
.mig-ask {
  grid-row: 1;
  grid-column: 1 / -1;
  align-self: center;
  justify-self: center;
  max-width: 66rem;
  text-align: center;
  opacity: clamp(0, calc(1.9 - var(--tp) * 2.1), 1);
  transition: opacity 0.3s ease;
  pointer-events: none;
}
.mig-ask__head {
  font-family: Asgard, Impact, sans-serif;
  font-weight: 500;
  font-size: var(--hero);
  line-height: 0.96;
  letter-spacing: -0.01em;
  color: var(--white);
  margin: 0 0 1.25rem;
  text-wrap: balance;
}
/* НЕ роль «описание» и потому НЕ `.lede`: это моношрифтовая строка машины
   внутри тёмной сцены, тот же голос, что у подписей на карточках. Роль
   отдельная, кегль у неё один на оба своих места, см. `.mig-say__line`. */
.mig-ask__line {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-0);
  line-height: 1.6;
  color: var(--on-dark-muted);
  max-width: 34rem;
  margin: 0 auto;
}
.mig-ask .mig-say__hint { margin-top: 1.5rem; }
.mig-say__b[data-when="old"] {
  opacity: clamp(0, min(calc((var(--tp) - 1.05) * 2.5), calc((2.9 - var(--tp)) * 3.3)), 1);
}
.mig-say__b[data-when="cut"] {
  opacity: clamp(0, min(calc((var(--tp) - 2.9) * 4), calc((3.95 - var(--tp)) * 4)), 1);
}
.mig-say__b[data-when="new"] { opacity: clamp(0, calc((var(--tp) - 3.95) * 3.3), 1); }

/* Марки платформ. Читатель узнаёт СЕБЯ по знаку быстрее, чем по строке
   текста, и это ровно смысл номер один: платишь ты, неважно кому.
   Инлайнятся на сборке фильтром brandPath из simple-icons, поэтому
   страница ничего не грузит со стороны и ни одна марка не пропадает. */
.mig-logos {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-m);
}
.mig-logos li {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--on-dark-muted);
}
.mig-logos svg { width: 1.35rem; height: 1.35rem; flex: 0 0 auto; }
.mig-logos span {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.mig-logos[data-side="own"] li { color: var(--blue-lo); }

.mig-say__kick {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #6b6f7d;
  margin: 0 0 1rem;
}
.mig-say__head {
  font-family: Asgard, Impact, sans-serif;
  font-weight: 500;
  font-size: var(--step-4);
  line-height: 0.98;
  letter-spacing: -0.01em;
  color: var(--white);
  margin: 0 0 1rem;
}
.mig-say__line {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-0);
  line-height: 1.6;
  color: var(--on-dark-muted);
  max-width: 26rem;
  margin: 0;
}
/* Цифра. Цвет несёт смысл: красный это уходящие деньги и декоративно он
   запрещён; синий это своё и оплаченное. Третьего у сайта нет. */
.mig-say__run {
  font-family: PlexMono, ui-monospace, monospace;
  font-weight: 600;
  font-size: var(--step-0);
  margin: 1rem 0 0;
  min-height: 1.2em;
}
.mig-say__run[data-side="rent"] { color: var(--red); }
.mig-say__run[data-side="own"] { color: var(--blue-lo); }
.mig-say__hint {
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-0);
  color: var(--on-dark-muted);
  margin: 1.5rem 0 0;
}

/* --- Сцена --------------------------------------------------------------- */


/* Сцена уходит ДО того, как приходит лист, а не вместе с ним: линейный
   кросс-фейд оставлял на середине оба экрана читаемыми разом. */
.mig-say, .mig-stage { opacity: clamp(0, calc((0.52 - var(--hand, 0)) * 2.4), 1); }

.mig-stage {
  grid-row: 1;
  grid-column: 2;
  position: relative;
  display: grid;
  place-items: center;
  min-height: 0;
  height: 100%;
  perspective: 2000px;
  perspective-origin: 30% 50%;
}

.mig-card {
  width: min(100%, 50rem);
  height: min(100%, 34rem);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  border-radius: clamp(14px, 1.4vw, 20px);
  overflow: hidden;
  background: var(--panel-dark);
  border: 1px solid var(--line-dark);
  box-shadow: var(--lift-3, var(--lift-2));
  /* В такте вопроса карточки нет: первый экран это заголовок и описание
     во весь экран, по требованию PO. */
  --in: clamp(0, calc((var(--tp) - 1) * 1.7), 1);
  opacity: var(--in);
  transform:
    translateX(calc((1 - var(--in)) * 15vw))
    rotateY(-15deg)
    rotateX(2deg);
  transition: opacity 0.3s ease;
}

/* Адресная строка стоит НАД поверхностью и в волне не участвует. Это
   единственное, что не меняется, и в этом весь кадр. */
.mig-card__bar {
  display: flex;
  align-items: center;
  gap: var(--sp-s);
  padding: 0.62rem var(--sp-m);
  border-bottom: 1px solid var(--line-dark);
  background: rgb(252 252 252 / 0.05);
}
.mig-card__dots { display: inline-flex; gap: 0.34rem; }
.mig-card__dots i {
  width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--on-dark-muted); opacity: 0.4;
}
.mig-card__addr { flex: 1 1 auto; color: var(--white); font-size: var(--step--1); }
.mig-card__hold {
  flex: 0 0 auto;
  font-size: var(--step-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blue-lo);
  /* Различаем формой, а не тоном: у сайта два синих и третьего нет. */
  border: 1px dashed currentColor;
  border-radius: var(--radius);
  padding: 0.2rem 0.44rem;
}

/* --- Поверхность, которая пересобирается -------------------------------- */

.mig-flap {
  position: relative;
  min-height: 0;
  overflow: hidden;
  background: var(--ink);
  display: grid;
  grid-template-rows: repeat(var(--rows, 12), 1fr);
  /* Перспектива нужна ЗДЕСЬ: у сцены она применяется к карточке, а не к её
     внукам, и без своей строки перещёлкивались бы плоско. */
  perspective: 900px;
  perspective-origin: 50% 50%;
}

.mig-flap__row {
  position: relative;
  transform-style: preserve-3d;
  /* Своя доля волны. Ряд i начинает позже предыдущего и успевает провернуться
     до конца общей волны: получается бегущая сверху вниз строка, а не
     одновременный переворот всего. */
  --t: clamp(0, calc((var(--wave) - var(--i) * 0.045) * 2.4), 1);
  transform: rotateX(calc(var(--t) * -180deg));
}

.mig-flap__s {
  position: absolute;
  inset: 0;
  overflow: hidden;
  backface-visibility: hidden;
}
.mig-flap__s[data-when="new"] { transform: rotateX(180deg); }

/* Картинка каждой полосы это ОДИН И ТОТ ЖЕ кадр страницы, сдвинутый на свою
   полосу. Высота в `--rows` раз больше полосы, то есть ровно во всю
   поверхность, и `object-fit: cover` даёт тот же кроп, что был бы у цельного
   кадра. Поэтому полосы складываются в непрерывную страницу. */
.mig-flap__s img {
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(var(--rows, 12) * 100%);
  top: calc(var(--i) * -100%);
  object-fit: cover;
  object-position: 50% calc(var(--yo, 0) * 1%);
}
.mig-flap__s[data-when="old"] img { --yo: clamp(0, calc((var(--tp) - 1.6) * 83), 100); }
/* Едет дольше: страница длиннее, 6334 против 5054 при той же ширине. */
.mig-flap__s[data-when="new"] img { --yo: clamp(0, calc((var(--tp) - 3.9) * 62), 100); }

/* Торец полосы на просвет: без него ряд на девяноста градусах пропадает и
   в волне появляются дыры. */
.mig-flap__row::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: rotateX(90deg);
  background: linear-gradient(to right, rgb(74 108 255 / 0.15), rgb(74 108 255 / 0.7), rgb(74 108 255 / 0.15));
  opacity: calc(1 - max(var(--t) - 0.5, 0.5 - var(--t)) * 2);
  pointer-events: none;
}

/* Цельные кадры поверх строк.

   Двенадцать строк с поворотом оставляли в покое швы в пиксель, и на
   светлом новом сайте они читались линейкой поперёк дизайна. Чинится не
   подгонкой долей пикселя, а сменой предмета: пока волна не началась и
   после того, как отыграла, читатель смотрит на цельную картинку. */
.mig-solid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% calc(var(--yo, 0) * 1%);
  pointer-events: none;
}
.mig-solid[data-when="old"] {
  --yo: clamp(0, calc((var(--tp) - 1.6) * 83), 100);
  opacity: clamp(0, calc((0.03 - var(--wave)) * 40), 1);
}
.mig-solid[data-when="new"] {
  --yo: clamp(0, calc((var(--tp) - 3.9) * 62), 100);
  opacity: clamp(0, calc((var(--wave) - 0.97) * 40), 1);
}

/* Версия поверхности БЕЗ волны. Показывается в двух случаях: узкий экран и
   отказ JS. Поэтому размеры живут здесь, а не в медиазапросе: без них на
   широком без скриптов две картинки вставали в натуральную высоту, замерено
   11 447 px. */
.mig-plain { display: none; background: var(--ink); }
.mig-plain__tag {
  padding: 0.4rem var(--sp-m);
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--on-dark-muted);
  background: rgb(252 252 252 / 0.04);
}
.mig-plain__img {
  display: block;
  width: 100%;
  height: 46svh;
  object-fit: cover;
  object-position: 50% 0%;
}


/* --- Лист, который ПОЯВЛЯЕТСЯ -----------------------------------------

   Не следующая секция, а второй лист в том же припиненном кадре. Кадр
   целиком меняет грунт с тёмного на светлый: тёмное это аренда, светлое
   это своё. Токены сбрасываются вместе с фоном, иначе заголовок остался бы
   белым по белому.

   Начинается НИЖЕ шапки: секция несёт `data-dark-top`, шапка прозрачная и
   логотип белый. */
.mig-sheet {
  position: absolute;
  /* Во весь пин, до самого верха. Раньше лист начинался ниже шапки, и в
     зазоре оставалась видна ТЁМНАЯ СЦЕНА: поперёк светлого блока лежала
     тёмная полоса, которая читалась чужой плитой. Теперь плиту несёт сама
     шапка (правило ниже), а зазора нет. */
  inset: 0;
  z-index: 2;
  display: grid;
  /* `safe` обязателен: при содержимом выше контейнера обычный center уводит
     верх в отрицательную область, и до него не докрутиться. */
  /* Строка ОБЯЗАНА иметь потолок: при авторазмерной она вырастает под
     содержимое, и `height: 100%` внутри опирается на пустоту. Замерено:
     строка 1212 при высоте листа 731. */
  grid-template-rows: minmax(0, 1fr);
  align-content: stretch;
  /* `clip` вместо `auto`: содержимое влезает (замерено), а `auto` резал
     тень панели по краю листа. Полоса прокрутки внутри листа тут не нужна
     и была бы признаком, что раскладка не сошлась. */
  overflow: clip;
  background: var(--white);
  color: var(--text);
  --heading-color: var(--text);
  --lede-color: var(--text-muted);
  opacity: clamp(0, calc((var(--hand, 0) - 0.5) * 2.4), 1);
  transform: translateY(calc(clamp(0, calc(1.2 - var(--hand, 0) * 1.6), 1) * 1.1rem));
  /* Лист сам указатель не ловит, чтобы не накрывать сцену под ним, пока он
     невидим. Но панель внутри КЛИКАБЕЛЬНАЯ, и наследование  гасило её
     целиком: под курсором на месте вкладки оказывался текст сцены снизу.
     Поэтому `pointer-events: auto` возвращается на самой панели.

     Плата: пока лист невидим, коробка панели всё равно ловит указатель над
     сценой. Сцена не несёт ничего интерактивного, только снимки и текст,
     поэтому цена нулевая. */
  pointer-events: none;
}
/* Высоту панели отдаёт СЕТКА, а не процент. При `max-height: 56svh` панель
   выходила 717x448, то есть 0.56 от нарисованного, и рельса в 15px
   становилась восемью. Дом это уже записал: фиксированная высота кадра не
   знает, сколько занял текст над ней. */
/* Отступ от шапки берёт СОДЕРЖИМОЕ, а не коробка листа: лист обязан быть
   белым до самого верха, иначе в зазоре видна тёмная сцена и она читается
   чужой плитой поперёк светлого блока.

   Строки по содержимому, а не `1fr`: растянутая панель оставляла под
   таблицей пустое белое поле в половину экрана. Высоту панели задаёт её
   собственное поле (`.admin__pane`), пара центрируется в остатке. */
.mig-sheet .wrap {
  display: grid;
  grid-template-rows: auto auto;
  gap: var(--sp-m);
  height: 100%;
  align-content: center;
  padding-top: calc(var(--header-h) + var(--sp-m));
  padding-bottom: var(--sp-m);
}
/* Заголовок слева, описание справа у правого края, обе стопки на нижнем
   крае. Так пустое поле сверху и снизу уходит в сам блок.

   Колонка заголовка `auto`, а перелом задаёт `max-width` В ЕДИНИЦАХ ch НА САМОМ
   ЗАГОЛОВКЕ: `ch` в `grid-template-columns` считался бы от кегля
   контейнера (18px), а не от кегля заголовка (70.4px), и колонка вышла бы
   вчетверо уже нужного. */
.mig-sheet__say {
  display: grid;
  grid-template-columns: auto minmax(0, 31rem);
  justify-content: space-between;
  align-items: end;
  gap: 0.5rem var(--sp-xl);
}
/* Кегль по РОЛИ, а не по месту: это заголовок блока, значит `--step-4`, как
   у каждого заголовка секции на сайте. Здесь он стоял на `--step-2`, потому
   что «не влезал», и это ровно то, что PO назвал 24.08. Влезать чинится
   раскладкой: таблица встала рядом с цифрами, а не под ними. */
.mig-sheet__h {
  font-family: Asgard, Impact, sans-serif;
  font-weight: 500;
  font-size: var(--step-4);
  line-height: 0.98;
  letter-spacing: -0.01em;
  /* Две строки: перелом приходится на запятую и не зависит от вьюпорта. */
  max-width: 11ch;
  color: var(--text);
}
/* Описание НЕ несёт своего кегля: оно берёт общесайтовый класс `.lede`
   (`--step-1`, 24px, London 300), которым описания идут на каждой
   странице. Здесь оно успело постоять на `--step--1` (моношрифт, мелкое)
   и на `--step-0`, то есть роль дважды получила чужой размер, потому что
   я подгонял его под высоту припиненного листа. Правило в CLAUDE.md
   заведено ровно про это.

   Класс остаётся только ради подъёма по `--hand` (правило ниже) и ради
   колонки: ширину задаёт say-сетка, поэтому 44ch из `.lede` не связывает.
   Цвет тоже брать не нужно, лист уже переопределяет `--lede-color`. */
.mig-sheet__line { margin: 0; }
.mig-sheet .admin { min-height: 0; }

/* --- Живая панель -------------------------------------------------------

   Язык снят ЗАМЕРОМ с `tools/screens.mjs`, который рисует дашборд Un-SaaS:
   тёмная рельса на `--panel-dark`, шаг 50 при высоте 46 и радиусе 10,
   акцент `--blue`, пара цифр дисплеем, таблица в четыре колонки, подвал.
   Компонент СВОЙ: `.screen` принадлежит главной, а `PAGE-CONCEPTS` не даёт
   ему появляться где-то ещё.

   Переключение без скрипта. Инпуты стоят перед рельсой и панелью, потому
   что `~` ходит только по соседям. */
.admin {
  pointer-events: auto;
  display: grid;
  /* Узкая рельса: тёмного вдвое меньше, но контраст тёмного против белого
     сохранён. Формула записана в `screen.njk`: два прошлых захода
     провалились именно на её отмене. */
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-height: 0;
  /* Тень по требованию PO. Не режется: панель отбита от краёв листа
     отступами `.wrap`, и `overflow: clip` на листе до неё не достаёт.
     Прошлый раз резало именно потому, что панель упиралась в край. */
  overflow: hidden;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: clamp(14px, 1.4vw, 20px);
  box-shadow: var(--lift-2);
}
/* Скрыты, но фокусируемы с клавиатуры: рельса обязана работать без мыши. */
.admin__pick { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }

.admin__bar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--sp-s);
  padding: 0.6rem var(--sp-m);
  border-bottom: 1px solid var(--line);
}
.admin__dots { display: inline-flex; gap: 0.3rem; }
.admin__dots i { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--text-muted); opacity: 0.35; }
.admin__addr { flex: 1 1 auto; font-size: var(--step--1); color: var(--text); }
.admin__you {
  font-size: var(--step-label); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--blue); border: 1px dashed currentColor; border-radius: var(--radius);
  padding: 0.16rem 0.44rem;
}

.admin__rail {
  background: var(--panel-dark);
  padding: 0.4rem 0.35rem;
  display: grid;
  align-content: start;
  gap: 0.15rem;
  min-height: 0;
  overflow: auto;
}
.admin__tab {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.42rem 0.5rem;
  border-radius: 10px;
  cursor: pointer;
  color: rgb(252 252 252 / 0.62);
  transition: background 0.18s ease, color 0.18s ease;
}
.admin__tab svg { width: 0.95rem; height: 0.95rem; flex: 0 0 auto; opacity: 0.7; }
.admin__tab b { font-weight: 400; font-size: var(--step-label); letter-spacing: 0.08em; text-transform: uppercase; }
.admin__tab:hover { color: #fff; }

/* Высота задана ЗДЕСЬ, потому что виды внутри позиционированы абсолютно и
   не держат коробку. Фиксированная высота заодно убирает прыжок раскладки
   при переключении вкладок: разделы разной длины, а окно одно. */
.admin__pane {
  position: relative;
  min-height: min(24rem, 44vh);
  overflow: hidden;
}
/* Две колонки, а не три полосы: цифры и описание слева, таблица во всю
   правую часть. Так разметил PO, и так блок перестаёт быть стопкой полос.
   Заодно освобождает высоту, которой не хватало заголовку в штатном кегле. */
.admin__view {
  position: absolute;
  inset: 0;
  padding: var(--sp-m);
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  /* Строки заданы ЯВНО: без этого цифры и описание, будучи двумя
     элементами одной колонки, расходились по неявным строкам и описание
     улетало в самый низ. */
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--sp-m) var(--sp-l);
  align-content: stretch;
  opacity: 0;
  pointer-events: none;
  transform: translateY(0.4rem);
  transition: opacity 0.22s ease, transform 0.22s ease;
}

/* Левая колонка: цифры и описание одной стопкой. */
.admin__view > .admin__figs { grid-column: 1; grid-row: 1; align-self: start; }
.admin__figs { display: grid; gap: var(--sp-m); }
.admin__figs p { display: grid; gap: 0.15rem; }
.admin__figs b { font-family: Asgard, Impact, sans-serif; font-weight: 500; font-size: var(--step-3); line-height: 1; }
/* Синий, а не зелёный: у системы --green значит «деньги идут, но их не
   видно», это конкретный смысл с /money-trail/, а не «успех вообще».
   Употребить его тут было бы декоративным использованием смыслового токена. */
.admin__figs p[data-tone="good"] b { color: var(--blue); }
.admin__figs span { font-size: var(--step-label); letter-spacing: 0.12em; color: var(--text-muted); }
/* ИСКЛЮЧЕНИЕ из правила «у роли один кегль», с причиной прямо здесь: это
   текст ВНУТРИ снимка чужого интерфейса, а не текст страницы. У панели своя
   шкала, снятая с генератора `tools/screens.mjs`, и страничный `--step-0`
   внутри окна приложения читался бы как надпись поверх скриншота, а не как
   его часть. Роль страницы, описание блока, при этом идёт `--step-0`: см.
   `.mig-sheet__line`, `.mig-ask__line`, `.mig-say__line`. */
.admin__line {
  grid-column: 1;
  grid-row: 2;
  align-self: start;
  font-size: var(--step--1);
  color: var(--text-muted);
}
/* Таблица занимает вторую колонку целиком, обе строки. */
.admin__table { grid-column: 2; grid-row: 1 / -1; }

.admin__table { min-height: 0; overflow: auto; align-self: start; }
.admin__head, .admin__row {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.1fr) minmax(0, 1.1fr) auto;
  gap: var(--sp-m);
  align-items: baseline;
  padding: 0.42rem 0.6rem;
}
.admin__head { font-size: var(--step-label); letter-spacing: 0.12em; color: var(--text-muted); }
.admin__row { font-size: var(--step--1); border-top: 1px solid var(--line); }
.admin__row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.admin__row.is-live { background: var(--blue); color: #fff; border-radius: 8px; border-top-color: transparent; }
.admin__row.is-live .admin__state { color: #fff; }
.admin__state { color: var(--text-muted); text-align: right; }
.admin__state.is-good { color: var(--blue); }

.admin__foot {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--sp-l);
  padding: 0.6rem var(--sp-m);
  border-top: 1px solid var(--line);
  font-size: var(--step--1);
  color: var(--text-muted);
}
.admin__go {
  margin-left: auto;
  color: var(--blue);
  background: rgb(1 48 249 / 0.08);
  border-radius: 8px;
  padding: 0.3rem 1.1rem;
}

/* Выбор. Одно правило на раздел, потому что `~` не умеет читать значение
   инпута: сопоставляем по id. */
#pan-pages:checked ~ .admin__rail .admin__tab[for="pan-pages"] { background: var(--blue); color: #fff; }
#pan-pages:checked ~ .admin__rail .admin__tab[for="pan-pages"] svg { opacity: 1; }
#pan-pages:checked ~ .admin__pane .admin__view[data-key="pages"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-services:checked ~ .admin__rail .admin__tab[for="pan-services"] { background: var(--blue); color: #fff; }
#pan-services:checked ~ .admin__rail .admin__tab[for="pan-services"] svg { opacity: 1; }
#pan-services:checked ~ .admin__pane .admin__view[data-key="services"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-reviews:checked ~ .admin__rail .admin__tab[for="pan-reviews"] { background: var(--blue); color: #fff; }
#pan-reviews:checked ~ .admin__rail .admin__tab[for="pan-reviews"] svg { opacity: 1; }
#pan-reviews:checked ~ .admin__pane .admin__view[data-key="reviews"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-blog:checked ~ .admin__rail .admin__tab[for="pan-blog"] { background: var(--blue); color: #fff; }
#pan-blog:checked ~ .admin__rail .admin__tab[for="pan-blog"] svg { opacity: 1; }
#pan-blog:checked ~ .admin__pane .admin__view[data-key="blog"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-photos:checked ~ .admin__rail .admin__tab[for="pan-photos"] { background: var(--blue); color: #fff; }
#pan-photos:checked ~ .admin__rail .admin__tab[for="pan-photos"] svg { opacity: 1; }
#pan-photos:checked ~ .admin__pane .admin__view[data-key="photos"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-contacts:checked ~ .admin__rail .admin__tab[for="pan-contacts"] { background: var(--blue); color: #fff; }
#pan-contacts:checked ~ .admin__rail .admin__tab[for="pan-contacts"] svg { opacity: 1; }
#pan-contacts:checked ~ .admin__pane .admin__view[data-key="contacts"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-leads:checked ~ .admin__rail .admin__tab[for="pan-leads"] { background: var(--blue); color: #fff; }
#pan-leads:checked ~ .admin__rail .admin__tab[for="pan-leads"] svg { opacity: 1; }
#pan-leads:checked ~ .admin__pane .admin__view[data-key="leads"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-redirects:checked ~ .admin__rail .admin__tab[for="pan-redirects"] { background: var(--blue); color: #fff; }
#pan-redirects:checked ~ .admin__rail .admin__tab[for="pan-redirects"] svg { opacity: 1; }
#pan-redirects:checked ~ .admin__pane .admin__view[data-key="redirects"] { opacity: 1; pointer-events: auto; transform: none; }
#pan-publish:checked ~ .admin__rail .admin__tab[for="pan-publish"] { background: var(--blue); color: #fff; }
#pan-publish:checked ~ .admin__rail .admin__tab[for="pan-publish"] svg { opacity: 1; }
#pan-publish:checked ~ .admin__pane .admin__view[data-key="publish"] { opacity: 1; pointer-events: auto; transform: none; }
/* Запас на случай, когда НЕ ВЫБРАНО НИЧЕГО.

   Браузер восстанавливает состояние формы после перезагрузки, и после
   горячей пересборки дев-сервера в панели не оказывалось ни одной выбранной
   кнопки: рельса без подсветки, поле пустое. Атрибут `checked` в разметке от
   этого не спасает, потому что восстановленное состояние его перебивает.

   Поэтому вид по умолчанию рисуется НЕ атрибутом, а правилом: нет выбранного
   ни одного, значит показывается первый раздел. Панель не может оказаться
   пустой ни при каком состоянии формы. */
.admin:not(:has(.admin__pick:checked)) .admin__view[data-key="pages"] {
  opacity: 1; pointer-events: auto; transform: none;
}
.admin:not(:has(.admin__pick:checked)) .admin__tab[for="pan-pages"] {
  background: var(--blue); color: #fff;
}
.admin:not(:has(.admin__pick:checked)) .admin__tab[for="pan-pages"] svg { opacity: 1; }

/* --- Шапка сайта над светлым листом ------------------------------------

   Канон в `main.css` решает этот вопрос прямо: «Логотип и кнопка остаются
   белыми ВСЕГДА. Перекрашивать их в чёрный над светлым это лечение
   симптома: под белым логотипом должна быть тёмная плита». Поэтому шапку
   не осветляем, а даём ей плиту, пока лист поднят. Так же поступает
   `/site/`, когда его светлый лист выезжает под ещё живой секцией.

   Значения повторены, а не взяты из `.header[data-solid="true"]`: у плиты
   шапки нет токена, а дописывать свой селектор в общий `main.css` ради
   стенда неправильно. Если значения там поменяются, поменять и здесь.

   Сигнал берётся у рельсы: `main.js` ставит `data-state="live"` на
   последний слайд, и это единственное, по чему CSS снаружи секции может
   узнать про поднятый лист. */
body:has(.mig-sheet[data-rail-slide][data-state="live"]) .header {
  background: rgb(0 1 8 / 0.92);
  backdrop-filter: blur(10px);
  border-bottom-color: var(--line-dark);
}

/* Видимая рамка фокуса: рельса обязана работать с клавиатуры. */
.admin__pick:focus-visible ~ .admin__rail .admin__tab { outline: 2px solid var(--blue-lo); outline-offset: -2px; }

/* Описание листа поднимается по `--hand`, а не по прокрутке: лист припинен,
   `r.top` у него постоянен, и `data-read` застыл бы наполовину. Ступенька
   по `--i`, чтобы строки приходили одна за другой, а не разом. */
/* Считаем от ТАКТА, а не от `--hand`.

   На `--hand` вся подача умещалась в 37 пикселей прокрутки: `--hand` растёт
   в 1.7 раза быстрее такта, и множитель 6 поверх него схлопывал появление в
   один кадр. Анимация формально была, увидеть её было нельзя. Замерено: PO
   трижды сказал, что анимации нет, и был прав.

   Такт даёт окно: заголовок выходит с 5.9 по 6.52, это около 240 пикселей
   прокрутки. Описание идёт НЕ отсюда, а от `data-read` на самом элементе, и
   его окно записано ТАМ, в разметке: 6.0 .. 6.8. Пин заканчивается на 6.98,
   значит успевают оба.

   Порядок такой: лист приходит (`--hand` 0.5 ... 1, такт 5.69 ... 5.99),
   и уже на готовый белый лист вписывается текст. */
/* Только заголовок: у описания своя подача, по словам (`data-read`). */
.mig-sheet__h {
  --in: clamp(0, calc((var(--tp, 0) - 5.9) * 1.6), 1);
  opacity: var(--in);
  transform: translateY(calc((1 - var(--in)) * 0.9rem));
}

@media (prefers-reduced-motion: reduce) {
  .admin__view, .admin__tab { transition: none; }
  .mig-sheet__h, .mig-sheet__line { transform: none; }
}

/* --- Экран админки ------------------------------------------------------

   Свой класс, а не `.scr` и не `.rail__desk`: первый принадлежит главной,
   второй рельсе `/un-saas/`. Заимствование класса на этом сайте уже дважды
   молча перекрашивало живые страницы.

   Радиус и тень взяты у карточек главной, а не назначены. */
/* Ни радиуса, ни box-shadow на обёртке: РИСУНОК САМ обрезает себя по своим
   скруглённым углам (rx=20 в SVG), и CSS-радиус кладёт вторую, другую форму
   поверх первой. Дом это уже проходил и записал у .rail__desk. Тень идёт
   drop-shadow на сам SVG, потому что она обязана следовать тому, что рисунок
   действительно красит.

   Ширина ограничена натуральной: при 1710 язык раздувался в 1.34 раза против
   нарисованного, и рельса в 15px становилась двадцаткой. */
.edit__shot {
  margin: var(--sp-l) auto 0;
  max-width: 80rem;
  /* Рисунок настольный: на 430 он ужимался до 387px, и рельса в 15px
     превращалась в четыре с половиной. Широкое содержимое обязано
     прокручиваться В СВОЁЙ коробке, а не растягивать страницу. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.edit__shot svg { min-width: 44rem; }
.edit__shot svg {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 1.4rem 2.6rem rgb(0 2 24 / 0.22));
}
.edit__note {
  margin: var(--sp-m) 0 0;
  font-size: var(--step--1);
  color: var(--text-muted);
  max-width: 46rem;
}

/* --- Отказ движка: кадр держится готовым, а не пустым -------------------- */

/* Без JS `--p` не ставится, значит `--tp` ноль, значит все три состояния
   левой колонки и лист с картой адресов имели прозрачность ноль: от экрана
   оставался вопрос и старый снимок. Требование канона обратное: страница
   читается ЦЕЛИКОМ. Поэтому кадр распинивается ровно так же, как на узком
   экране, и всё показывается подряд. */
html:not(.js) .mig-sect { height: auto; margin-top: 0; }
html:not(.js) .mig-pin {
  position: static;
  height: auto;
  overflow: visible;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto;
}
html:not(.js) .mig-card { opacity: 1; transform: none; width: 100%; height: auto; }
html:not(.js) .mig-say, html:not(.js) .mig-stage { opacity: 1; }
html:not(.js) .mig-sheet {
  position: static;
  inset: auto;
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
html:not(.js) .mig-flap { display: none; }
html:not(.js) .mig-plain { display: grid; }
html:not(.js) .mig-ask { opacity: 1; max-width: none; text-align: left; }
html:not(.js) .mig-say__b {
  grid-area: auto;
  opacity: 1;
  padding-block: var(--sp-m);
  border-top: 1px solid var(--line-dark);
}

@media (prefers-reduced-motion: reduce) {
  .mig-card, .mig-say__b { transition: none; }
  .mig-flap__row { transform: none; }
  .mig-flap__s[data-when="new"] { display: none; }
  /* Ехало не только перещёлкивание: страницы прокручивались внутри кадра,
     карточка заезжала на 15vw, лист поднимался. При включённом флаге всё
     это стоит на месте. */
  .mig-flap__s img, .mig-solid { object-position: 50% 0; }
  .mig-card { transform: rotateY(-15deg) rotateX(2deg); }
}

/* --- Узкий экран --------------------------------------------------------

   ⚠️ ЗДЕСЬ СТОЯЛО РАСПИНИВАНИЕ, И ЭТО БЫЛА ОШИБКА ТОГО ЖЕ РОДА, ЧТО НА
   ГЛАВНОЙ. Довод был: у сцены пропадает определённая высота, и всё, что
   считалось от её размера, получает ноль, поэтому волны на телефоне быть не
   может. Высота пропадала не сама - её отнимало распинивание. Секция несёт
   `data-rail-phone`, пин остаётся липким, и волна играет.

   PO: «все первые экраны иммерсивный опыт при скролле, а не просто скролл».

   Раскладка та же, что на большом экране, только колонка одна: смыслы сверху
   по содержимому, сцена во весь остаток. Тянущаяся строка РОВНО ОДНА, иначе
   карточка снова считает высоту от нуля. */
@media (width < 60rem) {
  /* Подтяжку под шапку НЕ снимаем. Секция несёт `data-dark-top`, то есть
     шапка прозрачная и логотип белый; при `margin-top: 0` она вставала на
     белый фон страницы, и получался белый логотип по белому. Это вторая из
     двух записанных в CLAUDE.md ситуаций тёмного верха. Компенсация
     возвращается паддингом, а не отменой подтяжки. */
  .mig-pin {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-m), 2.6svh));
    padding-bottom: min(var(--sp-m), 2.4svh);
    gap: min(var(--sp-m), 2.4svh);
    /* Карточка стоит по центру колонки, а не сбоку: смещённая точка схода
       разворачивала её к краю экрана, которого на телефоне нет. */
    perspective: 1200px;
    perspective-origin: 50% 50%;
  }
  /* Вопрос по-прежнему отдельный такт во весь кадр, только кегль течёт по
     ширине окна: 70.4px на 390 точках ломались по одному слову в строку. */
  .mig-ask { grid-row: 1 / -1; grid-column: 1; max-width: 34rem; }
  .mig-ask__head { font-size: min(var(--step-4), 7.6vw); line-height: 1.06; }
  .mig-ask__line { margin-inline: 0; max-width: none; font-size: min(var(--step-1), 4.1vw); }
  /* Смыслы держат ВЕРХ кадра и не растягиваются: тянется только сцена. */
  .mig-say { grid-row: 1; grid-column: 1; align-self: start; }
  .mig-say__head { font-size: min(var(--step-2), 4.8svh); margin-bottom: 0.5rem; }
  .mig-say__line { max-width: none; font-size: min(var(--step-0), 2.1svh); }
  .mig-say__run { font-size: var(--step-label); }
  .mig-logos { gap: 0.5rem; }
  .mig-logos svg { width: 1.1rem; height: 1.1rem; }
  .mig-stage { grid-row: 2; grid-column: 1; }
  .mig-card { width: 100%; height: 100%; }
  /* Лист передачи остаётся ЛИСТОМ, а не следующим блоком: в одну колонку и
     кеглем по экрану. */
  .mig-sheet__say { grid-template-columns: minmax(0, 1fr); }
  .mig-sheet__h { max-width: none; font-size: min(var(--step-3), 6.4vw); }
  .mig-sheet__line { font-size: min(var(--step-0), 4vw); }
}

/* ===== Страница «О компании», `/about/` ======================================

   Первый экран: сайт агентских лет едет вверх под словами по такту рельсы,
   строки сменяются, лист с разворотом Then / Now поднимается по `--hand`.
   Механика та же, что у `/knowledge-os/`; своих механизмов нет. */

.abt-sect {
  --n: 4;
  height: calc(var(--n, 4) * 92svh);
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}
.abt-pin {
  display: flex; flex-direction: column; justify-content: center;
  background: var(--ink);
  --tp: min(calc(var(--p, 0) * var(--n, 4)), calc(var(--n, 4) - 0.02));
  --arv-pad: clamp(2rem, 5vh, 4rem);
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--arv-pad));
  padding-bottom: var(--arv-pad);
  overflow: clip;
}

/* Предмет: слово во весь экран, работа внутри букв. Два слова одно поверх
   другого; «Then» несёт старый сайт фактурой (`background-clip: text`) и
   едет по такту, «Now» несёт фирменный свет. Кегль по ширине окна: слово
   обязано упираться в края, а не лежать в рамке. */
.abt-word {
  position: relative;
  /* Высота слова от ВЫСОТЫ окна, не только от ширины: на 1900x800 слово по
     ширине давало 448px и вместе с заголовком не влезало в экран, верх
     резала шапка, низ резал край. Замерено кадром. */
  height: min(28rem, 40svh);
  margin-inline: -0.02em;
}
.abt-word__k {
  position: absolute; inset: 0;
  font-family: Asgard, Impact, sans-serif; font-weight: 500;
  font-size: min(32rem, 34vw, 46svh); line-height: 0.86; letter-spacing: -0.05em;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  white-space: nowrap;
}
.abt-word__k--then {
  /* ⚠️ Внутри букв НИЧЕГО. Сначала там ехал старый сайт, PO: «не используй
     старый сайт». Прошлое плоское и приглушённое, свет только у «Now». */
  background-image: linear-gradient(rgb(255 255 255 / 0.22), rgb(255 255 255 / 0.22));
  /* ⚠️ Слова НЕ пересекаются во времени: «Then» погасло к 1.3, «Now»
     начинает с 1.45. Пересечение читалось как «странно переносятся слова»,
     PO. */
  opacity: clamp(0, calc((1.3 - var(--tp)) * 3), 1);
}
.abt-word__k--now {
  background-image: linear-gradient(87deg, var(--blue), #00ffff 51%, #74ff80);
  opacity: clamp(0, calc((var(--tp) - 1.32) * 3), 1);
}

.abt-lay {
  position: relative; z-index: 1;
  display: grid; gap: var(--sp-m); align-content: center;
  min-height: 0; flex: 1 1 auto;
}
/* Одна колонка: заголовок в две строки, строка такта под ним. Строки справа
   внизу читались оторванными от заголовка, PO: «композиционно не верно». */
.abt-read { display: grid; gap: var(--sp-s); max-width: 60rem; }
/* ⚠️ ИСКЛЮЧЕНИЕ ИЗ ПРАВИЛА «h1 = --hero», и оно записано.
   Герой этого экрана - слово Then -> Now кеглем 368px, оно и даёт странице
   контраст 15x. Заголовок под ним это подпись к слову. На --step-5 он
   выходил ШЕСТЬЮ строками, 898 точек при экране 800, и два гиганта в одном
   кадре спорили: одна ось выкручивается до предела, а не все сразу
   (REFERENCES, принцип 3). Замерено 05.09 на 1900x800. */
.abt-read h1 { max-width: 20ch; font-size: var(--step-4); }
.abt-read h1 { color: #fff; }
.abt-lines { position: relative; min-height: 3.6rem; margin-top: 0.2rem; }
.abt-line {
  position: absolute; inset: 0 auto auto 0; max-width: 48ch;
  color: rgb(255 255 255 / 0.6);
  /* Окна строк НЕ пересекаются: строка i гаснет к такту (i+1)*0.8-0.3, а
     следующая начинает с него же. Прежние окна давали наложение двух строк на
     0.3 такта, поймано кадром. Последняя держится до передачи листом. */
  --in: clamp(0, calc((var(--tp) + 0.3 - var(--i) * 0.8) / 0.15), 1);
  --out: clamp(0, calc(((var(--i) + 1) * 0.8 - 0.3 - var(--tp)) / 0.15), 1);
  opacity: calc(var(--in) * var(--out));
}
.abt-line b { color: #fff; font-weight: 400; }

/* Передача листом, числа как у `/ai-seo/`. */
.abt-pin { --hand: clamp(0, calc((var(--tp) - 2.35) * 1.9), 1); --done-at: 2.624; }
.abt-pin[data-done="true"] .abt-lay { pointer-events: none; visibility: hidden; }
.abt-lay { opacity: clamp(0, calc((0.52 - var(--hand, 0)) * 2.4), 1); }
.abt-sheet {
  position: absolute; inset: 0; z-index: 2;
  display: grid; align-content: center;
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m));
  padding-bottom: var(--sp-m);
  padding-inline: max(1.25rem, 5vw);
  overflow: clip;
  background: var(--ink);
  color: var(--on-dark);
  --heading-color: var(--on-dark);
  --lede-color: var(--on-dark-muted);
  opacity: clamp(0, calc((var(--hand, 0) - 0.5) * 2.4), 1);
  transform: translateY(calc(clamp(0, calc(1.2 - var(--hand, 0) * 1.6), 1) * 1.1rem));
  pointer-events: none;
}
.abt-pin[data-done="true"] .abt-sheet { pointer-events: auto; }
.abt-sheet .wrap { padding-inline: 0; }

/* Лист: предмет, а не полотно. Голова секции, строка вычеркнутых коробок,
   ряд дверей нынешней линейки. */
.abt-sheet__head { margin-bottom: var(--sp-l); }
.abt-sheet__head h2 { color: #fff; }

.abt-then {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.4rem;
  margin: 0 0 var(--sp-m);
}
.abt-then__k, .abt-now__k {
  font-size: var(--step-label); letter-spacing: 0.16em; text-transform: uppercase;
  color: rgb(255 255 255 / 0.5);
}
.abt-now__k { margin: 0 0 var(--sp-s); }
.abt-then__box {
  display: inline-flex; align-items: center; gap: 0.45rem;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-label); letter-spacing: 0.1em; text-transform: uppercase;
  color: rgb(255 255 255 / 0.38);
}
.abt-then__box svg { width: 1.1rem; height: 1.1rem; flex: 0 0 auto; }
.abt-then__box s { text-decoration-thickness: 1px; text-decoration-color: rgb(255 255 255 / 0.55); }

/* Десять дверей в два ряда по пять: имя, срок, цена, и ничего больше. */
.abt-now .door { grid-template-rows: auto 1fr auto; padding: var(--sp-s) var(--sp-m); min-height: 9.5rem; }
.abt-now .door__voice { font-size: var(--step-1); }
@media (min-width: 80rem) { .abt-now { grid-template-columns: repeat(5, minmax(0, 1fr)); } }

/* Пять ячеек ценностей, по фразе в каждой. */
.abt-cov .cov__text { font-size: var(--step-0); color: rgb(255 255 255 / 0.72); }
/* Подпись в одну строку, чтобы линии над подписями стояли на одной высоте. */
.abt-cov .cov__foot b { white-space: nowrap; }

/* Двери команды: фото сверху, в общей рамке двери. Пять в ряд на широком. */
@media (min-width: 80rem) { .abt-doors { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.abt-door { grid-template-rows: auto auto auto 1fr; }
.abt-door__img {
  display: block; width: 100%; aspect-ratio: 480 / 560; object-fit: cover;
  border-radius: 8px; margin-bottom: var(--sp-xs, 0.4rem);
  /* Стиль ОДИН на все четыре снимка, и он запечён в файлы: чёрно-белое,
     контраст выровнен по гистограмме, лицо одного масштаба. Фильтром здесь
     ничего не делается: четыре разных источника фильтром не сводятся,
     PO: «стиль фото разный, можно в одном сделать?». */
}
.abt-door__img--none, .abt-door__img--fleet {
  display: grid; place-items: center;
  background: rgb(255 255 255 / 0.06); color: rgb(255 255 255 / 0.5);
  font-size: var(--step-3); letter-spacing: 0.08em;
}
/* Без подложки: звезда прямо на синем, поле только держит высоту в ряд с
   фотографиями. PO: «подложка под иконкой лишняя». */
.abt-door__img--fleet { background: transparent; color: #fff; }
.abt-door__img--fleet svg { width: 34%; height: auto; }

/* Без скрипта такта нет: страница обязана читаться как обычная. */
html:not(.js) .abt-sect { height: auto; }
html:not(.js) .abt-pin { position: static; height: auto; overflow: visible; }
html:not(.js) .abt-line { position: static; opacity: 1; }
html:not(.js) .abt-sheet { position: static; opacity: 1; transform: none; }
html:not(.js) .abt-lay { opacity: 1; }
/* ⚠️ Здесь стояло `position: static` у листа и `opacity: 1` у сцены, то есть
   слово Then -> Now и лист становились двумя обычными блоками. Секция несёт
   `data-rail-phone`, пин липкий, слово живёт своим тактом, лист поднимается.

   Лист на телефоне ДОЛЖЕН влезть в экран целиком: он внутри припиненного
   кадра с обрезкой, и всё, что не влезло, читателю недоступно совсем. Поэтому
   двери идут в две колонки и роли отдают высоту. Замерено на 390x844. */
@media (width < 60rem) {
  .abt-pin { padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.6svh)); padding-bottom: min(var(--sp-s), 1.6svh); }
  .abt-read h1 { font-size: min(var(--step-4), 7.6vw); line-height: 1.06; }
  .abt-line { font-size: min(var(--step-0), 2.1svh); max-width: none; }
  .abt-sheet .wrap { padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.4svh)); }
  .abt-sheet__head h2 { font-size: min(var(--step-3), 6.6vw); line-height: 1.06; }
  .abt-sheet__head .lede { font-size: min(var(--step-0), 3.6vw); }
  .abt-then { font-size: min(var(--step--1), 3.3vw); }
  .abt-now { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.3rem; }
  /* ⚠️ У двери четыре дорожки, третья `1fr`: на большом экране она держит
     цену прижатой к низу карточки, на телефоне она растягивала каждую дверь
     до 152 точек, пять рядов = 760, и нижние двери оказывались за обрезкой
     листа, то есть НЕДОСТУПНЫ вовсе. Дорожки по содержимому. */
  .abt-now .door { padding: 0.45rem 0.55rem; gap: 0.15rem; grid-template-rows: auto auto auto; min-height: 0; }
  .abt-now .door__voice { font-size: min(var(--step--1), 3.5vw); line-height: 1.15; }
  .abt-now .door__mark { font-size: 0.52rem; min-height: 0; }
  .abt-now .door__foot { font-size: min(var(--step-label), 2.9vw); }
}
@media (prefers-reduced-motion: reduce) {
  .abt-word__k--then { background-position: 50% 0; }
}

/* ⚠️ Узкий экран, `/knowledge-os/`: строка чека складывается в две, как у
   `.tar` на `/pricing/`. В один ряд имя, отточие, срок и количество не
   влезают: замерено на 390px, документ 638 вместо 390, количество уходило
   на 618. Видео первого экрана шире экрана на 8 точек по той же причине,
   что и всякое видео с `width` в разметке: ширина не ограничена. */
@media (width < 34rem) {
  .bpr { flex-wrap: wrap; row-gap: 0.15rem; }
  .bpr__n { white-space: normal; flex: 1 1 100%; }
  .bpr__dots { display: none; }
  .bpr__w { order: 1; }
  .bpr__q { order: 2; min-width: 0; margin-left: auto; }
}
.arv-view video, .arv-view img { max-width: 100%; }

/* ===== Страница цен, `/pricing/` =============================================

   Предмет: печатный прейскурант `.tariff` с главной, один на всю страницу,
   с тремя группами внутри. Своих правил два: первый экран под шапкой и
   строка-группа. */

/* ⚠️ Специфичность выше класса: правило на `[data-dark-top]` в main.css
   обнуляет верхнее поле, и `.prc-hero` один его не перебивал. Замерено:
   padding-top 0 при валидном правиле в каскаде. */
.sect.prc-hero[data-dark-top] {
  /* Контейнер света `data-glow`: единственный статичный из всех. */
  position: relative;
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-xl));
}
/* Потолок кеглем от окна по самому длинному слову: «Something» на 104px это
   480 точек, а колонка на 1280 - 376, и слово уходило под лист (замерено).
   На 1900 потолок не срабатывает: 6.2vw = 118 > --step-5. */
.prc-hero .sect__head h1 { color: #fff; max-width: 12ch; font-size: min(var(--hero), 6.2vw); }
.prc-hero .tar__n { color: inherit; text-decoration: none; }
.prc-hero a.tar__n:hover { text-decoration: underline; }
.prc-hero .tariff-lay { align-items: start; }
/* ⚠️ Заголовок ЛИПНЕТ. Лист длиннее экрана, и левая колонка кончалась на
   первом экране: дальше читатель прокручивал прайс мимо пустоты. PO: «как-то
   пусто слева снизу». Теперь колонка едет с листом и отпускает его в конце.
   Приём уже стоит на сайте: так держится шапка рельсы. */
@media (min-width: 64rem) {
  .prc-hero .sect__head {
    position: sticky;
    top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-xl));
  }
}

/* Строка-группа: подпись раздела внутри листа, как заголовок колонки в
   печатном прейскуранте. */
.tar--group {
  margin-top: 0.9rem; padding-top: 0.55rem;
  border-top: 1px solid rgb(20 22 28 / 0.14);
  font-size: var(--step-label); letter-spacing: 0.2em; text-transform: uppercase;
  color: rgb(20 22 28 / 0.55);
}
.tar--group:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* ===== Страница услуг, `/services/` ==========================================

   Третья сборка. Две прошлые записаны в шапке `services.njk`; здесь только
   то, что стоит сейчас: припиненная рельса, за такт одна пара «фраза и
   ответ». Механика взята у `/knowledge-os/`, ни одного своего движка.

   ⚠️ ИЕРАРХИЯ КЕГЛЕЙ: h1 страницы `--step-5`, фраза `--step-4`, имя услуги
   `--step-2`. Роль задаёт размер, и порядок ролей не переворачивается. */

/* Рельса: такт 0 заголовок, дальше экран на пару. Заголовок не уезжает, он
   гаснет на месте, ровно как `.mig-ask` на `/migration/`. */
.say-sect { height: calc(var(--n, 7) * 92svh); margin-top: calc(var(--chrome-h, var(--header-h)) * -1); }
.say-pin {
  /* ⚠️ `position` НЕ ЗАДАЁТСЯ. Здесь стояло `relative` ради света
     `::before`, и оно перебивало `sticky` у `.rail-sect__pin`: пин переставал
     прилипать, уезжал вверх, и после первого экрана шли ЧЕТЫРЕ ТЫСЯЧИ точек
     пустоты. PO: «эта страница не доделана и не работает». Замерено на
     собранной версии: `position: relative`, пин 800 при секции 5152.
     `sticky` сам создаёт систему координат для абсолютных детей, так что
     свету ничего не нужно. Та же ловушка записана у `.arv-pin`. */
  display: grid; grid-template-rows: 1fr auto;
  background: var(--ink);
  --tp: min(calc(var(--p, 0) * var(--n, 7)), calc(var(--n, 7) - 0.02));
  padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-l), 4svh));
  padding-bottom: min(var(--sp-l), 4svh);
  gap: min(var(--sp-l), 4svh);
  overflow: clip;
}
/* Свет за сценой. Тот же приём и те же числа, что у `.mig-pin`: синее пятно
   справа и белое слева, оба мягкие и без объектов. */
.say-pin::before {
  content: "";
  position: absolute; inset: -20%; z-index: 0; pointer-events: none;
  background:
    radial-gradient(48rem 44rem at 66% 44%, rgb(74 108 255 / 0.11), transparent 68%),
    radial-gradient(34rem 34rem at 8% 70%, rgb(252 252 252 / 0.03), transparent 72%);
}
.say-pin > * { position: relative; z-index: 1; }

/* Заголовок: первый слайд рельсы. Живёт ровно свою долю проезда, как и
   каждая пара, потому что состояние ставит `main.js`, а не наша формула. */
.say-ask {
  grid-row: 1; grid-column: 1 / -1;
  align-self: center; justify-self: center;
  opacity: 0; transition: opacity 0.3s ease;
  pointer-events: none;
}
.say-ask[data-state="live"] { opacity: 1; }

/* Сцена: все пары в одной клетке, видна одна. Поля страницы держит `.wrap`
   ВНУТРИ пары: `inset: 0` на самой паре клало цитату на нулевую координату
   экрана, мимо поля в 95 точек, на котором стоит заголовок. */
.say-stage { position: relative; min-height: 0; grid-row: 1; grid-column: 1 / -1; }
.say-pair { position: absolute; inset: 0; }
.say-pair__in {
  height: 100%;
  display: grid; align-content: center; gap: min(var(--sp-m), 3svh);
  /* ⚠️ Состояние берётся у РЕЛЬСЫ, а не считается своими окнами. `main.js`
     сам ставит `data-state="live"` ровно на один `[data-rail-slide]`, и это
     единственный источник правды: собственные окна по `--from`/`--to`
     накладывались на 0.7 такта, и на кадре читались две фразы поверх друг
     друга. Заодно кнопка живой пары ловит клики, а мёртвой нет. */
}
/* ⚠️ Состояние берётся у РЕЛЬСЫ, а не считается своими окнами.
   Свои окна по `--from`/`--to` давали ПРОВАЛЫ: на границе такта одна пара
   уже погасла, вторая ещё не зажглась, и экран оставался пустым. Замерено
   на живом хосте: при прокрутке 0.25 и 0.70 не видно НИ ОДНОЙ пары, а
   последняя гасла к концу проезда до 0.12.

   `main.js` держит `data-state="live"` ровно на одном `[data-rail-slide]`
   и распределяет их по всей прокрутке. Заголовок теперь такой же слайд, и
   считать вручную нечего. */
.say-pair {
  opacity: 0; transform: translateY(1.2rem); pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.say-pair[data-state="live"] { opacity: 1; transform: none; pointer-events: auto; }


/* Фраза владельца: второй кегль страницы, в кавычках, как цитата. */
/* Паре достался весь экран, значит цитата берёт его высоту. Потолок ниже
   `h1` первого экрана: иерархия ролей не переворачивается. */
.say-q {
  margin: 0; max-width: 20ch;
  font-family: Asgard, Impact, sans-serif; font-weight: 500;
  font-size: min(var(--step-4), 11svh); line-height: 1.0; letter-spacing: -0.02em;
  color: #fff;
}

/* Ответ: подпись, имя, объяснение, цена и действие одной колонкой справа. */
.say-ans { display: grid; gap: min(var(--sp-s), 1.8svh); align-content: start; max-width: 48ch; }
.say-ans__name { font-size: min(var(--step-3), 6svh); line-height: 1.03; color: #fff; }
.say-ans__text { font-size: min(var(--step-0), 2.3svh); color: rgb(255 255 255 / 0.74); }
.say-ans__meta { display: flex; align-items: baseline; gap: 1rem; margin: 0; }
.say-ans__price { font-family: PlexMono, ui-monospace, monospace; font-weight: 600; font-size: min(var(--step-1), 3svh); color: var(--blue-lo); }
.say-ans__when { font-size: var(--step-label); letter-spacing: 0.12em; text-transform: uppercase; color: rgb(255 255 255 / 0.5); }
.say-ans__go { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin: 0; }

@media (min-width: 68rem) {
  .say-pair__in { grid-template-columns: minmax(0, 34rem) minmax(0, 1fr); column-gap: clamp(2rem, 6vw, 7rem); align-content: center; align-items: center; }
  .say-ans { align-content: center; }
}

/* Остальные шесть: строка прейскуранта, а не второй интерактив. */
.say-more { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.1rem; }
.say-more__row { display: flex; align-items: baseline; gap: 0.6rem; padding: 0.5rem 0; border-bottom: 1px solid var(--line); }
.say-more__q { font-size: var(--step-0); }
.say-more__dots { flex: 1 1 auto; min-width: 1.5rem; height: 0.55em; border-bottom: 1px dotted var(--line); }
.say-more__a { font-family: PlexMono, ui-monospace, monospace; font-size: var(--step--1); white-space: nowrap; }

/* Без скрипта такта нет: все пары читаются подряд. */
html:not(.js) .say-sect { height: auto; }
html:not(.js) .say-pin { height: auto; overflow: visible; }
html:not(.js) .say-stage { min-height: 0; }
html:not(.js) .say-pair { position: static; opacity: 1; transform: none; pointer-events: auto; margin-bottom: var(--sp-xl); }
html:not(.js) .say-pair__in { height: auto; }
/* ⚠️ НА ТЕЛЕФОНЕ РЕЛЬСА ОСТАЁТСЯ. Здесь стояло отключение: пин становился
   обычным, пары ложились подряд, и заголовок пропадал вовсе. PO: «а мобайл
   почему так выглядит? там должен быть заголовок и по скроллу смена слайдов,
   а не просто прокрутка». Опыт на телефоне тот же, что на десктопе, меняются
   только размеры.

   Свет `::before` при этом остаётся снятым: `inset: -20%` давал прокрутку
   вбок, документ 468 при экране 390, ровно 20% ширины. */
@media (width < 68rem) {
  /* ⚠️ Общее правило сайта отключает рельсы на узких экранах
     (`main.css`: `.rail-sect__pin { position: static }`). Здесь оно
     перебивается ДВУМЯ классами, потому что опыт этой страницы и есть смена
     слайдов: без этого пин не липнет, пары уезжают, и остаётся простыня.
     Замерено: `position: static` при живых тактах. */
  .rail-sect.say-sect { height: calc(var(--n, 7) * 92svh); }
  .rail-sect__pin.say-pin { position: sticky; top: 0; height: 100svh; overflow: clip; padding-block: 0; }
  /* Свет остаётся, но переезжает С ПСЕВДОЭЛЕМЕНТА НА ФОН ПИНА. Причина в
     общем блоке ниже: `inset: -20%` давал прокрутку вбок, а фоновый слой
     элемента не может выйти за его же коробку ни при какой раскладке. */
  .say-pin::before { display: none; }
  .say-pin { background-image: var(--night-glow); }
  /* Заголовок остаётся по центру и на телефоне: `.mig-ask` центрируется
     `justify-self`, а в одноколоночной сетке пина это надо повторить. */
  .say-ask { justify-self: center; text-align: center; max-width: 34rem; }
  /* Пара в одну колонку и по высоте окна: цитата, имя, объяснение, цена и
     кнопка обязаны влезть в один экран телефона. */
  .say-pair__in { grid-template-columns: minmax(0, 1fr); align-content: center; gap: min(var(--sp-m), 2.6svh); }
  .say-q { font-size: min(var(--step-3), 6.4svh); max-width: none; }
  .say-ans { gap: min(var(--sp-s), 1.6svh); max-width: none; }
  .say-ans__name { font-size: min(var(--step-2), 4.4svh); }
  .say-ans__text { font-size: min(var(--step-0), 2.1svh); }
}
@media (prefers-reduced-motion: reduce) { .say-pair { transform: none; } }

/* ===== Первые экраны на телефоне ============================================

   ⚠️ Общая правка, не десять частных. Все иммерсивные первые экраны держали
   десктопный кегль заголовка, и на 390px он не влезал: `/brand-platform/`
   ломался по ОДНОМУ слову в строку («Six / people / answer / for / you.»),
   замерено, колонка 350 при кегле 37.3.

   Кегль тут ТЕКУЧИЙ по ширине окна, а не назначенный вторым размером: роль
   у заголовка одна, и на узком экране она просто перестаёт упираться в край.
   Потолок прежний, `--step-4`, поэтому на планшете и выше ничего не меняется.

   Список секций закрытый и перечислен поимённо: правило не должно ловить
   заголовки обычных секций, у них своя шкала. */
@media (width < 48rem) {
  /* ⚠️ `.paper`, `.stage` и `.opener` СЮДА НЕ ПИШУТСЯ: они объявлены в
     `paper.css` и `main.css`, и гейт `collide` считает имя в двух файлах
     коллизией. Их мобильные правила лежат рядом с их же компонентами,
     см. `paper.css`. Замерено: пять коллизий стали восемью. */
  .bp-read h1,
  .arv-read h1,
  .abt-read h1,
  .mig-ask__head,
  .prc-hero h1 {
    font-size: min(var(--step-4), 7.6vw);
    line-height: 1.06;
  }

  /* ⚠️ Колонки первых экранов складываются в одну. Двухколонная сетка на
     390px отдавала заголовку НУЛЕВУЮ ширину: у `.bp-read` вторая колонка
     просит до 30rem, первая сжималась в ноль, и заголовок ломался по одной
     букве. Замерено: ширина h1 = 0 при колонке 350, десять строк на девять
     слов. То же у `/un-saas/` и `/site/`. */
  .bp-read, .arv-lay, .abt-read, .say-pair__in, .week__head { grid-template-columns: minmax(0, 1fr); }

  /* ⚠️ Свет `::before` с `inset: -20%` даёт прокрутку вбок, когда пин
     перестаёт быть позиционированным: он считается от другого предка, и
     обрезка на пине его не режет. Замерено на `/migration/` и
     `/money-trail/`: документ 468 при экране 390, ровно 20% ширины.

     Но снять свет насовсем нельзя: тёмный фон сайта СИНЕВАТЫЙ, а без света
     он становится плоско-чёрным, и первый экран на телефоне перестаёт быть
     тем же местом, что на большом экране. PO: «так там фон синеватый, не
     чёрный». Поэтому свет переезжает на ФОН САМОГО ПИНА: фоновый слой
     рисуется по коробке элемента и не может её превысить, чей бы предок ни
     оказался системой координат. */
  .rail-sect__pin::before, .rail-sect__pin::after,
  .mig-pin::before, .case-pin::before, .ai-pin::before { display: none; }
  /* ⚠️ Ровно ТЁМНЫЕ пины, а не все подряд: `/un-saas/` держит рельсу
     `#scope` на светлом фоне, и синее пятно в 0.15 на белом читается грязью.
     Отбор идёт по `[data-dark-top]` — по тому же признаку, по которому
     страница подтягивает секцию под шапку. */
  [data-dark-top] .rail-sect__pin,
  .mig-pin, .case-pin, .ai-pin { background-image: var(--night-glow); }
  /* Строки под заголовком тоже: на телефоне они шли тем же кеглем, что на
     десктопе, и занимали по три экрана. */
  .bp-line, .arv-line, .abt-line { font-size: min(var(--step-1), 4.1vw); }

  /* Витрина главной: телефон в ней стоит под наклоном, и его тень выходила
     за экран на 5 точек. Обрезка ТОЛЬКО по горизонтали: `clip` по X не
     ломает липкость, в отличие от `hidden`. */
  .case__o, .feed, .fone { overflow-x: clip; }
}


/* ===== Страница «Отдел маркетинга», `/department/` ===========================

   Предмет по `docs/PAGE-CONCEPTS.md`: «пять вакансий против одной строки».
   Приём первого экрана ВЫБРАН PO на стенде `/_dept/` (05.09.2026): вариант B,
   доска с кнопками со стенда `/_prices/` W. Остальные пять вариантов и замер
   записаны в `docs/TASTE-GATE.md`.

   Хореография по рельсе, одним числом `--tp` (0..8):
     0      заголовок во весь кадр, доска поднимается под ним;
     1..5   объявление i пришпиливается: падает сверху, садится, кнопка
            появляется, когда листок лёг;
     6      пришпиливается синяя строка прейскуранта;
     6.5..7 пять объявлений СНИМАЮТСЯ: падают вниз с доски. Это и есть то,
            что делает продукт: пять найма, которых не будет;
     6.9..  передача светлому листу, тем же `--hand`, что на `/brand-platform/`.

   Переходов (`transition`) на величинах прокрутки НЕТ намеренно: значение
   меняется каждый кадр, и переход поверх него рвёт картинку. Записано на
   `/money-trail/` и повторено на главной. */

.dpt-sect {
  height: calc(var(--n, 8) * 92svh);
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
}
.dpt-pin {
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  /* Фон: ink, а синий свет отдельным слоем ниже - РОВНО тем же, что у
     `.mig-pin::before` на `/migration/` и `.say-pin::before` на `/services/`.
     PO: «синий должен быть такой же, как на странице создания сайтов».
     Замерено: у `/site/` синего нет вовсе (сцена гасится в `--ink`), синий
     первого экрана на сайте один, и он у переезда. Первая версия здесь была
     втрое плотнее (0.32 против 0.11) - снята. */
  background: var(--ink);
  color: var(--on-dark);
  --tp: min(calc(var(--p, 0) * var(--n, 8)), calc(var(--n, 8) - 0.02));
  /* Доска поднимается под уходящим заголовком. */
  --bd: clamp(0, calc((var(--tp) - 0.4) * 1.6), 1);
  /* Снятие пяти объявлений. Начинается ПОСЛЕ того, как синяя строка легла
     (такт 6 плюс её посадка), и заканчивается до прихода листа. */
  /* ⚠️ Имя `--gone`, НЕ `--off`. Первая версия звалась `--off`, и это имя
     уже занято: `--off` это токен светлого грунта в `main.css`, которым залит
     лист передачи. Своё число перекрыло цвет, `background: var(--off)`
     вычислился в `clamp(...)`, и лист стал прозрачным. Замерено: фон листа
     `rgba(0,0,0,0)` при заданном `var(--off)`. Проверка имени перед новой
     переменной - та же, что для классов (DESIGN-SYSTEM §8). */
  --gone: clamp(0, calc((var(--tp) - 6.5) * 1.6), 1);
  --hand: clamp(0, calc((var(--tp) - 6.9) * 1.9), 1);
  /* ⚠️ Равно такту, где сцена доходит до нуля: 6.9 + 0.52 / 1.9. Та же
     арифметика и та же ловушка, что у `.ai-pin` и `.bp-pin`. */
  --done-at: 7.174;
  padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-m), 3svh));
  padding-bottom: min(var(--sp-m), 3svh);
  padding-inline: max(1.25rem, 5vw);
  overflow: clip;
}
/* Свет за сценой: те же числа, что у `.mig-pin::before`. На телефоне общее
   правило снимает `::before` и кладёт `--night-glow` на фон пина. */
.dpt-pin::before {
  content: "";
  position: absolute; inset: -20%; z-index: 0; pointer-events: none;
  background:
    radial-gradient(48rem 44rem at 66% 44%, rgb(74 108 255 / 0.11), transparent 68%),
    radial-gradient(34rem 34rem at 8% 70%, rgb(252 252 252 / 0.03), transparent 72%);
}
.dpt-pin > * { position: relative; z-index: 1; }

/* --- Такт 0: вопрос во весь кадр ------------------------------------------- */
.dpt-ask {
  grid-area: 1 / 1;
  align-self: center; justify-self: center;
  max-width: 44rem; text-align: center;
  opacity: clamp(0, calc(1.9 - var(--tp) * 2.1), 1);
  pointer-events: none;
  z-index: 3;
}
.dpt-ask h1 { margin: 0 0 var(--sp-m); }
.dpt-ask h1 b { font-weight: 500; color: var(--blue-lo); }
/* Строка под заголовком первого экрана: ТА ЖЕ роль и те же числа, что у
   `.mig-ask__line` на `/migration/` и `/services/` (моно, `--step-0`, 34rem).
   Одна роль, один кегль на сайт. */
.dpt-ask__line {
  margin: 0 auto; max-width: 34rem;
  font-family: PlexMono, ui-monospace, monospace;
  font-size: var(--step-0); line-height: 1.6;
  color: var(--on-dark-muted);
}
.dpt-ask__hint { margin: var(--sp-m) 0 0; font-family: PlexMono, ui-monospace, monospace; font-size: var(--step--1); color: var(--on-dark-muted); }

/* --- Сцена: доска -------------------------------------------------------- */
.dpt-scene {
  grid-area: 1 / 1;
  position: relative;
  opacity: clamp(0, calc((0.52 - var(--hand, 0)) * 2.4), 1);
}
.dpt-pin[data-done="true"] .dpt-scene { visibility: hidden; pointer-events: none; }

/* Пробка. ⚠️ Первая версия была узором из точек, как на стенде, и PO назвал её
   примитивом. Он прав: точки в сетке это не пробка, у пробки вкрапления
   РАЗНОГО размера и лежат неровно. Здесь материал собран из трёх слоёв SVG-
   шума (feTurbulence), и это не картинка: ни одного файла, всё в CSS.
     - крупные вкрапления: шум с частотой 0.05, обрезанный матрицей в пятна;
     - мелкие: то же, частота 0.22;
     - зерно: тонкий шум поверх, чтобы плоскость не была гладкой.
   Свет сверху-слева, виньетка к краям, рама из дерева с фаской и внутренней
   тенью. Доска занимает весь кадр и режется его краем. */
.dpt-board {
  position: absolute; inset: -4% -3%;
  --frame: clamp(12px, 1.3vw, 20px);
  background:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.35 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='520' height='520'><filter id='f'><feTurbulence type='fractalNoise' baseFrequency='0.11' numOctaves='3' seed='7' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.30  0 0 0 0 0.18  0 0 0 0 0.07  0 0 0 -2.6 1.5'/></filter><rect width='100%' height='100%' filter='url(%23f)'/></svg>"),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.38' numOctaves='2' seed='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.38  0 0 0 0 0.24  0 0 0 0 0.10  0 0 0 -3.2 1.75'/></filter><rect width='100%' height='100%' filter='url(%23g)'/></svg>"),
    radial-gradient(120% 90% at 40% 30%, rgb(255 232 195 / 0.22), transparent 62%),
    linear-gradient(160deg, #dcb075 0%, #c6955a 45%, #a97a44 100%);
  background-size: 240px 240px, 360px 360px, 140px 140px, auto, auto;
  box-shadow:
    inset 0 0 0 var(--frame) #4a3117,
    inset 0 0 0 calc(var(--frame) + 2px) rgb(255 255 255 / 0.10),
    inset 0 0 0 calc(var(--frame) + 3px) rgb(0 0 0 / 0.55),
    inset 0 26px 70px rgb(0 0 0 / 0.32),
    0 40px 80px rgb(0 0 0 / 0.6);
  opacity: var(--bd);
  transform: translateY(calc((1 - var(--bd)) * 6svh));
}
/* Рама: волокно дерева по периметру. */
.dpt-board::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border: var(--frame) solid transparent;
  border-image: linear-gradient(0deg, #5a3a1c, #5a3a1c) 20;
  /* Волокно двумя слоями разного периода поверх ровного дерева: один период
     читался штрих-кодом. Контраст ниже, чем у полос первой версии. */
  background:
    repeating-linear-gradient(93deg, rgb(255 220 170 / 0.08) 0 2px, transparent 2px 9px, rgb(0 0 0 / 0.14) 9px 11px, transparent 11px 23px),
    repeating-linear-gradient(91deg, rgb(0 0 0 / 0.10) 0 1px, transparent 1px 5px, rgb(255 230 190 / 0.05) 5px 6px, transparent 6px 14px);
  background-clip: border-box;
  -webkit-mask: linear-gradient(#000 0 0) padding-box exclude, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) padding-box exclude, linear-gradient(#000 0 0);
}
/* Виньетка и блик: край темнее, середина под светом. */
.dpt-board::after {
  content: ""; position: absolute; inset: var(--frame); pointer-events: none;
  background:
    linear-gradient(180deg, rgb(255 255 255 / 0.07), transparent 30%),
    radial-gradient(85% 75% at 50% 42%, transparent 55%, rgb(30 15 0 / 0.34) 100%);
}

/* Объявление. Бумага та же, что у `.pin`. Положение на доске задано
   инлайном (`--x`, `--y`, `--r`), потому что доска это не сетка: на настоящей
   доске листки висят там, где их пришпилили. */
.dpt-post {
  position: absolute;
  left: var(--x); top: var(--y);
  width: min(17rem, 72vw);
  display: grid; gap: 0.3rem;
  padding: 1.5rem 1.1rem 1rem;
  /* Бумага: зерно тем же шумом, лёгкий свет сверху-слева, тень ДВУМЯ
     слоями - контактная у кнопки и мягкая от висящего листа. Листок висит
     на кнопке, поэтому и вращается вокруг неё, а не вокруг своего центра. */
  background:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='p'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.10 0'/></filter><rect width='100%' height='100%' filter='url(%23p)'/></svg>"),
    linear-gradient(135deg, #fffdf7 0%, #f7f1e3 60%, #eee6d3 100%);
  background-size: 240px 240px, auto;
  color: #14161c;
  box-shadow:
    0 1px 2px rgb(0 0 0 / 0.28),
    0 14px 26px -8px rgb(0 0 0 / 0.55),
    0 34px 54px -22px rgb(0 0 0 / 0.5);
  transform-origin: 50% 6%;
  --in: clamp(0, calc((var(--tp) - var(--i)) * 3), 1);
  opacity: var(--in);
  transform:
    translate(-50%, -50%)
    translateY(calc((1 - var(--in)) * -40svh + var(--gone) * 130svh))
    rotate(calc(var(--r) + (1 - var(--in)) * -6deg + var(--gone) * 28deg));
}
.dpt-post__tack, .dpt-one__tack {
  position: absolute; top: 0.5rem; left: 50%;
  width: 0.95rem; height: 0.95rem; margin-left: -0.475rem; border-radius: 50%;
  /* Объём: блик в верхней трети, тёмный ободок, и тень на листке отдельным
     слоем (::after), смещённая вниз-вправо, как от света сверху-слева. */
  background: radial-gradient(circle at 32% 28%, #ffc1b3 0 10%, #ef5a42 38%, #9a2616 88%, #6e180d 100%);
  box-shadow: inset 0 1px 1px rgb(255 255 255 / 0.55), inset 0 -2px 3px rgb(0 0 0 / 0.35);
  opacity: clamp(0, calc((var(--in) - 0.85) * 8 - var(--gone) * 8), 1);
  z-index: 1;
}
.dpt-post__tack::after, .dpt-one__tack::after {
  content: ""; position: absolute; left: 25%; top: 72%;
  width: 150%; height: 70%; border-radius: 50%;
  background: rgb(0 0 0 / 0.38); filter: blur(3px);
  z-index: -1;
}
.dpt-post__k { margin: 0; font-size: var(--step-label); letter-spacing: 0.14em; text-transform: uppercase; color: rgb(20 22 28 / 0.5); }
/* ⚠️ Цвет заголовка задан явно: секция тёмная, `.on-dark` отдаёт `h2` белый,
   и на бумаге название должности пропадало. Поймано кадром на стенде. */
.dpt-post__t { margin: 0; font-family: Asgard, Impact, sans-serif; font-weight: 500; font-size: var(--step-2); line-height: 1; letter-spacing: -0.02em; color: #14161c; }
.dpt-post__pay { margin: 0.15rem 0 0; font-size: var(--step-0); font-variant-numeric: tabular-nums; }
.dpt-post__pay b { font-weight: 600; }
.dpt-post__src { margin: 0; font-size: var(--step-label); letter-spacing: 0.06em; color: rgb(20 22 28 / 0.5); }
.dpt-post__do { margin: 0.3rem 0 0; font-size: var(--step--1); line-height: 1.4; color: rgb(20 22 28 / 0.78); }

/* Одна строка: та же бумага, синяя, и на ней строка принятого прейскуранта
   (`.tar` с `/pricing/`). Пришпиливается на такте 6 и ОСТАЁТСЯ. */
.dpt-one {
  position: absolute; left: 50%; top: 50%;
  width: min(30rem, 84vw);
  display: grid; gap: 0.25rem;
  padding: 1.5rem 1.1rem 1rem;
  background: var(--blue); color: #fff;
  box-shadow: 0 10px 24px rgb(0 0 0 / 0.45);
  --i: 6;
  --in: clamp(0, calc((var(--tp) - var(--i)) * 3), 1);
  --gone: 0;
  opacity: var(--in);
  transform: translate(-50%, -50%) translateY(calc((1 - var(--in)) * -40svh)) rotate(calc(-1.5deg + (1 - var(--in)) * -6deg));
  z-index: 2;
}
.dpt-one__k { margin: 0; font-size: var(--step-label); letter-spacing: 0.14em; text-transform: uppercase; color: rgb(255 255 255 / 0.72); }
.dpt-one .tar { font-size: var(--step-0); color: #fff; }
.dpt-one .tar__p { color: #fff; }
.dpt-one .tar__w { color: rgb(255 255 255 / 0.7); }
.dpt-one .tar__dots { border-bottom-color: rgb(255 255 255 / 0.5); }

/* --- Лист передачи: пять функций одной строки -------------------------------
   Светлый, и шапка переворачивается с ним (`data-sheet-light` на секции), как
   на `/brand-platform/`. */
.dpt-sheet {
  position: absolute; inset: 0; z-index: 2;
  display: grid; align-content: center;
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m));
  padding-bottom: var(--sp-m);
  padding-inline: max(1.25rem, 5vw);
  overflow: clip;
  background: var(--off);
  color: var(--text);
  --heading-color: var(--text);
  --lede-color: var(--text-muted);
  opacity: clamp(0, calc((var(--hand, 0) - 0.50) * 2.6), 1);
  transform: translateY(calc(clamp(0, calc(1.2 - var(--hand, 0) * 1.6), 1) * 1.1rem));
  pointer-events: none;
}
.dpt-pin[data-done="true"] .dpt-sheet { pointer-events: auto; }
.dpt-sheet .wrap { padding-inline: 0; }
.dpt-sheet .sect__head { margin-bottom: var(--sp-l); }

/* Пять функций строкой каждая: роль из объявления, что делает отдел вместо
   найма, и какая страница сайта это несёт. Это карта, а не прайс: цен здесь
   нет, они принадлежат `/pricing/`. */
.dpt-fn { list-style: none; margin: 0; padding: 0; display: grid; }
.dpt-fn__row {
  display: grid; grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
  gap: var(--sp-m); align-items: baseline;
  padding: 0.85rem 0; border-top: 1px solid var(--line);
}
.dpt-fn__role { margin: 0; font-family: PlexMono, ui-monospace, monospace; font-size: var(--step--1); letter-spacing: 0.04em; color: var(--text-muted); }
.dpt-fn__role s { color: var(--text-muted); text-decoration-color: var(--red); }
.dpt-fn__what { margin: 0; font-size: var(--step-0); line-height: 1.45; }
.dpt-fn__what a { color: var(--blue); white-space: nowrap; }

/* --- Ниже рельсы ------------------------------------------------------------- */
.dpt-cov .cov__text { font-size: var(--step-0); }
/* «После первой коробки»: слева живая первая коробка (`.work` с главной),
   справа лист заказа на той же бумаге, что строка первого экрана. Первая
   версия блока была заголовком и абзацем без предмета - забракована. */
.dpt-after { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: var(--sp-xl); align-items: center; }
.dpt-order {
  padding: clamp(1.3rem, 2.6vw, 2rem) clamp(1.4rem, 3vw, 2.4rem);
  background: #f7f5ef; color: #14161c;
  border-radius: 3px;
  box-shadow: 0 2px 4px rgb(0 0 0 / 0.3), 0 30px 60px rgb(0 0 0 / 0.25);
}
.dpt-order__k { margin: 0 0 0.6rem; font-size: var(--step-label); letter-spacing: 0.14em; text-transform: uppercase; color: rgb(20 22 28 / 0.5); }
.dpt-order__rows { list-style: none; margin: 0; padding: 0; counter-reset: line; }
.dpt-order__row { font-size: var(--step-0); padding: 0.55rem 0; border-top: 1px solid rgb(20 22 28 / 0.12); counter-increment: line; }
.dpt-order__row .tar__n::before { content: counter(line) "  "; color: rgb(20 22 28 / 0.45); }
.dpt-order__row:last-child .tar__p { color: var(--blue); }
.dpt-order__f { margin: 0.9rem 0 0; padding-top: 0.7rem; border-top: 1px dashed rgb(20 22 28 / 0.3); font-size: var(--step--1); color: rgb(20 22 28 / 0.62); }

/* Без скрипта такта нет: страница обязана читаться как обычная. */
html:not(.js) .dpt-sect { height: auto; }
html:not(.js) .dpt-pin { position: static; height: auto; overflow: visible; }
html:not(.js) .dpt-ask { opacity: 1; position: static; }
html:not(.js) .dpt-scene { opacity: 1; }
html:not(.js) .dpt-board { position: static; opacity: 1; transform: none; display: grid; gap: 1rem; padding: 1.4rem; }
html:not(.js) .dpt-post, html:not(.js) .dpt-one { position: static; opacity: 1; transform: none; width: auto; }
html:not(.js) .dpt-sheet { position: static; opacity: 1; transform: none; }

/* Телефон: рельса остаётся (`data-rail-phone`). Листки ложатся стопкой по
   центру со сдвигом по такту, потому что пяти мест на 390 точках нет; синяя
   строка ложится поверх стопки. */
@media (width < 60rem) {
  .dpt-pin { padding-top: calc(var(--chrome-cover, var(--header-h)) + min(var(--sp-s), 1.6svh)); }
  .dpt-ask h1 { font-size: min(var(--step-4), 7.6vw); line-height: 1.06; }
  .dpt-ask__line { font-size: min(var(--step-0), 3.7vw); }
  /* Стопка со сдвигом по ДВУМ осям, чтобы у каждого листка было видно
     название: при сдвиге только вниз на 5svh из-под верхнего торчали одни
     полоски «help wanted». */
  .dpt-post { left: calc(50% + (mod(var(--i), 2) - 0.5) * 7vw); top: calc(46% + (var(--i) - 3) * 8svh); }
  .dpt-post__do { display: none; }
  .dpt-one { top: 52%; }
  .dpt-sheet .sect__head { margin-bottom: min(var(--sp-m), 2.2svh); }
  .dpt-sheet .sect__head h2 { font-size: min(var(--step-3), 6.2vw); line-height: 1.04; }
  .dpt-sheet .sect__head .lede { font-size: min(var(--step-0), 3.4vw); }
  .dpt-fn__row { grid-template-columns: minmax(0, 1fr); gap: 0.2rem; padding: 0.55rem 0; }
  .dpt-fn__what { font-size: min(var(--step--1), 3.4vw); }
  .dpt-after { grid-template-columns: minmax(0, 1fr); gap: var(--sp-l); }
}
/* Ноутбук 60-75rem: точки доски раздвинуты к краям. Листки 282 точки в
   ширину, и на 1024 соседи перекрывались на 29 точек (замерено). `!important`
   намеренно: положение задано инлайном как переменная, и перебить его может
   только важное объявление. */
@media (60rem <= width < 75rem) {
  .dpt-post:nth-of-type(1) { --x: 17% !important; --y: 27% !important; }
  .dpt-post:nth-of-type(2) { --x: 50% !important; --y: 23% !important; }
  .dpt-post:nth-of-type(3) { --x: 83% !important; --y: 29% !important; }
  .dpt-post:nth-of-type(4) { --x: 22% !important; --y: 75% !important; }
  .dpt-post:nth-of-type(5) { --x: 79% !important; --y: 73% !important; }
}
@media (prefers-reduced-motion: reduce) {
  .dpt-post, .dpt-one { transform: translate(-50%, -50%) rotate(var(--r, 0deg)); }
}

/* ===== /work/: первый экран - сама работа, во весь кадр ======================

   Приём: Kononenko, REFERENCES.md #1 (10.9x, 4 цвета, медиа 1.19). Работа
   занимает весь экран и режется его краем, имя лежит НА ней подписью снизу
   слева. Здесь работа это платформа Gotlift, едущая в окне на плите цвета
   клиента - тот же компонент `.work`, что на главной и на `/department/`,
   только в рост экрана. Первая версия была заголовком и абзацем на чёрном:
   контраст 4.7x, медиа 0, ни одна ось не выкручена (аудит 05.09). */

.wk-hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-items: end;
  overflow: clip;
  background-color: var(--ink); /* не шортхенд: фон-картинку кладёт `data-glow` */
  color: var(--on-dark);
  margin-top: calc(var(--chrome-h, var(--header-h)) * -1);
  padding-top: calc(var(--chrome-cover, var(--header-h)) + var(--sp-l));
  padding-bottom: var(--sp-l);
}
/* Плита с работой: в правых двух третях, режется правым и нижним краем. */
/* Плита начинается ПОД шапкой, а не под её низом: с `top: 0` навигация
   ложилась на жёлтое, и белая кнопка «Get a price» тонула. Замерено кадром. */
.wk-hero__work {
  position: absolute;
  inset: calc(var(--chrome-cover, var(--header-h)) + var(--sp-m)) -6% -12% 34%;
  z-index: 0;
}
/* ⚠️ Высота ОПРЕДЕЛЁННАЯ по всей цепочке `.work > .work__plate > .work__win`.
   `.work` это grid с `align-content: start` и без высоты, `height: 100%` у
   плиты опирался на auto и схлопывался: на кадре от работы оставалась жёлтая
   полоса в 40 точек под шапкой. Та же ловушка, что у `.mig-stage`: то, что
   считается от высоты, обязано иметь высоту. */
.wk-hero__work .work { display: block; height: 100%; }
.wk-hero__work .work__plate { display: block; height: 100%; box-sizing: border-box; }
.wk-hero__work .work__win { aspect-ratio: auto; height: calc(100% - clamp(1.4rem, 3.2vw, 3rem)); }
.wk-hero .wrap { position: relative; z-index: 1; }
/* Подпись на работе: имя, строка, статус. Ширина под колонку слева от плиты. */
.wk-hero__cap { max-width: 34rem; display: grid; gap: var(--sp-m); }
.wk-hero__cap h1 { margin: 0; color: #fff; }
.wk-hero__cap .lede { margin: 0; color: var(--on-dark-muted); }
html:not(.js) .wk-hero__work .work__win img { animation: none; }

@media (width < 60rem) {
  .wk-hero { align-items: start; grid-template-rows: auto auto; }
  /* На телефоне работа стоит ПОД подписью, а не за ней: то, что на десктопе
     слева направо, здесь сверху вниз. В разметке плита идёт первой (на
     десктопе она фон), поэтому порядок задаётся сеткой. */
  .wk-hero .wrap { grid-row: 1; }
  .wk-hero__work { grid-row: 2; position: relative; inset: auto; margin: var(--sp-l) calc(max(1.25rem, 5vw) * -1) 0; height: 60svh; }
  .wk-hero__cap h1 { font-size: min(var(--hero), 12vw); }
}
