/* ============================================================================
   МТК-29 — стили приложения. Зона `app`.

   Границы, которые здесь держатся:
   — цвет ТОЛЬКО из design-core (tokens.css); сырого hex нет ни одного,
     полупрозрачности — через канальные токены rgba(var(--x-rgb), a);
   — метрики берутся из кита с ЗАПАСОМ: var(--touch-min, 64px). Для цвета
     запас запрещён (маскирует опечатку токена второй палитрой), для метрики
     обязателен — он единственное, что спасает тач-цель при отсутствующем
     токене. Снятие запаса «по правилу» — само нарушение;
   — :hover НЕ используется нигде: киоск тач, состояние наведения на нём
     залипает и читается как выбранное. Только :active;
   — активное состояние — латунь (--active), не красная заливка.

   МАСШТАБ. Вёрстка авторская в 1920x1080 логических (инвариант ядра
   проекта), поэтому числа в calc() заданы под 1920, а не под 3840, как
   в README кита. На киоске 4K оператор ставит scale.ui=2 / scale.content=2 —
   это и есть «масштаб x2 через --ui-scale». Расхождение с каноном 38-42
   намеренное и заявлено оркестратору.
   ============================================================================ */

/* ТАЧ-ГЕОМЕТРИЯ — китовая, своей нет. Решено dvn 2026-08-16, вариант A
   (COORDINATION §6b): `--touch-min` 64 (любой интерактив) · `--touch-primary`
   96 (навигация и режимы) · `--touch-gap` 16, все уже завязаны на --ui-scale.
   Токена `--touch-hit` в вендоре не существует, и писать его нельзя: `var()`
   без запаса гасит объявление ЦЕЛИКОМ (min-height: auto), тач-цель исчезает
   без единой ошибки в консоли. Второй тач-геометрии поверх китовой не заводим.
   И всё равно цель меряется ПОПАДАНИЕМ, а не стилями: честный min-height 64
   у зоны `simbirsk` давал фактические 65 px, и по стилям это не видно. */

:root {
  /* Кегли контента под 1920. Пол — из кита, чтобы регуляторы оператора
     и режим слабовидящих срабатывали сами. */
  --t-body: max(var(--type-min-label, 24px), calc(24px * var(--content-scale, 1)));
  --t-caption: max(var(--type-min-label, 24px), calc(20px * var(--content-scale, 1)));
  --t-tile: max(var(--type-min-ui, 32px), calc(26px * var(--content-scale, 1)));
  --t-h1: calc(52px * var(--content-scale, 1));
  --t-h2: calc(40px * var(--content-scale, 1));
  /* ⚠️ `--t-h3` ИСПОЛЬЗОВАЛСЯ, НО НЕ БЫЛ ОБЪЯВЛЕН — и стрелка лайтбокса
     рисовалась унаследованным кеглем: объявление `font-size` гасло
     целиком, `var()` без запаса не «берёт значение по умолчанию»,
     он делает свойство недействительным. Ряд продолжен: 52 / 40 / 32. */
  --t-h3: calc(32px * var(--content-scale, 1));

  --gap: calc(24px * var(--content-scale, 1));
}

/* Сброс модели бокса — ТОЛЬКО в своём пространстве имён. В вендоре
   (kiosk.css, kiosk-core.css, tokens.css) box-sizing не задан вовсе, и хром
   ядра свёрстан без него: глобальный `*` перекроил бы чужую вёрстку из
   нашего файла — правка, верная для нас и неверная для всех остальных
   потребителей. Поймано замером: у заглушки плитки width:100% + рамка 2px
   давали 305 px в боксе 301. */
.p29,
.p29 * {
  box-sizing: border-box;
}

/* Видимый скролл — всегда, не auto-hide: скрытый скролл означает, что
   посетитель не догадается о продолжении списка. Полоса — КИТА (18 px латунью,
   `--scrollbar-w`), своих правил полосы нет.
   🔴 СНЯТЫ `scrollbar-width`/`scrollbar-color` и свои `::-webkit-scrollbar*`
   (приёмка design d2ac955, README ядра 1.29.2): Chrome ≥ 121, увидев любое
   стандартное свойство не в `auto`, игнорирует ВСЕ `::-webkit-scrollbar`-правила
   и рисует системную полосу — латунная полоса кита у нас не стояла никогда. */
.kiosk-scroll {
  overflow-y: auto;
}

/* ------------------------------------------------------------------ раздел */

/* Отступы — от зон хрома ядра, не хардкодом: оператор двигает навигацию из
   настроек, в a11y таргеты растут, в раскладке «стрелки по бокам» занята
   ширина, а не высота. */
.p29 {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  padding: var(--chrome-top, 64px) var(--chrome-right, 64px)
           var(--chrome-bottom, 80px) var(--chrome-left, 64px);
  color: var(--text-on-dark);
  font-family: var(--font-text);
}

/* `.p29__head` снят 2026-08-25 (ход «мёртвые стили», ограда с него
   снята координатором: `design` подтвердила, что класс остался только
   в её пробах `venn-probe.html` и `design29-preview.js`, а на приёмке
   она их не открывает). Разбор — ниже, у сетки `.chr.has-map`. */

/* Nolde — только заголовочный уровень. Ниже него он нечитаем. */
.p29__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--t-h1);
  font-weight: 400;
  line-height: 1.1;
  color: var(--text-on-dark);
}

.p29__count {
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

/* ═══ ЧИПЫ АКТИВНОГО ОТБОРА (задание №20)
   Видимый ответ на «чем отфильтровано». Пришедший глубокой ссылкой
   (вход из Симбирска) иначе видит 10 записей вместо 72 и не знает почему:
   критерий живёт в лупе, а лупа закрыта.
   ⚠️ Крестик НЕ отдельная цель: тап-целью служит весь чип, поэтому его
   высота — китовый `--touch-min`, а не кегль подписи. Крестик внутри
   объясняет, что будет, а не даёт вторую цель рядом с первой. */
/* Мост в шапке Симбирска: тот же вид, что в карточке партии, но там он
   стоит в колонке и тянется на её ширину, а здесь — в строке рядом
   с тумблером. Ширину снимаем, прижимаем вправо. */
.lr__persons.p29-campgo {
  width: auto;
  margin-left: auto;
}

.p29__chips {
  display: flex;
  flex-wrap: wrap;
  gap: calc(10px * var(--content-scale, 1));
  margin-top: calc(8px * var(--content-scale, 1));
}

.p29__chips:empty { display: none; }

.p29-chip {
  display: inline-flex;
  align-items: center;
  gap: calc(10px * var(--content-scale, 1));
  min-height: var(--touch-min, 64px);
  padding: 0 calc(18px * var(--content-scale, 1));
  border: 1px solid rgba(var(--brass-rgb), 0.45);
  border-radius: calc(6px * var(--content-scale, 1));
  background: rgba(var(--paper-rgb), 0.06);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(24px * var(--content-scale, 1)));
  cursor: pointer;
}

.p29-chip__n { color: var(--active); }
.p29-chip__x { color: var(--active); }

.p29-chip:active {
  background: rgba(var(--brass-rgb), 0.22);
  border-color: var(--active);
}

.p29__body {
  flex: 1 1 auto;
  min-height: 0; /* иначе flex-элемент не даёт вложенному скроллу сжаться */
}

/* `grid-auto-rows: min-content` ОБЯЗАТЕЛЕН, а не косметика. У сетки с
   определённой высотой (height:100% внутри flex-колонки) ряды `auto`
   получились по 120 px при содержимом плитки 387: плитки молча
   переклеивались поверх соседних — overflow у них visible, и глазом это
   читается как «плотная вёрстка», а не как дефект. Замер: строки
   gridTemplateRows = «120px 120px …» при scrollHeight 369 и clientHeight 118. */
.p29__grid {
  height: 100%;
  display: grid;
  /* Минимум колонки ОБЯЗАН масштабироваться вместе с содержимым. Голое
     `minmax(300px, 1fr)` на киоске 4K при content=2 дало 10 колонок по
     324 px при плитке высотой 972 — сливер вместо карточки, и на 1920
     этого не видно вовсе. Замер снят в разрешении киоска, не в окне. */
  /* ⚠️ ЧИСЛО КОЛОНОК ЗДЕСЬ НЕ ЖИВЁТ. Минимум колонки задаёт НАСТРОЙКА
     ОПЕРАТОРА `tileMin`, и `applySettings` пишет `grid-template-columns`
     ИНЛАЙНОМ — то есть любое значение отсюда перебивается молча.
     Я на этом попалась: завела токен `--p29-col-min`, он честно
     вычислялся в 540 px, а на экране оставалось восемь колонок вместо
     шести. Ровно тот же случай, что мёртвый `--crop-anchor`: сперва
     проверь, нет ли уже своего механизма. Поразделу число задаётся
     умолчанием настройки (`opts.tileMin` в `scenes/*.js`).
     Значение ниже — запас на случай, если сцена смонтирована без
     настроек; в норме до него не доходит. */
  grid-template-columns: repeat(auto-fill, minmax(calc(300px * var(--content-scale, 1)), 1fr));
  grid-auto-rows: min-content;
  gap: calc(19px * var(--content-scale, 1));
  align-content: start;
  padding-right: var(--scrollbar-w, 18px);
}

/* -------------------------------------------------------------- плитка */

/* ОПРАВА КАРТОЧКИ СТИЛЯ А (макеты `design/section-A-persons|states.html`).
   Изображение идёт ВРОВЕНЬ С КРОМКОЙ, подписи — в своём поле `__meta`:
   поэтому у самой плитки поля больше нет, а `overflow: hidden` обязателен,
   иначе кадр и краевая лента вылезут за скруглённый угол. */
.p29-tile {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Плитка заведомо крупнее минимума, но нижняя граница задана явно —
     сжатие сетки не должно молча увести её под норму кита. */
  min-height: var(--touch-min, 64px);
  background:
    linear-gradient(180deg, rgba(var(--amber-black-rgb), 0.14), rgba(var(--amber-black-rgb), 0.30));
  border: calc(1px * var(--content-scale, 1)) solid rgba(var(--brass-rgb), 0.24);
  border-radius: calc(6px * var(--content-scale, 1));
  text-align: left;
}

.p29-tile__meta {
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 4);
  padding: calc(15px * var(--content-scale, 1)) calc(16px * var(--content-scale, 1))
           calc(16px * var(--content-scale, 1));
}

/* Только :active — hover в киоске запрещён. */
.p29-tile:active {
  background: rgba(var(--brass-rgb), 0.22);
  border-color: var(--active);
}

/* ── ПРЕДМЕТНЫЙ ЦВЕТ КАРТОЧКИ ────────────────────────────────────────────
   Атрибут на корне плитки говорит ЧЕМ КРАСИТЬ, а `camp-mark` внутри —
   КАКОЙ ЛАГЕРЬ. Значение одно и то же поле данных, разойтись нечему.
   Источник цвета — единственный блок предметного слоя выше, своих
   значений здесь нет. */
.p29-tile[data-camp="red"]          { --camp-c: var(--camp-red); }
.p29-tile[data-camp="white"]        { --camp-c: var(--camp-white); }
.p29-tile[data-camp="national"]     { --camp-c: var(--camp-national); }
.p29-tile[data-camp="rev-dem"]      { --camp-c: var(--camp-rev-dem); }
.p29-tile[data-camp="green"]        { --camp-c: var(--camp-green); }
.p29-tile[data-camp="intervention"] { --camp-c: var(--camp-intervention); }
.p29-tile[data-camp="uprising"]     { --camp-c: var(--camp-uprising); }

/* Высота задана ЧИСЛОМ, а не через aspect-ratio. Замер поймал схлоп:
   `aspect-ratio` на flex-элементе колонки + `height:100%` у ребёнка дают
   круговую зависимость, и Chromium разрешает её нулём — бокс 301x0 при
   картинке, которая «есть». Ноль по одной оси не виден глазом (плитка
   выглядит просто плотной) и глушит целый класс дальнейших замеров. */
/* 🔒 ЯКОРЬ ПЛИТКИ НАЗВАН ПОРАЗДЕЛЬНО — «в центр ПРЕДМЕТА, а не файла».
   Общего верного якоря нет: у портрета предмет в верхней трети, у листа
   карты — поле без заголовка и легенды, у документа предмет занимает
   весь лист. Поэтому решение принимается разделом, а `--anchor` объявляет,
   что оно ПРИНЯТО: `50% 50%` по умолчанию и `50% 50%`, выбранное для
   документа, машинно неразличимы, и гейт хода 4 требует именно выбора.

   Замер `design`: плитки 4:5 срезают сверху медианно 7 % (персоналии)
   и 4 % (гособразования) — рамка свою работу делает, форму не меняем,
   меняем только точку кропа. */
.p29.is-persons .p29-tile__img,
.p29.is-states .p29-tile__img {
  /* 🔒 Слово dvn 2026-08-24: «фото персонажей обрезаны сверху, так нельзя».
     Было `50% 25%` — компромисс, и он всё равно РЕЗАЛ. Замер на живых
     70 портретах: кадрируются 49, медиана среза сверху 3.4 % кадра,
     МАКСИМУМ 10 %. Десять процентов сверху у поясного портрета — это
     темя, и на музейном фонде читается как брак, а не как кадрирование.
     `50% 0%` уводит весь срез вниз: сверху не режется НИЧЕГО.
     ⚠️ Низ при этом режется сильнее — это осознанный размен: у портрета
     низ кадра обычно пиджак и фон, верх — лицо. Замер после правки:
     срез сверху 0 у всех 49. */
  object-position: 50% 0%;
  --anchor: portrait-top;
}

.p29.is-parties .p29-tile__img {
  /* Центр — ВЫБОР, а не умолчание: символика и групповые снимки — кадр,
     где предмет занимает его целиком, и якорь к верху срезал бы низ
     ни за что. */
  object-position: 50% 50%;
  --anchor: subject-centre;
}

.p29-tile__media {
  flex: 0 0 auto;
  height: calc(190px * var(--content-scale, 1));
  overflow: hidden;
  background: rgba(var(--amber-black-rgb), 0.35);
}

/* 🔴 РАМКА 4:5 — ТОЛЬКО ПЕРСОНАЛИЯМ И ГОСОБРАЗОВАНИЯМ. Решение dvn
   2026-08-17: плиток в кадре 15 → 10, кроп со сдвигом отклонён.

   Селектор идёт через МОДИФИКАТОР РАЗДЕЛА (`.p29.is-persons`), а не
   поверх общего `.p29-tile__media`, и это не аккуратность, а условие
   решения: у событий медиа горизонтальное (медиана аспекта 1.414),
   4:5 срезало бы 43 % ширины, а у 13 записей из 36 без изображения
   заглушка выросла бы вдвое — раздел стал бы сеткой пустых
   прямоугольников (`docs/design/events-01.md`).

   Основание рамки: у персоналий вертикальны 62 портрета из 70, макушка
   срезана у 69 из 70 при нынешней ландшафтной полосе; у гособразований
   вертикальных 61 %.

   `height: auto` обязателен ПЕРВЫМ: без него `aspect-ratio` проигрывает
   явной высоте выше и правило умирает молча — ровно «мёртвое
   переопределение выглядит рабочим». */
.p29.is-persons .p29-tile__media,
.p29.is-states .p29-tile__media {
  height: auto;
  aspect-ratio: 4 / 5;
}

.p29-tile__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ── КРАЕВАЯ ЛЕНТА ЛАГЕРЯ ────────────────────────────────────────────────
   🔒 Условие координатора и правка dvn, доехавшая до макета
   (`f7e8ec0`, «камп-полоса → тонкая краевая лента»): полоса идёт
   ВДОЛЬ ПРАВОЙ КРОМКИ, а НЕ поперёк портрета. Косая через кадр — это
   та же обрезка головы, только диагональю.
   Ширина в ПРОЦЕНТАХ, а не в пикселях: «10 % кадра» — это и есть
   формулировка условия, и при 6 колонках у Персоналий и 5
   у Гособразований одно и то же число пикселей дало бы разную долю.
   Лента вылезает за края (`top/bottom: -8%`) намеренно — скос срезал бы
   её углы внутри бокса; удерживает `overflow: hidden` на плитке. */
.p29-tile[data-camp] .p29-tile__media::after {
  content: "";
  position: absolute;
  top: -8%;
  bottom: -8%;
  right: calc(-4px * var(--content-scale, 1));
  width: 10%;
  background: var(--camp-c, var(--window));
  opacity: 0.95;
  transform: skewX(-15deg);
  z-index: 1;
}

.p29-tile__media { position: relative; }

/* МОНОГРАММА — под заглушкой, а не вместо кадра. Видна ровно тогда, когда
   виден честный отказ («не собрано» / «изображения нет»): рядом с живым
   кадром её нет, и подменить его она не может по устройству. */
.p29-tile__mono {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: calc(111px * var(--content-scale, 1));
  line-height: 1;
  color: var(--camp-c, var(--text-muted));
  opacity: 0.55;
  pointer-events: none;
}

/* ⚠️ ПРИЗНАК «КАДРА НЕТ» СТАВИТ JS КЛАССОМ, А НЕ `:has()` В СЕЛЕКТОРЕ.
   Первая редакция была `.p29-tile__media:has(.p29-tile__noimg)`. Проверила:
   `:has()` не использует НИ ВЕНДОР, ни остальная зона — эти две строки
   оказались бы единственными на весь офлайн-киоск. Отказ такой опоры
   молчаливый: селектор просто не совпадёт, монограмма не появится
   никогда, и это не отличить от «так и задумано». Разметка и без того
   знает, нарисовала она кадр или заглушку, — пусть и говорит. */
.p29-tile__media.is-noimg .p29-tile__mono { display: flex; }

/* Заглушка поверх монограммы — её текст обязан читаться, а не тонуть
   в вензеле. */
.p29-tile__media.is-noimg .p29-tile__noimg {
  position: relative;
  z-index: 2;
  background: transparent;
}

/* Заглушка обязана ВЫГЛЯДЕТЬ недоделанной: пунктир, а не аккуратная
   серая плашка. Молчаливая подмена хуже честной дырки. */
.p29-tile__noimg {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  text-align: center;
}

/* 🔴 ДВЕ ЗАГЛУШКИ РАЗНОГО СМЫСЛА — РАЗНОЙ ФОРМЫ, а не только разного
   текста. Находка зоны `design` (`docs/design/events-01.md`): у событий
   13 записей из 36 БЕЗ `lead_media`, и после сборки медиа треть раздела
   осталась бы с заглушкой, выглядящей временной.

   `is-pending` — состояние СТЕНДА, пройдёт: пунктир, он обещает
   продолжение, и обещание здесь верное.
   `is-absent` — состояние ДАННЫХ, не пройдёт: сплошная рамка и заливка,
   законченная форма без обещания. Пунктир у неё был бы ложным обещанием
   — та же молчаливая подмена, что затычка, выглядящая готовой.

   Цвет обоих — из интерфейсных токенов (`--telegrey`), предметного слоя
   здесь нет: это состояние поставки, а не сущность экспозиции. */
.p29-tile__noimg.is-pending {
  border: 2px dashed rgba(var(--telegrey-rgb), 0.45);
}

.p29-tile__noimg.is-absent {
  border: 1px solid rgba(var(--telegrey-rgb), 0.28);
  background: rgba(var(--telegrey-rgb), 0.08);
}

/* 🔒 NOLDE СНЯТ НИЖЕ ЗАГОЛОВОЧНОГО УРОВНЯ — РЕШЕНИЕ dvn 2026-08-25:
   «давай не нарушать дизайн-канон, но исправление делаем малой кровью».
   Исключения канону не будет; меняется ГАРНИТУРА, а не раскладка —
   кегли, ритм, цвета, ленты и бейджи остаются как были.

   🔴 И ВОТ ПОЧЕМУ КАНОН ПРАВ, ЧИСЛОМ, А НЕ НА ВКУС. Замер обеих гарнитур
   на живой странице (canvas, один кегль 100 px, шрифты загружены):

       Nolde     «х» 43.4×75.9   «Х» 43.4×75.9   ← ОДИН И ТОТ ЖЕ ГЛИФ
                 «о» 43.9×77.0   «О» 43.9×77.0
       21 Cent   «х» 53.8×45.8   «Х» 76.7×72.2   ← строчная и прописная

   **У Nolde НЕТ СТРОЧНЫХ.** Что бы ни лежало в данных, на экране выходит
   капитель: «Троцкий Л. Д.» рисуется как «ТРОЦКИЙ Л. Д.». У капители
   прямоугольная огибающая — у всех слов одинаковая, и опознать слово
   по силуэту нельзя, только прочитать по буквам. На заголовке это цена
   в одно слово, на 70 плитках — в семьдесят.
   Отсюда же становится понятно, почему канон формулирует запрет
   по УРОВНЮ, а не по кеглю: дело не в мелкости, а в том, что ниже
   заголовка текст читают, а не опознают.

   ⚠️ ЧТО НЕ ТРОГАЕТСЯ И ПОЧЕМУ: монограмма-фолбэк (111 px) и типо-плитки
   лонгрида (64 и 120 px) — это ОДНА БУКВА КАК ГРАФИКА, а не текст;
   опознавать там нечего, читать нечего. Заголовочные уровни
   (`--t-h1`/`--t-h2`, масштхед, буквица-номер) остаются Nolde по канону. */
.p29-tile__title {
  margin: 0;
  font-family: var(--font-text);
  font-size: max(var(--type-min-ui, 32px), calc(28px * var(--content-scale, 1)));
  font-weight: 600;
  line-height: 1.1;
  color: var(--paper);
}

/* ── МЕТКИ НАД ИМЕНЕМ ────────────────────────────────────────────────────
   Бейдж-аббревиатура и метка «карта» стоят НАД именем: карточка
   гособразований — «имя-вперёд», и метки отвечают на «что это за запись»
   прежде, чем читается само имя. */
.p29-tile__marks {
  display: flex;
  align-items: center;
  gap: calc(var(--gap) / 3);
  flex-wrap: wrap;
}

/* 🔴 БЕЙДЖ: КРОМКА ЛАГЕРНАЯ, БУКВЫ БУМАЖНЫЕ. Макет красит лагерем и то
   и другое; по замеру (числа в блоке подписи лагеря ниже) пять цветов
   из семи не проходят как текст. Кромка — не текст, её контраст меряется
   иначе, и лагерь на бейдже читается ровно так же ясно. */
.p29-tile__badge {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-ui, 32px), calc(28px * var(--content-scale, 1)));
  line-height: 1;
  color: var(--paper);
  border: calc(2px * var(--content-scale, 1)) solid var(--camp-c, var(--brass));
  border-radius: calc(4px * var(--content-scale, 1));
  padding: calc(4px * var(--content-scale, 1)) calc(12px * var(--content-scale, 1));
}

/* Метка «карта» — сигнальный красный, и это ЕДИНСТВЕННЫЙ его случай
   на плитке: карта у 7 записей из 59, то есть метка редка по данным,
   а не сделана редкой оформлением. Бумага на сигнальном даёт 7.20 : 1 —
   замерено, а не принято на веру. */
.p29-tile__onmap {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(13px * var(--content-scale, 1)));
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper);
  background: var(--signal);
  padding: calc(4px * var(--content-scale, 1)) calc(10px * var(--content-scale, 1));
}

/* Даты — латунью: в макете это единственный акцент на карточке, и он
   отвечает за «когда», то есть за ось всего комплекса. */
.p29-tile__dates {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(21px * var(--content-scale, 1)));
  letter-spacing: 0.04em;
  color: var(--active);
}

/* 🔴 ПОДПИСЬ ЛАГЕРЯ ОСТАЁТСЯ ПРИГЛУШЁННЫМ ТЕКСТОМ, А НЕ КРАСИТСЯ ЛАГЕРЕМ,
   И ЭТО ЗАМЕР, А НЕ ОСТОРОЖНОСТЬ. Макет ставит `color: var(--c)`.
   Контраст лагерных цветов на фоне карточки (`#2F383E`), порог 4.5:
     красный 1.56 · интервенция 1.32 · рев-дем 2.02 · восстания 2.54
     зелёный 3.09 · белый 7.73 ✓ · национальные 6.13 ✓
   Пять из семи не проходят, причём хуже всех — самый частый лагерь.
   Поэтому цвет несёт ЗАЛИВКА (краевая лента и плашка `camp-mark`),
   а слово остаётся телесерым: ровно та же развязка, что принята
   в Хронике для цвета линий, и та же конвенция, что уже действует
   в проекте для предметного цвета. */
.p29-tile__camp {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(16px * var(--content-scale, 1)));
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Незаконченное состояние — точечный сигнал, а не заливка. */
.p29-tile__stub {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--signal);
}

/* ------------------------------------------------------------- карточка */

.p29-card {
  position: absolute;
  inset: 0;
  /* Выше липкого заголовка месяца (`.chr-month`, z-index 1). Без этого он
     пробивает карточку насквозь: оба позиционированы в одном контексте
     наложения, и z-index:1 бьёт z-index:auto. Видно было не как «месяц
     поверх текста», а как ПЕРЕХВАЧЕННОЕ нажатие по строке рельса —
     поймала проба попаданием, глазом читалось как случайный промах. */
  z-index: 5;
  display: flex;
  align-items: stretch;
  justify-content: center;
  padding: var(--chrome-top, 64px) var(--chrome-right, 64px)
           var(--chrome-bottom, 80px) var(--chrome-left, 64px);
  background: rgba(var(--amber-black-rgb), 0.82);
}

.p29-card[hidden] {
  display: none;
}

/* 🔒 ЛИСТ — БЕЛЫЙ И ПО ЦЕНТРУ, НЕ ВО ВЕСЬ ЭКРАН. Заявка `design`
   (`card-request-app.md`, `d3e56b0`) по бренду §12. Прежде лист был
   тёмной плашкой на 3840×2160 — экран проглатывался целиком, фирменный
   мотив вокруг не читался.

   🔢 ЧИСЛА — В БАЗЕ (1920×1080), на экран их множит `--content-scale`.
   Спек `design` даёт экранные при масштабе 2, поэтому здесь ровно
   половина: лист 3320×2004 экранных = 1660×1002 базовых. Это та самая
   грабля «1125»: чужое число без единиц однажды уже уехало вдвое. */
.p29-card__sheet {
  position: relative;
  display: grid;
  /* фото · косая полоса лагеря · текст · связанное */
  /* Размеры листа — из макета `design/section-A-card.html`, делённые
     на 1.35: лист 3400×1900 → 2519×1407, колонки 1040·140·1fr·660 →
     770·104·1fr·489, зазор 52 → 38, поля 60 → 44, радиус 14 → 10.
     ⚠️ ВЫСОТА КЛАМПИТСЯ, И ЭТО НЕ НЕДОДЕЛКА. Макет кладёт оверлей на весь
     кадр 2160, поверх хрома ядра; у нас лист живёт ВНУТРИ бокса сцены
     (2160 − 190 верх − 214 низ = 1756), потому что закрывать собой панели
     ядра нельзя — на них язык, поиск и сервис. Отсюда `min(100%, …)`:
     на 4K высота упирается в бокс, пропорции колонок сохраняются. */
  /* 🔴 КОЛОНОК ТЕПЕРЬ ТРИ, А БЫЛО ЧЕТЫРЕ: полоса лагеря перестала быть
     колонкой и ушла за содержимое диагональю (правка dvn 25.08, макет
     `design/section-A-card.html` @ `f0ac625`). Мера прежняя — числа макета
     делятся на 1.35: колонки 1040·1fr·720 → 770·1fr·533, зазор 64 → 47. */
  /* 🔴 КОЛОНКИ — ДОЛИ, А НЕ ФИКСИРОВАННЫЕ ПИКСЕЛИ, И ЭТО ЧИНИТ a11y.
     Пропорция та же самая — `770 : 1034 : 533` из макета, — но записана
     долями, поэтому не зависит от того, влез лист в свою натуральную
     ширину или упёрся в кап.
     Диагноз `design` замером обоих режимов: лист капнут `min(100%, …)`,
     а фикс-колонки капу не подчиняются. В a11y `2519 × 2.025 = 5101`
     упирается в 3712 (вьюпорт − 128), фикс-фото и фикс-рельс уже съели
     1559 + 1079, и **весь дефицит 5101 − 3712 поглощал `1fr`** — читаемая
     строка падала до 703 px. Обычный режим не капнут вовсе (3400 —
     натуральная ширина), поэтому ломался ТОЛЬКО a11y.
     📌 Записано как `770fr 1034fr 533fr`, а не `0.745fr 1fr 0.516fr`:
     это те же доли без округления, и числа читаются как числа макета. */
  grid-template-columns:
    minmax(0, 770fr)
    minmax(0, 1034fr)
    minmax(0, 533fr);
  gap: calc(47px * var(--content-scale, 1));
  width: min(100%, calc(2519px * var(--content-scale, 1)));
  height: min(100%, calc(1407px * var(--content-scale, 1)));
  min-height: 0;
  margin: auto;
  padding: calc(44px * var(--content-scale, 1));
  border-radius: calc(10px * var(--content-scale, 1));
  /* 🔴 ЛИСТ ТЁМНЫЙ, А БЫЛ БУМАЖНЫМ. Довод dvn 25.08: карточка — оверлей
     над затемнённым разделом, белый лист «нечестен» и выбивается.
     ⚠️ Макет называет цвет сырым hex `#333c44`, и брать его сюда нельзя —
     интерфейсный слой красится только токенами. Но брать и не нужно:
     `#333c44` = (51,60,68), а графит (67,80,89) × 0.76 = (51,61,68).
     То есть это САМ ГРАФИТ, приглушённый на 24 % чёрным, — и так оно
     и записано, двумя брендовыми токенами без восьмого цвета.
     Зачем вообще темнее раздела: подложка ядра в центре кадра — ровно
     графит (радиальный градиент там даёт `rgba(67,80,89,.34)` поверх
     графита же), и лист в чистый графит СЛИЛСЯ БЫ с фоном под собой.
     ⚠️ Роль «текст на светлом» больше не нужна — заявка `design` на неё
     снимается вместе с белым листом. */
  background-color: var(--graphite);
  background-image:
    linear-gradient(rgba(var(--amber-black-rgb), 0.24), rgba(var(--amber-black-rgb), 0.24));
  color: var(--text-on-dark);
  border: 1px solid rgba(var(--brass-rgb), 0.4);
  /* 🔴 `overflow: hidden` ЗДЕСЬ ОБЯЗАТЕЛЕН, И ЕГО НЕ БЫЛО. Полоса лагеря
     нарочно длиннее листа на 178 базовых сверху и снизу — иначе диагональ
     обрывалась бы на кромке. Пока она была колонкой, её держал
     `overflow: hidden` самой колонки; став фоновой фигурой листа, она
     стала торчать НАРУЖУ: замер 239 px сверху и снизу, верхний край
     полосы на y = −49, то есть предметный цвет выезжал на хром ядра.
     ⚠️ В первой редакции этого блока я написала, что лист «и без того»
     обрезает по скруглению, — и это было неправдой: `overflow` у листа
     стоял `visible`. Поймала проба («переполнение», 2016 > 1776), а не
     глаз: на экране полоса тонкая и по краям листа почти не читается. */
  overflow: hidden;
}

/* 🔴 НОМЕР КОЛОНКИ У КАЖДОГО РЕБЁНКА — ЯВНО. Дефект `design` 2026-08-19,
   бил все 36 событий и любую запись без лагеря: у события лагеря нет,
   полоса уходит в `hidden` и ВЫПАДАЕТ ИЗ ПОТОКА ГРИДА, а три оставшихся
   ребёнка с `grid-column: auto` съезжают влево на одну колонку.
   Замер до правки: текст 150 (ширина полосы!), связанное 1292 — колонка
   текста схлопывалась в полоску по слову в строке.

   ⚠️ Скрытый элемент не «занимает своё место» — он его теряет, и
   соседи занимают его молча. Автоматическое размещение выглядит
   надёжным ровно до первого необязательного элемента. */
.p29-card__photo { grid-column: 1; min-width: 0; min-height: 0; overflow: hidden; }
/* 🔴 КОЛОНКА ФОТО ПРОКРУЧИВАЕТСЯ, КАК ДВЕ СОСЕДНИЕ (№25, 28.09).
   Замер по 18 справкам событий с 2+ снимками: у ДЕСЯТИ сетка 2×2
   не влезала в колонку и БЕЗ строки листания — срез до 485 px, то есть
   5–6 миниатюр, из которых нижние посетителю не достать и видны
   половинкой. Строка листания добавила бы ~96 px и ещё три справки.
   `overflow: hidden` здесь не был решением: это гигиена грид-ребёнка
   (`min-height: 0`), а правило листа от `design` — «скроллы ВНУТРИ
   колонок»; текст и связанное прокручиваются, фото — нет.
   ⚠️ Вернуть прежнее — снять эту строку и класс `kiosk-scroll`
   в `card-view.js`; тогда срез вернётся числом выше. */
.p29-card__photo { overflow-y: auto; overflow-x: hidden; }
.p29-card__main { grid-column: 2; }
.p29-card__rail { grid-column: 3; }
.p29-card__photo[hidden] { display: none; }

/* 🔴 НЕТ СНИМКОВ — НЕТ И ДОРОЖКИ ПОД НИХ. `display: none` уносит элемент,
   но НЕ трек: ширины заданы явным `grid-template-columns`, и пустая колонка
   держала поле молча. Замер: у записи без медиа слева от текста стояло
   1104 px пустоты (трек 1039.5 + зазор 63.45), проба звала это «31 % ширины
   листа в пустых дорожках».
   ⚠️ Это НЕ регресс широких колонок: до них трек был 675 (19.8 % — под
   порогом правила), и дыра просто молчала. Правило не изменилось, изменилась
   её громкость.
   📌 Намерение было объявлено давно — в `card-view.js` стоит комментарий
   «пустая колонка фото не должна держать ширину… а не рисует белую дыру»,
   — и КОД ЕГО НЕ ИСПОЛНЯЛ. Здесь код приводится к объявленному.
   Задето 9 записей продукта: 1 событие, 3 гособразования, 5 партий.

   ⚠️ Класс ставит JS, а не `:has()`: у скрытого соседа состояние читается
   только селектором, который в киоске не проверялся ни разу, а цена молчания
   у грида — схлопнутая раскладка. Признак живой (у него есть правила),
   а не крючок на будущее. */
.p29-card__sheet.is-nophoto {
  grid-template-columns:
    minmax(0, 1034fr)
    minmax(0, 533fr);
  /* 🔴 ЛИСТ СУЖАЕТСЯ РОВНО НА СНЯТОЕ, И БЕЗ ЭТОЙ СТРОКИ ПРАВКА ХУЖЕ
     БОЛЕЗНИ. Первая редакция снимала только трек: ширина листа оставалась
     2519 базовых, освободившиеся 770 + 47 уходили в `1fr`, и замер показал,
     что дыра не исчезла, а ПЕРЕЕХАЛА — 1104 px слева стали 904 px справа
     от текста (строка упирается в свой потолок 1180 базовых и дальше
     не растёт). Хуже того, у `__main` растянулись дети БЕЗ потолка:
     скан листовки в `__extra` встал на 2497 px при нативе 1066 — ×2.34,
     и проба назвала это растяжкой растра. Дыры не стало, появился
     раздутый документ.
     Поэтому лист сужается на 770 + 47 = 817 базовых: 2519 → 1702.
     Ни пустого поля, ни растяжки — колонки те же 1394 и 720, что у записи
     со снимком, а лист centred (`margin: auto`). */
  /* 🔴 `max-width`, А НЕ `width`, И ЭТО НЕ СТИЛИСТИКА. Объявленное `width`
     на этом листе НЕ ДЕЙСТВУЕТ в a11y: замер давал 3712 при объявленных
     `min(100%, 1702 × 2.025) = 3447`. Лист — флекс-элемент (`.p29-card`
     это flex row), и ширину ему задаёт не `width`. `max-width` даёт ровно
     объявленное в обоих режимах.
     ⚠️ Мою первую гипотезу — автоминимум флекс-элемента — `design`
     опровергла подстановкой: `min-width: 0` не меняет ничего. Я её
     и не выдавала за доказанную, но записываю снятой, чтобы её
     не проверяли заново.
     🔢 Что это даёт: лист в a11y 3712 → 3447, читаемая строка 2268 → 2003.
     Это НЕ потеря выигрыша хода №4, а его доводка: 2268 при кегле 49 px
     — 79 знаков в строке, потолок читаемости низкого зрения; 2003 — около
     70, комфортная зона. У строки есть потолок, а не только пол, и в режиме
     для слабовидящих длинная строка ХУЖЕ: глаз теряет начало следующей. */
  max-width: min(100%, calc(1702px * var(--content-scale, 1)));
}
.p29-card__sheet.is-nophoto .p29-card__main { grid-column: 1; }
.p29-card__sheet.is-nophoto .p29-card__rail { grid-column: 2; }

/* Содержимое — НАД полосой. Полоса ушла из потока, и без явного слоя
   она легла бы поверх текста: у неё `position: absolute`, у соседей
   порядок документа. */
.p29-card__photo,
.p29-card__main,
.p29-card__rail { position: relative; z-index: 1; }

/* 🔒 КОСАЯ ПОЛОСА ЛАГЕРЯ — ДЛИННАЯ ДИАГОНАЛЬ ЧЕРЕЗ ВЕСЬ ЛИСТ.
   Правка dvn 25.08: короткая колонка посередине «выглядела странно»;
   в оригинале полоса длинная и уместна. Была отдельной КОЛОНКОЙ грида
   (ширина 104), стала фоновым акцентом за содержимым.

   ⚠️ Прежнее предупреждение `design` («skew выметает фигуру на ±190 px
   в текст») НЕ снято, оно исполнено иначе: раньше фигуру держал
   `overflow: hidden` у колонки, теперь — такой же у самого листа.
   ⚠️ Его пришлось ЗАВЕСТИ: у листа стоял `visible`, и полоса торчала
   наружу на 239 px в обе стороны. Разбор — там же, у `.p29-card__sheet`.

   🔴 МЕСТО ПОЛОСЫ — ИЗ МАКЕТА, И ЭТО НЕ ЛЕНЬ. Сперва я вывела его
   «правильно»: поле + колонка 1 + половина зазора − половина ширины,
   то есть по центру щели между колонками. Вышло 809 базовых против
   макетных 932 (1258 ÷ 1.35) — полоса встала в жёлоб. Но `design`
   поставила её НЕ в жёлоб, а на 123 базовых правее, за краем текстовой
   колонки: диагональ должна проходить ПОД текстом, а не между блоками.
   Вывод из вёрстки был стройнее замысла и молча его отменял; координатор
   так и написал — «бери из макета как есть».
   Отсчёт идёт от левого края листа, а левое поле и первая колонка
   фиксированы, поэтому число устойчиво к сужению листа.

   🔒 ЦВЕТ — ЧИСТЫЙ ПРЕДМЕТНЫЙ, приглушение ТОЛЬКО прозрачностью.
   `design` держала полосу через `color-mix(лагерь → чёрный)` и сама
   от этого отказалась: цвет менялся по длине, и предметный цвет
   становился двумя. Линтер такого не ловит — он сверяет объявления,
   а не то, во что они превращаются на градиенте. */
.p29-card__camp {
  position: absolute;
  z-index: 0;
  top: calc(-178px * var(--content-scale, 1));
  bottom: calc(-178px * var(--content-scale, 1));
  /* 🔴 ПОЛОСА УШЛА ИЗ-ПОД ТЕКСТА В ФОТО-КОЛОНКУ. Слово dvn 25.08:
     «полосу сдвинем под картинки, а то текст так себе читается — под
     картинки, но чтобы просвечивала под рамками картинок, но была при
     этом под картинками». Спека `design`, прототип проверен ею живьём.
     932 → 540 базовых: полоса встаёт в фото-колонку (207…977 базовых).
     `z-index: 0` и `pointer-events` НЕ трогаем — полоса и так позади
     содержимого (у него z 1), значит ляжет ЗА фото и просветит в зазорах
     рамок миниатюр. `opacity: .6` тоже остаётся: над текстом полосы
     больше нет, и прозрачность перестала быть вопросом читаемости.
     ⚠️ Прежнее число 932 было «из макета, а не из вёрстки» и специально
     защищено разбором выше — оно отменено НЕ выводом из вёрстки,
     а новым макетом: `design` перемерила прототип на стенде.
     🔢 Точный left 500…580 подбирается на приёмке глазом: сколько
     полосы видно в зазорах, зависит от числа фотографий у записи. */
  left: calc(540px * var(--content-scale, 1));
  width: calc(56px * var(--content-scale, 1));
  pointer-events: none;
}

/* 🔴 СУЖЕНИЯ КОЛОНОК У ЗАПИСИ БЕЗ ЛАГЕРЯ БОЛЬШЕ НЕТ, И ВОЗВРАЩАТЬ ЕГО
   НЕЛЬЗЯ. Здесь стоял вариант `.is-nocamp` с колонками 500 · 1fr · 310
   вместо общих 770 · 1fr · 533: у события нет портрета-«лица», и фото
   со «Связанным» казались незаслуженно широкими. **Решение dvn 25.08
   отменяет это: «карточку события по ширине блоков делаем так же, как
   персоналию, а то сейчас фото уже и связанное уже».** Ширины у всех
   четырёх разделов одни.

   ⚠️ ВАРИАНТ УШЁЛ ЦЕЛИКОМ, ВМЕСТЕ С КЛАССОМ: он держал ровно это
   объявление, и оставленный без правил класс — мёртвый крючок, который
   при следующей чистке прочитают как живой. Всё прочее у записи без
   лагеря остаётся: полосы нет (`campEl.hidden`), дорожки под неё нет
   (полоса давно не колонка, а фон).

   🔢 Что это стоит по замеру, а не по ощущению: колонка `__main`
   у события 2059 → 1394. Читаемая строка при этом сужается НЕ на столько:
   у `.p29-card__text` стоит потолок 1180 базовых = 1593 экранных, и
   фактическая мера идёт 1593 → 1394, то есть −199 px. Разница 2059 − 1593
   была пустым полем справа от текста. */
.p29-card__camp[hidden] { display: none; }
.p29-card__camp > i {
  position: absolute;
  inset: 0;
  /* Приглушение — прозрачностью, и она стоит НА ФИГУРЕ, а не в цвете:
     цвет обязан остаться тем же значением, что точка, свотч и заливка. */
  opacity: 0.6;
  /* ⚠️ `--brand-skew` В ВЕНДОРЕ НЕТ. `design` назвала его как готовый
     токен, но в `assets/shared/design/tokens.css` его нет ни под этим
     именем, ни под другим — grep по вендору даёт ноль. Это тот же случай,
     что записан в CLAUDE.md про `--touch-hit`: `var()` БЕЗ ЗАПАСА гасит
     объявление целиком, и полоса просто не наклонилась бы — молча.
     Запас −15° взят из письма `design`; сообщено ей. Появится токен —
     запас снимется сам, значение придёт из вендора. */
  transform: skewX(var(--brand-skew, -15deg));
  background: var(--stripe-c, var(--telegrey));
}

/* Латунный волосок вдоль полосы — из макета: он отделяет предметный цвет
   от листа и не даёт полосе читаться как дефект печати. */
.p29-card__camp > i::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(-15px * var(--content-scale, 1));
  width: calc(4px * var(--content-scale, 1));
  background: var(--active);
  opacity: 0.42;
}
.p29-card__camp[data-camp="red"]      { --stripe-c: var(--camp-red); }
.p29-card__camp[data-camp="white"]    { --stripe-c: var(--camp-white); }
.p29-card__camp[data-camp="rev-dem"]  { --stripe-c: var(--camp-rev-dem); }
.p29-card__camp[data-camp="national"] { --stripe-c: var(--camp-national); }
.p29-card__camp[data-camp="green"]    { --stripe-c: var(--camp-green); }
.p29-card__camp[data-camp="intervention"] { --stripe-c: var(--camp-intervention); }
.p29-card__camp[data-camp="uprising"]     { --stripe-c: var(--camp-uprising); }

/* 🔴 КНОПКА «ЗАКРЫТЬ» БЫЛА НЕЧИТАЕМА НА БЕЛОМ ЛИСТЕ — ЗАМЕР, А НЕ ВКУС.
   Стояло: латунный текст на почти прозрачной бумажной подложке, то есть
   латунь по бумаге. Контраст **1.84 : 1** при пороге 4.5 — и это не
   подпись, а единственный орган выхода из карточки.
   Дефект достался в наследство от тёмного листа: там латунь по графиту
   давала 4.25, и правило было верным ровно до того дня, когда лист стал
   белым. Ровно тот класс, что подпись, пережившая данные.
   Стало (форма из макета `design/section-A-card.html`): сигнальная
   заливка, бумажный текст — **7.20 : 1**, замерено. Латунь осталась
   кромкой: это не текст, её контраст меряется иначе.
   ⚠️ Слово «Закрыть» НЕ заменено на «×» из макета: в музее подпись словом
   надёжнее значка, а тач-цель от слова только растёт. Названо вслух. */
/* Цель — ПРЯМОУГОЛЬНИК, пилюля — подложкой `::before` (как плашки Партий).
   Скругление самой кнопки режет попадание: движок считает его по форме, и
   углы бокса мимо (приёмка design app-270: 7/9 точек в обычном режиме). */
.p29-card__close {
  position: absolute;
  top: var(--gap);
  right: var(--gap);
  min-width: var(--touch-primary, 96px);
  min-height: var(--touch-primary, 96px);
  padding: 0 calc(var(--gap) / 1.5);
  background: transparent;
  border: 0;
  border-radius: 0;
  isolation: isolate;
  color: var(--paper);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  z-index: 2;
}
.p29-card__close::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--signal);
  border: calc(2px * var(--content-scale, 1)) solid var(--active);
  border-radius: calc(48px * var(--content-scale, 1));
}

.p29-card__close:active { color: var(--paper); }
.p29-card__close:active::before { background: var(--graphite); }

/* 🔴 ВАРИАНТ 1B (решение dvn 2026-08-17): текст ограничен колонкой,
   изображения занимают освободившееся место.

   Довод — замер `design`, и он не про красоту: строка была **111 знаков**
   при комфорте 45–75. Вариант 1A (обтекание) лечил её только до 102 —
   обтекание кончается вместе с портретом, дальше текст снова во всю
   ширину. То есть 1A ВЫГЛЯДИТ правкой, не будучи ею.
   1B даёт 70 знаков сразу. Цена — прокрутка 2.36 → 3.52 экрана: текст
   уже, значит выше. Это честная цена, а не дефект.

   Сетка объявлена ЗДЕСЬ и одна; сторону задаёт атрибут ниже. */
.p29-card__main {
  min-height: 0;
  display: grid;
  align-content: start;
  gap: 0 var(--gap);
}

/* 🔒 СТОРОНА ПОРТРЕТА — ПЕРЕКЛЮЧАЛКА, и меняет её ТОЛЬКО атрибут на
   `<html>`. Разметка одна, JS карточки не трогается вовсе: по замеру
   `design` переключение не перерисовывает DOM. По читаемости варианты
   равны (70 знаков и 3.52 экрана у обоих) — различие композиционное,
   поэтому выбирает оператор, а не мы.

   ⚠️ 750, А НЕ 1500 — И ЭТО НЕ ОПЕЧАТКА. `design` намерила комфортную
   колонку **1500 px на экране 3840** при `--content-scale 2`. Наши числа
   пишутся в базе 1920 и умножаются на масштаб, поэтому 1500 px экрана —
   это 750 в базе. Поставив 1500 × 2 = 3000, я получила ОГРАНИЧЕНИЕ,
   КОТОРОЕ НИКОГДА НЕ СРАБАТЫВАЕТ: замер дал текст 1678 px при пороге
   3000 — то есть ширину задавал остаток сетки, а не правило. Мёртвое
   ограничение выглядит рабочим, пока не сравнишь его с фактом.
   Поймано замером ширины, а не чтением. */
/* 🔴 Правила `[data-portrait-side]` УДАЛЕНЫ 2026-08-19 вместе с настройкой:
   фото только слева, решение dvn. Стороной больше никто не управляет. */

/* Колонка изображений в обоих видах занимает все строки текста и липнет:
   портрет остаётся в кадре, пока листается справка. */
.p29-shots {
  grid-row: 1 / span 99;
  position: sticky;
  top: 0;
  align-self: start;
  width: calc(450px * var(--content-scale, 1));
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 2);
}

/* 🔒 ВЕДУЩЕЕ — СОДЕРЖАНИЕ, А НЕ ЗНАЧОК: показывается ЦЕЛИКОМ, никогда
   не обрезается. Форму задаёт картинка, зона задаёт только ПОТОЛОК.
   Правило `design` (`docs/design/card-media-02.md`), решения dvn 18.08:
   потолок 1125 px, четыре хода одним пакетом.

   Замер, ради которого ход сделан: событие «Октябрьское восстание
   в Москве» — оригинал 1244×900 в боксе 900×1125 терял **42.1 %**
   кадра. Стало 900×651, потеря 0.

   ⚠️ ЛОВУШКА, НАЗВАННАЯ ЗАРАНЕЕ И ОБОЙДЁННАЯ: заменить `cover` на
   `contain`, ОСТАВИВ `aspect-ratio`, — обрезка уходит, но появляется
   пустая рамка (у портретов выше 4:5 — до 17 % бокса), и это выглядит
   как выполненная задача. Поэтому `aspect-ratio` снимается СОВСЕМ,
   а бокс задаётся ограничениями: картинка сама выбирает размер
   в пределах зоны, и бокс равен ей по обеим осям. Пустых полей
   не возникает вовсе, `object-fit` при этом не нужен ни в каком виде.

   🔴 РАСХОЖДЕНИЕ С БУКВОЙ РАЗБОРА, НАЗВАННОЕ ВСЛУХ. `design` пишет
   «ширина — колонка медиа (900 px), как сейчас» И «бокс не больше
   картинки ни по одной оси». Одновременно это невозможно: у портрета
   788×1562 ширина 900 даёт высоту 1785, потолок режет её до 1125,
   и `contain` возвращает ту самую пустую рамку. Держу ИНВАРИАНТ
   («потеря 0, пустых полей нет»), а не букву про ширину: картинка
   уже оригинала не растягивается. Следствие — узкие оригиналы
   показываются мельче колонки; число таких замерено и названо
   в `app/README.md`.

   Заливка-подложка убрана: под живой картинкой она читалась бы
   дефектом. Она остаётся признаком «изображения нет в фонде» —
   у `.is-absent`, и только там. */
.p29-shots__big {
  width: auto;
  height: auto;
  max-width: 100%;
  /* 🔴 ПОТОЛОК ВЫРАЖЕН ЧЕРЕЗ КОЛОНКУ, А НЕ СКОПИРОВАННЫМ ЧИСЛОМ.
     Решение dvn — «1125 px», и это ширина колонки, умноженная на 1.25
     (то, что даёт 4:5). Колонка — `450px * --content-scale`, то есть
     900 при масштабе 2, на котором `design` и мерила.
     Написав `1125px * --content-scale`, я получила потолок 2250 —
     ВДВОЕ выше задуманного, и он не срабатывал НИ РАЗУ: у `lg` рамка
     1500, до 2250 не достаёт ничто. Проверка «сколько карточек упёрлось
     в потолок» дала 0 вместо ожидаемых `design` 117 — расхождение
     и вскрыло ошибку. Число, скопированное из чужого замера вместе
     с его единицами, — это то же, что копия констант без сверки. */
  max-height: calc(450px * var(--content-scale, 1) * 1.25);
  display: block;
}

/* 🔒 БЛОК ВЕДУЩЕГО ЖМЁТСЯ К КАРТИНКЕ, А НЕ К КОЛОНКЕ. Ход `design`
   под семь карточек, остающихся мельче колонки при любом снимке.

   `width: fit-content` берёт ширину у картинки. Подпись и инвентарный
   номер не должны при этом ДИКТОВАТЬ ширину — иначе длинная атрибуция
   растянет блок обратно на всю колонку и ход отменится молча. Отсюда
   `width: 0; min-width: 100%` на них: во внутренний расчёт ширины они
   вносят ноль, а по готовому числу растягиваются на него целиком.

   ⚠️ Прижат к левому краю, НЕ центрирован. Проверяемо и назначено
   `design`: левый край снимка у мелких карточек обязан совпадать
   с левым краем у крупных ДО ПИКСЕЛЯ. Центрирование заставило бы
   мелкие «плавать» относительно крупных, и разнобой был бы заметнее
   самой мелкости. Поэтому `margin-inline: auto` здесь нет и быть
   не должно. */
.p29-shots__lead {
  width: fit-content;
  max-width: 100%;
}

.p29-shots__cap {
  width: 0;
  min-width: 100%;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  line-height: 1.35;
  color: var(--text-muted);
}

/* Инвентарный номер музея. Тише подписи и отдельной строкой: подпись
   говорит, ЧТО на снимке, номер — ЧЕЙ это предмет. Моноширинного
   начертания в бренде нет, поэтому различаем кеглем и цветом. */
.p29-shots__inv {
  width: 0;
  min-width: 100%;
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  line-height: 1.3;
  color: var(--text-muted);
  opacity: 0.8;
}

.p29-shots__inv[hidden] { display: none; }

/* `.p29-shots__strip` снят 2026-08-25 — мёртвый блок, найден по дороге
   при правке миниатюр, добро координатора отдельной строкой.
   Лента миниатюр («видно 5 из 5 без прокрутки, колонка 1405 против 2155
   у сетки») проиграла решению dvn о сетке 2×2, и разметка перестала
   класть этот класс: `grep` по `lib` и `scenes` даёт единственный
   контейнер галереи — `p29-shots__grid`. Замер её остаётся верным для
   ПРЕЖНЕЙ раскладки и записан у `.p29-shots__grid` — смена предмета,
   не отмена замера.
   ⚠️ В блоке было и своё: `flex-wrap: wrap` стояло ДВАЖДЫ. Второй раз
   ничего не менял и ничем себя не выдавал — как и весь блок. */

/* 🔴 МИНИАТЮРА БОЛЬШЕ НЕ РЕЖЕТ КАДР. Правка dvn 25.08: «не-основные фото
   не резать в квадраты — показывать нормально». Рамка стала 4:3 (макет
   `f0ac625`), кадр вписывается целиком.
   ⚠️ Прежний довод («квадрат честен и к портрету, и к предмету фонда»)
   не был неверным — он был про ВЫБОР РАМКИ при обязательном кропе.
   Кроп отменён, и вопрос о форме рамки перестал быть про честность.
   ⚠️ Поля вокруг вертикальных кадров — названная `design` и принятая dvn
   ЦЕНА, а не дефект: подгонкой рамки её «чинить» нельзя, иначе вернётся
   кроп под другим именем. */
.p29-shots__thumb {
  flex: 0 0 auto;
  width: calc(108px * var(--content-scale, 1));
  height: calc(81px * var(--content-scale, 1));
  min-width: var(--touch-min, 64px);
  min-height: var(--touch-min, 64px);
  padding: 0;
  border: 2px solid rgba(var(--brass-rgb), 0.3);
  background: rgba(var(--amber-black-rgb), 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* 🔒 ЯКОРЬ КРОПА НАЗВАН, А НЕ ОСТАВЛЕН ПО УМОЛЧАНИЮ. Миниатюра — ЗНАЧОК:
   рамка тут нужна, ритм сетки важнее полноты кадра, полный кадр в одном
   тапе. Но раз рамка режет — она обязана резать с якорем.

   Замер `design`: у квадратной миниатюры срезано сверху медианно 15 %,
   и больше десятой части — у 58 портретов из 62 (гособразования 33 из 34),
   максимум 26 %. На миниатюре это лицо.

   `50% 50%` — это НЕ названный якорь, а его отсутствие; гейт хода 4
   требует именно названного. Ландшафту и квадрату 25 % не меняют ничего:
   они режутся по ширине либо не режутся вовсе. */
/* 🔒 ЯКОРЯ БОЛЬШЕ НЕТ, И ЭТО НЕ ПОТЕРЯ ПРАВИЛА. `--anchor: portrait-top`
   существовал ровно затем, чтобы отличить ВЫБРАННЫЙ кроп от кропа
   по умолчанию: у квадратной миниатюры центральный срез отрезал сверху
   медианно 15 %, а у 58 портретов из 62 — больше десятой части, и это
   было лицо. Кроп отменён — отличать нечего, `contain` не режет ничего.
   Правило пробы «у значка якорь назван» само перестаёт видеть эти
   миниатюры: оно считает потерю по аспектам и молчит там, где потери 0.
   Вернётся `cover` — вернётся и якорь; запись об этом оставлена здесь,
   чтобы возврат не сделали молча. */
.p29-shots__thumb > img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
}
.p29-shots__thumb.is-on { border-color: var(--active); }
.p29-shots__thumb:active { border-color: var(--active); }

.p29-card__title {
  margin: 0 0 calc(var(--gap) / 2);
  font-family: var(--font-display);
  font-size: var(--t-h2);
  font-weight: 400;
  line-height: 1.15;
  /* Курсива у Nolde нет — файла начертания не существует, браузерный
     синтетический наклон на крупном кегле виден как дефект засечек. */
  font-style: normal;
}

.p29-card__dates {
  margin: 0 0 var(--gap);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  white-space: pre-line;
}

.p29-card__regalia {
  margin: 0 0 var(--gap);
  padding-left: 1.2em;
  font-size: var(--t-body);
  line-height: 1.45;
  color: var(--text-muted);
}

.p29-card__text {
  font-size: var(--t-body);
  line-height: 1.5;
  /* Колонка чтения — с запасом от расплывания на всю ширину панели. */
  max-width: calc(1180px * var(--content-scale, 1));
}

.p29-card__text p {
  margin: 0 0 var(--gap);
}

/* Разрешённое упоминание: акцент, но НЕ кнопка. Попасть пальцем в слово
   внутри абзаца нельзя, а промах хуже отсутствия — цель несёт рельс. */
.c-ref {
  color: var(--active);
}

/* Неразрезолвленное упоминание остаётся жирным курсивом без ссылки:
   деградация визуально незаметна, раздел не ломается (схема §5). */
.c-mention {
  font-weight: 700;
}

.p29-card__stub {
  margin: 0;
  padding: var(--gap);
  border: 2px dashed rgba(var(--telegrey-rgb), 0.45);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

.p29-card__wait {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

/* ---------------------------------------------------------------- рельс */

/* Верхний отступ РЕЗЕРВИРУЕТ место под «Закрыть»: кнопка абсолютная и
   лежит над этой колонкой. Без резерва она перекрывала заголовок рельса и
   правую часть первой строки — а перехваченная тач-цель хуже мелкой:
   мелкая честно не нажимается, перехваченная нажимается не туда.
   Поймано пересечением боксов, не глазами. */
.p29-card__rail {
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 2);
  padding-left: var(--gap);
  padding-top: calc(var(--touch-primary, 96px) + var(--gap) / 2);
  border-left: 1px solid rgba(var(--brass-rgb), 0.3);
}

.p29-rail__head {
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Внешняя ссылка: выделена как упоминание, но НЕ ведёт никуда — офлайн.
   Форма та же, что у неразрешённого упоминания (§5), и это намеренно:
   для посетителя оба случая одинаковы — термин важен, перехода нет.
   Заводить третий вид значило бы обещать различие, которого он
   не может использовать. Курсор и `:active` не трогаем: не контрол. */
.c-ref.is-external {
  text-decoration: none;
}

/* Место события. Рядом с датой: оба отвечают на «когда и где», и оба
   приходят из данных. */
.p29-card__place {
  margin: 0 0 calc(var(--gap) / 2);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-on-dark);
}

/* Основание редакторского решения — тише самого места и ВСЕГДА рядом
   с ним. Город, назначенный нами по тексту справки, не должен читаться
   как атрибуция музея: подпись под экспонатом читают как слова музея,
   и цена подмены здесь выше обычной. */
.p29-card__place-src {
  display: block;
  margin-top: 0.15em;
  font-size: var(--t-caption);
  color: var(--text-muted);
  opacity: 0.85;
}

/* Миниатюра связи. Квадрат по той же причине, что в галерее: цели рельса —
   не только портреты (партии, гособразования, события). Замер по всем
   1451 связи: с изображением 1309, без — 142.
   ⚠️ Здесь стояло «2902 / 2618 / 284» — вдвое больше. Ошибка счёта, а не
   данных: перебор считал `X.json` и `X.gen.json` как две разные справки.
   Неверное число успело уйти в `design` через координатора. */
.p29-rail__thumb {
  flex: 0 0 auto;
  width: calc(56px * var(--content-scale, 1));
  height: calc(56px * var(--content-scale, 1));
  object-fit: cover;
  /* Якорь назван по той же причине, что в галерее, — см. `.p29-shots__thumb`.
     На 56 px срез в 15 % сверху съедает лоб. */
  object-position: 50% 25%;
  --anchor: portrait-top;
  display: block;
  background: rgba(var(--amber-black-rgb), 0.35);
}

/* Форма СУЩЕСТВУЮЩАЯ, третью не заводим: сплошная рамка с заливкой —
   «изображения нет в фонде». Пунктир означал бы «не собрано на стенде»
   и обещал бы картинку, которой не будет. */
.p29-rail__thumb.is-absent {
  border: 1px solid rgba(var(--telegrey-rgb), 0.28);
  background: rgba(var(--telegrey-rgb), 0.08);
}

.p29-rail__row {
  /* 🔴 СТРОКА НЕ СЖИМАЕТСЯ НИЖЕ СВОЕГО СОДЕРЖИМОГО. Рельс — колоночный
     флекс, и без этого он ужимал строки до `min-height` (96), а текст
     в 2–3 строки вылезал поверх соседней: замер на «Белорусской ССР» —
     все 12 строк имели height 96 при scrollHeight 149…265.
     Заметил dvn на экране, гейт молчал: в проверке переполнения стояли
     `.p29-tile, .chr-row, .chr-cell`, а рельса в списке не было —
     список по памяти снова состарился.
     Рельс прокручиваемый (`kiosk-scroll`), поэтому расти строкам есть
     куда: лишнее уходит в прокрутку, а не поверх соседа. */
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: center;
  gap: 0.2em calc(var(--gap) / 2);
  /* Кнопка рельса несёт цель вместо некликабельной ссылки в тексте —
     это основной контрол, значит норма --touch-primary, а не минимум. */
  min-height: var(--touch-primary, 96px);
  padding: calc(var(--gap) / 2);
  background: rgba(var(--paper-rgb), 0.06);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  color: var(--text-on-dark);
  text-align: left;
  font-family: var(--font-text);
  font-size: var(--t-body);
}

/* Миниатюра занимает обе строки, подписи идут во второй колонке. */
.p29-rail__thumb { grid-row: 1 / span 2; grid-column: 1; }
.p29-rail__title { grid-column: 2; }
.p29-rail__where { grid-column: 2; }

/* ХОД 6 — ПЕРЕХОД К ПЕРСОНАЛИЯМ ЛАГЕРЯ. Форма взята у строки рельса
   намеренно: это тот же жест «уйти отсюда туда», и второй вид кнопки
   для одного смысла посетитель читал бы как другой смысл.
   Отличие одно — метка лагеря слева, тем же предметным цветом, что
   на плитке, точке поля и в легенде. Цвет ЗАЛИВКОЙ, текст бумагой:
   тело букв предметным цветом не красим (канон `design` 24.08). */
.p29-campgo {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: calc(var(--gap) / 2);
  width: 100%;
  min-height: var(--touch-primary, 96px);
  padding: calc(var(--gap) / 2);
  background: rgba(var(--paper-rgb), 0.06);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: max(var(--type-min-ui, 32px), calc(26px * var(--content-scale, 1)));
  text-align: left;
}

.p29-campgo__t { flex: 1 1 auto; min-width: 0; }

/* Счётчик — сколько персон в лагере. Не украшение: он отвечает на вопрос
   «стоит ли туда идти» ДО перехода, а пустых переходов здесь не бывает
   по устройству (кнопки без единой персоны сцена не рисует). */
.p29-campgo__n {
  flex: 0 0 auto;
  font-family: var(--font-caption);
  color: var(--active);
}

.p29-campgo__chev { flex: 0 0 auto; color: var(--active); }

.p29-campgo:active {
  background: rgba(var(--brass-rgb), 0.22);
  border-color: var(--active);
}

.p29-rail__row:active {
  background: rgba(var(--brass-rgb), 0.22);
  border-color: var(--active);
}

/* Недостижимое — глухое и подписанное. Ни битую ссылку, ни несобранный
   раздел нельзя оформлять как рабочую кнопку: промах по ней читается
   как поломка движка, а не как незаконченная работа. */
.p29-rail__row.is-pending,
.p29-rail__row.is-broken {
  border-style: dashed;
  background: none;
  color: var(--text-muted);
}

.p29-rail__row.is-broken {
  border-color: rgba(var(--red-rgb), 0.6);
}

.p29-rail__title {
  font-weight: 700;
}

.p29-rail__where {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

.p29-rail__count,
.p29-rail__empty {
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

/* Пустой отбор — это состояние, а не отказ: подписано и сдержанно,
   без сигнального красного, который здесь означал бы поломку. */
.p29-empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: var(--gap);
  border: 2px dashed rgba(var(--telegrey-rgb), 0.45);
  font-family: var(--font-caption);
  font-size: var(--t-body);
  color: var(--text-muted);
}

/* ============================================================================
   === ПРЕДМЕТНЫЙ ЦВЕТ: начало ===

   ПРЕДМЕТНЫЕ ЦВЕТА ЛАГЕРЕЙ — законный слой, а не долг.

   🔒 Решено dvn 2026-08-17 (CLAUDE.md §Инварианты): в проекте ДВА слоя
   цвета, и они не связаны. Интерфейс — 7 брендовых токенов, там «только
   из токенов» в полной силе. Предметные цвета (лагеря, партии, слои карт)
   назначаются САМИ по читаемости и к брендовой палитре не привязаны.

   Поэтому прежняя ограда «вторая палитра · долг переноса в design-core»
   СНЯТА: переносить нечего, заявка вверх по течению отозвана. Это не
   послабление — это исправление неверной постановки: предметный цвет
   никогда и не обязан был жить в брендовых девяти.

   Значения взяты из старого 29 (`29-30/src/styles/tokens.css`) без правок:
   их выбирали под эту же предметную область и записали причину прямо
   в комментарии токена. Три лагеря совпадают с брендовыми — это совпадение,
   а не привязка: красный можно поменять, не спрашивая бренд.

   ⚠️ ЕДИНСТВЕННОЕ ТРЕБОВАНИЕ, ОСТАВШЕЕСЯ В СИЛЕ: предметный цвет ОДИН
   на всё приложение. Цвет лагеря в списке, на карте, в Венне и в легенде
   обязан совпадать; расхождение между разделами — дефект. Держится это
   тем, что источник ровно один — блок ниже, и других объявлений
   `--camp-*` в зоне нет.
   ============================================================================ */

:root {
  --camp-red:          #A02128;   /* совпадает с брендовым --red, но не привязан */
  --camp-white:        #CFD0CF;   /* совпадает с --telegrey */
  --camp-national:     #D2B773;   /* совпадает с --brass; если мешает читать
                                     рядом с «активным», меняется здесь одной
                                     строкой — разрешения больше не нужно */
  --camp-rev-dem:      #8C4A99;
  --camp-green:        #5A8E55;
  --camp-intervention: #2F4A6B;
  --camp-uprising:     #2A8079;
  /* ⚠️ КАНАЛЬНАЯ КОПИЯ КРАСНОГО — ЕДИНСТВЕННАЯ, И ОНА ЗДЕСЬ.
     `rgba(var(--camp-red-rgb), a)` стояло в рельсе Симбирска с запасом
     `160, 33, 40` — токена не было, работал запас, и то же значение
     жило в файле ВТОРОЙ строкой, рядом с `#A02128`. Ровно то, от чего
     ограждает шапка блока: источник должен быть один.
     Понадобится прозрачность другому лагерю — заводи канал ЗДЕСЬ,
     а не запасом по месту. */
  --camp-red-rgb:      160, 33, 40;   /* = --camp-red #A02128 */

  /* ЦВЕТ ЛИНИЙ ХРОНИКИ — тот же предметный слой, что лагеря, и по той же
     причине: политическая и военная линии — предмет, а не элемент интерфейса.
     Значения назначила зона `design` (макет `section-A-chronicle.html`,
     правка dvn 24.08): стальной синий и рыжая бронза — пара, различимая
     и при дейтеранопии, и в музейном свете.

     🔒 ЦВЕТ ПОВЕРХ ПОЗИЦИИ И ПОДПИСИ, А НЕ ВМЕСТО НИХ. Линию по-прежнему
     кодирует колонка (двухпутка) или подпись (асимметричная раскладка);
     цвет добавляется третьим признаком. Это условие макета, и оно же —
     причина, по которой его можно применять ЗАЛИВКОЙ, не считаясь
     с контрастом текста.

     🔴 И ПОТОМУ ЖЕ ИМИ НЕ КРАСИТСЯ ТЕКСТ — ЗАМЕР, А НЕ ОСТОРОЖНОСТЬ.
     На графите `#435059` контраст `--line-pol` = 1.89 : 1,
     `--line-mil` = 2.25 : 1 при пороге 4.5 для подписи. В макете подпись
     линии и метка «на карте» покрашены этим цветом — на живом экране
     это нечитаемо, и в разделе так не сделано.
     Второй слой «чернильных» оттенков я НЕ ЗАВОДИЛА: в проекте УЖЕ ЕСТЬ
     своя конвенция — предметный цвет идёт ЗАЛИВКОЙ (метка лагеря, полигон
     карты, область Венна), а текст остаётся бумагой. Ровно тот случай,
     на котором зона попалась с мёртвым токеном `--crop-anchor`: сперва
     проверь, нет ли уже своего правила, потом заводи общее.
     Поэтому линию несёт кромка ячейки, плашка легенды и значок метки —
     всё это заливка, — а слова остаются читаемыми. */
  --line-pol:          #4E7CA8;   /* Политическая линия — стальной синий */
  --line-mil:          #C0722F;   /* Военные действия — рыжая бронза */

  /* РОЛИ СЛОЁВ КАРТЫ. Тот же предметный слой (CLAUDE.md §Инварианты прямо
     называет «слои карт»), поэтому живут здесь, а не отдельной палитрой.
     Роли стороны — АЛИАСЫ к лагерям, и это исполнение единственного
     оставшегося требования: цвет лагеря один на всё приложение. Красный
     на карте и красный в списке — буквально одно значение, а не два
     похожих; разойтись они не могут по устройству.
     Нейтральные роли берутся из БРЕНДОВЫХ токенов: бумага, границы, реки,
     города, подписи, рамка — это интерфейсный слой карты, не предметный.
     ⚠️ dvn 2026-08-17: под векторной базой будет РАСТР, и тогда часть этих
     ролей рисовать не понадобится вовсе. Пока растра нет — они временные
     «чтобы было видно», и снимаются вместе с приходом растра. */
  --map-red:          var(--camp-red);
  --map-white:        var(--camp-white);
  --map-green:        var(--camp-green);
  --map-national:     var(--camp-national);
  --map-rev-dem:      var(--camp-rev-dem);
  --map-intervention: var(--camp-intervention);
  --map-uprising:     var(--camp-uprising);
  --map-front:        var(--text-on-dark);
  --map-paper:        var(--telegrey);
  --map-territory:    var(--window);
  /* ⚠️ `--map-border` и `--map-label` НАРОЧНО остаются на `--graphite`,
     а не переходят на роль `--text-on-light` вместе с интерфейсом
     (задание №6). Это ПРЕДМЕТНЫЙ слой — он к брендовым ролям не привязан
     решением dvn 2026-08-17, и `--map-label` вдобавок не текст интерфейса,
     а `fill` подписи на SVG-карте. Перевод втянул бы предметный цвет
     в интерфейсную роль: поменяется роль — поедет карта. */
  --map-border:       var(--graphite);
  --map-river:        var(--blue-grey);
  --map-city:         var(--amber-black);
  --map-label:        var(--graphite);
  --map-frame:        var(--brass);
  --map-decor:        var(--window);
  --map-sea:          var(--blue-grey);
}

/* === ПРЕДМЕТНЫЙ ЦВЕТ: конец === */

/* ------------------------------------------------- метка лагеря: три вида */

/* Общий носитель. Какой из трёх видов показан — решает атрибут на корне
   приложения, который ставит настройка УРОВНЯ ПРИЛОЖЕНИЯ: лагерь делят
   персоналии, партии, гособразования и карты, и объявить его в одной сцене
   значило бы соврать подписью в панели. */
.camp-mark {
  display: inline-block;
  flex: 0 0 auto;
  width: calc(28px * var(--content-scale, 1));
  height: calc(28px * var(--content-scale, 1));
}

/* — Вариант А: цветом. Носитель — заливка, не текст. */
[data-camp-mode="color"] .camp-mark { border-radius: 50%; }
[data-camp-mode="color"] .camp-mark[data-camp="red"]          { background: var(--camp-red); }
[data-camp-mode="color"] .camp-mark[data-camp="white"]        { background: var(--camp-white); }
[data-camp-mode="color"] .camp-mark[data-camp="national"]     { background: var(--camp-national); }
[data-camp-mode="color"] .camp-mark[data-camp="rev-dem"]      { background: var(--camp-rev-dem); }
[data-camp-mode="color"] .camp-mark[data-camp="green"]        { background: var(--camp-green); }
[data-camp-mode="color"] .camp-mark[data-camp="intervention"] { background: var(--camp-intervention); }
[data-camp-mode="color"] .camp-mark[data-camp="uprising"]     { background: var(--camp-uprising); }

/* — Вариант Б: формой. Латунь одна на всех, различает ОЧЕРТАНИЕ.
   Формы подобраны так, чтобы отличаться силуэтом, а не деталью: на 4K
   с двух метров деталь не читается. Заливка против контура — первое
   различие, круг против квадрата против ромба — второе. */
[data-camp-mode="shape"] .camp-mark {
  border: calc(3px * var(--content-scale, 1)) solid var(--active);
}
[data-camp-mode="shape"] .camp-mark[data-camp="red"] {
  background: var(--active);                                   /* залитый квадрат */
}
[data-camp-mode="shape"] .camp-mark[data-camp="white"] {
  border-radius: 50%;                                          /* контурный круг */
}
[data-camp-mode="shape"] .camp-mark[data-camp="national"] {
  border-radius: 50%;
  background: var(--active);                                   /* залитый круг */
}
[data-camp-mode="shape"] .camp-mark[data-camp="rev-dem"] {
  transform: rotate(45deg);                                    /* контурный ромб */
}
[data-camp-mode="shape"] .camp-mark[data-camp="green"] {
  transform: rotate(45deg);
  background: var(--active);                                   /* залитый ромб */
}
[data-camp-mode="shape"] .camp-mark[data-camp="intervention"] {
  border-style: dashed;                                        /* пунктирный квадрат */
}
[data-camp-mode="shape"] .camp-mark[data-camp="uprising"] {
  background: repeating-linear-gradient(45deg,
    var(--active) 0 calc(4px * var(--content-scale, 1)),
    transparent calc(4px * var(--content-scale, 1)) calc(8px * var(--content-scale, 1)));
}

/* — Вариант В: подписью. Метки нет вовсе, лагерь читается словом. */
[data-camp-mode="label"] .camp-mark { display: none; }

/* Подпись словом идёт во ВСЕХ трёх видах: без неё цвет и форма остаются
   шифром, который посетителю негде расшифровать. Цвет и форма ускоряют
   узнавание, а не заменяют его. */
.p29-tile__camp {
  display: flex;
  align-items: center;
  gap: calc(10px * var(--content-scale, 1));
}

/* ------------------------------------------------------------ отказы */

/* Отказ виден и назван. Пустой экран вместо сообщения — тот самый
   «молчаливый зелёный», из-за которого дефект живёт неделями. */
.p29-fail {
  margin: 0;
  padding: var(--gap);
  border: 2px solid var(--signal);
  font-family: var(--font-caption);
  font-size: var(--t-body);
  color: var(--text-on-dark);
}

/* ============================================================================
   ХРОНИКА — двухпутка. Левая колонка политическая, правая военная, между
   ними ось даты.

   Форма продиктована данными, а не вкусом: `track` трёхзначное — 212 строк
   только политических, 153 только военных, 31 в обеих колонках. Одна лента
   у этих 31 либо теряет половину, либо двоит их.
   ============================================================================ */

.chr,
.chr * {
  box-sizing: border-box;
}

.chr {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 2);
  padding: var(--chrome-top, 64px) var(--chrome-right, 64px)
           var(--chrome-bottom, 80px) var(--chrome-left, 64px);
  color: var(--text-on-dark);
  font-family: var(--font-text);
  --chr-row-gap: calc(16px * var(--content-scale, 1));
  /* Ось даты — своя колонка, а не поле: подписи «Конец октября – начало
     ноября» длинные, и по узкой колонке они рвутся в лапшу. */
  --chr-axis-w: calc(240px * var(--content-scale, 1));
}

/* `.chr__head`, `.chr__head .chr__title`, `.chr__head .chr__count`
   и `.chr__title` сняты 2026-08-25 вместе с `.p29__head`. ⚠️ `.chr__count`
   НЕ снят — его рисует `scenes/chronicle.js`, и он живой. */

.chr__count {
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

/* 🔴 ПРАВИЛА ПЕРЕКЛЮЧАТЕЛЯ РАСКЛАДКИ УДАЛЕНЫ ВМЕСТЕ С ОРГАНОМ — №23 п.5
   (замечание историка: «две линии / по ширине» посетителю непонятен).
   Узлов `.chr__modes` и `.chr-mode` в разметке больше нет, значит нет
   и правил: мёртвое правило читается как действующее и переживает того,
   кто знал, что оно мертво. Сама настройка раскладки ЖИВА и осталась
   в сервис-панели — убирали орган у посетителя, а не выбор у оператора. */

/* --------------------------------------------------------------- годы */

.chr__years {
  flex: 0 0 auto;
  display: flex;
  gap: var(--touch-gap, 16px);
}

.chr-year {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1em;
  /* Ось сцены — основной контрол, значит --touch-primary. */
  min-width: calc(var(--touch-primary, 96px) * 1.6);
  min-height: var(--touch-primary, 96px);
  padding: 0 calc(var(--gap) / 2);
  background: rgba(var(--paper-rgb), 0.06);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
}

/* Активное состояние — латунь, не красная заливка. */
.chr-year.is-active {
  background: var(--active);
  border-color: var(--active);
  color: var(--text-on-light);
}

.chr-year:active {
  background: rgba(var(--brass-rgb), 0.35);
}

/* Год без попаданий при активном поиске гасится, но НЕ прячется: пропавший
   чип читался бы как «такого года нет в хронике». */
.chr-year.is-empty {
  opacity: 0.45;
}

.chr-year__n {
  font-size: max(var(--type-min-ui, 32px), calc(28px * var(--content-scale, 1)));
  font-weight: 600;
}

/* 🔴 ЗДЕСЬ СТОЯЛА `opacity: 0.75`, И ЭТО БЫЛО ОСЛАБЛЕНИЕ ТЕЛА БУКВ.
   На активном чипе счётчик — графит по латуни: в полную силу 4.84 : 1,
   с ослаблением 2.84 при пороге 3.0 для крупного. Соподчинение счётчика
   году держится кеглем (`--t-caption` против кегля чипа), а не блёклостью.
   Канон `design` 24.08: тело букв в слабый цвет не красим нигде. */
.chr-year__c {
  font-size: var(--t-caption);
}

/* ------------------------------------------------------------- полотно */

.chr__body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* ДВЕ РАСКЛАДКИ, один DOM. Порядок в разметке фиксирован (ось, политическая,
   военная), место задаёт `grid-column` — поэтому переключение настройки не
   трогает разметку и не может разойтись с ней. */
.chr__legend,
.chr-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--chr-axis-w) minmax(0, 1fr);
  gap: var(--gap);
}

.chr-row__axis { grid-column: 2; grid-row: 1; }
.chr-cell.is-pol { grid-column: 1; grid-row: 1; }
.chr-cell.is-mil { grid-column: 3; grid-row: 1; }

/* Подпись линии в двухпутке не нужна: линию кодирует КОЛОНКА, и дублировать
   её словом значит повторять то, что и так видно. */
.chr-cell__track { display: none; }

/* ── Раскладка 2: асимметричная ───────────────────────────────────────────
   Ось даты уходит влево одной рельсой, односторонняя строка идёт во всю
   ширину, и ДВЕ КОЛОНКИ ОСТАЮТСЯ ТОЛЬКО У СОВПАДЕНИЙ — 31 строки из 396.
   Смысл смены: в двухпутке половина экрана простаивает (212 строк только
   политические, 153 только военные), а совпадение линий тонет в общем
   ритме. Здесь оно становится единственным местом, где лента раздваивается,
   то есть событием, а не фоном. Цена — линию односторонней строки больше
   не кодирует позиция, поэтому появляется подпись. */
.chr.is-asym .chr__legend,
.chr.is-asym .chr-row {
  grid-template-columns: var(--chr-axis-w) minmax(0, 1fr) minmax(0, 1fr);
}

.chr.is-asym .chr-row__axis {
  grid-column: 1;
  border-left: none;
  align-items: flex-start;
  text-align: left;
}

/* Односторонняя строка занимает обе контентные колонки. */
.chr.is-asym .chr-row.is-pol .chr-cell.is-pol,
.chr.is-asym .chr-row.is-mil .chr-cell.is-mil { grid-column: 2 / -1; }
.chr.is-asym .chr-row.is-pol .chr-cell.is-mil,
.chr.is-asym .chr-row.is-mil .chr-cell.is-pol { display: none; }

/* Совпадение линий — единственное место, где лента раздваивается. */
.chr.is-asym .chr-row.is-both .chr-cell.is-pol { grid-column: 2; }
.chr.is-asym .chr-row.is-both .chr-cell.is-mil { grid-column: 3; }

/* Военная колонка выключена по левому краю: правое выключение кириллицы
   читается заметно хуже, а зеркальность здесь уже не нужна — линию
   называет подпись. */
/* ⚠️ ВЫКЛЮЧКА ЗДЕСЬ ТОЖЕ ПЕРЕЕХАЛА НА `__main` — по той же причине, что
   и в базовом блоке: на строчном флексе `align-items` управляет высотой,
   а не краем. Правило, оставленное на `.chr-cell`, продолжало бы
   совпадать с селектором и не делать ничего видимого. */
.chr.is-asym .chr-cell.is-mil .chr-cell__main {
  align-items: flex-start;
  text-align: left;
}

.chr.is-asym .chr-cell__track {
  display: flex;
  align-items: center;
  gap: calc(var(--gap) / 3);
  font-family: var(--font-caption);
  font-size: calc(var(--t-caption) * 0.85);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Плашка перед подписью — тот же предметный цвет заливкой. Слово остаётся
   приглушённым текстом: покрась его линией, и вышло бы 1.89 : 1. */
.chr.is-asym .chr-cell__track::before {
  content: "";
  flex: 0 0 auto;
  width: calc(26px * var(--content-scale, 1));
  height: calc(14px * var(--content-scale, 1));
  background: var(--line-c);
  transform: skewX(-15deg);
}

/* Совпадение подписано осью, а не заливкой: точечный акцент, не поверхность.
   ⚠️ В асимметричной раскладке ось стоит СЛЕВА ОТ ОБЕИХ колонок, и одна
   кромка не может назвать две линии. Поэтому здесь она делится пополам:
   сверху политическая, снизу военная — «здесь сходятся обе». Градиентом,
   а не двумя узлами: узел пришлось бы позиционировать, а фон видит CSS. */
.chr.is-asym .chr-row.is-both .chr-row__axis {
  border-left: 0;
  border-right: 0;
  padding-left: calc(var(--gap) / 2);
  background:
    linear-gradient(180deg, var(--line-pol) 0 50%, var(--line-mil) 50% 100%)
    left / calc(3px * var(--content-scale, 1)) 100% no-repeat;
}

/* Легенда двухпутки в асимметричной раскладке ЛЖЁТ: колонки под ней больше
   не значат «политическая» и «военная». Меняется целиком, а не правится
   выключением по краю. */
.chr__legend-note { display: none; }

/* 🔴 И ДЕРЖИТ ДОРОЖКИ, КОТОРЫХ БОЛЬШЕ НЕ ЗАПОЛНЯЕТ. В асимметричной
   раскладке из четырёх детей легенды видно одно примечание (324 px),
   а сетка сохраняет три дорожки прежней двухпутки: 324 + 1088.6 + 1088.6.
   Замер: 85 % ширины легенды — пустые дорожки.
   Нашла НОВАЯ проверка пробы «поле не отдано пустым дорожкам», заведённая
   часом раньше под дефект «Витрины». Искали в одном месте — нашлось
   в другом, где никто не смотрел: признак работает шире случая,
   ради которого написан. */
.chr.is-asym .chr__legend { display: block; }

/* 🔴 ПОПУТНЫЙ ДЕФЕКТ, НАЙДЕННЫЙ ЗДЕСЬ ЖЕ: КОММЕНТАРИЙ РАЗРЕЗАЛ СЕЛЕКТОР.
   Стояло `.chr.is-asym /* … *\/ .chr__untranslated` — комментарий внутри
   селектора работает как ПРОБЕЛ, то есть правило читалось как «плашка
   ВНУТРИ асимметричной раскладки». В двухпутке плашка оставалась
   неоформленной: без отступа, без кегля, без латунной кромки. Ошибки
   при этом нет нигде — селектор синтаксически верен и даже совпадает,
   просто не с тем.
   Видно это только в английском режиме и только в раскладке «две линии
   рядом», поэтому и прожило до сегодня. Раскладке плашка безразлична —
   правило общее.
   Плашка «текст не переведён» — одна на сцену, а не метка на каждой
   из 396 строк. Та же форма, что `card.untranslated` в карточке:
   перевода в данных нет вовсе, и честно можно только сказать об этом. */
.chr__untranslated {
  margin: 0 0 calc(var(--gap) / 2);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  line-height: 1.35;
  color: var(--text-muted);
  border-left: 2px solid rgba(var(--brass-rgb), 0.45);
  padding-left: calc(var(--gap) / 2);
}

.chr__untranslated[hidden] { display: none; }

.chr__legend {
  grid-template-columns: 1fr;
}

.chr.is-asym .chr__legend-cell { display: none; }

.chr.is-asym .chr__legend-note { display: block; }

.chr__legend {
  flex: 0 0 auto;
  padding: calc(var(--gap) / 3) 0;
  border-bottom: 1px solid rgba(var(--brass-rgb), 0.35);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* 🔴 РАСПОРКА-ОСЬ ИЗ ЛЕГЕНДЫ СНЯТА, А КОЛОНКИ НАЗВАНЫ ЯВНО.
   В легенде стоял пустой `<span class="chr__legend-axis">`, чтобы занять
   среднюю колонку сетки. В двухпутке проба назвала его: «нулевой бокс
   по одной оси — chr__legend-axis 2566×0». Пустой узел без содержимого
   не имеет высоты, и это ровно тот признак, которым ловят схлопнувшуюся
   раскладку: одна ось в ноль. Вреда он не делал, но глушил проверку
   собственным шумом — а проверка, привыкшая к шуму, пропускает настоящий
   случай.
   ⚠️ Прежде он был НУЖЕН: колонки раздавались автоматически, и без
   распорки военная подпись уехала бы во вторую колонку вместо третьей.
   Теперь колонки названы явно — распорка не нужна вовсе.
   ⚠️ Замечено только потому, что замер шёл в ОБЕИХ раскладках: в
   асимметричной (она по умолчанию) легенда скрыта целиком, и все прежние
   прогоны «0 отказов» её не видели. */
.chr__legend-cell.is-pol { grid-column: 1; }

.chr__legend-cell.is-mil {
  grid-column: 3;
  text-align: right;
}

/* Плашка линии в легенде — та же заливка, что кромка ячейки и значок
   метки. Один цвет на линию во всех местах разом: разойдись они, легенда
   объясняла бы ленту неверно, и краснеть было бы нечему. */
.chr__legend-cell.is-pol { --line-c: var(--line-pol); }
.chr__legend-cell.is-mil { --line-c: var(--line-mil); }

.chr__legend-cell.is-pol::before,
.chr__legend-cell.is-mil::after {
  content: "";
  display: inline-block;
  vertical-align: baseline;
  width: calc(26px * var(--content-scale, 1));
  height: calc(14px * var(--content-scale, 1));
  background: var(--line-c);
  transform: skewX(-15deg);
}

/* Плашка идёт по ту же сторону, что и выключка подписи: слева у левой
   колонки, справа у правой. Иначе две метки сошлись бы к середине
   и читались как одна пара. */
.chr__legend-cell.is-pol::before { margin-right: calc(var(--gap) / 3); }
.chr__legend-cell.is-mil::after { margin-left: calc(var(--gap) / 3); }

.chr__scroll {
  flex: 1 1 auto;
  min-height: 0;
  padding-right: var(--scrollbar-w, 18px);
}

/* ── МАРКЕР ПОЗИЦИИ (правка dvn 24.08) ────────────────────────────────────
   «◉ ГОД · МЕСЯЦ · вы здесь» над лентой. Латунь — интерфейсный слой:
   полоса говорит о ПОЛОЖЕНИИ в разделе, а не о предмете, и цвет линий
   здесь был бы враньём (у одной строки обе линии сразу).
   Стоит НАД прокруткой, а не липким внутри неё: на `top: 0` ленты уже
   живёт месячный заголовок, и две липкие полосы на одну координату
   наложились бы — разбор в `scenes/chronicle.js`. */
.chr-now {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: calc(var(--gap) / 2);
  margin: calc(var(--gap) / 3) 0;
  padding: calc(var(--gap) / 3) calc(var(--gap) / 1.5);
  background: var(--active);
  color: var(--text-on-light);
}

.chr-now[hidden] { display: none; }

/* Точка — сигнальный красный, единственная его роль в разделе: «вот
   здесь». Кольцо вокруг — не тень, а запас различимости на латуни. */
.chr-now__dot {
  flex: 0 0 auto;
  width: calc(16px * var(--content-scale, 1));
  height: calc(16px * var(--content-scale, 1));
  border-radius: 50%;
  background: var(--red);
  box-shadow: 0 0 0 calc(4px * var(--content-scale, 1)) rgba(var(--red-rgb), 0.35);
}

.chr-now__year {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-ui, 32px), calc(47px * var(--content-scale, 1)));
  line-height: 1.2;
  color: var(--text-on-light);
}

.chr-now__month {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-ui, 32px), calc(38px * var(--content-scale, 1)));
  line-height: 1.2;
  color: rgba(var(--graphite-rgb), 0.82);
}

/* ⚠️ И ТУТ ЖЕ БЫЛА ВТОРАЯ ФОРМА ТОГО ЖЕ: `rgba(graphite, 0.7)` — тот же
   графит по латуни, ослабленный альфой вместо `opacity`. 2.62 : 1.
   Слово «вы здесь» — подпись, а не украшение, и обязано читаться. */
.chr-now__label {
  margin-left: auto;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-on-light);
}

/* Месячный заголовок липкий: 19 таких строк держат ориентацию в ленте на
   139 записей. ⚠️ Это ДОПОЛНЕНИЕ к строке, а не замена — сама строка
   с этим признаком тоже несёт событие и рисуется ниже. */
.chr-month {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: var(--gap) 0 calc(var(--gap) / 3);
  padding: calc(var(--gap) / 3) 0;
  background: var(--bg);
  border-bottom: 1px solid rgba(var(--brass-rgb), 0.5);
  font-family: var(--font-caption);
  font-size: max(var(--type-min-ui, 32px), calc(26px * var(--content-scale, 1)));
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--active);
}

.chr-row {
  align-items: start;
  padding: var(--chr-row-gap) 0;
  border-bottom: 1px solid rgba(var(--paper-rgb), 0.1);
}

/* Строка, заполненная в ОБЕИХ колонках (31 из 396) — единственное, что
   двухпутка показывает, а лента прячет. Помечена осью, а не заливкой.
   
   🔴 ЗДЕСЬ СТОЯЛ ЦВЕТ БЕЗ ШИРИНЫ, И МЕТКИ НЕ БЫЛО ВОВСЕ. Правило задавало
   `border-left-color`/`border-right-color`, а `.chr-row__axis` границы
   не объявляет — ширина осталась 0. Замер в двухпутке:
   `borderLeftWidth 0px / borderRightWidth 0px` при честно выставленном
   латунном цвете. То есть признак совпадения линий не рисовался ИМЕННО
   в той раскладке, ради которой двухпутка и существует; в асимметричной
   он виден, потому что там объявлена полная `border-left`.
   Ни ошибки, ни исключения: цвет применён, просто красить нечего.

   🔒 И цвет теперь ЛИНЕЙНЫЙ, а не латунный (ход 4): ось физически
   разделяет политическую колонку слева и военную справа, поэтому левая
   кромка несёт цвет политической линии, правая — военной. Латунь здесь
   была бы «выбранным», а выбора никто не делал: это признак данных. */
.chr-row.is-both .chr-row__axis {
  border-left: calc(3px * var(--content-scale, 1)) solid var(--line-pol);
  border-right: calc(3px * var(--content-scale, 1)) solid var(--line-mil);
}

/* ХОД C зоны `design` — вес события читается на скане.
   Было: кромка 2 px при тексте 48 px, то есть невидима — «Октябрьский
   переворот» читался как рядовой декрет. Тяжёлых 76 из 396 по всей хронике
   (6 из 35 в 1917), поле `weight` в данных лежало готовым и просто
   не доезжало до экрана.
   Стало: ЛАТУННАЯ ДАТА — переиспользует ось хода B, читается при прокрутке
   и остаётся точечным акцентом, а не заливкой (канон §1). */
.chr-row.is-heavy .chr-axis__date {
  color: var(--active);
  font-weight: 700;
}

/* Верхняя линейка вместо боковой кромки: она идёт поперёк скана, а боковая
   шла вдоль него и терялась. */
.chr-row.is-heavy {
  border-top: 3px solid rgba(var(--brass-rgb), 0.55);
}

/* --------------------------------------------------------------- ось */

/* ХОД B зоны `design` — дата становится ОСЬЮ, а не подписью.
   Её замер: год повторялся в каждой из 35 строк (я проверила — 35 из 35),
   якорей для скана в кадре 0. Ключевое в её предложении и причина, по
   которой оно принято без спора: высота строк НЕ РАСТЁТ — ритм добавляется
   бесплатно, строку диктует текст, а не дата.
   Выключка по левому краю, декоративные боковые кромки убраны. */
.chr-row__axis {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1em;
  padding: 0 calc(var(--gap) / 2);
  text-align: left;
}

.chr-axis__date {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-ui, 32px), calc(30px * var(--content-scale, 1)));
  font-weight: 600;
  color: var(--text-on-dark);
  line-height: 1.1;
}

/* Год — мелким и приглушённым под днём: он задан активным чипом и в каждой
   строке лишний, но убирать совсем нельзя — на стыке лет и в поиске по
   всей хронике он единственное, что отличает 3 января 1918 от 3 января 1919. */
.chr-axis__year {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  line-height: 1.1;
}

/* Двойной стиль («15 (28) января») и неточность даты подписаны явно:
   без подписи и то и другое читается как опечатка данных. */
.chr-axis__dual,
.chr-axis__prec {
  font-family: var(--font-caption);
  font-size: calc(var(--t-caption) * 0.8);
  color: var(--text-muted);
  border: 1px solid rgba(var(--telegrey-rgb), 0.4);
  padding: 0 0.4em;
}

/* ------------------------------------------------------------- ячейка */

/* ⚠️ ЯЧЕЙКА СТАЛА СТРОКОЙ «ТЕКСТ + МИНИАТЮРА», А БЫЛА КОЛОНКОЙ, И ЭТО
   ЕДИНСТВЕННАЯ ПРИЧИНА, ПО КОТОРОЙ ПОЯВИЛСЯ `.chr-cell__main`.
   Выключка (`align-items`) переехала на него ЦЕЛИКОМ: на строчном флексе
   `flex-end` прижимает не к правому краю, а к НИЖНЕМУ, и правило,
   оставшееся на месте, выглядело бы исполненным и молча делало другое. */
.chr-cell {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: calc(var(--gap) / 2);
  font-size: var(--t-body);
  line-height: 1.45;
}

.chr-cell__main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: calc(var(--gap) / 2);
}

.chr-cell.is-mil .chr-cell__main {
  align-items: flex-end;
  text-align: right;
}

/* В двухпутке военная половина зеркальна — миниатюра уходит к внешнему
   краю вслед за текстом, а не встаёт между колонками. */
.chr-cell.is-mil { flex-direction: row-reverse; }

/* ── ЦВЕТ ЛИНИИ (правка dvn 24.08) ────────────────────────────────────────
   Кромка ячейки — ЗАЛИВКА предметным цветом, третий признак линии поверх
   позиции и подписи. Пустая половина односторонней строки кромки НЕ несёт:
   цветная черта рядом с пустотой обещала бы содержимое, которого нет. */
.chr-cell.is-pol { --line-c: var(--line-pol); }
.chr-cell.is-mil { --line-c: var(--line-mil); }

.chr-cell:not(.is-blank) {
  border-inline-start: calc(6px * var(--content-scale, 1)) solid var(--line-c);
  padding-inline-start: calc(var(--gap) / 2);
}

/* Двухпутка: у военной колонки кромка зеркалится к внешнему краю — она
   идёт по той же стороне, что и выключка текста, и обе колонки читаются
   как две дорожки, а не как одна со сдвигом. */
.chr-cell.is-mil:not(.is-blank) {
  border-inline-start: 0;
  border-inline-end: calc(6px * var(--content-scale, 1)) solid var(--line-c);
  padding-inline-start: 0;
  padding-inline-end: calc(var(--gap) / 2);
}

/* Асимметричная раскладка: обе линии выключены влево, зеркалить нечего. */
.chr.is-asym .chr-cell.is-mil { flex-direction: row; }

.chr.is-asym .chr-cell.is-mil:not(.is-blank) {
  border-inline-start: calc(6px * var(--content-scale, 1)) solid var(--line-c);
  border-inline-end: 0;
  padding-inline-start: calc(var(--gap) / 2);
  padding-inline-end: 0;
}

/* Пустая половина односторонней строки — факт данных, не дырка: заглушки
   тут быть не должно, она обещала бы недостающее. */
.chr-cell.is-blank {
  min-height: 0;
}

.chr-cell__text p {
  margin: 0 0 calc(var(--gap) / 3);
}

.chr-cell__text p:last-child {
  margin-bottom: 0;
}

/* МИНИАТЮРА ДОП-МАТЕРИАЛА (правка dvn 24.08).
   Числа макета делятся на 1.35 — макет в экранных px панели, наш контент
   множится на `--content-scale`: 180×134 → 133×99. Писать экранные напрямую
   нельзя, это молча отключило бы регулятор оператора и режим слабовидящих.
   ⚠️ Бокс ФИКСИРОВАН намеренно: JS выбирает тир по его фактической ширине
   (`_thumbBox` меряет этот самый класс), и «пусть картинка сама решит»
   означало бы перепись позиций строк, протухающую от каждой догрузки.
   Якорь кадра — `50% 0%`, как у всех портретов раздела после правки
   обрезки голов: у событий кадры групповые, и темя режется так же. */
.chr-cell__thumb {
  flex: 0 0 auto;
  width: calc(133px * var(--content-scale, 1));
  height: calc(99px * var(--content-scale, 1));
  object-fit: cover;
  object-position: 50% 0%;
  border: 1px solid rgba(var(--brass-rgb), 0.45);
  background: rgba(var(--amber-black-rgb), 0.5);
}

/* Подвал ячейки: метка «на карте» и переход стоят в одной строке — оба
   отвечают на «что ещё есть про это событие», и разносить их по уровням
   значило бы делать вид, что это разные этажи. */
.chr-cell__foot {
  display: flex;
  align-items: center;
  gap: calc(var(--gap) / 2);
  flex-wrap: wrap;
}

.chr-cell.is-mil .chr-cell__foot { justify-content: flex-end; }
.chr.is-asym .chr-cell.is-mil .chr-cell__foot { justify-content: flex-start; }

/* МЕТКА «НА КАРТЕ» — не кнопка и не обещание перехода: она сообщает, что
   у события есть точка на карте, а показывает её касание строки (та же
   механика, что была). Поэтому ни `min-height` тач-цели, ни `:active`:
   это подпись, и делать её похожей на кнопку значило бы обещать нажатие.
   ⚠️ Цвет линии несёт ЗНАЧОК (заливка), слово остаётся приглушённым
   текстом — 1.89 : 1 на графите нечитаемо, замер в блоке предметного цвета. */
.chr-cell__onmap {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--gap) / 3);
  font-family: var(--font-caption);
  font-size: calc(var(--t-caption) * 0.85);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.chr-cell__pin {
  width: calc(22px * var(--content-scale, 1));
  height: calc(22px * var(--content-scale, 1));
  flex: 0 0 auto;
  fill: var(--line-c);
  stroke: var(--line-c);
  stroke-width: 1.2;
}

.chr-cell__pin circle { fill: var(--bg); stroke: none; }

/* Переход к карточке события — свой на каждой линии. Общего перехода на
   строку нет намеренно: у 31 строки линии разные, и один переход увёл бы
   с военной колонки на политическую справку. */
.chr-cell__jump {
  min-height: var(--touch-primary, 96px);
  padding: 0 var(--gap);
  background: rgba(var(--paper-rgb), 0.08);
  border: 1px solid rgba(var(--brass-rgb), 0.45);
  color: var(--active);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}

.chr-cell__jump:active {
  background: rgba(var(--brass-rgb), 0.28);
  color: var(--text-on-dark);
}

/* ── ЧИП ОТБОРА «ЛИНИЯ» В ПАНЕЛИ ПОИСКА ──────────────────────────────────
   🔴 ЕДИНСТВЕННОЕ МЕСТО, ГДЕ ЦВЕТ ЛИНИИ МОГ РАЗОЙТИСЬ МОЛЧА. Чипы рисует
   ЯДРО (`_renderFinderBody`), сцена отдаёт только пары «значение — подпись».
   Пока в ленте линия синяя, а в отборе — безымянный чип, посетитель не
   связывает одно с другим, и ничего при этом не краснеет: разметка верна,
   отбор работает, подписи на месте.
   ⚠️ Вендор при этом НЕ ПРАВИТСЯ: правило живёт здесь и цепляется за
   `data-path`/`data-v`, которые ядро проставляет само. Ключ `track` в зоне
   ровно один (проверено grep-ом по всем сценам) — чужой отбор этим
   правилом не заденется.
   ⚠️ `:not(.is-on)` у цвета нет намеренно: активный чип заливается латунью
   ядра, и плашка линии на нём остаётся той же — иначе выбранная линия
   теряла бы свой цвет ровно в тот момент, когда она выбрана. */
.kiosk-finder__chip[data-path="f:track"] {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--gap) / 3);
}

.kiosk-finder__chip[data-path="f:track"][data-v="pol"] { --line-c: var(--line-pol); }
.kiosk-finder__chip[data-path="f:track"][data-v="mil"] { --line-c: var(--line-mil); }

.kiosk-finder__chip[data-path="f:track"][data-v="pol"]::before,
.kiosk-finder__chip[data-path="f:track"][data-v="mil"]::before {
  content: "";
  flex: 0 0 auto;
  width: calc(26px * var(--content-scale, 1));
  height: calc(14px * var(--content-scale, 1));
  background: var(--line-c);
  transform: skewX(-15deg);
}

/* ---------------------------------------------------------------- карта */

/* Полигон-заливка поверх базы, режим A контракта оверлеев. Композиция
   тривиальна: оба файла в кадре Альберс 0 0 1820 954.1, никакого
   масштабирования — совпадение по построению. */
.map {
  margin: var(--gap) 0 0;
  padding: 0;
}

.map__stage {
  background: rgba(var(--amber-black-rgb), 0.25);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
}

.map__svg {
  display: block;
  width: 100%;
  height: auto;
}

/* ЗАЛИВКА ПО ЛАГЕРЮ — из временных токенов за оградой, чтобы dvn увидел
   цвета живьём. Роли `--map-*` тут НЕ участвуют: база придёт растром,
   и роли уйдут; лагерь — отдельный вопрос и отдельный долг.

   Было до этого: контур латунью на ВСЕХ лагерях. Честно показывало
   геометрию и честно не показывало сторону — но заодно красило любую
   территорию в цвет «активного состояния», то есть спорило с латунью
   сильнее, чем спорит национальный лагерь. Уходит вместе с этой правкой. */
.map__overlay {
  fill: var(--map-fill, none);
  fill-opacity: 0.45;
  stroke: var(--map-fill, var(--active));
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
}

.map__overlay[data-camp="red"]          { --map-fill: var(--camp-red); }
.map__overlay[data-camp="white"]        { --map-fill: var(--camp-white); }
.map__overlay[data-camp="national"]     { --map-fill: var(--camp-national); }
.map__overlay[data-camp="rev-dem"]      { --map-fill: var(--camp-rev-dem); }
.map__overlay[data-camp="green"]        { --map-fill: var(--camp-green); }
.map__overlay[data-camp="intervention"] { --map-fill: var(--camp-intervention); }
.map__overlay[data-camp="uprising"]     { --map-fill: var(--camp-uprising); }

/* Геометрии нет у 53 записей из 59 — состояние данных, не отказ.
   Заглушка обязана выглядеть недоделанной, а не аккуратной. */
.map__none {
  margin: 0;
  padding: var(--gap);
  border: 2px dashed rgba(var(--telegrey-rgb), 0.45);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  text-align: center;
}

/* Пустая подпись НЕ занимает бокс. Без этого она давала 1065×0 — ноль
   по одной оси, который проба ловит как схлоп. Здесь схлопа нет, но
   исключение в правило я вносить не стала: правило верное, а пустой
   элемент действительно не должен занимать место. Чинится причина,
   а не проверка. */
.map__cap:empty { display: none; }

/* Подпись ПОСЕТИТЕЛЮ — та же типографика, но узел отдельный: тумблер
   технических подписей её не касается (`map-view.js`, разбор там же).
   `:empty` по той же причине, что у `.map__cap`: пустой узел не должен
   занимать бокс, иначе проба увидит схлоп по одной оси. */
.map__note:empty { display: none; }
.map__note {
  margin: calc(8px * var(--content-scale, 1)) 0 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  text-align: center;
}

.map__cap {
  margin-top: calc(var(--gap) / 3);
  font-family: var(--font-caption);
  font-size: calc(var(--t-caption) * 0.9);
  color: var(--text-muted);
}

/* Подогнанный руками полигон — сигнал, а не примечание. */
.map__alarm { color: var(--signal); }

/* Официальное длинное название — ОТДЕЛЬНЫЙ элемент, не заголовок.
   У партий `title_full_ru` в справке это многострочное название из
   источника, а не полная форма подписи: одно имя поля, два смысла,
   различает только сторона (content §13.5a). */
.p29-card__official {
  margin: 0 0 var(--gap);
  font-family: var(--font-text);
  font-size: var(--t-body);
  line-height: 1.35;
  color: var(--text-muted);
  white-space: pre-line;
}

/* Таймлайн срезов территории — UI, а не пиксели: подписи вынесены в данные
   затем, чтобы приложение нарисовало их своими средствами (контракт §2).
   Это же и есть «анимация региона» — переключение фаз на готовых данных,
   а не проигрыватель. */
.map__phases {
  display: flex;
  flex-wrap: wrap;
  gap: var(--touch-gap, 16px);
  margin-top: calc(var(--gap) / 2);
}

.map__phase {
  min-height: var(--touch-primary, 96px);
  padding: 0 var(--gap);
  background: rgba(var(--paper-rgb), 0.06);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}

.map__phase.is-active {
  background: var(--active);
  border-color: var(--active);
  color: var(--text-on-light);
}

.map__phase:active { background: rgba(var(--brass-rgb), 0.35); }

/* Канва аттрактора. Лежит в корне приложения ПЕРЕД вуалью ядра, поэтому
   порядок наложения берётся из DOM: материал внизу, призыв «Коснитесь
   экрана» сверху. Ядро при своём аттракторе прячет собственную канву
   (`.kiosk-standby.is-bare .kiosk-standby__canvas`), рисовать некуда,
   кроме своего слоя. */
/* z-index 49 — НЕ на глаз, а из замера чужих слоёв: сцена (`.kiosk-stage`)
   z=1, навигация 30, лупа и шестерёнка 45, сервис-панель 70, вуаль заставки
   `.kiosk-standby` z=50. Аттрактор обязан лежать ВЫШЕ сцены и НИЖЕ вуали,
   то есть строго между 45 и 50.

   Без z-index канва оставалась с `auto` и уходила ПОД сцену: `z=1` создаёт
   контекст наложения выше любого `auto`, сколько бы канва ни стояла позже
   в DOM. На экране это выглядело как «аттрактор рисуется, но его не видно» —
   портрет под лентой хроники. Поймано снимком после того, как замер уже
   сказал «нарисовано, непрозрачных сэмплов 4141»: рисовалось честно,
   а видно не было. Тот же класс, что бокс 0×0, — только наоборот. */
.attract__canvas {
  position: absolute;
  inset: 0;
  z-index: 49;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

.attract__canvas[hidden] { display: none; }

/* Плашка «перевода нет» — вместо длинного текста в английском режиме.
   Не «загрузка» и не пустота: штатное состояние надолго (переводов 0 из 198,
   и содержание по решению заказчика не переводится вовсе). Поэтому она
   выглядит как сообщение, а не как заглушка-обещание. */
.p29-card__untranslated {
  margin: 0;
  padding: var(--gap);
  border: 2px dashed rgba(var(--telegrey-rgb), 0.45);
  font-family: var(--font-caption);
  font-size: var(--t-body);
  line-height: 1.4;
  color: var(--text-muted);
  max-width: calc(1180px * var(--content-scale, 1));
}

/* Пометка «по-русски». Стоит там, где русский ОСТАЁТСЯ в английском
   режиме, — у заголовка и дат. Ключевое в решении dvn: не «молча русский».
   Помеченный русский не молчит, а пустой заголовок сломал бы навигацию. */
.c-ru {
  display: inline-block;
  margin-left: 0.4em;
  padding: 0 0.35em;
  border: 1px solid rgba(var(--brass-rgb), 0.5);
  font-family: var(--font-caption);
  font-size: 0.5em;
  vertical-align: middle;
  color: var(--active);
}

/* ------------------------------------------------- карта рядом с хроникой */

/* Вариант Б задания: «карта дышит по дате». Колонка справа от ленты,
   включается настройкой — dvn сравнивает варианты на стенде. */
.chr.has-map .chr__body { min-width: 0; }

/* ⚠️ МЕСТО В СЕТКЕ ЗАДАЁТСЯ ЯВНО КАЖДОМУ. Таймлайн, добавленный без
   `grid-row`, уехал авторазмещением ВНИЗ — а по ТЗ он вверху главной.
   На экране это выглядело как «полоса внизу», а не как дефект вёрстки:
   элемент на месте, просто не на том. Тот же класс, что ряды сетки
   по 120 px — автоматика молча выбрала не то. */
.chr.has-map {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(calc(420px * var(--content-scale, 1)), 30%);
  grid-template-rows: auto auto auto auto 1fr;
  column-gap: var(--gap);
}

/* 🔴 ЗДЕСЬ СТОЯЛО `.chr.has-map .chr__head`, И КЛАССА `chr__head` В РАЗДЕЛЕ
   БОЛЬШЕ НЕТ — его снял ход 2 (масштхед стал общим, `lib/masthead.js`,
   класс `mh`). Правило продолжало быть синтаксически верным и НИ С ЧЕМ
   не совпадало, а масштхед оставался без места в сетке — и уезжал
   АВТОРАЗМЕЩЕНИЕМ ВНИЗ. Замер на стенде: заголовок «ХРОНИКА» стоял
   на 1735 px, то есть ПОД лентой, у самой навигации, при карте включённой
   по умолчанию. Раздел выглядел не сломанным, а странным — и потому
   пережил и линтер, и пробу, и сдачу хода 2 «0 отказов».
   ⚠️ Ровно тот случай, о котором предупреждает комментарий ТРЕМЯ СТРОКАМИ
   ВЫШЕ, слово в слово: «элемент на месте, просто не на том». Правило
   было записано и нарушено в соседней строке — гейт на мёртвые селекторы
   у зоны пока не заведён, ловится это только глазами на живом экране.
   ⚠️ И `chr__count` тоже был без места: счётчик уезжал туда же. Ему
   заведена своя строка (сетка стала пятирядной), а не «куда-нибудь
   в первый ряд» — соседство с кнопками режима разошлось бы при первом
   же длинном числе.
   ⚠️ ЗДЕСЬ СТОЯЛА ОГРАДА, И ЕЁ БОЛЬШЕ НЕТ — не удаляю молча, потому что
   пустота на этом месте прочтётся как «тут ничего не было». Ограда гласила:
   мёртвые `.p29__head`, `.chr__head`, `.chr__title`, `.venn__head`,
   `.venn__title` не трогать, пока `.p29__head` читает стенд `design`.
   Снята координатором 2026-08-25 (`6fbfbab`, наряд `docs/zones/app.md`):
   `design` подтвердила, что класс остался только в её пробах
   (`venn-probe.html`, `design29-preview.js`), на приёмке она их не
   открывает, а все живые вхождения — в `app/**`. Блоки сняты, сдача —
   замером по девяти разделам до и после, а не объявлением. */
/* 🔴 КНОПКИ РЕЖИМА УЕХАЛИ ИЗ ПЕРВОГО РЯДА, И ЭТО ВТОРАЯ ПОЛОВИНА ТОГО ЖЕ
   ДЕФЕКТА. Пока масштхед был без места и лежал внизу, первый ряд стоял
   пустым, и кнопки в нём «работали». Как только заголовок встал на своё
   место, он занял ряд целиком (`h1` — блок во всю ширину) и НАКРЫЛ их
   собой: стили кнопок исправны, кнопки видны, палец до них не доходит.
   Поймала ПРОБА попаданием (`elementFromPoint` по верху, центру и низу):
   «button: перехвачено: верх→mh__title, центр→mh__title, низ→mh__title»,
   оба режима сразу. Стилями это не видно вовсе — мерить надо попаданием.
   Теперь ряд второй, и он общий со счётчиком: счётчик у левого края,
   кнопки у правого. */
.chr.has-map .mh            { grid-column: 1; grid-row: 1; }
.chr.has-map .chr__count    { grid-column: 1; grid-row: 2; justify-self: start; align-self: center; }
.chr.has-map .chr__timeline { grid-column: 1; grid-row: 3; }
.chr.has-map .chr__years    { grid-column: 1; grid-row: 4; }
.chr.has-map .chr__body     { grid-column: 1; grid-row: 5; }

.chr__map {
  grid-column: 2;
  grid-row: 1 / -1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 2);
  padding-left: var(--gap);
  border-left: 1px solid rgba(var(--brass-rgb), 0.3);
}

.chr__map[hidden] { display: none; }

/* ВОЗРАСТ СРЕЗА подписан всегда и не мелким кеглем: 126 строк из 396
   попадают на один и тот же срез сентября 1917-го, и без подписи карта
   выглядела бы актуальной, отставая на месяцы. Это не сноска, это
   единственное, что отличает приближение от подмены. */
.chr-map__note {
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  line-height: 1.35;
  color: var(--text-muted);
}

/* Подсказка про касание строки — ОТДЕЛЬНЫЙ узел под картой, а не текст
   подписи: со входным срезом обе роли пришлись на один момент, а один
   узел может играть только одну. Гаснет после первого касания строки,
   возвращается сбросом визита. Тише подписи: подпись говорит, ЧТО на
   карте, подсказка — что с ней делать. */
.chr-map__hint {
  /* ⚠️ Здесь стоял `var(--sp-2, 8px)` — шкалы отступов нет ни в вендоре,
     ни в зоне, токен приехал из чужого проекта и работал только запасом.
     Местная идиома одна: пиксель × `--content-scale`. */
  margin: calc(8px * var(--content-scale, 1)) 0 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  line-height: 1.35;
  color: var(--text-muted);
  opacity: 0.75;
}

.chr-map__hint[hidden] { display: none; }

/* Лист события — крупное изображение с подписью сюжета. Не карта:
   георефренса у него нет, возить пальцем и сверять с базой нечем.
   `contain` намеренно: лист это скан целиком, обрезка съела бы легенду
   и рамку, ради которых его и показывают. */
.ev-sheet {
  margin: var(--gap) 0 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 3);
}

.ev-sheet__img {
  width: 100%;
  height: auto;
  display: block;
  background: rgba(var(--amber-black-rgb), 0.35);
}

.ev-sheet__cap {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

/* Точка события на общей базе (вариант А). Цвет — ИНТЕРФЕЙСНЫЙ сигнальный,
   не предметный: точка говорит «вот здесь», а не «чья это сторона».
   Лагерь у событий отсутствует по данным (camp 0 из 36), и красить точку
   предметным цветом значило бы приписать событию сторону. */
.map__place-dot {
  fill: var(--signal);
  stroke: var(--paper);
  stroke-width: 3;
}

.map__place-ring {
  fill: none;
  stroke: var(--signal);
  stroke-width: 3;
  opacity: 0.55;
}

/* Подпись в единицах кадра: карта тянется, и пиксельный кегль поехал бы
   отдельно от точки. */
.map__place-label {
  fill: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: 34px;
  paint-order: stroke;
  stroke: var(--amber-black);
  stroke-width: 6;
  stroke-linejoin: round;
}

/* ------------------------------------------------- операционный лист (режим B) */

/* Лист — САМОСТОЯТЕЛЬНАЯ карта в своей системе координат, а не оверлей
   поверх базы. Кадр у поволжской 0 0 302 204; сверять его с кадром Альберс
   было бы неверно применённым правилом. */
.map__sheet { background: var(--map-paper); }

/* Цвет — РОЛЬЮ из паспорта карты (`data-role`), и роли объявлены один раз
   в предметном блоке. Роли стороны — алиасы к лагерям: красный на карте
   и красный в списке буквально одно значение, разойтись не могут. */
.map__sheet [data-role="map-paper"]        { fill: var(--map-paper); }
.map__sheet [data-role="map-territory"]    { fill: var(--map-territory); }
.map__sheet [data-role="map-sea"]          { fill: var(--map-sea); }
.map__sheet [data-role="map-decor"]        { fill: var(--map-decor); }

.map__sheet [data-role="map-border"]       { stroke: var(--map-border); fill: none; }
.map__sheet [data-role="map-river"]        { stroke: var(--map-river); fill: none; }
.map__sheet [data-role="map-front"]        { stroke: var(--map-front); fill: none; }
.map__sheet [data-role="map-frame"]        { stroke: var(--map-frame); fill: none; }

.map__sheet [data-role="map-red"]          { fill: var(--map-red); stroke: var(--map-red); }
.map__sheet [data-role="map-white"]        { fill: var(--map-white); stroke: var(--map-white); }
.map__sheet [data-role="map-green"]        { fill: var(--map-green); stroke: var(--map-green); }
.map__sheet [data-role="map-uprising"]     { fill: var(--map-uprising); stroke: var(--map-uprising); }
.map__sheet [data-role="map-intervention"] { fill: var(--map-intervention); stroke: var(--map-intervention); }

.map__sheet [data-role="map-city"]         { fill: var(--map-city); }
.map__sheet [data-role="map-label"]        { fill: var(--map-label); }

/* ================================================= ЛОНГРИД «СИМБИРСК» ==== */

.lr {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 2);
  padding: var(--chrome-top, 64px) var(--chrome-right, 64px)
           var(--chrome-bottom, 80px) var(--chrome-left, 64px);
  color: var(--text-on-dark);
  font-family: var(--font-text);
}

.lr, .lr * { box-sizing: border-box; }

.lr__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--t-h1);
  font-weight: 400;
  line-height: 1.1;
}

.lr__sub {
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

.lr__body { flex: 1 1 auto; min-height: 0; padding-right: var(--scrollbar-w, 18px); }

.lr-sec { margin-bottom: calc(var(--gap) * 2); }

.lr-sec__title {
  margin: 0 0 calc(var(--gap) / 2);
  font-family: var(--font-display);
  font-size: var(--t-h2);
  font-weight: 400;
  font-style: normal;      /* курсива у Nolde нет — файла начертания не существует */
}

.lr-sec__lede {
  margin: 0 0 var(--gap);
  font-size: var(--t-body);
  line-height: 1.45;
  color: var(--active);
}

.lr-sec__text { font-size: var(--t-body); line-height: 1.5; }
.lr-sec__text p { margin: 0 0 var(--gap); max-width: calc(1180px * var(--content-scale, 1)); }

/* ЗАГЛУШКА ФОНДА. `media[].file` ведёт в longread/placeholders — это место
   под изображение с подписью куратора, а не изображение. Подпись сохраняем
   (это его работа), но за фото не выдаём. */
.lr-ph { margin: 0 0 var(--gap); max-width: calc(880px * var(--content-scale, 1)); }

.lr-ph__box {
  display: flex;
  align-items: center;
  justify-content: center;
  height: calc(160px * var(--content-scale, 1));
  border: 2px dashed rgba(var(--telegrey-rgb), 0.45);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

.lr-ph figcaption {
  margin-top: calc(var(--gap) / 3);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

/* ЗАПРОШЕНО И НЕ ПОЛУЧЕНО — 21 позиция. Именно этого старый рантайм
   не читал, и лонгрид выглядел законченным. Сигнальная рамка, не серая:
   это не оформление, это недостающая работа заказчика. */
.lr-wanted {
  margin: 0 0 var(--gap);
  padding: calc(var(--gap) / 1.5);
  border-left: 3px solid var(--signal);
  background: rgba(var(--paper-rgb), 0.04);
  max-width: calc(880px * var(--content-scale, 1));
}

/* 🔴 ТЕЛО БУКВ — БУМАГОЙ, А НЕ СИГНАЛОМ. Замер: `--signal` (красный
   #A02128) на подложке `rgba(paper,.04)` над тёмным полем давал
   **1.03 при пороге 3.0** — буквы практически неразличимы.
   Канон `DESIGN-CODE` §1, вердикт `design` 25.08: «красный — сигнал,
   не поверхность» и «тело букв в слабый цвет не красим нигде».
   У нас это третий такой случай (линии Хроники, латунные ссылки).
   ⚠️ Красный НЕ УБРАН — он переехал в свою роль: точка-маркер перед
   строкой. Сигнал остался сигналом, читаемость появилась.
   🔒 Плашку НЕ ПРЯЧЕМ — решение dvn 25.08: она честно называет, каких
   материалов музей не прислал, и на показе остаётся видимой. Задача была
   сделать её читаемой, а не спрятать. */
.lr-wanted__head {
  margin: 0 0 calc(var(--gap) / 3);
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-on-dark);
}

.lr-wanted__head::before {
  content: "";
  flex: 0 0 auto;
  width: 0.45em;
  height: 0.45em;
  border-radius: 50%;
  background: var(--signal);
}

.lr-wanted ul { margin: 0; padding-left: 1.2em; font-family: var(--font-caption); font-size: var(--t-caption); color: var(--text-muted); }

/* Ссылки секции — кнопками ≥ --touch-primary: в тексте они некликабельны
   намеренно, цель несёт кнопка. */
.lr-refs { display: flex; flex-wrap: wrap; gap: var(--touch-gap, 16px); margin-top: var(--gap); }

.lr-ref {
  min-height: var(--touch-primary, 96px);
  padding: 0 var(--gap);
  background: rgba(var(--paper-rgb), 0.06);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}

.lr-ref:active { background: rgba(var(--brass-rgb), 0.28); }

/* Подпись-сирота из кэша `ref_labels`: id жив, ссылка цела, а в индексе
   записи нет. Проверка существования такого не ловит — видно только здесь. */
.lr-ref.is-stale { border-color: rgba(var(--red-rgb), 0.6); border-style: dashed; color: var(--text-muted); }
.lr-ref.is-pending { border-style: dashed; background: none; color: var(--text-muted); }

.lr-maps { display: grid; gap: var(--gap); margin: var(--gap) 0; }

.lr-sources { margin-top: calc(var(--gap) * 2); font-family: var(--font-caption); font-size: var(--t-caption); color: var(--text-muted); }
.lr-sources h2 { font-family: var(--font-display); font-size: var(--t-h2); font-weight: 400; color: var(--text-on-dark); }

/* ------------------------------------------------------------- таймлайн */

/* Ось ВСЕЙ хроники, не текущего года: по ТЗ он вверху главной и связывает
   разделы. Высота столбика — от фактического числа событий в месяце,
   знаменатель один на всю ленту (максимум 20). Ровная линейка соврала бы,
   что война шла равномерно: месяцев с событиями 57 из 72, медиана 7. */
.chr__timeline {
  flex: 0 0 auto;
  /* Полоса — единственная тач-цель здесь, и её высота задана явно
     не ниже нормы основного контрола. */
  min-height: var(--touch-primary, 96px);
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: calc(72px * var(--content-scale, 1));
  padding-bottom: calc(var(--gap) / 3);
  border-bottom: 1px solid rgba(var(--brass-rgb), 0.3);
  position: relative;
}

/* Столбики — ПОКАЗАНИЕ, не кнопки: 72 месяца на ширину экрана дают 33 px,
   вдвое ниже нормы `--touch-min` 64, и вёрсткой это не лечится. Цель —
   вся полоса (2600×127), месяц определяется по координате нажатия.
   `pointer-events: none` обязателен: иначе столбик перехватывает попадание
   обратно, и цель снова становится 33 px. */
.chr-tl__m {
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  pointer-events: none;
}

.chr-tl__bar {
  display: block;
  width: 100%;
  background: rgba(var(--paper-rgb), 0.35);
}

/* Месяц текущего года — латунью: это «где я сейчас», активное состояние. */
.chr-tl__m.is-year .chr-tl__bar { background: var(--active); }

.chr-tl__m:active .chr-tl__bar { background: var(--text-on-dark); }

/* ПУСТОЙ МЕСЯЦ ОСТАЁТСЯ В ОСИ и не кликабелен. Выкинуть его значило бы
   сжать время и соврать, что пропусков не было; сделать кликабельным —
   завести кнопку, ведущую в никуда. Пятнадцать таких месяцев. */
.chr-tl__m.is-empty { cursor: default; }
.chr-tl__m.is-empty .chr-tl__bar {
  height: 2px !important;
  background: rgba(var(--telegrey-rgb), 0.25);
}

.chr-tl__scale {
  position: absolute;
  right: 0;
  top: 0;
  font-family: var(--font-caption);
  font-size: calc(var(--t-caption) * 0.8);
  color: var(--text-muted);
}

/* Временный служебный раздел — справочник карт. Рамка на всю сцену;
   оформления не заводим: страница несёт своё, и она временная. */
.guide {
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 2);
  min-height: 0;
  height: 100%;
}

.guide__note {
  margin: 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

.guide__frame {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  background: var(--paper);
}

/* ─────────────────────────────────────────────────────────── лайтбокс */

/* 🔒 ЛАЙТБОКС — ЕДИНСТВЕННОЕ МЕСТО, ГДЕ КАРТИНКА КРУПНЕЕ КОЛОНКИ.
   Правило `design` «содержание показывается целиком» тут в самой строгой
   форме: кадр не обрезается ни при каких пропорциях, и поля берёт
   картинка, а не рамка. Отсюда `contain` — здесь он законен, потому что
   `aspect-ratio` нет и бокс задан вьюпортом, а не пропорцией. */
.p29-lb {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--gap);
  padding: var(--gap);
  background: rgba(var(--amber-black-rgb), 0.94);
}

.p29-lb__img {
  grid-column: 2;
  grid-row: 1;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  justify-self: center;
  align-self: center;
}

.p29-lb__cap {
  grid-column: 1 / -1;
  grid-row: 2;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
  align-items: baseline;
  justify-content: center;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

.p29-lb__inv { color: var(--text-muted); opacity: 0.75; }
.p29-lb__n { color: var(--text-muted); opacity: 0.75; }

/* Навигация и закрытие — навигационные цели: порог `--touch-primary`,
   а не `--touch-min`. Ими пользуются, стоя перед стендом, и промах
   по «вперёд» в лайтбоксе означает закрытие или ничего. */
.p29-lb__close,
.p29-lb__step {
  min-width: var(--touch-primary, 96px);
  min-height: var(--touch-primary, 96px);
  border: 1px solid rgba(var(--brass-rgb), 0.35);
  background: rgba(var(--amber-black-rgb), 0.55);
  color: var(--text-on-dark);
  font-size: var(--t-h3);
  line-height: 1;
}

.p29-lb__close { position: absolute; top: var(--gap); right: var(--gap); }
.p29-lb__step.is-prev { grid-column: 1; grid-row: 1; }
.p29-lb__step.is-next { grid-column: 3; grid-row: 1; }
.p29-lb__close:active,
.p29-lb__step:active { border-color: var(--active); }

/* ─────────────────────────── размер изображения: два положения */

/* Положение `frame` — растянуть на всю рамку. Ветвление по ПРОПОРЦИИ
   делать нечем: CSS не знает натуральных размеров. Поэтому рамку задаёт
   ширина колонки, а потолок высоты остаётся прежним; вертикальному
   кадру, упирающемуся в потолок, `contain` даёт поля — и это честная
   цена положения, в котором мы РАСТЯГИВАЕМ. В положении `native`
   (умолчание) полей не бывает вовсе. */
/* ⚠️ ОБЁРТКА ТОЖЕ ОБЯЗАНА РАСПРЯМИТЬСЯ. `.p29-shots__lead` ужат до
   картинки (`fit-content`) ради мелких карточек, и `width: 100%` на
   картинке считался бы от УЖЕ УЖАТОЙ обёртки — растяжение выходило
   ×1.28 вместо ×2.81, то есть настройка выглядела исполненной и
   работала вполовину. */
[data-image-fit="frame"] .p29-shots__lead { width: 100%; }

[data-image-fit="frame"] .p29-shots__big {
  width: 100%;
  max-width: none;
  object-fit: contain;
}

/* Маркер функциональный: он обязан читаться как оговорка музея,
   а не как элемент композиции. Тише подписи и над картинкой —
   утверждение про изображение стоит до изображения, а не после. */
.p29-shots__upscaled {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  opacity: 0.8;
}

/* ──────────────────────────────── раздел «Партии»: три вида A · B · D */

/* 🔒 ТРИ ВИДА ПОД ПОСТОЯННЫМ ПЕРЕКЛЮЧАТЕЛЕМ (решение dvn 28.09), прототип
   design `design/parties-variants.html`. Поле Эйлера снято (ответ (а)
   координатора) — его правила ушли вместе с ним, история в git.
   Числа прототипа — экранные при 3840 (`--content-scale` 1.35), поэтому
   база = прототип / 1.35 и дальше × `--content-scale`, как в хабе.
   Тач-пороги — токенами кита, НЕ масштабом: масштаб про плотность,
   порог про палец. */
.pv { gap: calc(13.3px * var(--content-scale, 1)); }
.pv__head { position: relative; flex: 0 0 auto; display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: var(--gap); }
/* Масштхед не сжимается, как у Хроники: его декоративная буквица выходит
   за бокс, и сжимаемый узел с вылетом штатная проба честно считает
   налезанием (замер: 284 при боксе 211 у обеих сцен). */
.pv__head .mh { flex: 0 0 auto; }

/* Переключатель — постоянный орган посетителя: 96, активный латунью,
   подпись вида и пояснение ≥24 px (design 748c385). */
.pv__views {
  flex: 0 0 auto; display: flex; overflow: hidden;
  border: 1.5px solid rgba(var(--brass-rgb), 0.5);
  border-radius: calc(8.9px * var(--content-scale, 1));
}
.pv__view {
  min-height: var(--touch-primary, 96px);
  padding: 0 calc(23.7px * var(--content-scale, 1));
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
  background: transparent; color: var(--text-on-dark);
  border: 0; border-left: 1.5px solid rgba(var(--brass-rgb), 0.28);
  font-family: var(--font-caption); font-size: calc(20px * var(--content-scale, 1));
  letter-spacing: 0.04em; line-height: 1.1;
}
.pv__view:first-child { border-left: 0; }
.pv__view small {
  font-size: max(var(--type-min-label, 24px), calc(17.8px * var(--content-scale, 1)));
  color: var(--text-muted); letter-spacing: 0.02em;
}
.pv__view.is-on { background: var(--active); color: var(--graphite); }
/* Пояснение активного — графитом целиком: 0.75 на латуни давало 2.84 < 3. */
.pv__view.is-on small { color: var(--graphite); }
.pv__view:active { outline: 2px solid var(--active); }

/* «Спектр на год» — только вид A. */
.pv__yrbar { position: absolute; top: 0; right: 0; display: flex; justify-content: flex-end; align-items: center; gap: calc(7.4px * var(--content-scale, 1)); }
:root.a11y .pv__yrbar { position: static; flex: 0 0 100%; order: 3; flex-wrap: wrap; }
.pv__yrbar[hidden] { display: none; }
.pv__yrlab, .pv__yrcnt {
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(17.8px * var(--content-scale, 1)));
}
.pv__yrlab { letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); margin-right: calc(3px * var(--content-scale, 1)); }
.pv__yrcnt { color: var(--active); margin-left: calc(6px * var(--content-scale, 1)); min-width: calc(140px * var(--content-scale, 1)); }
.pv__yrcnt:empty { display: none; }
/* 🔴 ГОД — РЕЖИМ ПОЛЯ, ПОРОГ 96 ПО КАНОНУ (в прототипе 64). Как годы Хроники.
   ЦЕЛЬ ПРЯМОУГОЛЬНАЯ, «ПИЛЮЛЯ» — НА ПОДЛОЖКЕ `::before`: у скруглённой
   кнопки углы бокса вне фигуры, и палец в углу попадает в соседа
   (форма GRABLI «скошенная цель»). */
.pv__yr {
  position: relative; isolation: isolate;
  min-height: var(--touch-primary, 96px); min-width: calc(71px * var(--content-scale, 1));
  padding: 0 calc(11.9px * var(--content-scale, 1));
  font-family: var(--font-caption); font-size: calc(20px * var(--content-scale, 1));
  color: var(--text-on-dark); background: transparent; border: 0;
}
.pv__yr::before {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: 999px;
  background: rgba(var(--window-rgb), 0.14); border: 1.5px solid rgba(var(--brass-rgb), 0.3);
}
.pv__yr.is-on { color: var(--graphite); }
.pv__yr.is-on::before { background: var(--active); border-color: var(--active); }
.pv__yr:active::before { border-color: var(--active); }

.pv__field { flex: 1 1 auto; min-height: 0; position: relative; }

/* ── вид A: карта-поле ── */
.pv-a { position: absolute; inset: 0; }
/* Подписи полос — крупно и к своей половине поля (dvn: «это тач-экран,
   не сайт»): латунная скобка во всю высоту полосы + подпись посередине. */
.pv-a__band {
  position: absolute; left: 0; width: calc(333px * var(--content-scale, 1));
  display: flex; align-items: center; z-index: 1; padding-left: calc(22px * var(--content-scale, 1));
}
.pv-a__band::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: calc(9px * var(--content-scale, 1));
  border: 3px solid rgba(var(--brass-rgb), 0.75); border-right: 0;
  border-radius: calc(7.4px * var(--content-scale, 1)) 0 0 calc(7.4px * var(--content-scale, 1));
}
.pv-a__band.is-up { top: 4.2%; bottom: calc(50% + 25px * var(--content-scale, 1)); }
.pv-a__band.is-dn { top: calc(50% + 25px * var(--content-scale, 1)); bottom: 4.2%; }
.pv-a__band span {
  font-family: var(--font-caption); font-size: calc(40px * var(--content-scale, 1));
  line-height: 1.06; color: var(--paper);
}
/* Ось — бокс толщиной с линию (нулевой бокс штатная проба считает дефектом). */
.pv-a__axis {
  position: absolute; left: calc(385px * var(--content-scale, 1)); right: calc(47px * var(--content-scale, 1));
  top: calc(50% - 5.2px * var(--content-scale, 1)); height: calc(10.4px * var(--content-scale, 1));
  z-index: 2; pointer-events: none;
}
.pv-a__line {
  position: absolute; left: 0; right: 0; top: 0; bottom: 0; border-radius: 999px;
  background: linear-gradient(90deg, var(--camp-red) 0%, var(--camp-red) 30%, var(--camp-rev-dem) 50%,
    var(--window) 70%, var(--camp-white) 100%);
}
.pv-a__line::before, .pv-a__line::after {
  content: ""; position: absolute; top: 50%; transform: translateY(-50%); width: 0; height: 0;
  border-top: calc(22px * var(--content-scale, 1)) solid transparent;
  border-bottom: calc(22px * var(--content-scale, 1)) solid transparent;
}
.pv-a__line::before { left: calc(-22px * var(--content-scale, 1)); border-right: calc(34px * var(--content-scale, 1)) solid var(--camp-red); }
.pv-a__line::after { right: calc(-22px * var(--content-scale, 1)); border-left: calc(34px * var(--content-scale, 1)) solid var(--camp-white); }
/* Буквы — бумагой, цвет лагеря — свотчем: телом букв на графите он не читается. */
.pv-a__lab {
  position: absolute; top: calc(31.9px * var(--content-scale, 1)); transform: translateX(-50%);
  display: flex; align-items: center; gap: calc(8.9px * var(--content-scale, 1));
  font-family: var(--font-caption); font-size: calc(25.2px * var(--content-scale, 1));
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--paper); white-space: nowrap;
}
.pv-a__lab::before { content: ""; width: calc(14.8px * var(--content-scale, 1)); height: calc(14.8px * var(--content-scale, 1)); border-radius: 4px; background: var(--pc); }
.pv-a__lab.is-l { left: 9%; } .pv-a__lab.is-m { left: 50%; } .pv-a__lab.is-r { left: 90%; }
.pv-a__plane {
  position: absolute; left: calc(355.6px * var(--content-scale, 1)); right: calc(17.8px * var(--content-scale, 1));
  top: calc(41.5px * var(--content-scale, 1)); bottom: calc(41.5px * var(--content-scale, 1));
}

/* Плашка партии МЯГКО (dvn: «заливка целиком — грубо»): кольцо вокруг
   медальона — цвет лагеря, плашка нейтральная, от медальона вправо
   градиент цвета лагеря в серое. Медальон — ≥92 на экране (решение dvn). */
/* 🔴 ЦЕЛЬ ПРЯМОУГОЛЬНАЯ, ПИЛЮЛЯ — НА ПОДЛОЖКЕ (та же форма, что у года). */
.pv-chip {
  position: absolute; transform: translate(-50%, -50%); isolation: isolate;
  display: flex; align-items: center; gap: calc(9.6px * var(--content-scale, 1));
  min-height: var(--touch-min, 64px);
  padding: calc(5.9px * var(--content-scale, 1)) calc(17.8px * var(--content-scale, 1))
           calc(5.9px * var(--content-scale, 1)) calc(7.4px * var(--content-scale, 1));
  white-space: nowrap; color: var(--paper); background: transparent; border: 0;
  transition: opacity 0.35s ease, filter 0.35s ease;
}
.pv-chip::before {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: 999px;
  border: 1.5px solid rgba(var(--paper-rgb), 0.22);
  box-shadow: 0 calc(4.4px * var(--content-scale, 1)) calc(13.3px * var(--content-scale, 1)) rgba(var(--amber-black-rgb), 0.32);
  background: linear-gradient(90deg, color-mix(in srgb, var(--pc) 62%, transparent) 0,
    color-mix(in srgb, var(--pc) 26%, transparent) 30%, rgba(var(--window-rgb), 0.16) 72%);
}
.pv-chip:active { filter: brightness(1.12); }
.pv-chip.is-off { opacity: 0.14; filter: saturate(0.15); }
.pv-chip__med {
  width: calc(68px * var(--content-scale, 1)); height: calc(68px * var(--content-scale, 1));
  flex: 0 0 auto; border-radius: 50%; overflow: hidden; background: var(--amber-black);
  border: calc(4.4px * var(--content-scale, 1)) solid var(--pc);
}
.pv-chip__med img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 24%; filter: grayscale(0.15); display: block; }
.pv-chip__dot { width: calc(14.8px * var(--content-scale, 1)); height: calc(14.8px * var(--content-scale, 1)); flex: 0 0 auto; border-radius: 50%; background: var(--pc); }
.pv-chip__nm { font-family: var(--font-text); font-size: calc(25.2px * var(--content-scale, 1)); line-height: 1.02; }
/* Лагерь-якорь («общая» запись) — пунктиром, подписью капителью. */
.pv-chip.is-gen::before { border-radius: calc(7.4px * var(--content-scale, 1)); border-style: dashed; border-color: var(--pc); background: rgba(var(--window-rgb), 0.1); box-shadow: none; }
/* Имя якоря — бумагой: цветом лагеря на графите было 1.88 < 3; цвет несут точка и пунктир. */
.pv-chip.is-gen .pv-chip__nm { font-family: var(--font-caption); font-size: calc(20px * var(--content-scale, 1)); letter-spacing: 0.1em; text-transform: uppercase; color: var(--paper); }

/* 🔴 a11y: ПОЛЕ A НЕ ВМЕЩАЕТ 29 ПЛАШЕК ×1.5 — замер: 52 налегания после
   80 проходов; при имени ×1.2 — 14 и 7 плашек не ловят палец. Медальон
   держит 92 (решение dvn «≥92»; без множителя), имя — как в обычном
   режиме (×1.0), тач-порог — режима (80). Отступление от общего множителя режима
   названо числом; приёмка a11y вида A — design (в прототипе a11y нет). */
:root.a11y .pv-chip__med {
  width: calc(68px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  height: calc(68px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  border-width: calc(4.4px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}
:root.a11y .pv-chip__nm { font-size: calc(25.2px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)); }
:root.a11y .pv-chip {
  gap: calc(9.6px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  padding: calc(5.9px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5))
           calc(17.8px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5))
           calc(5.9px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5))
           calc(7.4px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

/* ПАРТИИ · a11y ВИДА A «КАРТА-ПОЛЕ» — ход design (28.09, для app-268).
   Источник: design 8bb2984 `design/parties-a11y-A.css`, вставлено app-311.

   Причина дефекта — не плашки (они в a11y уже ×1.0), а ПОЛЕ: в a11y оно
   теряет 331 px высоты и 240 px ширины.
     шапка 211 → 485: «Спектр на год» уехал третьей строкой под масштхед;
     колонка подписей полос 450 → 674 (кегль ×1.5), поле уехало вправо;
     поля плоскости сверху/снизу ×1.5.
   Ход:
   1) Шапка a11y — сетка: масштхед слева на две строки, справа «Спектр на
      год» над переключателем вида. Шапка = высоте масштхеда (316).
   2) Годы a11y — две строки: «СПЕКТР НА ГОД … в 1918: 28 из 28» над
      кнопками. Счётчик не меняет ширину строки годов — иначе масштхед
      сжимается, переносится, и поле теряет 58 px (замер: +5 налеганий).
   3) Колонка подписей полос и левый край оси — обычной ширины (кегль
      подписи полосы 54, крупнее имён). Подписи ОСИ (Красные/Рев.дем/Белые)
      остаются ×1.5.
   4) Плоскость без полей сверху/снизу; у плашки вертикальный отступ 4 px
      (плашка 108 → 100; медальон 92 не трогается — решение dvn).
   Замер 3840×2160, a11y включён тапом, CDP (design/_tools/cdp-drv.mjs):
     app-268 как есть: поле 2956×1050, налеганий 18, мимо пальца 7;
     с ходом:          поле 3196×1386, налеганий 0, мимо пальца 0, 48 проходов;
     годы Все/1917…1922 тапом — 0/0 каждый; шапка 317 на любом годе;
     годы и виды попаданием 9/9 точек, зазор годов 20 (было 15 < 16);
     B, D — строка годов скрыта, поле 1387; тап по плашке → своя справка.
     Запас: на вьюпорте 3840×2073 (на 87 px ниже) — тоже 0/0.
   Обычный режим не затронут (всё под :root.a11y): 0/0, тап → справка. */

:root.a11y .pv__head {
  display: grid; grid-template-columns: auto auto; justify-content: space-between; align-items: end;
  row-gap: var(--touch-gap, 16px); column-gap: var(--gap);
}
:root.a11y .pv__head .mh { grid-column: 1; grid-row: 1 / span 2; align-self: end; }
:root.a11y .pv__views { grid-column: 2; grid-row: 2; justify-self: end; }

:root.a11y .pv__yrbar {
  grid-column: 2; grid-row: 1; justify-self: end; position: static; order: 0;
  display: grid; grid-template-columns: repeat(7, auto); grid-template-rows: auto auto;
  column-gap: var(--touch-gap, 16px); row-gap: calc(4px * var(--content-scale, 1));
  align-items: center; justify-content: end;
}
:root.a11y .pv__yrbar[hidden] { display: none; }
:root.a11y .pv__yrlab { grid-row: 1; grid-column: 1 / 5; justify-self: start; margin: 0; }
:root.a11y .pv__yrcnt { grid-row: 1; grid-column: 5 / 8; justify-self: end; margin: 0; min-width: 0; }
:root.a11y .pv__yr { grid-row: 2; }

:root.a11y .pv-a__band {
  width: calc(333px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  padding-left: calc(22px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}
:root.a11y .pv-a__band span { font-size: calc(40px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)); }
:root.a11y .pv-a__axis { left: calc(385px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)); }
:root.a11y .pv-a__plane { left: calc(355.6px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)); top: 0; bottom: 0; }
:root.a11y .pv-chip {
  padding-top: calc(3px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  padding-bottom: calc(3px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

/* ── вид B: динамика (Гантт, доли — без замера скрытого блока) ── */
.pv-b { position: absolute; inset: 0; display: flex; flex-direction: column; --name-w: calc(414.8px * var(--content-scale, 1)); }
.pv-b__top { flex: 0 0 auto; display: flex; align-items: flex-end; height: calc(29.6px * var(--content-scale, 1)); margin-bottom: calc(5.9px * var(--content-scale, 1)); }
.pv-b__top > span { flex: 0 0 var(--name-w); align-self: stretch; }
.pv-b__yrs { flex: 1; position: relative; height: 100%; margin-right: calc(52px * var(--content-scale, 1)); }
.pv-b__yrs span { position: absolute; bottom: 0; transform: translateX(-50%); font-family: var(--font-caption); font-size: calc(20px * var(--content-scale, 1)); color: var(--text-muted); }
.pv-b__yrs span.is-key { color: var(--active); }
.pv-b__wrap { flex: 1 1 auto; min-height: 0; position: relative; }
.pv-b__over { position: absolute; left: var(--name-w); right: calc(52px * var(--content-scale, 1)); top: 0; bottom: 0; z-index: 0; pointer-events: none; }
.pv-b__over i { position: absolute; top: 0; bottom: 0; width: 0; border-left: 1px dashed rgba(var(--paper-rgb), 0.09); }
.pv-b__over i.is-key { border-left: 2px solid rgba(var(--brass-rgb), 0.45); }
.pv-b__body { position: absolute; inset: 0; overflow-y: auto; z-index: 1; padding-right: calc(7.4px * var(--content-scale, 1)); }
.pv-ghead { display: flex; align-items: center; height: calc(43px * var(--content-scale, 1)); margin-top: calc(10.4px * var(--content-scale, 1)); }
.pv-ghead:first-child { margin-top: 0; }
.pv-ghead__t {
  flex: 0 0 var(--name-w); display: flex; align-items: center; gap: calc(8.9px * var(--content-scale, 1));
  font-family: var(--font-caption); font-size: calc(23px * var(--content-scale, 1)); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--paper);
}
.pv-ghead__t i { width: calc(16.3px * var(--content-scale, 1)); height: calc(16.3px * var(--content-scale, 1)); border-radius: 4px; background: var(--pc); flex: 0 0 auto; }
.pv-ghead__t b { color: var(--active); font-weight: 400; }
.pv-ghead__ln { flex: 1; height: 2px; background: var(--pc); opacity: 0.45; }
/* Строка ≥ --touch-min: имя — кнопка во всю высоту строки (design 241c3ea). */
.pv-row { display: flex; align-items: stretch; height: max(var(--touch-min, 64px), calc(53.3px * var(--content-scale, 1))); border-bottom: 1px solid rgba(var(--paper-rgb), 0.06); }
.pv-row__nm {
  flex: 0 0 var(--name-w); display: flex; align-items: center; min-height: var(--touch-min, 64px);
  padding: 0 calc(13.3px * var(--content-scale, 1)) 0 0; background: transparent; border: 0; text-align: left;
  font-family: var(--font-text); font-size: calc(22.2px * var(--content-scale, 1)); color: var(--paper);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pv-row__nm:active { color: var(--active); }
.pv-row__trk { flex: 1; position: relative; height: 100%; margin-right: calc(44.4px * var(--content-scale, 1)); }
.pv-row__bar {
  position: absolute; top: calc(50% - 14.8px * var(--content-scale, 1)); height: calc(29.6px * var(--content-scale, 1));
  min-width: calc(10.4px * var(--content-scale, 1)); border-radius: calc(5.2px * var(--content-scale, 1)); background: var(--pc);
}
.pv-row__bar.is-pre::before, .pv-row__bar.is-post::after {
  position: absolute; top: 50%; transform: translateY(-50%); color: var(--pc);
  font-size: calc(20.7px * var(--content-scale, 1)); line-height: 1;
}
.pv-row__bar.is-pre::before { content: "‹"; left: calc(-16.3px * var(--content-scale, 1)); }
.pv-row__bar.is-post::after { content: "›"; right: calc(-16.3px * var(--content-scale, 1)); }
.pv-b__hint { flex: 0 0 auto; margin: calc(6px * var(--content-scale, 1)) 0 0; font-family: var(--font-caption); font-size: max(var(--type-min-label, 24px), calc(17px * var(--content-scale, 1))); color: var(--text-muted); }

/* ── вид D: лейблы (грамматика карточек гособразований) ── */
.pv-d {
  position: absolute; inset: 0; overflow-y: auto; padding-right: calc(11.9px * var(--content-scale, 1));
  display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); grid-auto-rows: max-content;
  gap: calc(8.9px * var(--content-scale, 1)); align-content: start;
}
/* Лагерь — плитка в общем потоке, а не строка во всю ширину: нет пустот
   после малых лагерей (dvn: меньше прокрутки). */
.pv-grp {
  position: relative; display: flex; flex-direction: column; justify-content: flex-end;
  gap: calc(5.9px * var(--content-scale, 1));
  padding: calc(14.8px * var(--content-scale, 1)) calc(13.3px * var(--content-scale, 1))
           calc(16.3px * var(--content-scale, 1)) calc(22.2px * var(--content-scale, 1));
  border-radius: calc(7.4px * var(--content-scale, 1));
  background: linear-gradient(180deg, color-mix(in srgb, var(--pc) 38%, transparent), rgba(var(--window-rgb), 0.1) 70%);
  border: 1.5px solid rgba(var(--brass-rgb), 0.28);
}
.pv-grp::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: calc(8.9px * var(--content-scale, 1)); border-radius: calc(7.4px * var(--content-scale, 1)) 0 0 calc(7.4px * var(--content-scale, 1)); background: var(--pc); }
.pv-grp__t { font-family: var(--font-caption); font-size: calc(22.2px * var(--content-scale, 1)); line-height: 1.1; letter-spacing: 0.06em; text-transform: uppercase; color: var(--paper); }
.pv-grp__n { font-family: var(--font-display); font-size: calc(68px * var(--content-scale, 1)); line-height: 0.9; color: var(--active); }
.pv-grp__u { font-family: var(--font-caption); font-size: calc(15.6px * var(--content-scale, 1)); color: var(--text-muted); }
.pv-lc {
  position: relative; display: flex; flex-direction: column; overflow: hidden; text-align: left; padding: 0;
  border-radius: calc(7.4px * var(--content-scale, 1)); color: var(--paper);
  background: rgba(var(--window-rgb), 0.16); border: 1.5px solid rgba(var(--brass-rgb), 0.3);
}
.pv-lc:active { background: rgba(var(--window-rgb), 0.26); }
.pv-lc::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: calc(5.9px * var(--content-scale, 1)); background: var(--pc); z-index: 2; }
/* Вертикальная плашка — портреты вертикальные (dvn). */
.pv-lc__plate { position: relative; display: block; aspect-ratio: 1 / 1.15; overflow: hidden; background: var(--amber-black); border-bottom: 1px solid rgba(var(--brass-rgb), 0.22); }
/* Якоря названы (`--anchor`, конвенция файла): подложка — размытый фон,
   срез ей безразличен; снимок — к верху, головы не резать (задача №6). */
.pv-lc__fg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 12%; filter: grayscale(0.15); --anchor: portrait-top; }
/* Плашка аббревиатуры: цвет лагеря заливкой, буквы бумагой; на светлых
   лагерях — графитом (конвенция проекта). */
.pv-lc__badge {
  position: absolute; left: calc(10.4px * var(--content-scale, 1)); top: calc(8.9px * var(--content-scale, 1)); z-index: 3;
  font-family: var(--font-caption); font-size: calc(17px * var(--content-scale, 1)); line-height: 1;
  color: var(--paper); background: var(--pc); border-radius: 4px;
  padding: calc(4.4px * var(--content-scale, 1)) calc(7.4px * var(--content-scale, 1));
}
/* На светлых лагерях — чёрным янтарём: графит давал 4.25 < 4.5 (мелкий кегль). */
.pv-lc[data-camp="white"] .pv-lc__badge, .pv-lc[data-camp="national"] .pv-lc__badge { color: var(--amber-black); }
.pv-lc__body { flex: 1 1 auto; display: flex; flex-direction: column; gap: calc(3.7px * var(--content-scale, 1)); padding: calc(5.9px * var(--content-scale, 1)) calc(8.9px * var(--content-scale, 1)) calc(7.4px * var(--content-scale, 1)) calc(13.3px * var(--content-scale, 1)); }
.pv-lc__nm {
  font-family: var(--font-text); font-size: calc(17.8px * var(--content-scale, 1)); line-height: 1.1;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  min-height: calc(39.3px * var(--content-scale, 1));
}
.pv-lc__dt { font-family: var(--font-caption); font-size: calc(14.8px * var(--content-scale, 1)); color: var(--active); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pv-lc__who { margin-top: auto; display: flex; align-items: center; gap: 6px; padding-top: 4px; border-top: 1px solid rgba(var(--paper-rgb), 0.1); white-space: nowrap; }
.pv-lc__who img { width: calc(25px * var(--content-scale, 1)); height: calc(25px * var(--content-scale, 1)); border-radius: 50%; object-fit: cover; object-position: 50% 22%; margin-right: calc(-13px * var(--content-scale, 1)); }
.pv-lc__who span { margin-left: calc(16px * var(--content-scale, 1)); font-family: var(--font-caption); font-size: calc(14.8px * var(--content-scale, 1)); color: var(--text-muted); }
/* a11y: портреты и «Члены партии: N» в одну строку не входят (замер:
   257–327 px при месте 244, nowrap резал подпись у 13 партий) — подпись
   своей строкой под портретами. */
:root.a11y .pv-lc__who { flex-wrap: wrap; row-gap: 4px; }
:root.a11y .pv-lc__who span { flex: 1 0 100%; margin-left: 0; }

/* Цвет лагеря — из предметного слоя (один на всё приложение). */
.pv [data-camp="red"] { --pc: var(--camp-red); }
.pv [data-camp="white"] { --pc: var(--camp-white); }
.pv [data-camp="rev-dem"] { --pc: var(--camp-rev-dem); }
.pv [data-camp="national"] { --pc: var(--camp-national); }
.pv [data-camp="green"] { --pc: var(--camp-green); }
.pv [data-camp="uprising"] { --pc: var(--camp-uprising); }
.pv [data-camp="intervention"] { --pc: var(--camp-intervention); }

/* ─────────────────────────────────────── карта (компонент atlas) */

/* 🔴 КАМЕР-ОБЁРТОК И РАСТРОВОЙ БАЗЫ БОЛЬШЕ НЕТ — задание №21, карты на MapLibre. Перелёт делает движок,
   холст занимает бокс карты целиком. Правила удалены, а не оставлены
   «на всякий случай»: мёртвое правило читается как действующее и
   переживает того, кто знал, что оно мертво. */
.atlas { position: absolute; inset: 0; }
.atlas canvas { display: block; outline: none; }

/* 🔴 ПРАВИЛА SVG-КОНТУРА УДАЛЕНЫ ВМЕСТЕ С УЗЛОМ — задание
   №21. Контур рисует MapLibre тремя слоями (слабая заливка + размытый
   край + кромка), цвет берётся из поля `camp` самих данных, а не из
   атрибута разметки. Оставить правила «на всякий случай» нельзя:
   мёртвое правило читается как действующее и переживает того, кто знал,
   что оно мертво. Условие «под контуром обязана читаться география»
   осталось в силе и живёт теперь в `app/lib/atlas-style.js`
   (`fill-opacity` 0.17). */

/* ─────────────────────── карточка: фото по ПРАВИЛУ, а не по размеру */

/* 🔒 ЗОНА ФОТО — ПОТОЛОК, А НЕ БОКС. Требование `design`, и оно же
   доказано её замером: ведущее эсеров натурально 1000×667; в высоком
   боксе тянулось до 1000×1170 — апскейл ×1.75. По правилу ниже показ
   1000×667, апскейл ×1.00, обрезка 0.
   ❌ НЕ задавать `aspect-ratio` фиксом. ❌ НЕ `cover` в высокий бокс:
   он «выполняет задачу» на вид и режет кадр молча. */
/* 🔴 ЗДЕСЬ БЫЛО `width: 100%` ПО БУКВЕ ЗАЯВКИ — И ЭТО РАСТЯГИВАЛО.
   Замер поймал сразу: событие с нативом 277×469 показывалось 900×1160,
   апскейл ×3.25. Буква спека («width:100% колонки, ≤1000») и его же
   требование («апскейла нет») несовместимы для снимков УЖЕ колонки,
   а таких 7 из 181 — их мы мерили с `design` два дня назад.
   Держу инвариант: колонка — ПОТОЛОК по обеим осям, ширина от снимка.
   Крупные (натив ≥ колонки) занимают её целиком, мелкие остаются
   мелкими и резкими. Положение `frame` оператора по-прежнему может
   растянуть — с маркером, правило ниже. */
.p29-card__photo .p29-shots__big {
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: calc(580px * var(--content-scale, 1));
  object-fit: contain;
}

/* Оверлей на нижней трети ведущего: скрим вниз, чтобы белый текст
   читался на любом снимке — и на светлом небе, и на тёмной шинели. */
/* ⚠️ ОБЁРТКА ВЕДУЩЕГО РАСПРЯМЛЯЕТСЯ В КОЛОНКЕ ФОТО. `.p29-shots__lead`
   ужата до картинки (`fit-content`) — это ход `design` под семь мелких
   карточек, и он остаётся в силе там, где ширину диктует снимок.
   Здесь ширину диктует КОЛОНКА: без этой строки фото показывалось
   900 px при колонке 1000 — старое правило прежней раскладки молча
   ужимало новую. */
/* 🔴 ФИГУРА ГАЛЕРЕИ ТОЖЕ ЖИЛА В ПРЕЖНЕЙ РАСКЛАДКЕ. `.p29-shots` была
   липкой колонкой шириной 450 базовых (900 экранных) внутри текста —
   в новой раскладке она сама КОЛОНКА листа, и старая ширина ужимала
   фото до 900 при колонке 1000. Три правила прежней вёрстки цеплялись
   друг за друга: фигура 900 → обёртка 100 % от 900 → картинка 100 %
   от обёртки. Ни одно из них не выглядело виноватым по отдельности. */
.p29-card__photo .p29-shots {
  position: static;
  width: 100%;
  grid-row: auto;
}

.p29-card__photo .p29-shots__lead {
  position: relative;
  width: 100%;
  max-width: none;
}

/* 🔴 ОВЕРЛЕЙ НЕ ЛОВИТ ТАПЫ. Он лежит НАД ведущим снимком и является ему
   СОСЕДОМ, а не родителем: `data-shot-big` стоит на самом `<img>`
   (`card-view.js:344`), поэтому `closest("[data-shot-big]")` от узла
   оверлея не находит ничего, и тап по нижней трети фото просто пропадал —
   лайтбокс не открывался. Аудит 23.08 нашёл это через чип «На карте»,
   но перехватывал весь оверлей: заголовок, даты, бейдж.
   ⚠️ Заведёшь здесь настоящий орган управления — верни ему
   `pointer-events: auto` поимённо, иначе он не нажмётся, и это будет
   выглядеть как «не работает код». */
.p29-card__over {
  position: absolute;
  inset: auto 0 0 0;
  pointer-events: none;
  display: grid;
  gap: calc(6px * var(--content-scale, 1));
  padding: calc(16px * var(--content-scale, 1));
  background: linear-gradient(to top,
    rgba(var(--amber-black-rgb), 0.88) 0%,
    rgba(var(--amber-black-rgb), 0.72) 45%,
    transparent 100%);
  color: var(--text-on-dark);
}

/* Бейдж типа: различает ИКОНКА, не цвет — цвет здесь предметный (лагерь),
   и у разных типов он совпадает сплошь и рядом. */
.p29-card__badge {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: calc(8px * var(--content-scale, 1));
  min-height: calc(42px * var(--content-scale, 1));
  padding: 0 calc(12px * var(--content-scale, 1));
  background: rgba(var(--amber-black-rgb), 0.7);
  border: 1px solid rgba(var(--brass-rgb), 0.5);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}
.p29-card__badge > i { font-style: normal; font-size: 1.2em; }

/* Афиша «На карте» — только там, где карта есть (события, гособразования).
   🔴 ЭТО МЕТКА, А НЕ КНОПКА, и выглядеть она обязана меткой. Сплошная
   латунная плашка 135×57 читалась именно кнопкой — посетитель по ней
   давил, а она ничего не делала: обработчика у неё нет и не было
   (аудит 23.08). Приведена к виду бейджа типа рядом: та же тёмная
   подложка, та же латунная обводка, то же семейство подписи.
   ⚠️ Решишь всё-таки сделать её органом — тогда это `<button>`,
   `pointer-events: auto` и китовый порог `--touch-min` 64, а не 42:
   нынешние 42 × 1.35 = 57 ниже порога, и «сделать кнопкой» без этого
   означает завести МЕЛКУЮ кнопку вместо мёртвой. */
.p29-card__mapchip {
  position: absolute;
  top: calc(-1 * (42px * var(--content-scale, 1) + 16px * var(--content-scale, 1)));
  right: calc(16px * var(--content-scale, 1));
  min-height: calc(42px * var(--content-scale, 1));
  display: inline-flex;
  align-items: center;
  padding: 0 calc(12px * var(--content-scale, 1));
  background: rgba(var(--amber-black-rgb), 0.7);
  border: 1px solid rgba(var(--brass-rgb), 0.5);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}

/* 🔴 ПЛАШКА «НА КАРТЕ» СТАЛА ОРГАНОМ (№25 п.4, решение `design`
   34dd33d) — ровно по условию, записанному выше: `<button>`,
   `pointer-events: auto` поимённо (оверлей тапы не ловит) и китовый
   порог `--touch-min`, а не 42 базовых. Высота выросла — подъём над
   оверлеем считается от неё же, иначе плашка налезла бы на бейдж.
   Отличие от метки — сплошная латунная обводка: метка у гособразований
   осталась меткой и выглядит по-прежнему. */
.p29-card__mapchip.is-go {
  pointer-events: auto;
  min-height: var(--touch-min, 64px);
  top: calc(-1 * (var(--touch-min, 64px) + 16px * var(--content-scale, 1)));
  padding: 0 calc(20px * var(--content-scale, 1));
  border-color: rgba(var(--brass-rgb), 0.95);
}
.p29-card__mapchip.is-go:active { border-color: var(--active); }

/* ЛИСТАНИЕ ГАЛЕРЕИ — ЯВНОЕ (№25 п.4): «‹ N / M ›» строкой ПОД снимком.
   Поверх снимка снизу лежит оверлей с заголовком — стрелки по бокам
   легли бы на него. Тач — `--touch-min`: шаг галереи не главное
   действие листа, главное — закрыть (`--touch-primary`). */
.p29-shots__nav {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: calc(12px * var(--content-scale, 1));
  margin-top: calc(12px * var(--content-scale, 1));
}
.p29-shots__step {
  min-width: var(--touch-min, 64px);
  min-height: var(--touch-min, 64px);
  border: 1px solid rgba(var(--brass-rgb), 0.35);
  background: rgba(var(--amber-black-rgb), 0.55);
  color: var(--text-on-dark);
  font-size: var(--t-h3);
  line-height: 1;
}
.p29-shots__step:active { border-color: var(--active); }
.p29-shots__count {
  justify-self: center;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-on-dark);
  font-variant-numeric: tabular-nums;
}

/* 🔁 Сетка 2×2 вместо ленты — решение dvn. Прежний довод за ленту
   («видно 5 из 5 при колонке 1405») верен для ПРЕЖНЕЙ раскладки:
   колонка стала 500 базовых. Смена предмета, не отмена замера. */
.p29-shots__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: calc(12px * var(--content-scale, 1));
  margin-top: calc(12px * var(--content-scale, 1));
}
/* 🔴 ИМЕННО ЭТА СТРОКА И ДЕРЖАЛА КВАДРАТ, а не размеры в базовом правиле:
   в сетке 2×2 ширина тянется колонкой, а форму задаёт `aspect-ratio`.
   Правку 4:3 без неё я бы «сделала» и не увидела — замер показал плитку
   512×109 вместо 146×109, то есть базовое правило тут не работает вовсе. */
/* ⚠️ `height: auto` ОБЯЗАТЕЛЕН, иначе `aspect-ratio` не действует вовсе:
   при заданной высоте пропорция игнорируется, и плитка осталась 512×109.
   Первую правку я так и «сделала» — число в базовом правиле поменялось,
   на экране не изменилось ничего. Поймано замером бокса, не чтением стиля. */
.p29-shots__grid .p29-shots__thumb { width: 100%; height: auto; aspect-ratio: 4 / 3; }

/* Кикер над текстом: тип и лагерь словами, дублирует бейдж. */
.p29-card__kicker {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

/* ═══ ТУМБЛЕР БИОГРАФИИ [Вся биография | Симбирск · 1918] — задание №18
   Макет `design/card-bio-simbirsk.html`, принят dvn. Числа макета экранные
   при 3840, база = макет / 1.35: кнопка 76 → 56, поле 40 → 30, зазор 6 → 4.

   🔒 ТУМБЛЕР ЕСТЬ ТОЛЬКО У КАРТОЧЕК С ДВУМЯ ТЕКСТАМИ (их 4 из 72). Разметку
   рисует `card-view.js`, и её нет вовсе, когда второго текста нет: ни
   спрятанной вкладки, ни выключенной кнопки — отсутствие второго уровня
   показывается отсутствием органа управления.

   ⚠️ Высота кнопки — `max()` с китовым порогом, а не голые 56 базовых:
   на узкой ширине 56 × scale опускается ниже 64, и цель пальца пропала бы
   молча. Тот же приём, что у строки связанного. */
.p29-card__bio {
  display: inline-flex;
  margin-top: calc(13px * var(--content-scale, 1));
  padding: calc(4px * var(--content-scale, 1));
  gap: calc(4px * var(--content-scale, 1));
  border-radius: calc(10px * var(--content-scale, 1));
  background: rgba(var(--paper-rgb), 0.06);
  border: 1px solid rgba(var(--paper-rgb), 0.16);
}

.p29-card__bio-tab {
  display: flex;
  align-items: center;
  gap: calc(10px * var(--content-scale, 1));
  min-height: max(var(--touch-min, 64px), calc(56px * var(--content-scale, 1)));
  padding: 0 calc(30px * var(--content-scale, 1));
  border: 0;
  border-radius: calc(7px * var(--content-scale, 1));
  background: transparent;
  font-family: var(--font-text);
  font-size: calc(24px * var(--content-scale, 1));
  color: var(--text-soft, var(--paper));
  cursor: pointer;
}

/* Активная вкладка — латунью по графиту: то же соответствие «выбрано»,
   что у чипов лагерей, и оно уже прочитано посетителем в других разделах. */
.p29-card__bio-tab[data-on] {
  background: var(--brass);
  color: var(--graphite);
}

/* Красная точка у «Симбирска» — та же предметная метка, что «на карте».
   Она говорит, что текст привязан к месту, а не к персоне целиком. */
.p29-card__bio-tab.is-simb::before {
  content: "";
  flex: 0 0 auto;
  width: calc(12px * var(--content-scale, 1));
  height: calc(12px * var(--content-scale, 1));
  border-radius: 50%;
  background: var(--red);
}

.p29-card__bio-tab.is-simb[data-on]::before { background: var(--graphite); }

/* Область в кикере (у четырёх карточек она заменяет лагерь — решение
   координатора 23.09, вариант А). Тот же кегль и цвет, что у лагеря:
   меняется слово, а не вид строки. */
.p29-card__kicker-scope { color: var(--brass); }

/* Связанное: плашки-тап-цели. Высота 39 базовых = 78 экранных при
   масштабе 2 — выше порога `--touch-min` 64, и занижать её нельзя. */
/* 🔒 СТРОКА СВЯЗАННОГО В КАРТОЧКЕ — ОДНОСТРОЧНАЯ, ВЫСОТА 39 БАЗОВЫХ.
   Спек `design`: 78 экранных при масштабе 2. Прежняя строка росла
   до 270 — миниатюра 56 базовых плюс две текстовые строки, — и в лист
   фиксированной высоты влезало вчетверо меньше связей, чем задумано.

   ⚠️ ЭТО СНИЖЕНИЕ ТАЧ-ЦЕЛИ С 96 ДО 78, И ОНО НАМЕРЕННОЕ. Прежде здесь
   стоял `--touch-primary` (96) по доводу «кнопка рельса — основной
   контрол». `design` считает порогом `--touch-min` (64) и прямо пишет
   «не занижать». 78 > 64, инвариант держится; решение её, замер её.

   Миниатюру СОХРАНЯЮ квадратом в высоту строки: замер по всем 1451 связи
   — с изображением 1309 (90 %), и лицо в списке различается быстрее
   названия. В спеке её нет, но и запрета нет; скажу об этом отдельно. */
.p29-card__rail .p29-rail__row {
  position: relative;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  grid-template-rows: auto;
  align-items: center;
  /* 🔴 ТАЧ-ПОЛ НЕ МАСШТАБИРУЕТСЯ. Здесь стояло просто `39px * scale`:
     при масштабе 2 это 78 (спек `design`), но оператор крутит ручку, и
     на 1.35 строка вышла 60 — НИЖЕ порога 64. Поймано замером после
     правки `scale.content` 2 → 1.35: все 5 строк мельче порога.
     Масштаб — про плотность, порог — про палец; смешивать их нельзя.
     `max()` держит оба: где масштаб крупнее, строка растёт; где мельче,
     упирается в токен. */
  /* 🔴 СТРОКА ВЫРОСЛА: правка dvn 25.08 «связанное было мелко».
     Макет `f0ac625` даёт плашку 26/32 при пороге `--touch-primary`;
     у нас числа макета делятся на 1.35 → 19/24. Прежний пол 39 × scale
     считался ПЛОТНОСТЬЮ, а не пальцем, и на 1.35 давал 60 — ниже 64;
     `max()` оставался единственным, что держало норму. Теперь пол —
     сразу `--touch-primary`: строка стала основным контролом карточки,
     а не строчкой списка. */
  min-height: var(--touch-primary, 96px);
  gap: calc(19px * var(--content-scale, 1));
  padding: calc(19px * var(--content-scale, 1)) calc(24px * var(--content-scale, 1));
  background: rgba(var(--paper-rgb), 0.055);
  border: 1px solid rgba(var(--paper-rgb), 0.16);
  border-radius: calc(9px * var(--content-scale, 1));
  overflow: hidden;
}

.p29-card__rail .p29-rail__row:active {
  background: rgba(var(--brass-rgb), 0.18);
  border-color: var(--active);
}

.p29-card__rail .p29-rail__thumb {
  grid-row: 1;
  grid-column: 1;
  width: calc(31px * var(--content-scale, 1));
  height: calc(31px * var(--content-scale, 1));
}

/* Название в одну строку с многоточием: перенос вернул бы рост строки,
   ради устранения которого всё и делается.
   Кегль 38 из макета (28 × 1.35 = 37.8) вместо прежних 27. */
.p29-card__rail .p29-rail__title {
  grid-column: 2;
  grid-row: 1;
  font-size: max(var(--type-min-ui, 32px), calc(28px * var(--content-scale, 1)));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Число упоминаний и шеврон — ЛАТУНЬЮ и крупно (58 из макета = 43 × 1.35).
   Латунь здесь законна: это активный акцент интерфейса, а не предметный
   цвет, и на тёмном листе она даёт 8.0 : 1. */
.p29-card__rail .p29-rail__n {
  font-size: max(var(--type-min-ui, 32px), calc(43px * var(--content-scale, 1)));
  min-width: calc(56px * var(--content-scale, 1));
  text-align: center;
  color: var(--active);
}

.p29-card__rail .p29-rail__chev {
  font-size: max(var(--type-min-ui, 32px), calc(43px * var(--content-scale, 1)));
  color: var(--active);
}

/* Заголовок «Связанное» — 28 из макета (21 × 1.35 = 28.35) вместо 27. */
.p29-card__rail .p29-rail__head {
  font-size: max(var(--type-min-label, 24px), calc(21px * var(--content-scale, 1)));
}

/* Раздел связи прячется: в одной строке он вытесняет название, а тип
   виден по карточке, в которую ведёт переход. */
.p29-card__rail .p29-rail__where { display: none; }
.p29-card__rail .p29-rail__n { grid-column: 3; grid-row: 1; }
.p29-card__rail .p29-rail__chev { grid-column: 4; grid-row: 1; }

/* Мера заполнения — ФОН ВНУТРИ плашки (правка dvn), не бар слева:
   бар отнимал бы ширину у текста и читался как второй элемент. */
.p29-card__rail .p29-rail__row::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--fill, 0%);
  background: rgba(var(--brass-rgb), 0.14);
  pointer-events: none;
}
.p29-rail__row > * { position: relative; }

.p29-rail__n {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}
.p29-rail__chev { font-size: 1.4em; color: var(--text-muted); }

/* ─────────────────────── Симбирск: раздел, а не простыня */

/* 🔒 ДВЕ РАСКЛАДКИ ВЕДЁМ ОБЕ (решение dvn). V1 — оглавление слева
   и разворот справа: видно целое, 11 глав сразу. V2 — глава-экран
   с точками снизу: погружение в одну. Разметка ОДНА, различает класс. */
.lr { display: flex; flex-direction: column; gap: var(--gap); height: 100%; min-height: 0; }

.lr__stage {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: calc(380px * var(--content-scale, 1)) minmax(0, 1fr);
  gap: var(--gap);
}
.lr.is-v2 .lr__stage { grid-template-columns: minmax(0, 1fr); }
.lr.is-v2 .lr__toc { display: none; }

/* Оглавление: строка главы 39 базовых = 78 экранных при масштабе 2.
   ⚠️ Пол не масштабируется — та же грабля, что стоила строки
   «Связанного» 60 px при ручке 1.35. */
.lr__toc { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: calc(var(--gap) / 4); }

.lr-toc__row {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: calc(var(--gap) / 2);
  min-height: max(calc(39px * var(--content-scale, 1)), var(--touch-min, 64px));
  padding: 0 calc(var(--gap) / 2);
  border: 1px solid rgba(var(--brass-rgb), 0.22);
  background: rgba(var(--amber-black-rgb), 0.35);
  color: var(--text-on-dark);
  text-align: left;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}
.lr-toc__row.is-on { border-color: var(--active); }
.lr-toc__row:active { border-color: var(--active); }
.lr-toc__n { color: var(--text-muted); }
.lr-toc__t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Точки глав (V2): навигационные цели, порог primary. */
.lr__dots { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: var(--touch-gap, 16px); }
.lr.is-v1 .lr__dots { display: none; }
.lr-dot {
  min-width: var(--touch-primary, 96px);
  min-height: var(--touch-primary, 96px);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  background: rgba(var(--amber-black-rgb), 0.45);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}
.lr-dot.is-on { border-color: var(--active); }
.lr-dot:active { border-color: var(--active); }

/* Герой главы — наша схема. Метка отличает её от превью Госкаталога:
   за одно мы отвечаем сами, второе взято на время. */
.lr-sec__hero:empty { display: none; }
.lr-map { position: relative; margin: 0 0 var(--gap); }
.lr-map__mark,
.lr-shot__mark {
  position: absolute;
  top: calc(var(--gap) / 3);
  left: calc(var(--gap) / 3);
  z-index: 1;
  padding: calc(var(--gap) / 6) calc(var(--gap) / 3);
  background: rgba(var(--amber-black-rgb), 0.78);
  color: var(--text-on-dark);
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}

/* 🔴 ПРЕВЬЮ ГОСКАТАЛОГА — ВСЕГДА С МЕТКОЙ. Права ещё оформляются,
   и кадр без пометки читался бы как музейный фонд. Метка стоит НА
   изображении, а не в сноске: сноску глаз пропускает. */
.lr-shot { position: relative; margin: 0 0 var(--gap); }
.lr-shot img {
  width: auto;
  max-width: 100%;
  max-height: calc(320px * var(--content-scale, 1));
  display: block;
}
.lr-shot figcaption,
.lr-map figcaption {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
}

/* ─────────── Симбирск: Витрина и Сцена (заявка design, одобрено dvn) */

/* 🔴 ПЕРЕКЛЮЧАТЕЛЬ ВИДЕН ПОСЕТИТЕЛЮ. Прежде раскладка меняласьсервис-
   панелью: построена была, а пути к ней не было. Пилюли — навигация,
   значит порог `--touch-primary`. */
.lr__modes { flex: 0 0 auto; display: flex; gap: var(--touch-gap, 16px); }
.lr__mode {
  min-height: var(--touch-primary, 96px);
  padding: 0 var(--gap);
  border: 1px solid rgba(var(--brass-rgb), 0.35);
  background: rgba(var(--amber-black-rgb), 0.45);
  color: var(--text-on-dark);
  font-family: var(--font-text);
  font-size: var(--t-body);
}
.lr__mode.is-on { border-color: var(--active); background: rgba(var(--brass-rgb), 0.22); }
.lr__mode:active { border-color: var(--active); }

/* Витрина — мозаика 11 глав, 4 колонки. Глава 8 крупной плиткой:
   у неё 7 изображений из 13, и по материалу она главная. */
/* 🔴 ШАБЛОН СЕТКИ МЕНЯЕТСЯ ВМЕСТЕ С СОДЕРЖИМЫМ. Здесь стоял только
   `display: none` у ненужного блока — а `.lr__stage` сохранял ДВЕ
   дорожки от прежней раскладки (оглавление + текст). Мозаика попадала
   в узкую боковую дорожку: замер на стенде — колонки 513px 3166.6px,
   плитка 104×270 вместо ~800, `.lr__body` 0×0, и 3166 px справа
   простаивали.
   ⚠️ Скрытый элемент не освобождает свою дорожку — он лишь перестаёт
   рисоваться. Тот же класс, что дефект колонок карточки без лагеря:
   там ребёнок выпал из потока и соседи съехали, здесь остался пустой
   трек и соседи ужались. */
/* ⚠️ ОДНА ДОРОЖКА В ОБОИХ РЕЖИМАХ. Правило заводилось под «Витрину»,
   а «Сцену» я не проверила — и получила тот же дефект зеркально:
   `.lr__grid` скрыт, значит грид-элементом не является, `.lr__body`
   встаёт в ПЕРВУЮ дорожку (513 px), вторая (3166) простаивает.
   Замер: глава 495 px шириной при поле 3712.
   Третий случай за смену: скрытый элемент не держит свою дорожку,
   а сосед молча съезжает не туда. */
.lr.is-vitrina .lr__stage { grid-template-columns: minmax(0, 1fr); }

/* «Сцена» — ДВЕ дорожки на уровне поля: рельс и остальное. Третья
   (герой | текст) делится внутри главы, потому что герой живёт
   в секции, а не в поле. */
.lr.is-scene .lr__stage { grid-template-columns: var(--rail-w) minmax(0, 1fr); }
.lr.is-vitrina .lr__body { display: none; }
.lr.is-scene .lr__grid { display: none; }

.lr__grid {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: calc(200px * var(--content-scale, 1));
  gap: var(--gap);
}

.lr-tile {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto auto;
  gap: calc(var(--gap) / 6);
  min-height: var(--touch-min, 64px);
  padding: calc(var(--gap) / 3);
  border: 1px solid rgba(var(--brass-rgb), 0.25);
  background: rgba(var(--amber-black-rgb), 0.4);
  color: var(--text-on-dark);
  text-align: left;
  overflow: hidden;
}
.lr-tile.is-big { grid-column: span 2; grid-row: span 2; }
.lr-tile.is-on { border-color: var(--active); }
.lr-tile:active { border-color: var(--active); }

.lr-tile__media { position: relative; overflow: hidden; display: block; }
.lr-tile__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 30%; display: block; }
.lr-tile__typo {
  display: grid; place-items: center; height: 100%;
  font-family: var(--font-display);
  font-size: calc(64px * var(--content-scale, 1));
  color: rgba(var(--brass-rgb), 0.45);
}
.lr-tile__mark {
  position: absolute; top: 0; left: 0; z-index: 1;
  padding: calc(var(--gap) / 8) calc(var(--gap) / 4);
  background: rgba(var(--amber-black-rgb), 0.8);
  font-family: var(--font-caption); font-size: var(--t-caption);
}
.lr-tile__mark.is-map { background: var(--camp-red); color: var(--text-on-dark); }
.lr-tile__n { font-family: var(--font-caption); font-size: var(--t-caption); color: var(--text-muted); }

/* 🔴 НЕВИДИМО ГЛАЗУ, НО ЧИТАЕТСЯ ВСЛУХ — №22, задвоенные номера глав.
   Не `display: none` и не `visibility: hidden`: оба убирают узел и из
   доступности тоже, а номер главы обязан остаться для чтения вслух —
   видимую его копию несёт литера-заглушка, которая сама `aria-hidden`.
   ⚠️ Размер 1×1 с `clip-path`, а не нулевой бокс: ноль по одной оси
   ловит штатная проба зоны («нулевой бокс по одной оси»), и правка
   доступности покраснела бы как дефект вёрстки. */
.lr-tile__n.is-quiet,
.lr-rail__n.is-quiet {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.lr-tile__title { font-family: var(--font-text); font-size: var(--t-body); line-height: 1.2; }

/* 🔒 ВЕРТИКАЛЬНЫЙ РЕЛЬС ГЛАВ СЛЕВА. Ширина настраиваемая — `--rail-w`,
   умолчание 172 базовых = 344 экранных при масштабе 2 (число спека).
   Герой справа отдаёт и забирает ширину.

   ⚠️ Это НЕ отвергнутое «оглавление слева»: там была колонка ЗАГОЛОВКОВ
   и текст справа — паттерн документа. Здесь ячейка узкая и служебная,
   а хозяин экрана — герой. Разница не в вёрстке, а в том, кто главный. */
/* Ширина рельса — решение dvn 2026-08-25: 400 px на экране и перенос
   названия в две строки. Прежние 172 × 1.35 = 232 давали под текст 136 px,
   и резались ВСЕ 11 названий из 11 (самому длинному нужно 632 в одну
   строку). 296 × 1.35 = 400. Базу держим в множителе `--content-scale`,
   чтобы в режиме слабовидящих рельс рос вместе с текстом, а не отставал. */
.lr { --rail-w: calc(296px * var(--content-scale, 1)); }

.lr__rail {
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 3);
}
.lr.is-vitrina .lr__rail { display: none; }

/* Высота по содержимому плюс зазор: `flex: 0 0 auto` обязателен —
   колоночный флекс иначе жмёт плашки до `min-height`, и текст лезет
   поверх соседней. Это уже случалось в рельсе карточки. */
.lr-rail__row {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: center;
  gap: calc(var(--gap) / 4);
  /* 🔴 ПОРОГ ЗДЕСЬ `--touch-primary`, А НЕ `--touch-min`. Рельс глав —
     НАВИГАЦИЯ, а канон относит навигацию к 96. Я поставила 64 по инерции
     от рельса связей в карточке (там это список ссылок, не навигация),
     и собственная проба сразу покраснела: 11 ячеек 232×64 при норме 96.
     Проверено, что помещается: 11 × 96 плюс зазоры ≈ 1120 при высоте
     рельса 1424 — прокрутки не требуется.
     ⚠️ Тот же вопрос стоит у точек ядра (64 при каноне 96), и он поднят
     заявкой вверх. Пока канон говорит 96 — держим 96 у себя. */
  min-height: var(--touch-primary, 96px);
  padding: calc(var(--gap) / 4);
  border: 1px solid rgba(var(--brass-rgb), 0.22);
  background: rgba(var(--amber-black-rgb), 0.35);
  color: var(--text-on-dark);
  text-align: left;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
}
.lr-rail__row.is-on { border-color: var(--active); background: rgba(var(--brass-rgb), 0.2); }
.lr-rail__row:active { border-color: var(--active); }

.lr-rail__mini {
  width: calc(28px * var(--content-scale, 1));
  height: calc(28px * var(--content-scale, 1));
  object-fit: cover;
  --anchor: "верх — лицо и шапка документа";
  object-position: 50% 25%;
  display: block;
}
.lr-rail__mini.is-map { background: rgba(var(--camp-red-rgb), 0.35); }
.lr-rail__mini.is-typo {
  display: grid; place-items: center;
  color: var(--text-muted);
}
.lr-rail__n { color: var(--text-muted); }
/* Две строки вместо обрезки (решение dvn 25.08). `nowrap` + многоточие
   резали название на первом же слове; перенос читается, а высоту плашки
   держит `flex: 0 0 auto` выше. Третья строка обрезается — это потолок,
   а не отсутствие потолка. */
.lr-rail__t {
  overflow: hidden;
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-height: 1.2;
}

/* Метка карты — красная точка, предметный цвет лагеря красных. */
.lr-rail__map {
  width: calc(8px * var(--content-scale, 1));
  height: calc(8px * var(--content-scale, 1));
  border-radius: 50%;
  background: var(--camp-red);
}

/* ───────── Симбирск, «Сцена»: герой в паспарту (правка dvn через design) */

/* Рамка ~85 % героя, изображение утоплено в тёмное поле — экспонат
   в паспарту, а не полотно во всю ширь. Числа спека экранные при
   масштабе 2, здесь базовые: 70/80 полей = 140/160 на экране. */
/* 🔴 «СЦЕНА» — ДВЕ КОЛОНКИ: ГЕРОЙ ВЕДЁТ, ТЕКСТ ВТОРИЧЕН. Без этого
   правила глава рисовалась одной колонкой, герой выходил 495 px,
   и рамка занимала 56 % × 30 % вместо ~85 % — то есть паспарту было,
   а сцены не было. Замер поймал это сразу: доля рамки к герою.
   ⚠️ Соотношение из спека `design`: изображение крупнее текста,
   а не рядом с ним. */
.lr.is-scene .lr-sec {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: var(--gap);
  align-items: start;
}
/* 🔴 БЕЗ ЭТОЙ СТРОКИ «СЦЕНА» ПОКАЗЫВАЛА ВСЕ 11 ГЛАВ СРАЗУ, простынёй
   54 084 px, и переключение глав было мёртвым. `_goChapter` ставит
   `hidden` честно (`simbirsk.js:321`), но правило выше сильнее
   браузерного `[hidden] { display: none }` — атрибут стоял и не
   действовал. Тот же случай, что и семь других `[hidden]` в этом файле:
   глобального правила нет, поэтому КАЖДОЕ объявление `display` обязано
   вернуть себе `[hidden]` рядом, иначе гасит его молча.
   Мера — попаданием: число видимых секций и высота листа, не чтение стилей. */
.lr.is-scene .lr-sec[hidden] { display: none; }
/* 🔴 `span 99` СНЯТ 25.08: 99 строк = 98 зазоров, и пустые строки давали
   ПУСТОЙ ХВОСТ 3013 px под главой — «Источники» уезжали за две высоты
   экрана. Пролёт теперь считается по числу блоков второй колонки
   и приходит инлайном из `scenes/simbirsk.js`. Запас здесь стоил дороже
   недостачи: пустая строка невидима, её зазор — нет. */
.lr.is-scene .lr-sec__hero { grid-column: 1; grid-row: 1; }
.lr.is-scene .lr-sec__title,
.lr.is-scene .lr-sec__lede,
.lr.is-scene .lr-sec__text,
.lr.is-scene .lr-shot,
.lr.is-scene .lr-ph,
.lr.is-scene .lr-wanted,
.lr.is-scene .lr-refs { grid-column: 2; }

.lr-hero {
  margin: 0 0 var(--gap);
  padding: calc(70px * var(--content-scale, 1)) calc(80px * var(--content-scale, 1));
  background: rgba(var(--amber-black-rgb), 0.5);
  display: grid;
  justify-items: center;
  gap: calc(var(--gap) / 2);
}

.lr-hero__frame {
  width: 100%;
  aspect-ratio: 16 / 11;
  padding: calc(14px * var(--content-scale, 1));
  background: var(--paper);
  border: 1px solid rgba(var(--brass-rgb), 0.5);
  box-shadow: 0 calc(6px * var(--content-scale, 1)) calc(24px * var(--content-scale, 1))
    rgba(var(--amber-black-rgb), 0.55);
  overflow: hidden;
}

/* 🔴 Фото ЗАПОЛНЯЕТ рамку (`cover`), карта ВПИСЫВАЕТСЯ (`contain`).
   Это не вкусовщина: обрезать фотографию можно, обрезать схему нельзя —
   она утверждение о географии, и срезанный край меняет утверждение. */
/* 🔒 ЯКОРЬ ОБРЕЗКИ НАЗВАН, А НЕ ОСТАВЛЕН ПО УМОЛЧАНИЮ. Требование
   `design`: «50% 50% — не названный якорь, а его ОТСУТСТВИЕ». Здесь
   кадр заполняет рамку (`cover`), значит что-то срезается, и решать,
   что именно, обязаны мы, а не браузер. Верх — потому что на музейных
   портретах и обложках смысл в верхней трети: лицо, заголовок, шапка
   документа. Переменная `--anchor` ничего не рисует; она существует,
   чтобы гейт пробы отличал ВЫБОР от УМОЛЧАНИЯ. */
.lr-hero__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  --anchor: "верх — лицо и шапка документа";
  object-position: 50% 25%;
  display: block;
}
.lr-hero__map { width: 100%; height: 100%; display: grid; place-items: center; }
.lr-hero__map svg { max-width: 100%; max-height: 100%; height: auto; width: auto; }
.lr-hero__typo {
  display: grid; place-items: center; height: 100%;
  font-family: var(--font-display);
  font-size: calc(120px * var(--content-scale, 1));
  color: rgba(var(--graphite-rgb), 0.3);
}

/* Музейная подпись на паспарту. Метка обязательна — договорённость
   с `design` и условие прав: превью Госкаталога нельзя показывать
   без пометки, а свою схему нельзя выдавать за чужой фонд. */
.lr-hero__cap {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  letter-spacing: 0.06em;
  color: var(--text-muted);
  text-align: center;
}

/* ═══════════════════════════════════════════════════════════════════════
   ХРОНИКА — ТРИ РАСКЛАДКИ ПОД ПЕРЕКЛЮЧАТЕЛЕМ (решение dvn 28.09 вечер)

   Источник правды — прототип `design/chronicle-rethink.html` (97bfcd0).
   Размеры прототипа — пиксели кадра 3840; здесь они переведены в ДОЛИ
   поля (сетка `fr`) и в токены кегля кита: иначе a11y и `--content-scale`
   прошли бы мимо этой сцены, как проходили бы мимо любого жёсткого px.
   ⚠️ Две раскладки после выбора dvn уйдут — их не полируем.
   📌 Прежние правила `.chr*` ниже по файлу — от прежней Хроники, сцена их
   больше не ставит; снять вместе с лишними раскладками после выбора dvn.
   ═══════════════════════════════════════════════════════════════════ */
.chx {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--gap) / 2);
  padding: var(--chrome-top, 64px) var(--chrome-right, 64px)
           var(--chrome-bottom, 80px) var(--chrome-left, 64px);
  color: var(--text-on-dark);
  font-family: var(--font-text);
}
.chx__head {
  flex: 0 0 auto;
  display: flex;
  align-items: flex-end;
  gap: var(--gap);
}
.chx__head .mh { flex: 0 0 auto; }
.chx__time { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: calc(var(--gap) / 3); }

/* Плотность по месяцам: 72 столбика. Хит — ВСЯ высота полосы, а не
   закрашенная часть: у пустого месяца столбик 4 %, попасть в него пальцем
   нельзя. Ширина столбика меньше китового порога (72 на полосу) — так было
   и в прежней Хронике; соседний месяц — соседний столбик. */
.chx__hist {
  height: var(--touch-min, 64px);
  display: flex;
  align-items: stretch;
  gap: 2px;
}
.chx__bar {
  flex: 1 1 0;
  background: linear-gradient(to top,
    rgba(var(--paper-rgb), 0.22) 0 calc(var(--h, 0) * 100%), transparent 0);
}
.chx__bar.is-year {
  background: linear-gradient(to top,
    rgba(var(--brass-rgb), 0.5) 0 calc(var(--h, 0) * 100%), transparent 0);
}
.chx__bar.is-now {
  background: linear-gradient(to top, var(--brass) 0 calc(max(var(--h, 0), 0.12) * 100%), transparent 0);
}
.chx__years { display: flex; align-items: center; gap: calc(var(--gap) / 2); }
.chx__yearbtns { display: flex; gap: calc(var(--gap) / 3); }
/* Годы — НАВИГАЦИЯ (они в `<nav>`), и кит относит её к `--touch-primary`:
   прототип ставит 64, штатная проба мерит 96 — берём канон, как держала
   его прежняя Хроника. */
.chx__year {
  min-height: var(--touch-primary, 96px);
  padding: 0 calc(12px * var(--content-scale, 1));
  display: inline-flex; align-items: baseline; gap: calc(6px * var(--content-scale, 1));
  background: rgba(var(--window-rgb), 0.14);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  color: var(--text-on-dark);
  font-family: var(--font-caption); font-size: var(--t-caption);
}
.chx__year small { font-size: 0.8em; color: var(--text-muted); }
.chx__year.is-on { background: var(--active); border-color: var(--active); color: var(--graphite); }
/* На латуни мелкая подпись — ТЁМНЫМ, не графитом: графит 0.8 на латуни дал
   3.07 при пороге 4.5 (штатная проба). */
.chx__year.is-on small { color: rgba(var(--amber-black-rgb), 0.8); }
.chx__year:active { border-color: var(--active); }
.chx__legend { margin-left: auto; display: flex; gap: var(--gap); font-family: var(--font-caption); font-size: var(--t-caption); }
.chx__legend span { display: inline-flex; align-items: center; gap: calc(6px * var(--content-scale, 1)); }
.chx__legend b { width: calc(18px * var(--content-scale, 1)); height: calc(6px * var(--content-scale, 1)); }
.chx__legend .is-pol b { background: var(--line-pol); }
.chx__legend .is-mil b { background: var(--line-mil); }

/* Сегмент А/Б/В — МАЛЕНЬКИЙ (решение dvn 28.09: «потом уйдёт, когда
   решим, что рабочее»). Было по ответу design 84e290a — тач 96, латунная
   заливка активного; теперь `--touch-min` 64 (меньше нельзя — инвариант
   тача), буква без подписи, активное — БЕЗ заливки: светлая буква и
   латунная черта снизу. Не спорит с навигацией по времени, у которой
   латунь — главное. Подпись раскладки — в `aria-label` и в дубле
   сервис-панели. ВРЕМЕННЫЙ: после выбора dvn снимается. */
.chx__views {
  flex: 0 0 auto;
  align-self: flex-start;
  display: flex;
  border: 1px solid rgba(var(--brass-rgb), 0.3);
}
.chx__view {
  min-height: var(--touch-min, 64px);
  min-width: var(--touch-min, 64px);
  padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: 0; border-left: 1px solid rgba(var(--brass-rgb), 0.2);
  box-shadow: none;
  color: var(--text-muted);
  font-family: var(--font-caption); font-size: var(--t-caption);
  line-height: 1;
}
.chx__view:first-child { border-left: 0; }
.chx__view.is-on { color: var(--text-on-dark); box-shadow: inset 0 -3px 0 var(--active); }
.chx__view:active { outline: 2px solid var(--active); }
/* Отбор «с роликом» (dvn 29.09): переключатель на ките рядом с сегментом,
   тот же язык — рамка, активное светлым и латунной чертой снизу. Вид —
   до строки design. */
.chx__filmonly {
  flex: 0 0 auto;
  align-self: flex-start;
  min-height: var(--touch-min, 64px);
  padding: 0 calc(var(--touch-min, 64px) / 3);
  background: transparent;
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  box-shadow: none;
  color: var(--text-muted);
  font-family: var(--font-caption); font-size: var(--t-caption);
  line-height: 1;
}
.chx__filmonly.is-on { color: var(--text-on-dark); box-shadow: inset 0 -3px 0 var(--active); }
.chx__filmonly:active { outline: 2px solid var(--active); }

.chx__untranslated { margin: 0; font-family: var(--font-caption); font-size: var(--t-caption); color: var(--text-muted); }
.chx__untranslated[hidden] { display: none; }

/* ── поле ── */
.chx__field { flex: 1 1 auto; min-height: 0; position: relative; display: grid; gap: var(--gap); }
.chx[data-chx-v="A"] .chx__field { grid-template-columns: 42fr 57fr; grid-template-rows: minmax(0, 1fr); }
.chx[data-chx-v="B"] .chx__field { grid-template-columns: 28fr 41fr 28fr; grid-template-rows: auto minmax(0, 1fr); }
.chx__strip { grid-column: 1 / -1; }
.chx__strip[hidden] { display: none; }
.chx__strip .chx-ovr { margin: 0; }
.chx__rib { min-height: 0; overflow-y: auto; padding-right: calc(8px * var(--content-scale, 1)); }
.chx__col { min-height: 0; display: flex; flex-direction: column; }
.chx__col .chx__rib { flex: 1 1 auto; }
.chx__colh {
  flex: 0 0 auto; display: flex; align-items: center; gap: calc(8px * var(--content-scale, 1));
  padding: calc(6px * var(--content-scale, 1)) calc(10px * var(--content-scale, 1));
  border-bottom: 2px solid var(--lc);
  font-family: var(--font-caption); font-size: var(--t-caption);
  letter-spacing: 0.1em; text-transform: uppercase;
}
.chx__colh b { width: calc(12px * var(--content-scale, 1)); height: calc(12px * var(--content-scale, 1)); background: var(--lc); }
.chx__colh span { margin-left: auto; color: var(--active); text-transform: none; letter-spacing: 0.04em; }
.chx__colh.is-pol { --lc: var(--line-pol); }
.chx__colh.is-mil { --lc: var(--line-mil); }
.chx__side, .chx__mid { min-height: 0; display: flex; flex-direction: column; gap: calc(var(--gap) / 2); }
.chx__empty { font-family: var(--font-caption); font-size: var(--t-caption); color: var(--text-muted); }

/* ── заголовок месяца ── */
.chx__month {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: calc(10px * var(--content-scale, 1));
  padding: calc(6px * var(--content-scale, 1)) calc(10px * var(--content-scale, 1));
  margin-bottom: calc(6px * var(--content-scale, 1));
  background: var(--graphite);
  border-bottom: 1px solid rgba(var(--brass-rgb), 0.5);
  font-family: var(--font-caption); font-size: var(--t-caption);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--active);
}
.chx__here {
  margin-left: auto; padding: 0 calc(6px * var(--content-scale, 1));
  background: var(--red); color: var(--paper);
  font-size: 0.85em; letter-spacing: 0.1em;
}

/* ── строка хроники ── */
.chx-row {
  position: relative;
  display: grid;
  grid-template-columns: calc(120px * var(--content-scale, 1)) minmax(0, 1fr);
  gap: calc(12px * var(--content-scale, 1));
  padding: calc(10px * var(--content-scale, 1)) calc(12px * var(--content-scale, 1))
           calc(10px * var(--content-scale, 1)) calc(18px * var(--content-scale, 1));
  margin-bottom: calc(6px * var(--content-scale, 1));
  background: rgba(var(--window-rgb), 0.1);
  border: 1px solid transparent;
}
.chx-row::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: calc(5px * var(--content-scale, 1)); background: var(--lc);
}
.chx-row[data-line="pol"] { --lc: var(--line-pol); }
.chx-row[data-line="mil"] { --lc: var(--line-mil); }
.chx-row[data-line="both"]::before { background: linear-gradient(180deg, var(--line-pol) 0 50%, var(--line-mil) 50% 100%); }
.chx-row:active { background: rgba(var(--window-rgb), 0.22); }
.chx-row.is-sel { background: rgba(var(--window-rgb), 0.22); border-color: var(--active); }
.chx-row__dt { font-family: var(--font-caption); font-size: var(--t-caption); line-height: 1.15; }
.chx-row__ln {
  margin-top: calc(4px * var(--content-scale, 1));
  display: flex; align-items: center; gap: calc(4px * var(--content-scale, 1));
  font-family: var(--font-caption); font-size: calc(var(--t-caption) * 0.72);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted);
}
.chx-row__ln b { width: calc(8px * var(--content-scale, 1)); height: calc(8px * var(--content-scale, 1)); background: var(--lc); }
.chx-row[data-line="both"] .chx-row__ln b { background: linear-gradient(90deg, var(--line-pol) 0 50%, var(--line-mil) 50% 100%); }
.chx-row__tx { font-size: var(--t-body); line-height: 1.34; }
.chx-row__tx p { margin: 0; }
.chx-row__tx a { color: var(--active); text-decoration: none; }
.chx-row__part { padding-left: calc(8px * var(--content-scale, 1)); border-left: 2px solid var(--line-pol); }
.chx-row__part.is-mil { border-left-color: var(--line-mil); margin-top: calc(6px * var(--content-scale, 1)); }
.chx-row__th {
  float: right;
  width: calc(96px * var(--content-scale, 1)); height: calc(64px * var(--content-scale, 1));
  margin: 0 0 calc(6px * var(--content-scale, 1)) calc(10px * var(--content-scale, 1));
  object-fit: cover; border: 1px solid rgba(var(--brass-rgb), 0.3);
  /* Якорь — ВЫБОР раздела «События» (как у их плиток): снимок сцены,
     предмет занимает кадр целиком, к верху увести — срезать низ ни за что. */
  object-position: 50% 50%;
  --anchor: subject-centre;
}
.chx-row__tags { clear: both; margin-top: calc(8px * var(--content-scale, 1)); display: flex; flex-wrap: wrap; gap: calc(6px * var(--content-scale, 1)); }
.chx-tag {
  display: inline-flex; align-items: center; gap: calc(6px * var(--content-scale, 1));
  min-height: calc(32px * var(--content-scale, 1));
  padding: 0 calc(10px * var(--content-scale, 1));
  background: rgba(var(--amber-black-rgb), 0.18);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  font-family: var(--font-caption); font-size: calc(var(--t-caption) * 0.85); color: var(--text-on-dark);
}
.chx-tag.is-map::before { content: ""; width: 0.5em; height: 0.5em; border-radius: 50%; background: var(--red); }
/* Переход в справку — ОРГАН, а не метка: китовый порог. */
.chx-tag.is-card { min-height: var(--touch-min, 64px); color: var(--active); border-color: rgba(var(--brass-rgb), 0.6); }
.chx-tag.is-card:active { border-color: var(--active); }
/* Б/В: колонка сама кодирует линию — подпись линии не нужна, дата сверху. */
.chx[data-chx-v="B"] .chx-row { grid-template-columns: minmax(0, 1fr); gap: calc(4px * var(--content-scale, 1)); }
.chx[data-chx-v="B"] .chx-row__ln { display: none; }
.chx[data-chx-v="B"] .chx-row__dt { color: var(--active); }

/* ── обзорная карта — запись ленты на свою дату ── */
.chx-ovr {
  position: relative;
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
  gap: calc(12px * var(--content-scale, 1));
  padding: calc(8px * var(--content-scale, 1)) calc(12px * var(--content-scale, 1))
           calc(8px * var(--content-scale, 1)) calc(18px * var(--content-scale, 1));
  margin-bottom: calc(6px * var(--content-scale, 1));
  background: rgba(var(--brass-rgb), 0.08);
  border: 1px solid rgba(var(--brass-rgb), 0.45);
}
.chx-ovr::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: calc(5px * var(--content-scale, 1)); background: var(--active); }
.chx-ovr.is-sel { background: rgba(var(--brass-rgb), 0.16); border-color: var(--active); }
.chx-ovr:active { background: rgba(var(--brass-rgb), 0.22); }
.chx-ovr__ico {
  width: calc(48px * var(--content-scale, 1)); height: calc(48px * var(--content-scale, 1));
  border: 1px solid rgba(var(--brass-rgb), 0.5); color: var(--active);
  display: flex; align-items: center; justify-content: center;
}
.chx-ovr__ico svg { width: 70%; height: 70%; fill: none; stroke: currentColor; stroke-width: 2.2; }
/* Кикер обзорной — СВЕТЛЫМИ чернилами, латунь остаётся полосой и значком.
   Латунь на латунном оттенке подложки дала 3.73 (у выбранной 3.28) при
   пороге 4.5 — штатная проба на ленте 1918 года, куда content перенесла
   `srez-1918` (a20b393). */
.chx-ovr__k { font-family: var(--font-caption); font-size: calc(var(--t-caption) * 0.8); letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-on-dark); }
.chx-ovr__t { font-size: var(--t-body); font-weight: 700; }
.chx-ovr__s {
  font-size: calc(var(--t-body) * 0.85); line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ── карта ── */
.chx__mapbox {
  position: relative; overflow: hidden; min-height: 0;
  border: 1px solid rgba(var(--brass-rgb), 0.3);
  background: rgba(var(--amber-black-rgb), 0.5);
}
.chx[data-chx-v="A"] .chx__mapbox { flex: 0 0 55%; }
.chx[data-chx-v="B"] .chx__mapbox { flex: 0 0 50%; }
.chx__stamp {
  position: absolute; left: calc(10px * var(--content-scale, 1)); top: calc(10px * var(--content-scale, 1)); z-index: 3;
  padding: calc(6px * var(--content-scale, 1)) calc(10px * var(--content-scale, 1));
  background: rgba(var(--amber-black-rgb), 0.5);
  font-family: var(--font-caption); font-size: calc(var(--t-caption) * 0.85); color: var(--text-on-dark);
  pointer-events: none;
}
.chx__stamp b { display: block; color: var(--active); font-size: var(--t-caption); letter-spacing: 0.06em; font-weight: 400; }
/* a11y, пока идёт ролик: штамп ОДНОЙ строкой «<месяц год> · на карте —
   ролик МТК-30» (строка design после приёмки app-270). Кадр встаёт под
   штампом, и три строки a11y (209 px) съедали окно: в Б кадр 56–61 %. */
:root.a11y .chx__stamp.is-film { white-space: nowrap; }
:root.a11y .chx__stamp.is-film b { display: inline; }
:root.a11y .chx__stamp.is-film .chx__stamp-x { display: none; }
.chx__stamp-sep { display: none; }
:root.a11y .chx__stamp.is-film .chx__stamp-sep { display: inline; }
.chx__pin {
  position: absolute; left: 0; top: 0; z-index: 2; pointer-events: none;
  width: calc(22px * var(--content-scale, 1)); height: calc(22px * var(--content-scale, 1));
  margin: calc(-11px * var(--content-scale, 1)) 0 0 calc(-11px * var(--content-scale, 1));
  border-radius: 50%; background: var(--red);
  box-shadow: 0 0 0 calc(6px * var(--content-scale, 1)) rgba(var(--brass-rgb), 0.35),
              0 0 0 calc(8px * var(--content-scale, 1)) var(--brass);
}
.chx__pin[hidden] { display: none; }

/* ── материал выбранного ── */
/* Панель материала: тело прокручивается, «Справка» — снизу вне прокрутки
   (приёмка design 1928fb6, a11y: кнопка была недостижима 0/5). */
.chx__mat {
  flex: 1 1 auto; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: calc(8px * var(--content-scale, 1));
  padding: calc(12px * var(--content-scale, 1)) calc(14px * var(--content-scale, 1));
  background: rgba(var(--window-rgb), 0.12);
  border: 1px solid rgba(var(--brass-rgb), 0.3);
}
.chx__mat h2 { margin: 0; font-size: var(--t-h2); line-height: 1.12; font-weight: 700; }
.chx__kick {
  display: flex; align-items: center; gap: calc(6px * var(--content-scale, 1));
  font-family: var(--font-caption); font-size: var(--t-caption);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--active);
}
.chx__kick b { width: calc(10px * var(--content-scale, 1)); height: calc(10px * var(--content-scale, 1)); background: var(--line-pol); }
.chx__kick[data-line="mil"] b { background: var(--line-mil); }
.chx__kick[data-line="both"] b { background: linear-gradient(90deg, var(--line-pol) 0 50%, var(--line-mil) 50% 100%); }
.chx__kick.is-ovr b { background: var(--active); }
.chx__photo {
  align-self: flex-start;
  max-width: 100%; max-height: calc(200px * var(--content-scale, 1));
  object-fit: cover; border: 1px solid rgba(var(--brass-rgb), 0.3);
}
.chx__matbody {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: calc(8px * var(--content-scale, 1));
}
/* Пояснение — три строки, как в прототипе: целиком текст живёт в справке. */
.chx__why {
  margin: 0; font-size: var(--t-body); line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.chx__acts { flex: 0 0 auto; display: flex; gap: calc(8px * var(--content-scale, 1)); }
.chx__pri {
  min-height: var(--touch-primary, 96px);
  padding: 0 calc(18px * var(--content-scale, 1));
  background: var(--active); border: 1px solid var(--active); color: var(--graphite);
  font-family: var(--font-caption); font-size: var(--t-caption); font-weight: 700;
}
.chx__pri:active { outline: 2px solid var(--paper); }

/* ── вкладки материала: ролик на карте / фото / лист ── */
/* Зазор между вкладками — `--touch-gap` кита (приёмка design: было 8). */
.chx__tabs { display: flex; flex-wrap: wrap; gap: var(--touch-gap, 16px); }
.chx__tab {
  min-height: var(--touch-min, 64px);
  padding: 0 calc(14px * var(--content-scale, 1));
  display: inline-flex; align-items: center;
  background: rgba(var(--window-rgb), 0.14);
  border: 1px solid rgba(var(--brass-rgb), 0.35);
  color: var(--text-on-dark);
  font-family: var(--font-caption); font-size: var(--t-caption);
}
.chx__tab.is-on { background: var(--active); border-color: var(--active); color: var(--graphite); }
.chx__tab:active { border-color: var(--active); }
/* Нет материала — метка, не кнопка: пунктир и приглушение (прототип). */
.chx__tab.is-off { opacity: 0.45; border-style: dashed; }
.chx__photo.is-sheet { object-fit: contain; }
.chx__fit { margin: 0; font-family: var(--font-caption); font-size: var(--t-caption); color: var(--text-muted); }
.chx__tech { margin: 0; font-family: var(--font-caption); font-size: calc(var(--t-caption) * 0.85); color: var(--text-muted); }
/* Ролик поверх карты — слой без тапов, кадр по проекции углов. */
.chx__anim { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
.chx__anim video { position: absolute; top: 0; left: 0; background: transparent; }

/* ═══════════════════════════════════════════════════════════════════════
   ТЕХНИЧЕСКИЕ ПОДПИСИ — СКРЫТЫ ОТ ПОСЕТИТЕЛЯ, ВИДНЫ ОПЕРАТОРУ

   🔴 Аудит 23.08 нашёл на экране посетителя четыре строки, написанные
   для нас: кадр карты с `viewBox`, счёт достижимых связей, пояснение
   раскладки хроники со счётом строк, отставание контура от даты.
   Тумблер «Технические подписи на экране» в сервис-панели
   (`app.js` → `appSettings.techText`) поднимает их обратно.

   🔑 БЛОК СТОИТ В КОНЦЕ ФАЙЛА НАМЕРЕННО. Правила ниже гасят `display`
   у чужих элементов, и при равной силе побеждает последнее — а сила
   здесь как раз равная у `.chr.is-asym .chr__legend-note` (0,2,1
   против 0,2,1 у нашего). Перенесёшь блок выше — пояснение хроники
   вернётся на экран, и вернётся МОЛЧА.

   ⚠️ Направление выбрано так, что ОТСУТСТВИЕ атрибута = скрыто:
   `:not([data-tech-text="on"])` истинен и до первого applyAppSettings.
   Обратное («показывать, пока не сказано прятать») дало бы вспышку
   техстрок на старте — ровно там, где смотрит приёмка.
   ═══════════════════════════════════════════════════════════════════ */
:root:not([data-tech-text="on"]) .map__cap,
:root:not([data-tech-text="on"]) .p29-rail__count,
/* Хроника: служебная строка точности привязки ролика — туда же. */
:root:not([data-tech-text="on"]) .chx__tech,
/* Служебные отметки справок и лонгрида — решение dvn 28.09 (опись
   `content/_service-fields.json`): метка «превью · Госкаталог»,
   список «Запрошено и не получено». Класс ОДИН на все такие места —
   `.is-svc`, чтобы следующая отметка пряталась без правки этого
   списка. Само превью остаётся (вариант (б), выбор dvn). */
:root:not([data-tech-text="on"]) .is-svc,
:root:not([data-tech-text="on"]) .chr.is-asym .chr__legend-note,
:root:not([data-tech-text="on"]) .chr__legend-note {
  display: none;
}

/* ═══════════════════════════════════════════════════════════════════════
   РЕЕСТР АНИМАЦИЙ МТК-30 — список слева, ролик своим кадром справа

   🔒 Решение dvn 2026-08-23: «только видео и без наложения а просто
   реестр». Карты в раскладке нет и не предусмотрено.
   ═══════════════════════════════════════════════════════════════════ */
.areg {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  height: 100%;
  /* Резерв под хром ЯДРА берётся у ядра — переменные слоя, а не своя
     арифметика от тач-порога. И `box-sizing` обязателен: без него отступ
     прибавляется СВЕРХ высоты, правило выглядит исполненным и не
     действует. Поймано на служебном экране сравнения показов: `.acmp`
     дал 0…2288 при слое 0…2160. ⚠️ Экрана этого больше нет — снят
     после решения dvn; искать `.acmp` в зоне бесполезно, случай
     оставлен здесь как единственный его след. */
  box-sizing: border-box;
  padding: var(--chrome-top, 190px) var(--gap) var(--chrome-bottom, 214px);
}
/* 🔒 РАСКЛАДКА ПЕРЕВЁРНУТА (dvn 25.08): плеер во всю разрешённую ширину
   сверху, карточки — сеткой под ним. Прежняя раскладка (список слева,
   ролик справа) снята вместе с заголовком раздела и подсказкой «выберите
   сюжет»: при входе теперь играет общая карта, приглашать не к чему.
   ⚠️ `.areg__title`, `.areg__sub`, `.areg__body` больше не рисуются —
   правила сняты здесь же, чтобы не остались мёртвыми. */

/* Карточки — СЕТКА, а не колонка: их 17, и в столбик они не помещаются
   под плеером. Ширина колонки задана минимумом, число колонок считает
   браузер: рукописное «шесть» разошлось бы с масштабом оператора. */
.areg__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(420px * var(--content-scale, 1)), 1fr));
  /* ⚠️ Зазор и отступ карточки подобраны ЗАМЕРОМ, а не на глаз: при 10
     и 12 сетка 17 карточек давала `scrollHeight` 416 при высоте 403,
     то есть прокрутку в 13 px — ровно то, чего просили избежать. */
  gap: calc(8px * var(--content-scale, 1));
  align-content: start;
  overflow-y: auto;
  min-height: 0;
  -webkit-overflow-scrolling: touch;
}
/* 🔴 СТРОКА СПИСКА НЕ СЖИМАЕТСЯ НИЖЕ СОДЕРЖИМОГО — СПИСОК ДЛЯ ТОГО
   И ПРОКРУЧИВАЕТСЯ. Найдено сплошным замером a11y 2026-08-24: во флекс-
   колонке `.areg__list` строки ужимались до `min-height` 118 при
   содержимом 151, то есть название налезало на соседнюю строку.
   Тот же дефект, что был у строк рельса Симбирска: флекс сжимает по
   умолчанию, и `min-height` его не держит.
   В обычном режиме содержимое влезает и правило ничего не двигает —
   проверено замером до и после. */
.areg-card {
  flex-shrink: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: calc(4px * var(--content-scale, 1)) calc(12px * var(--content-scale, 1));
  /* Китовый порог навигации: строка — основной орган этого раздела.
     Мерить всё равно попаданием, а не этой строкой. */
  min-height: var(--touch-primary, 96px);
  padding: calc(10px * var(--content-scale, 1)) calc(12px * var(--content-scale, 1));
  text-align: left;
  background: rgba(var(--amber-black-rgb), 0.55);
  border: 1px solid rgba(var(--brass-rgb), 0.28);
  color: var(--text-on-dark);
}
.areg-card.is-on { border-color: var(--active); background: rgba(var(--brass-rgb), 0.18); }
.areg-card:active { background: rgba(var(--brass-rgb), 0.28); }
.areg-card__name {
  grid-column: 1;
  font-family: var(--font-text);
  font-size: var(--t-tile);
  line-height: 1.2;
}
/* 🔴 «НАЗВАНИЕ НЕ ЗАДАНО» ОБЯЗАНО ЧИТАТЬСЯ КАК ПРОБЕЛ В ДАННЫХ, а не как
   название. Приглушённый курсив: он не спорит с настоящими названиями
   в соседних строках и не выглядит стилем оформления.
   ⚠️ Курсив здесь законен — шрифт `--font-text` (21 Cent), а не Nolde:
   у Nolde начертания нет, и синтетический наклон виден как дефект
   засечек (запрет в трёх документах и в линтере). */
.areg-card__name.is-nameless {
  font-style: italic;
  color: var(--text-muted);
}
.areg-card__kind,
.areg-card__parts {
  grid-column: 2;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  white-space: nowrap;
}
.areg-card__none {
  grid-column: 1 / -1;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--signal);
}
.areg-card.is-empty .areg-card__name { color: var(--text-muted); }

/* Сцена показа — сверху, во всю ширину. Прокрутки у неё больше нет:
   она была нужна, пока ролик жил в узкой правой колонке. */
.areg__stage {
  flex: 0 0 auto;
  min-height: 0;
}

/* Полоса над картой: слева имя карты, справа номер и период части.
   Роли разные и набираются разным — решение dvn №3 о гарнитурах. */
.areg-bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap);
  /* ⚠️ Отступ полосы — последний резерв высоты, и он потрачен ЗАМЕРОМ.
     После сужения карточек сетка давала `scrollHeight` 408 при высоте
     403: пять пикселей прокрутки. Отнимать их у карточек нельзя — там
     тач-пол 96; отнимаем у отступа, он ни на что не претендует. */
  margin: 0 0 calc(4px * var(--content-scale, 1));
}
.areg-bar__map {
  margin: 0;
  min-width: 0;
  font-family: var(--font-display);
  font-size: var(--t-h1);
  line-height: 1.05;
}
/* Номер и период — гарнитура чисел и дат, не заголовочная. */
.areg-bar__part {
  margin: 0;
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: calc(var(--gap) / 2);
  font-family: var(--font-caption);
  font-size: var(--t-h2);
}
/* Номер части — 20 Kopeek: гарнитура цифр и техтекста (наследуется
   от `.areg-bar__part`). */
.areg-bar__n { color: var(--text-muted); }

/* 🔒 НАЗВАНИЕ ЧАСТИ — ДВЕ ГАРНИТУРЫ ПО ИСТОЧНИКУ (правка `design` 25.08).
   Период из имени файла — даты, значит 20 Kopeek (наследуется).
   Редакторская подпись `parts[].title_ru` — слова, значит 21 Cent.
   ⚠️ И НЕ Nolde, даже когда крупно: слева над тем же кадром уже стоит имя
   карты заголовочной гарнитурой, и второй заголовочный блок справа
   конкурировал бы с ним. Название части — подзаголовок, ниже по иерархии. */
.areg-bar__lbl { color: var(--active); }
.areg-bar__lbl.is-words { font-family: var(--font-text); }
.areg__hint {
  font-family: var(--font-caption);
  font-size: var(--t-body);
  color: var(--text-muted);
  text-align: center;
  margin: calc(48px * var(--content-scale, 1)) 0 0;
}
/* `.areg-play__title` и `.areg-play__cap` сняты 25.08: название карты
   и номер части переехали в полосу `.areg-bar` НАД картой (решение dvn).
   Оставлять их правилами без разметки значило бы завести мёртвый блок
   в тот же день, когда мы сняли предыдущий. */
.areg-play { margin: 0 0 var(--gap); }
/* 🔴 `height: auto` СТОИТ ПЕРВЫМ РЯДОМ С `aspect-ratio` — без него
   браузер берёт атрибутную высоту до загрузки метаданных, и первый кадр
   прыгает. Правило уже стоило нам одного разбора, повторять не будем.
   Пропорция — кадр ПОСТАВКИ 1920×622, а не «широкая на глаз». */
.areg-play__video {
  height: auto;
  aspect-ratio: 1920 / 622;
  width: 100%;
  display: block;
  background: rgba(var(--amber-black-rgb), 0.8);
  /* 🔴 ПЛАВНОСТЬ КЛАДЁТСЯ ПОВЕРХ ОЧЕРЕДИ, А НЕ ВМЕСТО НЕЁ. Очередь частей
     идёт по событию `ended` — кадров не требует и в фоновой вкладке
     работает. Переход добавлен только на прозрачность.
     ⚠️ Снимается класс по событию ВИДЕО (`playing`/`loadeddata`),
     а НЕ по `transitionend`: в скрытой вкладке переход не проигрывается
     вовсе и застревает навсегда, `transitionend` не наступает, и ролик
     остался бы невидимым — а проба, которая идёт именно в скрытой
     вкладке, мерила бы пустой кадр. На этой же механике зона уже
     напечатала 28 ложных дефектов.
     ⚠️ Переход попадает под общее гашение оснастки (`*` в правиле
     «заморозить переходы на время замера»), проверено прогоном. */
  transition: opacity 260ms ease;
}
.areg-play__video.is-swap { opacity: 0; }

/* ═══════════════════════════════════════════════════════════════════════
   СИМВОЛИКА В КАРТОЧКЕ — флаги и гербы из ключа `symbols[]`

   🔴 Полоса заведена потому, что ключ `symbols[]` не читала НИ ОДНА
   сцена: 43 записи размечены, 69 производных собраны, файлы отдаются —
   и на экране не было ничего. Отсутствующий потребитель ничем себя
   не выдаёт, поэтому дефект нашёлся замером на стенде, а не по коду.
   ═══════════════════════════════════════════════════════════════════ */
.p29-syms { margin: var(--gap) 0 0; }
.p29-syms__head {
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 0 0 calc(12px * var(--content-scale, 1));
}
/* 🔴 `auto-fit`, А НЕ `auto-fill`, И ПОТОЛОК У ДОРОЖКИ.
   Проба зоны поймала на моей же полосе: при одном символе `auto-fill`
   держал 62 % ширины ПУСТЫМИ ДОРОЖКАМИ — они существуют и занимают
   поле, просто в них нечего класть. `auto-fit` схлопывает пустые.
   ⚠️ Потолок 240 обязателен: без него единственный флаг растянулся бы
   на всю колонку карточки, и знак читался бы как экспонат. Нижняя
   граница держит читаемость подписи, верхняя — соразмерность. */
.p29-syms__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,
    minmax(calc(180px * var(--content-scale, 1)), calc(240px * var(--content-scale, 1))));
  justify-content: start;
  gap: var(--gap);
}
.p29-sym { margin: 0; }
/* 🔴 `height: auto` ПЕРВЫМ РЯДОМ С `aspect-ratio` — иначе браузер берёт
   атрибутную высоту до загрузки и кадр прыгает.
   Пропорция приходит ИЗ ДАННЫХ (`--sym-aspect`, поле `aspect`): у флагов
   она 2:1, у гербов около 1:1, и общая рамка на оба врала бы полям.
   Запас 1.5 — на записи без поля; он ЯВНЫЙ, потому что у метрики
   `var()` без запаса гасит объявление целиком. */
.p29-sym__img {
  height: auto;
  aspect-ratio: var(--sym-aspect, 1.5);
  width: 100%;
  display: block;
  object-fit: contain;
  background: rgba(var(--paper-rgb), 0.06);
  padding: calc(8px * var(--content-scale, 1));
  box-sizing: border-box;
}
/* Пробел в материале ВИДЕН и занимает то же место, что картинка:
   схлопнутая дырка читалась бы как «символики нет вовсе», а она есть
   и её ещё добирать. */
.p29-sym__hole {
  aspect-ratio: var(--sym-aspect, 1.5);
  width: 100%;
  border: 1px dashed rgba(var(--brass-rgb), 0.4);
  background: rgba(var(--amber-black-rgb), 0.35);
}
.p29-sym__cap {
  margin: calc(8px * var(--content-scale, 1)) 0 0;
  font-family: var(--font-caption);
  font-size: var(--t-caption);
  color: var(--text-muted);
  line-height: 1.25;
}
.p29-sym__why { display: block; color: var(--signal); }

/* ═══════════════════════════════════════════════════════════════════════
   МАСШТХЕД РАЗДЕЛА — ОБЩАЯ ОБОЛОЧКА СТИЛЯ А (макет `design`, принят dvn)

   Буквица-номер водяным знаком, кикер, заголовок Nolde, латунная
   диагональ. Одна на все разделы; разбор — в `app/lib/masthead.js`.

   🔴 Числа — базовые, то есть ЭКРАННЫЕ МАКЕТА ДЕЛЁННЫЕ НА 1.35
   (`--content-scale` из конфига). Некруглые они потому, что пересчитаны,
   а не подобраны: 300 → 222, 132 → 98, 30 → 22.
   ═══════════════════════════════════════════════════════════════════ */
:root {
  --mh-num: calc(222px * var(--content-scale, 1));
  --mh-title: calc(98px * var(--content-scale, 1));
  --mh-kicker: calc(22px * var(--content-scale, 1));
  --mh-rail: calc(156px * var(--content-scale, 1));   /* отступ под буквицу */
}

.mh {
  position: relative;
  flex: 0 0 auto;
  padding-left: var(--mh-rail);
  min-height: var(--mh-rail);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Буквица-номер. `attr()` вместо своего узла: номер — оформление, и лишний
   элемент в разметке стал бы ещё одним листом для пробы содержимого. */
.mh[data-mh-num]::before {
  content: attr(data-mh-num);
  position: absolute;
  left: calc(-10px * var(--content-scale, 1));
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-display);
  font-size: var(--mh-num);
  line-height: 0.78;
  color: var(--brass);
  opacity: 0.14;
  letter-spacing: -0.04em;
  z-index: 0;
}
/* Латунная диагональ — брендовый наклон. Токена наклона в вендоре нет,
   поэтому запас −15° явный: `var()` без запаса гасит объявление целиком,
   и линейка просто не наклонилась бы — молча. Разбор тот же, что
   у `--brand-skew` в косой полосе лагеря. */
.mh::after {
  content: "";
  position: absolute;
  left: calc(138px * var(--content-scale, 1));
  top: calc(4px * var(--content-scale, 1));
  bottom: calc(4px * var(--content-scale, 1));
  width: calc(2px * var(--content-scale, 1));
  background: var(--brass);
  opacity: 0.5;
  transform: skewX(var(--brand-skew, -15deg));
  z-index: 0;
}
/* Содержимое поверх водяного знака и линейки. */
.mh > * { position: relative; z-index: 1; }

.mh__kicker {
  margin: 0 0 calc(10px * var(--content-scale, 1));
  font-family: var(--font-caption);
  font-size: var(--mh-kicker);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.mh__kicker:empty { display: none; }
/* Nolde — только заголовочный уровень, и без курсива: начертания нет,
   синтетический наклон виден как дефект засечек (правило в линтере). */
/* 🔴 ИНТЕРЛИНЬЯЖ 1.2, А НЕ 0.94 ИЗ МАКЕТА — И ЭТО НЕ ВКУСОВОЕ.
   Замер на живой странице: при 0.94 бокс строки 124 px, а ЧЕРНИЛА
   заголовка 159 px (у Nolde высокие выносные). Верх чернил — 242,
   низ кикера — 246: НАЛОЖЕНИЕ 4 px, буквы лезли на кикер.
   Проба зоны поймала это как «переполнение при overflow:visible»
   (141 > 124) — и была права: содержимое выходило за свой бокс.
   ⚠️ Дефект есть и в самом макете: там та же пара 132/0.94. Сообщено
   `design` числами, а не «поправьте отступ».
   🔴 ПИСЬМА РАЗМИНУЛИСЬ — ИТОГОВОЕ ЗНАЧЕНИЕ 1.2, НЕ 1.1.
   Зона `app` поставила 1.1 по канону `design`; в те же минуты `design`
   перемерила стенд, увидела 1.200 живым, СНЯЛА своё 1.1 и подровняла
   эталон к 1.2 («перевыкатывать не нужно», `2360976`). Значение вернул
   координатор к 1.2 — не как решение по типографике, а чтобы экран
   и эталон не разошлись: обе стороны согласились на 1.2 последними
   словами, и расхождение возникло только из-за порядка писем.
   Разбор ниже оставлен целиком — он верен и объясняет обе величины.
   🔒 РАЗБОР `design`, действителен при любом из двух значений.
   Моё 1.2 было 159/132.3 — минимум, при котором бокс ГАРАНТИРОВАННО
   вмещает чернила: аварийная величина, посчитанная честно. Но вопрос
   «сколько воздуха у дисплейного заголовка» решает типографика,
   а не арифметика запаса: 1.2 для масштхеда рыхловат.
   Уточнение диагноза от `design`, и оно точнее моего: наложение дают
   не «высокие выносные» вообще, а АКЦЕНТ-МАРКИ КИРИЛЛИЧЕСКОГО КАПСА —
   бревис у Й, точки у Ё лезут выше cap-height.
   ⚠️ При 1.1 чернила по-прежнему выходят за бокс (климб ~7 px), но
   ДО КИКЕРА остаётся зазор — а предмет требования именно наложение,
   не переполнение бокса.
   ⚠️ Заголовки сейчас однострочные все до одного, поэтому плотность
   макета теряется только в гипотетическом переносе. Появится
   двухстрочный — мерить заново: там интерлиньяж уже виден. */
.mh__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--mh-title);
  font-weight: 400;
  line-height: 1.2;
  color: var(--paper);
}

/* ═══════════════════════════════════════════════════════ ГЛАВНАЯ (хаб)
   Макет `design/hub-A.html`. Числа макета делятся на 1.35 (экранные px
   панели 3840 → базовые контента): hero 720 → 533, заголовок 224 → 166,
   буквица карточки 340 → 252, кикер 34 → 25, год 76 → 56.

   🔴 РЕЗЕРВ СНИЗУ ЗДЕСЬ НЕ ПИШЕТСЯ РУКАМИ. У сцены `nav: false`, и ядро
   само роняет `--chrome-bottom` с 214 до `--edge-safe-bottom`. Общее
   правило `.p29 { padding: var(--chrome-*) }` подхватывает это без единой
   строки здесь — и именно поэтому число не продублировано: продублируй
   его, и хаб потерял бы полосу пустоты под несуществующей панелью
   в тот день, когда ядро поменяет отступ. */
.hub {
  /* 🔴 ЗАЗОР РЯДОВ 33 → 24 БАЗОВЫХ — ЭТО ЧАСТЬ ЛЕЧЕНИЯ ОБРЕЗКИ, А НЕ ВКУС.
     Зазоров между рядами три, и на узкой ширине они стоят 3 × 12 базовых
     = 48 px, которых ровно не хватало ряду четырёх разделов. Замер на
     1920: с 33 остаток обрезки 30 px у «Политических партий», с 24 — ноль.
     На 3840 обрезки не было ни при том, ни при другом. */
  gap: calc(24px * var(--content-scale, 1));
  overflow-y: auto;
}

/* — HERO-ПОСТЕР — */
.hub__hero {
  position: relative;
  flex: 0 0 auto;
  min-height: calc(533px * var(--content-scale, 1));
  overflow: hidden;
  isolation: isolate;
  border-radius: calc(6px * var(--content-scale, 1));
  background:
    radial-gradient(120% 140% at 12% 0%, rgba(var(--window-rgb), 0.28), transparent 60%),
    var(--graphite);
}

/* Фирменная диагональ 105° — плашка-постер, а не фон экрана: ограничена
   своим боксом. Угол тот же, что у полос бренда. */
.hub__hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0.92;
  background-image:
    linear-gradient(105deg, transparent 0, transparent 70%, var(--red) 70%, var(--red) 84%, transparent 84.2%),
    linear-gradient(105deg, transparent 0, transparent 2%, rgba(var(--window-rgb), 0.16) 2%, rgba(var(--window-rgb), 0.16) 13%, transparent 13.2%),
    linear-gradient(105deg, transparent 0, transparent 60%, var(--brass) 60%, var(--brass) 60.2%, transparent 60.4%);
}

/* 🔒 СИМБИРСК ВЫДЕЛЕН ЦВЕТОМ, БЕЗ ОТСТУПА (dvn: «нельзя отступать»; design
   96acbf6, заменяет 3de1da5): сине-серый градиент снизу (у остальных ч/б
   фото), латунная кромка, в строке номера — что это за режим. `::after`
   самой карточки занят косой латунной чертой — тон в фоне, не в нём. */
.hub-card.is-special { outline: 2px solid rgba(var(--brass-rgb), 0.55); outline-offset: -2px; }
.hub-card.is-special > .hub-card__bg {
  background-image:
    linear-gradient(0deg, rgba(var(--blue-grey-rgb), 0.62) 0%, rgba(var(--blue-grey-rgb), 0.26) 55%, rgba(var(--blue-grey-rgb), 0.1) 100%),
    /* снимок схемы гл. 4 тем же атласом (вариант А, e46eb74); ?v= — SNAP_V сцены */
    url(img/scheme-simbirsk-july-1918.webp?v=s3);
  filter: saturate(0.85) brightness(0.5);
}
.hub-card.is-special > .hub-card__bg::after {
  background: linear-gradient(180deg, rgba(var(--amber-black-rgb), 0.55) 0%,
    rgba(var(--amber-black-rgb), 0.22) 40%, rgba(var(--amber-black-rgb), 0.12) 100%);
}
.hub-card.is-special .hub-card__num { letter-spacing: 0.14em; text-transform: uppercase; }

/* Фото баннера (слово dvn 28.09 «героическую фотографию», кадр design
   bb3e826): правая часть под фирменной диагональю, ч/б и притушено до
   графита, левый край растворён маской — заголовок остаётся на ровном
   фоне. Слой ниже диагонали: `z-index: -1` внутри `isolation` героя.
   Маске важна только альфа — цвет взят токеном, сырого hex нет. */
.hub__photo {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 58%;
  z-index: -1;
  background-size: cover;
  background-position: 50% 45%;
  filter: grayscale(1) contrast(1.08) brightness(0.62);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, var(--graphite) 38%);
  mask-image: linear-gradient(90deg, transparent 0, var(--graphite) 38%);
}

.hub__watermark {
  position: absolute;
  right: calc(44px * var(--content-scale, 1));
  bottom: calc(-71px * var(--content-scale, 1));
  z-index: 1;
  font-family: var(--font-display);
  font-size: calc(415px * var(--content-scale, 1));
  line-height: 0.8;
  color: var(--paper);
  opacity: 0.1;
  transform: rotate(-12deg);
  transform-origin: bottom right;
  white-space: nowrap;
  pointer-events: none;
}

.hub__hero-in {
  position: relative;
  z-index: 2;
  height: 100%;
  padding: calc(56px * var(--content-scale, 1)) calc(65px * var(--content-scale, 1));
  display: flex;
  flex-direction: column;
}

/* Кикер — красная плашка-параллелограмм. Бумага на сигнальном 7.20 : 1. */
.hub__kicker {
  align-self: flex-start;
  margin: 0;
  padding: calc(12px * var(--content-scale, 1)) calc(25px * var(--content-scale, 1));
  background: var(--signal);
  color: var(--paper);
  font-family: var(--font-caption);
  font-size: max(var(--type-min-ui, 32px), calc(25px * var(--content-scale, 1)));
  letter-spacing: 0.28em;
  text-transform: uppercase;
  transform: skewX(-15deg);
}

.hub__kicker > span { display: inline-block; transform: skewX(15deg); }

/* Заголовок комплекса — заголовочный уровень, Nolde по канону. */
.hub__title {
  margin: calc(30px * var(--content-scale, 1)) 0 0;
  font-family: var(--font-display);
  font-weight: 400;
  /* 🔴 КЕГЛЬ КАПНУТ ПО ШИРИНЕ ЭКРАНА, И ЭТО ЛЕЧИТ ОБРЕЗКУ В КАРТОЧКАХ
     ВНИЗУ, А НЕ САМ ЗАГОЛОВОК. Разбор, потому что связь неочевидна:
     при сужении экрана заголовок героя ломается на ВТОРУЮ СТРОКУ, ряд
     героя растёт (замер: 787 → 1056 при 3840 → 2560, +269), а ряд четырёх
     разделов — единственный `flex: 1 1 auto` — ровно столько теряет
     и режет текст карточек.
     ⚠️ Прежняя спека («+70 ряду, −70 нижнему») была неверна ОБЕИМИ
     посылками: `+70` не закрывает `269`, а «запаса 80 px снизу» нет вовсе
     — `2160 − 80 − 2080 = 0`, видимый зазор и есть зона хрома. Оба числа
     сняты замером, разбор в наряде.
     ▶️ Решение `design` (замер на 1920 и 2560): герой отдаёт вторую строку.
     На 3840 `min` выбирает 224 — кит не меняется НИ НА ПИКСЕЛЬ; кегль
     сужается только там, где заголовок и так ломался.
     🔒 Кап кегля у самих карточек `design` рассмотрела и ОТВЕРГЛА:
     «Полит. партии» ради одной строки уехало бы в 38 — её слово
     «уродство». Кегль карточек не трогать. */
  font-size: min(calc(166px * var(--content-scale, 1)), 7.6vw);
  /* 🔒 1.2, А НЕ 1.05 ИЗ МАКЕТА — та же величина и та же причина, что
     у масштхеда: у Nolde высокие акцент-марки кириллического капса,
     и при 1.05 проба намерила чернила 252 при боксе 235. Бокс обязан
     вмещать чернила, иначе заголовок лезет на соседа. */
  line-height: 1.2;
  color: var(--paper);
  max-width: calc(1778px * var(--content-scale, 1));
}

.hub__span {
  margin-top: auto;
  display: flex;
  align-items: baseline;
  gap: calc(25px * var(--content-scale, 1));
  flex-wrap: wrap;
}

.hub__years-span {
  font-family: var(--font-display);
  font-size: calc(98px * var(--content-scale, 1));
  line-height: 1.2;   /* чернила 145 при боксе 132 — та же причина */
  color: var(--active);
}

.hub__sub {
  font-family: var(--font-text);
  font-size: calc(33px * var(--content-scale, 1));
  line-height: 1.18;
  color: var(--text-muted);
  max-width: calc(740px * var(--content-scale, 1));
}

/* — ГОДЫ — */
.hub__years { flex: 0 0 auto; }

.hub__years-lbl {
  display: flex;
  align-items: center;
  gap: calc(13px * var(--content-scale, 1));
  margin: 0 0 calc(16px * var(--content-scale, 1));
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(22px * var(--content-scale, 1)));
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.hub__years-lbl::before {
  content: "/";
  display: inline-block;
  font-size: calc(30px * var(--content-scale, 1));
  color: var(--active);
  transform: skewX(-15deg);
}

.hub__year-row { display: flex; gap: calc(18px * var(--content-scale, 1)); }

/* 🔴 ЦЕЛЬ ПРЯМОУГОЛЬНАЯ, ПАРАЛЛЕЛОГРАММ — РИСОВАННЫЙ. Скос на самой
   кнопке уводит УГЛЫ её бокса за пределы фигуры, и проба ловит это
   попаданием: «button: перехвачено: верх→hub-year, низ→hub-year» —
   в углу чипа палец попадает в соседний. Случай настоящий, а не придирка
   инструмента: угол параллелограмма действительно принадлежит соседу.
   Скос переехал на подложку `::before`; вид из макета сохранён,
   попадание стало прямоугольным. */
.hub-year {
  position: relative;
  flex: 1;
  min-height: max(var(--touch-primary, 96px), calc(111px * var(--content-scale, 1)));
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--text-on-dark);
}

.hub-year::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: rgba(var(--window-rgb), 0.16);
  border: 1px solid rgba(var(--brass-rgb), 0.28);
  transform: skewX(-15deg);
}

.hub-year__c {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: calc(19px * var(--content-scale, 1)) calc(25px * var(--content-scale, 1));
  text-align: left;
}

/* 🔴 ЧИП ГОДА — ЧИСЛО ИНТЕРФЕЙСА, А НЕ МОТИВ. Гарнитура `--font-caption`
   (20 Kopeek) по расширению `design`, принятому координатором 2026-08-24.
   Различение по РОЛИ, а не по кеглю: чип года читают И в него целятся
   пальцем — это показание, парное маркеру «вы здесь».

   🔒 ГРАНИЦА, КОТОРУЮ ДЕРЖИМ ЯВНО, ЧТОБЫ ЕЁ НЕ «ДОВЕЛИ ДО КОНЦА»:
   водяной знак `.hub__watermark` (560 px) и «1917–1922» `.hub__years-span`
   (132 px) ОСТАЮТСЯ Nolde по решению dvn №1 — «цифры-мотив: годы Nolde
   латунью, водяной знак». Водяной знак не читают, он фон и жест;
   «1917–1922» стоит в заголовочном блоке рядом с названием. Это не
   недоделанный канон, а сознательная граница. */
.hub-year__n {
  font-family: var(--font-caption);
  font-size: calc(56px * var(--content-scale, 1));
  line-height: 1.2;   /* чернила 83 при боксе 76 — та же причина */
  color: var(--paper);
}

.hub-year__cnt {
  margin-top: auto;
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(24px * var(--content-scale, 1)));
  color: var(--active);
}

.hub-year__cnt small {
  font-size: calc(18px * var(--content-scale, 1));
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.hub-year:active::before { background: var(--active); }
.hub-year:active .hub-year__n,
.hub-year:active .hub-year__cnt { color: var(--text-on-light); }
.hub-year:active .hub-year__cnt small { color: rgba(var(--graphite-rgb), 0.7); }

/* — ПЯТЬ РАЗДЕЛОВ В ОДИН РЯД (слово dvn 28.09, кадр design bb3e826):
   I–IV и «С Симбирск» пятой карточкой; второго ряда больше нет. — */
.hub__cards {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--sec-gap);
}

.hub-card {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* 🔴 ВЕРТИКАЛЬНОЕ ПОЛЕ 33 → 22 БАЗОВЫХ. Уменьшает ПОТРЕБНОСТЬ карточки,
     а не добавляет ей высоты: 11 базовых × 2 = 30 px с каждой карточки.
     Горизонтальное поле не тронуто — обрезка вертикальная.
     🔒 Кегль карточек НЕ трогаем: `design` рассмотрела кап и отвергла —
     «Полит. партии» ради одной строки ушло бы в 38, её слово «уродство». */
  padding: calc(22px * var(--content-scale, 1)) calc(30px * var(--content-scale, 1));
  background: linear-gradient(180deg, rgba(var(--amber-black-rgb), 0.16), rgba(var(--amber-black-rgb), 0.30));
  border: 1px solid rgba(var(--brass-rgb), 0.34);
  border-radius: calc(6px * var(--content-scale, 1));
  text-align: left;
}

/* ═══════════════ ФОТО-ФОН КАРТОЧКИ ВИТРИНЫ (макет `design` 67c4085)
   Заказ dvn 25.08: «шесть тёмных прямоугольников, однообразно». Под текстом
   — тематический снимок эпохи, сильно затемнённый к низу и в графит-тоне,
   чтобы шесть карточек читались одной коллекцией, а не шестью картинками.

   🔴 ЦВЕТА ЗАТЕМНЕНИЯ ВЗЯТЫ ТОКЕНОМ, А НЕ ИЗ МАКЕТА. В макете стоят сырые
   `rgba(23,28,32,…)` и `rgba(15,19,23,…)` — это графит, приглушённый
   до ~0.35 (23/67 = 0.34, 28/80 = 0.35, 32/89 = 0.36). Интерфейсный слой
   красится только токенами, поэтому берём чёрный янтарь с теми же альфами.
   ⚠️ Разница НЕ в мою пользу и потому безопасна: чистый чёрный ТЕМНЕЕ
   графитового оттенка, то есть контраст светлых букв на нём не падает,
   а растёт. Замеры координатора на макете (буквицы 3.78…4.59 при пороге
   3.0) остаются нижней границей, но я их всё равно перемеряю у себя:
   разметка наша, а не его.

   🔴 СЛОЙ НИЖЕ БУКВИЦЫ И КОСОЙ ЛИНИИ. У карточки уже есть два псевдоэлемента
   с `z-index: 0`, и они идут в порядке документа: `::before` (буквица) — до
   первого ребёнка. Фон-див, вставленный первым ребёнком, накрыл бы буквицу
   собой. Поэтому карточка объявляется контекстом наложения (`isolation`),
   фон получает 0, а оба псевдоэлемента поднимаются на 1 — ровно та
   раскладка, что в макете, где буквица настоящий узел, а не `::before`. */
.hub-card { isolation: isolate; }

/* 🔴 СЕЛЕКТОР ОБЯЗАН БЫТЬ СИЛЬНЕЕ `.hub-card > *` / `.hub-big > *`, И ЭТО
   НЕ ПРИДИРКА. У обоих контейнеров уже есть правило на всех детей
   (`position: relative; z-index: 1; margin-left: …`), и оно стоит НИЖЕ
   по файлу. При равной силе побеждает нижнее: одноклассовый `.hub-card__bg`
   проиграл целиком — фон получил `position: relative` вместо `absolute`
   и схлопнулся в бокс 790×0, а `z-index` стал 1, то есть фото легло бы
   ПОВЕРХ буквицы. Замер: шесть фонов, все нулевой высоты; на экране
   не изменилось ничего, и без замера правка выглядела бы сделанной. */
.hub-card > .hub-card__bg {
  position: absolute;
  inset: 0;
  margin: 0;                 /* гасит общий `margin-left` у `.hub-card > *` */
  z-index: 0;
  pointer-events: none;      /* палец обязан попадать в карточку, не в фон */
  background-size: cover;
  background-position: center 20%;
  filter: grayscale(0.5) contrast(1.03) brightness(0.66);
}

/* Затемнение: радиальный УЗЕЛ в правом верхнем углу — под буквицей, где
   фото самое светлое, — плюс общий градиент к низу, под подпись. */
.hub-card > .hub-card__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(72% 48% at 86% 1%, rgba(var(--amber-black-rgb), 0.66), transparent 64%),
    linear-gradient(179deg,
      rgba(var(--amber-black-rgb), 0.5) 0%,
      rgba(var(--amber-black-rgb), 0.72) 48%,
      rgba(var(--amber-black-rgb), 0.94) 100%);
}

/* «Персоналии» — КОЛЛАЖ лиц (решение dvn: не один портрет, а несколько).
   Сетка 3×2 под тем же затемнением и тем же графит-фильтром. */
.hub-card > .hub-card__bg.is-collage {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(2, 1fr);
}
.hub-card > .hub-card__bg.is-collage > i {
  background-size: cover;
  background-position: center 18%;
}

/* Режим слабовидящих: фото ГЛУШЕ. Контраст подписи и буквицы важнее
   картинки — решение зафиксировано координатором на замере, а не на вкусе.
   ⚠️ Признак — `:root.a11y` ядра, а НЕ `body[data-a11y]` из макета:
   у макета своя разметка, у нас признак ставит кит. */
:root.a11y .hub-card > .hub-card__bg {
  filter: grayscale(0.65) brightness(0.48);
}
:root.a11y .hub-card > .hub-card__bg::after {
  background:
    radial-gradient(76% 52% at 86% 1%, rgba(var(--amber-black-rgb), 0.78), transparent 62%),
    linear-gradient(179deg,
      rgba(var(--amber-black-rgb), 0.66) 0%,
      rgba(var(--amber-black-rgb), 0.8) 48%,
      rgba(var(--amber-black-rgb), 0.96) 100%);
}

/* Буквица-номер водяным знаком — тот же мотив, что в масштхеде раздела. */
.hub-card::before {
  content: attr(data-num);
  position: absolute;
  right: calc(-4px * var(--content-scale, 1));
  top: calc(-34px * var(--content-scale, 1));
  z-index: 1;   /* НАД фото-фоном, см. разбор у `.hub-card__bg` */
  font-family: var(--font-display);
  font-size: calc(252px * var(--content-scale, 1));
  line-height: 0.8;
  color: var(--active);
  opacity: 0.12;
  pointer-events: none;
}

.hub-card::after {
  content: "";
  position: absolute;
  left: calc(33px * var(--content-scale, 1));
  top: calc(30px * var(--content-scale, 1));
  bottom: calc(30px * var(--content-scale, 1));
  width: calc(2px * var(--content-scale, 1));
  background: var(--active);
  opacity: 0.42;
  transform: skewX(-15deg);
  z-index: 1;   /* НАД фото-фоном, см. разбор у `.hub-card__bg` */
}

.hub-card > * { position: relative; z-index: 1; margin-left: calc(25px * var(--content-scale, 1)); }

.hub-card__num {
  margin: 0 0 0 calc(25px * var(--content-scale, 1));
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(22px * var(--content-scale, 1)));
  letter-spacing: 0.24em;
  color: var(--active);
}

.hub-card__title {
  margin: calc(15px * var(--content-scale, 1)) 0 0 calc(25px * var(--content-scale, 1));
  font-family: var(--font-display);
  font-weight: 400;
  font-size: calc(53px * var(--content-scale, 1));
  line-height: 1.05;
  color: var(--paper);
}

.hub-card__text {
  margin: calc(15px * var(--content-scale, 1)) 0 0 calc(25px * var(--content-scale, 1));
  font-family: var(--font-text);
  font-size: calc(30px * var(--content-scale, 1));
  line-height: 1.22;
  color: var(--text-muted);
}

.hub-card__meta {
  margin: auto 0 0 calc(25px * var(--content-scale, 1));
  font-family: var(--font-caption);
  font-size: max(var(--type-min-label, 24px), calc(24px * var(--content-scale, 1)));
  color: var(--active);
}

.hub-card:active { border-color: var(--active); }

/* ═════════ ВИТРИНА В РЕЖИМЕ СЛАБОВИДЯЩИХ — УЖАТИЕ, А НЕ ПЕРЕСБОРКА ═══════
   🔒 Решение dvn 2026-08-24 по замеру: ужать плакат и ленту годов, второй
   ряд оставить, витрина остаётся полной.

   🔴 ЧТО БЫЛО СЛОМАНО, ЧИСЛОМ. Ряд карточек — ЕДИНСТВЕННЫЙ гибкий в хабе
   (`flex: 1 1 auto`), остальные три — `flex: 0 0 auto`. Значит карточки
   получают ОСТАТОК высоты, сколько бы ни просило их содержимое:

       обычный  hero 787 · годы 258 · карточки 426 · особые 300
       a11y     hero 787 · годы 376 · карточки 144 · особые 450
                содержимое карточки 650 — название уходило за кромку
                на 124 px, описание на 377, счётчик на 440,
                и посетитель видел ОДИН РИМСКИЙ НОМЕР

   ⚠️ Поэтому снять описание (первый выбор dvn) было НЕДОСТАТОЧНО, и это
   проверено подстановкой ДО правки: содержимое падало 650 → 441,
   а высота карточки оставалась те же 144. Уменьшение содержимого
   остатка не увеличивает — высоту надо было ОСВОБОДИТЬ.

   🔑 УЖИМАЮТСЯ ПОЛЯ И ЗАЗОРЫ, А НЕ КЕГЛИ. Режим существует ради тех, кто
   плохо видит: заголовок «Россия в Гражданской войне» остаётся 224 px —
   ровно как в обычном режиме, — плашка, диагональ и водяной знак на
   месте. Меняется воздух вокруг них, состав не меняется.

   ⚠️ Описание карточки всё же снимается: даже после ужатия ряд получает
   меньше, чем нужно на полную карточку (замер в отчёте координатору).
   Довод dvn: в этом режиме ЧИТАЮТ, а не разглядывают, — подсказка «с чего
   начать» помогает тому, кто видит витрину целиком; тому, у кого четыре
   карточки крупно, она стоит дороже, чем даёт. Счётчик ОСТАЁТСЯ: число —
   часть обещания раздела.

   🔒 Обычный режим не меняется ни на пиксель: всё ниже — только под
   `:root.a11y`, общие блоки не переписаны. */

:root.a11y .hub { gap: calc(12px * var(--content-scale, 1)); }

:root.a11y .hub__hero-in {
  padding-top: calc(10px * var(--content-scale, 1));
  padding-bottom: calc(10px * var(--content-scale, 1));
}

/* 🔴 ВЫСОТУ ПЛАКАТА ДЕРЖИТ `min-height`, А НЕ СОДЕРЖИМОЕ — и без этой
   строки снятие кикера с абзацем не дало бы НИЧЕГО. Замер: после A+B+C
   заголовок сел на 269 px, кикер стал 0, абзац исчез, а плакат остался
   1079 — ровно `533 × --content-scale`. Внутри него `.hub__span` просто
   отъехал вниз автоотступом 518 px.
   Ставим ту же высоту, что в обычном режиме (делим на множитель a11y):
   плакат буквально совпадает с обычным — та же плашка, та же диагональ,
   тот же кегль заголовка. «Плакат остаётся плакатом» здесь не оценка,
   а равенство чисел: 720 против 720.
   Освобождает 359 px — именно их не хватало карточкам. */
:root.a11y .hub__hero {
  min-height: calc(533px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

/* C — кикер «ИНТЕРАКТИВНАЯ КАРТА» снимается: он помогает тому, кто
   охватывает плакат целиком, а в этом режиме читают, а не разглядывают. */
:root.a11y .hub__kicker { display: none; }

/* A — ЗАГОЛОВОК ПЛАКАТА НЕ НАРАЩИВАЕТСЯ. Кегль остаётся тем же, что
   в обычном режиме: он и без наращивания самый крупный текст на экране,
   и в режиме, где растёт всё, он единственный, кому расти некуда.
   Замер до правки: 336 px против 224 — только заголовок занимал 807 px
   высоты при поле хаба 1730.
   Делим на множитель a11y, а не пишем число: `--a11y-content-boost`
   ставит `app.js` из `kiosk.config.json` (см. разбор там). */
:root.a11y .hub__title {
  margin-top: calc(7px * var(--content-scale, 1));
  font-size: calc(166px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

/* B — абзац под «1917–1922» снимается, сам размах лет остаётся. */
:root.a11y .hub__sub { display: none; }

/* 🔴 ПОРОГ ПО ШИРИНЕ, И ОН НЕ ПЕРЕСТРАХОВКА — ЧИСЛО ЗАМЕРА.
   `--content-scale` от разрешения НЕ зависит (инвариант проекта, проверено
   по ядру): кегли на 2560 те же, что на 3840, а ширина меньше — поэтому
   заголовок плаката и лента годов переносятся и съедают ряды:

       3840   плакат 720 · годы 342 · под ряды 772   все плитки влезают
       2560   плакат 831 · годы 405 · под ряды 581   не влезают: ряд 1
              схлопывается до 248, чернила режет в 12 местах
       1920   то же в худшую сторону

   Киоск — 3840, и макет `hub-A11y.html` нарисован под него. Ниже порога
   режим остаётся ЧЕТЫРЁХПЛИТОЧНЫМ, как до этой правки: это не потеря
   разделов (особые режимы — в нижнем меню ядра) и не регресс —
   ровно то, что там и было.
   ⚠️ На ноутбуке и проекторе мы увидим ЧЕТЫРЕ плитки, на киоске — пять.
   Увидевший четыре не нашёл дефект, а стоит ниже порога. */
@media (min-width: 3400px) {

/* ⚠️ Ряда 2 (особых режимов) больше нет — пять карточек в один ряд
   (bb3e826). Правила ниже оставлены как есть: мок design снят ровно
   с ними (a11y 720×772), их числа ниже — история прежней раскладки.
   🔴 РЯД 1 БЕРЁТ ОСТАТОК, А НЕ СВОЁ ЧИСЛО. Разбор и числа — в блоке D выше:
   под два ряда 755, макет просит 760, и не хватает 5 px. Закреплён ряд 2
   (280 — высота обычного режима), ряд 1 получает 476 вместо 480.
   ⚠️ Ниже порога 3400 это правило не действует вовсе: там ряд 1 снова
   во всю высоту, ряда 2 нет, и раскладка ровно та, что была до правки —
   проверено твином на 2560 и 1920 (числа совпали до пикселя). */
:root.a11y .hub__cards {
  flex: 1 1 0;
  min-height: 0;
  gap: calc(21px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

/* Зазор между рядами — тоже делится: три зазора по 24.3 съедали 73 px,
   которых не хватало второму ряду. */
:root.a11y .hub { gap: calc(12px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)); }

/* 🔒 КОРОТКОЕ ИМЯ РАЗДЕЛА — РЕШЕНИЕ dvn, И ПЕРЕКЛЮЧАЕТ ЕГО CSS, А НЕ JS.
   Хаб печатает оба пролёта; спрятанный `display: none` не попадает
   и в дерево доступности, поэтому скринридер читает ровно одно имя. */
} /* @media (min-width: 3400px) — конец шестиплиточной раскладки */

/* Короткие имена — ВНЕ порога: они привязаны к режиму, а не к ширине,
   и ниже порога ряд 1 такой же, как выше. */
.hub-card__title-short { display: none; }
:root.a11y .hub-card__title-full.has-short { display: none; }
:root.a11y .hub-card__title-short { display: inline; }

/* Лента годов: воздух внутри чипа, а не размер года. Год — цель пальца
   и число, которое читают, его кегль не трогаем. */
:root.a11y .hub-year__c {
  padding-top: calc(8px * var(--content-scale, 1));
  padding-bottom: calc(8px * var(--content-scale, 1));
}

/* Кегли ряда 1 — часть варианта B, поэтому под ТЕМ ЖЕ порогом: ниже
   3400 ряд снова во всю высоту, и уменьшенный кегль там был бы чужим. */
@media (min-width: 3400px) {

/* Карточка ряда 1: кегли и поля макета `hub-A11y.html`. Пояснение
   ОСТАЁТСЯ — оно есть и в макете. Кегли здесь МЕНЬШЕ, чем даёт множитель
   режима, но БОЛЬШЕ обычного: имя 80 px против 72 в обычном и 92 при
   полном множителе — середина, ради которой ряд и ужат до 480.
   Числа макета экранные при 3840, база = макет / 1.35. */
:root.a11y .hub-card {
  padding: calc(30px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5))
           calc(27px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

:root.a11y .hub-card__num {
  margin-left: calc(22px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  font-size: max(var(--type-min-label, 24px),
                 calc(28px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)));
}

:root.a11y .hub-card__title {
  margin: calc(12px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)) 0 0
          calc(22px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  font-size: calc(59px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

:root.a11y .hub-card__text {
  margin: calc(12px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)) 0 0
          calc(22px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  font-size: calc(36px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
}

:root.a11y .hub-card__meta {
  margin: auto 0 0 calc(22px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5));
  font-size: max(var(--type-min-label, 24px),
                 calc(30px * var(--content-scale, 1) / var(--a11y-content-boost, 1.5)));
}

} /* @media (min-width: 3400px) */

/* ── КАДР ПО ФОКУСУ (правило design focusCrop, `lib/focus.js`) ──
   contain правила — снимок целиком на размытом ТОМ ЖЕ снимке (приём плашек D
   и Симбирска), не пустые поля. Фон ставится в обёртку `data-crop-host`
   (плитка, медальон A, плашка D); снимок — над ним. */
[data-crop-host] { position: relative; isolation: isolate; }
.focus-bg {
  position: absolute; inset: -8%; width: 116%; height: 116%; z-index: 0;
  object-fit: cover; filter: blur(16px) brightness(0.45) grayscale(0.2);
  pointer-events: none;
  --anchor: backdrop-any;
}
.p29-tile__img, .pv-chip__med > img { position: relative; }
[data-crop-host] > img:not(.focus-bg) { z-index: 1; }

/* ═══════════════ СИМБИРСК — ВИД ПО СПЕКЕ design 29.09 (d56d5b3), app-311 ═══
   dvn принял концепт: фон — размытый снимок главы, главы колонкой слева,
   «Персоналии Симбирска» последним пунктом, глава справа (текст | кадр).
   Числа — экранные при 3840 (стенд живёт в них 1:1, замер: innerWidth 3840,
   `--touch-min` 64, кнопки ядра 96, строка ядра 64…173). Только «Сцена»:
   «Витрина» — выбор оператора в сервис-панели, её вёрстка прежняя. */
.lr.is-scene { padding: 0; display: block; }
.lr.is-scene .lr__stage { display: contents; }

.lr__bg { display: none; }
.lr.is-scene .lr__bg { display: block; position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.lr__bg-img {
  position: absolute; left: -80px; top: -80px;
  width: calc(100% + 160px); height: calc(100% + 160px);
  object-fit: cover;
  /* Х12 (dvn 30.09 «не такой размытый, а то грустно»): было blur 36 · grayscale .35 · brightness .55 */
  filter: blur(12px) brightness(0.8);
}
.lr__bg-img:not([src]) { display: none; }
.lr__bg::before, .lr__bg::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.lr__bg::before {
  /* Х12: затемнение — только под текстом (колонка глав и текст главы, 0…70 %),
     справа над кадром почти прозрачно. Было 92/78/35/55 %. */
  background: linear-gradient(90deg, rgba(var(--amber-black-rgb), 0.82) 0%, rgba(var(--amber-black-rgb), 0.7) 30%,
    rgba(var(--amber-black-rgb), 0.55) 72%, rgba(var(--amber-black-rgb), 0.12) 80%, rgba(var(--amber-black-rgb), 0.08) 100%);
  /* Х12б (проба V design 262de51): стопы 62/76 → 72/80 — правый конец заголовка главы
     был 4.3:1; подстановкой design — заголовок 7.8, текст 7.9–9.8, справа светло. */
}
.lr__bg::after {
  background: linear-gradient(180deg, rgba(var(--amber-black-rgb), 0.15) 0%, rgba(var(--amber-black-rgb), 0) 18%,
    rgba(var(--amber-black-rgb), 0) 80%, rgba(var(--amber-black-rgb), 0.45) 100%);
}

/* Шапка: верх букв ~50 px под строкой ядра (как МТК 39), справа — ничего. */
.lr.is-scene .lr__head {
  position: absolute; left: 64px; top: 212px;
  display: flex; flex-direction: column; gap: 12px;
}
.lr.is-scene .lr__title { font-size: 96px; line-height: 0.95; text-transform: uppercase; }
.lr.is-scene .lr__sub {
  font-size: 28px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-muted);
}

/* Колонка глав: x 64, ширина 860, верх 404, низ ≤ 1770. */
.lr.is-scene .lr__rail {
  position: absolute; left: 64px; top: 404px; width: 860px; max-height: 1366px;
  overflow-y: auto; display: flex; flex-direction: column; gap: 4px;
}
.lr-chap {
  flex: 0 0 auto;
  min-height: 108px;
  padding: 10px 24px 10px 20px;
  display: flex; align-items: center; gap: 24px;
  border: 0; border-radius: 16px;
  background: transparent; color: var(--text-on-dark);
  text-align: left;
}
.lr-chap.is-on { background: rgba(var(--brass-rgb), 0.16); box-shadow: inset 6px 0 0 var(--active); }
.lr-chap:active { background: rgba(var(--brass-rgb), 0.24); }
.lr-chap__n {
  width: 72px; flex-shrink: 0; text-align: right;
  font-family: var(--font-display); font-size: 52px; line-height: 1;
  color: rgba(var(--paper-rgb), 0.45);
}
.lr-chap.is-on .lr-chap__n { color: var(--active); }
.lr-chap__tx { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.lr-chap__t { font-family: var(--font-text); font-size: 32px; line-height: 1.05; }
.lr-chap__d { font-family: var(--font-caption); font-size: 24px; letter-spacing: 0.04em; color: var(--text-muted); }

/* «Персоналии Симбирска» — отделён ЦВЕТОМ, не отступом (слово dvn). */
.lr-chap--persons {
  padding-right: 32px;
  border: 2px solid rgba(var(--brass-rgb), 0.7);
  background: rgba(var(--amber-black-rgb), 0.32);
  color: var(--active);
  font-family: var(--font-caption); font-size: 30px; letter-spacing: 0.04em;
}
.lr-chap--persons .lr-chap__n { color: var(--active); display: flex; justify-content: flex-end; }
.lr-chap--persons svg { width: 48px; height: 48px; }
.lr-chap__pt { flex-grow: 1; text-transform: uppercase; }
.lr-chap__pn { padding: 2px 14px; border-radius: 20px; background: var(--active); color: var(--text-on-light); }
.lr-chap__chev { font-size: 40px; }

/* Глава: x 1080, ширина 2696, верх 404, низ 1920; сетка 1fr | 980, зазор 88. */
.lr.is-scene .lr__body {
  position: absolute; left: 1080px; top: 404px; width: 2696px; height: 1516px;
  padding: 0; overflow: visible;
}
/* ⚠️ Статья главы — НЕ `.lr-sec`: старое `.lr.is-scene .lr-sec` (сетка
   1.25fr | 1fr, align-items: start) сильнее по весу и подменяло колонки. */
.lr-ch {
  height: 100%; margin: 0;
  display: grid; grid-template-columns: minmax(0, 1fr) 980px; column-gap: 88px;
  /* 🔴 без строки 1fr дорожка росла по содержимому, и текст длинной главы
     (гл. 8) уходил под навигацию ядра — прокрутка не включалась */
  grid-template-rows: minmax(0, 1fr);
}
.lr-ch__text { display: flex; flex-direction: column; gap: 36px; min-width: 0; min-height: 0; }
.lr-ch__kick {
  margin: 0; font-family: var(--font-caption); font-size: 30px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--active);
}
.lr-ch__title {
  margin: 0; font-family: var(--font-display); font-weight: 400;
  font-size: 112px; line-height: 0.98; text-transform: uppercase;
}
.lr-ch__lede { margin: 0; max-width: 1560px; font-size: 46px; line-height: 1.3; }
.lr-ch__rule { flex: 0 0 auto; width: 120px; height: 3px; background: var(--active); }
.lr-ch__scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: var(--scrollbar-w, 18px); }
.lr-ch__cols { columns: 2; column-gap: 72px; font-size: 34px; line-height: 1.5; color: rgba(var(--paper-rgb), 0.92); }
.lr-ch__cols p { margin: 0 0 28px; }

.lr-ch__side { display: flex; flex-direction: column; gap: 24px; padding-top: 12px; min-height: 0; }
.lr-ch__fig { margin: 0; display: flex; flex-direction: column; gap: 18px; }
.lr-ch__frame {
  position: relative; width: 980px; height: 760px; overflow: hidden;
  background: rgba(var(--amber-black-rgb), 0.9);
  box-shadow: 0 30px 80px rgba(var(--amber-black-rgb), 0.55);
}
.lr-ch__blur {
  position: absolute; inset: -40px; width: calc(100% + 80px); height: calc(100% + 80px);
  object-fit: cover; filter: blur(24px) brightness(0.5);
}
.lr-ch__img, .lr-ch__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.lr-ch__map { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; background: rgba(var(--paper-rgb), 0.06); }
.lr-ch__ph, .lr-ch__typo {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  padding: 80px; text-align: center;
  background: rgba(var(--paper-rgb), 0.08); color: var(--text-muted);
  font-family: var(--font-caption); font-size: 28px;
}
.lr-ch__typo { font-family: var(--font-display); font-size: 240px; color: rgba(var(--paper-rgb), 0.18); }
.lr-ch__frame .lr-shot__mark { z-index: 2; }
.lr-ch__play {
  position: absolute; left: 50%; top: 50%; z-index: 2;
  width: 176px; height: 176px; margin: -88px 0 0 -88px;
  border: 0; border-radius: 88px;
  background: var(--active); color: var(--text-on-light);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 20px 60px rgba(var(--amber-black-rgb), 0.5);
}
.lr-ch__play svg { width: 72px; height: 72px; }
.lr-ch__play:active { filter: brightness(1.1); }
.lr-ch__pill {
  position: absolute; z-index: 2; height: 64px; padding: 0 28px;
  display: flex; align-items: center; border-radius: 32px;
  background: rgba(var(--amber-black-rgb), 0.6); color: var(--text-on-dark);
  font-family: var(--font-caption); font-size: 28px; pointer-events: none;
}
.lr-ch__pill.is-pos { right: 24px; top: 24px; }
.lr-ch__pill.is-film { left: 24px; bottom: 24px; font-size: 26px; letter-spacing: 0.08em; text-transform: uppercase; }
.lr-ch__cap { font-family: var(--font-caption); font-size: 26px; line-height: 1.35; color: var(--text-muted); }

/* Лента материалов: 108×108, зазор 16, больше 8 — листается (скролл виден). */
.lr-ch__strip { width: 980px; display: flex; gap: 16px; overflow-x: auto; padding-bottom: 8px; }
.lr-ch__thumb {
  position: relative; flex-shrink: 0; width: 108px; height: 108px; padding: 0;
  overflow: hidden; border-radius: 12px; border: 4px solid transparent;
  background: rgba(var(--amber-black-rgb), 0.9); opacity: 0.7;
}
.lr-ch__thumb.is-on { border-color: var(--active); opacity: 1; }
.lr-ch__thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.lr-ch__tnum {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(var(--paper-rgb), 0.08); color: var(--text-muted);
  font-family: var(--font-caption); font-size: 22px;
}
.lr-ch__tplay {
  position: absolute; left: 50%; top: 50%; width: 56px; height: 56px; margin: -28px 0 0 -28px;
  border-radius: 28px; background: var(--active); color: var(--text-on-light);
  display: flex; align-items: center; justify-content: center;
}
.lr-ch__tplay svg { width: 26px; height: 26px; }

.lr-ch__steps { margin-top: auto; display: flex; gap: 16px; }
.lr-ch__step {
  flex: 1 1 0; height: 112px; border: 0; border-radius: 56px;
  background: rgba(var(--amber-black-rgb), 0.36); color: var(--text-on-dark);
  font-family: var(--font-caption); font-size: 30px;
}
.lr-ch__step.is-next { background: var(--active); color: var(--text-on-light); }
.lr-ch__step:active { filter: brightness(1.12); }

/* ═══════════════ ХРОНИКА НАЧИСТО — ВИД ПО СПЕКЕ design 29.09 (f73cea0), app-311 ═══
   dvn: «да, принимаю». Раскладка А — единственная на экране (Б — выбор
   оператора в сервис-панели, её вёрстка прежняя; В снята dvn 07.10). Числа — экранные при 3840
   (стенд в них 1:1, как Симбирск app-311). Своего справа вверху нет: там лупа
   ядра. Отбор «С роликом» — финдером ядра, на экране — только плашка. */
.chx__title { margin: 0; font-family: var(--font-display); font-weight: 400; }
.chx__plate[hidden] { display: none; }
.chx:not([data-chx-v="A"]) .chx__plate { align-self: flex-start; }

.chx[data-chx-v="A"] { padding: 0; display: block; }
.chx[data-chx-v="A"] .chx__head {
  position: absolute; left: 64px; top: 212px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 12px;
}
.chx[data-chx-v="A"] .chx__title { font-size: 96px; line-height: 0.95; letter-spacing: 0.01em; text-transform: uppercase; }
.chx__subl {
  display: flex; gap: 28px; align-items: center;
  font-family: var(--font-caption); font-size: 28px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-muted);
}
.chx[data-chx-v="A"] .chx__legend { margin: 0; gap: 28px; font-size: 28px; }
.chx[data-chx-v="A"] .chx__legend span { gap: 12px; }
.chx[data-chx-v="A"] .chx__legend b { width: 22px; height: 22px; }
.chx[data-chx-v="A"] .chx__untranslated { position: absolute; left: 1100px; top: 340px; }

/* Плашка отбора: x 1100, высота 72, латунь; тап — снять отбор. */
.chx__plate {
  height: 72px; padding: 0 16px 0 28px;
  display: flex; align-items: center; gap: 18px;
  border: 0; border-radius: 36px;
  background: var(--active); color: var(--text-on-light);
  font-family: var(--font-caption); font-size: 28px; letter-spacing: 0.06em; text-transform: uppercase;
}
.chx[data-chx-v="A"] .chx__plate { position: absolute; left: 1100px; top: 250px; }
.chx__plate-x {
  width: 48px; height: 48px; border-radius: 24px;
  background: rgba(var(--graphite-rgb), 0.18);
  display: flex; align-items: center; justify-content: center; font-size: 34px;
}
.chx__plate:active { filter: brightness(1.1); }
/* app-311: чип запроса в строке подписи — вид сетки (.p29-chip): строчные и без
   разрядки подписи; `display` чипа иначе отменил бы [hidden]. */
.chx__qchip { text-transform: none; letter-spacing: normal; }
.chx__qchip[hidden] { display: none; }

/* Годы = график: x 64, верх 404, 3712×196, шесть колонок, зазор 16. */
.chx__yg { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 16px; }
.chx[data-chx-v="A"] .chx__yg { position: absolute; left: 64px; top: 404px; width: 3712px; height: 196px; }
.chx-yc {
  height: 196px; padding: 18px 24px 16px;
  display: flex; flex-direction: column; justify-content: space-between;
  border: 0; border-radius: 20px; text-align: left;
  background: rgba(var(--amber-black-rgb), 0.18); color: var(--text-on-dark);
}
.chx-yc.is-on { background: rgba(var(--brass-rgb), 0.14); box-shadow: inset 0 -4px 0 var(--active); }
.chx-yc:active { background: rgba(var(--brass-rgb), 0.2); }
.chx-yc.is-dead { opacity: 0.4; }
.chx-yc__bars { display: flex; align-items: flex-end; gap: 6px; height: 96px; }
.chx-yc__bar { flex: 1 1 0; background: rgba(var(--paper-rgb), 0.42); }
.chx-yc__bar.is-zero { background: rgba(var(--paper-rgb), 0.14); }
.chx-yc.is-on .chx-yc__bar:not(.is-zero) { background: var(--active); }
.chx-yc__bar.is-now { outline: 3px solid var(--paper); outline-offset: 3px; }
.chx-yc__foot { display: flex; align-items: baseline; justify-content: space-between; }
.chx-yc__y { font-family: var(--font-display); font-size: 56px; line-height: 1; }
.chx-yc__n { font-family: var(--font-caption); font-size: 28px; letter-spacing: 0.04em; color: var(--text-muted); }
.chx-yc.is-on .chx-yc__y, .chx-yc.is-on .chx-yc__n { color: var(--active); }

/* Поле А: лента 64…1504, скроллбар 1524, карта и материал 1600…3776; верх 648, низ 1968. */
.chx[data-chx-v="A"] .chx__field { position: absolute; left: 64px; top: 648px; width: 3712px; height: 1320px; display: block; }
.chx[data-chx-v="A"] .chx__ribw { position: absolute; left: 0; top: 0; width: 1440px; height: 1320px; display: flex; flex-direction: column; gap: 10px; }
.chx__ribh {
  flex: 0 0 64px; display: flex; align-items: center; justify-content: space-between;
  font-family: var(--font-caption); font-size: 30px; letter-spacing: 0.12em; text-transform: uppercase;
  border-bottom: 1.5px solid rgba(var(--brass-rgb), 0.35);
}
.chx__ribh span:first-child { color: var(--active); }
.chx__ribh span:last-child { color: var(--text-muted); }
.chx[data-chx-v="A"] .chx__rib {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0;
  display: flex; flex-direction: column; gap: 10px;
  scrollbar-width: none;
}
.chx[data-chx-v="A"] .chx__rib::-webkit-scrollbar { display: none; }
.chx[data-chx-v="A"] .chx__month {
  flex: 0 0 56px; margin: 0; padding: 0 0 6px;
  display: flex; align-items: flex-end; justify-content: space-between;
  border: 0; background: var(--graphite);
  font-size: 28px; letter-spacing: 0.12em; color: var(--text-muted);
}
.chx__mcnt { color: var(--text-muted); }
.chx-row.is-a {
  flex-shrink: 0; margin: 0;
  grid-template-columns: 200px minmax(0, 1fr) 100px; gap: 24px;
  padding: 22px 24px 22px 36px;
  border: 0; border-radius: 12px;
  background: rgba(var(--amber-black-rgb), 0.16);
  box-shadow: inset 8px 0 0 var(--lc);
}
.chx-row.is-a::before { display: none; }
.chx-row.is-a[data-line="both"] { box-shadow: inset 8px 0 0 var(--line-pol), inset 16px 0 0 var(--line-mil); }
.chx-row.is-a.is-sel { background: rgba(var(--brass-rgb), 0.14); box-shadow: inset 8px 0 0 var(--lc), inset 0 0 0 2px var(--active); }
.chx-row.is-a[data-line="both"].is-sel { box-shadow: inset 8px 0 0 var(--line-pol), inset 16px 0 0 var(--line-mil), inset 0 0 0 2px var(--active); }
.chx-row.is-a:active { background: rgba(var(--brass-rgb), 0.2); }
.chx-row.is-a .chx-row__dt { padding-top: 4px; font-size: 30px; color: var(--active); }
.chx-row.is-a .chx-row__tx { font-size: 34px; line-height: 1.3; }
.chx-row__ic { display: flex; gap: 14px; align-items: flex-start; padding-top: 6px; color: var(--text-muted); }
.chx-row__ic svg { width: 36px; height: 36px; flex: 0 0 auto; }
.chx-row__ic .is-film { fill: var(--active); }
.chx-row__ic .is-photo { fill: none; stroke: currentColor; stroke-width: 1.8; }
/* ВЕХИ (app-311; слово dvn 07.10, кадр design ed0cc96): `milestone: true` → дата
   весом 700 + линейка 3 px латунью сверху; кромка линии слева остаётся. Выбранная
   веха — линейка + кольцо 2 px. Б: та же линейка (кромка — ::before). Новых токенов нет. */
.chx-row.is-mile .chx-row__dt { font-weight: 700; }
.chx-row.is-mile:not(.is-a) { box-shadow: inset 0 3px 0 var(--active); }
.chx-row.is-a.is-mile { box-shadow: inset 0 3px 0 var(--active), inset 8px 0 0 var(--lc); }
.chx-row.is-a.is-mile[data-line="both"] { box-shadow: inset 0 3px 0 var(--active), inset 8px 0 0 var(--line-pol), inset 16px 0 0 var(--line-mil); }
.chx-row.is-a.is-mile.is-sel { box-shadow: inset 0 3px 0 var(--active), inset 8px 0 0 var(--lc), inset 0 0 0 2px var(--active); }
.chx-row.is-a.is-mile[data-line="both"].is-sel { box-shadow: inset 0 3px 0 var(--active), inset 8px 0 0 var(--line-pol), inset 16px 0 0 var(--line-mil), inset 0 0 0 2px var(--active); }
.chx__nofilm { margin-top: 120px; padding: 56px; background: rgba(var(--amber-black-rgb), 0.22); display: flex; flex-direction: column; gap: 24px; }
.chx__nofilm p { margin: 0; }
.chx__nofilm-t { font-size: 44px; }
.chx__nofilm-s { font-size: 34px; line-height: 1.35; color: var(--text-muted); }

/* Скроллбар ленты — виден всегда: 18×1320, трек paper 8 %, бегунок латунью. */
.chx__sb { display: none; }
.chx[data-chx-v="A"] .chx__sb {
  display: block; position: absolute; left: 1460px; top: 0; width: 18px; height: 1320px;
  border-radius: 9px; background: rgba(var(--paper-rgb), 0.08);
}
.chx__sb i { position: absolute; left: 0; right: 0; top: 0; height: 96px; border-radius: 9px; background: var(--active); }

/* Карта 2176×820 и материал 2176×468 (низ 1968 — над навигацией ядра). */
.chx[data-chx-v="A"] .chx__side { position: absolute; left: 1536px; top: 0; width: 2176px; height: 1320px; display: flex; flex-direction: column; gap: 32px; }
.chx[data-chx-v="A"] .chx__mapbox { flex: 0 0 820px; height: 820px; }
.chx[data-chx-v="A"] .chx__mat {
  flex: 0 0 468px; height: 468px; padding: 40px 48px;
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 48px;
  background: rgba(var(--amber-black-rgb), 0.22); border: 0;
}
.chx[data-chx-v="A"] .chx__matbody { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
.chx[data-chx-v="A"] .chx__kick { font-family: var(--font-caption); font-size: 28px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); }
.chx[data-chx-v="A"] .chx__mat h2 { font-size: 50px; line-height: 1.15; font-weight: 400; }
.chx[data-chx-v="A"] .chx__acts { flex-direction: column; gap: 16px; align-items: stretch; }
.chx[data-chx-v="A"] .chx__tabs {
  flex-wrap: nowrap; gap: 0; height: 96px; padding: 6px; border-radius: 48px;
  background: rgba(var(--amber-black-rgb), 0.28);
}
.chx[data-chx-v="A"] .chx__tab {
  min-height: 0; height: 84px; padding: 0 30px; border: 0; border-radius: 42px;
  background: transparent; font-family: var(--font-caption); font-size: 30px; color: var(--text-on-dark);
  display: flex; align-items: center;
}
.chx[data-chx-v="A"] .chx__tab.is-on { background: var(--active); color: var(--text-on-light); }
.chx[data-chx-v="A"] .chx__tab.is-off { opacity: 0.45; border: 0; }
.chx[data-chx-v="A"] .chx__pri {
  height: 96px; border-radius: 48px; border: 2px solid var(--active);
  background: transparent; color: var(--active);
  font-family: var(--font-caption); font-size: 30px;
}

/* Х2 (app-311): метка места у пина — крупно, на плашке, справа от точки. */
.chx__pin-l {
  position: absolute; left: calc(100% + 18px); top: 50%; transform: translateY(-50%);
  white-space: nowrap; padding: 6px 18px; border-radius: 8px;
  background: rgba(var(--amber-black-rgb), 0.78); color: var(--text-on-dark);
  font-family: var(--font-text); font-size: 44px; line-height: 1.15; font-weight: 700;
}
.chx__pin-l:empty { display: none; }

/* Х4 (app-311): фирменная красная лента ядра (.kiosk-stripes, z 0) просвечивала
   сквозь полупрозрачные фоны карты, материала и колонок годов — подложка
   непрозрачная: тот же тон поверх графита. */
.chx[data-chx-v="A"] .chx__mapbox { background: var(--graphite); }
.chx[data-chx-v="A"] .chx__mat {
  background: linear-gradient(rgba(var(--amber-black-rgb), 0.22), rgba(var(--amber-black-rgb), 0.22)), var(--graphite);
}
.chx-yc { background: linear-gradient(rgba(var(--amber-black-rgb), 0.18), rgba(var(--amber-black-rgb), 0.18)), var(--graphite); }
.chx-yc.is-on { background: linear-gradient(rgba(var(--brass-rgb), 0.14), rgba(var(--brass-rgb), 0.14)), var(--graphite); }
/* Х4: тело материала листается внутри 468, а не растёт под навигацию ядра. */
.chx[data-chx-v="A"] .chx__mat { grid-template-rows: minmax(0, 1fr); overflow: hidden; }
/* Х4: 468 и 820 — внешние размеры (поля и рамка внутри), иначе материал
   выходил 548 и кончался на 2050 — под навигацией ядра (замер app-311). */
.chx[data-chx-v="A"] .chx__mat, .chx[data-chx-v="A"] .chx__mapbox { box-sizing: border-box; }

/* Х6 (app-311, dvn 30.09 «Симбирск выглядит странно»): затемнения фона
   (.lr__bg::before/::after, z 1) всплывали НАД шапкой, главами и текстом —
   сам .lr__bg своего контекста наложения не создавал. Фон — свой контекст
   на дне, содержимое — над ним. ⚠️ .lr__stage в «Сцене» — display: contents,
   z-index на нём не действует: поднимаются сами блоки. (design 62f2945) */
.lr.is-scene .lr__bg { z-index: 0; isolation: isolate; }
.lr.is-scene .lr__head, .lr.is-scene .lr__rail, .lr.is-scene .lr__body, .lr.is-scene .lr__grid { z-index: 1; }

/* Х5 (app-311, dvn 30.09): карточки Гособразований и Персоналий — 8 в ряд
   (tileMin 320), снимок ниже: гособразования 7:6 (1:1 давал карточку 745 — второй ряд кончался на 2016 при навигации 1984), персоны 5:6 (лица — фокус из
   данных, как было). Основа карточки непрозрачная — лента ядра не сквозит. */
.p29.is-states .p29-tile__media { aspect-ratio: 7 / 6; }
.p29.is-persons .p29-tile__media { aspect-ratio: 5 / 6; }
.p29-tile { background-color: var(--graphite); }

/* Х7: отбор «Симбирск» в Персоналиях — крупно. */
.p29__chips.is-simb { align-items: center; gap: 28px; }
.p29-simb {
  font-family: var(--font-display); font-size: 64px; line-height: 1;
  text-transform: uppercase; color: var(--active);
}
.p29-simb__plate { font-size: 40px; height: 88px; }

/* Х8б (app-311): плашка отбора — в строке подписи после легенды, +48 (зазор 28 + 20), по центру строки. */
.chx[data-chx-v="A"] .chx__subl .chx__plate { position: static; margin-left: 20px; letter-spacing: 0.06em; }

/* Х9 (app-311, dvn 30.09): плавный блюр перелёта — только карта и слой ролика;
   штамп и пин поверх не размываются. Вход 300 мс ease-in, выход 400 мс ease-out. */
.chx__mapbox .maplibregl-map, .chx__mapbox .chx__anim { filter: blur(0); transition: filter 400ms ease-out; }
.chx__mapbox.is-flying .maplibregl-map, .chx__mapbox.is-flying .chx__anim { filter: blur(12px); transition: filter 300ms ease-in; }

/* Х10 (app-311): вид B Партий — строка целиком цель тапа (:active); заголовок
   группы в одну строку, число сразу за названием; заголовок липнет под осью
   лет — при прокрутке не срезается сверху. */
.pv-row:active { background: rgba(var(--brass-rgb), 0.14); }
.pv-row:active .pv-row__nm { color: var(--active); }
.pv-ghead { position: sticky; top: 0; z-index: 2; background: var(--graphite); }
.pv-ghead__t { flex: 0 0 auto; min-width: var(--name-w); white-space: nowrap; padding-right: 24px; }

/* Х11 (app-311, приёмка design 3e3d3eb): абзац материала (.chx__why) flex сжимал
   до высоты 0 с app-281 — текста не было. Абзац не сжимается, место берётся у фото. */
.chx__why { flex: 0 0 auto; }
.chx[data-chx-v="A"] .chx__photo { flex: 0 1 auto; min-height: 0; }

/* ── КАРТА ГОСОБРАЗОВАНИЯ НА АТЛАСЕ — спека design cf98113 ──────────────
   Блок 1395 × 784 (ширина колонки, 16:9), фон графит; узел атласа сам
   `position:absolute; inset:0`. Пин — язык Х2 Хроники (п. 10), плашка
   переезжает влево у правого края (п. 12). Строка источника — квадрат
   лагеря + «<Лагерь> · по карте музея, …» (п. 15). Фазы — пилюли 96 (п. 19). */
.stm__box {
  position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden;
  background: var(--graphite);
  border-radius: calc(6px * var(--content-scale, 1));
}
.stm__pin {
  position: absolute; left: 0; top: 0; z-index: 2; pointer-events: none;
  width: calc(22px * var(--content-scale, 1)); height: calc(22px * var(--content-scale, 1));
  margin: calc(-11px * var(--content-scale, 1)) 0 0 calc(-11px * var(--content-scale, 1));
  border-radius: 50%; background: var(--red);
  box-shadow: 0 0 0 calc(6px * var(--content-scale, 1)) rgba(var(--brass-rgb), 0.35),
              0 0 0 calc(8px * var(--content-scale, 1)) var(--brass);
}
.stm__pin[hidden] { display: none; }
.stm__pin-l {
  position: absolute; left: calc(100% + 18px); top: 50%; transform: translateY(-50%);
  white-space: nowrap; padding: 6px 18px; border-radius: 8px;
  background: rgba(var(--amber-black-rgb), 0.78); color: var(--text-on-dark);
  font-family: var(--font-text); font-size: 44px; line-height: 1.15; font-weight: 700;
}
.stm__pin.is-left .stm__pin-l { left: auto; right: calc(100% + 18px); }
.stm__src {
  display: flex; align-items: center; gap: 16px;
  min-height: 56px; margin: 16px 0 0;
  font-family: var(--font-caption); font-size: 28px; line-height: 1.25;
  color: var(--text-muted);
}
.stm__sq { flex: 0 0 22px; width: 22px; height: 22px; background: var(--camp-red); }
.stm__sq[data-camp="white"]        { background: var(--camp-white); }
.stm__sq[data-camp="national"]     { background: var(--camp-national); }
.stm__sq[data-camp="rev-dem"]      { background: var(--camp-rev-dem); }
.stm__sq[data-camp="green"]        { background: var(--camp-green); }
.stm__sq[data-camp="intervention"] { background: var(--camp-intervention); }
.stm__sq[data-camp="uprising"]     { background: var(--camp-uprising); }
.stm__phases {
  display: flex; gap: 16px; margin-top: 16px;
  overflow-x: auto; overflow-y: hidden; padding-bottom: 4px;
  /* 🔴 Ряд НЕ распирает колонку: без этого min-content длинного ряда (5 фаз
   * Северной обл. с региональными «Конец августа 1918») растил колонку карточки
   * — блок 1500×844 вместо 1395×784 (stm-probe ?fx=1, 02.10). Не влезло — прокрутка. */
  contain: inline-size;
}
.stm__phases[hidden] { display: none; }
/* Скроллбар виден всегда, когда пилюли не влезают (п. 19): системный
   «исчезающий» на macOS не показывает, что ряд листается. */
.stm__phases::-webkit-scrollbar { height: 12px; }
.stm__phases::-webkit-scrollbar-thumb { background: rgba(var(--brass-rgb), 0.5); border-radius: 6px; }
.stm__phase {
  flex: 0 0 auto; min-height: 96px; padding: 0 32px; border-radius: 48px;
  background: transparent; border: 2px solid rgba(var(--brass-rgb), 0.35);
  color: var(--text-on-dark);
  font-family: var(--font-caption); font-size: 32px; white-space: nowrap;
}
.stm__phase.is-active { background: var(--active); border-color: var(--active); color: var(--text-on-light); }
.stm__phase:active { background: rgba(var(--brass-rgb), 0.35); }

/* ── РОЛИК-КАРТА МТК-30: ПЛИТКА НА МЕСТЕ + СЛОЙ «ВО ВСЮ ШИРИНУ» ─────────────
   Спека design fd43777 (`docs/design/spec-video-zone-0110.md`), выбор dvn
   01.10. Механика — `lib/film-tile.js`. Ролики 1920×622 (3.09:1): плитка
   980×317 и полоса 1680×544 — та же пропорция, `cover` без полей. */
.lr-ch.has-film .lr-ch__frame { height: 660px; }           /* п. 4: окно 980×660 */
.lr-ch__film { margin-top: 16px; display: flex; flex-direction: column; gap: 12px; }  /* 24 + 16 = 40, п. 2 */
.p29-card__film { margin: 12px 0 calc(var(--gap) / 2); display: flex; flex-direction: column; gap: 12px; }
.ftl-head {
  margin: 0; display: flex; align-items: center; gap: 14px;
  font-family: var(--font-caption); font-size: 26px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--brass);
}
.ftl-head__ic { display: inline-flex; width: 34px; height: 34px; }
.ftl-head__ic svg { width: 100%; height: 100%; }
.ftl {
  position: relative; overflow: hidden; box-sizing: border-box;
  border-radius: 12px; border: 2px solid rgba(var(--brass-rgb), 0.55);
  background: rgba(var(--amber-black-rgb), 0.9);
}
.ftl.is-tile { width: 980px; height: 317px; }
.ftl.is-band { width: 100%; aspect-ratio: 1920 / 622; }
.ftl__v { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.ftl__veil { position: absolute; inset: 0; pointer-events: none; }
.ftl.is-tile .ftl__veil { background: linear-gradient(90deg, rgba(var(--amber-black-rgb), 0.82), rgba(var(--amber-black-rgb), 0)); }
.ftl.is-band .ftl__veil { background: rgba(var(--amber-black-rgb), 0.35); }
.ftl__face { position: absolute; inset: 0; display: flex; align-items: center; gap: 36px; padding: 0 48px; pointer-events: none; }
.ftl.is-band .ftl__face { justify-content: center; padding: 0; }
.ftl__play {
  flex: 0 0 auto; width: 128px; height: 128px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--active); color: var(--text-on-light);
  box-shadow: 0 16px 48px rgba(var(--amber-black-rgb), 0.5);
}
.ftl.is-band .ftl__play { width: 160px; height: 160px; }
.ftl__play svg { width: 52%; height: 52%; }
.ftl__txt { display: flex; flex-direction: column; gap: 10px; max-width: 520px; }
.ftl__txt b { font-family: var(--font-text); font-size: 36px; line-height: 1.15; font-weight: 700; color: var(--text-on-dark); }
.ftl__txt i { font-style: normal; font-family: var(--font-caption); font-size: 24px; color: var(--text-muted); }
.ftl__hit { position: absolute; inset: 0; z-index: 1; border: 0; padding: 0; background: transparent; }
.ftl__bar { position: absolute; left: 0; right: 0; bottom: 0; height: 6px; z-index: 2; pointer-events: none; }
.ftl.is-poster .ftl__bar { display: none; }
.ftl__wide, .ftl__again {
  position: absolute; z-index: 3; height: 96px; padding: 0 32px; border-radius: 48px; border: 0;
  display: inline-flex; align-items: center; gap: 14px;
  font-family: var(--font-caption); font-size: 28px;
}
.ftl__wide { right: 24px; top: 24px; background: rgba(var(--amber-black-rgb), 0.62); color: var(--text-on-dark); }
.ftl__again { right: 24px; bottom: 24px; background: var(--active); color: var(--text-on-light); }
.ftl__again[hidden] { display: none; }
.ftl__wide svg, .ftl__again svg { width: 36px; height: 36px; flex: 0 0 auto; }
.ftl__wide:active, .ftl__again:active { filter: brightness(1.12); }
/* Играет / кончился — лицо (▶ и подписи) и затемнение убраны; на паузе — ▶. */
.ftl.is-playing .ftl__veil, .ftl.is-playing .ftl__face,
.ftl.is-ended .ftl__veil, .ftl.is-ended .ftl__face { display: none; }
.ftl.is-paused .ftl__txt { display: none; }
.ftl.is-paused .ftl__veil { background: rgba(var(--amber-black-rgb), 0.35); }
.ftl.is-paused .ftl__face { justify-content: center; padding: 0; }

/* Слой «Во всю ширину» — внутри корня сцены (под строкой ядра и навигацией). */
.ftl-layer { position: absolute; inset: 0; z-index: 20; background: rgba(var(--amber-black-rgb), 0.95); }
.ftl-layer__kick {
  position: absolute; left: 64px; top: 212px; margin: 0;
  font-family: var(--font-caption); font-size: 30px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--brass);
}
.ftl-layer__title {
  position: absolute; left: 64px; top: 262px; right: 520px; margin: 0;
  font-family: var(--font-display); font-size: 80px; line-height: 1; text-transform: uppercase; color: var(--text-on-dark);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ftl-layer__close {
  position: absolute; right: 64px; top: 212px; height: 96px; padding: 0 40px; border-radius: 48px;
  border: 2px solid var(--active); background: transparent; color: var(--brass);
  font-family: var(--font-caption); font-size: 30px; text-transform: uppercase;
}
.ftl-layer__frame { position: absolute; left: 64px; top: 420px; width: 3712px; height: 1201px; background: rgba(var(--amber-black-rgb), 1); }
.ftl-layer__v { position: absolute; left: 0; top: 0; width: 100%; height: 100%; object-fit: contain; }
.ftl-layer__bar { position: absolute; left: 64px; top: 1637px; width: 3712px; height: 6px; }
.ftl-layer__foot { position: absolute; left: 64px; top: 1683px; display: flex; align-items: center; gap: 32px; }
.ftl-layer__again {
  height: 96px; padding: 0 40px; border-radius: 48px; border: 0; display: inline-flex; align-items: center; gap: 14px;
  background: var(--active); color: var(--text-on-light); font-family: var(--font-caption); font-size: 30px;
}
.ftl-layer__again svg { width: 36px; height: 36px; }
.ftl-layer__meta { font-family: var(--font-caption); font-size: 28px; color: var(--text-muted); }
.ftl-layer__close:active, .ftl-layer__again:active { filter: brightness(1.12); }

/* ═══ НИЖНИЙ РЯД ОДНОЙ ГЕОМЕТРИИ — спека design 551ee2e/7e02097 (слово dvn 02.10) ═══
   Кнопки «Главная» НЕТ ни вверху, ни в полосе (dvn 02.10: «хаб есть в начале, возврат —
   по бездействию»; выход из заставки ядра ведёт на defaultScene = hub).
   Одно число на два ряда: зазор разделов `--sec-gap` берут и `.hub__cards`, и полоса ядра
   `.kiosk-nav__cards` → 5 × 720, шаг 748 при 3840: колонка N полосы стоит под карточкой N хаба. */
:root { --sec-gap: calc(21px * var(--content-scale, 1)); }
/* 🔒 ВКЛЮЧАЕТСЯ САМО ВМЕСТЕ С ЯДРОМ 1.29.6 (`nav.homeCard:false`): пока ядро строит карточку
   дома, полоса — 6 карточек, и общий зазор дал бы 6 × 595 (ни прежние 605, ни хаб) — правило
   молчит (`:has` карточки дома). Карточку дома CSS-ом НЕ прячем (координатор, dvn «не спешим»). */
.kiosk-nav__cards:not(:has(> .kiosk-nav__card--home)) { gap: var(--sec-gap); }


/* ═══ РОЛИК ИЗ ЧАСТЕЙ — спека design dfd726c (`lib/film-parts.js`) ═══
   Две `<video>` друг на друге: видна `.is-on`, вторая — следующая часть,
   заранее загруженная (подмена без чёрного кадра). */
.ftl__v:not(.is-on), .ftl-layer__v:not(.is-on), .chx__anim.is-parts video:not(.is-on) { opacity: 0; }
/* п. 1: отрезная полоса — отрезок на часть (∝ длительности), зазор 4;
   пройденные — --active, текущий заполняется, будущие — paper 14 %. */
.fpt-bar { display: flex; gap: 4px; }
.fpt-bar > i { display: block; position: relative; flex: 1 1 0; min-width: 8px; height: 100%; background: rgba(var(--paper-rgb), 0.14); }
.fpt-bar > i > b { display: block; width: 0; height: 100%; background: var(--active); }
/* п. 2: штамп части — слева сверху на кадре, отступ 24 */
.fpt-stamp {
  position: absolute; left: 24px; top: 24px; z-index: 2; pointer-events: none;
  padding: 8px 20px; border-radius: 8px; background: rgba(var(--amber-black-rgb), 0.78);
  font-family: var(--font-caption); font-size: 28px; line-height: 1.2; color: var(--text-on-dark); white-space: nowrap;
}
.fpt-stamp[hidden] { display: none; }
.ftl.is-poster .ftl__stamp { display: none; }
/* п. 3: пилюли частей — 64 (--touch-min), поля 0/28, зазор 16, 20 Kopeek 32 */
.fpt-pills { display: flex; flex-wrap: wrap; gap: 16px; }
.fpt-pill {
  flex: 0 0 auto; height: var(--touch-min, 64px); padding: 0 28px; border-radius: 32px; box-sizing: border-box;
  border: 2px solid rgba(var(--brass-rgb), 0.35); background: transparent; color: var(--text-on-dark);
  font-family: var(--font-caption); font-size: 32px; white-space: nowrap;
}
.fpt-pill.is-past { border-color: var(--active); color: var(--brass); }
.fpt-pill.is-on { border-color: var(--active); background: var(--active); color: var(--text-on-light); }
.fpt-pill:active { filter: brightness(1.12); }
/* п. 9: справка — пилюли под полосой, отступ 16, не больше двух рядов */
.ftl-pills { margin-top: 16px; max-height: calc(2 * var(--touch-min, 64px) + 16px); overflow: hidden; }
/* п. 8: слой — пилюли в нижней строке после «↻ С начала», зазор 40, одним рядом;
   строка-мета — под пилюлями (верх 1795) */
.ftl-layer.is-parts .ftl-layer__foot { width: 3712px; flex-wrap: wrap; column-gap: 40px; row-gap: 16px; }
.ftl-layer__pills { flex-wrap: nowrap; }
.ftl-layer.is-parts .ftl-layer__meta { flex-basis: 100%; }
/* п. 10: Хроника — по низу карты слева, отступ 24: полоса (6 px, ширина карты
   минус 48) над плашкой пилюль .78 с полями 16; не влезло — прокрутка ряда */
.chx__parts { position: absolute; left: 24px; right: 24px; bottom: 24px; z-index: 4; display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }
.chx__pbar { width: 100%; height: 6px; }
.chx__pillrow { max-width: 100%; box-sizing: border-box; overflow-x: auto; padding: 16px; border-radius: 12px; background: rgba(var(--amber-black-rgb), 0.78); }
.chx__pills { flex-wrap: nowrap; }

/* ═══ СХЕМЫ СИМБИРСКА НА АТЛАСЕ (гл. 4/8) — спека design 0c5053b ═══
   Окно главы целиком отдано атласу (тот же MapLibre, что Хроника); поля
   кадра — в `SCHEME_PAD` сцены. Слова — DOM `.sch` поверх карты
   (`scheme-view.js`): глифов 21 Cent у движка нет, плашек символ не умеет. */
.lr-ch__map.is-atlas { padding: 0; display: block; background: none; }
.lr-sch__box { position: absolute; inset: 0; overflow: hidden; }
.sch { position: absolute; inset: 0; z-index: 2; overflow: hidden; pointer-events: none; }
.sch__l { position: absolute; left: 0; top: 0; white-space: nowrap; line-height: 1.2; }
.sch__l[hidden] { display: none; }
/* п. 2: дата — плашка, 20 Kopeek 30 */
.sch__date {
  padding: 4px 12px; border-radius: 6px;
  font-family: var(--font-caption); font-size: 30px;
  color: var(--text-on-dark); background: rgba(var(--amber-black-rgb), 0.78);
  z-index: 2;   /* даты видны ВСЕ (п. 2) — над подписями сёл и моста */
}
/* п. 5: сёла — 21 Cent, гало 3 px amber-black .8 */
.sch__place, .sch__front, .sch__bridge, .sch__river {
  -webkit-text-stroke: 6px rgba(var(--amber-black-rgb), 0.8); paint-order: stroke fill;
}
.sch__place { font-family: var(--font-text); font-size: 32px; color: var(--text-on-dark); }
.sch__place.is-focus { font-size: 40px; font-weight: 700; }
/* п. 6: «Волга» 36 курсивом, разрядка .2em; Свияга 28 */
.sch__river { font-family: var(--font-text); font-style: italic; font-size: 28px; letter-spacing: 0.2em; }
.sch__river.is-big { font-size: 36px; }
/* п. 4: «фронт 24 авг.» 20 Kopeek 28 */
.sch__front { font-family: var(--font-caption); font-size: 28px; color: var(--text-on-dark); }
/* п. 6: мост — «)(» 32 + «мост» 26; подорван — перечёркнут */
.sch__bridge { font-family: var(--font-caption); font-size: 26px; color: var(--text-on-dark); }
.sch__bicon { font-size: 32px; letter-spacing: -0.1em; }
.sch__bridge.is-blown .sch__bicon { text-decoration: line-through; text-decoration-thickness: 3px; }
/* п. 3: перекрестье боя поверх кружка 28 — две линии 2 px */
.sch__cross { width: 28px; height: 28px; }
.sch__cross::before, .sch__cross::after {
  content: ""; position: absolute; left: 3px; right: 3px; top: 13px; height: 2px;
  background: var(--text-on-dark); transform: rotate(45deg);
}
.sch__cross::after { transform: rotate(-45deg); }
/* п. 14: OSM — справа снизу, отступ 12, плашка .6, 20 Kopeek 24, сверху всего */
.sch__osm {
  position: absolute; right: 12px; bottom: 12px; z-index: 3;
  padding: 4px 12px; border-radius: 6px;
  font-family: var(--font-caption); font-size: 24px; line-height: 1.2;
  color: var(--text-muted); background: rgba(var(--amber-black-rgb), 0.6);
}
/* п. 13: легенда под окном — две строки 20 Kopeek 26 */
.sch-leg { display: flex; flex-direction: column; gap: 6px; font-family: var(--font-caption); font-size: 26px; color: var(--text-muted); }
.sch-leg__title { color: var(--text-on-dark); }
.sch-leg__row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.sch-leg__sq { display: inline-block; width: 22px; height: 22px; background: var(--camp-red); }
.sch-leg__sq[data-camp="white"] { background: var(--camp-white); }
.sch-leg__ln { display: inline-block; width: 40px; height: 0; border-top: 6px solid var(--text-on-dark); }
.sch-leg__ln.is-dash { border-top-style: dashed; }
