/* base.css — каркас, общий для всех 17 страниц.
 *
 * Выделен из site.css итерацией И1. Правило отнесения — не на глаз, а по
 * факту: сюда попало всё, чьи селекторы встречаются И на главной, И на
 * внутренних страницах, плюс всё бесклассовое (сброс, типографика,
 * элементы). Классификация построена по разметке всех 17 файлов.
 *
 * Здесь живут: сброс, база текста, кнопки, знаки бренда, шапка и её
 * выпадающие меню, переключатель сети, подвал, адаптивность шапки,
 * появление при прокрутке, снятие движения по `prefers-reduced-motion`.
 *
 * ЛОВУШКА, КОТОРУЮ ЭТОТ ФАЙЛ СУЩЕСТВУЕТ ЧТОБЫ ЗАКРЫТЬ.
 * Компонент, который выглядит «главностраничным», часто общий. На
 * `bridge-waf.com` так дважды уехали адаптивность шапки и аккордеон FAQ,
 * и внутренние страницы разваливались молча. Проверка перед закрытием
 * любой правки: открыть внутреннюю страницу БЕЗ `home.css` и убедиться,
 * что каркас цел.
 *
 * Подключается после tokens.css и до home.css / kb.css.
 */


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


/* ============================================================================
   Фоновая система — два слоя вместо пустоты
   ----------------------------------------------------------------------------
   Слои лежат за всем содержимым, помечены `aria-hidden` и не перехватывают
   указатель. Разметка добавлена на все 17 страниц двумя пустыми `div`.

   1. Сетка-чертёж 48×48 из `--line-soft` с радиальной маской сверху. Даёт
      ощущение чертежа, а не пустоты.
   2. Ambient-дрейф: три радиальных пятна, `blur(56px)`, 34s alternate.

   ПОТОЛОК ВОША ПОСЧИТАН, А НЕ ПОДОБРАН НА ГЛАЗ. Токены И3 решались против
   худшей текстовой подложки — `#1c1c21` в тёмной теме. Вош поднимает
   светлоту фона, то есть съедает запас, а запас там 0.01–0.04. Замер
   2026-08-27 дал предел, при котором композит ещё не превышает эту
   подложку: 4.70% для зелёного пятна, 3.46% для холодного, 2.96% для
   тёплого. Взято 2.5% на все три — с запасом к самому строгому из трёх.
   При 2.5% акцент на композите даёт 4.65:1 при норме 4.5.

   Отсюда правило на будущее: **менять эти проценты можно только вместе с
   пересчётом токенов.** Фон и контраст здесь не независимы, и
   `check-contrast.py` этой связи не видит — он меряет пары токенов, а не
   композит.

   Зернистого слоя нет намеренно. У эталона он есть и держит градиенты от
   пластиковости, но там градиенты крупные и насыщенные. У нас вош
   ограничен 2.5%, градиентов такого масштаба на странице нет, и зерно
   стоило бы data-URI и лишнего слоя ради неразличимого эффекта.
   ========================================================================= */

.site-bg,
.site-noise {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}


.site-bg::before,
.site-bg::after {
  content: '';
  position: absolute;
  inset: 0;
}


.site-bg::before {
  background-image:
    linear-gradient(to right, var(--line-soft) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line-soft) 1px, transparent 1px);
  background-size: 48px 48px;
  -webkit-mask-image: radial-gradient(120% 80% at 50% 0%, #000 0%, transparent 72%);
          mask-image: radial-gradient(120% 80% at 50% 0%, #000 0%, transparent 72%);
}


.site-bg::after {
  background:
    radial-gradient(48% 38% at 18% 12%, rgb(92 144 98 / 0.025), transparent 70%),
    radial-gradient(42% 34% at 82% 30%, rgb(56 189 248 / 0.025), transparent 70%),
    radial-gradient(38% 30% at 50% 88%, rgb(251 191 36 / 0.025), transparent 70%);
  filter: blur(56px);
  animation: bg-drift 34s ease-in-out infinite alternate;
}


@keyframes bg-drift {
  from { transform: translate3d(-1.5%, -1%, 0) scale(1); }
  to   { transform: translate3d(1.5%, 1%, 0) scale(1.06); }
}


/* Локальная плотность: секция рисует свою сетку поверх фоновой. Ставится
   ЧЕРЕЗ ОДНУ — плотный блок, воздух, плотный блок. Это ритм страницы, а
   не украшение: без чередования длинная страница читается однородной
   лентой. */
[data-density] { position: relative; }

[data-density]::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--line-soft) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line-soft) 1px, transparent 1px);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 22%, #000 78%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 22%, #000 78%, transparent);
}

[data-density="tight"]::before { background-size: 24px 24px; }
[data-density="open"]::before  { background-size: 96px 96px; }


/* Высота липкой шапки. Не украшение, а размер, от которого зависит, куда
   приземляется переход по якорю. Состояний ровно два: до 660px шапка
   двухрядная (бренд с кнопкой, под ними прокручиваемая строка ссылок),
   выше — однорядная. Значения снимались браузером, а не выводились из
   отступов; расхождение токена с настоящей высотой ловит
   `scripts/check-anchors.mjs`, иначе токен тихо разошёлся бы с вёрсткой. */
:root { --header-h: 62px; }

@media (max-width: 1040px) {
  :root { --header-h: 105px; }
}


html {
  /* Землю красит `html`, а не `body`: тело стало прозрачным, иначе оно
     накрыло бы фоновые слои (И4). Цвет — тот же, которым красилась
     `.landing-shell`: он и есть фон страницы, а не `--bg`. */
  /* Подложка сети, а не свой цвет: `.site-bg` красит тем же `--bg`, и два
     слоя должны совпадать, иначе на первом кадре мелькнёт чужой оттенок.
     Раньше здесь стоял `--index-hero-bg` (#282828) — светлее и теплее
     сетевого #17171c; отсюда и бралось «фон не соответствует». */
  background: var(--bg);
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-behavior: smooth;

  /* Один отступ на все якоря сайта вместо `scroll-margin-top` у каждого
     заголовка. Так было в kb.css: там стояло 64px, а мобильная шапка —
     105px, и на телефоне под ней пряталось 103 цели из 206. Ошибка была
     не в числе, а в том, что число было ОДНО на две высоты шапки. */
  scroll-padding-top: calc(var(--header-h) + var(--space-4));
}


body {
  margin: 0;
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-family);
  font-size: var(--font-base);
  font-weight: var(--weight-regular);
  line-height: var(--leading-normal);
}


h1, h2, h3, h4, p { margin-top: 0; }


h1 {
  margin-bottom: var(--space-2);
  color: var(--text-primary);
  font-size: var(--font-2xl);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
  line-height: var(--leading-tight);
}


h2 {
  margin-bottom: var(--space-3);
  color: var(--text-primary);
  font-size: var(--font-md);
  font-weight: var(--weight-semibold);
  line-height: 1.3;
}


h3 {
  margin-bottom: var(--space-3);
  font-size: var(--font-base);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}


p { color: var(--text-secondary); }


::selection {
  background: var(--accent-soft);
  color: var(--text-primary);
}


/* ЗНАК НЕ КРАСИТ ТЕКСТ. С переходом на индиго роль разошлась: `--accent`
   (#6366f1) как текст в тёмной теме даёт 3.8–4.1:1 при норме 4.5, а
   `--accent-secondary` (#818cf8) — 5.7–6.0:1. У сети это разные роли, и
   эталон красит ссылки в прозе именно вторым. На зелёном разницы почти не
   было, поэтому здесь всюду стоял знак. */
a {
  color: var(--accent-secondary);
  text-decoration: none;
  transition: color var(--transition);
}


a:hover { color: var(--accent-hover); }


button { font: inherit; color: inherit; }


:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


.u-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}


.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  min-height: 36px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--control-bg);
  color: var(--text-primary);
  cursor: pointer;
  font-size: var(--font-sm);
  font-weight: var(--weight-medium);
  line-height: 1.2;
  text-decoration: none;
  transition: background-color var(--transition), border-color var(--transition), color var(--transition);
  user-select: none;
  white-space: nowrap;
}


.btn:hover {
  background: var(--control-hover);
  border-color: var(--border-strong);
  color: var(--text-primary);
}


.btn:active { background: var(--control-active); }


.btn-primary {
  border-color: transparent;
  background: var(--accent-fill);
  color: var(--accent-color);
  font-weight: var(--weight-semibold);
}


.btn-primary:hover {
  border-color: transparent;
  background: var(--accent-hover-fill);
  color: var(--on-accent);
}


.btn-ghost {
  border-color: transparent;
  background: var(--bg-hero);
  color: var(--text-secondary);
}


.btn-ghost:hover {
  background: var(--control-hover);
  border-color: transparent;
  color: var(--text-primary);
}


.brand-logo {
  display: block;
  width: auto;
  height: 34px;
}


.brand-logo-black { display: none; }


[data-theme="proof-light"] .brand-logo-white { display: none; }


[data-theme="proof-light"] .brand-logo-black { display: block; }


/* ---- landing chrome ---- */

.landing-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* Фон снят и поднят на `html`. Непрозрачная заливка здесь накрывала
     `.site-bg` и `.site-noise` целиком — фоновая система И4 была не
     видна вовсе, и обнаружилось это только в браузере. */
  background-color: transparent;
}


.landing-shell .landing-footer { background-color: var(--bg); }


/* Правила `.skip-link` сняты 2026-08-27: разметки под них нет НИ НА ОДНОЙ
   из 17 страниц. Скип-ссылка убрана решением владельца 2026-08-20, и
   `verify.py` отдельно ловит её возвращение по строке «Skip to content».
   То есть CSS описывал элемент, который канон запрещает вернуть. Мёртвое
   правило хуже отсутствующего: следующий читатель принимает его за
   намерение и ищет пропавшую разметку. История — в git. */


.landing-header {
  position: sticky;
  top: 0;
  z-index: 40;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-5);
  width: 100%;
  margin: 0;
  padding: var(--space-3) max(40px, calc((100% - 1200px) / 2));
  background: color-mix(in srgb, var(--bg) 23%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}


/* Бренд по модели сети: знак, НАЗВАНИЕ СЛОВАМИ и разделитель после него.
   До этого в шапке стоял только знак — сайт не представлялся, и по одной
   шапке нельзя было сказать, куда попал читатель. Так устроены waf и ssl;
   разделитель отделяет имя сайта от навигации по нему. */
.landing-header-lede {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  justify-self: start;
  min-width: 0;
}


.landing-header-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-primary);
  font-size: 15px;
  font-weight: var(--weight-medium);
  letter-spacing: -0.01em;
  line-height: 1;
  white-space: nowrap;
}


.landing-header-slash {
  color: var(--line);
  font-size: 15px;
  line-height: 1;
  user-select: none;
}


.landing-header-links {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-width: 0;
}


/* Навигация набрана как у эталона: заглавными, мелко, с разрядкой. Стрелки
   ▾ сняты — у waf и ssl выпадающие меню ЕСТЬ, но стрелку они не рисуют:
   список раскрывается наведением и фокусом, а знак только шумит в строке. */
.landing-header-links a {
  min-height: 36px;
  padding: var(--space-2) var(--space-4);
  color: var(--text-secondary);
  font-size: var(--font-xs);
  font-weight: var(--weight-regular);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}


.landing-header-links a:hover { color: var(--text-primary); }


.landing-header-actions {
  display: flex;
  align-items: center;
  justify-self: end;
  gap: var(--space-2);
}


.landing-theme-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  min-width: 36px;
  min-height: 36px;
  padding: 0;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--bg-panel-soft);
  color: var(--text-secondary);
}


.landing-theme-btn:hover { color: var(--text-primary); }


.landing-theme-btn svg { width: 18px; height: 18px; }


/* Пустая кнопка не показывается. Сборщик отдаёт переключатель темы БЕЗ
   иконки — `<button ... aria-label="Toggle theme"></button>`, — а `site.js`
   подставляет её потом и подключён с `defer`. Следствий два, и второе
   задевает всех: без JavaScript в шапке навсегда остаётся пустая рамка
   36×36, а с ним она пустует при КАЖДОЙ загрузке до выполнения скрипта.

   `:empty` решает оба разом и без единой правки в разметке или скрипте:
   пока внутри ничего нет — кнопки нет; как только `site.js` вставил
   `<svg>`, она перестаёт быть пустой и появляется. Деградация верная:
   без скрипта переключатель всё равно не работал бы.

   Это B-63 сети. Там записано «правка в комплекте», и она там нужна —
   разметка чинится у источника. Здесь правка местная и в разметку не
   лезет, поэтому не расходится с комплектом: если сборщик научится
   отдавать иконку, правило просто перестанет срабатывать. */
.landing-theme-btn:empty { display: none; }


.landing-footer { padding: 0 var(--space-10) var(--space-10); }


.landing-footer-inner {
  display: grid;
  gap: var(--space-8);
  width: min(100%, 1200px);
  margin: 0 auto;
  padding-top: var(--space-10);
  border-top: 1px solid var(--border-subtle);
}


.landing-footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-10);
  align-items: start;
}


.landing-footer-col { display: grid; gap: var(--space-3); align-content: start; }


.landing-footer-tag {
  max-width: 260px;
  color: var(--text-secondary);
  font-size: var(--font-sm);
}


.landing-footer-heading {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--font-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}


.landing-footer-list {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--font-sm);
}


.landing-footer-list a { color: var(--text-secondary); font-weight: 500; }


.landing-footer-name { color: var(--text-secondary); font-weight: 400; }


.landing-footer-list a:hover { color: var(--text-primary); }


.landing-footer-desc {
  display: block;
  color: var(--text-secondary);
  font-size: var(--font-xs);
}


.landing-footer-brand {
  display: inline-flex;
  align-items: center;
}


.landing-footer-brand .brand-logo { height: 56px; }


/* --text-muted fails AA at 13px against both theme backgrounds; --text-secondary
   is the dimmest token that holds 4.5:1 in dark and light. Same for the three
   fine-print notes below. Contrast is non-negotiable here (AGENTS.md). */
.landing-footer-copy {
  color: var(--text-secondary);
  font-size: var(--font-sm);
}


/* Ссылка в строке копирайта различалась ОДНИМ цветом: #818cf8 против
   #a1a1aa вокруг — 1.16:1 по яркости при норме 3:1, когда цвет остаётся
   единственным различием (WCAG 1.4.1). Подчёркивание такое же, как у
   ссылок в прозе статей (`patterns/kb-article.css`), чтобы приём был один
   на весь сайт. Слой выбран сознательно: подвал сквозной, а `home.css`
   статьи не грузят. */
.landing-footer-copy a:not([class]) {
  text-decoration: underline;
  text-underline-offset: 2px;
}


@keyframes indexFadeUp {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}


.index-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
}


.index-actions .btn {
  min-height: 46px;
  padding: 0 var(--space-6);
  font-size: var(--font-base);
}


.index-actions-note {
  order: 5;
  flex-basis: 100%;
  color: var(--text-secondary);
  font-size: var(--font-sm);
}


/* Слой оформления героя-картинки снят 2026-08-27 вместе с законом
   наложения снимка. Решение владельца по В-8: страница остаётся схемной,
   фотографий и рендеров не будет. Правый столбец героя занимает схема
   сигнала, и картинке там места нет — значит правила описывали то, что не
   вернётся. Два неиспользованных PNG уведены в `docs/assets-source/`:
   каталог исключён из выкладки, файлы сохранены. Живым из семейства
   остался `.index-bridge-wrap` — обёртка схемы, она в `home.css`. */


/* Theme swap mirrors the .brand-logo pattern — CSS-toggled, no JS src rewrite. */


/* 48-unit viewBox rendered into a 20px tile: scale the strokes back up to the
   ~1.7-at-24 weight the sibling lucide icons carry. */
.index-iso-glyph--48 {
  --iso-stroke-strong: 3.4;
  --iso-stroke-faint: 2.2;
}


.origin-cases { margin-top: calc(var(--space-8) + var(--space-2)); }


.index-check-list {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}


.index-check-list li {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  gap: var(--space-3);
  color: var(--text-secondary);
  font-size: var(--font-sm);
  line-height: 1.55;
}


.index-check-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-top: 2px;
  border-radius: var(--radius-pill);
  background: var(--success-soft);
  color: var(--success);
}


.index-check-mark svg { width: 12px; height: 12px; }


/* ---- flagship ---- */

.index-flagship {
  padding: var(--space-8);
  border: 1px solid color-mix(in srgb, var(--accent) 50%, var(--border-subtle));
  border-radius: var(--radius-xl);
  background: color-mix(in srgb, var(--accent-soft) 55%, var(--bg-panel-soft));
}


/* ---- origin-controls demo panel ---- */

/* Load-bearing: the demo's fields carry display:grid, which outranks the UA's
   [hidden] { display: none }


/* ---- switch — ported from the dashboard's uiSwitch (imports/client/main.css
   .switch/.switch-track). Saving/saved states are dropped: nothing persists here.
   The track transition carries the whole animation, so no JS class flags. ---- */

.switch {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
  width: 44px;
  height: 24px;
}


/* Input drives a >=32px hit area without changing the visual track. */
.switch input {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  min-height: 32px;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  transform: translateY(-50%);
}


.switch .switch-track {
  width: 100%;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--control-active);
  transition: background-color var(--transition);
  pointer-events: none;
}


.switch .switch-track::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--text-secondary);
  transition: translate var(--transition), background-color var(--transition);
}


.switch input:checked + .switch-track { background: var(--accent); }


.switch input:checked + .switch-track::after {
  translate: 20px 0;
  background: var(--bg);
}


.switch input:focus-visible + .switch-track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


/* ---- drawbridge toggle — ported verbatim from the dashboard's uiBridge
   (imports/client/ui/bridge.css). Open ±48° / --text-muted, closed flat /
   --accent, 1280ms. .is-closed drives the visual, never input:checked. ---- */

.bridge {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 52px;
  height: 40px;
  cursor: pointer;
}


.bridge input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}


.bridge input:focus-visible + .bridge-visual .bridge-icon {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}


.bridge .bridge-visual {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}


.bridge .bridge-icon {
  display: block;
  /* 32×24 viewBox → 4:3 so the long bridge fills the box */
  width: 44px;
  height: 33px;
  flex: 0 0 auto;
  color: var(--text-muted);
  transition: color 120ms linear;
}


.bridge.is-closed .bridge-icon {
  color: var(--accent-secondary);
}


.bridge:hover:not(.is-animating):not(.is-closed) .bridge-icon {
  color: var(--text-primary);
}


.bridge:hover:not(.is-animating).is-closed .bridge-icon {
  color: var(--accent-hover);
}


.bridge .bridge-leaf {
  transition: none;
  will-change: transform;
}


.bridge .bridge-leaf-left {
  transform: rotate(-48deg);
  transform-box: view-box;
  transform-origin: 10px 16px;
}


.bridge .bridge-leaf-right {
  transform: rotate(48deg);
  transform-box: view-box;
  transform-origin: 22px 16px;
}


.bridge.is-closed .bridge-leaf-left,
.bridge.is-closed .bridge-leaf-right {
  transform: rotate(0deg);
}


.bridge.is-animating .bridge-leaf {
  transition: transform var(--bridge-anim-ms, 1280ms) cubic-bezier(0.22, 1, 0.36, 1);
}


.bridge.is-animating .bridge-icon {
  transition: color var(--bridge-anim-ms, 1280ms) linear;
}


/* ---- claim strip — a lighter sibling of .index-flagship ---- */

.index-panel {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  max-width: 860px;
  margin: 0 auto calc(var(--space-10) + var(--space-2));
  padding: var(--space-6);
  border: 1px solid color-mix(in srgb, var(--warning) 34%, var(--border-subtle));
  border-radius: var(--radius-xl);
  background: color-mix(in srgb, var(--warning-soft) 40%, var(--bg-panel-soft));
}


.index-panel-body { display: grid; gap: var(--space-2); min-width: 0; }


.index-panel-title {
  color: var(--text-primary);
  font-size: var(--font-lg);
  font-weight: 600;
  line-height: 1.3;
}


.index-panel-desc {
  color: var(--text-secondary);
  font-size: var(--font-sm);
  line-height: 1.6;
}


/* ---- migration ---- */

.index-migration {
  padding: var(--space-8);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-panel-soft);
}


.index-migration-list {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}


.index-migration-list--stack { grid-template-columns: 1fr; gap: var(--space-3); }


.index-migration-list--stack .index-migration-step {
  grid-template-columns: 24px minmax(0, 1fr);
  align-items: start;
  column-gap: var(--space-3);
  padding: var(--space-4);
  background: var(--bg-panel-soft);
}


.index-migration-list--stack .index-migration-step::before { grid-row: 1 / span 2; }


.index-migration-foot {
  color: var(--text-secondary);
  font-size: var(--font-sm);
}


.index-migration-step {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--bg-elevated);
}


.index-migration-step::before {
  counter-increment: step;
  content: counter(step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent-secondary);
  font-size: var(--font-xs);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}


.index-migration-step-title {
  color: var(--text-primary);
  font-size: var(--font-base);
  font-weight: var(--weight-semibold);
  line-height: 1.3;
}


.index-migration-step-desc {
  color: var(--text-secondary);
  font-size: var(--font-sm);
  line-height: 1.5;
}


/* ---- compare table ---- */

.index-compare-panel {
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-elevated);
}


.index-compare-scroll {
  display: block;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  contain: inline-size;
}


.compare-table {
  width: 100%;
  min-width: 560px;
  border-collapse: collapse;
  font-size: var(--font-sm);
}


.compare-table th,
.compare-table td {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  vertical-align: top;
}


.compare-table thead th {
  background: var(--bg-panel-soft);
  color: var(--text-secondary);
  font-size: var(--font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}


.compare-table tbody th {
  color: var(--text-primary);
  font-weight: 600;
}


.compare-table tbody td {
  color: var(--text-secondary);
}


.compare-table tbody tr:last-child th,
.compare-table tbody tr:last-child td {
  border-bottom: 0;
}


.compare-table .compare-us {
  background: color-mix(in srgb, var(--accent-soft) 45%, transparent);
  color: var(--text-primary);
  font-weight: var(--weight-medium);
}


.compare-table thead .compare-us { color: var(--accent-secondary); }


.compare-check {
  width: 14px;
  height: 14px;
  margin-right: var(--space-2);
  vertical-align: -2px;
  color: var(--success);
}


/* ---- responsive tail — must stay last ---- */

@media (max-width: 1080px) {


  .index-migration-list:not(.index-migration-list--stack) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


@media (max-width: 900px) {
  .index-flow-grid { grid-template-columns: 1fr; }


  .landing-header {
    gap: var(--space-4);
    padding-inline: var(--space-6);
  }


  .landing-header-links a { padding-inline: var(--space-3); }

/* Пункты прокручиваемой строки НЕ сжимаются. Без этого флекс ужимал
   первый пункт до 57px, а ссылка внутри оставалась 95px и вылезала на
   38 пикселей ПОВЕРХ соседней: на телефоне «How it works» и «Engines»
   читались слипшимися, и палец попадал не туда, куда целился.

   Сжимался ровно первый — остальным ширины хватало, — поэтому дефект и
   не бросался в глаза в измерениях: ни переполнения страницы, ни
   обрезанного содержимого, ни провала контраста. Нашлось просмотром
   мобильного кадра. */
.landing-header-links > * { flex-shrink: 0; }


  .landing-footer {
    padding: 0 max(var(--space-4), env(safe-area-inset-left, 0px))
      max(var(--space-8), env(safe-area-inset-bottom, 0px))
      max(var(--space-4), env(safe-area-inset-right, 0px));
  }
}


/* Печатный лист верстается как узкий экран. Лист A4 — 794 CSS-пикселя, а
   двухколоночные раскладки складываются ниже 760: бумага попадает ровно в
   зазор, узкая для двух колонок и широкая для складывания. Герой из-за
   этого печатался колонками 350px и 300px, вырастал до 1430px при листе в
   1123 и целиком уезжал со второго листа, оставляя первый пустым. Поэтому
   все девять блоков «сложить в одну колонку» распространены на `print`. */
@media (max-width: 760px), print {


  .index-migration-list:not(.index-migration-list--stack) { grid-template-columns: 1fr; }


  .index-flagship,
  .index-migration { padding: var(--space-6); }


  .landing-footer-grid { grid-template-columns: 1fr; gap: var(--space-8); }
}


/* Порог ИЗМЕРЕН, а не унаследован. Здесь стояло 660 — число из эталона, у
   которого в шапке нет названия сайта словами. У нас оно есть, и строка
   перестаёт помещаться гораздо раньше: двоичным поиском по ширине окна
   строка целая на 1001px и ломается на 1000. Двоичный поиск, впрочем,
   соврал: он опирается на монотонность, а частый обход шагом 20px нашёл
   наложения ещё на 1001 и 1020. Порог взят по обходу — 1040.

   Ломалась она молча и некрасиво: у `.landing-header-lede` стоит
   `min-width: 0`, что разрешает колонке сжаться ниже содержимого, а у
   бренда `white-space: nowrap` — переноситься ему нечем, и он выливался
   ПОВЕРХ навигации. На 768px «Bridge IndexNow» лежало на «HOW IT WORKS»,
   наложение 143px. Ни одна проверка не возражала: страница вбок не едет,
   за родителя ничего не вылезает, текст не обрезан — два узла просто
   занимают одно место. Нашлось просмотром печатного листа, где ширина
   бумаги как раз попадает в этот диапазон. */
@media (max-width: 1040px) {
  .landing-header {
    grid-template-columns: auto 1fr;
    grid-template-areas: 'brand actions';
    gap: var(--space-3);
    padding-block: var(--space-3);
    padding-inline: var(--space-4);
  }


  .landing-header-lede { grid-area: brand; }
  .landing-header-slash { display: none; }


  .landing-header-links { display: none; }


  .landing-header-actions { grid-area: actions; }
}


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


  *,
  *::before,
  *::after {
    animation: none !important;
    transition-duration: 0.01ms !important;
  }


  .landing-header {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--bg);
    border-bottom: 1px solid var(--border-subtle);
  }


  .bridge.is-animating .bridge-leaf,
  .bridge.is-animating .bridge-icon {
    transition: none !important;
  }
}


/* Ключевые факты страницы: короткие пары «что / значение» под лидом. */
.kb-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-10);
  padding: 0;
  list-style: none;
}

.kb-fact {
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-panel-soft);
}

.kb-fact-label {
  display: block;
  margin-bottom: var(--space-1);
  /* не --text-muted: он не проходит AA для мелкого текста (B-37) */
  color: var(--text-secondary);
  font-size: var(--font-xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.kb-fact-value {
  color: var(--text-primary);
  font-size: var(--font-sm);
  line-height: 1.45;
}

/* Мобильные ширины для внутренних страниц.
   У эталона `--index-page` переопределяется на 1080 и 760 ТОЛЬКО внутри
   `.index-homepage` — обёртки лендинга. Страницы KB её не носят (иначе
   `overflow: hidden` убил бы липкое оглавление), поэтому те же
   переопределения продублированы на `:root`. Без них на телефоне
   оставалось 80 px полей с каждой стороны. */
@media (max-width: 1080px) {
  :root { --index-page: min(100% - 64px, 1180px); }
}


@media (max-width: 760px), print {
  :root { --index-page: min(100% - 32px, 1180px); }
}


/* Навигация по разделам на телефоне.
   Эталон прячет ссылки шапки на узком экране — ему можно: он
   одностраничник, и ссылки там якорные. У нас это переходы между коконами,
   и прятать их значит оставить читателя без навигации до самого подвала.
   Поэтому вместо `display: none` — строка с горизонтальной прокруткой под
   брендом. Порог тот же измеренный 1000, что и у раскладки выше. */
@media (max-width: 1040px) {
  .landing-header {
    grid-template-columns: auto 1fr;
    grid-template-areas: 'brand actions' 'links links';
    row-gap: var(--space-2);
  }

  .landing-header-links {
    display: flex;
    grid-area: links;
    gap: var(--space-1);
    /* Базовое правило центрирует полосу — на широком экране это верно, а на
       узком содержимое шире контейнера, и центрирование выносит НАЧАЛО за
       левый край. Долистать туда нельзя: scrollLeft уже 0. Первый раздел
       («Errors») просто переставал существовать для посетителя. */
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    margin-inline: calc(var(--space-1) * -1);
    /* Полоса листается, но об этом ничто не сообщало: на 320px «Authorities»
       резался посреди слова и выглядел не как продолжение, а как обрезка.
       Затухание у конечного края читается как «дальше есть ещё».
       Отступ равен ширине затухания: когда разделы помещаются целиком,
       градиент ложится на пустоту и ничего не прячет. */
    padding-inline-end: 1.5rem;
    -webkit-mask-image: linear-gradient(to right, #000 0,
                        #000 calc(100% - 1.5rem), transparent 100%);
    mask-image: linear-gradient(to right, #000 0,
                #000 calc(100% - 1.5rem), transparent 100%);
  }

  .landing-header-links::-webkit-scrollbar { display: none; }

  .landing-header-links a {
    flex: 0 0 auto;
    padding-inline: var(--space-3);
    font-size: var(--font-xs);
  }
}


/* --- Выпадающее меню разделов в шапке ---------------------------------
   Пункт кокона, у которого есть страницы, раскрывается по наведению и по
   фокусу с клавиатуры. Открывается на CSS, без единой строки JS: меню и
   так строится на сборке, и добавлять ради него скрипт незачем. */

.nav-item {
  position: relative;
  display: flex;
  align-items: center;
}

.nav-item > a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1, 4px);
}


.nav-menu {
  position: absolute;
  top: 100%;
  left: 50%;
  z-index: 50;
  display: grid;
  gap: var(--space-1, 4px);
  min-width: 260px;
  max-width: 340px;
  margin: 0;
  padding: var(--space-2, 8px);
  list-style: none;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 12px);
  background: var(--bg-panel);
  box-shadow: var(--shadow-lg, 0 18px 48px rgb(0 0 0 / 0.5));
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 4px);
  transition: opacity var(--transition, 0.18s ease),
              transform var(--transition, 0.18s ease),
              visibility var(--transition, 0.18s ease);
}

.nav-item:hover .nav-menu,
.nav-item:focus-within .nav-menu {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
}


/* Закрытое клавишей Escape перебивает раскрытие по наведению. Стоит ИМЕННО
   ниже, и это не стилистика: специфичность у обоих правил одинакова — (0,3,0),
   потому что `:hover` считается за класс, как и атрибут. Сначала я поставил это
   правило выше, посчитав его специфичность выше на единицу, и Escape не
   закрывал ничего: признак ставился, а меню оставалось видимым.

   Признак вешает `js/site.js`, там же довод: WCAG 1.4.13 требует способа убрать
   заслоняющее меню, не уводя указатель и не меняя фокус. Без скрипта правило
   просто не срабатывает, и поведение остаётся прежним. */
.nav-item[data-dismissed] .nav-menu {
  opacity: 0;
  visibility: hidden;
}


/* Регистр и разрядка гасятся ЯВНО. Правило полосы навигации выше набирает
   ссылки заглавными, и оно протекает внутрь меню: «DOES INDEXNOW SUBMIT TO
   GOOGLE? NO, AND HERE IS WHY» кричало и переносилось на две строки.
   Заглавные уместны для семи коротких слов в строке, а не для названий
   статей. Эталон гасит их так же. */
.nav-menu a {
  display: block;
  padding: var(--space-2, 8px) var(--space-3, 12px);
  border-radius: var(--radius-md, 8px);
  color: var(--text-secondary);
  font-size: var(--font-sm);
  line-height: 1.35;
  letter-spacing: normal;
  text-transform: none;
  text-decoration: none;
}

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

   Текущий пункт ВЫДЕЛЕН, а не притенён. Напрашивается притенить его
   (opacity, --text-muted), но выше по файлу прямо записано: --text-muted
   не держит AA на 13px ни в одной из тем, а opacity поверх --text-secondary
   уводит вниз и его. «Вы здесь» одинаково читается и через усиление —
   при этом контраст растёт, а не падает. */
.nav-menu-network li[aria-current] {
  padding: var(--space-2, 8px) var(--space-3, 12px);
  border-radius: var(--radius-md, 8px);
  color: var(--text-primary);
  font-size: var(--font-sm);
  line-height: 1.35;
  font-weight: 500;
}
.nav-menu a:hover,
.nav-menu a:focus-visible {
  background: var(--bg-panel-soft);
  color: var(--text-primary);
}

/* Мост под меню: курсор не проваливается в зазор между пунктом и списком. */
.nav-item::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  height: var(--space-3, 12px);
}

@media (max-width: 1040px) {
  /* На узком экране раскрытие не нужно: пункты и так прокручиваются
     строкой, а вложенное меню поверх узкой полосы только мешает. Порог
     общий с раскладкой шапки. */
  .nav-menu { display: none; }
}


/* Ссылка на машиночитаемые документы: требование внешнего стандарта —
   `llms.txt` должен быть достижим из подвала, а не только по прямому
   адресу. Тон подписи, не акцента. */
.landing-footer-links {
  margin: 0 0 var(--space-2, 8px);
  font-size: var(--text-sm, 0.875rem);
  color: var(--text-secondary, #b8b0a6);
}
.landing-footer-links a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.landing-footer-links a:hover { color: var(--text-primary, #f2e9dd); }


/* ---- реестр оговорок ---- */

.index-ledger {
  display: grid;
  gap: var(--space-6);
  max-width: 900px;
}


.index-ledger-list {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-elevated);
}


.index-ledger-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-4);
  padding: var(--space-5) var(--space-6);
}


.index-ledger-row + .index-ledger-row { border-top: 1px solid var(--border-subtle); }


.index-ledger-num {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border: 1px solid color-mix(in srgb, var(--accent-secondary) 40%, var(--border-subtle));
  border-radius: var(--radius-sm);
  background: var(--index-tone-teal-soft);
  color: var(--index-tone-teal-fg);
  font-family: var(--font-mono);
  font-size: var(--font-sm);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}


.index-ledger-body { display: grid; gap: var(--space-2); min-width: 0; }


.index-ledger-title {
  color: var(--text-primary);
  font-size: var(--font-lg);
  font-weight: 600;
}


.index-ledger-desc {
  color: var(--text-secondary);
  font-size: var(--font-md);
  line-height: 1.6;
}


/* ---- цепочка: кто что решает ---- */

.index-chain { display: grid; gap: var(--space-6); max-width: none; }


.index-chain-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}


/* Первый шаг делает платформа, три следующих решает поисковая система. Это и
   есть содержание раздела, поэтому оно выражено формой, а не только текстом. */
.index-step {
  position: relative;
  display: grid;
  gap: var(--space-2);
  align-content: start;
  padding: var(--space-5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-panel-soft);
}


.index-step--you {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border-subtle));
  background: var(--bg-elevated);
  box-shadow: var(--shadow-sm);
}


.index-step-num {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-pill);
  background: var(--index-tone-muted-soft);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--font-xs);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}


.index-step--you .index-step-num {
  background: var(--index-tone-green-soft);
  color: var(--index-tone-green-fg);
}


.index-step-title {
  color: var(--text-primary);
  font-size: var(--font-lg);
  font-weight: 600;
}


.index-step-desc {
  color: var(--text-secondary);
  font-size: var(--font-base);
  line-height: 1.6;
}


/* ---- адаптив новых разделов ---- */

/* Цепочка из четырёх шагов в один ряд перестаёт читаться раньше, чем ломается:
   на 1080 колонки сужаются до слипшихся столбцов текста. Два на два — предел. */
@media (max-width: 1080px) {
  .index-chain-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


@media (max-width: 760px), print {
  .index-ledger-row { padding: var(--space-4) var(--space-5); }
}


/* ============================================================================
   Инфографика и движение
   ----------------------------------------------------------------------------
   Схемы нарисованы разметкой, а не картинками: они переключают тему вместе со
   страницей, тянутся под любую ширину и не стоят ни одного лишнего запроса.
   Всё движение целиком снимается в `prefers-reduced-motion` в конце файла —
   ни одна схема не теряет при этом смысла, движение только подчёркивает то,
   что уже выражено формой.
   ========================================================================= */

/* ---- шапка: состояние прокрутки и полоса продвижения ---- */

/* Ведёт прокруточная шкала CSS, а не слушатель. До И4 это делал скрипт:
   на каждый кадр он писал `--scroll-progress` в стиль элемента и дёргал
   пересчёт стилей, а полоса продвижения анимировалась через `width` —
   то самое свойство, которое бюджет движения запрещает трогать (только
   `transform`, `opacity`, `filter`, `clip-path`, `stroke-dashoffset`).
   Теперь обе анимации живут вне главного потока.

   ДЛИННЫЕ СВОЙСТВА, НЕ ШОРТКАТ. `animation: nav-solid linear both`
   сбросил бы `animation-range` в `normal`, и правило с меньшей
   специфичностью его уже не вернёт. Ловушка §4.6 плана. */

.landing-header {
  border-bottom: 1px solid transparent;
}


@keyframes nav-solid {
  from {
    background: transparent;
    border-bottom-color: transparent;
    box-shadow: none;
  }
  to {
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    border-bottom-color: var(--border-subtle);
    box-shadow: 0 8px 24px -20px rgb(0 0 0 / 0.55);
  }
}


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

   Растёт `transform: scaleX`, а не `width`: ширина заставляет пересчитывать
   раскладку на каждом кадре, трансформ — нет. */
.landing-header::after {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  bottom: -1px;
  height: 2px;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 55%, transparent),
    var(--accent-secondary));
}


@keyframes read-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}


@supports (animation-timeline: scroll()) {
  .landing-header {
    animation-name: nav-solid;
    animation-timeline: scroll(root block);
    animation-range: 0 40px;
    animation-fill-mode: both;
    animation-timing-function: linear;
  }

  .landing-header::after {
    animation-name: read-progress;
    animation-timeline: scroll(root block);
    animation-fill-mode: both;
    animation-timing-function: linear;
  }
}


@keyframes sd-flow { to { stroke-dashoffset: -28; } }


@keyframes sd-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(1.55); }
}


/* Растёт `transform`, а не `height`. Бюджет движения §4.6 плана называет
   `width`/`height`/`top` запрещёнными поимённо, и здесь это стоило не буквы:
   бесконечный луп на трёх узлах делал каждый пересчёт раскладки впятеро
   дороже. Измерено дважды на вариант, шесть секунд после загрузки, 1440px:

     как есть     LayoutDuration +22.1мс и +19.6мс
     без анимации LayoutDuration  +3.5мс и  +3.9мс

   Число пересчётов при этом одинаково (48–49) — дорожал каждый, а не
   добавлялись новые. На B-79 (3347мс под замедлением) это не тянет, и я его
   туда не записываю.

   1.4615 — это 38/26, прежние высоты. Начало отсчёта снизу, потому что у
   контейнера `.tl-zone--future` стоит `align-items: flex-end` и засечки
   росли вверх. Единственное видимое отличие: скругление 2px по вертикали
   тоже растягивается, на полосе шириной 3px это неразличимо. */
@keyframes tl-tick {
  0%, 55%, 100% { opacity: 0.25; transform: scaleY(1); }
  20% { opacity: 1; transform: scaleY(1.4615); }
}


/* ---- появление при прокрутке ---- */

/* Ведёт `animation-timeline: view()`, а не IntersectionObserver.

   ТРИ ВЕЩИ, КОТОРЫЕ НАДО ЗНАТЬ, ПРЕЖДЕ ЧЕМ ТРОГАТЬ ЭТОТ БЛОК.

   1. Анимируется СОДЕРЖИМОЕ, а не сам блок. (История: `.index-counters`,
      снятая 09.09.2026 в пользу полосы знаков `.index-mark`, была собрана
      встык: `gap: 1px` плюс фон-разделитель у контейнера. Прозрачная или
      сдвинутая ячейка пропускает этот фон насквозь жирной полосой, и это
      читается как сломанная вёрстка. До И4 скрипт анимировал именно
      ячейку — дефект был на живой странице.
   2. Это СКРАБ, а не одноразовый показ: блок, ушедший обратно под нижнюю
      кромку, снова гаснет. Цена отказа от скрипта, принята сознательно.
   3. Стартовое состояние объявлено ТОЛЬКО внутри `@supports`. Где
      браузер шкалы не знает, содержимое видно сразу и ничего не прячется.
      Снаружи этого блока `opacity: 0` не появляется нигде.

   Ступенчатости через `animation-delay` нет намеренно: прокруточные шкалы
   его не читают. Ступень возникает сама — соседи входят в кадр не
   одновременно. */

@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}


@supports (animation-timeline: view()) {
  .index-section-header > *,
  .index-verdict > *,
  .index-ledger-statement > *,
  .index-timeline > *,
  .tl-clause > *,
  .tl-manual > *,
  .index-tier > *,
  .index-fit > *,
  .pl-side > *,
  .sq-step > *,
  .index-mech-panel > *,
  .index-parity-item > *,
  .index-mark,
  .logbook,
  .index-final > * {
    animation-name: rise;
    animation-timeline: view();
    animation-range: entry 0% entry 40%;
    animation-fill-mode: both;
    animation-timing-function: linear;
  }
}


/* ---- печать ---- */

/* СНАЧАЛА — СПЛОШНОЕ СНЯТИЕ АНИМАЦИЙ, и оно куплено дефектом.
   В настоящем рендере PDF анимация не идёт вовсе, и элемент с
   `animation-fill-mode: both` остаётся в НУЛЕВОМ кадре. У героя это
   `indexFadeUp` с прозрачностью 0 — и весь первый лист печатался ПУСТЫМ:
   ни заголовка, ни панели, ни кнопок, только шапка.

   Почему это не поймала проверка печати. Она переводит страницу в
   `media: print` уже ПОСЛЕ загрузки, когда анимация отыграла и
   прозрачность равна единице. Настоящая печать берёт нулевой кадр.
   Разницу видно только в самом PDF, и найдена она была именно так.

   Правило намеренно сплошное: в печати движения нет по определению, а
   перечислять селекторы значит однажды забыть новый. Снятие анимации
   возвращает элемент к БАЗОВОМУ стилю, где прозрачность не задана, —
   поэтому отдельный `opacity: 1` здесь не нужен и не ставится. */
@media print {
  *,
  *::before,
  *::after { animation: none !important; }

  /* Кнопки печатаются КОНТУРОМ. Браузер по умолчанию не печатает фоны, а
     основная кнопка держится на заливке: почти белая надпись
     (`--accent-color`) на неотпечатанном фоне превращается в белое на
     белом, и главный призыв страницы исчезает с листа. Проверено в
     настоящем PDF — не в `emulateMedia`, где фон ещё виден.

     `print-color-adjust: exact` не годится: он заставляет печатать все
     заливки, включая тёмную тему целиком, и это решение читателя, а не
     страницы. */
  .btn,
  .btn-primary,
  .btn-ghost {
    background: none !important;
    color: var(--text-primary) !important;
    border: 1px solid var(--border-strong) !important;
  }

  /* Переключатель темы с листа убран. Правило выше делает из него рамку с
     глифом — орган управления, который на бумаге нажать нельзя, и который
     стоит рядом с настоящим призывом, отбирая у него внимание. Остальная
     шапка печатается: бренд и названия разделов на бумаге работают как
     происхождение листа, а переключатель — нет. */
  .landing-theme-btn { display: none !important; }

  /* Разрывы страниц. Без этих трёх правил лист рвётся там, где кончилось
     место, а не там, где кончилась мысль: заголовок остаётся сиротой
     внизу листа, карточка из четырёх строк разъезжается на два листа, а
     блок, который не поместился целиком, уезжает весь — оставляя
     полстраницы пустоты. Замечено в настоящем PDF на листе с разделом
     про Google. */
  h1, h2, h3 { break-after: avoid; }

  .tk-step,
  .cp-item,
  .mv-card,
  .index-faq-item,
  .wt-card,
  .tl-clause,
  .pl-node,
  .kb-point { break-inside: avoid; }

  /* Строку журнала и запись таблицы рвать нельзя тем более: половина
     записи на одном листе и половина на другом — это не запись. */
  .lb-table tr,
  .rt-table tr { break-inside: avoid; }
}



/* У печати прокруточной шкалы нет. Без этого блока всё, до чего читатель
   не долистал, уходит в PDF прозрачным — и точно так же уезжает снимок
   страницы целиком. Ловушка §4.6 плана, третья из трёх. */

@media print {
  .landing-header,
  .landing-header::after,
  .index-section-header > *,
  .index-verdict > *,
  .index-ledger-statement > *,
  .index-timeline > *,
  .tl-clause > *,
  .tl-manual > *,
  .index-tier > *,
  .index-fit > *,
  .pl-side > *,
  .sq-step > *,
  .index-mech-panel > *,
  .index-parity-item > *,
  .index-mark,
  .logbook,
  .index-final > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}


/* ---- движение снимается целиком ---- */

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

@media (prefers-reduced-motion: reduce) {
  .landing-header,
  .landing-header::after,
  .index-section-header > *,
  .index-verdict > *,
  .index-ledger-statement > *,
  .index-timeline > *,
  .tl-clause > *,
  .tl-manual > *,
  .index-tier > *,
  .index-fit > *,
  .pl-side > *,
  .sq-step > *,
  .index-mech-panel > *,
  .index-parity-item > *,
  .index-mark,
  .logbook,
  .index-final > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .landing-header {
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    border-bottom-color: var(--border-subtle);
  }

  .site-bg::after { animation: none; }
}

@keyframes sd-flow { to { stroke-dashoffset: -28; } }

/* ============================================================================
   ФОНОВАЯ СИСТЕМА СЕТИ — перенос из bridge-waf-static

   Узлы `.site-bg` и `.site-noise` стояли в разметке всё это время и не имели
   НИ ОДНОГО правила: замер показывал прозрачные слои нулевого действия, а
   фон держался плоской заливкой `html`. Здесь восстановлены все четыре
   уровня эталона.

   1. `.site-bg` — фиксированная подложка под всем, `z-index: -2`.
   2. `::before` — сетка 48px, погашенная радиальной маской сверху: она
      читается у шапки и растворяется к середине экрана.
   3. `::after` — три радиальных свечения (акцент, глубина, тепло) под
      сильным размытием, медленно дрейфующие. Это то «живое», чего у
      страницы не было: фон дышит, не отвлекая.
   4. `.site-noise` — зерно поверх, `mix-blend-mode: overlay` в тёмной и
      `multiply` в светлой.

   Слои фиксированные и `pointer-events: none` — на прокрутку и клики не
   влияют. При `prefers-reduced-motion` дрейф снимается ниже.
   ========================================================================= */
.site-bg,
.site-noise {
  position: fixed;
  inset: 0;
  pointer-events: none;
}


.site-bg {
  z-index: -2;
  background: var(--bg);
}


.site-bg::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, var(--line-soft) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line-soft) 1px, transparent 1px);
  background-size: 48px 48px;
  -webkit-mask-image: radial-gradient(ellipse 90% 55% at 50% 0%, #000 35%, transparent 100%);
          mask-image: radial-gradient(ellipse 90% 55% at 50% 0%, #000 35%, transparent 100%);
}


.site-bg::after {
  content: "";
  position: absolute;
  inset: -25%;
  opacity: 0.42;
  background:
    radial-gradient(620px circle at 18% 12%, var(--accent-soft), transparent 62%),
    radial-gradient(760px circle at 86% 44%, var(--depth), transparent 62%),
    radial-gradient(520px circle at 42% 88%, var(--warn-soft), transparent 60%);
  filter: blur(48px);
  animation: bg-drift 34s var(--ease-io) infinite alternate;
}


:root[data-theme="proof-light"] .site-bg::after { opacity: 0.4; }


@keyframes bg-drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(-4%, 3%, 0) scale(1.09); }
}


.site-noise {
  z-index: -1;
  opacity: 0.028;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}


:root[data-theme="proof-light"] .site-noise {
  opacity: 0.05;
  mix-blend-mode: multiply;
}


/* ============================================================================
   ПЕРЕХОДЫ МЕЖДУ СЕКЦИЯМИ — тоже из эталона

   Линия в один пиксель, гаснущая к краям, и по ней при прокрутке проходит
   короткая вспышка акцента. У waf таких десять на главной; у нас секции были
   разделены только отступами.
   ========================================================================= */
.rule-horizon {
  position: relative;
  height: 1px;
  border: 0;
  margin: 0;
  background: linear-gradient(90deg, transparent, var(--line) 18%, var(--line) 82%, transparent);
}


.rule-horizon::after {
  content: "";
  position: absolute;
  inset: -1px auto auto 0;
  height: 3px;
  width: 26%;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  opacity: 0;
  filter: blur(2px);
}


/* Вспышка объявлена ВНУТРИ `@supports`, как в эталоне, и это не
   формальность. Без поддержки прокруточных шкал `animation: … both`
   отыграла бы один раз при загрузке — вспышка прошла бы по всем линиям
   разом, независимо от того, куда смотрит читатель. Внутри `@supports`
   такой браузер не получает анимации вовсе и видит просто линию. */
@supports (animation-timeline: view()) {
  .rule-horizon::after {
    animation: rule-sweep linear both;
    animation-timeline: view();
    animation-range: entry 10% cover 30%;
  }
}


@keyframes rule-sweep {
  0%   { transform: translateX(-40%); opacity: 0; }
  25%  { opacity: 0.75; }
  100% { transform: translateX(300%); opacity: 0; }
}


/* Совсем узкие экраны: в ряду «знак + название | действия» название словами
   не помещается и налезало на кнопки — на 320px пересечение 31px, на 340px
   11px. Нашёл частый обход по ширинам (check-header-fit), четыре
   контрольные точки его не видели.

   Скрывается ТОЛЬКО слово: знак остаётся, а `aria-label` на самой ссылке
   держит доступное имя целиком, так что для экранного диктора шапка не
   меняется. */
@media (max-width: 380px) {
  .landing-header-name { display: none; }
}
