/* ============================================================
   Fansfics — интерактивная книга. Премиум-редизайн (2026-07).
   Язык дизайна: тёмная «библиотека» по умолчанию + светлая тема,
   золото #C7A360, Literata (проза) + Golos Text (интерфейс).
   Токены 1-в-1 из design_import/Fansfics Premium.dc.html.
   Mobile-first: база — телефон, min-width-медиа — планшет/десктоп.
   ============================================================ */

/* ------------------------------------------------------------ токены */
:root {
  --bg: #141210;
  --surface: #1C1916;
  --raised: #26221D;
  --ink: #EEE7D9;
  --ink2: #B0A38D;
  /* --ink3 — «третий» приглушённый текст: подписи вкладок, метаданные карточек,
     подсказки полей, номер шага мастера. Было #7D7261 — 3.96:1 к --bg, 3.71 к
     --surface и 3.35 к --raised, то есть НИ ОДНОГО прохода AA даже в тёмной
     теме (замер axe, эпик 2026-08-28 P1-7; расчёт — scratchpad/contrast.py).
     Стало #9A8E77 — 5.79 / 5.42 / 4.90. Лестница приглушения сохранена:
     L* 91.8 (--ink) → 67.5 (--ink2) → 59.5 (--ink3). */
  --ink3: #9A8E77;
  --line: rgba(238,231,217,.09);
  --line2: rgba(238,231,217,.17);
  --gold: #C7A360;
  --gold-text: #D9B878;
  --gold-soft: rgba(199,163,96,.13);
  --on-gold: #191510;
  --danger: #C4644C;
  --good: #94B383;
  --cover-ink: #EFE6D2;
  --shadow: 0 24px 60px rgba(0,0,0,.5), 0 4px 14px rgba(0,0,0,.35);
  --shadow-soft: 0 10px 30px rgba(0,0,0,.35);
  /* Тени НАПРАВЛЕННЫЕ (макет Reader Chapter Interactive, 2026-08-21): у панели,
     прижатой к краю экрана, обычная тень падает за этот край и не отделяет её
     от страницы. Нижний лист светит вверх, боковая панель — влево. */
  --shadow-up: 0 -24px 60px rgba(0,0,0,.5);
  --shadow-left: -24px 0 60px rgba(0,0,0,.5);
  --scrim: rgba(10,8,6,.6);
  /* Конструктор книги, значения макета «Конструктор книги - редизайн»
     (фикс-раунд 2026-10-03, PLAN fidelity §3.3). ОДИНАКОВЫ В ОБЕИХ ТЕМАХ —
     светлая их не переопределяет, как и макет: книга без мира — грифельный
     переплёт и на кремовой панели (= library.js::WORLD_OWN_TONE), кольцо
     медальона героя на обложке, тень dock и нижней кромки экрана раздела
     (телефон), тень ручки тумблера «Как читать». builder.css сырых цветов не
     пишет — только эти имена. */
  --cover-empty: #383B42;
  --cover-ring: rgba(0,0,0,.35);
  --dock-shadow: 0 -10px 30px rgba(0,0,0,.22);
  --knob-shadow: 0 1px 3px rgba(0,0,0,.3);

  /* Сцена и миниатюра конструктора облика (2026-08-09). Градиенты, а не цвета:
     «сцена» — подсветка под персонажем в модалке, «миниатюра» — фон плиток
     опций. Живут в общих токенах, потому что конструктору нужна ТЕМА хоста
     (у forge.css своя тёмная палитра, и она обязана уметь светлеть). */
  --stage: radial-gradient(circle at 50% 36%, #2C2319 0%, #1B1714 55%, #141210 100%);
  --thumb: radial-gradient(circle at 50% 42%, #3A3128, #211C16 75%);

  /* семантические state-цвета (чипы/бейджи/баннеры модерации, теги, поиск) —
     пары для тёмной (здесь) и светлой (ниже) темы */
  --state-danger-bg: rgba(196,100,76,.16);
  --state-danger-ink: #E29B85;
  --state-good-bg: rgba(148,179,131,.15);
  --state-good-ink: #A9C79A;
  --state-warn-bg: rgba(199,163,96,.15);
  --state-warn-ink: #D9B878;
  --state-info-bg: rgba(199,163,96,.09);
  --state-info-ink: #B0A38D;
  --state-neutral-bg: rgba(238,231,217,.08);
  --state-neutral-ink: #B0A38D;

  /* ШКАЛА ОТНОШЕНИЙ — пять ступеней от вражды до преданности. Раньше пять
     хексов были вписаны прямо в правила `.rel-*` (:4700 и ниже) и потому были
     СЛЕПЫ К ТЕМЕ: одна и та же краска светила и на #141210, и на #F3EEE2.
     В светлой теме это давало 2.67-3.82:1 при кегле 13px — тот же корень, что
     у --ink3, только спрятанный от токенов (эпик 2026-08-28, P1-7).
     Теперь ступень — токен с парой значений. Тон (hue) и насыщенность у пары
     общие, разводится только светлота: «вражда» обязана остаться красной, а
     «доверие» зелёным, иначе шкала перестаёт читаться без подписи.
     Тёмные значения: правлены только две ступени, которые не добирали AA к
     --raised (вражда 3.97, преданность 4.13) — остальные три как были. */
  --rel-enmity: #CD6F68;
  --rel-dislike: #C07A45;
  --rel-warmth: #B98A3C;
  --rel-trust: #7D9C62;
  --rel-devotion: #CA6C8F;

  --font-ui: "Golos Text", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-serif: Literata, Georgia, "Times New Roman", "PT Serif", serif;
  --tap: 44px;             /* минимальный тач-таргет */

  /* алиасы старой палитры: их читают web/legal/*.html и возможные стилевые
     хвосты — маппим на новые токены, чтобы ничего не «выцвело» */
  --paper: var(--bg);
  --sheet: var(--surface);
  --sheet-deep: var(--raised);
  --ink-soft: var(--ink2);
  --line-soft: var(--line);
  --accent: var(--gold);
  --accent-ink: var(--gold-text);
  --bad: var(--danger);
  --mid: var(--gold);
  --shadow-page: var(--shadow);
}
:root[data-theme="light"] {
  --bg: #F3EEE2;
  --surface: #FAF6EB;
  --raised: #FFFDF6;
  --ink: #241D12;
  /* СВЕТЛАЯ ТЕМА, КОНТРАСТ (эпик 2026-08-28, P1-7). Аудит: «неактивная
     навигация — примерно #A0937B на #F3EEE2, 2.61:1 при 14px; так же бледны
     подписи сортировки, метаданные карточек и вспомогательные тексты».
     Замерено axe на семи экранах × две темы, посчитано scratchpad/contrast.py:
       --ink3 #A0937B  2.61 / 2.80 / 2.97 (bg / surface / raised) — провал;
       --ink2 #6E6350  5.09 / 5.46 / 5.79 — проходит.
     Чинить только --ink3 нельзя: чтобы добрать 4.5:1 к --bg, он обязан стать
     не светлее L*≈45, а --ink2 стоит на L*42.5 — две ступени слились бы в одну
     и «приглушённый» перестал бы отличаться от «вторичного». Поэтому вся
     лестница светлой темы сдвинута вниз одним движением:
       L* 11.3 (--ink) → 35.4 (--ink2) → 44.3 (--ink3),
     то есть --ink2 6.63 / 7.11 / 7.54 и --ink3 4.76 / 5.10 / 5.41.
     Тёмная тема считается отдельно (см. :root выше) — зеркалить её значения
     сюда нельзя, у неё другой знак разницы яркостей. */
  --ink2: #5C5240;
  --ink3: #726851;
  --line: rgba(36,29,18,.11);
  --line2: rgba(36,29,18,.22);
  /* --gold в светлой теме — не только рамка и фон CTA: им НАБРАН ТЕКСТ —
     звёзды рейтинга карточки (.cat-star, .stars .st.on, .rate-widget), «＋» и
     «▸» списков, точка бренда, счётчик серий. Прежний #8F6D2E давал 4.13 / 4.42
     / 4.69 (bg / surface / raised) — провал AA на двух поверхностях из трёх при
     кегле 12px. Гейт a11y этого НЕ ловил: под звёздами герой-карточки лежит
     градиент, axe уходит в `incomplete` и молчит (см. tests/e2e/a11y.spec.ts,
     блок про incomplete). Стало #876527 — 4.63 / 4.94 / 5.24, а --on-gold на
     нём поднялся с 4.62 до 5.18. Тёмная тема (#C7A360) не тронута: там золото
     светлее фона и проходит с запасом (7.86 / 7.36 / 6.65).
     Разница с --gold-text (#7A5A20, L*40.5) сохранена: L*48.2 → 45.1, две
     ступени по-прежнему различимы. --gold-soft пересчитан на новый RGB, иначе
     полутон подложки перестал бы быть «тем же золотом на 11%». */
  --gold: #876527;
  --gold-text: #7A5A20;
  --gold-soft: rgba(135,101,39,.11);
  --on-gold: #FFFBF0;
  --danger: #A34832;
  /* --good — цвет пилюль «завершена» / «в каталоге» / бонусов, а они набраны
     11-12px: правило крупного текста (≥24px, либо ≥18.7px полужирного) на них
     не распространяется. #5E7F50 давал 3.92 / 4.20 / 4.46 — провал на всех трёх
     поверхностях светлой темы. #527046 даёт 4.82 / 5.17 / 5.48. Тёмная
     (#94B383, 8.04 / 7.53 / 6.80) не тронута. */
  --good: #527046;
  --cover-ink: #EFE6D2;
  --shadow: 0 22px 54px rgba(64,48,25,.16), 0 3px 10px rgba(64,48,25,.08);
  --shadow-soft: 0 10px 26px rgba(64,48,25,.10);
  --shadow-up: 0 -22px 54px rgba(64,48,25,.16);
  --shadow-left: -22px 0 54px rgba(64,48,25,.16);
  --scrim: rgba(38,29,16,.45);
  --stage: radial-gradient(circle at 50% 36%, #FFFDF6 0%, #F7F0DF 55%, #EFE6D2 100%);
  --thumb: radial-gradient(circle at 50% 42%, #EFE7D3, #DFD3B8 78%);

  --state-danger-bg: #fbe0dc;
  --state-danger-ink: #9a2b1f;
  --state-good-bg: #e2f2e4;
  --state-good-ink: #2a6b39;
  --state-warn-bg: #fdf1d6;
  /* 4.37:1 к своему фону #fdf1d6 — единственная пара state-*, не добиравшая AA
     в светлой теме (остальные 4.94-7.94). #836716 даёт 4.78. */
  --state-warn-ink: #836716;
  --state-info-bg: #e7f0fb;
  --state-info-ink: #2c4a6b;
  --state-neutral-bg: #eceae6;
  --state-neutral-ink: #6a6357;

  /* Светлая половина шкалы отношений (разбор — в :root выше). Тон и
     насыщенность взяты у тёмных значений один в один, светлота опущена до
     первого прохода 4.5:1 к --bg — самой тёмной из трёх поверхностей, то есть
     худшему случаю: 4.58 / 4.55 / 4.60 / 4.58 / 4.55 к --bg и 4.88-4.93 к
     --surface, на котором эти пилюли и стоят (.rel-card). */
  --rel-enmity: #B8453D;
  --rel-dislike: #975E33;
  --rel-warmth: #88662C;
  --rel-trust: #5B7248;
  --rel-devotion: #B5426C;
}

/* ------------------------------------------------------------ база */
* { box-sizing: border-box; }
/* Скроллбар зарезервирован ВСЕГДА двумя способами (у scrollbar-gutter краевые
   случаи по браузерам): overflow-y:scroll держит трек всегда на месте, поэтому
   короткий вид (каталог/пустая библиотека) не убирает скроллбар и вьюпорт НЕ
   прыгает по горизонтали на ~17px при возврате на вкладку (десктоп Windows). */
/* overflow-x: clip (не hidden!) — пара к full-bleed секциям (.cat-hero):
   их margin-inline 50% − 50vw шире клиентской части окна на ~полскроллбара,
   и без clip у корня появился бы горизонтальный скролл на ровном месте.
   Именно clip: hidden сделал бы html скролл-контейнером и позволил бы
   программный горизонтальный сдвиг (focus/scrollIntoView), clip — нет.
   Внутренние ленты с overflow-x:auto скроллятся как раньше — правило
   касается только корня. */
html { scroll-behavior: smooth; scrollbar-gutter: stable; overflow-y: scroll; overflow-x: clip; }
/* Меньше движения — и прокрутка без анимации (ревью конструктора книги,
   2026-10-02): доводка фокуса Tab'ом и переходы к разделам анимировались
   полсекунды и при reduced-motion, а замеры «на виду ли фокус» мерили
   недокрученную страницу. */
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 15.5px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  transition: background .35s ease, color .35s ease;
}
::selection { background: rgba(199,163,96,.35); }

/* тонкие полосы прокрутки под тему */
* { scrollbar-width: thin; scrollbar-color: color-mix(in oklab, var(--ink3) 60%, transparent) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; background: transparent; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: color-mix(in oklab, var(--ink3) 55%, transparent); border-radius: 999px; border: 3px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: color-mix(in oklab, var(--ink3) 85%, transparent); border: 3px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
::-webkit-scrollbar-corner { background: transparent; }

.hidden { display: none !important; }
.muted { color: var(--ink2); }
a { color: var(--gold-text); }
button:focus-visible, input:focus-visible + .track, [tabindex]:focus-visible {
  outline: 2px solid var(--gold); outline-offset: 2px;
}
/* У input[type="range"] нет ни соседа .track, ни tabindex — базовое правило
   выше не ловит его НИ ОДНОЙ веткой, а .tone-block гасит системное кольцо
   своим appearance:none. Без этой строки фокус на «Мрачности», «Накале» и
   «Юморе» невидим совсем (P1-11, заявка WIZARD-EDITOR §1). offset 3px, а не 2:
   дорожка ползунка — линия в 3px внутри блока высотой 30-36px, и на 2px кольцо
   ложится вплотную к бегунку. */
input[type="range"]:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* ------------------------------------------------------------ шапка */
.topbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0 16px;
  min-height: 60px;
  /* TL-021: SAFE-AREA СВЕРХУ. `apple-mobile-web-app-status-bar-style:
     black-translucent` (index.html) кладёт вебвью ПОД статус-бар, а
     `viewport-fit=cover` — под чёлку; без компенсации сверху шапка уезжает под
     обе. По файлу `env(safe-area-inset-*)` встречается 28 раз — и все по
     нижнему краю (нижняя навигация, пилюли, .pad) либо в полноэкранной карте
     (`padding-top: max(8px, env(safe-area-inset-top))` — готовый образец
     идиомы). Верхний край не считал никто.
     Мобильные переопределения трогают только `padding-inline`, то есть верх
     переживают; `--tb-h` считается по `offsetHeight` (util.js), поэтому все
     sticky-отступы под шапкой подхватят прибавку сами. */
  padding: env(safe-area-inset-top, 0px) clamp(14px, 4vw, 40px) 0;
  background: var(--bg);
  background: color-mix(in oklab, var(--bg) 82%, transparent);
  /* Макет v3: блюр 16px и нижняя линия с золотым оттенком — шапка перестаёт
     быть «серой полосой» над золотой полкой каталога. */
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  border-bottom: 1px solid color-mix(in oklab, var(--gold) 16%, transparent);
  transition: transform .25s ease, background .35s ease;
}
.brand {
  display: flex; align-items: center; gap: 9px;
  padding: 12px 0;
  font-family: var(--font-serif);
  font-size: 21px; font-weight: 600; letter-spacing: .01em;
  color: var(--ink); white-space: nowrap;
}
.brand-dot { color: var(--gold); font-style: italic; }

/* Знак Turnleaves: страница с загнутым золотым уголком (конструкция 1-в-1 из
   design_import/naming_logo.dc.html §3a/3d). Размеры/толщины — через локальные
   переменные, цвета — через токены темы (контур = --ink, загиб = --gold). */
.tl-page {
  --tl-w: 14px; --tl-h: 19px; --tl-bw: 1.5px; --tl-r: 2px; --tl-fold: 7px;
  position: relative; display: inline-block; flex: none;
  width: var(--tl-w); height: var(--tl-h);
  border: var(--tl-bw) solid var(--ink);
  border-radius: var(--tl-r);
}
.tl-fold {
  position: absolute;
  top: calc(var(--tl-bw) * -1); right: calc(var(--tl-bw) * -1);
  width: var(--tl-fold); height: var(--tl-fold);
  background: var(--gold);
  border-radius: 0 var(--tl-r) 0 100%;
  /* живой уголок: hover родителя подращивает загиб (см. .auth-brand ниже) */
  transition: width .35s cubic-bezier(.2,.8,.2,1), height .35s cubic-bezier(.2,.8,.2,1);
}

/* Глиф-лист (§3b): страница в момент переворота — тот же контур, что у уголка.
   Живёт в статусах «пишется…», пустых состояниях и оверлее генерации. */
.leaf {
  display: inline-block; flex: none;
  width: 13px; height: 13px;
  background: var(--gold);
  border-radius: 0 100% 0 100%;
  transform: rotate(-45deg);
}
.leaf-pulse { animation: leaf-pulse 1.6s ease-in-out infinite; }
@keyframes leaf-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
.topbar nav { order: 5; width: 100%; display: flex; gap: 4px; border-top: 1px solid var(--line); }
.tab {
  flex: 1; min-height: var(--tap);
  font-family: var(--font-ui); font-size: 14px; font-weight: 500;
  background: none; border: none; cursor: pointer;
  color: var(--ink3);
  padding: 13px 8px; white-space: nowrap; text-align: center;
  transition: color .15s, box-shadow .15s;
}
.tab:active { color: var(--ink); }
.tab.active { color: var(--ink); box-shadow: inset 0 -2px 0 var(--gold); }
/* «Новая история» — золотая CTA в шапке, как в макете.
   Вертикального margin у неё НЕТ намеренно: в правом блоке шапки он
   складывался с его padding 11px и растил шапку до 70px при 62px макета
   (кнопка 36px + 11 + 11 = 58, дальше min-height 60 и аватар 40px). В РЯДУ
   ВКЛАДОК отступ, наоборот, нужен — его ставят отдельные правила
   .topbar nav .tab[data-view="new"] ниже, каждое в своём диапазоне. */
.tab[data-view="new"] {
  flex: none; align-self: center;
  background: var(--gold); color: var(--on-gold);
  border-radius: 10px; padding: 10px 16px;
  font-size: 13.5px; font-weight: 600;
  transition: filter .15s, transform .15s, box-shadow .15s;
}
.tab[data-view="new"]:active { transform: translateY(1px); }
.tab[data-view="new"].active { color: var(--on-gold); box-shadow: 0 0 0 2px var(--gold-soft), 0 0 0 1px var(--gold); }
.topbar-right { display: flex; align-items: center; gap: 10px; margin-left: auto; padding: 11px 0; }
/* Очередь правого блока по макету v3: искры · «Новая история» · тема ·
   колокольчик · профиль. Держим её flex-порядком, а не порядком в DOM: app.js
   (relocateNewStoryCta) на десктопе всегда вставляет CTA ПЕРВЫМ ребёнком
   .topbar-right, и спорить с ним разметкой бессмысленно — после первого же
   resize кнопка снова уедет в начало ряда. #notif-panel и #health в очереди не
   участвуют: первый позиционируется absolute, второй скрыт. */
#header-chips { order: 1; display: inline-flex; align-items: center; }
/* Пустой слот всё равно был бы флекс-элементом и получил бы свои 10px gap —
   у гостя это лишняя дыра между брендом и темой. */
#header-chips:empty { display: none; }
.topbar-right .tab[data-view="new"] { order: 2; }
.topbar-right .theme-toggle { order: 3; }
.topbar-right .notif-bell { order: 4; }
.topbar-right .userbox { order: 5; }
.theme-toggle {
  flex: none; width: 38px; height: 38px; border-radius: 999px;
  border: 1px solid var(--line2); background: transparent;
  color: var(--ink2); font-size: 15px; line-height: 1; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color .15s, border-color .15s;
}
.theme-toggle:active { color: var(--gold-text); border-color: var(--gold); }
/* Глифы темы (макет v3) — контурные луна и солнце, ОБА лежат в разметке, а
   видимый выбирается по html[data-theme]. Раньше глиф писал util.js в
   textContent; с SVG это значило бы держать иконку JS-строкой и терять её при
   каждой чужой перерисовке кнопки. */
.theme-toggle .ico { display: none; flex: none; }
.theme-toggle .ico-moon { display: block; }
:root[data-theme="light"] .theme-toggle .ico-moon { display: none; }
:root[data-theme="light"] .theme-toggle .ico-sun { display: block; }
/* #health остаётся в DOM (в него пишет loadHealth из auth.js — JS-контракт),
   но в шапке по дизайну не показывается вовсе. */
#health { display: none; }
.userbox { display: flex; align-items: center; }
.header-login { min-height: 38px; padding: 8px 13px; white-space: nowrap; }
.userchip {
  display: inline-flex; align-items: center;
  background: none; border: none; padding: 2px; cursor: pointer;
  font-family: inherit; color: var(--ink);
}
/* 2026-08-09 (редизайн): в шапке аватар 40px по макету. Точечно, только внутри
   .userchip: тот же .avatar-sm живёт в комментариях/отзывах, там 32px и менять
   его нельзя. Кегль инициала перебиваем отдельным правилом — .avatar-sm.avatar-ph
   ниже по файлу той же силы и иначе выиграет.
   2026-08-13 (v3): кольцо золота 1.5px вместо 1px. */
.userchip .avatar {
  width: 40px; height: 40px;
  box-shadow: 0 0 0 1.5px var(--gold); transition: box-shadow .15s;
}
.userchip .avatar.avatar-ph { font-size: 16px; }
/* Обёртка ровно по кружку аватара — якорь для бейджа тарифа. Нужна именно
   отдельным узлом: сам .avatar приезжает из avatarHtml() одним готовым
   спаном без места для детей. */
.userchip-ava { position: relative; display: inline-flex; flex: none; }
/* Бейдж тарифа (макет v3): P — Premium, U — Premium+. У бесплатного тарифа
   бейджа НЕТ вовсе (billing.js::renderTierDot), поэтому «пустого» состояния у
   этого правила не бывает. Тёмный конец градиента — через color-mix, чтобы
   светлая тема не получила сырой хекс тёмной. */
.tier-dot {
  position: absolute; right: -3px; bottom: -3px;
  width: 16px; height: 16px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, var(--gold), color-mix(in oklab, var(--gold) 78%, #000));
  color: var(--on-gold);
  font: 800 9px/1 var(--font-ui);
  box-shadow: 0 0 0 2px var(--bg);
  pointer-events: none;
}

/* --------------------------------------------------- меню профиля (макет v3)
   Выпадающая карточка под аватаром: имя и тариф, месячная квота глав, искры,
   апгрейд и две ссылки. Позиционируется absolute от .topbar-right — тот же
   якорь, что у панели уведомлений (правило .topbar-right{position:relative}
   живёт в блоке уведомлений внизу файла; здесь на него только опираемся).
   Всю разметку рисует auth.js В МОМЕНТ ОТКРЫТИЯ — без этих правил меню приедет
   столбиком нестилизованных кнопок поверх страницы. */
.profile-menu {
  position: absolute; z-index: 60;
  top: calc(100% + 12px); right: 0;
  /* Ширина зажата вьюпортом, как у .notif-panel: на 320px экране карточка
     фиксированных 288px вылезла бы за правый край вместе с тенью. */
  width: min(288px, calc(100vw - 24px));
  display: flex; flex-direction: column; gap: 14px;
  padding: 16px;
  background: var(--raised);
  border: 1px solid color-mix(in oklab, var(--line2) 82%, transparent);
  border-radius: 16px;
  box-shadow: var(--shadow);
  cursor: default; text-align: left;
}
.pm-id { display: flex; align-items: center; gap: 11px; }
.pm-id .avatar { width: 38px; height: 38px; flex: none; }
.pm-id .avatar.avatar-ph { font-size: 15px; }
.pm-id-txt { display: flex; flex-direction: column; min-width: 0; }
.pm-id-txt b { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pm-tier { font: 700 11.5px var(--font-ui); letter-spacing: .04em; color: var(--gold-text); }
.pm-quota {
  display: flex; flex-direction: column; gap: 7px;
  padding: 12px 13px;
  background: color-mix(in oklab, var(--ink) 4%, transparent);
  border: 1px solid color-mix(in oklab, var(--line2) 59%, transparent);
  border-radius: 12px;
}
.pm-quota-top {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: 12px; color: var(--ink2);
}
.pm-quota-top b { color: var(--ink); font-variant-numeric: tabular-nums; }
.pm-bar {
  display: block; height: 4px; border-radius: 3px; overflow: hidden;
  background: color-mix(in oklab, var(--ink) 12%, transparent);
}
.pm-bar > span { display: block; height: 100%; border-radius: 3px; background: var(--gold); }
.pm-left { font-size: 11px; color: var(--ink3); }
/* СЛУЖЕБНЫЙ ДОСТУП (QA3, пункт 8) — карточка без чисел и без полосы.
   Форму карточки она сохраняет намеренно: место квоты в меню остаётся тем же,
   и человек не ищет пропавший блок. Подпись читается золотом — тем же цветом,
   что и бейдж тарифа выше (.pm-tier), потому что это утверждение о доступе, а
   не служебная сноска. Полосы здесь нет: заполнять нечего, а полная полоса
   читалась бы как ещё одно число («пул полон»). */
.pm-quota-nolimit .pm-quota-top { color: var(--gold-text); font-weight: 600; }
.pm-sparks { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pm-sparks-n {
  display: inline-flex; align-items: center; gap: 6px;
  font: 600 13px var(--font-ui); color: var(--gold-text);
  font-variant-numeric: tabular-nums;
}
.pm-topup {
  flex: none; min-height: 32px; padding: 0 13px;
  font: 600 12px var(--font-ui); color: var(--gold-text);
  background: none; border: 1px solid color-mix(in oklab, var(--gold) 50%, transparent);
  border-radius: 999px; cursor: pointer; white-space: nowrap;
  transition: background .15s;
}
.pm-upgrade {
  min-height: 42px; font: 700 13.5px var(--font-ui);
  background: linear-gradient(135deg, var(--gold), color-mix(in oklab, var(--gold) 78%, #000));
  color: var(--on-gold); border: none; border-radius: 11px; cursor: pointer;
  box-shadow: 0 8px 20px color-mix(in oklab, var(--gold) 25%, transparent);
  transition: filter .15s;
}
.pm-links { display: flex; flex-direction: column; border-top: 1px solid var(--line); padding-top: 6px; }
.pm-link {
  text-align: left; background: none; border: none; padding: 9px 2px;
  font: 400 13.5px var(--font-ui); color: var(--ink2); cursor: pointer;
  transition: color .15s;
}
.pm-logout { color: var(--ink3); }

/* ≥720: ГОСТЕВАЯ шапка легковесна (бренд · Каталог · тема · «Войти») — ряд
   вкладок помещается рядом с брендом уже здесь. */
@media (min-width: 720px) {
  body.guest-mode .topbar nav { order: 0; width: auto; flex: 1 1 220px; min-width: 0; border-top: none; }
  body.guest-mode .tab { flex: none; min-height: 0; padding: 20px 14px; }
}
/* ≥1150: АВТОРИЗОВАННАЯ шапка наконец влезает в одну строку. Порог был 1000
   (контент ≈910px на 2026-07-26), но колокольчик (ret1) и «Модерация» рядом с
   CTA v2 подняли полный ряд владельца до ≈1200px — на 1000–1150px вкладки
   НАЕЗЖАЛИ на правый блок (скрин владельца 2026-08-13): nav с min-width:0
   сжимался ниже контента, а nowrap-текст кнопок рисовался поверх соседей.
   Поэтому: порог 1150; у nav больше НЕТ min-width:0 — не сжимается ниже
   контента, и при нехватке места правый блок честно переносится строкой ниже,
   а не перекрывается.
   2026-08-13 (макет v3): ряд стал КОРОЧЕ на всю пилюлю квоты (~110px) — она
   уехала карточкой в меню профиля, а тариф виден бейджем на аватаре. Полный
   ряд владельца с «Модерацией» просит теперь ≈1090px, то есть влезает сразу за
   порогом, и полоса 1150–1250 с ужиманием пилюли до цифр стала мёртвой:
   правила удалены вместе с самой пилюлей. САМ ПОРОГ 1150 НЕ ТРОГАЕМ — он
   продублирован в app.js::relocateNewStoryCta, и разъехавшись, они положат CTA
   в ряд, которого нет.
   В 720–1149.9 вкладки остаются отдельным рядом (это и есть .om-topnav
   макетов v2 — мобильная база .topbar nav выше), CTA «Новая история» живёт в
   этом ряду (app.js relocateNewStoryCta — его диапазон обязан совпадать с
   этим). НИЖЕ 720px ряда нет вовсе: вкладки уехали в нижнюю навигацию
   (.lownav в конце файла). */
@media (min-width: 1150px) {
  .topbar nav { order: 0; width: auto; flex: 1 1 220px; border-top: none; }
  .tab { flex: none; min-height: 0; padding: 20px 14px; }
  .tab[data-view="new"] { padding: 10px 16px; }
}
@media (min-width: 720px) and (max-width: 1149.9px) {
  body:not(.guest-mode) .topbar nav .tab[data-view="new"] {
    flex: none; align-self: center; margin: 6px 0 6px auto;
    padding: 8px 13px; font-size: 12.5px;
  }
}
/* мобильный ридер: скролл вниз прячет шапку (game-bar остаётся) */
body.chrome-hidden .topbar { transform: translateY(-105%); }
body.chrome-hidden .game-bar { top: calc(env(safe-area-inset-top, 0px) + 6px); }
/* Нижняя навигация уезжает ВМЕСТЕ с шапкой и по той же причине: в читалке
   человек читает, а не переключает разделы, и бар из пяти пунктов съедает
   64px нижней трети экрана. Возврат — тем же движением вверх (app.js снимает
   chrome-hidden при скролле вверх), так что навигация всегда в одном жесте. */
body.chrome-hidden .lownav { transform: translateY(105%); }

/* ------------------------------------------------------------ каркас */
main {
  max-width: 1120px; margin: 0 auto;
  padding: clamp(18px, 4vw, 44px) clamp(12px, 4vw, 40px) calc(56px + env(safe-area-inset-bottom));
  /* fill-mode строго backwards: у «both/forwards» финальный кадр держит на main
     identity-transform НАВСЕГДА, main становится containing block'ом и все
     position:fixed внутри (tts-бар, .wiz-nav) прибивает к низу документа. */
  animation: ff-fade-up .45s ease backwards;
}
@keyframes ff-fade-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.view { display: none; }
.view.active { display: block; }

/* ХОЛОДНЫЙ СТАРТ (P1-9 эпика 2026-08-28: «новая вкладка сначала показала
   пустой каталог, а после асинхронной инициализации переключилась на последнюю
   открытую книгу»).

   Стартовый вид задан статикой разметки (#view-catalog.view.active), а
   настоящий выбирается в app.js::init ПОСЛЕ двух сетевых round-trip'ов. Класс
   ставит state.js::ffBootGate — но только если сохранённый вид не каталог, то
   есть человеку, впервые пришедшему на сайт, ничего не гасится вовсе.

   visibility, а НЕ display:none: display сбросил бы высоту документа, и в
   момент снятия завесы страница дала бы прыжок скролла — ровно тот дефект,
   от которого мы уходим. Вид продолжает занимать место, просто не рисуется. */
body.booting .view.active { visibility: hidden; }
body.booting::after {
  content: ""; position: fixed; z-index: 5; pointer-events: none;
  left: 50%; top: 50%; width: 26px; height: 26px; margin: -13px 0 0 -13px;
  border-radius: 999px;
  border: 2px solid color-mix(in oklab, var(--gold) 26%, transparent);
  border-top-color: var(--gold);
  /* keyframes общие на документ — берём уже живущие в файле (tts-bar). */
  animation: tts-spin .9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  body.booting::after { animation: none; }
}
.view-title {
  margin: 4px 0 22px;
  font-family: var(--font-serif);
  font-size: clamp(28px, 4.5vw, 40px); font-weight: 600;
  letter-spacing: -.01em; line-height: 1.12;
}

/* лист (карточка-поверхность) */
.sheet {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: var(--shadow-soft);
}

/* ------------------------------------------------------------ кнопки */
.primary {
  font-family: var(--font-ui); font-size: 15px; font-weight: 600;
  background: var(--gold); color: var(--on-gold);
  border: none; border-radius: 10px;
  padding: 12px 24px; min-height: var(--tap); cursor: pointer;
  transition: filter .15s, transform .15s;
  -webkit-tap-highlight-color: transparent;
}
.primary:active { transform: translateY(1px); }
.primary:disabled { opacity: .55; cursor: default; transform: none; }
/* Видимое следствие клика по «К созданию» (эпик QA3 2026-08-28, п.1 отчёта
   тестировщика №3). Класс ставит web/src/wizard.js::goToCreate.
   ЗАЧЕМ. Закреплённая кнопка мастера — единственная ВСЕГДА видимая на пятом
   шаге — не давала ни одного пикселя изменений: renderSummary рисует то же
   самое, scrollIntoView({block:"center"}) на уже центрированной кнопке no-op,
   а программный focus() в ходе обработки клика МЫШЬЮ не поднимает
   :focus-visible (правила button:focus в файле нет вовсе). Человек делал
   единственный возможный вывод — «зависло» — и жал ещё раз.
   Один прогон, не бесконечная пульсация: это ответ на клик, а не индикатор
   процесса. */
.cta-flash { animation: ctaFlash .9s ease-out 1; }
@keyframes ctaFlash {
  0%   { box-shadow: 0 0 0 0 var(--gold); }
  100% { box-shadow: 0 0 0 14px transparent; }
}
/* Кому анимация противопоказана — тот же ответ статикой, а не тишиной. */
@media (prefers-reduced-motion: reduce) {
  .cta-flash { animation: none; outline: 2px solid var(--gold); outline-offset: 2px; }
}

/* Отклик создания БЕЗ `disabled` (тот же эпик, п.1-бис). wizard.js::createStory
   больше не выключает `#create` атрибутом `disabled`: браузер снимает фокус с
   элемента, ставшего disabled, и выбрасывает его на <body> — а фокус туда
   привёл сам goToCreate, ради клавиатурного пути. Вместо этого ставится пара
   aria-disabled/aria-busy, и кнопка остаётся в дереве фокуса: смена её имени
   на «Создаю…» звучит для диктора как смена имени сфокусированного элемента.
   БЕЗ ЭТОГО ПРАВИЛА кнопка перестанет гаснуть — то есть фикс доступности
   станет регрессом для глаза. Значения дословно те же, что у :disabled. */
.primary[aria-disabled="true"] { opacity: .55; cursor: default; transform: none; }
.ghost {
  font-family: var(--font-ui); font-size: 13.5px;
  background: transparent; color: var(--ink2);
  border: 1px solid var(--line2); border-radius: 10px;
  padding: 10px 14px; min-height: var(--tap); cursor: pointer;
  transition: color .15s, border-color .15s, background .15s;
  -webkit-tap-highlight-color: transparent;
}
.ghost:active { color: var(--ink); border-color: var(--ink3); }
/* опасная ghost-кнопка («Выйти» в профиле): нейтральна в покое, краснеет при действии */
.ghost.danger:active { color: var(--danger); border-color: var(--danger); }
.linklike {
  border: none; background: none; cursor: pointer;
  color: var(--ink3); font: inherit; font-size: 13px;
  text-decoration: underline; text-underline-offset: 3px; padding: 4px;
}
.cta { width: 100%; min-height: 56px; font-size: 16.5px; box-shadow: var(--shadow-soft); }
.hint { font-size: 13px; font-style: italic; color: var(--ink3); }
.hint.center { display: block; text-align: center; margin: 12px 0 0; }

/* ------------------------------------------------------------ поля и формы */
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field > span {
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .09em; color: var(--ink3);
}
.note { font-style: italic; font-size: 12.5px; color: var(--ink3); }
/* Подпись «иллюстрации не получились» под главой (0037): тихая строка рядом с
   кнопкой повтора, а не плашка — глава уже прочитана, картинки украшение. */
.media-note { display: flex; align-items: center; gap: 10px; margin: 14px 0 0; }
.media-note button { flex: 0 0 auto; }
.field + .field, .field + .switch, .switch + .field { margin-top: 12px; }
.duo { display: grid; grid-template-columns: 1fr; gap: 14px; margin-top: 12px; }
.duo .field + .field { margin-top: 0; }
@media (min-width: 900px) { .duo { grid-template-columns: 1fr 1fr; gap: 14px 22px; } }

input, select, textarea {
  font-family: var(--font-ui); font-size: 16px; color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line2); border-radius: 10px;
  padding: 11px 14px;
  width: 100%;
  min-height: 48px;
  transition: border-color .15s, box-shadow .15s;
}
textarea { min-height: 0; resize: vertical; line-height: 1.5; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--gold);
  box-shadow: 0 0 0 3px var(--gold-soft);
}
input::placeholder, textarea::placeholder { color: var(--ink3); }
select { cursor: pointer; }
select option { background: var(--raised); color: var(--ink); }
input[type="checkbox"], input[type="radio"] { width: auto; min-height: 0; accent-color: var(--gold); }
input[type="range"] {
  padding: 0; border: none; background: transparent;
  accent-color: var(--gold); box-shadow: none;
  min-height: 36px; width: 100%;
}
input[type="number"] { -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* ------------------------------------------------------------ слаг */
.slug-row {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px; min-height: 32px; flex-wrap: wrap;
}
.slug-k { font-size: 12px; font-style: italic; color: var(--ink3); }
.slug-display {
  background: none; border: none; cursor: pointer;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 13px; color: var(--gold-text);
  border-bottom: 1px dashed var(--line2);
  padding: 6px 2px; min-height: 32px;
}
.slug-display::after { content: " ✎"; color: var(--gold); font-size: 12px; }
.slug-display.placeholder { color: var(--ink3); font-style: italic; font-family: var(--font-ui); }
.slug-input { flex: 1 1 220px; min-height: var(--tap); font-family: ui-monospace, Consolas, monospace; font-size: 16px; }

/* ------------------------------------------------------------ переключатель (switch) */
.switch {
  display: flex; align-items: center; gap: 12px;
  cursor: pointer; min-height: var(--tap); margin-top: 14px;
  -webkit-tap-highlight-color: transparent;
}
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch .track {
  flex: none; width: 44px; height: 26px; border-radius: 999px;
  background: var(--line2); position: relative; transition: background .2s;
}
.switch .track::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--raised); box-shadow: 0 1px 3px rgba(0,0,0,.35);
  transition: left .2s;
}
.switch input:checked + .track { background: var(--gold); }
.switch input:checked + .track::after { left: 21px; }
.switch input:focus-visible + .track { outline: 2px solid var(--gold); outline-offset: 2px; }
.switch-text { font-size: 14.5px; }

/* ------------------------------------------------------------ сегмент-контролы */
.seg {
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 5px;
  background: var(--raised);
  border: 1px solid var(--line); border-radius: 12px;
}
.seg label { flex: 1 1 auto; position: relative; cursor: pointer; min-width: 0; -webkit-tap-highlight-color: transparent; }
.seg input { position: absolute; opacity: 0; pointer-events: none; }
.seg span {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  min-height: 46px; padding: 8px 8px;
  font-weight: 600; font-size: 14px; text-align: center;
  border-radius: 9px; color: var(--ink2);
  transition: background .15s, color .15s;
}
/* ПРИГЛУШЕНИЕ ЧЕРЕЗ opacity ЗДЕСЬ УБРАНО (эпик 2026-08-28, P1-7).
   Подпись сеттинга — 10.5px, то есть самый мелкий текст мастера, и множитель
   прозрачности съедал у неё ровно ту разницу, которую даёт токен:
     .seg small          --ink3 × .9  → 4.36:1 (без множителя 5.41);
     checked small       --on-gold × .7 → 3.15:1 светлая / 4.26:1 тёмная
                                         (без множителя 4.62 и 7.64);
     .seg-intensity      --gold-text × .8 → 3.62:1 светлая (без множителя 5.44).
   Разницу между заголовком опции и её подписью несут кегль (14 → 10.5px),
   насыщенность (600 → 400) и сам токен --ink3 — этого достаточно, а opacity
   поверх токена делает контраст непредсказуемым: он зависит от того, какая
   поверхность окажется под элементом. */
.seg small {
  font-weight: 400; font-size: 10.5px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink3);
}
.seg label:active span { background: var(--gold-soft); }
.seg input:checked + span { background: var(--gold); color: var(--on-gold); }
.seg input:checked + span small { color: var(--on-gold); }
.seg input:focus-visible + span { outline: 2px solid var(--gold); outline-offset: -2px; }
/* тональность — карточки в сетке (по макету рейтинга), не сплошной сегмент.
   2026-07-28: было .seg-rating с одной буквой (G/T/M/E) — теперь подпись словом,
   поэтому колонка шире, а кегль меньше (длинное «Драматичная» не переносится). */
.seg-intensity {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;
  padding: 0; background: transparent; border: none; border-radius: 0;
}
.seg-intensity span {
  min-height: 74px; gap: 4px; padding: 14px 10px;
  border: 1px solid var(--line2); border-radius: 12px;
}
.seg-intensity span, .seg-intensity input:checked + span {
  font-family: var(--font-serif); font-size: 17px; font-weight: 600;
}
.seg-intensity input:checked + span {
  background: var(--gold-soft); color: var(--gold-text); border-color: var(--gold);
}
.seg-intensity input:checked + span small { color: var(--gold-text); }
.seg-intensity small { font-family: var(--font-ui); letter-spacing: .06em; }

#f-setting-text { margin-top: 10px; }
#f-setting-text:disabled { opacity: .5; background: var(--surface); }
#f-language-custom { margin-top: 10px; }

.intensity-desc {
  margin: 10px 2px 0; min-height: 20px;
  font-size: 13.5px; font-style: italic; color: var(--ink2);
}

/* ------------------------------------------------------------ чипы */
.chips {
  border: 1px solid var(--line2); border-radius: 12px;
  background: var(--raised); padding: 7px; cursor: text;
}
.chips:focus-within { border-color: var(--gold); box-shadow: 0 0 0 3px var(--gold-soft); }
.chips-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.chip {
  display: inline-flex; align-items: center;
  background: var(--gold-soft);
  border: 1px solid var(--gold); color: var(--gold-text);
  border-radius: 999px;
  padding: 0 0 0 14px; min-height: 38px;
  font-size: 13.5px; max-width: 100%;
}
.chip-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* МНОЖИТЕЛЬ ПРОЗРАЧНОСТИ НА ТЕКСТЕ УБРАН (эпик 2026-08-28, P1-7) — здесь и ещё
   в одиннадцати местах файла. Правило общее: контраст считается по СОСТАВНОМУ
   цвету, поэтому `color: <приглушённый токен>; opacity: <1` съедает ровно ту
   разницу, которую токен даёт. Крестик чипа наследовал --gold-text (5.48:1 к
   своей подложке) и ронял его до 3.0:1. Приглушение теперь несёт сам токен и
   кегль, а не множитель поверх них. */
.chip-x {
  background: none; border: none; cursor: pointer;
  color: inherit; font-size: 12px;
  padding: 9px 12px; min-height: 38px; min-width: 36px;
  border-radius: 999px;
}
.chip-x:active { color: var(--danger); }
.chips-input {
  flex: 1 1 130px; width: auto; min-width: 90px;
  border: none; background: transparent;
  padding: 8px 6px; min-height: 38px; font-size: 15px;
}
.chips-input:focus { box-shadow: none; border: none; outline: none; }

/* подсказки-пилюли (жанры; фандомы — плитки раздела «Мир», builder.css) */
.suggest {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-ui); font-size: 13.5px; font-weight: 500;
  background: transparent; color: var(--ink2);
  border: 1px solid var(--line2); border-radius: 999px;
  padding: 9px 14px; min-height: 40px; cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
  -webkit-tap-highlight-color: transparent;
}
.suggest:active { transform: scale(.97); }
.suggest.on {
  background: var(--gold-soft); color: var(--gold-text);
  border-color: var(--gold);
}
/* сгруппированные подсказки жанров */
.suggest-grouped { margin-top: 10px; }
.sg-group { margin-top: 14px; }
.sg-group:first-child { margin-top: 0; }
.sg-label { display: block; font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3); margin: 0 0 8px; }
.sg-row { display: flex; flex-wrap: wrap; gap: 8px; }

/* тропы: группы + пилюли-тумблеры со звездой */
.trope-groups { margin-top: 4px; }
.trope-group { margin-top: 14px; }
.trope-group:first-child { margin-top: 0; }
.trope-group-title { display: block; font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3); margin: 0 0 8px; }
.trope-row { display: flex; flex-wrap: wrap; gap: 8px; }
.trope-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-ui); font-size: 13.5px; font-weight: 500; cursor: pointer;
  background: transparent; color: var(--ink2);
  border: 1px solid var(--line2); border-radius: 999px;
  padding: 9px 14px 9px 12px; min-height: 40px;
  transition: background .15s, border-color .15s, color .15s;
  -webkit-tap-highlight-color: transparent;
}
.trope-toggle .tr-star { opacity: .4; font-size: 11px; }
.trope-toggle.active { background: var(--gold-soft); color: var(--gold-text); border-color: var(--gold); }
.trope-toggle.active .tr-star { opacity: 1; color: var(--gold); }

/* ============================================================
   МАСТЕР «Новая история» — с 2026-10 это конструктор книги, его вёрстка
   живёт в builder.css. Здесь остались общие с ним правила: отступ вида,
   блок .wiz-block (импорт книги, пейринги, витрина завязок starters.js,
   карточка пробного опыта generation.js) и панель .wiz-nav. Прогресс из
   пяти точек, лист шагов и панель «Назад / Далее» пятишагового мастера
   ушли вместе с ним.
   ============================================================ */
#view-new { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }

.wiz-block { margin-top: 30px; padding-top: 22px; border-top: 1px solid var(--line); }
.wiz-block h2 {
  font-size: 11px; font-weight: 600; margin: 0 0 14px;
  text-transform: uppercase; letter-spacing: .15em; color: var(--gold-text);
}

/* sticky-панель «Назад / Далее» — сегодня у разворота сцены ролевой книги
   (roleplay/setup.js, #rp-spread-nav) */
.wiz-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  background: var(--bg);
  background: color-mix(in oklab, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
  padding: 10px clamp(14px, 4vw, 40px) calc(10px + env(safe-area-inset-bottom));
}
.wiz-nav-inner { max-width: 780px; margin: 0 auto; display: flex; align-items: center; gap: 14px; }

/* необязательный блок (импорт книги) — details/summary */
.optional-block { padding: 0; }
.optional-block[open] { padding-bottom: 6px; }
.optional-summary {
  list-style: none; cursor: pointer; display: flex; align-items: center;
  gap: 12px; padding: 16px 2px; min-height: var(--tap); user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: color .15s;
}
.optional-summary::-webkit-details-marker { display: none; }
.opt-badge {
  flex: none; font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .1em; color: var(--ink3);
  border: 1px solid var(--line2); border-radius: 999px; padding: 3px 9px;
}
.opt-title { flex: 1; font-size: 13.5px; color: var(--ink2); }
.opt-chevron { flex: none; color: var(--gold-text); transition: transform .2s; }
.optional-block[open] .opt-chevron { transform: rotate(180deg); }
.optional-body { padding: 4px 2px 8px; }

/* ------------------------------------------------------------ галерея примеров */
.example-gallery {
  display: flex; gap: 10px;
  overflow-x: auto; padding-bottom: 8px; margin-bottom: 6px;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}
.example-gallery > .muted { font-size: 13.5px; }
.example-card {
  flex: 0 0 78%; max-width: 300px; scroll-snap-align: start;
  display: flex; align-items: flex-start; gap: 12px;
  text-align: left; font-family: var(--font-ui); color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line2);
  border-radius: 12px; padding: 14px 15px; min-height: 96px;
  cursor: pointer;
  transition: border-color .15s, background .15s;
  -webkit-tap-highlight-color: transparent;
}
.ex-swatch {
  flex: none; width: 10px; height: 15px; border-radius: 2px;
  margin-top: 3px; background: var(--gold);
}
.ex-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; align-self: stretch; }
.example-card strong { font-size: 14px; font-weight: 600; line-height: 1.3; }
.example-card .ex-fandoms { font-style: italic; font-size: 12px; color: var(--ink3); }
.example-card .ex-meta {
  display: flex; gap: 10px; align-items: center; margin-top: auto; padding-top: 4px;
  font-size: 12px; color: var(--ink3);
}
.example-card:active { transform: scale(.99); }
.example-card.selected {
  border-color: var(--gold); background: var(--gold-soft);
  box-shadow: 0 0 0 1px var(--gold);
}
@media (min-width: 720px) {
  .example-gallery { flex-wrap: wrap; overflow: visible; }
  .example-card { flex: 1 1 220px; max-width: none; }
}
/* сохранённый пресет пользователя */
.preset-card-saved { position: relative; }
.preset-del {
  position: absolute; top: 6px; right: 6px; width: 24px; height: 24px;
  border: none; border-radius: 50%; background: var(--gold-soft); color: var(--ink2);
  cursor: pointer; line-height: 1; font-size: 12px; padding: 0;
  transition: background .15s, color .15s;
}
.preset-del:active { background: var(--danger); color: #fff; }

/* ------------------------------------------------------------ канон-паки */
.packs { display: grid; grid-template-columns: 1fr; gap: 10px; }
@media (min-width: 720px) { .packs { grid-template-columns: 1fr 1fr; } }
.packs .muted { margin: 0; font-size: 13.5px; }
.pack-card {
  position: relative; display: flex; align-items: center; gap: 12px;
  background: var(--raised);
  border: 1px solid var(--line2); border-radius: 12px;
  padding: 13px 15px; min-height: 58px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .15s, background .15s;
}
.pack-card:active { background: var(--gold-soft); }
.pack-card:has(input:checked) { border-color: var(--gold); background: var(--gold-soft); }
.pack-card input { position: absolute; opacity: 0; pointer-events: none; }
.pack-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pack-body strong { font-size: 14.5px; font-weight: 600; }
.pack-name { font-size: 12px; color: var(--ink3); font-family: ui-monospace, Consolas, monospace; }
.pick-tick {
  flex: none; margin-left: auto;
  width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--line2); color: transparent;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600;
  transition: background .15s, border-color .15s, color .15s;
}
.pack-card input:checked ~ .pick-tick {
  background: var(--gold); border-color: var(--gold); color: var(--on-gold);
}

/* ------------------------------------------------------------ вайбы */
/* цветовая точка вайба (палитра из макета) */
.vibe-dot {
  flex: none; width: 12px; height: 12px; border-radius: 50%; margin-top: 4px;
  background: var(--sw, var(--gold));
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--sw, var(--gold)) 25%, transparent);
}
.vibe-warm  { --sw: #C98A4B; }
.vibe-dark  { --sw: #4A4E5A; }
.vibe-witty { --sw: #B9A33B; }
.vibe-lyric { --sw: #7A6E9E; }
.vibe-cine  { --sw: #A34832; }

/* ------------------------------------------------------------ тон-слайдеры */
.tone-block { margin-top: 20px; }
.tone-block:first-of-type { margin-top: 0; }
.tone-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.tone-name {
  font-size: 12px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink2);
}
.tone-top b {
  font-family: var(--font-serif); font-weight: 600; color: var(--gold-text);
  font-variant-numeric: tabular-nums; font-size: 18px; min-width: 34px; text-align: right;
}
.tone-anchors {
  display: flex; justify-content: space-between;
  font-size: 11.5px; font-style: italic; color: var(--ink3);
  margin-top: -2px;
}
/* дорожка с золотой заливкой до бегунка (--pct обновляет wizard.js) */
.tone-block input[type="range"] {
  -webkit-appearance: none; appearance: none;
  min-height: 30px; margin: 2px 0; border-radius: 2px;
  background:
    linear-gradient(90deg, color-mix(in oklab, var(--gold) 45%, transparent), var(--gold))
      no-repeat left center / var(--pct, 40%) 3px,
    linear-gradient(var(--line2), var(--line2)) no-repeat left center / 100% 3px;
}
.tone-block input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%;
  background: var(--gold); border: 4px solid var(--surface);
  box-shadow: 0 0 0 1px var(--gold); cursor: grab;
}
.tone-block input[type="range"]::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--gold); border: 3px solid var(--surface);
  box-shadow: 0 0 0 1px var(--gold); cursor: grab;
}
.tone-block input[type="range"]::-moz-range-track { background: transparent; }

/* ------------------------------------------------------------ персонажи */
.char-card {
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--raised);
  padding: 16px 18px 18px; margin-bottom: 14px;
}
/* Шапка: аватар · [кикер «Персонаж N · канонный»] / [имя + роль] · шеврон · ✕.
   У свёрнутой карточки отступ снизу схлопывается в ноль (макет 2026-08-09):
   иначе свёрнутый список выглядит рыхлым — карточка ростом с одну строку
   держала бы 16px пустоты под собой. */
.char-head { display: flex; align-items: center; margin-bottom: 16px; cursor: pointer; }
.char-card.collapsed .char-head { margin-bottom: 0; }
.char-body { display: flex; flex-direction: column; gap: 14px; }
/* Аватар персонажа в шапке карточки (2026-07-28): виден и у свёрнутой карточки.
   Тот же ключ, что .rel-ava2 в панели «Отношения» и .avatar-* у пользователей.
   2026-08-09: база 40px — это размер В ШАПКЕ; на остальных поверхностях
   (.ava-row / .c-creator-ava) размер задают более
   специфичные правила ниже, они базу перебивают. */
.char-ava {
  position: relative;  /* портрет ложится АБСОЛЮТОМ поверх инициала — см. ниже */
  flex: none; width: 40px; height: 40px; border-radius: 50%; margin-right: 12px;
  display: inline-flex; align-items: center; justify-content: center; overflow: hidden;
  color: #fff; font-family: var(--font-serif); font-size: 17px; font-weight: 600;
}
/* Инициал лежит в кружке ВСЕГДА, портрет накрывает его сверху. Портрета может
   не быть (реальный человек — гард на бэке, снятый фандом, не завезённая
   картинка): тогда reader.js снимает <img> по ошибке загрузки, и инициал уже
   на месте — без прыжка вёрстки и без пустой рамки. `background: inherit`
   добивает случай полупрозрачного PNG: буква не просвечивает сквозь портрет. */
.char-ava > img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border-radius: 50%; object-fit: cover; display: block; background: inherit;
}
/* Образ персонажа на ОСТАЛЬНЫХ поверхностях мастера (2026-08-07): ряд лиц
   (.ava-row — карточка пресета, панель «Ваша книга», строки пар) и строка
   облика кастомного персонажа (.c-creator-ava). Кружок везде один и тот же
   .char-ava — ниже только раскладка и размеры вокруг него, чтобы одно и то же
   лицо не выглядело в разных местах по-разному и одинаково деградировало до
   инициала. Витрина лиц ростера и «Состав» резюме шага 5 ушли вместе с
   пятишаговым мастером (2026-10): героя ростера берёт комбобокс раздела
   «Герои», состав показывает панель «Ваша книга» (builder.css). */
/* Ряд лиц карточки пресета: кружки мельче карточных и внахлёст — четыре
   персонажа не должны распирать узкую карточку витрины. */
.ava-row { display: flex; align-items: center; margin: 2px 0 1px; }
.ava-row .char-ava {
  width: 26px; height: 26px; font-size: 12px;
  margin: 0 0 0 -7px; border: 1.5px solid var(--raised);
}
.ava-row .char-ava:first-child { margin-left: 0; }
/* Шапка карточки героя — строка раздела «Герои» (builder.css): кикер
   «ПЕРСОНАЖ 2 · КАНОННЫЙ», шеврон и кнопка удаления пятишагового мастера ушли
   вместе с ним (2026-10). */
.char-card.collapsed .char-body { display: none; }
.char-grid { display: grid; grid-template-columns: 1fr; gap: 13px; }
.char-grid .full { grid-column: 1 / -1; }
/* Расстояние в сетке задаёт gap. Глобальное `.field + .field {margin-top:12px}`
   (см. «поля и формы») тут только ломает выравнивание: с 2026-08-09 «Имя» и
   «Роль» стоят В ОДНОЙ строке, и лишний верхний отступ ронял правую колонку
   ниже левой. Тот же приём, что у .duo. */
.char-grid .field + .field { margin-top: 0; }
@media (min-width: 900px) { .char-grid { grid-template-columns: 1fr 1fr; gap: 13px 20px; } }
/* Строка облика кастомного персонажа: лицо · кнопка-пилюля · «Сохранить как
   персону». Сегменты типа и роли карточки (.char-card .seg) ушли с
   пятишаговым мастером (2026-10): роль — чипы раздела «Герои», builder.css. */
.char-canon-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0;
  padding: 12px; background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
}
.c-creator-ava { width: 52px; height: 52px; font-size: 20px; margin-right: 0; }
.c-creator-btn {
  flex: none; font-family: var(--font-ui); font-size: 13.5px; font-weight: 600;
  background: var(--gold-soft); color: var(--gold-text);
  border: 1px solid var(--gold); border-radius: 10px;
  padding: 10px 16px; min-height: var(--tap); cursor: pointer;
  transition: filter .15s;
  -webkit-tap-highlight-color: transparent;
}
.c-creator-btn:active { filter: brightness(1.08); }

/* ------------------------------------------------------------ степперы */
.stepper {
  display: flex; align-items: stretch;
  border: 1px solid var(--line2); border-radius: 12px;
  overflow: hidden; background: var(--raised);
}
.step-btn {
  flex: none; width: 52px; min-height: 50px;
  font-family: var(--font-ui); font-size: 22px; line-height: 1;
  border: none; background: none; color: var(--gold-text);
  cursor: pointer; transition: background .15s;
}
.step-btn:first-child { border-right: 1px solid var(--line); }
.step-btn:last-child { border-left: 1px solid var(--line); }
.step-btn:active { background: var(--gold-soft); }
.stepper input {
  flex: 1; min-width: 0; min-height: 50px;
  border: none; border-radius: 0; background: transparent;
  text-align: center; font-family: var(--font-serif); font-size: 19px; font-weight: 600;
}
.stepper input:focus { box-shadow: inset 0 0 0 2px var(--gold-soft); }

/* ------------------------------------------------------------ карточка-резюме «подпись — значение» */
/* Сегодня — карточка «Что входит в пробный опыт» (generation.js). Резюме шага
   «Итог» пятишагового мастера («титульный лист», секции с «изменить») ушло
   вместе с ним (2026-10): итог книги — панель «Ваша книга», builder.css. */
.summary-card {
  background: var(--raised);
  border: 1px solid var(--line); border-radius: 14px;
  padding: 8px 18px; margin-bottom: 18px;
}
.sum-line { display: flex; gap: 10px; margin-top: 7px; font-size: 13.5px; line-height: 1.5; }
.sum-k { flex: none; width: 108px; color: var(--ink3); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; padding-top: 2px; }
.sum-v { min-width: 0; overflow-wrap: anywhere; }

/* сохранить как пресет — инлайн-форма */
.preset-save { margin-top: 14px; padding-top: 14px; border-top: 1px dashed var(--line); }
.preset-save-form { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.preset-save-form input { flex: 1 1 160px; min-width: 0; }
.preset-save-form .primary, .preset-save-form .ghost { flex: none; }

/* ============================================================
   БИБЛИОТЕКА и КАТАЛОГ — «полка»: обложки-корешки + мета
   ============================================================ */
/* шапка вида: киккер + заголовок слева, поиск и фильтры справа */
.view-head { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 16px 28px; margin: 4px 0 28px; }
.view-head-main { flex: 1 1 240px; min-width: 0; }
.view-kicker {
  font-size: 11px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink3); margin-bottom: 10px;
}
.view-head .view-title { margin: 0; }

.empty-note { font-style: italic; color: var(--ink2); }
/* лист-орнамент над текстом пустого состояния */
.empty-note .leaf { display: block; width: 12px; height: 12px; margin: 2px 0 12px 3px; opacity: .85; }
/* Кнопка внутри пустого состояния («Сбросить фильтры», «Повторить» — заявка
   CARDS §4): .empty-note это курсивный <p>, и наследованный курсив на кнопке
   читается как опечатка. Отступ сверху, а не слева: на 320px кнопка обязана
   уезжать ПОД текст, а не выталкивать его. */
.empty-note .ghost { display: inline-block; margin-top: 10px; font-style: normal; }

/* «Вы остановились здесь» — герой-карточка продолжения чтения (макет 153-173) */
.lib-hero {
  display: flex; flex-wrap: wrap; gap: clamp(22px, 4vw, 44px);
  background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
  padding: clamp(22px, 3.5vw, 36px); margin: 0 0 30px;
  box-shadow: var(--shadow-soft);
}
.lib-hero-cover { flex: none; width: 148px; cursor: pointer; }
.lib-hero-cover .book-cover { width: 100%; }
.lib-hero-body {
  flex: 1 1 340px; min-width: 0;
  display: flex; flex-direction: column; justify-content: center; gap: 14px;
}
.lib-hero-kicker {
  font-size: 11px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--gold-text);
}
.lib-hero-title {
  font-family: var(--font-serif); font-size: clamp(22px, 3vw, 30px);
  font-weight: 600; line-height: 1.2;
}
.lib-hero-fandom { color: var(--ink3); font-size: .62em; font-weight: 400; font-style: italic; }
.lib-hero-prog { display: flex; align-items: center; gap: 14px; max-width: 520px; }
.lib-hero-track { flex: 1; height: 3px; border-radius: 2px; background: var(--line2); overflow: hidden; }
.lib-hero-fill { display: block; height: 100%; border-radius: 2px; background: var(--gold); }
.lib-hero-note { font-size: 13px; color: var(--ink2); white-space: nowrap; font-variant-numeric: tabular-nums; }
.lib-hero-choice { font-family: var(--font-serif); font-style: italic; font-size: 15px; line-height: 1.5; color: var(--ink2); }
.lib-hero-choice:empty { display: none; }
.lib-hero-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }

/* полка-заголовок «Все книги · N книг · M завершены» (макет 176-180) */
.lib-shelf-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 20px; }
.lib-shelf-head h2 { margin: 0; font-family: var(--font-serif); font-size: 20px; font-weight: 600; }
.lib-shelf-note { font-size: 13px; color: var(--ink3); white-space: nowrap; }
.lib-shelf-rule { flex: 1; height: 1px; background: var(--line); }

/* полка: сетка вертикальных книг */
.library {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: clamp(18px, 3vw, 30px) clamp(14px, 3vw, 26px);
}
@media (min-width: 720px) {
  .library { grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); }
}
/* #library-grid сам — .library, а группы кладутся вложенными .library-сетками
   (renderLibrary): вложенные секции и заголовки групп занимают всю ширину */
.library > .library, .library > .lib-group-head { grid-column: 1 / -1; }

.story-card {
  position: relative;  /* якорь для .cover-pop (пикер цвета обложки) */
  display: flex; flex-direction: column; gap: 5px;
  min-width: 0; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Появление полки анимируем ТОЛЬКО при входе на вкладку (класс .cards-enter,
   ставится флагом state._animateCards и гаснет первым рендером) — не на каждый
   символ поиска/фильтр. Горизонтальный сдвиг от скроллбара убран отдельно
   (html{overflow-y:scroll}), так что fade читается как аккуратная анимация, а не
   «подрагивание». */
.library.cards-enter .story-card { animation: ff-fade-up .4s ease backwards; }
/* Уважение к «уменьшить движение» — и заодно к измеримости контраста
   (эпик 2026-08-28, P1-7). Пока карточка едет по ff-fade-up, ВЕСЬ её текст
   полупрозрачен: axe ловил метаданные полки на середине анимации и печатал
   не цвет токена, а случайную смесь (#ADA18B вместо #A0937B — как будто
   поверх стоит opacity ≈ .84). Значит и живой читатель эти 0.4 секунды видит
   текст бледнее заявленного, а прогон набора становится зависимым от того,
   успел ли кадр. У остальных декоративных анимаций файла гард по
   prefers-reduced-motion уже есть (.ca-dot, .drawer-tab.has-beat, body.booting),
   у появления полки его не было. */
@media (prefers-reduced-motion: reduce) {
  .library.cards-enter .story-card { animation: none; }
}
/* Строка звёзд карточки. Пилюли действий каталога (.card-acts / .card-follow /
   .card-fork / .card-mine) отсюда УДАЛЕНЫ 2026-08-12 вместе с редизайном v2.
   Их наследники v2 (.cat-follow / .cat-fork) прожили ровно сутки и удалены
   редизайном v3: подписка живёт в герое «Книга недели» и в панели книги, форк —
   кнопкой в панели книги; на карточке осталась только метка «ваша книга»
   (.cat-card-mine). Разметку с этими классами не печатает больше ни один
   web/*.js (проверено grep'ом по web/ и index.html); .card-stars жив — его
   печатает читалка (reader.js).
   wrap: на узкой карточке содержимое переносится ВНИЗ, а не на соседнюю книгу */
.card-stars { display: flex; align-items: center; gap: 4px 8px; margin-top: 2px; flex-wrap: wrap; min-width: 0; }
/* обложка-корешок: переплёт слева, рамка-«тиснение», заголовок и виньетка */
.book-cover {
  position: relative; width: 100%; aspect-ratio: 2 / 3;
  margin-bottom: 7px;
  border-radius: 6px 10px 10px 6px;
  background:
    linear-gradient(105deg, rgba(255,255,255,.09), rgba(255,255,255,0) 26%),
    var(--spine, #4C3B26);
  color: var(--cover-ink);
  box-shadow: var(--shadow-soft);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  padding: 16px 12px;
  transition: transform .25s ease, box-shadow .25s ease;
}
.cover-frame {
  position: absolute; inset: 7px;
  border: 1px solid rgba(239,230,210,.28);
  border-radius: 4px 7px 7px 4px; pointer-events: none;
}
.cover-title {
  font-family: var(--font-serif); font-size: 17px; font-weight: 600;
  line-height: 1.3; text-align: center; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
}
.cover-mark { font-size: 11px; letter-spacing: .35em; padding-left: .35em; opacity: .7; }
.story-card:active .book-cover { transform: scale(.985); }

/* мета под обложкой */
.card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 2px; }
.card-top h3 {
  margin: 0; min-width: 0;
  font-family: var(--font-ui); font-size: 15px; font-weight: 600; line-height: 1.35;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Заголовок карточки — КНОПКА (P1-1, эпик UXA 2026-08-28; заявка пакета CARDS,
   notes/cards_requests.md §1). Роль/tabindex на самой .story-card запрещены:
   внутри карточки живут <button> (палитра, удаление, отписка, теги, язык,
   автор), и axe-правило nested-interactive (serious, wcag2a) уронило бы
   a11y.spec.ts на трёх экранах сразу. Фокус получает ЗАГОЛОВОК, карточка
   остаётся мышиной зоной; клик по кнопке всплывает до слушателя карточки,
   поэтому книга открывается ровно один раз.
   Кнопка обязана выглядеть ровно как прежний <h3>/<b> — отсюда полный сброс.
   МЕСТО ОБЯЗАТЕЛЬНО ЗДЕСЬ, а не в конце файла: .cat-card-title (:6388)
   переопределяет `font: inherit` своими font-* только стоя ПОЗЖЕ, и переезд
   этого блока вниз съехал бы витринный заголовок на шрифт карточки полки. */
.card-open, .cat-card-title {
  background: none; border: 0; margin: 0; padding: 0;
  font: inherit; color: inherit; text-align: inherit; cursor: pointer;
  display: block; max-width: 100%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.card-top-right { flex: none; display: flex; align-items: center; gap: 6px; }
.card-fandoms {
  font-style: italic; font-size: 12.5px; color: var(--ink3); padding: 0 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.card-prog { display: flex; align-items: center; gap: 10px; padding: 3px 2px 0; }
.prog-track { flex: 1; height: 2px; border-radius: 1px; background: var(--line2); overflow: hidden; }
.prog-fill { display: block; height: 100%; border-radius: 1px; background: var(--gold); }
.prog-fill.done { background: var(--good); }
.prog-note { font-size: 11.5px; color: var(--ink3); font-variant-numeric: tabular-nums; white-space: nowrap; }
.card-meta {
  display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center;
  font-size: 12px; color: var(--ink2); padding: 2px 2px 0;
}
/* Пилюля тональности на карточке библиотеки.
   2026-07-28: раньше внутри была ОДНА буква рейтинга (G/T/M/E) — под неё и был
   разрежен трекинг (.06em). Теперь там слово («Драматичная»), и тот же трекинг
   растягивал пилюлю на пол-строки метаданных. Ширина не фиксирована, поэтому
   обрезки не было и нет, но nowrap добавлен явно: перенос «Драма-/тичная»
   внутри пилюли выглядел бы поломкой. */
.rating-pill {
  border: 1px solid var(--line2); border-radius: 5px; padding: 2px 6px;
  font-weight: 600; font-size: 10.5px; letter-spacing: .01em; color: var(--ink3);
  white-space: nowrap;
}
.done-pill { color: var(--good); font-weight: 600; }
.writing-pill { display: inline-flex; align-items: center; gap: 7px; color: var(--gold-text); font-weight: 600; }
/* статус-точка «пишется…» — пульсирующий глиф-лист Turnleaves (§3b «СТАТУС В UI») */
.writing-pill::before {
  content: ""; flex: none; width: 8px; height: 8px;
  border-radius: 0 100% 0 100%; transform: rotate(-45deg);
  background: var(--gold); animation: leaf-pulse 1.6s ease-in-out infinite;
}
.pub-pill { color: var(--good); font-weight: 600; }
.card-del {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: none; cursor: pointer;
  color: var(--ink3); font-family: var(--font-ui); font-size: 13px; line-height: 1;
  padding: 5px 6px; border-radius: 7px;
  /* opacity: .75 убран (P1-7): --ink3 × .75 давал 2.98:1 в светлой теме —
     крестик удаления был бледнее всего, что вокруг него на карточке. */
  transition: color .15s;
}
.card-del:active { color: var(--danger); }
/* Крестик «убрать из библиотеки» на карточке СОХРАНЁННОЙ книги. Форма как у
   .card-del, но без опасного акцента: отписка ничего не удаляет — книга
   остаётся у автора, вернуть её на полку можно одним кликом. */
.card-unfollow {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: none; cursor: pointer;
  color: var(--ink3); font-family: var(--font-ui); font-size: 13px; line-height: 1;
  padding: 5px 6px; border-radius: 7px;
  /* opacity: .7 убран (P1-7): --ink3 × .7 = 2.73:1 в светлой теме. */
  transition: color .15s;
}
.card-unfollow:active { color: var(--gold-text); }
/* «слежу» — метка карточки-ссылки: спокойная, в отличие от золотой .pub-pill
   («в каталоге»), потому что сообщает не достижение, а состояние. */
.follow-pill { color: var(--ink3); font-weight: 600; }
.card-author { color: var(--ink3); }
.card-rating { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--ink2); }

/* панель управления библиотекой/каталогом (живёт в .view-head) */
.lib-toolbar { flex: 1 1 100%; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.lib-search { width: 100%; box-sizing: border-box; }
.lib-controls { display: flex; flex-wrap: wrap; gap: 10px; }
.lib-ctl { flex: 1 1 140px; display: flex; flex-direction: column; gap: 4px; margin: 0; min-width: 0; }
.lib-ctl > span { font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .1em; color: var(--ink3); }
.lib-ctl select { width: 100%; min-height: 42px; font-size: 13.5px; color: var(--ink2); }
.lib-group-head {
  margin: 12px 0 0; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .14em; color: var(--ink3);
  display: flex; align-items: center; gap: 12px;
}
.lib-group-head::after { content: ""; flex: 1; height: 1px; background: var(--line); }
@media (min-width: 720px) {
  .lib-toolbar { flex: 0 1 auto; flex-direction: row; align-items: flex-end; justify-content: flex-end; }
  .lib-search { flex: 0 1 280px; width: auto; }
  .lib-controls { flex: none; }
  .lib-ctl { flex: none; min-width: 150px; }
}

/* рейтинг звёздами */
.stars { display: inline-flex; gap: 1px; font-size: 14px; line-height: 1; letter-spacing: .05em; }
.stars .st { color: var(--line2); }
.stars .st.on { color: var(--gold); }
.star-row { font-size: 12.5px; letter-spacing: .05em; color: var(--line2); }
.star-row .st.on { color: var(--gold); }

/* ============================================================
   ЧИТАЛКА
   ============================================================ */
/* прогресс чтения — тонкая золотая линия под шапкой (только вид «Книга»);
   ширину заполнения ведёт updateReadProgress (пассивный scroll-листенер) */
.read-progress {
  position: fixed; top: var(--tb-h, 60px); left: 0; right: 0; height: 2px;
  /* z-index 45, а не прежние 55 (P1-4 эпика 2026-08-28). .topbar — sticky с
     z-index:50, то есть СОБСТВЕННЫЙ стековый контекст: выпадашки шапки
     (.profile-menu и .notif-panel, по 60) живут ВНУТРИ него и наружу выходят
     плоскостью 50. Всё, что снаружи стоит выше 50, ложится поверх них. Линия
     прогресса кликов не крала (pointer-events:none), но в тёмной теме золотая
     полоса резала карточку меню пополам. */
  z-index: 45; pointer-events: none;
  opacity: 0; transition: opacity .25s ease, top .25s ease;
}
.read-progress.on { opacity: 1; }
.read-progress-fill {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, color-mix(in oklab, var(--gold) 60%, transparent), var(--gold));
  box-shadow: 0 0 6px color-mix(in oklab, var(--gold) 40%, transparent);
}
/* мобильный ридер спрятал шапку — линия поднимается к верхнему краю */
body.chrome-hidden .read-progress { top: 0; }

.book-bar {
  max-width: 920px; margin: 0 auto 14px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
/* «＋ Добавить к себе» — золотистая пилюля (макет addLibCss) */
.add-lib-btn {
  border-radius: 10px; padding: 10px 16px; min-height: var(--tap);
  font-family: var(--font-ui); font-size: 13px; font-weight: 600; cursor: pointer;
  background: var(--gold-soft); border: 1px solid var(--gold); color: var(--gold-text);
  white-space: nowrap; transition: filter .15s;
}
.add-lib-btn:active { filter: brightness(1.1); }
/* «Создать свою ветку» рядом с «Сохранить»: ghost вместо золота. Иерархия
   намеренная — подписка бесплатна и обратима, ветка платит за генерацию и
   отрывает читателя от автора, поэтому громкой быть не должна. */
.add-lib-btn.branch-btn {
  background: none; border-color: var(--line2); color: var(--ink2);
}
/* «Слежу»: кнопка гаснет до индикатора, оставаясь кликабельной (отписка). */
.add-lib-btn.on {
  background: none; border-color: var(--line2); color: var(--ink3);
}
/* Группа действий книги: одна рамка, кнопки через тонкие разделители (макет) */
.book-actions {
  display: flex; align-items: stretch; flex-wrap: wrap; row-gap: 4px;
  max-width: 100%; box-sizing: border-box;
  background: var(--surface); border: 1px solid var(--line2);
  border-radius: 11px; padding: 2px;
}
.ba-btn {
  display: inline-flex; align-items: center; gap: 7px;
  background: none; border: none; border-radius: 9px;
  color: var(--ink2); padding: 9px 13px; min-height: 40px;
  font-family: var(--font-ui); font-size: 13px; cursor: pointer;
  position: relative; transition: color .15s, background .15s;
  -webkit-tap-highlight-color: transparent;
}
.ba-btn:active { color: var(--gold-text); }
.ba-glyph { font-size: 12px; opacity: .65; }
/* разделитель — на каждом видимом соседе, кроме первого (hidden не ломает ряд) */
.book-actions > .ba-btn + .ba-btn::before,
.book-actions > * + .ba-more-wrap::before {
  content: ""; position: absolute; left: 0; top: 7px; bottom: 7px;
  width: 1px; background: var(--line);
}
.ba-more-wrap { position: relative; display: flex; }
.ba-more { font-size: 16px; line-height: 1; padding: 9px 14px; }
/* меню «⋯»: публикация (с подписью), поделиться, жалоба */
.book-menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 80;
  min-width: 260px; background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; box-shadow: var(--shadow-soft); padding: 6px;
  display: flex; flex-direction: column;
  animation: ff-fade-up .2s ease both;
}
.bm-item {
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
  text-align: left; background: none; border: none; border-radius: 8px;
  padding: 10px 12px; font-family: var(--font-ui); font-size: 13.5px;
  color: var(--ink2); cursor: pointer; width: 100%;
  transition: background .15s, color .15s;
}
.bm-item:disabled { opacity: .6; cursor: default; }
.bm-item:active:not(:disabled) { background: var(--raised); }
.bm-row { display: inline-flex; align-items: center; gap: 8px; color: inherit; }
.bm-publish .bm-row { color: var(--ink); }
.bm-glyph { font-size: 12px; opacity: .8; width: 14px; text-align: center; flex: none; }
.bm-glyph.bm-gold { color: var(--gold-text); opacity: 1; }
.bm-glyph.bm-good { color: var(--good); opacity: 1; }
.bm-glyph.bm-warn { color: var(--danger); opacity: 1; }
.bm-cap { font-size: 11.5px; color: var(--ink3); padding-left: 22px; line-height: 1.45; }
.bm-item.bm-publish:not(.hidden) + .bm-item { margin-top: 5px; border-top: 1px solid var(--line); border-radius: 0; }
.chap-nav { order: 5; width: 100%; display: flex; align-items: center; gap: 10px; }
.chap-nav select { flex: 1; min-width: 0; min-height: var(--tap); font-size: 13.5px; color: var(--ink); background: var(--surface); }
.nav-arrow {
  flex: none; width: 42px; height: 42px; border-radius: 50%;
  border: 1px solid var(--line2); background: transparent;
  font-size: 18px; line-height: 1; color: var(--ink2); cursor: pointer;
  transition: border-color .15s, color .15s;
  -webkit-tap-highlight-color: transparent;
}
.nav-arrow:active:not(:disabled) { border-color: var(--gold); color: var(--gold-text); }
.nav-arrow:disabled { opacity: .35; cursor: default; }
/* ≥1000: выбор главы, «＋ Добавить к себе» и группа действий — ОДНА строка,
   прижатая к правому краю. Ниже 1000px инлайн-раскладка не влезает (только
   select+группа просят ≈900px) и рвала бар на две «лесенки» у правого края с
   пустой левой половиной (скрин владельца 2026-07-26) — там работает мобильная
   раскладка: группа действий слева, выбор главы отдельной строкой. */
@media (min-width: 1000px) {
  .book-bar { margin-bottom: 20px; gap: 12px; justify-content: flex-end; flex-wrap: nowrap; }
  /* select сжимаемый — стрелки не выталкиваются за края бара при нехватке места */
  .chap-nav { order: 0; width: auto; flex: 0 1 auto; min-width: 0; }
  .chap-nav select { flex: 1 1 auto; width: auto; min-width: 120px; max-width: 340px; }
  .add-lib-btn { flex: none; }
  .book-actions { flex: none; flex-wrap: nowrap; }
}
/* «Слушать» в группе: при активной озвучке — золотая подложка (макет listenCss) */
.listen-btn.playing { background: var(--gold-soft); color: var(--gold-text); }
.listen-btn.playing .ba-glyph { opacity: 1; }

/* Полотно главы — макет Reader Chapter Interactive (2026-08-21).
   Глава читается как страница книги, а не как карточка поверх стола: подложку,
   рамку, скругление и тень снимаем, оставляя текст прямо на var(--bg).
   Переопределяем ТОЛЬКО .book-sheet — базовый .sheet общий (до 2026-10 его
   держал и лист пятишагового мастера, .wiz-sheet), и трогать его нельзя.
   676px внешней ширины при боковых полях 18px дают ровно 640px строки — ту же
   меру, что у .prose, поэтому проза и блоки конца главы стоят по одной оси.
   ВЕРХНЕЕ ПОЛЕ 22px — не из макета, а вместо того, чего в макете нет: там
   первым элементом полотна идёт шапка главы с `margin:36px 0 30px`, а в проде
   перед ней стоит ещё и колонтитул .running-head. С нулевым верхом от
   статус-строки до колонтитула оставалось 14px (её margin-bottom) — верх экрана
   слипался. 14 + 22 дают ровно макетные 36px. Именно ПАДДИНГ, а не margin-top у
   колонтитула: при нулевом верхнем поле полотна его margin схлопнулся бы с
   полем .book-sheet и дальше со строкой состояния — зазор вышел бы 22px, а не
   сумма (проверено в рендере). */
.book-sheet {
  max-width: 676px; margin: 0 auto;
  background: none; border: none; border-radius: 0; box-shadow: none;
  padding: 22px 18px 70px;
}
.running-head {
  display: flex; justify-content: space-between; gap: 16px;
  font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .18em;
  color: var(--ink3);
  padding-bottom: 14px; margin-bottom: 34px;
  border-bottom: 1px solid var(--line);
}
@media (min-width: 720px) { .running-head { margin-bottom: 40px; } }

/* проза */
/* Меру, кегль, межстрочье и семейство крутит панель «Настройки чтения»
   (web/reading_prefs.js) — через переменные на html. ФОЛБЭК КАЖДОЙ
   ПЕРЕМЕННОЙ — прежний литерал: пока человек ничего не трогал, переменная не
   объявлена вовсе и вычисленное значение побайтово совпадает с тем, что было
   до правки. Дефолт «19px» вместо clamp был бы регрессией: на телефоне
   (375px) clamp даёт 17px, и панель молча увеличила бы кегль каждому, кто её
   открыл ради другой ручки. */
.prose {
  max-width: var(--prose-measure, 640px); margin: 0 auto;
  font-family: var(--prose-font, var(--font-serif));
  font-size: var(--prose-size, clamp(17px, 2vw, 19px));
  line-height: var(--prose-leading, 1.78); color: var(--ink);
}
.prose h1 {
  text-align: center; font-family: var(--font-serif);
  font-size: clamp(24px, 3.5vw, 31px); font-weight: 600;
  letter-spacing: .01em; margin: 6px 0 8px;
}
.prose h1::after {
  content: "⁂"; display: block; font-size: 13px; color: var(--gold);
  letter-spacing: .4em; padding-left: .4em; margin-top: 16px; font-weight: 400;
}
.prose h1 + * { margin-top: 30px; }
.prose h2 { font-size: 22px; font-weight: 600; margin: 32px 0 14px; }
.prose h3, .prose h4, .prose h5, .prose h6 { font-size: 18px; font-weight: 600; margin: 24px 0 10px; }
/* Шапка главы (chapterHeadHtml в reader.js): киккер «Глава N» + заголовок;
   h1 внутри наследует стиль .prose h1 (по центру, виньетка ⁂ после).
   Меры по макету Reader Chapter Interactive (2026-08-21): 36px сверху — воздух
   титула на голом фоне (карточки-листа под главой больше нет, отбивать шапку
   нечем, кроме пустоты); киккер мельче и разряженнее заголовка, иначе на
   плоском полотне два центрированных блока читаются как один. */
.chap-head { margin: 36px 0 30px; text-align: center; }
.chap-kicker {
  display: block; font-family: var(--font-ui);
  font-size: 10.5px; font-weight: 600; letter-spacing: .22em;
  text-transform: uppercase; color: var(--ink3); margin-bottom: 10px;
}
/* Отступы титула держит сам .chap-head: у h1 внутри шапки своих полей нет,
   иначе они складываются с margin-top виньетки ⁂ (.prose h1::after). */
.chap-head .chap-title { margin: 0; }
.prose p { margin: 0 0 .5em; text-indent: 1.5em; }
.prose h1 + p, .prose h2 + p, .prose .vignette + p, .prose .chap-head + p { text-indent: 0; }
/* Буквица первого абзаца главы (макет Reader Chapter Interactive, 2026-08-21) —
   чисто CSS, без хирургии над DOM: любая обёртка первой буквы сломала бы
   штампы data-scene (музыка) и посимвольную подсветку озвучки.
   Смежный комбинатор `+ p` сам отсекает всё, что буквицей быть не должно:
   <figure> иллюстрации, <blockquote>, заголовки сцен. Классы исключаем руками:
   .dlg — реплика (буквицей стало бы длинное тире), .li — пункт списка
   (буквицей стал бы маркер «•»). */
.prose h1 + p:not(.dlg):not(.li)::first-letter,
.prose .chap-head + p:not(.dlg):not(.li)::first-letter {
  float: left; font-size: 3.05em; line-height: .84;
  padding: .04em .1em 0 0; color: var(--gold-text); font-weight: 500;
}
.prose p.li { text-indent: 0; margin-left: 1.2em; }
.prose blockquote {
  margin: 14px 0 14px 1.5em; padding-left: 14px;
  border-left: 2px solid var(--line2); color: var(--ink2); font-style: italic;
}
.vignette {
  text-align: center; color: var(--ink3); font-size: 15px;
  letter-spacing: .5em; padding-left: .5em; margin: 34px 0;
}
.empty-book { text-align: center; font-style: italic; color: var(--ink2); text-indent: 0 !important; padding: 40px 0; }

/* tap-to-listen: подсветка абзаца */
#view-book .prose p, #view-book .prose blockquote { cursor: pointer; }
/* Макет Reader Chapter Interactive (2026-08-21): подсветка озвучиваемого абзаца
   — только мягкая заливка со скруглением. Ореол box-shadow снят намеренно: на
   плоском полотне (карточки-листа под главой больше нет) он расплывался в пятно
   и наезжал на соседние абзацы. !important оставлен — заливку абзаца могут
   перебивать стили состояний, а «где сейчас читают» обязано быть видно всегда. */
.tts-cur {
  background: var(--gold-soft) !important; border-radius: 6px;
  transition: background .3s ease;
}

/* иллюстрации глав */
.chapter-art { margin: 30px auto; max-width: 640px; }
.chapter-art img { width: 100%; border-radius: 12px; display: block; box-shadow: var(--shadow-soft); cursor: zoom-in; }

/* лайтбокс: тап по иллюстрации открывает её крупно (закрыть — тап или Esc) */
.img-lightbox {
  position: fixed; inset: 0; z-index: 130;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  padding: 16px; cursor: zoom-out;
}
.img-lightbox img {
  max-width: min(1280px, 96vw); max-height: 92vh;
  border-radius: 10px; box-shadow: var(--shadow);
  animation: ff-fade-up .22s ease both;
}
.img-lightbox-close {
  position: absolute; top: 10px; right: 12px;
  width: var(--tap); height: var(--tap); border-radius: 50%;
  background: color-mix(in oklab, var(--bg) 55%, transparent);
  border: 1px solid var(--line2); color: var(--ink2);
  font-size: 16px; cursor: pointer;
  transition: color .15s, border-color .15s;
}
.img-lightbox-close:active { color: var(--danger); }

/* аудио-бар главы */
.audio-bar {
  display: flex; align-items: center; gap: 12px; margin: 0 0 18px;
  padding: 10px 14px; border-radius: 12px;
  background: var(--raised); border: 1px solid var(--line);
  font-family: var(--font-ui); font-size: 13.5px; color: var(--ink2);
}
.audio-bar audio { flex: 1; height: 34px; }
/* Подпись ПЕРЕНОСИТСЯ (фидбэк владельца 2026-08-21): у живой озвучки она —
   единственное содержимое бара («Читаю главу по ролям по мере написания»,
   по-немецки вдвое длиннее), и с nowrap на 375px текст вылезал за рамку.
   flex 0 1 auto вместо none: рядом с <audio> легаси-аудиоверсии короткая
   «Аудиоверсия» по-прежнему стоит в одну строку, а ужимается только когда
   места действительно нет. overflow-wrap — страховка от слова длиннее строки. */
.audio-bar { min-width: 0; max-width: 100%; box-sizing: border-box; }
.audio-bar .audio-label {
  flex: 0 1 auto; min-width: 0; white-space: normal; overflow-wrap: anywhere;
  line-height: 1.4; font-weight: 600;
}
.audio-running { opacity: .75; }

/* конец главы */
#chapter-end { max-width: 640px; margin: 0 auto; font-family: var(--font-ui); }
.chosen-note {
  margin-top: 44px; padding-top: 20px; border-top: 1px solid var(--line);
  text-align: center; font-family: var(--font-serif); font-style: italic;
  font-size: 14.5px; color: var(--ink2);
}
.choices-block { margin-top: 52px; }
.choices-title {
  display: flex; align-items: center; gap: 16px;
  font-size: 11.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .18em; color: var(--ink2);
  margin-bottom: 22px; white-space: nowrap;
}
.choices-title::before, .choices-title::after { content: ""; flex: 1; height: 1px; background: var(--line2); }
.choices { display: flex; flex-direction: column; gap: 12px; }

/* ---- развилка главы: макет Reader Chapter Interactive (2026-08-21) ----
   Всё ниже ЗАПЕРТО на .choices-block намеренно. Те же .choice-card/.choices
   переиспользуют бюллетень живых сериалов (serials.js) и переигрывание выбора
   (generation.js), а у них другой набор детей: сетка из двух колонок увела бы
   там процент голосов под букву варианта. Общий облик остаётся flex'ом выше,
   а книжную развилку одевает эта секция. */
.choices-block .choices-title { margin-bottom: 10px; }
/* Без строки «Запас сил» между заголовком и кнопками воздух возвращаем сюда. */
.choices-block .choices-title + .choices { margin-top: 12px; }
/* П19 в развилке: полоска остатка сил стоит по центру, ПОД вопросом и НАД
   кнопками — она называет цену нажатия, а не место в книге. Пунктирная линия
   и растянутый трек достались ей от карточки прогресса, где рамка-хозяин была;
   здесь ни того, ни другого нет (см. reader.js::renderChapterEnd). */
.choices-block .resolve-row {
  justify-content: center; margin: 0 0 18px; padding-top: 0; border-top: none;
}
.choices-block .resolve-track { flex: none; width: 90px; min-width: 0; height: 4px; }
.choices-block .resolve-fill { background: var(--gold-text); }
.choices-block .choice-card {
  display: grid; grid-template-columns: 34px minmax(0, 1fr);
  column-gap: 14px; row-gap: 8px; align-items: start;
  border-radius: 14px; padding: 15px 16px 12px;
}
/* Вторая колонка — всё, кроме буквы: авто-раскладка иначе уронила бы подвал
   под кружок, в колонку шириной 34px. */
.choices-block .choice-card > * { grid-column: 2; }
.choices-block .choice-letter { grid-column: 1; grid-row: 1; }
.choices-block .choice-card:active:not(:disabled) { transform: scale(.995); }
/* Подвал карточки: ставка (⚡), счёт мира (⚑), ось хода и подсказки интуиции
   одной строкой под пунктирной чертой. Пустой подвал гасится вместе с чертой —
   у ветки без единой пометки линия ни под чем читалась бы обрывком таблицы. */
.choice-foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding-top: 7px; border-top: 1px dashed var(--line);
  font-family: var(--font-ui);
}
.choice-foot:empty { display: none; }
/* Пометки внутри подвала выстраиваются слева одной группой: отступы-auto им
   достались от старой плоской кнопки, где они держали правый край. Правый край
   теперь держат подсказки интуиции — у них auto своё (см. .rel-hints). */
.choice-foot .choice-cost,
.choice-foot .choice-price,
.choice-foot .choice-axis { margin-left: 0; align-self: center; line-height: 1.2; }
.choice-foot .rel-hints { margin-left: auto; align-self: center; }
/* «✓ ваш выбор» на нажатой карточке: между кликом и экраном генерации висит
   сетевой запрос, и всё это время читатель обязан видеть, ЧТО он нажал. */
.choice-mark {
  margin-left: auto; display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--gold-text);
}
/* Подсказки интуиции и пометка выбора делят правый край: два margin-left:auto
   поделили бы свободное место пополам и увели пометку в середину строки. */
.choice-foot .rel-hints + .choice-mark { margin-left: 6px; }
/* Выбранная ветка остаётся ЖИВОЙ на вид, хотя и заблокирована: серая карточка
   под надписью «ваш выбор» читалась бы как «выбор не принят». */
.choices-block .choice-card.is-chosen:disabled {
  opacity: 1; border-color: var(--gold); background: var(--gold-soft);
}
/* Плашка «пишу следующую главу…»: живёт до перехвата экраном генерации. */
.choice-accepted {
  margin-top: 14px; display: flex; align-items: center; gap: 10px;
  padding: 12px 15px; border: 1px solid var(--line2); border-radius: 12px;
  background: var(--surface); font-size: 13px; color: var(--ink2);
}
.ca-dot {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--gold); animation: tl-pulse 1.4s ease infinite;
}
.ca-spend {
  margin-left: auto; flex: none;
  font-family: var(--font-serif); font-style: italic; color: var(--ink3);
}
@keyframes tl-pulse { 0%, 100% { opacity: .4; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .ca-dot { animation: none; }
}
.choice-card {
  display: flex; align-items: center; gap: 16px;
  width: 100%; text-align: left;
  font-family: var(--font-serif); font-size: 16px; line-height: 1.5; color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line2); border-radius: 12px;
  padding: 16px 20px; min-height: 60px;
  cursor: pointer;
  transition: transform .18s ease, border-color .18s, box-shadow .18s, background .18s;
  -webkit-tap-highlight-color: transparent;
}
.choice-card:active:not(:disabled) {
  transform: scale(.99);
  border-color: var(--gold);
  background: var(--gold-soft);
}
.choice-card:disabled { opacity: .6; cursor: default; }
.choice-letter {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--gold); color: var(--gold-text);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-ui); font-size: 13px; font-weight: 600;
}
/* Ось ветки (⚔ 💬 🔍 🎲) — подсказка о ПРИРОДЕ хода, а не украшение: приглушена
   до фона и не спорит с текстом выбора. margin-left:auto прижимает её к правому
   краю карточки; если рядом стоят подсказки «интуиции» (у них своё auto), край
   держат они, а глиф просто примыкает — два auto-отступа поделили бы свободное
   место пополам и увели глиф в середину строки. */
.choice-axis {
  flex: 0 0 auto; align-self: flex-start; margin-left: auto;
  font-size: 13px; line-height: 1.7; opacity: .55;
}
.choice-card .rel-hints + .choice-axis { margin-left: 6px; }
/* Витрина готовых завязок (решение С6, ТЗ §11.1-11.2; заявка ENTRY №11).
   Стоит РЯДОМ с .choice-card намеренно: витрина повторяет его форму, потому что
   выбор в готовом начале и выбор в главе — одно и то же действие. Токены
   существующие, новых не заводим. */
.starter-grid { display: grid; gap: 12px; grid-template-columns: 1fr; margin-top: 12px; }
@media (min-width: 720px) {
  .starter-grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
}
.starter-card {
  display: grid; grid-template-columns: 48px 1fr; gap: 12px; align-items: start;
  width: 100%; text-align: left; cursor: pointer;
  background: var(--raised); border: 1px solid var(--line2); border-radius: 12px;
  padding: 12px 14px; color: var(--ink);
  transition: transform .18s ease, border-color .18s, background .18s;
}
.starter-card:active { transform: scale(.99); border-color: var(--gold); background: var(--gold-soft); }
/* Обложка — inline-элемент внутри кнопки: без block она имеет нулевую высоту. */
.starter-cover { display: block; width: 48px; height: 64px; border-radius: 6px; background: var(--line2); }
.starter-body { display: grid; gap: 4px; min-width: 0; }
.starter-title { font-family: var(--font-serif); font-size: 16px; font-weight: 600; }
.starter-pitch { color: var(--ink2); font-size: 13.5px; line-height: 1.45; overflow-wrap: anywhere; }
.starter-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
.starter-tag {
  padding: 1px 8px; border-radius: 999px; background: var(--surface);
  border: 1px solid var(--line); color: var(--ink3); font-size: 11px;
}
.starter-reader { margin-top: 18px; display: grid; gap: 12px; }
.starter-h { font-family: var(--font-serif); font-size: 20px; font-weight: 600; }
.starter-prose { font-family: var(--font-serif); font-size: 16px; line-height: 1.65; }
.starter-prose p { margin: 0 0 .8em; }
/* Происхождение печатается ВСЕГДА (§11.2) — отдельной строкой, а не в подвале. */
.starter-origin { font-size: 12.5px; }
.starter-q { font-size: 13px; color: var(--ink3); text-transform: uppercase; letter-spacing: .08em; }
.starter-choices { display: grid; gap: 8px; }
.starter-conflicts {
  display: grid; gap: 10px; padding: 14px;
  background: var(--surface); border: 1px solid var(--line2); border-radius: 12px;
}
.starter-conflict-title { font-weight: 600; }
.starter-conflicts ul { margin: 0; padding-left: 1.1em; display: grid; gap: 4px; }
.starter-conflict-k { color: var(--ink3); margin-right: 6px; }
.starter-resolutions, .starter-actions { display: flex; flex-wrap: wrap; gap: 8px; }
/* UX-04: подпись оценки — отдельной строкой под полосой, а не вплотную к ней. */
.gen-progress-note { margin-top: 6px; color: var(--ink3); font-size: 11.5px; }
/* Секция «Вкус чтения» в читалке (решение С10, ТЗ §12.1; заявка TASTE Т-7).
   Разметка стоит на готовых классах вкладки «Память» (`mem-group`, `tone-block`,
   `note`), здесь — только свои якоря. Три кнопки на ось это ГРУППА
   переключателей: высота не меньше 44px (тач-цель), перенос по ширине — иначе
   девять осей дают горизонтальный скролл на 360px (§21.4, запрет прямой). */
.book-taste .taste-axis {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 6px 0; border-top: 1px solid var(--line);
}
.book-taste .taste-axis:first-child { border-top: 0; }
.taste-name { flex: 1 1 140px; min-width: 0; color: var(--ink2); font-size: 13.5px; }
.taste-btns { display: flex; flex: 0 0 auto; gap: 6px; }
.taste-btn {
  min-height: 44px; padding: 0 12px; border-radius: 10px;
  background: var(--raised); border: 1px solid var(--line2); color: var(--ink2);
  font-size: 13px; cursor: pointer;
  transition: border-color .18s, background .18s, color .18s;
}
.taste-btn.on { background: var(--gold-soft); border-color: var(--gold); color: var(--gold-text); }
.taste-btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.taste-scope { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 13.5px; }
.next-block { margin-top: 42px; text-align: center; }
.next-block .primary { width: 100%; max-width: 360px; }
/* Книга без развилки: полоска запаса сил стоит НАД «Следующей главой» и играет
   ту же роль, что в развилке, — называет цену шага. Значит и вид тот же, что у
   .choices-block .resolve-row: по центру и без верхней пунктирной линии —
   рамки-хозяина здесь нет, и линия читалась бы обрывком таблицы. */
.next-block .resolve-row {
  justify-content: center; margin: 0 0 18px; padding-top: 0; border-top: none;
}
.next-block .resolve-track { flex: none; width: 90px; min-width: 0; height: 4px; }
.next-block .resolve-fill { background: var(--gold-text); }

/* Нижняя навигация готовой главы: книжная разделительная линия и крупные
   тач-таргеты, чтобы продолжить чтение можно было без возврата к шапке. */
.chapter-bottom-nav {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px;
  margin-top: 46px; padding-top: 22px;
  border-top: 1px solid var(--line);
}
.chapter-bottom-btn {
  width: 100%; min-height: 52px;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  line-height: 1.3; text-align: center;
  -webkit-tap-highlight-color: transparent;
}
.chapter-bottom-prev:disabled { opacity: .32; }
@media (max-width: 419px) {
  .chapter-bottom-nav { gap: 8px; }
  .chapter-bottom-btn { min-height: 54px; padding-inline: 10px; font-size: 12.5px; }
}

/* свой вариант читателя */
.custom-choice {
  margin-top: 22px;
  padding: 16px 18px;
  border: 1px dashed var(--line2);
  border-radius: 12px;
  background: transparent;
}
.custom-title { font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink2); margin-bottom: 10px; }
.custom-choice textarea {
  width: 100%; box-sizing: border-box; resize: none;
  font-family: var(--font-ui); font-size: 14px; min-height: 64px;
  padding: 12px 14px; line-height: 1.5;
  background: var(--surface); border: 1px solid var(--line2); border-radius: 10px;
}
.custom-choice button {
  margin-top: 10px; width: 100%; min-height: 44px;
  background: transparent; border: 1px solid var(--gold); border-radius: 10px;
  color: var(--gold-text); font-family: var(--font-ui);
  font-size: 13.5px; font-weight: 600; cursor: pointer;
  transition: background .15s;
}
.custom-choice button:active { background: var(--gold-soft); }
.custom-choice button:disabled { opacity: .55; }
@media (min-width: 720px) { .custom-choice button { width: auto; padding: 12px 18px; } }
/* В развилке главы кнопка остаётся во всю ширину и на десктопе (макет Reader
   Chapter Interactive 2026-08-21): колонка чтения там узкая, и «свой вариант»
   обязан весить столько же, сколько готовые ветки над ним, — иначе читается
   как второстепенная приписка. У переигрывания выбора правило прежнее. */
.choices-block .custom-choice button { width: 100%; }

/* финал */
.finale { margin-top: 52px; text-align: center; }
.finale .vignette { margin: 0 0 14px; }
.finale .fin {
  font-family: var(--font-serif);
  font-size: 21px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .4em; padding-left: .4em; margin-bottom: 28px;
}
.finale .primary { width: 100%; max-width: 360px; }

/* переигрывание выбора */
.replay-block { text-align: center; margin: 18px 0 0; }
.replay-btn { font-size: 13px; opacity: .85; }
.replay-panel { max-width: 640px; margin: 20px auto 0; }
.replay-warn {
  background: var(--gold-soft); border: 1px solid var(--gold); border-radius: 12px;
  padding: 12px 14px; font-size: 13.5px; color: var(--ink); margin: 0 0 14px;
}
.replay-choice.is-current { border-color: var(--gold); background: var(--gold-soft); }

/* обсуждение (комментарии) */
.comments-block {
  max-width: 640px; margin: 40px auto 0; text-align: left;
  font-family: var(--font-ui);
  border-top: 1px solid var(--line); padding-top: 26px;
}
.comments-head {
  display: flex; align-items: center; gap: 10px;
  font-size: 11.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .18em; color: var(--ink2); margin: 0 0 16px;
}
.comments-head .count {
  border: 1px solid var(--line2); border-radius: 999px; padding: 1px 9px;
  font-size: 11px; letter-spacing: 0; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}
.comments-head::after { content: ""; flex: 1; height: 1px; background: var(--line); }
/* компоуз по макету: аватар слева, textarea + «Отправить» справа-снизу */
.comment-compose { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 18px; }
.compose-main { flex: 1; min-width: 0; }
.comment-compose textarea {
  width: 100%; min-height: 48px; resize: none; padding: 12px 14px;
  border: 1px solid var(--line2); border-radius: 12px;
  font-family: var(--font-ui); font-size: 14px;
  background: var(--raised); color: var(--ink);
}
.compose-send-row { display: flex; justify-content: flex-end; margin-top: 8px; }
.comment-send {
  padding: 9px 18px; border-radius: 10px; min-height: 40px;
  background: transparent; border: 1px solid var(--gold);
  color: var(--gold-text); font-family: var(--font-ui);
  font-size: 13px; font-weight: 600; cursor: pointer;
  transition: background .15s;
  /* nowrap — из-за обратного отсчёта лимита («Подождите 43 с»,
     social.js::startSendCooldown): подпись там длиннее «Отправить» и на узком
     экране рвалась бы на две строки, дёргая высоту всего блока компоуза. */
  white-space: nowrap;
}
.comment-send:active { background: var(--gold-soft); }
.comment-send:disabled { opacity: .5; cursor: default; }
.comment-item { display: flex; gap: 12px; padding: 14px 0; border-top: 1px dashed var(--line); }
.comment-body-wrap { flex: 1; min-width: 0; }
.comment-meta { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.comment-author { font-weight: 600; font-size: 13.5px; }
.comment-date { font-size: 11.5px; color: var(--ink3); }
.comment-del {
  margin-left: auto; font-size: 11.5px; color: var(--ink3);
  background: none; border: none; cursor: pointer; transition: color .15s;
}
.comment-del:active { color: var(--danger); }
.comment-body {
  margin: 5px 0 0; font-family: var(--font-serif);
  font-size: 14.5px; line-height: 1.6; color: var(--ink2);
  white-space: pre-wrap; word-wrap: break-word; overflow-wrap: anywhere;
}
.comments-empty { font-size: 13.5px; color: var(--ink3); font-style: italic; }
.comments-login { font-size: 13px; color: var(--ink3); }

/* отзывы и оценки */
.reviews-empty { font-size: 13.5px; color: var(--ink3); font-style: italic; }
.rate-widget {
  margin: 26px auto 0; max-width: 640px; text-align: center; padding: 16px;
  background: var(--raised); border: 1px solid var(--line); border-radius: 12px;
  font-family: var(--font-ui);
}
.rate-title {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--gold-text); margin-bottom: 8px;
}
/* Звёзды финала — КНОПКИ (P1-10, эпик UXA 2026-08-28; заявка FINALE-SOCIAL §1).
   Тег сменился ради клавиатуры — оценку нельзя было поставить с неё вовсе, —
   а вид меняться не должен, отсюда полный сброс кнопочной отделки.
   letter-spacing больше не работает разделителем: между <button> в разметке
   нет ни одного пробельного узла (иначе он попал бы в доступное имя кнопки),
   расстояние даёт flex-gap. Кольцо фокуса приходит из базового
   button:focus-visible (:147) — именно оно и закрывает половину дефекта. */
.rate-widget .rate-stars {
  display: inline-flex; gap: 6px; justify-content: center;
  font-size: 28px; cursor: pointer; user-select: none;
}
.rate-widget .rate-stars .st {
  background: none; border: 0; margin: 0; padding: 0 2px;
  font: inherit; line-height: 1; color: var(--line2); cursor: pointer;
  transition: color .12s, transform .12s;
}
.rate-widget .rate-stars .st:active { transform: scale(1.12); color: var(--gold); }
/* Тач-зона звезды (заявка FINALE-SOCIAL §1б, принята): 28px по глифу — это
   ~32×30, вдвое меньше var(--tap). Гейт по (hover: none), потому что на
   десктопе прицел мышью точен, а ряд из пяти 44-пиксельных кнопок там просто
   разъехался бы шире карточки. На 320px ряд занимает 5×44 + 4×6 = 244px и
   влезает с запасом. */
@media (hover: none) {
  .rate-widget .rate-stars .st, .rev-rate-stars .st {
    min-width: var(--tap); min-height: var(--tap);
    display: inline-flex; align-items: center; justify-content: center;
  }
}
.rate-widget .rate-stars .st.on { color: var(--gold); }
.rate-caption { font-size: 12px; color: var(--ink3); font-style: italic; margin: 6px 0 0; }
.review-input {
  width: 100%; max-width: 560px; margin: 12px auto 0; display: block; min-height: 74px;
  resize: vertical; padding: 12px 14px; border: 1px solid var(--line2); border-radius: 10px;
  font-family: var(--font-ui); font-size: 14px; background: var(--surface); color: var(--ink);
}
.review-submit {
  margin: 10px auto 0; display: inline-block; padding: 9px 18px; border-radius: 10px;
  border: 1px solid var(--gold); background: transparent; color: var(--gold-text);
  font-family: var(--font-ui); font-size: 13px; font-weight: 600; cursor: pointer;
  transition: background .15s;
}
.review-submit:active { background: var(--gold-soft); }
.review-submit:disabled { opacity: .55; cursor: default; }
#panel-reviews .reviews-empty { padding: 8px 0; }

/* шторка «Отзывы» (по макету design_import/Fansfics Premium.dc.html):
   сводка рейтинга + карточка «ваша оценка» + ряды отзывов через пунктир */
#panel-reviews { font-family: var(--font-ui); }
.rev-summary { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
.rev-avg {
  font-family: var(--font-serif); font-size: 44px; font-weight: 600;
  line-height: 1; color: var(--ink);
}
.rev-summary-meta { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.rev-stars { font-size: 15px; letter-spacing: .12em; white-space: nowrap; }
.rev-stars .st { color: var(--line2); }
.rev-stars .st.on { color: var(--gold); }
.rev-count { font-size: 12.5px; color: var(--ink3); }
.rev-rate-card {
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 14px 16px 12px; margin-bottom: 22px;
}
.rev-rate-kicker {
  font-size: 10.5px; font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--gold-text); margin-bottom: 8px;
}
.rev-rate-stars { display: flex; gap: 2px; }
.rev-rate-stars .st {
  background: none; border: none; cursor: pointer;
  padding: 2px 4px; font-size: 24px; line-height: 1;
  color: var(--line2); transition: color .12s, transform .12s;
}
.rev-rate-stars .st.on { color: var(--gold); }
.rev-rate-stars .st:active { transform: scale(1.12); color: var(--gold); }
.rev-rate-note { font-size: 12px; color: var(--ink3); font-style: italic; margin-top: 4px; }
/* Ряды отзывов по макету Reader Chapter Interactive (2026-08-21): цветной
   кружок-инициал 26px, имя, дата при имени, звёзды у правого края. */
.rev-item { padding: 14px 0; border-bottom: 1px dashed var(--line); }
/* :last-of-type, а не :last-child — под списком может стоять кнопка «Оставить
   отзыв», и пунктир перед ней читался бы как обрыв списка. */
.rev-item:last-of-type { border-bottom: none; }
.rev-item-top { display: flex; align-items: center; gap: 10px; }
.avatar.rev-ava-img { width: 26px; height: 26px; }
/* Инициал в плейсхолдере — интерфейсным шрифтом: в кружке 26px антиква
   Literata читается хуже, чем в крупных аватарах профиля. */
.rev-ava-img.avatar-ph { font-family: var(--font-ui); font-size: 12px; }
.rev-name {
  font-size: 13.5px; font-weight: 600; color: var(--ink);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Пустые звёзды — тот же золотой на прозрачности, а не серая линия: ряд
   читается как одна шкала, а не как два разных значка. */
.rev-stars-inline {
  margin-left: auto; flex: none; font-size: 12px;
  letter-spacing: 2px; white-space: nowrap; color: var(--gold);
}
/* .35 ОСТАВЛЕНА НАМЕРЕННО (эпик 2026-08-28, P1-7, единственное исключение из
   зачистки множителей). Это ПУСТАЯ звезда — дорожка шкалы, а не значение:
   значение несут `.st.on` (--gold, непрозрачные). Тот же приём у всех остальных
   рядов файла, только другим средством: `.stars .st { color: var(--line2) }` —
   1.3:1 к фону. Поднять пустую звезду до 4.5:1 значит сделать «не поставлено»
   таким же заметным, как «поставлено», то есть сломать сам виджет. Разбор — в
   docs/UXA_FIX_2026-08-28/notes/theme_contrast_done.md §«Открытые вопросы». */
.rev-stars-inline .st { color: var(--gold); opacity: .35; }
.rev-stars-inline .st.on { opacity: 1; }
.rev-date { flex: none; font-size: 11.5px; color: var(--ink3); }
.rev-text {
  margin: 8px 0 0; font-size: 13px; line-height: 1.55; color: var(--ink2);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* «Оставить отзыв»: дверь к форме в конце главы (см. reviewsCtaHtml). */
.rev-cta {
  display: block; width: 100%; min-height: var(--tap); margin-top: 8px;
  padding: 10px 16px; border: 1px solid var(--gold); border-radius: 10px;
  background: transparent; color: var(--gold-text);
  font-family: var(--font-ui); font-size: 13.5px; font-weight: 600;
  cursor: pointer; transition: background .15s;
}
.rev-cta:active { background: var(--gold-soft); }

/* ============================================================
   ИГРОВОЙ РЕЖИМ (статус-строка + дневник)
   ============================================================ */
/* Статус-строка по макету Reader Chapter Interactive (2026-08-21): одна
   кнопка-строка над главой, единственная дверь в дневник. top считается от
   высоты шапки (--tb-h), а не от нуля как в макете: в проде над строкой живёт
   sticky-топбар, и «top:0» увело бы её под него. */
.game-bar {
  /* z-index 45, а не прежние 60 (P1-4 эпика 2026-08-28 — «HUD перекрывает меню
     профиля и уведомления»). Разбор стека: .topbar — sticky z-index:50 и потому
     СВОЙ стековый контекст, а .profile-menu/.notif-panel с их 60 — порядок
     ВНУТРИ шапки; наружу обе карточки выходят плоскостью 50. HUD же лежит в
     потоке #view-book, где стекового контекста нет вовсе, и его 60 сравнивались
     с 50 шапки на КОРНЕВОМ уровне — то есть строка здоровья честно накрывала
     обе выпадашки. Тестировщик кликал «Профиль и настройки», а попадал в
     #game-bar и открывал «Дневник».
     45 выбрано так, чтобы HUD остался выше прозы (z-index: auto) и ушёл под
     шапку. Снизу его никто не перекрывает: .lownav/.wiz-nav (60), .tts-bar (65)
     и .gen-mini (64) прибиты к НИЖНЕМУ краю экрана. */
  position: sticky; top: calc(var(--tb-h, 64px) + 6px); z-index: 45;
  display: flex; align-items: center; gap: 9px; width: 100%; max-width: 720px;
  margin: 0 auto 14px; min-height: var(--tap); padding: 8px 12px;
  border: 1px solid var(--line2); border-radius: 12px;
  background: var(--surface); box-shadow: var(--shadow-soft);
  font-family: var(--font-ui); font-size: 13px; color: var(--ink);
  cursor: pointer; text-align: left;
  transition: top .25s ease, border-color .15s ease;
}
/* Нажимаемость показываем золотой рамкой. :active обязателен ОТДЕЛЬНО от
   :hover — на тач-экранах hover не наступает никогда, и без него строка
   выглядела бы мёртвой ровно там, где по ней и тапают. */
.game-bar:hover, .game-bar:active { border-color: var(--gold); }
.game-bar.hidden { display: none; }
.gb-loc { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* «вы здесь» сменилось по ходу чтения — короткая золотая вспышка */
.gb-loc.gb-flash { animation: gb-loc-flash 1.1s ease; }
@keyframes gb-loc-flash { 0%, 45% { color: var(--accent-ink); } 100% { color: inherit; } }
.gb-hp { display: flex; align-items: center; gap: 8px; }
/* Полоса тянется вместе с экраном (clamp вместо брейкпоинта): на узком телефоне
   она не отжимает название локации, на широком не выглядит огрызком.
   flex:none обязателен — общий `.hp-track { flex: 1 }` дневника объявлен НИЖЕ и
   иначе растянул бы полосу статус-строки во всю ширину, съев локацию. */
.gb-hp .hp-track { flex: none; display: inline-block; width: clamp(52px, 8vw, 70px);
  height: 8px; border-radius: 5px; background: var(--line2); overflow: hidden;
  vertical-align: middle; }
.gb-hp .hp-fill { display: block; height: 100%; border-radius: 5px; }
.gb-hp b { font-weight: 600; }
.gb-inv { color: var(--ink2); white-space: nowrap; }
.gb-more { font-size: 12px; color: var(--ink3); white-space: nowrap; }
@media (max-width: 419px) { .gb-more-w { display: none; } }

/* ---- дельта главы (итоги, книжной виньеткой) ----
   Макет Reader Chapter Interactive (2026-08-21): игровые итоги и реакции
   персонажей — ОДИН столбец, а не две плашки подряд. Линейки достаются только
   первой строке: они отбивают прозу от итогов, а разлинованный каждый факт
   получал бы вес заголовка. Первая строка не всегда про здоровье (в книге без
   игрового режима первой окажется строка отношений) — поэтому правило висит
   на :first-child, а не на классе. */
.chapter-delta {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  max-width: 640px; margin: 28px auto 0;
  font-family: var(--font-serif); font-style: italic; font-size: 14.5px;
  line-height: 1.5; color: var(--ink2); text-align: center;
}
.cd-line { max-width: 100%; }
.chapter-delta > .cd-line:first-child {
  display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%;
}
/* .cd-in нужен именно как обёртка: без него текст строки сам стал бы
   flex-элементами и разъехался по gap между словами. */
.chapter-delta > .cd-line:first-child > .cd-in { flex: 0 1 auto; min-width: 0; }
.chapter-delta > .cd-line:first-child::before,
.chapter-delta > .cd-line:first-child::after {
  content: ""; flex: 1; min-width: 20px; height: 1px; background: var(--line2);
}
.cd-line b { font-weight: 500; color: var(--ink); }
/* Знак несёт цвет: прибавка зелёная, убыль красная (специфичность выше
   .cd-line b — иначе имя вещи перекрасило бы и число). */
.cd-val.up { color: var(--state-good-ink); }
.cd-val.down { color: var(--state-danger-ink); }
.cd-dot { opacity: .5; margin: 0 .4em; }
.cd-level { font-size: 13.5px; }
.gd-dot { opacity: .5; }

/* дневник (панель game) — по макету Premium: секции с линейкой, карта в рамке */
/* Макет Reader Chapter Interactive (2026-08-21): расстояние до соседа задаёт
   ТОЛЬКО gap строки-хозяина (12px у здоровья, 10px у заголовка секции). Свой
   margin-right у метки складывался с ним и разводил метку и полосу на 20px —
   строка теряла вид одной шкалы. */
.hud-label { font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--ink3); flex: none; }
.hud-sec { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.hud-rule { flex: 1; height: 1px; background: var(--line); }
.hud-hp { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 220px; }
.hud-hp b { font-size: 13px; font-variant-numeric: tabular-nums; }
.hp-track { flex: 1; height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.hp-fill { height: 100%; border-radius: 999px; transition: width .8s ease; }
.hp-good { background: linear-gradient(90deg, color-mix(in oklab, var(--good) 80%, #000), var(--good)); }
.hp-mid { background: linear-gradient(90deg, color-mix(in oklab, var(--gold) 80%, #000), var(--gold)); }
.hp-low { background: linear-gradient(90deg, color-mix(in oklab, var(--danger) 80%, #000), var(--danger)); }
.hud-loc { margin: 14px 0 0; font-family: var(--font-serif); font-style: italic;
  font-size: 15px; color: var(--ink); }
/* Название места выделено весом, но не жирным: у курсивной Literata 700
   выглядит криком посреди тихой строки (макет — 600). */
.hud-loc b { font-weight: 600; }
.hud-inv { margin-top: 18px; }
.inv-item { display: inline-block; margin: 2px 6px 4px 0; padding: 7px 13px;
  border-radius: 999px; background: var(--raised); border: 1px solid var(--line2);
  font-size: 12.5px; color: var(--ink2); }
.hud-map-sec, .hud-log-sec { margin-top: 18px; }
/* У журнала заголовок прижат к записям сильнее остальных секций: первая строка
   и так открывается своим padding-top 10px, и общие 10px давали двойной провал
   (макет Reader Chapter Interactive, 2026-08-21). */
.hud-log-sec .hud-sec { margin-bottom: 4px; }
.hud-log-row { display: flex; gap: 10px; padding: 10px 0;
  border-bottom: 1px dashed var(--line); font-size: 13.5px; line-height: 1.5; color: var(--ink2); }
/* Последняя запись без линии — иначе журнал заканчивается обрывком таблицы. */
.hud-log-row:last-child { border-bottom: none; }
.hud-log-ch { flex: none; min-width: 34px; font-size: 11px; color: var(--ink3);
  padding-top: 2px; font-variant-numeric: tabular-nums; }
/* Знак дельты здоровья несёт цвет — тот же язык, что у итогов главы
   (.cd-val выше): урон красным, прибавка зелёной, ноль остаётся серым. */
.hud-log-hp.up { color: var(--state-good-ink); }
.hud-log-hp.down { color: var(--state-danger-ink); }
/* ============================================================
   КАРТА ПУТИ v3 (ревью 2026-07-28): врезка + полный экран + жанровый скин
   ============================================================
   Б-4: старая врезка держала viewBox шириной 476 при max-height 260px и
   width:100% — в шторке (≤ min(430px,96vw)) натуральный масштаб был НЕДОСТИЖИМ,
   и обещанные комментарием 10.5px рендерились как ~6.9px, а на телефоне при 24
   узлах — как ~4.3px. Теперь svg отдаётся в натуральную величину (1 юнит = 1px),
   а узкий экран честно скроллит карту по горизонтали.

   Скин задаётся ОДНИМ токеном-семенем --map-tint; всё остальное считается от
   тем-токенов (--raised/--ink3/--gold) через color-mix, поэтому один и тот же
   набор правил работает и в светлой, и в тёмной теме, и в самих элементах нет
   ни одного хардкод-цвета. Фактуры — только CSS-градиенты: ни внешних файлов,
   ни base64-картинок (CSP self + вес страницы). Иконографии конкретных
   вселенных, гербов и шрифтов-имитаций здесь нет и быть не должно (юр-риск). */
.hud-map, .map-full {
  --map-tint: var(--gold);
  --map-bg: color-mix(in oklab, var(--map-tint) 8%, var(--raised));
  --map-frame: color-mix(in oklab, var(--map-tint) 24%, var(--line));
  --map-edge: color-mix(in oklab, var(--map-tint) 30%, var(--ink3));
  --map-node: var(--gold);
  --map-hub: var(--gold-text);
  --map-cur: var(--danger);
  --map-lbl: var(--ink2);
  --map-halo: var(--map-bg);
  --map-tex: none;
}
/* пергамент: тёплая сепия, косая штриховка тушью и виньетка по краю */
.hud-map[data-skin="parchment"], .map-full[data-skin="parchment"] {
  --map-tint: #A8752E;
  --map-bg: color-mix(in oklab, var(--map-tint) 13%, var(--raised));
  --map-tex:
    repeating-linear-gradient(58deg, color-mix(in oklab, var(--map-tint) 10%, transparent) 0 1px, transparent 1px 7px),
    radial-gradient(125% 95% at 50% 0%, transparent 42%, color-mix(in oklab, var(--map-tint) 17%, transparent) 100%);
}
/* тушь-акварель: мягкие размывы, почти без каркаса */
.hud-map[data-skin="ink"], .map-full[data-skin="ink"] {
  --map-tint: #5B6E86;
  --map-bg: color-mix(in oklab, var(--map-tint) 10%, var(--raised));
  --map-edge: color-mix(in oklab, var(--map-tint) 46%, var(--ink3));
  --map-tex:
    radial-gradient(58% 46% at 22% 26%, color-mix(in oklab, var(--map-tint) 20%, transparent) 0%, transparent 70%),
    radial-gradient(52% 52% at 78% 68%, color-mix(in oklab, var(--map-tint) 15%, transparent) 0%, transparent 72%),
    radial-gradient(40% 42% at 55% 92%, color-mix(in oklab, var(--map-tint) 12%, transparent) 0%, transparent 70%);
}
/* звёздная карта: тёмно-синее с золотом; «звёзды» — фиксированные точки,
   позиции захардкожены НАРОЧНО (разброс обязан быть одинаковым при каждой
   перерисовке — то же правило, что для раскладки узлов) */
.hud-map[data-skin="stars"], .map-full[data-skin="stars"] {
  --map-tint: #2E3E78;
  --map-bg: color-mix(in oklab, var(--map-tint) 26%, var(--raised));
  --map-edge: color-mix(in oklab, var(--gold) 38%, var(--ink3));
  --map-tex:
    radial-gradient(1.3px 1.3px at 11% 21%, color-mix(in oklab, var(--ink) 70%, transparent) 50%, transparent 100%),
    radial-gradient(1px 1px at 27% 66%, color-mix(in oklab, var(--ink) 55%, transparent) 50%, transparent 100%),
    radial-gradient(1.6px 1.6px at 44% 13%, color-mix(in oklab, var(--gold) 75%, transparent) 50%, transparent 100%),
    radial-gradient(1px 1px at 61% 48%, color-mix(in oklab, var(--ink) 50%, transparent) 50%, transparent 100%),
    radial-gradient(1.3px 1.3px at 79% 29%, color-mix(in oklab, var(--ink) 65%, transparent) 50%, transparent 100%),
    radial-gradient(1px 1px at 88% 74%, color-mix(in oklab, var(--gold) 60%, transparent) 50%, transparent 100%),
    radial-gradient(1.2px 1.2px at 35% 88%, color-mix(in oklab, var(--ink) 55%, transparent) 50%, transparent 100%),
    linear-gradient(180deg, color-mix(in oklab, var(--map-tint) 20%, transparent), transparent 62%);
}
/* схема подземелья: чертёж по сетке */
.hud-map[data-skin="dungeon"], .map-full[data-skin="dungeon"] {
  --map-tint: #3E6B72;
  --map-bg: color-mix(in oklab, var(--map-tint) 12%, var(--raised));
  --map-edge: color-mix(in oklab, var(--map-tint) 58%, var(--ink3));
  --map-node: color-mix(in oklab, var(--map-tint) 42%, var(--gold));
  --map-tex:
    repeating-linear-gradient(0deg, color-mix(in oklab, var(--map-tint) 17%, transparent) 0 1px, transparent 1px 16px),
    repeating-linear-gradient(90deg, color-mix(in oklab, var(--map-tint) 17%, transparent) 0 1px, transparent 1px 16px);
}

/* --- врезка в шторке (превью) --- */
.hud-map { position: relative; border-radius: 12px; padding: 10px 8px 6px;
  background-color: var(--map-bg); background-image: var(--map-tex);
  border: 1px solid var(--map-frame); }
.hud-map-scroll { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; }
.hud-map .map-svg { display: block; margin: 0 auto; max-width: none; cursor: zoom-in; }
.hud-map-expand {
  position: absolute; top: 6px; right: 6px;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 4px 11px; cursor: pointer;
  border: 1px solid var(--map-frame); border-radius: 999px;
  background: color-mix(in oklab, var(--map-bg) 55%, var(--surface));
  color: var(--ink2); font-family: var(--font-ui); font-size: 11.5px;
  transition: color .15s, border-color .15s;
}
@media (max-width: 419px) { .hud-map-expand-w { display: none; } }
/* О-7: пустой visited больше не убирает секцию — конструктор обещал карту */
.hud-map-empty { margin: 0; padding: 24px 14px; text-align: center;
  border: 1px dashed var(--line2); border-radius: 12px; color: var(--ink3);
  font-family: var(--font-serif); font-style: italic; font-size: 14px; }
/* П39: места, не влезшие в кап раскладки. Строка-счётчик под врезкой, список
   разворачивается тапом — чипы те же, что у рюкзака (.inv-item). */
.hud-map-rest { margin-top: 8px; }
.hud-map-rest-btn { display: inline-flex; align-items: center; gap: 5px;
  min-height: 32px; padding: 4px 11px; cursor: pointer;
  border: 1px solid var(--line2); border-radius: 999px; background: none;
  color: var(--ink3); font-family: var(--font-ui); font-size: 11.5px; }
.hud-map-rest-btn:active { color: var(--ink); border-color: var(--ink3); }
.hud-map-rest-list { margin-top: 6px; }

/* --- сама графика (общая для врезки и полного экрана) --- */
.map-svg .map-dot { fill: var(--map-node); }
/* хаб — узел с наибольшей степенью: база героя узнаётся без подписи */
.map-svg .map-hub { fill: var(--map-hub);
  stroke: color-mix(in oklab, var(--map-hub) 26%, transparent); stroke-width: 5; }
/* «Вы здесь» — красная точка с мягким ореолом (просьба владельца), следует за
   чтением внутри главы (_syncMapCurrent из updateRouteLoc); правило ПОСЛЕ хаба,
   иначе база-и-она-же-текущая точка осталась бы золотой */
.map-svg .map-cur { fill: var(--map-cur);
  stroke: color-mix(in oklab, var(--map-cur) 32%, transparent); stroke-width: 8; }
.map-svg .map-edge { fill: none; stroke: var(--map-edge); stroke-dasharray: 3 5;
  stroke-linecap: round; opacity: .9; }
/* Возврат в уже известное место — золотой пунктир НАЗАД к старому узлу */
.map-svg .map-return { stroke: var(--gold); stroke-dasharray: 2 4; opacity: .95; }
/* Ореол под подписью: узлы и подписи не сливаются */
.map-svg .map-lbl { font-family: var(--font-ui); font-size: 11px; fill: var(--map-lbl);
  paint-order: stroke; stroke: var(--map-halo); stroke-width: 3px; stroke-linejoin: round; }
.map-svg .map-step { font-family: var(--font-ui); font-size: 8.5px; fill: var(--ink3);
  opacity: .85; font-variant-numeric: tabular-nums;
  paint-order: stroke; stroke: var(--map-halo); stroke-width: 2px; stroke-linejoin: round; }
/* единственное место на карте — не точка в углу, а композиция из двух колец */
.map-svg .map-solo { fill: none; stroke: var(--map-edge); stroke-dasharray: 2 5; opacity: .5; }
.map-svg .map-solo2 { opacity: .26; }

/* --- полноэкранный вид --- */
.map-full { position: fixed; inset: 0; z-index: 135; display: flex; flex-direction: column;
  background-color: var(--map-bg); background-image: var(--map-tex); }
.map-full.hidden { display: none !important; }
.map-full-head { flex: none; display: flex; align-items: center; gap: 12px;
  padding: 8px 10px; padding-top: max(8px, env(safe-area-inset-top));
  border-bottom: 1px solid var(--map-frame);
  background: color-mix(in oklab, var(--surface) 62%, transparent); }
.map-full-head h3 { flex: 1; margin: 0; font-family: var(--font-ui); font-size: 11.5px;
  font-weight: 600; text-transform: uppercase; letter-spacing: .14em; color: var(--ink2); }
.map-full-tools { display: flex; gap: 6px; }
.map-tool { width: var(--tap); height: var(--tap); border-radius: 10px; cursor: pointer;
  border: 1px solid var(--map-frame); background: transparent; color: var(--ink2);
  font-family: var(--font-ui); font-size: 17px; line-height: 1;
  transition: color .15s, border-color .15s; }
.map-tool-close { color: var(--ink); }
.map-full-canvas { flex: 1; min-height: 0; overflow: hidden; touch-action: none; }
.map-full-canvas .map-svg { display: block; width: 100%; height: 100%; }
/* Узлы в полном экране — кнопки с клавиатуры (tabindex=0). Общий контур
   [tabindex]:focus-visible из базовых правил НЕ гасим — он и есть индикатор
   фокуса; кольцо на самой точке добавляем сверху, чтобы было видно и на карте. */
.map-full .map-node { cursor: pointer; }
.map-full .map-node:focus-visible .map-dot { stroke: var(--gold); stroke-width: 4; }
/* карточка узла (О-4): глава визита, ран/находок, сделанный выбор */
.map-node-card { position: absolute; left: 50%; transform: translateX(-50%); z-index: 2;
  bottom: calc(62px + env(safe-area-inset-bottom));
  width: min(430px, calc(100% - 22px)); max-height: 46vh; overflow: auto;
  padding: 12px 14px 14px; border-radius: 14px;
  border: 1px solid var(--map-frame); background: var(--surface);
  box-shadow: var(--shadow); font-size: 13.5px; color: var(--ink2); }
.map-node-card.hidden { display: none !important; }
.mnc-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.mnc-name { flex: 1; font-family: var(--font-serif); font-size: 16px; color: var(--ink); }
.mnc-close { flex: none; width: 32px; height: 32px; border: none; border-radius: 8px;
  background: none; color: var(--ink3); font-size: 14px; cursor: pointer; }
.mnc-row { padding: 5px 0; border-bottom: 1px dashed var(--line); line-height: 1.5; }
.mnc-row:last-of-type { border-bottom: none; }
.mnc-k { color: var(--ink3); }
.mnc-go { margin-top: 10px; width: 100%; min-height: var(--tap); cursor: pointer;
  border: 1px solid var(--gold); border-radius: 10px; background: var(--gold-soft);
  color: var(--gold-text); font-family: var(--font-ui); font-size: 13.5px; }
/* легенда (О-5): красная точка, золотой пунктир, размер узла и цифра порядка
   больше нигде не подписаны — читатель узнавал их только угадыванием */
.map-legend { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px;
  padding: 8px 14px; padding-bottom: calc(8px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--map-frame); font-size: 11.5px; color: var(--ink3);
  background: color-mix(in oklab, var(--surface) 62%, transparent); }
.map-legend-t { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .14em; }
.mlg { display: inline-flex; align-items: center; gap: 7px; }
.mlg-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--map-node); }
.mlg-cur { background: var(--map-cur);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--map-cur) 30%, transparent); }
.mlg-hub { width: 15px; height: 15px; background: var(--map-hub); }
.mlg-line { width: 26px; height: 0; border-top: 2px dashed var(--gold); }
.mlg-num { padding: 0 5px; border: 1px solid var(--line2); border-radius: 4px;
  font-size: 10px; font-variant-numeric: tabular-nums; }
@media (hover: hover) {
  .hud-map-expand:hover { color: var(--gold-text); border-color: var(--gold); }
  .map-tool:hover { color: var(--gold-text); border-color: var(--gold); }
  .mnc-close:hover { color: var(--danger); }
  .mnc-go:hover { filter: brightness(1.15); }
  .map-full .map-node:hover .map-dot { stroke: color-mix(in oklab, var(--gold) 45%, transparent); stroke-width: 6; }
}

/* ============================================================
   ПАНЕЛЬ (drawer): bottom sheet ↔ боковая
   ============================================================ */
/* Затемнение — только у нижнего листа (медиа-запрос ниже гасит его на
   десктопе): боковая панель страницу не перекрывает и гасить её нечем.
   z-index 69 — ровно под шторкой и НАД аудио-пилюлей (65) по макету
   Reader Chapter Interactive (2026-08-21): открытый дневник забирает
   плоскость целиком, иначе плеер торчит поверх затемнения. */
.drawer-backdrop {
  position: fixed; inset: 0; z-index: 69;
  background: var(--scrim);
  opacity: 0; pointer-events: none;
  transition: opacity .3s;
}
.drawer-backdrop.open { opacity: 1; pointer-events: auto; }
/* Макет Reader Chapter Interactive (2026-08-21): лист не растягивается во всю
   ширину планшета — max-width + margin auto держат его колонкой по центру.
   Тень направлена ВВЕРХ: у прижатого к низу листа обычная тень уходит за
   край экрана и лист «прилипает» к странице без единой границы света. */
.drawer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 70;
  max-width: 560px; margin: 0 auto;
  max-height: 82vh; max-height: 82dvh;
  background: var(--surface);
  border: 1px solid var(--line2); border-bottom: none;
  border-radius: 18px 18px 0 0;
  box-shadow: var(--shadow-up);
  transform: translateY(105%);
  transition: transform .3s ease;
  display: flex; flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
}
.drawer.open { transform: translateY(0); }
.drawer-grip { flex: none; display: flex; justify-content: center; padding: 10px 0 4px; cursor: pointer; }
.drawer-grip span { width: 46px; height: 5px; border-radius: 999px; background: var(--line2); }
.drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  border-bottom: 1px solid var(--line); padding: 0 8px 0 14px; flex: none;
}
/* Полоса вкладок со своим горизонтальным скроллом (ревью 2026-08-05: пятая
   вкладка «Мир» выталкивала ✕ за экран на 375px, «Отзывы» обрезались, а
   доскроллить было нельзя — flex без min-width:0 не сжимается). */
.drawer-tabs {
  display: flex; gap: 2px;
  flex: 1 1 auto; min-width: 0; overflow-x: auto;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.drawer-tabs::-webkit-scrollbar { display: none; }
.drawer-tab {
  font-family: var(--font-ui); font-size: 11.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .14em;
  background: none; border: none; cursor: pointer; color: var(--ink3);
  /* 13px 11px — кегль макета; min-height остаётся тач-таргетом: у вкладки
     всего одна строка в 11.5px, и без него ряд не добирает до 44px. */
  padding: 13px 11px; min-height: var(--tap);
  flex: none; white-space: nowrap;
  transition: color .15s, box-shadow .15s;
  -webkit-tap-highlight-color: transparent;
}
#drawer-close { flex: none; }
.drawer-tab.active { color: var(--gold-text); box-shadow: inset 0 -2px 0 var(--gold); }
#drawer-close {
  background: none; border: none; cursor: pointer;
  font-size: 15px; color: var(--ink3);
  /* Ровный квадрат тач-таргета (44×44 по макету): паддингами он расползался
     шире ряда вкладок и сдвигал ✕ от края. */
  width: var(--tap); height: var(--tap); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color .15s;
}
#drawer-close:active { color: var(--danger); }
.drawer-panel { display: none; overflow-y: auto; flex: 1; -webkit-overflow-scrolling: touch; }
.drawer-panel.active { display: block; }
@media (min-width: 900px) {
  .drawer-backdrop { display: none; }
  .drawer {
    left: auto; top: 0; right: 0; bottom: 0;
    /* max-width/margin нижнего листа снимаем явно: иначе панель, прижатая к
       правому краю, ужалась бы до 560px и повисла бы посреди экрана. */
    width: min(430px, 96vw); max-width: none; margin: 0; max-height: none;
    border-radius: 0; border: none; border-left: 1px solid var(--line2);
    box-shadow: var(--shadow-left);
    transform: translateX(105%);
    padding-bottom: 0;
  }
  .drawer.open { transform: translateX(0); }
  .drawer-grip { display: none; }
}
/* Hover-состояния шторки — отдельным @media (hover: hover) по правилу дома:
   на телефоне :hover залипает после тапа, и вкладка оставалась бы подсвеченной
   уже после переключения на соседнюю. */
@media (hover: hover) {
  .drawer-tab:hover { color: var(--ink2); }
  .drawer-tab.active:hover { color: var(--gold-text); }
  #drawer-close:hover { color: var(--danger); }
}

.pad { padding: 20px 22px calc(34px + env(safe-area-inset-bottom)); }
@media (min-width: 900px) { .pad { padding: 20px 22px 34px; } }
.pad h4 {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--ink3); margin: 22px 0 8px;
  display: flex; align-items: center; gap: 10px;
}
.panel-sub { font-size: 13px; color: var(--ink2); margin-bottom: 14px; }

/* память истории */
.digest {
  background: var(--raised); border: 1px solid var(--line); border-radius: 12px;
  padding: 15px 17px; font-size: 13.5px; margin-bottom: 8px;
}
.prose-small p { margin: 0 0 8px; text-indent: 0; font-family: var(--font-ui);
  font-size: 13.5px; line-height: 1.6; color: var(--ink2); }
.prose-small b, .prose-small strong { color: var(--ink); }
.prose-small h1, .prose-small h2, .prose-small h3, .prose-small h4 {
  font-family: var(--font-ui); font-size: 10.5px; font-weight: 600;
  margin: 12px 0 6px; text-transform: uppercase; letter-spacing: .14em; color: var(--gold-text);
}
.prose-small h1::after { content: none; }
.prose-small .vignette { margin: 10px 0; font-size: 13px; }
.mem-group h4 .count {
  display: inline-block;
  border: 1px solid var(--line2);
  border-radius: 999px; padding: 1px 8px; font-size: 10.5px; color: var(--ink3);
}
.mem-group h4::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.mem-entry { border-bottom: 1px dashed var(--line); padding: 12px 0; font-size: 13.5px; }
.mem-entry.inactive { opacity: .55; }
.mem-entry .mem-head { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }
.mem-entry .mem-head b { font-size: 14px; font-weight: 600; }
.mem-entry .stars { color: var(--gold); font-size: 11px; letter-spacing: .1em; white-space: nowrap; }
.mem-entry p { margin: 5px 0 4px; line-height: 1.55; color: var(--ink2); }
.mem-meta { font-size: 11.5px; color: var(--ink3); }

/* ---- вкладка «Память» по макету Reader Chapter Interactive (2026-08-21) ----
   ВСЁ под #panel-memory намеренно. .pad h4, .mem-group и .count делят разметку
   со вкладкой «Мир» (ls-world-sec), с комментариями и с мастером, а золотой
   заголовок без хвостовой линии — язык именно «Памяти»; глобальная правка
   перекрасила бы соседние вкладки, которых эта волна не касается. */
/* gap 4px — бейдж-счётчик стоит вплотную к заголовку, как в макете: общий
   gap:10px у .pad h4 держит хвостовую линию, которой у «Памяти» больше нет. */
#panel-memory .pad h4 { color: var(--gold-text); margin: 22px 0 6px; gap: 4px; }
/* Первая секция стоит вплотную к верху панели: 22px сверху плюс padding .pad
   давали бы провал в пол-экрана на телефоне. */
#panel-memory .pad > :first-child h4,
#panel-memory .pad > h4:first-child { margin-top: 0; }
#panel-memory .mem-group h4::after { content: none; }
#panel-memory .mem-group h4 .count {
  border-radius: 6px; padding: 0 6px; font-size: 10px;
}
#panel-memory .ls-echo-row { padding: 9px 0; }
#panel-memory .mem-entry { padding: 10px 0; }
/* Номер главы — цифра в колонке: пропорциональные цифры дёргают строки списка. */
#panel-memory .mem-meta { font-variant-numeric: tabular-nums; }
/* Последняя строка секции живёт без разделителя: пунктир под ней читается как
   обрыв списка, а следующий заголовок уже сам отбивает секцию. */
#panel-memory .ls-echo .ls-echo-row:last-child,
#panel-memory .ls-scars .ls-scar:last-child,
#panel-memory .mem-group .mem-entry:last-child { border-bottom: none; }
#panel-memory .digest { border-color: var(--line2); }

/* --- §4.1 «Память»: статус записи и история изменений ---
   Разметку ставит web/src/reader.js (memEntryHtml). Бейдж
   статуса заменил прежнюю подпись «· заменена» в mem-meta: подпись сливалась
   с номером главы, и снятая запись читалась как активная из другой главы. */
#panel-memory .mem-badge {
  display: inline-block; margin-left: 8px; padding: 0 6px; border-radius: 6px;
  border: 1px solid var(--line2); font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink3); vertical-align: 1px;
}
#panel-memory .mem-badge-resolved { color: var(--gold-text); border-color: var(--gold-text); }
/* «заменена записью #N» — переход к живому преемнику, поэтому ссылка, а не текст. */
#panel-memory .mem-goto {
  margin-left: 8px; padding: 0; border: 0; background: transparent;
  font: inherit; color: var(--gold-text); text-decoration: underline dotted;
  cursor: pointer;
}
/* Короткая подсветка цели перехода: без неё прокрутка выглядит как «ничего
   не произошло». Класс снимается таймером в reader.js::memScrollToEntry. */
#panel-memory .mem-flash { animation: mem-flash 1.2s ease-out; }
@keyframes mem-flash {
  from { background: color-mix(in srgb, var(--gold) 22%, transparent); }
  to { background: transparent; }
}
#panel-memory .mem-history-toggle { margin: 14px 0 4px; }
#panel-memory .mem-history-toggle .ghost { width: 100%; }
#panel-memory .mem-history-toggle .count { margin-left: 8px; }

/* Блок «Возможные дубли» (карточка предложения, радиокнопки выбора канонной,
   кнопки «Объединить»/«Оставить отдельно»/«Удалить») снят 2026-08-27 вместе
   с самой правкой памяти — фидбэк владельца, см. шапку §4.1 в reader.js.
   Вкладка читающая, и мёртвым правилам здесь делать нечего. */

/* «Тон книги» — единственная рабочая ручка владельца в шторке (П34), и она
   остаётся ползунком целиком. Макет рисует тон тихой строкой «Мрачность
   [полоса] NN%», поэтому крупный золотой счётчик мастера здесь ужимается до
   подписи — но только в шторке: те же .tone-* классы держат тон-слайдеры
   мастера, где счётчик обязан быть крупным. */
#panel-memory .book-tone .tone-name {
  font-size: 12.5px; font-weight: 500;
  text-transform: none; letter-spacing: 0; color: var(--ink2);
}
#panel-memory .book-tone .tone-top b {
  font-family: var(--font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--ink3); min-width: 0;
}
#panel-memory .book-tone .tone-pct {
  font-size: 12.5px; color: var(--ink3); font-variant-numeric: tabular-nums;
}
#panel-memory .book-tone .tone-anchors { font-size: 10.5px; opacity: .8; }
#panel-memory .book-tone .intensity-desc { font-size: 12.5px; margin-top: 8px; }
#panel-memory .book-tone .note { display: block; margin-top: 6px; line-height: 1.45; }

/* ============================================================
   ОВЕРЛЕЙ ГЕНЕРАЦИИ
   ============================================================ */
.overlay {
  position: fixed; inset: 0; z-index: 100;
  background: var(--scrim);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
}
.overlay-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
  box-shadow: var(--shadow);
  padding: 34px 24px; text-align: center; max-width: 440px; width: 100%;
}
@media (min-width: 720px) { .overlay-card { padding: 46px 54px; max-width: 480px; } }
/* орнамент оверлея: глиф-лист Turnleaves — страница, застывшая в перевороте */
.leaf-wrap { margin-bottom: 16px; }
.leaf-wrap .leaf {
  width: 30px; height: 30px;
  background: linear-gradient(135deg, var(--gold-text), var(--gold) 60%);
  box-shadow: 0 10px 26px rgba(199,163,96,.25);
}
.leaf-wrap::after {
  content: ""; display: block; margin: 12px auto 0;
  height: 2px; width: 110px;
  background: linear-gradient(90deg, var(--gold), transparent);
  animation: inkflow 1.5s ease-in-out infinite;
}
@keyframes inkflow { 0% { transform: scaleX(.15); } 50% { transform: scaleX(1); } 100% { transform: scaleX(.15); } }
.stage { font-family: var(--font-serif); font-style: italic; font-size: 20px; margin-bottom: 4px; }
.stage-sub { font-size: 12.5px; letter-spacing: .08em; color: var(--ink3); min-height: 18px; }
.dots { display: flex; justify-content: center; gap: 8px; margin-top: 16px; }
.dots span { width: 7px; height: 7px; border-radius: 50%; background: var(--gold); animation: dot 1.2s infinite ease-in-out; }
.dots span:nth-child(2) { animation-delay: .2s; }
.dots span:nth-child(3) { animation-delay: .4s; }
@keyframes dot { 0%, 100% { opacity: .25; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-4px); } }

.overlay-error { text-align: center; }
.err-title { font-size: 18px; font-weight: 600; color: var(--danger); margin-bottom: 10px; }
#overlay-error-text {
  background: color-mix(in oklab, var(--danger) 12%, transparent);
  border: 1px solid var(--danger); border-radius: 10px;
  color: var(--ink); font-size: 13.5px; padding: 12px 14px; margin: 0 0 18px;
  text-align: left; white-space: pre-wrap;
  max-height: 40vh; overflow-y: auto;
}
.err-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/* карточки-факты фандома (экран ожидания) */
.fact-card {
  margin: 26px auto 0; max-width: 480px; padding: 18px 22px;
  border-radius: 14px; background: var(--raised);
  border: 1px solid var(--line); text-align: center; position: relative; overflow: hidden;
}
.fact-card::after { content: ""; position: absolute; top: 0; left: -45%; width: 40%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.06), transparent);
  animation: fact-shimmer 5.5s ease-in-out infinite; will-change: transform; }
@keyframes fact-shimmer { 0%, 60% { transform: translateX(0); } 100% { transform: translateX(460%); } }
.fact-kicker { display: block; font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink3); margin-bottom: 8px; }
.fact-kicker .leaf { width: 9px; height: 9px; margin-right: 7px; }
.fact-text { display: block; font-family: var(--font-serif); font-style: italic;
  font-size: 14.5px; line-height: 1.55; color: var(--ink2); }
.fact-anim .fact-text { animation: fact-in .9s ease both; }
@keyframes fact-in { from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); } }

/* ------------ загрузочный экран главы (staging, дизайн Premium) */
.gen-staging {
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  padding: 34px 0 8px; text-align: center;
  animation: ff-fade-up .45s ease both;
}
.gen-stage-title { font-family: var(--font-serif); font-style: italic; font-size: 21px; }
.gen-progress { width: min(440px, 100%); }
.gen-progress-track { height: 5px; border-radius: 999px; background: var(--line); overflow: hidden; }
.gen-progress-fill {
  display: block; height: 100%; border-radius: 999px; background: var(--gold);
  transition: width .6s ease;
}
.gen-steps { display: flex; justify-content: space-between; margin-top: 10px; padding: 0 2px; }
.gen-step { display: inline-flex; align-items: center; gap: 6px; }
.gen-step-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--line2); transition: background .3s; }
.gen-step.done .gen-step-dot { background: var(--gold); }
.gen-step.current .gen-step-dot { background: var(--gold); animation: leaf-pulse 1.6s ease-in-out infinite; }
.gen-step-label { font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink3); transition: color .3s; }
.gen-step.done .gen-step-label { color: var(--ink2); }
.gen-step.current .gen-step-label { color: var(--gold-text); }
.gen-caption {
  font-size: 12.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink3); font-variant-numeric: tabular-nums;
}
.gen-log { width: min(440px, 100%); margin: 0 auto; text-align: left; }
.gen-log-line {
  display: flex; align-items: baseline; gap: 10px; padding: 6px 2px;
  font-size: 13px; line-height: 1.5; color: var(--ink3);
}
/* Анимируется только СВЕЖАЯ строка: журнал перерисовывается целиком, и общая
   анимация перезапускала бы fade-up у всех видимых строк при каждом переходе. */
.gen-log-line.current { color: var(--ink2); animation: ff-fade-up .35s ease both; }
.gen-log-icon { flex: none; font-size: 12px; color: var(--ink3); }
.gen-log-line.current .gen-log-icon { color: var(--gold-text); }
.gen-teaser {
  width: min(440px, 100%); box-sizing: border-box; text-align: left;
  border: 1px solid var(--gold); background: var(--gold-soft);
  border-radius: 14px; padding: 15px 19px;
  animation: ff-fade-up .4s ease both;
}
.gen-teaser-kicker {
  font-size: 10.5px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase;
  color: var(--gold-text); margin-bottom: 7px;
}
.gen-teaser-text {
  font-family: var(--font-serif); font-style: italic;
  font-size: 14.5px; line-height: 1.6; color: var(--ink);
}
/* C-4 «Ставка читателя»: чипы пари на экране ожидания (ревью 2026-08-05 —
   классы печатались без единого правила и слипались в одну строку). */
.gen-bets { display: flex; flex-direction: column; gap: 10px; }
.gen-bet { display: flex; flex-direction: column; gap: 6px; }
.gen-bet .gen-teaser-text { display: block; }
.gen-bet-actions { display: flex; gap: 8px; }
.gen-bet-btn { font-size: 12.5px; padding: 4px 10px; }
.gen-bet-done { font-size: 12px; color: var(--gold-text); }
.gen-fact-slot { width: min(440px, 100%); }
.gen-fact-slot .fact-card { margin-top: 0; max-width: none; }
.gen-minimize { display: flex; flex-direction: column; align-items: center; gap: 7px; margin-top: 8px; }
.gen-minimize-note { font-size: 12px; color: var(--ink3); font-style: italic; }

/* «Книга создаётся»: существующий reader view с чистой первой генерацией (P5). */
#view-book:not(.gen-first-mode) #gen-first { display: none; }
#view-book.gen-first-mode:has(#gen-first) > :is(.book-bar, #book-tags, #game-bar, #mod-notice),
#view-book.gen-first-mode:has(#gen-first) .running-head { display: none; }
#view-book.gen-first-mode:has(#gen-first) .book-sheet { padding: 0; border: 0; background: transparent; box-shadow: none; }
#view-book.gen-first-mode:has(#gen-first) .prose { max-width: none; }
#view-book.gen-first-mode:has(#gen-first) .gen-staging { padding: 0; gap: 0; }
#view-book.gen-first-mode:has(#gen-first) .gen-staging > :not(#gen-first) { display: none; }
.gen-first {
  display: flex; flex-direction: column; align-items: center; gap: 14px; width: min(600px, 100%);
  margin: 0 auto; padding: clamp(16px, 4vw, 48px) 0 30px; text-align: center;
  font-family: var(--font-ui); font-size: 14px; line-height: 1.5;
}
.gen-first .gen-first-cover { width: 168px; margin: 0 0 8px; }
.gen-first .gen-first-kicker {
  margin: 0; font-size: 11px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: var(--ink3);
}
.gen-first .gen-first-title {
  margin: 0; font-family: var(--font-serif); font-weight: 600; color: var(--ink);
  font-size: clamp(26px, 3.4vw, 36px); line-height: 1.15; letter-spacing: -.01em; overflow-wrap: anywhere;
}
.gen-first .gen-first-title::after { content: none; }
.gen-first .gen-first-line { margin: 0; font-size: 14px; color: var(--ink2); overflow-wrap: anywhere; }
.gen-first .gen-first-status {
  display: flex; align-items: center; gap: 10px; margin: 10px 0 0; padding: 12px 18px;
  border-radius: 999px; border: 1px solid var(--line2); background: transparent;
  font-size: 14px; font-weight: 600; color: var(--gold-text);
}
.gen-first-status .leaf { width: 11px; height: 11px; }
.gen-first .gen-first-note { max-width: 420px; margin: 0; font-size: 13px; line-height: 1.55; color: var(--ink3); }
.gen-first .gen-first-actions { margin: 0; }
.gen-first-buttons { display: flex; flex-wrap: nowrap; justify-content: center; gap: 10px; }
.gen-first-buttons .ghost { min-height: 44px; padding: 10px 16px; font-size: 13.5px; color: var(--ink2); }
.gen-first-buttons #gen-to-library { color: var(--ink); font-weight: 600; }
@media (max-width: 719.9px) {
  .gen-first-buttons .ghost { padding-inline: 10px; }
}
@media (prefers-reduced-motion: reduce) {
  .gen-first-status .leaf-pulse { animation: none; }
}

/* токен-стриминг (chat-bot стиль) */
/* «пишется…» — приглушение ТОКЕНОМ, а не множителем (P1-7): было --ink2 × .7 =
   3.34:1 в светлой теме. --ink3 без множителя даёт 4.76 / 5.10 / 5.41 и на
   экране выглядит так же тихо. */
.thinking { font-style: italic; display: inline-flex; align-items: center; padding: .6em 0; color: var(--ink3); }
.thinking-dots { display: inline-flex; gap: .22em; margin-left: .45em; }
.thinking-dots i { width: .4em; height: .4em; border-radius: 50%; background: var(--gold); display: inline-block; animation: dot 1.2s infinite ease-in-out; }
.thinking-dots i:nth-child(2) { animation-delay: .2s; }
.thinking-dots i:nth-child(3) { animation-delay: .4s; }
.live-stage {
  display: flex; width: fit-content; max-width: 100%;
  margin: 22px auto 2px; padding: 8px 12px;
  border: 1px solid var(--line); border-radius: 999px;
  background: color-mix(in srgb, var(--paper) 88%, var(--gold) 12%);
  font-size: 12.5px; line-height: 1.4;
}
.live-stage .thinking-label { white-space: normal; }
.live-stage .thinking-dots { flex: 0 0 auto; }
.type-cursor { display: inline-block; width: .5ch; height: 1.02em; margin-left: 2px; background: var(--gold); vertical-align: -.15em; animation: type-blink 1s steps(1) infinite; }
@keyframes type-blink { 50% { opacity: 0; } }

/* ============================================================
   МОДАЛЬНЫЕ ОКНА (жалоба, профиль)
   ============================================================ */
.modal {
  position: fixed; inset: 0; z-index: 120;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim);
  padding: 16px;
}
.modal-card {
  width: 100%; max-width: 560px;
  max-height: calc(100vh - 32px); overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: var(--shadow);
  padding: 20px;
}
@media (min-width: 720px) { .modal-card { padding: 24px 28px 26px; } }
.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding-bottom: 14px; margin-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.modal-head h3 { margin: 0; font-family: var(--font-serif); font-size: 20px; font-weight: 600; }
.modal-close {
  flex: none; min-width: var(--tap); min-height: var(--tap);
  border: none; border-radius: 10px;
  background: transparent; color: var(--ink3);
  font-family: var(--font-ui); font-size: 15px; cursor: pointer;
  transition: color .15s;
}
.modal-close:active { color: var(--danger); }
.feedback-context { margin: 0 0 14px; color: var(--ink2); font-size: 13.5px;
  font-family: var(--font-serif); font-style: italic; }
.feedback-card .field + .field { margin-top: 14px; }
.modal-actions {
  display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap;
  margin-top: 18px;
}
.modal-actions .primary, .modal-actions .ghost { min-width: 120px; }
/* «Выйти» прижат к левому краю ряда действий профиля — подальше от «Сохранить» */
.modal-actions #profile-logout { margin-right: auto; min-width: 0; }

/* ---------------------------------------- единый app-диалог (web/src/dialog.js)

   НОВЫХ ЦВЕТОВЫХ ТОКЕНОВ ЗДЕСЬ НЕТ, и это не экономия: гейт контраста
   (tests/e2e/a11y.spec.ts) работает с ПУСТЫМ допуском в обеих темах, а любая
   новая приглушённая краска красит прогон СРАЗУ у всех пакетов эпика. Всё
   собрано из уже проверенных `--ink`/`--ink2`/`--line`, а карточка, кнопки и
   ряд действий — общие классы .modal-card/.primary/.ghost/.modal-actions:
   визуальные соглашения модалок у проекта одни, и вторая их копия разъехалась
   бы с первой на первой же правке. */
.dlg-card { max-width: 460px; }
.dlg-text { margin: 0; color: var(--ink2); font-size: 14.5px; line-height: 1.55; }
/* Последствия необратимого действия (TL-017): список, а не абзац — человек
   читает их по пунктам и обязан видеть, сколько их всего. */
.dlg-details {
  margin: 14px 0 0; padding: 12px 14px 12px 30px;
  border: 1px solid var(--line); border-radius: 12px;
  color: var(--ink2); font-size: 13.5px; line-height: 1.5;
}
.dlg-details li + li { margin-top: 6px; }
.dlg-form { margin-top: 16px; }
.dlg-label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--ink2); }
.dlg-input {
  width: 100%; box-sizing: border-box;
  min-height: 44px;                 /* тач-зона: тот же минимум, что у полей мастера */
  padding: 10px 12px;
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line2); border-radius: 10px;
  font: inherit; font-size: 15px;   /* < 16px на iOS даёт зум при фокусе — держим 15+ */
}
.dlg-input:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
.dlg-area { min-height: 96px; resize: vertical; }
/* Слово подтверждения (typed two-step) — не переводится и потому data-no-i18n;
   моноширинный кегль, чтобы его нельзя было спутать при наборе. */
.dlg-hint { margin: 8px 0 0; color: var(--ink2); font-size: 13px; }

/* ------------------------------------------------------------ профиль и достижения */
.profile-head { display: flex; align-items: center; gap: 18px; margin-bottom: 10px; }
.profile-head .avatar {
  box-shadow: 0 0 0 1px var(--gold), 0 0 0 6px var(--gold-soft);
}
.profile-avatar-actions { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
/* TL-019/TL-021: «Загрузить фото» стала настоящей кнопкой (auth.js:
   ensureProfileAvatarButton) — и обязана иметь полноценную тач-зону, как
   любая кнопка на телефоне. 38px были нижней границей «почти попадаю». */
.profile-avatar-actions .ghost { min-height: var(--tap); font-size: 12.5px; }
#profile-avatar-btn { min-height: var(--tap); font-size: 12.5px; }

/* TL-028: навигация по разделам профиля. Липкая — окно скроллится на три
   тысячи пикселей, и навигация, уехавшая вверх, перестаёт быть навигацией.
   `scroll-margin-top` у заголовков обязателен: без него липкий ряд накрывает
   ровно тот заголовок, к которому только что уехали, — и «перешёл в раздел»
   выглядит как «ничего не произошло». */
.profile-nav {
  position: sticky; top: 0; z-index: 2;
  display: flex; gap: 6px; overflow-x: auto;
  background: var(--surface); padding: 6px 0; margin-bottom: 8px;
}
.profile-nav:empty { display: none; }
.profile-nav button {
  flex: none; min-height: var(--tap); padding: 6px 12px;
  border: 1px solid var(--line2); border-radius: 999px;
  background: transparent; color: var(--ink2);
  font: 600 12.5px var(--font-ui); cursor: pointer; white-space: nowrap;
}
.profile-nav button:hover { background: var(--gold-soft); color: var(--ink); }
.profile-sec-h {
  margin: 18px 0 6px; scroll-margin-top: 56px;
  font-family: var(--font-serif); font-size: 15px; font-weight: 600; color: var(--ink);
}
.profile-sec-h:first-of-type { margin-top: 4px; }
.profile-field { margin-top: 16px; }
.profile-field label {
  display: block; font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink3); margin-bottom: 6px;
}
.profile-field input, .profile-field textarea {
  width: 100%; padding: 12px 14px; font-family: var(--font-ui); font-size: 14.5px;
  border: 1px solid var(--line2); border-radius: 10px;
  background: var(--raised); color: var(--ink);
}
.profile-field textarea { min-height: 90px; resize: vertical; line-height: 1.55; }
#profile-username { font-size: 13.5px; color: var(--ink3); }

/* достижения: витрина */
.profile-ach { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
.ach-level { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 14px; margin-bottom: 10px; }
.ach-level-badge {
  font-family: var(--font-serif); font-weight: 600; font-size: 17px;
}
.ach-level-sub { font-size: 12.5px; color: var(--ink3); font-variant-numeric: tabular-nums; margin-left: auto; }
.ach-bar { height: 3px; border-radius: 2px; background: var(--line2); overflow: hidden; margin-bottom: 16px; }
.ach-bar > span { display: block; height: 100%; border-radius: 2px; background: var(--gold); }
.ach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; }
.ach-badge {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center; padding: 16px 8px 13px; border-radius: 12px;
  border: 1px solid var(--line2); background: var(--raised); position: relative;
}
.ach-badge.locked { border-style: dashed; background: transparent; opacity: .5; filter: grayscale(1); }
.ach-badge .ach-b-ico { font-size: 24px; line-height: 1; }
.ach-badge .ach-b-title { font-size: 11.5px; font-weight: 600; line-height: 1.25; }
.ach-badge .ach-b-prog { font-size: 10.5px; color: var(--ink3); font-variant-numeric: tabular-nums; }
.ach-badge.tier-silver { border-color: var(--line2); }
.ach-badge.tier-gold:not(.locked) {
  border-color: color-mix(in oklab, var(--gold) 60%, transparent);
  background: var(--gold-soft);
}
.ach-badge.tier-gold:not(.locked) .ach-b-ico { color: var(--gold-text); }
.ach-badge.tier-platinum:not(.locked) { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); }

/* ============================================================
   ТОСТЫ
   ============================================================ */
#toasts {
  position: fixed; left: 12px; right: 12px;
  bottom: calc(88px + env(safe-area-inset-bottom));
  z-index: 200;
  display: flex; flex-direction: column; gap: 10px; align-items: stretch;
}
@media (min-width: 720px) {
  #toasts { left: auto; right: 18px; bottom: 18px; max-width: 360px; }
}
/* На телефоне стопка тостов идёт во всю ширину, и аудио-пилюля стоит ровно
   на ней: 14px + --lownav-h снизу плюс собственная высота — это те же 88px,
   с которых начинается первый тост. Пока плеер показывался только во время
   озвучки, случай был редким; с макета Reader Chapter Interactive (2026-08-21)
   пилюля видна всю главу, и сообщение об ошибке было бы наполовину закрыто
   ВСЕГДА. Поднимаем стопку над пилюлей — гейт тот же, что у самой пилюли
   (без нижней навигации она стоит низко и тостам не мешает). */
@media (max-width: 719.9px) {
  body:not(.guest-mode):not(.email-unverified).tts-on #toasts,
  body:not(.guest-mode):not(.email-unverified).audiobar-on #toasts {
    bottom: calc(88px + var(--lownav-h) + env(safe-area-inset-bottom));
  }
}
/* Тот же приём для закреплённой панели мастера (P2 эпика 2026-08-28: «тост
   „Пример загружен“ перекрывает закреплённую основную кнопку мастера»).
   .wiz-nav — position:fixed у нижнего края, высотой ≈68px (кнопка 48 + 20
   паддингов), а #toasts с z-index:200 ложится точно на #wiz-next у правого
   края. Компенсации под неё не было написано ни для одного брейкпоинта.
   Гейт по виду, а не по самой панели: .wiz-nav лежит внутри #view-new и на
   других вкладках просто не отрисована, а класс .active на виде ставит
   state.js::showView. :has() в файле уже используется (шапка модератора). */
/* С редизайна «Конструктор книги» (2026-10-01, PLAN MS-7) панели «Назад /
   Далее» нет, а нижняя поверхность — закреплённый dock (#bk-dock, ≈75px:
   кнопка-итог 52 — мини-корешок 44 по высоте плюс поля, — 20 паддингов и
   рамка; текст в две строки на узком экране — до ≈80px) на ширинах <960:
   смещение 78px (+88 над нижней навигацией <720) оставляет над ним зазор;
   на двух колонках (≥960) внизу нет ничего, и тост садится на своё место.
   Открыт лист «Итог книги» — внизу его закреплённые кнопки создания, и тост
   встаёт над ними (правило ниже). Внутри конструктора тост —
   пилюля ПО ЦЕНТРУ над dock (макет §12), а не карточка в углу: стопка
   растянута по ширине и центрирует детей, сама при этом клики не ловит
   (иначе полоса над dock глотала бы нажатия по его краям). Система тостов
   прежняя (util.toast, 6 с) — меняется только место и форма. */
body:has(#view-new.view.active) #toasts {
  bottom: calc(88px + 78px + env(safe-area-inset-bottom));
  left: 12px; right: 12px; max-width: none;
  align-items: center; pointer-events: none;
}
body:has(#view-new.view.active) #toasts .toast {
  pointer-events: auto; max-width: min(100%, 440px);
  border-radius: 999px; padding: 10px 16px; text-align: center;
  font-size: 13px; line-height: 1.45; box-shadow: var(--shadow);
  background: var(--raised); border-color: var(--line2);
}
@media (min-width: 720px) {
  body:has(#view-new.view.active) #toasts { bottom: calc(18px + 78px); }
}
@media (min-width: 960px) {
  body:has(#view-new.view.active) #toasts { bottom: 24px; }
}
/* На двух колонках колонка панели «Ваша книга» доходит до низа окна, и её
   закреплённые кнопки (и форма пресета под ними) стоят ровно там, где
   садится тост: на 960–1100 длинный тост по центру страницы ложился на
   «Первую главу напишу сам» (повторная проверка B11). Стопка — над лентой:
   от края контента main (max-width 1120, поля clamp(12px, 4vw, 40px)) до
   колонки панели (320) с зазором. */
@media (min-width: 960px) {
  body:has(#view-new.view.active) #toasts {
    --bk-toast-side: calc(max(0px, (100vw - 1120px) / 2) + clamp(12px, 4vw, 40px));
    left: var(--bk-toast-side); right: calc(var(--bk-toast-side) + 320px + 12px);
  }
}
@media (max-width: 959.9px) {
  body:has(#view-new.view.active):has(#bk-sheet:not(.hidden)) #toasts {
    bottom: calc(290px + env(safe-area-inset-bottom));
  }
}
.toast {
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line2); border-radius: 12px;
  padding: 13px 16px; font-size: 13.5px; line-height: 1.45;
  box-shadow: var(--shadow-soft);
  cursor: pointer;
  animation: toast-in .25s ease;
}
.toast.fade { opacity: 0; transition: opacity .4s; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* тост-достижение */
.ach-toast {
  display: flex; align-items: center; gap: 12px;
  border-left: 3px solid var(--gold); max-width: 340px;
}
.ach-toast.tier-silver { border-left-color: var(--ink3); }
.ach-toast.tier-gold { border-left-color: var(--gold); }
/* тост отношений «X это запомнит»: не ошибка и не награда — реплика мира.
   Тот же корпус, что у обычного тоста, плюс тонкая полоса тона (тепло/холод). */
.telltale-toast {
  display: flex; align-items: center; gap: 10px;
  border-left: 3px solid var(--line2); max-width: 340px;
  font-family: var(--font-serif); font-size: 14px;
}
.telltale-toast.pos { border-left-color: var(--gold); }
.telltale-toast.neg { border-left-color: var(--danger); }
.telltale-ico { flex: none; font-size: 15px; line-height: 1; color: var(--ink3); }
.ach-toast .ach-ico { font-size: 28px; line-height: 1; }
.ach-toast .ach-txt { font-size: 13px; line-height: 1.4; }
.ach-toast .ach-xp { color: var(--gold-text); font-weight: 600; }

/* ============================================================
   ЕДИНЫЙ АУДИО-БАР ГЛАВЫ (макет Reader Chapter Interactive, 2026-08-21)
   Пилюля снизу по центру: слева — динамическая зона озвучки (#tts-zone,
   её пишет tts.js::renderTtsBar), за разделителем — статические контролы
   фоновой музыки (#music-toggle/#music-vol, проводка в web/music.js).
   Пилюля видна всё время, пока открыта глава, — в покое это её кнопка ▶.
   ============================================================ */
.tts-bar {
  position: fixed; left: 50%; bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  /* z-index 65 — по макету: ВЫШЕ нижней навигации (.lownav 60) и мини-панели
     генерации (.gen-mini 64), но НИЖЕ затемнения шторки (69) и самой шторки
     (70), а значит и ниже оверлея генерации, модалок, лайтбокса и карты.
     Прежние 900 почти не всплывали, пока пилюля жила только во время озвучки;
     теперь она видна всю главу — и на телефоне висела бы поверх открытого
     дневника, перехватывая тапы по его полосе. */
  z-index: 65; display: flex; align-items: center; gap: 8px;
  max-width: calc(100vw - 24px); padding: 8px 10px; border-radius: 999px;
  background: var(--surface); color: var(--ink2);
  border: 1px solid var(--line2);
  box-shadow: var(--shadow); font-size: 13px; font-family: var(--font-ui);
  /* ВЫСОТА ЗАФИКСИРОВАНА (эпик 2026-08-27 §2.3). Её не было вовсе, и блок
     скакал 58 → 50 (подготовка: вместо кнопки 42px спиннер и ✕ 34px) → 48
     (режим «только музыка»). Для пилюли, приклеенной к низу экрана, это
     прыгающая граница читаемого текста. 58px = самый высокий орган ряда
     (.tts-main 42px) плюс padding 8+8 и рамка — считается вместе с ними по
     глобальному `* { box-sizing: border-box }` (:109). */
  min-height: 58px;
}
.tts-bar.hidden { display: none; }
/* ЕДИНЫЙ БЛОК АУДИОКОНТРОЛОВ. Одна группа на всё, что не переписывается:
   предпрослушивание и громкость голоса, включение/громкость/приглушение/тема
   музыки. Ряд пилюли центрован через translateX(-50%), поэтому смена его
   ширины двигает ВСЕ органы разом — группа даёт им общий якорь и уходит на
   вторую строку целиком, а не рассыпается по одному. */
.audio-ctl { display: flex; align-items: center; gap: 8px; flex: none; }
/* Орган, которого сейчас нет, но он вот-вот появится: место под него ЗАНЯТО.
   Кнопка музыки показывается только после того, как семейство резолвится ПО
   СЕТИ (0.3-2 с после отрисовки главы), и раньше её появление сдвигало весь
   ряд на 21px в сторону — ровно та жалоба, что «значок музыки в случайном
   месте». Класс `music-pending` ставит music.js::musicSyncBtn и снимает, как
   только ответ известен: у книги без музыки резерв не висит вечно.
   `display` перебиваем через !important — глобальный `.hidden` тоже !important
   (приём web/src/styles.css: .pw-strength.hidden). */
.audio-ctl.music-pending .music-btn.hidden,
.audio-ctl.music-pending .music-vol.hidden,
.audio-ctl.music-pending .music-theme.hidden { visibility: hidden; pointer-events: none; }
.audio-ctl.music-pending .music-btn.hidden,
.audio-ctl.music-pending .music-theme.hidden { display: inline-flex !important; }
.audio-ctl.music-pending .music-vol.hidden { display: inline-block !important; }
/* Выключенный регулятор виден, но приглушён: «состояние каждого регулятора
   понятно без клика» (§2.3). Раньше ползунок и тумблер ИСЧЕЗАЛИ вместе с
   выключением музыки — это и меняло ширину ряда, и лишало человека ответа на
   вопрос «а какая там громкость».
   Множитель прозрачности здесь остаётся РАДИ ПОЛЗУНКА: у `input[type=range]`
   нет текста, его вид несёт `accent-color`, и приглушить его токеном нечем.
   Кнопка тем из этого правила выписывается (`.tts-bar .music-theme.off`
   ниже) — на глифе множитель ронял контраст ниже 4.5:1. */
.audio-ctl .off { opacity: .45; }
/* Зона озвучки — своя flex-строка внутри пилюли. Отдельный узел нужен НЕ для
   вида: только он переписывается innerHTML'ом, иначе перерисовка плеера уносила
   бы контролы музыки вместе со слушателями, повешенными один раз. */
.tts-zone { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* Пол ширины зоны. Полный плеер (кнопки + «кто · 3 / 41» + скорость + ✕) шире
   покоя на добрую сотню пикселей, и без пола ряд ужимался бы на каждом уходе в
   покой, утаскивая за собой контролы музыки. Это ПОЛ, а не фиксированная
   ширина: состояние отказа («Повторить» / «Пропустить абзац») длиннее любого
   другого, и обрезать его нельзя. На узком экране пол снимается — там ряд и так
   переносится. */
.tts-bar:not(.music-only) .tts-zone { min-width: 300px; }
.tts-sep { flex: none; width: 1px; height: 22px; background: var(--line2); }
/* Режим «только музыка» (tts.js::ttsBarSync): вне главы — в библиотеке,
   каталоге, на экране генерации — пилюля остаётся ради одного: выключить или
   приглушить фон, который иначе нечем было достать (фидбэк владельца
   2026-08-21). Зона озвучки и разделитель убираются, пилюля ужимается до ♪ и
   громкости; контролы музыки при этом те же узлы — music.js их не теряет. */
.tts-bar.music-only .tts-zone, .tts-bar.music-only .tts-sep { display: none; }
/* КОМПАКТНАЯ ПИЛЮЛЯ — ОСОЗНАННЫЙ ВТОРОЙ РАЗМЕР, а не побочный эффект (§2.3).
   Вне главы (библиотека, каталог, экран генерации) плееру нечего показывать, и
   растягивать пустой блок до 58px значило бы вешать над нижней навигацией
   полосу ни о чём. Поэтому здесь СВОЯ, тоже зафиксированная высота — и в
   пределах каждого режима она не меняется ни от чего.
   Органы главы (громкость голоса, тема) в этом режиме прячем: тема без книги
   не выбирается, а голоса вне главы нет. Остаются ровно два: включатель и
   громкость музыки. */
.tts-bar.music-only { padding: 5px 8px; min-height: 46px; }
.tts-bar.music-only .tts-vol,
.tts-bar.music-only .music-theme { display: none !important; }
/* У книги нет музыки (music.js прячет кнопку) — разделитель повис бы без пары.
   Прячем ВИДИМОСТЬЮ, а не display: пока идёт резолв, место под группу музыки
   зарезервировано, и снятая ширина черты дала бы ещё один сдвиг ряда — второй
   после появления самой кнопки. :has() не поддержан — правило просто выпадет,
   и черта останется видимой: это косметика, а не поломка. */
#tts-bar:has(#music-toggle.hidden) .tts-sep { visibility: hidden; }
/* Низ читалки: пилюля перекрыла бы концовку главы и развилку. Класс ставит
   tts.js::ttsBarSync — он же решает видимость; .tts-on оставлен для случая,
   когда звук идёт, а человек ушёл из книги. */
body.tts-on #view-book, body.audiobar-on #view-book { padding-bottom: 120px; }
/* Автопрокрутка озвучки (tts.js::ttsHighlight → scrollIntoView block:"nearest"):
   без запаса читаемый абзац прилегает нижним краем к низу вьюпорта — ровно под
   пилюлю. scroll-margin-bottom = высота пилюли (58px) + её отступ (14px) +
   воздух: под пилюлей оказывается СЛЕДУЮЩИЙ абзац, а не читаемый. Сверху —
   зеркальный запас под липкие топбар и статус-строку для прыжков назад (⏮).
   Только .tts-on: прокруткой управляет одна ttsHighlight, в покое пилюля
   ничего не двигает. На телефоне пилюля стоит выше нижней навигации — добавка
   var(--lownav-h) в мобильном блоке ниже (~«ниже .lownav»). */
body.tts-on #prose [data-p] {
  scroll-margin-bottom: calc(88px + env(safe-area-inset-bottom, 0px));
  scroll-margin-top: calc(var(--tb-h, 64px) + 60px);
}
.tts-bar button {
  border: 1px solid var(--line2); background: transparent; color: var(--ink2);
  width: 34px; height: 34px; border-radius: 50%; cursor: pointer; font-size: 14px;
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; padding: 0;
  transition: border-color .15s, color .15s, background .15s;
}
.tts-bar button:active { border-color: var(--gold); color: var(--gold-text); }
/* ТАЧ-ЗОНА до var(--tap). Круги остаются 34px — их в ряду полдюжины, и раздуть
   их некуда, — но зона нажатия растягивается ПО ВЕРТИКАЛИ внутри самой пилюли
   (её высота это позволяет). Здесь, на десктопе, она и остаётся вертикальной:
   прицел мышью точен, а ряд из 44-пиксельных зон только разъехался бы шире
   капсулы. Рост ВШИРЬ живёт ниже, под гейтом (hover: none), — вместе с
   поднятым шагом ряда, без которого соседние зоны перехватывали бы чужие тапы
   (⏮/⏭/♪ стоят в 6-8px друг от друга). */
.tts-bar button { position: relative; }
.tts-bar button::after {
  content: ""; position: absolute; left: 0; right: 0;
  top: 50%; height: var(--tap); transform: translateY(-50%);
}
/* Покой: перематывать и ускорять нечего, пока карта абзацев не готова. */
.tts-bar button[disabled] { opacity: .4; cursor: default; }
.tts-bar button[disabled]:active { border-color: var(--line2); color: var(--ink2); }
.tts-bar .tts-main {
  width: 42px; height: 42px;
  background: var(--gold-soft); color: var(--gold-text); border-color: var(--gold);
}
/* кнопка скорости: текст «1,25×» в круг не влезает — авто-ширина, скруглённый прямоугольник */
.tts-bar .tts-rate {
  width: auto; min-width: 44px; border-radius: 16px; padding: 0 10px;
  font-size: 12px; font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.tts-bar .tts-rate:active { border-color: var(--gold); color: var(--gold-text); }
/* «К голосу» (QA3, пункт 7b). Геометрию берёт у .tts-rate — это такой же
   текстовый орган ряда, — но читается ЗОВОМ, а не справкой: кнопка появляется
   только тогда, когда подсветка уехала из вьюпорта, и обязана быть заметнее
   соседей. Золото здесь — то же, что у главной кнопки плеера (.tts-main), и
   ничего нового в палитру не вводит. `white-space: nowrap` — потому что в
   переводах строка длиннее русской («Back to voice», «Retour à la voix»), а
   перенос внутри пилюли сделал бы ряд двухстрочным. */
.tts-bar .tts-follow {
  border-color: var(--gold); background: var(--gold-soft); color: var(--gold-text);
  white-space: nowrap;
}
.tts-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.tts-msg {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 600; font-size: 12.5px;
}
.tts-track { display: block; padding: 4px 0; cursor: pointer; }
.tts-rail { display: block; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
.tts-track-fill {
  display: block; height: 100%; border-radius: 2px;
  background: var(--gold); transition: width .3s ease;
}
.tts-spin {
  width: 16px; height: 16px; flex: none; border-radius: 50%;
  border: 2px solid var(--line2); border-top-color: var(--gold);
  animation: tts-spin .8s linear infinite;
}
@keyframes tts-spin { to { transform: rotate(360deg); } }
.audio-bar .tts-spin { margin-right: 2px; }
/* Внутри пилюли подпись «кто · 3 / 41» не имеет права распирать ряд: она тут
   спутник кнопок, а не главный орган. Голый .tts-info (flex:1) остаётся как был —
   его носит ещё и .audio-bar готовой аудиоверсии главы. */
.tts-bar .tts-info { flex: 0 1 auto; max-width: 230px; }

/* ---- контролы фоновой музыки внутри пилюли (переехали из .book-actions) ----
   Кнопка приходит сюда с классом .music-btn, за который цепляется music.js
   (.playing/.hidden) и общий блок BGM ниже по файлу: он даёт золотой фон и цвет
   включённой музыке, а рамку — здешнее правило (в панели книги рамок не было
   вовсе). Геометрию круга задаёт .tts-bar button; min-height обнуляем на случай,
   если кнопке вернут класс .ba-btn — его 40px растянули бы круг в овал. */
.tts-bar .music-btn { min-height: 0; gap: 0; font-size: 14px; }
.tts-bar .music-btn .ba-glyph { font-size: 14px; }
/* Подпись «Музыка» в ряду глифов не нужна, но узел живёт: его прячет CSS,
   а не JS — music.js разметку не трогает. */
.tts-bar .music-btn .ba-label { display: none; }
.tts-bar .music-btn.playing { border-color: var(--gold); }
/* «Готово к игре» (BUG-BGM, music.js::musicUiState): музыку включали в прошлый
   раз, но после перезагрузки она сама НЕ стартует — ждёт клика по этой кнопке.
   Пунктир золотом и золотой глиф отличают её и от выключенной (серая рамка),
   и от играющей (сплошное золото на подложке): «нажми — продолжится». */
.tts-bar .music-btn.ready { border-color: var(--gold); border-style: dashed; color: var(--gold-text); }
/* Видимое в кнопке — один глиф ♪, а глобальное `.ba-glyph { opacity: .65 }`
   снимает только «играет» (блок BGM ниже). Без сброса золото «готово» на
   светлой теме гасло до 2.8:1 — бледнее выключенного ♪ (--ink2 · .65 = 3.1:1),
   и новая подсказка читалась хуже старой. Непрозрачный глиф — 5.9:1 / 9.2:1. */
.tts-bar .music-btn.ready .ba-glyph { opacity: 1; }
.tts-bar .music-vol { width: 76px; flex: none; margin: 0 2px 0 0; align-self: center; }
/* Тумблера «приглушать музыку под голосом» в РЯДУ больше нет (фидбэк владельца
   2026-08-27): круглая кнопка «⇩» без подписи не объясняла ничего. Настройка
   живёт подписанной строкой в списке тем — см. `.mt-duck` ниже. */
/* Выбор музыкальной темы (эпик §2.1). Тот же круг, что у остальных органов;
   выбранная вручную тема читается золотом — «состояние понятно без клика».

   ПРИГЛУШЕНИЕ ГЛИФА НЕСЁТ ТОКЕН, А НЕ МНОЖИТЕЛЬ ПРОЗРАЧНОСТИ (заявка
   THEME-CONTRAST §1 к эпику P1-7). Здесь стояло `opacity: .6`, и оно роняло
   глиф ≡ до 2.79:1 в светлой теме и 3.38:1 в тёмной при кегле 15px, а `.off`
   ниже — до 2.09:1 и 2.46:1. Это НЕ `[disabled]`: кнопка нажимается в любом
   состоянии, поэтому послабление WCAG 1.4.3 для неактивных компонентов на неё
   не распространяется — соседнее `.tts-bar button[disabled] { opacity: .4 }`
   законно, а эти два правила были нет. Множитель ещё и делает контраст
   заложником того, какая поверхность окажется снизу: починка палитры в токенах
   до таких мест просто не доезжает — ровно поэтому весь остальной файл от них
   вычищен (P1-7, 13 правил).
   В покое это --ink2 (7.11:1) — тот же цвет, что у соседей по ряду
   (`.tts-bar button`): кнопка тем такой же орган, как ⏮/⏭/♪, а не подпись при
   них. Токен назван ЯВНО, хотя и наследовался бы: все три состояния кнопки
   читаются тогда в одном месте, и правка цвета ряда не утащит за собой шаг
   лестницы молча. */
.tts-bar .music-theme { font-size: 15px; line-height: 1; color: var(--ink2); }
.tts-bar .music-theme.on {
  opacity: 1; border-color: var(--gold);
  background: var(--gold-soft); color: var(--gold-text);
}
.tts-bar .music-theme.hidden { display: none; }
/* ПРИГЛУШЕНИЕ ВЫКЛЮЧЕННЫХ ОРГАНОВ — ЗДЕСЬ, А НЕ ТОЛЬКО В `.audio-ctl .off`.
   Общее правило (:2955) до кнопки тем не доходило вовсе: у него специфичность
   (0,2,0), а у `.tts-bar .music-theme.on` — (0,3,0), то есть выбранная вручную
   тема горела золотом при ВЫКЛЮЧЕННОЙ музыке — рядом с приглушённым до .45
   ползунком громкости. Ряд читался ровно наоборот обещанию §2.3 «состояние
   каждого регулятора понятно без клика». Правило обязано стоять ПОСЛЕ блоков
   `.on` — специфичность у них та же, и спор решает порядок. Возвращаем ровно
   базу `.tts-bar button` (прозрачный фон, рамка --line2, цвет --ink2), а не
   выдумываем третий вид. Ползунку громкости хватает общего правила: своего
   `opacity` у него нет, и перебивать нечего. */
.tts-bar .music-theme.off {
  /* `opacity: 1` — это ОСОЗНАННЫЙ ОТКАЗ ОТ ОБЩЕГО `.audio-ctl .off`, а не
     украшение. Там множитель нужен ПОЛЗУНКУ громкости: у `input[type=range]`
     текста нет вовсе, его вид несёт `accent-color`, и токеном его не
     приглушить. У кнопки же множитель ронял глиф до 2.09:1 — приглушение
     берёт на себя --ink3 (5.10:1 в светлой теме, 5.42:1 в тёмной).
     Ряд читается в ТРИ шага, и все три выше 4.5:1: покой — --ink2, музыка
     выключена — --ink3, выбранная вручную тема — золото. */
  opacity: 1; border-color: var(--line2);
  background: transparent; color: var(--ink3);
}
/* Громкость ГОЛОСА. Уже музыкальной на 8px намеренно: ползунков в ряду два, и
   одинаковые сбивали бы с толку — какой из них какой. Отличает их и соседство:
   голосовой стоит первым в группе, музыкальный — сразу за ♪. */
.tts-bar .tts-vol { width: 68px; flex: none; margin: 0; align-self: center; }

/* ---- список музыкальных тем (эпик §2.1) ----
   Всплывает НАД пилюлей и по её центру — то есть там, где стоит палец,
   нажавший ≡. Собственного затемнения нет намеренно: список ничего не
   блокирует, а закрывается кликом мимо (music.js::musicWireUi). */
.music-theme-menu {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(80px + env(safe-area-inset-bottom, 0px));
  z-index: 66;              /* ровно над пилюлей (65) и ниже шторки (69/70) */
  display: flex; flex-direction: column; gap: 2px;
  width: min(320px, calc(100vw - 32px));
  max-height: min(46vh, 360px); overflow-y: auto;
  padding: 6px; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line2);
  box-shadow: var(--shadow); font-family: var(--font-ui);
  -webkit-overflow-scrolling: touch;
}
.music-theme-menu.hidden { display: none; }
.music-theme-menu .mt-item {
  display: block; width: 100%; text-align: left;
  padding: 9px 10px; min-height: var(--tap); border: none; border-radius: 10px;
  background: transparent; color: var(--ink2); font-size: 13px; cursor: pointer;
}
.music-theme-menu .mt-item.on { background: var(--gold-soft); color: var(--gold-text); }
.music-theme-menu .mt-item:active { background: var(--raised); }
/* ТУМБЛЕР АВТОПРИГЛУШЕНИЯ — СТРОКОЙ ВНУТРИ СПИСКА (фидбэк владельца
   2026-08-27). В ряду пилюли он был круглой кнопкой «⇩» без подписи: узнать,
   что она делает, можно было только нажав (title на телефоне не показывается
   вовсе). Здесь у настройки есть место под её собственное имя, а состояние
   несёт глиф ☑/☐ справа — то же «понятно без клика», но словами.

   ПРИЛИПАЕТ К НИЗУ: тем бывает под сорок, список прокручивается, и строка,
   уехавшая за его край, была бы спрятана ровно так же, как раньше. Отрицательный
   `bottom` — на величину внутреннего отступа контейнера (6px): иначе под
   прилипшей строкой оставалась бы полоса фона, сквозь которую видно пункты. */
.music-theme-menu .mt-duck {
  position: sticky; bottom: -6px; z-index: 1;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 4px; border-top: 1px solid var(--line2); border-radius: 0 0 10px 10px;
  background: var(--surface); color: var(--ink3);
}
/* Отметку рисует КАСКАД по `aria-checked`, а не JS: состояние живёт в одном
   месте — в атрибуте, который читает и скринридер. */
.music-theme-menu .mt-duck::after {
  content: "☐"; flex: none; font-size: 15px; line-height: 1;
}
.music-theme-menu .mt-duck[aria-checked="true"] { color: var(--ink2); }
.music-theme-menu .mt-duck[aria-checked="true"]::after {
  content: "☑"; color: var(--gold-text);
}

/* ---- состояния зоны озвучки (заявка FE-TTS, notes/fe_tts_requests.md §1) ----
   Абзац не озвучился: плеер ждёт решения человека («Повторить»/«Пропустить»).
   Спиннера здесь быть не должно — ничего не готовится. */
#tts-zone[data-mode="fail"] .tts-msg { color: var(--warn, #c88); }
#tts-zone[data-mode="fail"] .tts-rate { white-space: nowrap; }
/* Подготовка ВНУТРИ играющего плеера: спиннер стоит перед именем говорящего и
   не сносит органы управления (режим busy для этого не годится — он
   переписывает зону целиком, и кнопки моргали бы на каждом абзаце). */
#tts-zone[data-mode="play"] .tts-msg .tts-spin {
  display: inline-block; margin-inline-end: 6px; vertical-align: -2px;
}

@media (max-width: 559.9px) {
  /* Органов в пилюле много (плеер + музыка: транспорт, скорость, стоп, два
     ползунка, ♪ и ≡), и в штатную геометрию 360px они не влезают даже после
     чистки ряда 2026-08-27 — он вылез бы за скруглённые края. Ужимаем ШАГ и
     подпись скорости, но НЕ сами кнопки: 34px и так на пределе тач-таргета,
     и отнимать у них ещё пару пикселей ради воздуха нельзя. */
  .tts-bar { gap: 6px; padding: 8px; }
  .tts-zone { gap: 6px; }
  .audio-ctl { gap: 6px; }
  .tts-bar .tts-rate { width: auto; min-width: 38px; padding: 0 7px; }
  /* Пол ширины зоны снимаем: здесь ряду и так тесно, а лишние 300px гарантией
     переноса сделали бы двухстрочной даже пилюлю в покое. */
  .tts-bar:not(.music-only) .tts-zone { min-width: 0; }
  /* Ползунки ужимаем оба и одинаково: разница в 8px на 60px ширины уже не
     читается, а десять лишних пикселей на строку — читаются. */
  .tts-bar .tts-vol { width: 56px; }
}
/* ТАЧ-ЗОНА 44×44 ПО ОБЕИМ ОСЯМ (QA4, п.8 отчёта тестировщика №4: «кнопки
   34×34; pseudo-зона расширяет высоту, но не ширину до 44px»).

   Круги остаются 34px — их в ряду полдюжины, и раздувать сами кнопки некуда, —
   но зона нажатия растёт и вширь. Чтобы соседние зоны не перехватывали чужие
   тапы, ШАГ ряда поднят до 10px = 44 − 34: зоны встают ВСТЫК, ни одного
   перекрытого пикселя. Гейт (hover: none) — тот же и по той же причине, что у
   звёзд рейтинга (:2183): мышь целится точно, и на десктопе ряду незачем расти.

   ПОЧЕМУ БЛОК СТОИТ ЗДЕСЬ, а не выше и не ниже. Выше — `@media (max-width:
   559.9px)` с его `gap: 6px`: специфичность у медиа-блоков одинаковая, решает
   порядок, и стоя ПЕРЕД ним этот блок проиграл бы на телефоне, то есть ровно
   там, где он и нужен. Ниже — блок переноса: он объявляет `row-gap: 6px` у
   двухстрочной пилюли, и `gap: 10px` из шортхенда, поставленный ПОСЛЕ него,
   молча раздвинул бы строки. Между ними обе величины получаются те, что
   задуманы: шаг вдоль строки 10px, между строками 6px. */
@media (hover: none) {
  .tts-bar, .tts-zone, .audio-ctl { gap: 10px; }
  .tts-bar button::after {
    left: 50%; right: auto;
    width: max(100%, var(--tap));
    transform: translate(-50%, -50%);
  }
  /* Текстовые органы (скорость, «К голосу») на узком экране ужаты до 38px —
     ниже тач-таргета. `max(100%, …)` их не сжимает: в ru/en-подписях они шире
     44px, и зона просто повторяет их ширину. */
  .tts-bar .tts-rate { min-width: var(--tap); }
}
/* ПОРОГ ПЕРЕНОСА ОПУЩЕН ДЛЯ ТАЧА (QA4, п.8). Диапазон 420-559.9px — тот
   единственный, где ряд стоит В ОДНУ строку и уже впритык (≈361px органов при
   380 доступных на 420px), а поднятый шаг добавляет к нему ещё ~18px: кнопки
   `flex: none`, ужиматься нечему, и ряд вылез бы за скруглённые края. Список
   через запятую — это ИЛИ: правила ниже применяются на любом экране ≤419.9px
   (как и раньше, включая мышь) и на ЛЮБОМ тач-экране ≤559.9px. Цена названа
   вслух: на телефоне 430-560px пилюля становится двухстрочной и в покое.
   Альтернатива (оставить 8px и зону 42×42) была бы тач-таргетом «почти» — а
   «почти 44» это тот же дефект отчёта, только тише. */
@media (max-width: 419.9px), (hover: none) and (max-width: 559.9px) {
  /* ПИЛЮЛЯ ПЕРЕНОСИТСЯ, А НЕ ПРЯЧЕТ ОРГАНЫ (эпик 2026-08-27 §2.4, корень D).

     Было: `body.tts-on .tts-bar .music-vol { display: none }` — на узком
     телефоне во время озвучки ползунок громкости музыки исчезал, и поднять
     приглушённый фон было НЕЧЕМ. При этом ряд всё равно не помещался: органы
     дают под 300px минимума при схлопнутой в ноль подписи «кто · n/m», то есть
     даже без ползунка они наезжали друг на друга и вылезали за скруглённые
     края (та же жалоба, п. 2.3). Две снятые кнопки (♬ и ⇩) этого не отменяют:
     ряду по-прежнему тесно, и перенос остаётся.

     Стало: ряду разрешено переноситься. Контролы музыки стоят в разметке
     последними и уходят на вторую строку целиком — ничего не скрыто, ничего не
     перекрывается, а на экранах, где всё влезает в одну строку (покой, режим
     «только музыка»), перенос не срабатывает вовсе. Скругление ужимаем: у
     двухстрочной пилюли `999px` дало бы капсулу с пустыми боками. */
  .tts-bar { flex-wrap: wrap; justify-content: center; row-gap: 6px; border-radius: 20px; }
  /* Черта-разделитель между строками читалась бы как обрезок — она косметика. */
  .tts-bar .tts-sep { display: none; }
  /* ПЕРЕНОС ДЕТЕРМИНИРОВАННЫЙ (эпик §2.3). Волна 1 разрешила ряду переноситься,
     но переносился он ПО ФАКТУ ширины: в покое пилюля была однострочной, в игре
     — двухстрочной, и высота блока прыгала на каждый старт озвучки. Здесь зона
     занимает всю первую строку ВСЕГДА, группа аудиоконтролов — всю вторую. Два
     ряда в обоих состояниях, высота одна. В `.music-only` зоны нет вовсе, и
     компактная пилюля остаётся однострочной. */
  .tts-bar:not(.music-only) .tts-zone { flex: 1 1 100%; justify-content: center; }
  .tts-bar:not(.music-only) .audio-ctl { flex: 1 1 100%; justify-content: center; }
  .tts-bar:not(.music-only) { min-height: 100px; }
  /* Вторая строка съедает ещё ~48px: без запаса пилюля накрыла бы конец главы
     и развилку. Гейт теперь и `.audiobar-on`: две строки стоят и в покое, то
     есть запас нужен всякий раз, когда пилюля вообще видна. */
  body.tts-on #view-book, body.audiobar-on #view-book { padding-bottom: 168px; }
  body.tts-on #prose [data-p] {
    scroll-margin-bottom: calc(136px + env(safe-area-inset-bottom, 0px));
  }
  /* Список тем поднимается выше двухстрочной пилюли. */
  .music-theme-menu { bottom: calc(122px + env(safe-area-inset-bottom, 0px)); }
}

/* ============================================================
   АВАТАРКИ
   ============================================================ */
.avatar {
  display: inline-block; width: 40px; height: 40px; border-radius: 50%;
  background-size: cover; background-position: center;
  flex: 0 0 auto; vertical-align: middle;
}
.avatar-sm { width: 32px; height: 32px; }
.avatar-lg { width: 76px; height: 76px; }
.avatar-ph {
  display: inline-flex; align-items: center; justify-content: center; color: #fff;
  font-family: var(--font-serif); font-weight: 600;
  background: hsl(var(--ah, 30), 38%, 46%);
}
.avatar-sm.avatar-ph { font-size: 14px; } .avatar-lg.avatar-ph { font-size: 30px; }

/* ============================================================
   AUTH-ГЕЙТ: сплит-экран (бренд-панель + карточка формы)
   ============================================================ */
#auth-gate {
  position: fixed; inset: 0; z-index: 1000;
  overflow-y: auto;
  background: var(--bg);
  display: grid; grid-template-columns: 1fr; align-items: stretch;
}
#auth-gate.hidden { display: none; }
.auth-brand {
  display: none;
  position: relative;
  background: linear-gradient(160deg, #26221B, #171310 70%);
  color: #EFE6D2;
  flex-direction: column; justify-content: center; gap: 26px;
  padding: clamp(32px, 7vw, 88px);
  overflow: hidden;
}
.auth-brand::before {
  content: ""; position: absolute; inset: 14px;
  border: 1px solid rgba(239,230,210,.14); border-radius: 10px; pointer-events: none;
}
/* лого-лакап бренд-панели (§3hero): знак-страница 52×68 + словомарка 46px.
   Панель всегда тёмная (свой градиент) — цвета знака здесь фиксированные,
   как и остальные цвета панели, а не темовые токены. */
.auth-brand .tl-page {
  --tl-w: 52px; --tl-h: 68px; --tl-bw: 2.5px; --tl-r: 5px; --tl-fold: 26px;
  border-color: #EFE6D2;
}
.auth-brand .tl-fold { background: #C7A360; }
/* живой уголок из дизайна: наведение на панель подращивает загиб до 34px */
.auth-brand:hover .tl-fold { width: 34px; height: 34px; }
.auth-brand-logo {
  font-family: var(--font-serif); font-size: clamp(34px, 3.6vw, 46px);
  font-weight: 600; letter-spacing: .005em; line-height: 1.1;
}
.auth-brand-logo .brand-dot { color: #C7A360; }
.auth-brand-tagline {
  margin-top: -10px;
  font-family: var(--font-serif); font-style: italic;
  font-size: 15px; color: rgba(239,230,210,.72);
}
.auth-brand-sub { margin: 0; font-size: 15.5px; line-height: 1.65; color: rgba(239,230,210,.72); max-width: 440px; }
.auth-brand-mark { color: #C7A360; font-size: 13px; letter-spacing: .4em; padding-left: .4em; }
.auth-pane {
  display: flex; align-items: center; justify-content: center;
  padding: clamp(24px, 6vw, 72px) clamp(16px, 5vw, 72px);
}
@media (min-width: 880px) {
  #auth-gate { grid-template-columns: repeat(auto-fit, minmax(min(440px, 100%), 1fr)); }
  .auth-brand { display: flex; }
  .auth-card-head { display: none; }
}
.auth-card { width: min(400px, 100%); }
.auth-card-head { margin-bottom: 18px; }
.auth-card h1 { margin: 0 0 4px; font-family: var(--font-serif); font-size: 24px; font-weight: 600; }
.auth-card h1 .brand-dot { color: var(--gold); font-style: italic; }
.auth-card h1 .tl-page { --tl-w: 18px; --tl-h: 24px; --tl-fold: 9px; margin-right: 10px; vertical-align: -3px; }
.auth-card .auth-card-head .muted { margin: 0; font-family: var(--font-serif); font-style: italic; font-size: 14px; }
.auth-mode {
  display: flex; gap: 6px; margin-bottom: 18px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; padding: 5px;
}
.auth-mode-tab {
  flex: 1; min-width: 0; min-height: 44px;
  border: none; border-radius: 9px; padding: 10px 8px;
  background: transparent; color: var(--ink2);
  font-family: var(--font-ui); font-size: 14px; font-weight: 600;
  cursor: pointer; text-align: center;
  transition: background .15s, color .15s;
}
.auth-mode-tab.active { background: var(--gold); color: var(--on-gold); }
.auth-card label { display: block; margin-bottom: 12px; font-size: 12px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink3); }
.auth-card input {
  width: 100%; box-sizing: border-box; margin-top: 6px;
  padding: 13px 15px; font-size: 15px; font-weight: 400;
  letter-spacing: 0; text-transform: none;
  background: var(--surface); border: 1px solid var(--line2); border-radius: 10px;
}
.auth-card .primary { width: 100%; margin-top: 6px; padding: 14px; font-size: 15.5px; }
.auth-card .linklike { display: block; margin: 8px auto 0; }
.auth-error { color: var(--danger); min-height: 18px; font-size: 13px; margin: 10px 0 0; }
.auth-guest { width: 100%; margin-top: 8px; min-height: 44px; }
.auth-card .fld-hint { display: block; font-size: 12px; font-weight: 400; font-style: normal;
  letter-spacing: 0; text-transform: none;
  color: var(--ink3); margin: 5px 0 0; line-height: 1.4;
  /* резервируем 2 строки: смена текста подсказки при печати (1↔2 строки) больше
     не меняет высоту карточки, иначе форма «дышит» на каждую клавишу. */
  min-height: 2.4em; }
.auth-card .fld-hint.ok { color: var(--good); }
.auth-card .fld-hint.bad { color: var(--danger); }
.auth-card input.bad { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 14%, transparent); }
.auth-card input.ok { border-color: var(--good); }
.pw-strength { display: block; height: 4px; border-radius: 3px; margin-top: 8px;
  background: var(--line2); overflow: hidden; }
/* место под шкалу зарезервировано ВСЕГДА: глобальный .hidden{display:none!important}
   надо перебить (моё правило специфичнее) — держим box (display:block), прячем
   лишь видимость. Появление шкалы при первом символе пароля больше не толкает
   контент вниз, карточка не «дышит». */
.pw-strength.hidden { display: block !important; visibility: hidden; }
.pw-strength .pw-bar { display: block; height: 100%; width: 0; border-radius: 3px;
  transition: width .2s, background .2s; }
.pw-weak .pw-bar { width: 33%; background: var(--danger); }
.pw-mid .pw-bar { width: 66%; background: var(--gold); }
.pw-strong .pw-bar { width: 100%; background: var(--good); }
.consent { margin: 16px 0 6px; display: flex; flex-direction: column; gap: 10px; }
.consent-row {
  display: flex; gap: 10px; align-items: flex-start;
  font-size: 13px; font-weight: 400; letter-spacing: 0; text-transform: none;
  line-height: 1.5; color: var(--ink2); margin: 0; cursor: pointer;
}
.consent-row b { color: var(--ink); }
/* Консент-чекбоксы НЕ должны наследовать стили текстового поля от .auth-card input
   (padding 13px, border, border-radius, background, font-size 15px) — нативный
   чекбокс с таким конфликтующим боксом перерисовывается рывком при клике (:checked/
   :focus) на Windows. Сбрасываем до чистого нативного вида фиксированного размера. */
.consent-row input {
  width: 17px; height: 17px; flex: none;
  min-height: 0; margin: 2px 0 0; padding: 0;
  border: none; border-radius: 0; background: none;
  accent-color: var(--gold);
  transition: none; box-shadow: none;
}
.consent-row a { color: var(--gold-text); }
.auth-legal-foot { font-size: 11.5px; color: var(--ink3); text-align: center; margin: 14px 0 0; }
.auth-legal-foot a { color: var(--gold-text); }

/* ============================================================
   HOVER (только там, где есть курсор)
   ============================================================ */
@media (hover: hover) {
  .tab:hover { color: var(--ink); }
  .tab[data-view="new"]:hover { color: var(--on-gold); filter: brightness(1.08); }
  .theme-toggle:hover { color: var(--gold-text); border-color: var(--gold); }
  /* Шапка v3: пилюля искр и пункты меню профиля. Hover'ы здесь, а не рядом со
     своими правилами — на телефоне :hover залипает после тапа. */
  .credits-chip:hover { border-color: var(--gold); }
  .pm-topup:hover { background: var(--gold-soft); }
  .pm-upgrade:hover { filter: brightness(1.07); }
  .pm-link:hover { color: var(--ink); }
  .primary:hover { filter: brightness(1.08); }
  .ghost:hover { color: var(--ink); border-color: var(--ink3); }
  .linklike:hover { color: var(--ink2); }
  .c-creator-btn:hover { filter: brightness(1.08); }
  /* Персоны (2026-08-11). Все hover'ы фичи живут ЗДЕСЬ, а не рядом со своими
     правилами: на телефоне :hover залипает после тапа, и единственная защита —
     общий блок @media (hover: hover).
     .world-card:hover отсюда УБРАН 2026-08-13: карточку мира переписал блок
     «Каталог v3», и её hover уехал в его собственный @media (hover: hover).
     Дубль был не вреден (объявление совпадало), но два места на один селектор
     кончаются тем, что правят не то. */
  .c-persona-btn:hover { border-color: var(--gold); color: var(--gold-text); }
  .persona-card:hover { border-color: var(--line2); }
  .persona-add:hover { background: var(--gold-soft); border-color: var(--gold); }
  .persona-counts:hover { color: var(--gold-text); }
  .persona-chron-row:hover { border-color: var(--gold); }
  .suggest:hover, .trope-toggle:hover { border-color: var(--gold); color: var(--gold-text); }
  .suggest.on:hover, .trope-toggle.active:hover { color: var(--gold-text); }
  .story-card:hover .book-cover { transform: translateY(-6px); box-shadow: var(--shadow); }
  .card-del:hover { color: var(--danger); }
  .nav-arrow:hover:not(:disabled) { border-color: var(--gold); color: var(--gold-text); }
  .choice-card:hover:not(:disabled) {
    transform: translateY(-2px);
    border-color: var(--gold);
    box-shadow: var(--shadow-soft);
  }
  /* Развилка книги (макет Reader Chapter Interactive 2026-08-21): наведение
     ЗАЛИВАЕТ карточку, а не приподнимает её. Подъём с тенью остаётся у
     бюллетеня сериалов и у переигрывания — там карточек мало и они спорят за
     внимание с фоном; здесь их три подряд, и три летающих плитки на экране
     превращают выбор в карусель. */
  .choices-block .choice-card:hover:not(:disabled) {
    transform: none;
    background: var(--gold-soft);
    box-shadow: none;
  }
  .example-card:hover { border-color: var(--gold); }
  .pack-card:hover { border-color: var(--gold); }
  .slug-display:hover { color: var(--gold-text); border-bottom-color: var(--gold); }
  /* было `opacity: 1` — компенсация приглушения, которого у .chip-x больше нет
     (P1-7). Наведение теперь означает то же, что нажатие: «уберу этот чип». */
  .chip-x:hover { color: var(--danger); }
  #drawer-close:hover { color: var(--danger); }
  .modal-close:hover { color: var(--danger); }
  .step-btn:hover { background: var(--gold-soft); }
  .optional-summary:hover .opt-title { color: var(--ink); }
  .comment-del:hover { color: var(--danger); }
  /* жалоба и mute — не удаление: краснеть не должны (ревью 2026-08-11) */
  .comment-report:hover, .comment-mute:hover, .comment-unmute:hover { color: var(--gold-text); }
  .comment-send:hover, .custom-choice button:hover:not(:disabled),
  .review-submit:hover:not(:disabled) { background: var(--gold-soft); }
  /* .drawer-tab:hover ЖИВЁТ ВЫШЕ (рядом со своим блоком, ~строка 2410): по
     макету наведённая НЕАКТИВНАЯ вкладка светлеет до --ink2, а золото носит
     только активная. Здешний дубль с той же специфичностью стоял позже и
     побеждал порядком — на десктопе неактивная вкладка красилась ровно как
     активная. Дубль снят намеренно, обратно не возвращать. */
  .preset-del:hover { background: var(--danger); color: #fff; }
  #view-book .prose p:hover { background: var(--gold-soft); border-radius: 4px; }
  /* :not([disabled]) — в покое ⏮/⏭/скорость выключены, и подсветка обещала бы
     действие, которого нет (макет Reader Chapter Interactive, 2026-08-21). */
  .tts-bar button:not([disabled]):hover { border-color: var(--gold); color: var(--gold-text); }
  .userchip:hover .avatar { box-shadow: 0 0 0 2px var(--gold); }
  .ghost.danger:hover { color: var(--danger); border-color: var(--danger); }
  .rev-rate-stars .st:hover { color: var(--gold); }
  .rev-cta:hover { background: var(--gold-soft); }
  .write-own-link:hover { color: var(--gold-text); }
  .assist-hide:hover { color: var(--ink2); }
  .card-unfollow:hover { color: var(--gold-text); }
  .create-manual-card:hover:not(:disabled) { border-color: var(--gold); background: var(--gold-soft); }
  .img-lightbox-close:hover { color: var(--danger); border-color: var(--danger); }
  .lib-hero-cover:hover .book-cover { transform: translateY(-5px) rotate(-.4deg); box-shadow: var(--shadow); }
  .add-lib-btn:hover { filter: brightness(1.12); }
  .add-lib-btn.branch-btn:hover,
  .add-lib-btn.on:hover { border-color: var(--gold); color: var(--gold-text); filter: none; }
  .ba-btn:hover:not(:disabled) { color: var(--gold-text); }
  .bm-item:hover:not(:disabled) { background: var(--raised); }
  .bm-item.bm-danger:hover { color: var(--danger); }
  .assist-bar .assist-act:hover:not(:disabled) {
    color: var(--gold-text); border-color: var(--gold); background: var(--gold-soft);
  }
  /* Живые сериалы: только КНОПКИ вариантов — закрытый бюллетень рисуется
     теми же карточками в <div>, и подсветка на них обещала бы клик, которого
     не будет. .choice-card:hover выше их и так не трогает — селектор общий,
     поэтому гасим его явно. */
  .serial-option:not(button):hover { transform: none; border-color: var(--line2); box-shadow: none; }
  .serial-option.is-mine:hover { border-color: var(--gold); }
  .serial-btn:hover { color: var(--gold-text); border-color: var(--gold); }
  .serial-fork-btn:hover { color: var(--gold-text); border-color: var(--gold); background: var(--gold-soft); }
  /* Story Rooms (2026-08-11). Hover'ы фичи живут ЗДЕСЬ, как персоны и миры
     выше: на телефоне :hover залипает после тапа, и общий блок — единственная
     защита. Подсвечиваем только то, что действительно кликается: карточка
     комнаты, кнопка, свободное место и схлопнутый чужой ход. */
  .room-card:hover { border-color: var(--gold); }
  .room-btn:hover { color: var(--gold-text); border-color: var(--gold); }
  .room-book:hover { border-color: var(--gold); background: var(--gold-soft); }
  button.room-seat:hover { border-color: var(--gold); color: var(--gold-text); }
  /* .room-prop-text снят вместе с классом (эпик комнат 2026-08-29): проза хода
     перестала быть кнопкой, стала <p class="room-move-card-text"> с отдельной
     кнопкой раскрытия. Замена — .room-move-card:hover в блоке экрана комнаты
     ниже; второй копии здесь не заводим, она бы спорила с той. */
  /* Уведомления и полка похожих (2026-08-11). Hover'ы обеих фич живут ЗДЕСЬ по
     той же причине, что у персон, миров и комнат выше: на телефоне :hover
     залипает после тапа, и общий блок — единственная защита. */
  .notif-bell:hover { color: var(--gold-text); border-color: var(--gold); }
  .notif-item:hover { background: var(--gold-soft); }
  .notif-all:hover { color: var(--gold-text); }
  .notif-more:hover { color: var(--ink); border-color: var(--ink3); }
  .similar-card:hover .book-cover { transform: translateY(-5px); box-shadow: var(--shadow); }
  .similar-card:hover .card-top h3 { color: var(--gold-text); }
}

/* ============================================================
   МОБИЛЬНЫЕ УТОЧНЕНИЯ
   ============================================================ */
@media (max-width: 719px) {
  /* панель книги на телефоне: группа действий во всю ширину, навигация — своей строкой */
  .book-actions { width: 100%; order: 0; justify-content: space-between; }
  .ba-btn { font-size: 12.5px; padding: 8px 10px; }
  .add-lib-btn { width: 100%; order: -1; }
  .book-menu { max-width: min(280px, 88vw); }
  .book-bar { margin-bottom: 10px; gap: 8px; }
  .chap-nav { order: 5; }
}
@media (max-width: 700px) {
  /* iOS не зумит поля при фокусе, если шрифт ≥ 16px */
  input, select, textarea { font-size: 16px !important; }
}

/* ================= Модерация (Wave 1) ================= */
.view-head-actions { display: flex; gap: 8px; align-items: center; }
.mod-list { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.mod-empty { color: var(--ink2); padding: 24px 4px; text-align: center; }
.mod-case {
  display: grid; gap: 6px; width: 100%; text-align: left; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line2);
  border-radius: 12px; padding: 14px 16px; color: inherit; font: inherit;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.mod-case:hover { border-color: var(--gold); box-shadow: var(--shadow-soft); }
.mod-case-title { font-weight: 600; }
.mod-case-reason { color: var(--ink2); font-size: .9em; }
.mod-badges { display: flex; flex-wrap: wrap; gap: 6px; }
.mod-badge {
  font-size: .72em; padding: 2px 8px; border-radius: 999px;
  background: var(--state-neutral-bg); color: var(--ink2); white-space: nowrap;
}
.mod-badge.s-pending, .mod-badge.risk-medium { background: var(--state-warn-bg); color: var(--state-warn-ink); }
.mod-badge.s-quarantined, .mod-badge.s-escalated,
.mod-badge.risk-high, .mod-badge.risk-critical { background: var(--state-danger-bg); color: var(--state-danger-ink); }
.mod-badge.s-approved { background: var(--state-good-bg); color: var(--state-good-ink); }
.mod-badge.s-hidden, .mod-badge.s-rejected { background: var(--state-neutral-bg); color: var(--state-neutral-ink); }
.mod-badge.cat { background: var(--state-neutral-bg); color: var(--state-neutral-ink); }

/* Курсор — обещание клика, и до TL-013 он врал дважды: `.mod-case` наследовали
   и апелляции, и жалобы на реплики, а кликом там не происходило ничего.
   Открывается теперь ТОЛЬКО заголовок апелляции (вложенная кнопка), карточка
   жалобы на реплику не открывается вовсе — решение принимается прямо в списке. */
.mod-case.appeal, .mod-case.comment-report { cursor: default; }
.mod-case.appeal:hover, .mod-case.comment-report:hover {
  border-color: var(--line2); box-shadow: none;
}
.mod-appeal-open {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: none; border: 0; padding: 0; color: inherit; font: inherit;
}
.mod-appeal-open:hover .mod-case-title { color: var(--gold); }
/* ПОЛНЫЙ ТЕКСТ вместо обрезки (TL-013 Ф1/Ф2): апелляция до 5000 символов,
   реплика до 4000. Высоту держит скролл, а не `.slice()` — модератор обязан
   иметь возможность прочитать всё, по чему выносит необратимое решение.
   pre-wrap: переносы строк автора — часть того, что он написал. */
.mod-case-text {
  white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 12em; overflow-y: auto;
}
.mod-appeal-text {
  font-size: .95em; line-height: 1.6; color: var(--ink);
  max-height: 22em; background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
}
.mod-case.active { border-color: var(--gold); background: var(--gold-soft); }
/* --- кейс без истории: решать по нему нечего (P1-14, эпик UXA 2026-08-28;
   заявка MODERATION §1). Пунктир, а не приглушение: opacity уронила бы
   контраст ровно там, где его этим же эпиком поднимают (THEME-CONTRAST). */
.mod-case.mod-case-dead { border-style: dashed; }
.mod-case.mod-case-dead .mod-case-title { color: var(--ink2); }
/* id · дата · автор — то, чем сто карточек «(история недоступна)» наконец
   отличаются друг от друга. tabular-nums, чтобы столбик id не плясал. */
.mod-case-sub { color: var(--ink3); font-size: .78em; font-variant-numeric: tabular-nums; }
.mod-detail-head .mod-case-sub { display: block; margin-top: 4px; }
.mod-dead-note { margin-bottom: 10px; padding: 9px 11px; border-radius: 10px;
  background: var(--state-neutral-bg); color: var(--ink2); font-size: .9em; }
/* Кнопки решений остаются на месте, но не нажимаются: убрать их совсем
   значило бы оставить человека гадать, где решения. */
.mod-decide [disabled] { cursor: not-allowed; opacity: .6; }

/* --- рабочее место модератора v2 (2026-07-24): список слева, ревью справа --- */
.mod-split { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; gap: 18px; margin-top: 12px;
  align-items: start; }
.mod-split .mod-list { margin-top: 0; max-height: calc(100vh - 220px); overflow-y: auto; padding-right: 4px; }
.mod-detail { position: sticky; top: 84px; max-height: calc(100vh - 120px); overflow-y: auto; }
.mod-detail-empty { border: 1px dashed var(--line2); border-radius: 14px; padding: 60px 24px;
  text-align: center; color: var(--ink3); }
/* opacity: .6 убран (P1-7): --gold × .6 = 2.17:1 в светлой теме — глиф пустого
   состояния не добирал даже послабления для крупного текста (3:1 от 24px). */
.mod-detail-empty span { font-size: 30px; display: block; margin-bottom: 12px; color: var(--gold); }
.mod-detail-empty p { margin: 0; }
.mod-detail-card { border: 1px solid var(--line2); border-radius: 14px; padding: 18px 20px; background: var(--surface); }
.mod-detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.mod-detail-head h3 { margin: 0; font-size: 1.15em; }
.mod-detail-head #mod-detail-close { flex: none; padding: 4px 10px; }
.mod-head-btns { display: flex; align-items: flex-start; gap: 6px; flex: none; }
/* Точка приземления фокуса на узком экране: обводки браузера достаточно, свой
   outline-хак здесь был бы вторым правилом фокуса на одном экране. Карточка в
   обход Tab не попадает (tabindex="-1") — фокус ставит только код. */
.mod-detail-card:focus { outline: 2px solid var(--gold); outline-offset: 2px; }
/* «← К списку» существует ТОЛЬКО там, где список уехал за край экрана. На
   широком макете он стоит рядом, и кнопка была бы шумом. */
.mod-back-list { display: none; }
.mod-ncmec-done { background: var(--state-good-bg); color: var(--state-good-ink); }
.mod-meta-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.mod-tag { font-size: .74em; padding: 3px 9px; border-radius: 999px;
  background: var(--state-neutral-bg); color: var(--ink2); white-space: nowrap; }
.mod-tag.cat { background: var(--state-neutral-bg); color: var(--state-neutral-ink); }
.mod-tag.warn { background: var(--state-warn-bg); color: var(--state-warn-ink); }
.mod-tag.risk-high, .mod-tag.risk-critical { background: var(--state-danger-bg); color: var(--state-danger-ink); }
.mod-tag.risk-medium { background: var(--state-warn-bg); color: var(--state-warn-ink); }
.mod-warn-banner { margin: 12px 0; padding: 10px 12px; border-radius: 10px;
  background: var(--state-warn-bg); color: var(--state-warn-ink); font-size: .9em; }
.mod-verdict { margin: 12px 0; padding: 11px 13px; background: var(--gold-soft); border-radius: 10px; line-height: 1.5; }
.mod-verdict .note, .mod-work-note, .mod-chapter-body .mod-clip { color: var(--ink3); font-size: .85em; }
.mod-block { margin-top: 14px; }
.mod-block h4 { margin: 0 0 6px; font-size: .82em; text-transform: uppercase; letter-spacing: .06em; color: var(--ink3); }
.mod-block ul { margin: 0; padding-left: 18px; }
.mod-block li { margin: 3px 0; }
/* сама работа — читаемые главы */
.mod-work-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.mod-open-book { font-size: .85em; white-space: nowrap; }
.mod-work-note { margin: 4px 0 8px; }
/* Отпечатки текста в карточке кейса (QA4 п.3): те же плашки, что у сводки
   мастера (.sum-v code, :1349-1353) — восемь символов sha должны читаться как
   код, а не как слово. Правило .sum-v приколочено к своей секции, поэтому
   здесь дубль, а не переиспользование. */
.mod-shas { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.mod-shas code {
  font-family: ui-monospace, Consolas, monospace; font-size: 12.5px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 5px; padding: 1px 5px;
}
/* Панель источника: разделитель между переключателем и текстом работы. */
.mod-work-pane { margin-top: 8px; }
.mod-work-empty { color: var(--ink3); font-style: italic; padding: 8px 0; }
.mod-chapter { border: 1px solid var(--line); border-radius: 10px; margin-top: 8px; background: var(--bg); overflow: hidden; }
.mod-chapter > summary { cursor: pointer; padding: 10px 14px; font-weight: 600; font-size: .92em;
  list-style: none; display: flex; align-items: center; gap: 8px; }
.mod-chapter > summary::-webkit-details-marker { display: none; }
.mod-chapter > summary::before { content: "▸"; color: var(--gold); transition: transform .15s ease; }
.mod-chapter[open] > summary::before { transform: rotate(90deg); }
.mod-chapter-body { padding: 4px 16px 16px; font-family: var(--font-serif); font-size: 15px;
  line-height: 1.72; color: var(--ink); max-height: 460px; overflow-y: auto; }
.mod-chapter-body p { margin: 0 0 .8em; }
/* решение */
.mod-decide { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 14px; }
.mod-actions { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 12px; align-items: center; }
.mod-actions-primary, .mod-actions-secondary { display: flex; flex-wrap: wrap; gap: 8px; }
.mod-actions-secondary { margin-left: auto; }
.mod-act.primary { background: var(--gold); border-color: var(--gold); color: var(--on-gold); font-weight: 600; }
.mod-act.primary:hover { filter: brightness(1.08); }
.mod-act.danger { border-color: var(--state-danger-ink); color: var(--state-danger-ink); }
.mod-act.danger:hover { background: var(--state-danger-bg); }
@media (max-width: 820px) {
  .mod-split { grid-template-columns: 1fr; }
  .mod-detail { position: static; max-height: none; }
  .mod-split .mod-list { max-height: none; }
  .mod-actions-secondary { margin-left: 0; }
  /* Единственная колонка: карточка лежит НИЖЕ списка, и вернуться к очереди
     иначе можно было только прокруткой вслепую (TL-013 Ф5). */
  .mod-back-list { display: inline-flex; align-items: center; }
}

/* Пилюля статуса модерации на карточке владельца */
.mod-pill {
  font-size: .72em; padding: 2px 8px; border-radius: 999px;
  background: var(--state-warn-bg); color: var(--state-warn-ink);
}
.mod-pill.s-quarantined, .mod-pill.s-hidden, .mod-pill.s-rejected {
  background: var(--state-danger-bg); color: var(--state-danger-ink);
}

/* Statement of reasons + обжалование (Wave 2) — баннер владельцу снятой книги */
.mod-notice {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between;
  margin: 10px 0; padding: 12px 15px; border-radius: 10px;
  background: var(--state-danger-bg); border: 1px solid var(--state-danger-ink); color: var(--state-danger-ink);
}
.mod-notice-body { flex: 1 1 260px; line-height: 1.5; }
/* §9.3: инфо-вариант (правки на ре-ревью, книга опубликована) — не тревожный */
.mod-notice.info { background: var(--state-info-bg); border-color: var(--state-info-ink); color: var(--state-info-ink); }
/* Ф1 (QA3-2026-08-28): «сверка текста не прошла — отправьте ещё раз». Не danger:
   правил автор не нарушал, книга просто не опубликована и ждёт его клика. */
.mod-notice.warn { background: var(--state-warn-bg); border-color: var(--state-warn-ink); color: var(--state-warn-ink); }
.mod-notice-tag { font-size: .85em; opacity: .85; white-space: nowrap; }
.mod-notice .ghost { border-color: var(--state-danger-ink); color: var(--state-danger-ink); background: transparent; }

/* Редактор главы + ИИ-ассист (авторское письмо, дизайн Premium) */
.editor-card { max-width: 860px; width: min(96vw, 860px); }
.editor-card #editor-chapter-title { font-family: var(--font-serif); font-size: 16px; }
.editor-card #editor-text {
  width: 100%; min-height: 40vh; resize: vertical;
  font-family: var(--font-serif); font-size: 16px; line-height: 1.7;
}
.editor-card #editor-count { text-align: right; font-variant-numeric: tabular-nums; }
.editor-draft-note { margin-left: auto; flex: none; }
.assist-panel {
  margin: 12px 0 4px; padding: 15px 17px;
  background: var(--raised); border: 1px solid var(--line); border-radius: 14px;
}
.assist-panel-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 12px; }
.assist-glyph {
  flex: none; width: 26px; height: 26px; border-radius: 50%;
  background: var(--gold-soft); border: 1px solid var(--gold); color: var(--gold-text);
  display: inline-flex; align-items: center; justify-content: center; font-size: 12px;
}
.assist-title { font-size: 11px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--gold-text); }
.assist-note { font-size: 12px; color: var(--ink3); font-style: italic; }
.assist-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.assist-bar .assist-act { font-size: 12.5px; padding: 8px 14px; min-height: 36px; }
.assist-act-glyph { opacity: .65; margin-right: 7px; }
.assist-bar #assist-instruction {
  flex: 1 1 190px; min-width: 150px; font-size: 12.5px;
  background: var(--surface); border-radius: 999px; padding: 8px 15px; min-height: 36px;
}
.assist-status:empty { display: none; }
.assist-status { display: block; margin-top: 8px; }
.assist-preview {
  margin-top: 14px; padding: 14px 18px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line2); border-left: 3px solid var(--gold);
  animation: ff-fade-up .25s ease both;
}
.assist-preview-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.assist-preview-kicker { font-size: 11px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--gold-text); }
.assist-preview-note { font-size: 11.5px; color: var(--ink3); font-style: italic; }
.assist-preview-text {
  font-family: var(--font-serif); font-style: italic; font-size: 15px; line-height: 1.68;
  white-space: pre-wrap; max-height: 32vh; overflow-y: auto;
}
.assist-preview-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.assist-preview-actions .primary,
.assist-preview-actions .ghost { min-height: 36px; padding: 8px 16px; font-size: 12.5px; }
.assist-hide {
  margin-left: auto; background: none; border: none; color: var(--ink3);
  font-family: var(--font-ui); font-size: 12.5px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px; padding: 6px 4px; min-height: 36px;
}
.write-own-block { margin: 18px 0 14px; text-align: center; }
.write-own-link {
  background: none; border: none; color: var(--ink3);
  font-family: var(--font-ui); font-size: 13.5px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 4px;
  padding: 10px 8px; min-height: var(--tap);
  transition: color .15s;
}
.write-own-link:active { color: var(--gold-text); }

/* Финал мастера (макет, line 731-740): обе кнопки создания — одна колонка
   min(420px, 100%) по центру, одинаковой высоты с карточкой (68px). */
#create.cta {
  display: block; width: min(420px, 100%); margin: 0 auto;
  min-height: 68px; padding: 17px 26px; border-radius: 12px;
}

/* Карточка «первую главу напишу сам» (макет: финал мастера, ✒ + подзаголовок).
   Живёт на пустой книге (editor.js); у конструктора книги это кнопка панели
   #create-manual (builder.css) — карточка шага «Итог» ушла с мастером (2026-10). */
.create-manual-card {
  width: min(420px, 100%); box-sizing: border-box;
  display: flex; align-items: center; gap: 14px; text-align: left;
  background: transparent; border: 1px solid var(--line2); border-radius: 12px;
  padding: 12px 18px 12px 12px; margin: 0 auto;
  color: var(--ink); font-family: var(--font-ui); cursor: pointer;
  transition: border-color .15s, background .15s, transform .15s;
}
.create-manual-card:active { transform: translateY(1px); }
.create-manual-card:disabled { opacity: .6; cursor: default; }
.cmc-glyph {
  flex: none; width: 42px; height: 42px; border-radius: 50%;
  background: var(--gold-soft); border: 1px solid var(--gold); color: var(--gold-text);
  display: inline-flex; align-items: center; justify-content: center; font-size: 18px;
}
.cmc-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cmc-title { font-size: 15px; font-weight: 600; }
.cmc-sub { font-size: 12.5px; color: var(--ink3); }
.cmc-arrow { margin-left: auto; color: var(--gold-text); font-size: 15px; }

/* Боковая панель поиска каталога (справа; запрос + include/exclude теги) */
.search-open-btn {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font-ui); font-size: 13.5px; font-weight: 600;
  padding: 10px 16px; border-radius: 10px; cursor: pointer;
  border: 1px solid var(--line2); background: var(--surface); color: inherit;
  transition: border-color .15s, box-shadow .15s;
}
.search-open-btn:hover { border-color: var(--gold); }
.search-badge {
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--gold); color: var(--on-gold);
  font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center;
}
.search-panel {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 80;  /* НАД drawer-backdrop (69) и самой шторкой (70) — иначе тапы по панели ловил бэкдроп */
  width: min(380px, 94vw);
  background: var(--surface); border-left: 1px solid var(--line2);
  box-shadow: var(--shadow);
  border-radius: 14px 0 0 14px;
  padding: 18px 18px calc(18px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 12px;
  transform: translateX(105%); transition: transform .28s cubic-bezier(.2,.8,.25,1);
  overflow-y: auto;
}
.search-panel.open { transform: none; }
.search-panel-head { display: flex; align-items: center; justify-content: space-between; }
.search-panel-head h3 { margin: 0; font-family: var(--font-serif); font-size: 20px; }
.sp-section { display: flex; flex-direction: column; gap: 8px; }
.sp-title {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink2); display: flex; align-items: center; gap: 6px;  /* «+»/«−» вплотную */
}
.sp-title .sp-mode { margin-left: auto; }  /* сегмент И/ИЛИ — к правому краю */
.sp-title.inc-title::before { content: "＋"; color: var(--gold); }
.sp-title.exc-title::before { content: "−"; color: var(--state-danger-ink); }
.sp-chips { display: flex; flex-wrap: wrap; gap: 5px; min-height: 22px; }
/* «ничего не выбрано» — тот же приём (P1-7): --ink2 × .6 = 2.72:1 в светлой
   теме, --ink3 без множителя = 4.76:1. */
.sp-none { font-size: 12px; color: var(--ink3); }
.sp-mode { display: inline-flex; border: 1px solid var(--line2); border-radius: 999px; overflow: hidden; }
.sp-mode-btn {
  font-family: var(--font-ui); font-size: 11px; font-weight: 600; text-transform: none;
  letter-spacing: 0; padding: 4px 10px; border: 0; background: transparent;
  color: var(--ink2); cursor: pointer;
}
.sp-mode-btn.active { background: var(--gold); color: var(--on-gold); }
.sp-tag-add { position: relative; }
.sp-tag-add input {
  width: 100%; font-size: 13.5px; padding: 8px 11px;
  border: 1px solid var(--line2); border-radius: 9px; background: transparent; color: inherit;
}
.sp-suggest {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 5;
  background: var(--surface); border: 1px solid var(--line2);
  border-radius: 10px; box-shadow: var(--shadow-soft);
  max-height: 220px; overflow-y: auto; display: flex; flex-direction: column;
}
/* TL-016: опции подсказок — `div role="option"`, а не кнопки. Фокусируемых
   потомков внутри комбобокса быть не должно: активную называет
   `aria-activedescendant` поля, а Tab обязан уводить ИЗ поля наружу, а не
   внутрь списка. Селектор по роли, а не по классу: роль здесь и есть контракт
   (`library.js::showTagSuggest`), и разъехаться им нечем. */
.sp-suggest [role="option"] {
  text-align: left; font-family: var(--font-ui); font-size: 13px;
  padding: 8px 12px; border: 0; background: transparent; color: inherit; cursor: pointer;
  display: flex; justify-content: space-between; gap: 8px;
}
/* Виртуальный фокус ОБЯЗАН быть виден глазами, а не только диктору. Фон тот
   же, что у наведения: --gold-soft — это 11-13% альфы поверх --surface, то
   есть текст остаётся на своём же контрасте (--ink на --surface), и оба
   гейта a11y с ПУСТЫМ допуском проходят по построению. Рамка слева — второй,
   не-цветовой признак: подсветка одним фоном на 13% альфы в светлой теме
   читается хуже, чем хотелось бы. */
.sp-suggest [role="option"]:hover,
.sp-suggest [role="option"][aria-selected="true"] { background: var(--gold-soft); }
.sp-suggest [role="option"][aria-selected="true"] { box-shadow: inset 2px 0 0 var(--gold); }
.sp-tags { display: flex; flex-wrap: wrap; gap: 6px; max-height: 46vh; overflow-y: auto; padding: 2px; }
.tag-chip.sp { font-size: 12.5px; padding: 6px 11px; }
/* Счётчик книг у тега — ЧИСЛО, а не украшение: opacity: .55 роняла его до
   2.2:1 в светлой теме (P1-7). Тише подписи он и так — 10.5px против 11.5px, а
   цвет чипа (--ink3) и есть дно приглушённой лестницы, ниже него токена нет. */
.tag-chip.sp .sp-count { font-size: 10.5px; }
.tag-chip.inc, .tag-chip.sp.inc {
  background: var(--gold-soft); border-color: var(--gold); color: var(--gold-text);
}
/* было `opacity: .8` — остаток той же компенсации: на включённом чипе счётчик
   наследует --gold-text, и множитель ронял его до 4.0:1 (P1-7). */
.tag-chip.exc, .tag-chip.sp.exc {
  background: color-mix(in oklab, var(--danger) 10%, transparent);
  border-color: color-mix(in oklab, var(--danger) 55%, transparent);
  color: var(--danger);
  text-decoration: line-through; text-decoration-thickness: 1px;
}
.tag-chip.excluded {
  background: color-mix(in oklab, var(--danger) 10%, transparent);
  border-color: color-mix(in oklab, var(--danger) 55%, transparent);
  color: var(--danger);
}
.tag-chip.excluded button, .tag-chip.active button {
  border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 11px; padding: 0 0 0 2px;
}
.sp-hint { margin: -4px 0 8px; font-size: 12px; color: var(--ink3); font-style: italic; }
.sp-summary { font-size: 13px; color: var(--ink2); }
.search-panel-actions { display: flex; gap: 8px; margin-top: auto; }
.search-panel-actions .primary { flex: 1; }

/* Теги из конструктора: чипы на карточках, в читалке и фильтр каталога */
.card-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; padding: 0 2px; }
/* Язык на карточке (дизайн 07-24): тонкий текст справа вместо 🌐-пилюли */
.card-lang { margin-left: auto; background: none; border: none; padding: 2px 0 2px 8px;
  font-family: var(--font-ui); font-size: 11.5px; color: var(--ink3); cursor: pointer;
  text-underline-offset: 3px; }
.card-lang:hover { color: var(--gold-text); text-decoration: underline dotted rgba(199,163,96,.6); }
.card-lang.static { cursor: default; }
.card-lang.static:hover { color: var(--ink3); text-decoration: none; }
.tag-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: var(--font-ui); font-size: 11.5px; line-height: 1;
  padding: 5px 11px; border-radius: 999px;
  background: var(--raised); color: var(--ink3);
  border: 1px solid var(--line2); cursor: pointer;
  transition: color .15s, border-color .15s;
}
button.tag-chip:hover { color: var(--gold-text); border-color: var(--gold); }
.tag-chip.static { cursor: default; }
/* фандом теперь кликабельный фильтр (2026-07-24): pointer наследуется от базы;
   default остаётся только у некнопочных span-чипов */
.tag-chip.fandom { color: var(--ink2); }
span.tag-chip.fandom { cursor: default; }
/* Чип тональности в каталоге/профиле (класс .rating исторический — это ТОНАЛЬНОСТЬ,
   а не читательские звёзды, у тех .card-rating). 2026-07-28: содержимое стало
   словом вместо буквы — трекинг сжат, перенос запрещён. */
.tag-chip.rating {
  background: var(--gold); border-color: var(--gold); color: var(--on-gold);
  cursor: default; font-weight: 700; letter-spacing: .01em; white-space: nowrap;
}
.tag-chip.warn {
  background: transparent; color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 45%, transparent); cursor: default;
}
.tag-chip.active { background: var(--gold-soft); border-color: var(--gold); color: var(--gold-text); }
.tag-chip.active button {
  border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 11px; padding: 0 0 0 2px;
}
/* Строка тегов живёт в ТОЙ ЖЕ колонке, что и лист книги (920px по центру) и
   НЕ прилипает к HUD-плашке под ней: в макете между ними 16px воздуха. */
.book-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; row-gap: 9px; max-width: 920px; margin: 10px auto 16px; }
/* --- дизайн 07-24: пейринг-чип + байлайн автор/язык справа ------------------ */
.tag-chip.pairing { background: var(--raised); gap: 6px; color: var(--ink2); }
.tag-chip.pairing .pair-x { color: var(--gold); font-size: 11px; }
.tag-byline { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; white-space: nowrap; }
.byline-author { display: inline-flex; align-items: center; gap: 7px; background: none; border: none;
  padding: 2px 0; font-family: var(--font-ui); font-size: 12.5px; color: var(--ink2); cursor: pointer; }
.byline-author:hover { color: var(--gold-text); }
.byline-author b { color: var(--ink); font-weight: 500; }
.byline-ava { width: 20px; height: 20px; border-radius: 50%; background: var(--gold-soft);
  border: 1px solid rgba(199,163,96,.45); color: var(--gold-text); font-size: 11px; font-weight: 600;
  display: inline-flex; align-items: center; justify-content: center; }
.byline-dot { color: var(--ink3); font-size: 12px; }
.byline-lang { background: none; border: none; padding: 2px 0; font-family: var(--font-ui);
  font-size: 12.5px; color: var(--ink3); cursor: pointer; text-underline-offset: 3px; }
.byline-lang:hover { color: var(--gold-text); text-decoration: underline dotted rgba(199,163,96,.6); }
.cat-active-filter { margin: 4px 0 10px; font-size: 13px; color: var(--ink2); }
.mod-warn { color: var(--state-danger-ink); }

/* Переключатель режимов страницы модератора (кейсы/апелляции) */
.mod-modes { display: inline-flex; gap: 4px; }
.tab-mini {
  font: inherit; font-size: .85em; padding: 5px 12px; border-radius: 999px;
  border: 1px solid var(--line2); background: transparent; color: var(--ink2); cursor: pointer;
}
.tab-mini.active { border-color: var(--gold); color: inherit; font-weight: 600; }
.mod-case.appeal .mod-actions { margin-top: 4px; }

/* ------------------------------------------------------------ мобильные уточнения (2026-07-08)
   Особое внимание телефону: шапка с CTA в один ряд, полноэкранная панель поиска,
   тач-таргеты (--tap), полноэкранный редактор главы. */
@media (max-width: 719.9px) {
  /* Авторизованная шапка: бренд + чипы + колокольчик + тема + профиль. */
  .topbar { gap: 0 10px; }
  .topbar-right { gap: 8px; }
  .topbar-right .tab[data-view="new"] { margin: 0; padding: 9px 12px; font-size: 12.5px; }
  .brand { font-size: 18px; }

  /* ---- макеты v2: вкладки уехали в нижнюю навигацию (.lownav) ----
     Ряд вкладок в шапке телефона больше не рисуется, но САМ <nav> остаётся
     живым и схлопывается в ноль: в нём висит кнопка «Модерация», которой в
     баре из пяти мест нет. Поэтому прячем ДЕТЕЙ, а не контейнер, и снимаем с
     него width:100%/border-top — пустой полноширинный ряд оставил бы под
     шапкой лишнюю линию. margin-left:auto здесь НЕ ставить: auto-отступ уже
     есть у .topbar-right, и два auto поделили бы свободное место пополам,
     утащив профиль от правого края к середине. */
  body:not(.guest-mode) .topbar nav {
    order: 0; width: auto; flex: 0 0 auto; gap: 0; border-top: 0;
  }
  body:not(.guest-mode) .topbar nav > .tab { display: none; }
  body:not(.guest-mode) .topbar nav > #nav-moderation {
    display: block; flex: none; min-height: var(--tap); padding: 10px 8px;
  }
  /* CTA «Новая история» на телефоне — это пункт «Новая» ✦ нижней навигации.
     Узел из шапки не удаляем (его двигает relocateNewStoryCta и показывает
     syncAuthNavigation) — прячем обе возможные посадки: правый блок шапки
     (нынешнее поведение app.js) и ряд вкладок (куда его положил бы СТАРЫЙ
     кэшированный app.js, не знающий про бар). */
  body:not(.guest-mode) .topbar-right .tab[data-view="new"] { display: none; }

  /* Скрин владельца 2026-08-13: авторизованная шапка телефона рвалась на два
     высоких ряда — колокольчик (ret1) добавил ~46px, и ряд «бренд + чипы +
     профиль» перестал влезать в 360–412px. Шапке запрещён перенос (nowrap).
     У модератора в ряду ещё и «Модерация» (~86px) — ради неё прячем слово
     бренда, остаётся знак-страница: тот же приём, что ≤380px для всех. */
  body:not(.guest-mode) .topbar { flex-wrap: nowrap; }
  body:not(.guest-mode) .topbar:has(#nav-moderation:not(.hidden)) .brand-word { display: none; }

  /* Тач-размеры макета v3 (.om-hd-btn): кружки шапки — полный var(--tap), а не
     ужатые 34px эпохи v2. Место под них взялось не из воздуха: из ряда ушла
     пилюля квоты (~110px), и «бренд + искры + тема + колокольчик + аватар»
     просит теперь ≈230px правого блока против прежних ≈330px. ::after
     колокольчику здесь больше не нужен — кружок и есть тач-таргет. */
  .theme-toggle, .notif-bell { width: var(--tap); height: var(--tap); }
  .notif-bell::after { inset: 0; }
  /* Пилюля искр — тоже тач-таргет: по макету min-height 44px. */
  .billing-chips { gap: 5px; }
  .credits-chip { min-height: var(--tap); padding: 0 12px; font-size: 12.5px; }
  .topbar-right { padding: 8px 0; }
  /* Меню профиля: пункты пальцем, а не курсором. Полный var(--tap), а не 38-40px
     по букве макета: карточка и так уже висит на всю ширину экрана, а «Выйти» и
     «Пополнить» — необратимые по последствиям пункты, промах по ним стоит дороже
     двух лишних пикселей высоты. */
  .pm-link { min-height: var(--tap); padding: 10px 2px; }
  .pm-topup { min-height: var(--tap); }
  /* Геометрия CTA внутри ряда вкладок. С макетами v2 у вошедшего этот ряд на
     телефоне не рисуется вовсе (правило выше), но правило оставлено: оно
     страхует посадку CTA в nav у СТАРОГО кэша app.js — тогда кнопка хотя бы
     не растянется на всю ширину ряда. */
  .topbar nav .tab[data-view="new"] {
    flex: none; align-self: center; margin: 6px 0 6px auto;
    padding: 8px 13px; font-size: 12.5px;
  }

  /* Гостевая шапка — одна строка: бренд, каталог, тема и вход. Приватные
     вкладки скрывает auth.js, поэтому здесь нет пустой второй строки. */
  body.guest-mode .topbar {
    flex-wrap: nowrap;
    gap: 8px;
    padding-inline: 12px;
  }
  body.guest-mode .topbar nav {
    order: 0;
    width: auto;
    flex: 0 0 auto;
    margin-left: auto;
    border-top: 0;
  }
  body.guest-mode .topbar nav .tab {
    flex: none;
    min-height: var(--tap);
    padding: 10px 8px;
  }
  body.guest-mode .topbar-right {
    margin-left: 0;
    gap: 6px;
    padding-block: 8px;
  }
  body.guest-mode .theme-toggle { width: var(--tap); height: var(--tap); }
  body.guest-mode .header-login { min-height: var(--tap); padding-inline: 10px; }

  /* панель поиска: полноэкранный лист справа */
  .search-panel { width: 100%; border-radius: 0; border-left: none; }
  .search-panel-actions .primary, .search-panel-actions .ghost { min-height: var(--tap); }
  .sp-suggest [role="option"] { min-height: 42px; align-items: center; }
  .sp-mode-btn { padding: 7px 12px; }
  .search-open-btn { min-height: var(--tap); flex: 1; justify-content: center; }

  /* теги: пальце-дружелюбные чипы */
  .tag-chip { padding: 6px 11px; font-size: 12px; }
  .tag-chip.sp { padding: 8px 12px; font-size: 13px; }

  /* редактор главы: полноэкранный, текст тянется на всю высоту */
  .modal:has(.editor-card) { padding: 0; align-items: stretch; }
  .editor-card {
    max-width: none; height: 100dvh; max-height: 100dvh;
    border-radius: 0; display: flex; flex-direction: column;
  }
  .editor-card .editor-text-field { flex: 1; display: flex; flex-direction: column; }
  .editor-card .editor-text-field span { flex: none; }
  .editor-card #editor-text { flex: 1; min-height: 180px; }
  .assist-panel { padding: 12px; }
  .editor-draft-note { display: none; }  /* на телефоне шапке модалки тесно */
  /* герой «Вы остановились здесь»: кнопки в ряд на всю ширину, обложка меньше */
  .lib-hero { padding: 16px; gap: 16px; }
  .lib-hero-cover { width: 110px; }
  .lib-hero-actions > button { flex: 1 1 auto; }

  /* Карточка-резюме (скрин владельца 2026-08-13, тогда — шаг 5 мастера): метка
     встаёт НАД значением — колонка 108px на телефоне душила значения. Лист
     шагов, состав резюме и панель «Назад / Далее» пятишагового мастера ушли
     вместе с ним (2026-10); хром экрана раздела телефона — builder.css. */
  .summary-card { padding: 6px 14px; }
  .sum-line { flex-direction: column; gap: 1px; }
  .sum-k { width: auto; }
}
/* ТАЧ-ЗОНЫ КОНТРОЛОВ КАРТОЧКИ (P1-6, эпик UXA 2026-08-28; заявка CARDS §2).
   Замеры тестировщика: удаление ≈21×23, смена обложки ≈27×27, крестик отписки
   ≈21×23, язык — высота ≈18. Токен --tap: 44px объявлен на :57 и добросовестно
   применён в мобильном блоке выше — к шапке, чипам искр, меню профиля и панели
   поиска. Контролы карточки в него просто не попали: секция писалась под
   шапку, мастер и поиск.

   Растить сами коробки до 44px нельзя: .card-top — это flex, где заголовок
   живёт с min-width:0 и многоточием, а справа стоят пилюля тональности и две
   кнопки. Три квадрата 44px съели бы всю ширину карточки на 320-360px и
   схлопнули заголовок в «…». Поэтому — ПСЕВДОЭЛЕМЕНТ-зона по образцу
   .tts-bar button, где ровно этот случай уже решён.

   Язык растягивается ТОЛЬКО ПО ВЕРТИКАЛИ, и это не экономия: .card-lang стоит
   в .card-tags с margin-left:auto, слева от него — последний .tag-chip с
   зазором 4px. Зона шириной 44px вылезла бы влево на ~22px и перехватывала бы
   тап по тегу, то есть меняла бы фильтр вместо ожидаемого. */
@media (max-width: 719.9px) {
  .card-del, .card-cover, .card-unfollow { position: relative; }
  .card-del::after, .card-cover::after, .card-unfollow::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: var(--tap); height: var(--tap);
    transform: translate(-50%, -50%);
  }
  .card-top-right { gap: 12px; }
  /* ЗОНЫ «ПАЛИТРЫ» И «УДАЛИТЬ» НЕ ИМЕЮТ ПРАВА ПЕРЕСЕКАТЬСЯ, а gap этого не
     решает: коробки ≈27px (🎨) и ≈21px (🗑), значит при gap 12px между их
     центрами 10.5+12+13.5 = 36px, а зоны шириной 44px расходятся только при
     44px. Прежний комментарий «перекрывались бы на 6px» ошибался вдвое:
     перекрытие оставалось ≈8px, и выигрывала его .card-del::after (позже в
     DOM) — тап по ПУСТОМУ месту между иконками открывал «Удалить историю
     «…» безвозвратно?» там, где раньше просто открывалась книга. Это ровно
     то место, про которое отчёт пишет «особенно опасно рядом с удалением».

     Лечим двумя строками, а не одним увеличенным gap.
     1) Разделяем пару на 20px, но ТОЛЬКО её: `gap` в flex общий, и подняв его
        до 20px мы забрали бы 16px у заголовка карточки (на мобильной полке
        две колонки по ≈164px, заголовок и так с многоточием). Отступ на самой
        кнопке удаления стоит 8px и ровно там, где нужен.
     2) Зоны НЕ центрируем по коробкам, а стыкуем по середине этого зазора:
        правый край зоны «палитры» и левый край зоны «удалить» — обе на -10px
        от своей коробки. Тогда непересечение держится КОНСТРУКЦИЕЙ, а не
        арифметикой: сколько бы ни намерил браузер эмодзи-глифу (метрики
        плавают между платформами и шрифтами), зоны остаются встык.
     Наружу зоны при этом растут в пустоту: «палитра» — на ≈7px в зазор до
     пилюли тональности (12px, пилюля не интерактивна), «удалить» — на ≈13px
     в колоночный жёлоб полки (14px), не доставая до соседней карточки. */
  .card-del { margin-inline-start: 8px; }
  .card-cover::after { left: auto; right: -10px; transform: translateY(-50%); }
  .card-del::after { left: -10px; right: auto; transform: translateY(-50%); }

  .card-lang { position: relative; }
  .card-lang::after {
    content: ""; position: absolute; left: 0; right: 0;
    top: 50%; height: var(--tap); transform: translateY(-50%);
  }
  /* чтобы вертикальная зона языка не залезла на .card-prog снизу */
  .card-tags { row-gap: 10px; }
}
/* Телефоны 380–430px (Samsung/Pixel 384–412 — самая массовая полоса): слово
   «Turnleaves» скрывается только с 380px, а ряд «бренд + ☾ + искры + квота +
   профиль» просил ≈395px — шапка вставала в ТРИ ряда (скрин владельца
   2026-07-26). Ужимали ряд на ~30px вместо того, чтобы прятать бренд.
   2026-08-13 (макет v3): ужимать больше нечего и незачем — пилюля квоты из
   ряда ушла, и без неё «бренд + искры + тема + колокольчик + аватар» просит
   ≈350px при 380px экрана. Кружки остаются полным тач-таргетом (v3 главнее
   компромисса v2), ужатыми живут только отступы. ЕСЛИ ряд всё же порвётся на
   каком-то устройстве — первый шаг отката ровно один: вернуть сюда
   .theme-toggle/.notif-bell 32px и .userchip .avatar 36px. */
@media (max-width: 429.9px) {
  .topbar { padding-inline: 8px; gap: 0 8px; }
  .brand { font-size: 17px; gap: 6px; }
  .billing-chips { gap: 4px; }
  .credits-chip { padding: 0 10px; }
  .topbar-right { gap: 6px; }
}
/* Очень узкие экраны: вкладка «Новая история» в шапке ужимается. (Сег-контрол
   языка пятишагового мастера ушёл вместе с ним в 2026-10: язык — радио
   «Тонкой настройки», builder.css.) */
@media (max-width: 480px) {
  .topbar nav .tab[data-view="new"] { padding: 7px 10px; font-size: 12px; }
}
@media (max-width: 379.9px) {
  /* совсем узкие экраны: остаётся знак-страница без слова */
  .brand-word { display: none; }

  /* P1-5 эпика 2026-08-28: «на ширине 320px ломается шапка» — документ 346px
     при клиентских 320, кнопка профиля на 302–346, горизонтальный скролл.
     Арифметика ряда у модератора: 8 (padding) + знак-страница 14 + 8 +
     «Модерация» ≈96 + 8 + [искры ≈60 + 6 + тема 44 + 6 + колокол 44 + 6 +
     аватар 44] + 8 ≈ 352px. Лишние ~26px даёт ровно одна нерезиновая кнопка:
     `body:not(.guest-mode) .topbar nav > #nav-moderation` объявлена
     `flex: none` при `white-space: nowrap` у .tab, а перенос ряда запрещён
     (`flex-wrap: nowrap`, скрин владельца 2026-08-13 — возвращать перенос
     НЕЛЬЗЯ, вернётся двухрядная шапка).
     Поэтому на 320px «Модерация» становится глифом весов. Имя кнопки при
     font-size:0 берётся из aria-label в web/index.html — без него кнопка
     осталась бы безымянной, и дефект доступности сменил бы дефект вёрстки. */
  body:not(.guest-mode) .topbar nav > #nav-moderation {
    /* TL-029/TL-021: 6 + глиф 17 + 6 = 29px ширины (замер приёмки — 27.9).
       Тач-зона обязана быть 44×44 (var(--tap)), и запас есть: ряд модератора
       на 320px по арифметике выше даёт 352 − 96 (слово) + 29 (глиф) = 285;
       с min-width:44 → 300 < 320. Перенос ряда возвращать по-прежнему НЕЛЬЗЯ. */
    font-size: 0; padding: 10px 6px; min-width: var(--tap);
  }
  body:not(.guest-mode) .topbar nav > #nav-moderation::before {
    content: "⚖"; font-size: 17px; line-height: 1;
  }

  /* Страховка от того же класса дефекта у ВСЕХ остальных: без min-width:0
     флекс-элемент не сжимается ниже своего содержимого, и любая будущая
     добавка снова вытолкнет за экран последний элемент ряда — профиль
     (`order: 5`). С этими двумя строками первой ужимается пилюля искр. */
  .topbar-right { min-width: 0; }
  #header-chips { min-width: 0; overflow: hidden; }
}

/* ============================================================
   Биллинг (2026-07-17): витрина тарифов, пейволл, искры, замочки.
   Токены темы — как во всём файле (gold/surface/ink).
   ============================================================ */

/* --- чипы в шапке ------------------------------------------------------- */
/* С макетом v3 чип в шапке ОДИН — пилюля искр (звезда-SVG + число). Пилюля
   квоты уехала карточкой в меню профиля, золотая CTA «⚡ Premium» — кнопкой
   апгрейда там же: обе занимали место, которого не хватало тач-кружкам. */
/* margin-right'а больше нет: чипы переехали в собственный слот #header-chips, и
   расстояние до соседа задаёт gap самой .topbar-right — прежний отступ давал
   двойной зазор между искрами и «Новой историей». */
.billing-chips { display: inline-flex; align-items: center; gap: 8px; }
.credits-chip {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line2); background: none; color: var(--gold-text);
  border-radius: 999px; padding: 0 13px; font: 600 13px var(--font-ui);
  cursor: pointer; min-height: 38px; white-space: nowrap;
  font-variant-numeric: tabular-nums; transition: border-color .15s, color .15s;
}
.credits-chip .ico-spark { flex: none; }
.tier-chip {
  border-radius: 999px; padding: 5px 12px; font: 700 12px var(--font-ui);
  background: var(--gold-soft); color: var(--gold-text); border: 1px solid var(--gold);
}
.tier-chip-ultra { background: linear-gradient(135deg, var(--gold-soft), rgba(199,163,96,.28)); }

/* --- баннер «подтвердите почту» ---------------------------------------- */
.verify-banner {
  display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap;
  padding: 9px 16px; background: var(--state-warn-bg); color: var(--state-warn-ink);
  border-bottom: 1px solid var(--line); font: 500 13.5px var(--font-ui);
}
.verify-banner.pulse { animation: vb-pulse 0.8s ease 2; }
@keyframes vb-pulse { 50% { background: rgba(199,163,96,.35); } }
.vb-btn {
  border: 1px solid var(--gold); background: transparent; color: var(--gold-text);
  border-radius: 8px; padding: 5px 12px; font: 600 12.5px var(--font-ui); cursor: pointer;
}
.vb-btn:hover { background: var(--gold-soft); }

/* --- модалка тарифов ---------------------------------------------------- */
.plans-card { max-width: 980px; width: min(980px, calc(100vw - 24px)); }
.pw-reason {
  display: flex; gap: 10px; align-items: center; margin: 4px 0 16px;
  background: var(--gold-soft); border: 1px solid var(--gold);
  border-radius: 12px; padding: 12px 16px; color: var(--ink); font: 500 14px var(--font-ui);
}
.pw-reason-ico { font-size: 20px; }
.plans-toggle {
  display: flex; gap: 6px; justify-content: center; margin: 2px 0 18px;
  background: var(--raised); border-radius: 999px; padding: 4px; width: fit-content;
  margin-inline: auto; border: 1px solid var(--line);
}
.pt-btn {
  border: none; background: transparent; color: var(--ink2); border-radius: 999px;
  padding: 7px 18px; font: 600 13.5px var(--font-ui); cursor: pointer; min-height: 34px;
}
.pt-btn.active { background: var(--gold); color: var(--on-gold); }
.pt-save { font-size: 11px; opacity: .85; margin-left: 4px; }
.plans-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px; margin-bottom: 22px;
}
/* Карточки страницы тарифов (дизайн «Fansfics Premium»: квота-бокс со шкалой) */
.plan-card {
  position: relative; border: 1px solid var(--line); border-radius: 16px;
  background: var(--surface); padding: 22px 20px 20px; display: flex; flex-direction: column;
}
.plan-card.hot { border-color: var(--gold); box-shadow: var(--shadow-soft); }
.plan-card.current { border-color: color-mix(in oklab, var(--good) 55%, transparent); }
.plan-ribbon {
  position: absolute; top: -11px; left: 50%; transform: translateX(-50%); white-space: nowrap;
  background: var(--gold); color: var(--on-gold); border-radius: 999px; padding: 4px 12px;
  font: 700 10px var(--font-ui); letter-spacing: .1em; text-transform: uppercase;
}
.plan-namerow { display: flex; align-items: baseline; gap: 9px; }
.plan-name { font: 600 21px var(--font-serif); color: var(--ink); }
.plan-desc { font: 400 11.5px var(--font-ui); color: var(--ink3); line-height: 1.4; margin-top: 3px; }
.plan-price { font: 600 34px var(--font-serif); color: var(--ink); letter-spacing: -.01em; margin-top: 12px; }
.plan-permo { font: 500 13px var(--font-ui); color: var(--ink3); margin-left: 4px; }
.plan-pricenote { font: 400 11.5px var(--font-ui); color: var(--ink3); margin-top: 3px; min-height: 15px; }
.plan-quota {
  margin: 16px 0 14px; padding: 12px 14px; background: var(--raised);
  border: 1px solid var(--line); border-radius: 10px;
}
.pqx-top { display: flex; align-items: baseline; gap: 6px; }
.pqx-num { font: 600 21px var(--font-serif); color: var(--gold-text); }
.pqx-lbl { font: 400 12px var(--font-ui); color: var(--ink2); }
.pqx-bar { display: block; height: 4px; border-radius: 999px; background: var(--line); overflow: hidden; margin-top: 8px; }
.pqx-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--gold); }
.pqx-sub { font: 400 11.5px var(--font-ui); color: var(--ink3); margin-top: 7px; }
.plan-feats2 { display: flex; flex-direction: column; gap: 8px; flex: 1; margin-bottom: 16px; }
.plan-feat { display: flex; gap: 9px; font: 400 13px var(--font-ui); line-height: 1.45; color: var(--ink2); }
.pf-check { flex: none; color: var(--gold-text); font-size: 11px; padding-top: 2px; }
.plan-cta {
  border: none; border-radius: 11px; padding: 13px; font: 600 14px var(--font-ui);
  background: var(--gold); color: var(--on-gold); cursor: pointer; min-height: var(--tap);
}
.plan-cta:hover { filter: brightness(1.07); }
.plan-cta[disabled] { opacity: .55; cursor: default; filter: none; }
.plan-cta-outline { background: transparent; border: 1px solid var(--gold); color: var(--gold-text); }
.plan-cta-outline:hover { filter: none; background: var(--gold-soft); }
.plan-cta-current {
  text-align: center; padding: 12px; border-radius: 11px; background: transparent;
  border: 1px solid color-mix(in oklab, var(--good) 55%, transparent);
  color: var(--good); font: 600 14px var(--font-ui);
}
.plan-cta-ghost {
  text-align: center; padding: 12px; border-radius: 11px; background: transparent;
  border: 1px solid var(--line2); color: var(--ink3); font: 600 13.5px var(--font-ui);
}
/* Версии предложений, которые ещё НЕ ПРОДАЮТСЯ (Д8, заявка OFFERS О-6).
   Разметка намеренно переиспользует существующие классы (`plan-card`,
   `plans-grid`, `plan-cta[disabled]` уже стилизованы), поэтому блок минимален и
   без него секция не ломается — только выглядит как обычная карточка тарифа.
   Визуально ПРИГЛУШЕНЫ: цена в карточке настоящая (взята ссылкой на базовую
   позицию), и карточка не должна конкурировать вниманием с тем, что
   действительно можно купить. */
.plans-drafts { opacity: .82; }
.plan-card.draft-card { border-style: dashed; background: transparent; }
/* Правка интегратора к тексту заявки: цвет — `--ink3`, а не `--muted`.
   Переменной `--muted` в этой теме НЕТ ни в светлой, ни в тёмной палитре, и
   `color: var(--muted)` без запасного значения молча наследует цвет родителя —
   то есть пометка «Цена не утверждена» печаталась бы обычным цветом текста.
   `--ink3` — тот самый приглушённый токен, которым набран сосед
   `.plan-cta-ghost`. */
.draft-badge {
  display: inline-block; padding: 2px 8px; border-radius: 999px;
  font-size: .78rem; letter-spacing: .02em;
  border: 1px solid var(--line); color: var(--ink3);
}
/* Страница тарифов: hero + полоса использования пула */
.plans-page { width: min(1180px, 100%); margin: 0 auto; }
.plans-hero { text-align: center; max-width: 640px; margin: 0 auto 18px; }
.ph-kicker {
  font: 600 11px var(--font-ui); letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink3); margin-bottom: 10px;
}
.ph-title {
  margin: 0 0 12px; font-family: var(--font-serif); font-size: clamp(30px, 4.5vw, 44px);
  font-weight: 600; letter-spacing: -.01em; line-height: 1.1;
}
.ph-sub { margin: 0; font: 400 15px var(--font-ui); line-height: 1.6; color: var(--ink2); }
.plans-usage { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 22px; }
.pu-track { width: 110px; height: 5px; border-radius: 999px; background: var(--line); overflow: hidden; }
.pu-track > span { display: block; height: 100%; border-radius: 999px; background: var(--gold); }
.pu-note { font: 400 12.5px var(--font-ui); color: var(--ink3); font-variant-numeric: tabular-nums; }
/* Чекаут-подстраница */
.co-page { max-width: 470px; margin: 0 auto; }
.co-back { display: inline-block; margin-bottom: 10px; font-size: 13px; }
.co-h { margin: 0 0 16px; font: 600 24px var(--font-serif); }
/* Пилюля квоты в шапке УДАЛЕНА вместе с макетом v3 (2026-08-13): месячный пул
   показывает карточка «Главы в этом месяце» в меню профиля (.pm-quota ниже), а
   тариф — бейдж-буква на аватаре (.tier-dot). Правил .quota-pill в файле
   больше нет ни одного, включая полосу 1150–1250, где она ужималась до цифр. */
/* Апгрейд-хинт (80% лимита) */
.upgrade-hint {
  display: flex; align-items: center; gap: 12px; justify-content: center;
  background: var(--gold-soft); border-bottom: 1px solid var(--gold);
  padding: 7px 14px; font: 500 12.5px var(--font-ui); color: var(--ink);
}
.uh-text span { color: var(--ink2); }
.uh-btn {
  border: 1px solid var(--gold); background: transparent; color: var(--gold-text);
  border-radius: 999px; padding: 4px 12px; font: 600 12px var(--font-ui); cursor: pointer;
}
.uh-btn:hover { background: var(--gold); color: var(--on-gold); }
.uh-x { border: none; background: none; color: var(--ink3); cursor: pointer; font-size: 13px; padding: 4px; }
/* Свотчи тона обложки (конструктор + поповер библиотеки) */
.cover-swatches { display: flex; flex-wrap: wrap; gap: 10px; }
.cp-swatch {
  width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line2);
  cursor: pointer; transition: transform .15s, box-shadow .15s;
}
.cp-swatch:hover { transform: translateY(-2px); }
.cp-swatch.on { box-shadow: 0 0 0 2px var(--gold); border-color: var(--gold); }
.cp-auto {
  height: 34px; border-radius: 8px; border: 1px dashed var(--line2); background: transparent;
  color: var(--ink3); padding: 0 12px; font: 600 12px var(--font-ui); cursor: pointer;
}
.cp-auto.on { border-color: var(--gold); color: var(--gold-text); }
.cover-pop {
  position: absolute; top: 44px; right: 10px; z-index: 5; display: flex; flex-wrap: wrap;
  gap: 8px; max-width: 200px; background: var(--surface); border: 1px solid var(--line2);
  border-radius: 12px; padding: 10px; box-shadow: var(--shadow-soft);
}
.card-cover {
  border: none; background: none; cursor: pointer; font-size: 14px; opacity: .75; padding: 4px;
}
.card-cover:hover { opacity: 1; }
.packs-head { font: 700 14.5px var(--font-ui); color: var(--ink); margin: 4px 0 10px; }
.packs-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
.pack-card {
  display: flex; flex-direction: column; gap: 3px; align-items: center; cursor: pointer;
  border: 1px solid var(--line2); border-radius: 12px; background: var(--raised);
  padding: 12px 18px; min-width: 108px; min-height: var(--tap);
}
.pack-card:hover { border-color: var(--gold); }
.pack-card[disabled] { opacity: .55; cursor: default; }
.pack-amount { font: 700 15px var(--font-ui); color: var(--gold-text); }
.pack-price { font: 500 12.5px var(--font-ui); color: var(--ink2); }
.plans-foot { font: 400 12.5px var(--font-ui); color: var(--ink3); line-height: 1.5; margin: 6px 0 0; }

/* --- гейт-пейвол / пред-чекаут / успех (дизайн «Fansfics Premium») -------- */
.gate-card { max-width: 430px; text-align: center; }
.gate-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; border-radius: 50%;
  background: var(--gold-soft); border: 1px solid var(--gold); font-size: 20px;
}
.gate-title { font: 600 22px var(--font-serif); line-height: 1.25; margin-top: 12px; }
.gate-desc { margin: 10px 0 4px; font-size: 13.5px; line-height: 1.6; color: var(--ink2); }
.gate-bar { display: block; height: 6px; border-radius: 999px; background: var(--line2); overflow: hidden; margin-top: 12px; }
.gate-bar > span { display: block; width: 100%; height: 100%; background: var(--danger); }
.gate-offer {
  margin: 18px 0 14px; background: var(--gold-soft); border: 1px solid var(--gold);
  border-radius: 12px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; text-align: left;
}
.go-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.go-main b { font-size: 14px; }
.go-line { font-size: 12px; color: var(--ink2); }
.go-price { font: 600 18px var(--font-serif); color: var(--gold-text); white-space: nowrap; }
.gate-cta { width: 100%; }
.gate-links { display: flex; justify-content: center; gap: 18px; margin-top: 12px; }

.co-card { max-width: 470px; }
.co-sum { background: var(--raised); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; margin-bottom: 14px; }
.co-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.co-name { font-size: 14.5px; font-weight: 600; }
.co-chip {
  font: 600 10.5px var(--font-ui); letter-spacing: .08em; text-transform: uppercase;
  color: var(--gold-text); background: var(--gold-soft);
  border: 1px solid var(--gold); border-radius: 999px; padding: 2px 9px;
}
.co-total { margin-left: auto; font: 600 21px var(--font-serif); }
.co-permo { font-size: 12px; color: var(--ink3); margin-top: 5px; }
.co-alt { margin-top: 6px; font-size: 12.5px; }
.co-quota { font-size: 12px; color: var(--ink2); margin-top: 8px; padding-top: 10px; border-top: 1px dashed var(--line); }
.co-rail {
  background: var(--raised); border: 1px solid var(--line); border-radius: 12px;
  padding: 12px 14px; font-size: 12.5px; line-height: 1.55; color: var(--ink2); margin-bottom: 14px;
}
.co-cta { width: 100%; }
.co-foot { margin: 12px 0 0; text-align: center; font-size: 11.5px; line-height: 1.6; color: var(--ink3); }
.co-foot a { color: var(--ink3); }

.pd-wrap { text-align: center; padding: 8px 0 2px; }
.pd-check {
  display: inline-flex; align-items: center; justify-content: center;
  width: 56px; height: 56px; border-radius: 50%;
  background: rgba(148, 179, 131, .14); border: 1px solid var(--good); color: var(--good); font-size: 24px;
}
.pd-note { margin: 8px 0 0; font-size: 13.5px; color: var(--ink2); }
.pd-quota { margin: 4px 0 0; font-size: 13.5px; color: var(--gold-text); font-weight: 600; }
#paydone-ok { width: 100%; margin-top: 16px; }

/* --- профиль: карточки подписки и языка ----------------------------------- */
.profile-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin: 14px 0 18px; }
.profile-cards:empty { display: none; }
.pcard {
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
  padding: 16px; display: flex; flex-direction: column; gap: 10px;
}
.pcard-head { display: flex; align-items: center; gap: 10px; }
.pcard-kicker { font: 600 11px var(--font-ui); letter-spacing: .14em; text-transform: uppercase; color: var(--gold-text); }
.pcard-sub { font-size: 12.5px; color: var(--ink3); margin-top: -4px; }
.pq-top { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; font-size: 12.5px; color: var(--ink2); }
.pq-count { margin-left: auto; font-size: 12px; color: var(--ink3); font-variant-numeric: tabular-nums; }
.pq-bar { display: block; height: 4px; border-radius: 999px; background: var(--line); overflow: hidden; }
.pq-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--gold); }
/* Кабинет, служебный доступ (QA3, пункт 8): строка занимает место полосы пула.
   Две соседние полосы — дневные квоты, они действуют и на админа, поэтому
   единственный, кто здесь меняется, — месячный пул. */
.pq-nolimit .pq-top { color: var(--gold-text); font-weight: 600; margin-bottom: 2px; }
.pcard-cta { margin-top: auto; align-self: flex-start; padding: 10px 16px; font-size: 13px; }
.seg { display: flex; flex-wrap: wrap; gap: 4px; align-self: flex-start; background: var(--raised); border: 1px solid var(--line); border-radius: 12px; padding: 4px; }
.seg-btn {
  border: none; border-radius: 999px; padding: 7px 13px; font: 600 12.5px var(--font-ui);
  cursor: pointer; background: none; color: var(--ink3); transition: background .15s, color .15s;
  /* TL-021: 7 + 7 + ~15 ≈ 29px высоты (замер приёмки). Пять кнопок языка
     интерфейса (billing.js, карточка «Язык интерфейса») — то, что на телефоне
     жмут пальцем, и промах здесь меняет язык всего интерфейса. `.seg` уже
     `flex-wrap: wrap`, так что ряд из пяти 44-px пилюль на 320px перенесётся
     сам; `rp-seg-btn` в читалке — ДРУГОЙ класс, побочных жертв нет. */
  min-height: var(--tap); display: inline-flex; align-items: center;
}
.seg-btn.on { background: var(--gold); color: var(--on-gold); }
.pcard-note { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--ink2); }
.pcard-fine { margin: auto 0 0; font-size: 11.5px; color: var(--ink3); font-style: italic; }

/* --- вход через Google + модалка даты рождения ---------------------------- */
.auth-google-row { margin: 12px 0 4px; }
.auth-or { display: flex; align-items: center; gap: 10px; color: var(--ink3); font-size: 12px; margin-bottom: 10px; }
.auth-or::before, .auth-or::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.auth-google-btn {
  display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%;
  min-height: var(--tap); border: 1px solid var(--line2); border-radius: 10px;
  background: var(--surface); color: var(--ink); font: 600 14px var(--font-ui); text-decoration: none;
  transition: border-color .15s;
}
.auth-google-btn:hover { border-color: var(--gold); }
.g-logo {
  display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px;
  border-radius: 50%; background: #fff; color: #4285F4; font: 700 12px var(--font-ui);
  border: 1px solid var(--line2);
}
.bd-input {
  width: 100%; margin-top: 14px; background: var(--raised); border: 1px solid var(--line2);
  border-radius: 10px; color: var(--ink); padding: 12px 14px; font-size: 14.5px;
}
#bd-save { width: 100%; margin-top: 12px; }

/* --- магазин искр (отдельное окно, стиль мобильных игр) ------------------- */
.sparks-card { max-width: 560px; }
.sk-balance { text-align: center; font: 400 14px var(--font-ui); color: var(--ink2); margin-bottom: 16px; }
.sk-balance b { color: var(--gold-text); font-size: 16px; }
.sk-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 14px; }
.sk-pack {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 22px 12px 16px; border-radius: 16px; cursor: pointer;
  border: 1px solid var(--line2); background: linear-gradient(180deg, var(--raised), var(--surface));
  transition: transform .15s, border-color .15s, box-shadow .15s;
}
.sk-pack:hover { transform: translateY(-3px); border-color: var(--gold); box-shadow: var(--shadow-soft); }
.sk-pack[disabled] { opacity: .55; cursor: default; transform: none; box-shadow: none; }
.sk-pack.sk-1 { border-color: var(--gold); }
.sk-pack.sk-2 {
  border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold), var(--shadow-soft);
  background: linear-gradient(180deg, var(--gold-soft), var(--surface));
}
.sk-badge {
  position: absolute; top: -10px; left: 50%; transform: translateX(-50%); white-space: nowrap;
  background: var(--gold); color: var(--on-gold); border-radius: 999px; padding: 3px 10px;
  font: 700 9.5px var(--font-ui); letter-spacing: .08em; text-transform: uppercase;
}
.sk-glyph { font-size: 26px; color: var(--gold-text); letter-spacing: 2px; text-shadow: 0 0 18px rgba(199, 163, 96, .45); }
.sk-amount { font: 600 26px var(--font-serif); color: var(--ink); }
.sk-sub { font: 400 12px var(--font-ui); color: var(--ink3); }
.sk-bonus { min-height: 16px; font: 700 11px var(--font-ui); color: var(--good); }
.sk-price { margin-top: 4px; background: var(--gold); color: var(--on-gold); border-radius: 999px; padding: 6px 14px; font: 700 13px var(--font-ui); }
.sk-foot { margin: 4px 0 0; text-align: center; font: 400 12px var(--font-ui); color: var(--ink3); line-height: 1.5; }
.sparks-open-btn { align-self: flex-start; margin-bottom: 14px; }
.co-rail-test { border-color: var(--good); color: var(--good); }

/* --- подтверждение искр -------------------------------------------------- */
.cc-card { max-width: 420px; }
.cc-text { font: 400 14.5px var(--font-ui); color: var(--ink2); line-height: 1.55; }

/* --- замочки (чипы «✦ Premium» по дизайну «Fansfics Premium») ------------- */
.lock-chip {
  display: inline-flex; align-items: center; margin-left: 6px; vertical-align: middle;
  font: 700 9.5px var(--font-ui); letter-spacing: .08em; text-transform: uppercase;
  color: var(--gold-text); background: var(--gold-soft);
  border: 1px solid var(--gold); border-radius: 999px; padding: 2px 8px;
}
body.tier-free .listen-btn .ba-label::after { content: " ✦"; font-size: 11px; color: var(--gold-text); }
.chapter-art-locked {
  border: 1.5px dashed var(--gold); border-radius: 14px; background: var(--gold-soft);
  padding: 26px 18px; text-align: center; cursor: pointer; margin: 22px 0;
}
.chapter-art-locked:hover { background: rgba(199,163,96,.2); }
.cal-inner { display: flex; flex-direction: column; gap: 6px; align-items: center; }
.cal-lock { font-size: 26px; }
.cal-inner b { font: 700 15px var(--font-ui); color: var(--ink); }
.cal-inner span { font: 400 13px var(--font-ui); color: var(--ink2); }
.cal-cta { color: var(--gold-text) !important; font-weight: 600 !important; margin-top: 4px; }

/* --- i18n: переключатель языка (2026-07-18) ----------------------------- */
.locale-switch {
  display: inline-flex; align-items: center; gap: 5px; margin-right: 8px;
  color: var(--ink2); font: 500 13px var(--font-ui);
}
.locale-switch select {
  appearance: none; -webkit-appearance: none; border: 1px solid var(--line2);
  background: var(--surface); color: var(--ink2); border-radius: 8px;
  padding: 5px 8px; font: 500 12.5px var(--font-ui); cursor: pointer; min-height: 30px;
}
.locale-switch select:hover { border-color: var(--gold); color: var(--ink); }
.ls-glyph { font-size: 14px; opacity: .85; }
@media (max-width: 640px) { .locale-switch .ls-glyph { display: none; } }

/* --- верификация: неверифицированный = гость (только каталог) ------------ */
body.email-unverified .auth-only-nav { display: none !important; }

/* ---- публичный профиль автора: ссылки-имена + модалка (2026-07-22) ---- */
.author-link { background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; }
.author-link:hover { color: var(--accent); text-decoration: underline; }
.card-author.author-link { color: var(--ink3); }
.card-author.author-link:hover { color: var(--accent); }
.author-name { font-weight: 600; font-size: 17px; }
.author-bio { color: var(--ink-soft); margin: 6px 0 12px; white-space: pre-wrap; }
.author-books { display: flex; flex-direction: column; gap: 8px; max-height: 45vh; overflow-y: auto; margin-top: 8px; }
.author-book { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 9px 12px; border: 1px solid var(--line); border-radius: 10px;
  background: none; font: inherit; color: inherit; cursor: pointer; text-align: left; }
.author-book:hover { border-color: var(--accent); }
.author-book .ab-title { font-weight: 600; flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.author-book .ab-meta { color: var(--ink-soft); font-size: 13px; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 4px; }

/* ---- «← Назад» на странице тарифов (возврат откуда пришли, 2026-07-22) ---- */
.plans-back { display: inline-block; margin: 2px 0 8px; font-size: 14.5px; }

/* ---- Отношения v1 (вкладка читалки, docs/RELATIONSHIPS_DESIGN.md §4) ---- */
.rel-card { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; margin-bottom: 10px; background: var(--sheet); }
.rel-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.rel-ava { width: 30px; height: 30px; border-radius: 50%; background: var(--sheet-deep);
  border: 1px solid var(--line); display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; flex: 0 0 auto; }
.rel-name { font-weight: 600; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rel-level { font-size: 13px; padding: 2px 9px; border-radius: 999px; border: 1px solid var(--line); white-space: nowrap; }
/* Ступени — ТОКЕНАМИ, а не хексом (P1-7): вписанная краска не умеет светлеть
   вместе с темой, и в светлой пилюли выцветали до 2.67-3.82:1. Рамка — тот же
   токен на 33% (прежний суффикс `55` = 0x55/0xFF), чтобы обвод и подпись
   продолжали быть одним цветом. */
.rel-enmity .rel-level { color: var(--rel-enmity);
  border-color: color-mix(in oklab, var(--rel-enmity) 33%, transparent); }
.rel-dislike .rel-level { color: var(--rel-dislike);
  border-color: color-mix(in oklab, var(--rel-dislike) 33%, transparent); }
.rel-neutral .rel-level { color: var(--ink-soft); }
.rel-warmth .rel-level { color: var(--rel-warmth);
  border-color: color-mix(in oklab, var(--rel-warmth) 33%, transparent); }
.rel-trust .rel-level { color: var(--rel-trust);
  border-color: color-mix(in oklab, var(--rel-trust) 33%, transparent); }
.rel-devotion .rel-level { color: var(--rel-devotion);
  border-color: color-mix(in oklab, var(--rel-devotion) 33%, transparent); }
.rel-scale { position: relative; height: 8px; border-radius: 999px; background: var(--sheet-deep);
  border: 1px solid var(--line); margin: 4px 0 6px; }
.rel-zero { position: absolute; left: 50%; top: -2px; bottom: -2px; width: 1px; background: var(--line); }
.rel-fill { position: absolute; top: 1px; bottom: 1px; border-radius: 999px;
  background: linear-gradient(90deg, var(--rel-warmth), var(--rel-devotion)); }
.rel-fill.neg { background: linear-gradient(270deg, var(--rel-dislike), var(--rel-enmity)); }
.rel-dot { position: absolute; top: 50%; width: 12px; height: 12px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--sheet); transform: translate(-50%, -50%); }
.rel-spark { display: block; width: 72px; height: 22px; margin: 2px 0; }
.rel-spark path { fill: none; stroke: var(--accent); stroke-width: 1.6; opacity: .8; }
.rel-last { font-size: 13.5px; color: var(--ink-soft); }
.rel-history { margin-top: 10px; }
.rel-history summary { cursor: pointer; font-weight: 600; }
.rel-log-line { font-size: 13px; color: var(--ink-soft); padding: 3px 0; border-bottom: 1px dashed var(--line-soft); }
/* .rel-delta / .rel-level-change удалены 2026-08-21: реакции персонажей на
   главу больше не отдельная плашка под прозой, а строка общего столбца
   .chapter-delta (см. блок «дельта главы» выше). */
@media (max-width: 360px) { .rel-spark { display: none; } }

/* ---- «Интуиция сердца» (Отношения, этап 2) ---- */
.rel-hints { display: inline-flex; gap: 4px; margin-left: auto; align-self: flex-start; flex: 0 0 auto; }
.rel-hint { font-size: 11px; padding: 2px 6px; border-radius: 999px; border: 1px solid var(--line);
  color: var(--ink-soft); white-space: nowrap; }
.rel-hint.pos { color: var(--rel-trust);
  border-color: color-mix(in oklab, var(--rel-trust) 33%, transparent); }
.rel-hint.neg { color: var(--rel-enmity);
  border-color: color-mix(in oklab, var(--rel-enmity) 33%, transparent); }
.finalizing-note { font-size: 13px; color: var(--ink2); margin: 10px 0 0;
  padding: 8px 12px; border: 1px dashed var(--line); border-radius: 10px; }
.rel-whose { font-size: 13px; margin: 0 0 10px; }
.rel-whose b { color: var(--ink); font-weight: 600; }
/* --- панель «Отношения» v2 (дизайн 07-24) ---------------------------------- */
/* Обёртка контента вкладки из макета: без неё карточки липнут к краям шторки. */
#panel-relations { padding: 20px 22px 34px; }
.rel-whose2 { display: flex; align-items: center; gap: 8px; padding-bottom: 12px;
  border-bottom: 1px solid var(--line); }
/* аватар протагониста в шапке (2026-08-10): тот же .rel-ava2, что и в парах */
.rel-whose2 .rel-ava2 { margin-left: 2px; }
.rel-whose2 .k { font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--ink3); }
.rel-whose2 b { font-family: var(--font-serif); font-size: 15px; font-weight: 600; color: var(--ink); }

/* --- Safety обсуждений (2026-08-11, по ревью): жалоба и mute — НЕ красные --- */
/* Кнопки жалобы/скрытия носят .comment-del ради базовой типографики, но не
   должны краснеть как удаление и расползаться по строке (у .comment-del
   margin-left:auto — три штуки в флексе делили свободное место поровну). */
.comment-meta .comment-report,
.comment-meta .comment-mute { margin-left: 0; }
.comment-meta .comment-del:not(.comment-report):not(.comment-mute) { margin-left: auto; }
.comment-report-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-top: 8px; padding: 10px; background: var(--raised); border: 1px solid var(--line);
  border-radius: 10px; }
.comment-report-form .comment-report-reason { flex: 1 1 180px; min-height: 40px;
  font-family: inherit; font-size: 13.5px; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line2); border-radius: 8px; padding: 8px 10px; }
.comment-report-send { color: var(--gold-text); }
.comments-blocked { margin-top: 10px; font-size: 12.5px; color: var(--ink3); }
.rel-intu-card { display: flex; align-items: center; gap: 12px; background: var(--raised);
  border: 1px solid var(--line); border-radius: 12px; padding: 12px 15px; margin: 14px 0 4px; }
.rel-intu-card .txt { flex: 1; min-width: 0; }
.rel-intu-card .t { display: block; font-size: 14px; font-weight: 600; color: var(--ink); }
.rel-intu-card .s { display: block; font-size: 12px; color: var(--ink3); font-style: italic; margin-top: 2px; }
.rel-sw { flex: none; position: relative; width: 38px; height: 22px; border-radius: 999px;
  border: none; cursor: pointer; background: var(--line2); transition: background .2s; padding: 0; }
.rel-sw.on { background: var(--gold); }
.rel-sw .knob { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--ink2); transition: left .2s, background .2s; }
.rel-sw.on .knob { left: 19px; background: var(--on-gold); }
/* Тач-таргет: пилюля свитча по макету всего 38×22 — палец в неё не попадает.
   Расширяем ЗОНУ НАЖАТИЯ псевдоэлементом до var(--tap), вид оставляя макетным.
   ::before рисуется до .knob (порядок в дереве), поэтому кноб не перекрывает. */
.rel-sw::before { content: ""; position: absolute; top: 50%; left: 50%;
  width: var(--tap); height: var(--tap); transform: translate(-50%, -50%); }
.rel-card2 { padding: 16px 0 14px; border-bottom: 1px dashed var(--line); }
.rel-head2 { display: flex; align-items: center; gap: 10px; }
.rel-ava2 { position: relative; flex: none; width: 26px; height: 26px; border-radius: 50%;
  color: #fff; font-size: 12px;
  font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
/* Аватар-картинка персонажа (2026-07-28): ложится АБСОЛЮТОМ поверх цветного
   кружка с инициалом. Инициал остаётся в DOM всегда — если портрет не доехал
   (его нет у реального человека, фандом снят, картинку ещё не завезли, протух
   токен), reader.js снимает <img>, и под ним уже готовая буква. `has-img` тут
   только для того, чтобы это снятие было одной операцией. */
.rel-ava2.has-img { overflow: hidden; }
.rel-ava2 > img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border-radius: 50%; object-fit: cover; display: block; background: inherit;
}
.rel-name2 { font-size: 14px; font-weight: 600; color: var(--ink); }
.rel-pill { margin-left: auto; flex: none; font-size: 10.5px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; border-radius: 999px; padding: 3px 9px; }
/* Оттенки пилюли берутся ОТ ТОКЕНА, а не хексом (макет Reader Chapter
   Interactive, 2026-08-21). Вписанное числом золото #C7A360 оставалось тёмным
   и в светлой теме, где --gold уже #876527: пилюля выпадала из палитры
   страницы, и то же самое повторялось у .sd-rels, которая её переиспользует. */
.rel-pill.warm { color: var(--gold-text);
  background: color-mix(in oklab, var(--gold) 15%, transparent);
  border: 1px solid color-mix(in oklab, var(--gold) 45%, transparent); }
.rel-pill.bad { color: var(--danger);
  background: color-mix(in oklab, var(--danger) 14%, transparent);
  border: 1px solid color-mix(in oklab, var(--danger) 45%, transparent); }
.rel-pill.neutral { color: var(--ink2); background: color-mix(in oklab, var(--ink) 6%, transparent);
  border: 1px solid var(--line2); }
.rel-meter { position: relative; height: 20px; margin: 10px 1px 0; }
.rel-meter .trk { position: absolute; left: 0; right: 0; top: 8.5px; height: 3px; border-radius: 2px; background: var(--line2); }
.rel-meter .tick { position: absolute; left: 50%; top: 5px; width: 1px; height: 10px;
  background: color-mix(in oklab, var(--ink) 25%, transparent); }
.rel-meter .fill { position: absolute; top: 8.5px; height: 3px; border-radius: 2px; background: var(--gold); }
.rel-meter .fill.neg { background: var(--danger); }
/* Ореол точки — тем же цветом знака, прозрачным на 20% (по той же причине,
   что и у пилюли: из хекса он не пережил бы светлую тему). */
.rel-meter .dot { position: absolute; top: 5.5px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--gold); box-shadow: 0 0 0 3px color-mix(in oklab, var(--gold) 20%, transparent); }
.rel-meter .dot.neg { background: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 20%, transparent); }
/* opacity: .8 убран (P1-7): --ink3 × .8 = 3.26:1 в светлой теме при кегле
   10.5px — это самая мелкая подпись шкалы отношений, и множитель отнимал у неё
   как раз проход AA. Тише соседей она и так: кегль и курсив. */
.rel-poles { display: flex; justify-content: space-between; font-size: 10.5px; color: var(--ink3);
  margin: 1px 1px 0; font-style: italic; }
.rel-last2 { margin: 8px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--ink3); }
.rel-last2 b.pos { color: var(--good); font-weight: 600; }
.rel-last2 b.neg { color: var(--danger); font-weight: 600; }
/* Кнопка-заголовок сворачиваемой секции («Больше не в истории», «История
   отношений»). min-height — не декор: с макетными 16/10 по вертикали и 10.5px
   капсом кнопка выходила ~39px, ниже тач-минимума проекта. */
.rel-hist-tg { display: flex; align-items: center; gap: 10px; width: 100%; background: none;
  border: none; cursor: pointer; padding: 16px 0 10px; font-family: var(--font-ui); font-size: 10.5px;
  font-weight: 600; text-transform: uppercase; letter-spacing: .14em; color: var(--ink3);
  min-height: var(--tap); box-sizing: border-box; }
.rel-hist-tg:hover { color: var(--gold-text); }
.rel-hist-tg .chev { display: inline-block; font-size: 11px; transition: transform .2s; }
.rel-hist-tg.open .chev { transform: rotate(90deg); }
.rel-hist-tg .rule { flex: 1; height: 1px; background: var(--line); }
.rel-tl-row { display: flex; gap: 12px; font-size: 13px; }
.rel-tl-rail { display: flex; flex-direction: column; align-items: center; width: 22px; flex: none; }
.rel-tl-rail .node { flex: none; width: 9px; height: 9px; margin-top: 4px;
  border: 1.5px solid color-mix(in oklab, var(--gold) 55%, transparent); border-radius: 50%; }
.rel-tl-rail .node.leaf { width: 11px; height: 11px; margin-top: 3px; background: var(--gold);
  border: none; border-radius: 0 100% 0 100%; transform: rotate(-45deg); }
.rel-tl-rail .thread { flex: 1; width: 0; margin-top: 4px; border-left: 1px dashed var(--line2); }
.rel-tl-body { flex: 1; min-width: 0; padding-bottom: 14px; }
.rel-tl-ch { font-size: 11px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--ink3); }
.rel-tl-body p { margin: 3px 0 0; line-height: 1.5; color: var(--ink2); }
.rel-tl-body b { color: var(--ink); font-weight: 600; }
.rel-tl-body b.pos { color: var(--good); }
.rel-tl-body b.neg { color: var(--danger); }
.rel-intuition-toggle { display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
  padding: 9px 12px; border: 1px dashed var(--line); border-radius: 10px; cursor: pointer; }
.rel-intuition-toggle span { font-weight: 600; }
.rel-intuition-toggle .note { font-size: 12.5px; }
/* Отношения этап 3: секция пар, полка выбывших */
.rel-sec-head { padding: 18px 0 4px; font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--ink3); }
.rel-duo { display: inline-flex; flex: none; }
.rel-duo .rel-ava2 + .rel-ava2 { margin-left: -8px; box-shadow: 0 0 0 2px var(--surface); }
.rel-ava2.gone { opacity: .55; filter: grayscale(.4); }
.rel-gone-row { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0;
  font-size: 13px; color: var(--ink3); }
.rel-gone-row b { color: var(--ink2); font-weight: 600; }
/* «(гл. N)» и причина выбытия носят общий .muted, а он ЯРЧЕ (--ink2) самой
   строки (--ink3) — на приглушённой полке хвост получался заметнее имени.
   По макету это самый тихий слой записи, отсюда наследование цвета строки.
   Прежний множитель opacity: .7 поверх наследования убран (P1-7): он давал
   3.33:1 в светлой теме. Задача правила — «не ярче строки», и равенство её
   решает; тише строки хвосту быть уже некуда, --ink3 и есть дно лестницы. */
.rel-gone-row .muted { color: inherit; }
.rel-gone-body { line-height: 1.5; padding-top: 3px; }

/* ------------------------------------------------------------ конструктор облика
   (2026-07-29, группа B). Модалка creator/creator.js: превью-канвас слева,
   группы опций справа; на узком экране — столбиком. Классы .modal/.modal-card
   уже дают каркас, здесь только внутренности. */
.creator-card { max-width: 880px; }
.creator-body { display: flex; gap: 22px; align-items: flex-start; }
.creator-preview { flex: none; width: 300px; max-width: 42vw; position: sticky; top: 0; }
.creator-preview canvas {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--line); border-radius: 14px;
  /* до первой отрисовки — нейтральная подложка вместо белой дыры */
  background: var(--raised);
}
.creator-controls { flex: 1; min-width: 0; }
.creator-group { margin-bottom: 14px; }
.creator-group h4 {
  margin: 0 0 7px; font-size: 10.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .12em; color: var(--gold-text);
}
.creator-opts { display: flex; flex-wrap: wrap; gap: 6px; }
.creator-opt {
  border: 1px solid var(--line2); border-radius: 999px;
  background: var(--surface); color: var(--ink2);
  font-family: var(--font-ui); font-size: 12.5px; line-height: 1;
  padding: 8px 12px; min-height: 32px; cursor: pointer;
  transition: border-color .15s, color .15s, background .15s;
}
.creator-opt.on {
  border-color: var(--gold); color: var(--ink);
  background: var(--gold-soft); font-weight: 600;
}
.creator-swatch {
  width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--line2); cursor: pointer; padding: 0;
  transition: box-shadow .15s, transform .15s;
}
.creator-swatch.on {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--gold);
  transform: scale(1.08);
}
/* кнопка «Создать облик» в карточке кастомного персонажа переехала в блок
   «персонажи» (.c-creator-btn, золотая пилюля по макету 2026-08-09) */
@media (max-width: 719px) {
  .creator-body { flex-direction: column; }
  .creator-preview { position: static; width: 100%; max-width: 300px; margin: 0 auto; }
}

/* ============================================================
   «Живая история», фаза A (docs/LIVING_STORY_PLAN_2026-08-04.md §3)
   Дневник последствий (🌱/🔥), нехоженые тропы, порог сцены,
   рекап «Пока вас не было», бейдж непрочитанной главы.
   Цвета — только токены темы: тёмная (:root) и светлая
   (:root[data-theme="light"]) получают блок автоматически.
   ============================================================ */

/* ---- «Последствия ваших выборов»: общая рамка секции ----
   Макет Reader Chapter Interactive (2026-08-21). Заголовок нужен как ГРАНИЦА:
   ниже — только то, что случилось из-за читателя, а ход мира начинается уже
   за пределами секции. Подпись не переносится (иначе линейка прыгала бы под
   неё), но и не толкает страницу вширь — длинный перевод обрежется. */
.ls-consq { max-width: 640px; margin: 34px auto 0; }
.ls-consq-head {
  display: flex; align-items: center; gap: 12px; margin: 0 0 14px;
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .16em; color: var(--ink3);
}
.ls-consq-head > span {
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ls-consq-head::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.ls-consq-body { display: flex; flex-direction: column; gap: 8px; }
/* Дети секции идут ВО ВСЮ ширину колонки. Свои `max-width:640px; margin:0 auto`
   они носят с прежней вёрстки, где стояли прямо в потоке главы, — здесь колонку
   держит уже сама .ls-consq. А авто-поля по горизонтали ОТМЕНЯЮТ stretch у
   элемента flex-колонки: карточки съёживались по длине своего текста и вставали
   лесенкой по центру, при том что заголовок секции и соседние блоки выровнены
   по левому краю (макет Reader Chapter Interactive, 2026-08-21: столбец
   заподлицо). Вертикальные поля не трогаем — ими разведены чипы и «назревает».
   Селектор с ДВУМЯ классами намеренно: у .ls-fired/.ls-seeds/.ls-beats своя
   `max-width` объявлена НИЖЕ по файлу, и при равной специфичности выигрывали
   бы они — правило молча не работало бы. */
.ls-consq .ls-consq-body > * { max-width: none; margin-left: 0; margin-right: 0; }

/* ---- A-2: «выстрелило» / «отозвалось эхом» под прозой ----
   Сетка 20px/1fr, а не флекс: глиф держит свою колонку, а цитата выбора и
   пояснение встают точно под лейблом, не под глифом. */
.ls-fired {
  display: grid; grid-template-columns: 20px minmax(0, 1fr);
  column-gap: 10px; row-gap: 3px;
  max-width: 640px; margin: 0 auto; padding: 12px 14px;
  border: 1px solid var(--gold); border-radius: 14px;
  background: var(--gold-soft); color: var(--ink);
}
.ls-fired.echoed { border-color: var(--line2); background: var(--state-neutral-bg); }
.ls-fired .ls-ico { font-size: 15px; line-height: 1.4; }
.ls-fired-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 6px; min-width: 0;
}
.ls-kick {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--gold-text);
}
/* Метка тише лейбла: «Выстрелило» — событие, «· выбор в гл. 3» — сноска к нему.
   Разницу несут регистр и разрядка (.ls-kick — uppercase + letter-spacing,
   сноска — нет), а не прозрачность: opacity: .7 роняла --gold-text до 3.00:1 в
   светлой теме и убрана вместе с остальными множителями (P1-7). */
.ls-kick-meta { font-size: 10.5px; font-weight: 600; color: var(--gold-text); }
.ls-kick-meta::before { content: "· "; }
.ls-fired.echoed .ls-kick,
.ls-fired.echoed .ls-kick-meta { color: var(--ink3); }
.ls-kick-n { font-variant-numeric: tabular-nums; }
.ls-quote {
  grid-column: 2; font-family: var(--font-serif); font-style: italic;
  font-size: 14px; line-height: 1.55; color: var(--ink);
}
.ls-payoff { grid-column: 2; font-size: 12.5px; line-height: 1.5; color: var(--ink2); }

/* ---- A-2: чипы «ещё аукнется» / «предмет сыграл» ---- */
.ls-seeds {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  max-width: 640px; margin: 8px auto 0;
}
.ls-seed {
  display: inline-flex; align-items: baseline; gap: 6px; max-width: 100%;
  padding: 6px 11px; border-radius: 999px;
  border: 1px dashed var(--line2); background: var(--state-neutral-bg);
  font-size: 12.5px; color: var(--ink2);
}
.ls-seed .ls-ico { font-size: 12.5px; }
.ls-seed-txt {
  font-family: var(--font-serif); font-style: italic; color: var(--ink3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px;
}
/* «· с гл. 2» у чипа сыгравшего предмета — та же сноска, что у карточек. */
.ls-chip-meta { color: var(--ink3); white-space: nowrap; font-variant-numeric: tabular-nums; }
.ls-chip-meta::before { content: "· "; }

/* ---- A-6: «на пороге сцены» ---- */
.ls-beats { max-width: 640px; margin: 12px auto 0; text-align: center; }
.ls-beat {
  display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 6px;
  font-family: var(--font-serif); font-style: italic; font-size: 14px; color: var(--ink2);
}
.ls-beat .ls-ico { font-style: normal; color: var(--gold-text); }
.ls-beat b { font-style: normal; font-weight: 600; color: var(--ink); }
.ls-beat + .ls-beat { margin-top: 4px; }
/* пульсирующая точка на вкладке «Отношения» (глиф-лист, как у «пишется…») */
.drawer-tab.has-beat { position: relative; }
.drawer-tab.has-beat::after {
  content: ""; position: absolute; top: 9px; right: 2px;
  width: 7px; height: 7px; border-radius: 0 100% 0 100%; transform: rotate(-45deg);
  background: var(--gold); animation: leaf-pulse 1.6s ease-in-out infinite;
}

/* ---- A-9: нехоженые тропы (они же «закрытые двери») ---- */
.ls-roads { max-width: 640px; margin: 18px auto 0; }
.ls-roads-head {
  display: flex; align-items: center; gap: 12px;
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--ink3); margin-bottom: 8px;
}
.ls-roads-head::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.ls-road {
  display: flex; align-items: baseline; gap: 10px;
  padding: 8px 10px; border-radius: 10px;
  border: 1px dashed var(--line); background: var(--state-neutral-bg);
  /* opacity: .8 убран (P1-7): --ink3 × .8 = 3.26:1. Без множителя --ink3 на
     --state-neutral-bg светлой темы (#ECEAE6) даёт 4.58:1 — «дорога, которой вы
     не пошли» остаётся пунктирной и тихой, но читаемой. */
  font-size: 13.5px; color: var(--ink3);
}
.ls-road + .ls-road { margin-top: 6px; }
.ls-road-axis { flex: none; font-size: 13px; filter: grayscale(.55); }
.ls-road-text { flex: 1; font-family: var(--font-serif); font-style: italic; line-height: 1.45; }
.ls-road-pct { flex: none; font-size: 11.5px; color: var(--ink3); font-variant-numeric: tabular-nums; }
.ls-road-note { margin-top: 7px; font-size: 12px; color: var(--ink3); text-align: right; }
.ls-road-act { margin-top: 10px; text-align: center; }
.ls-whatif { font-size: 13px; opacity: .85; }

/* ---- A-2: секция «Эхо ваших выборов» в панели «Память» ---- */
.ls-echo-row { border-bottom: 1px dashed var(--line); padding: 10px 0; font-size: 13.5px; }
.ls-echo-row.s-active { opacity: .85; }
.ls-echo-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px;
  font-size: 11.5px; color: var(--ink3);
}
.ls-echo-ch { font-variant-numeric: tabular-nums; }
.ls-echo-st { font-weight: 600; }
.ls-echo-row.s-paid .ls-echo-st { color: var(--gold-text); }
.ls-echo-row.s-echoed .ls-echo-st { color: var(--ink2); }
.ls-echo-row p { margin: 4px 0 0; line-height: 1.5; color: var(--ink2);
  font-family: var(--font-serif); font-style: italic; }
.ls-echo-payoff { font-style: normal !important; color: var(--ink3) !important; font-size: 13px; }

/* ---- A-7: рекап «Пока вас не было» ---- */
.living-recap {
  /* колонка прозы: как .ls-fired/.ls-roads и сама .prose (640px) */
  max-width: 640px; margin: 0 auto 26px; padding: 15px 17px;
  border: 1px solid var(--line2); border-radius: 14px;
  background: var(--raised); box-shadow: var(--shadow-soft);
}
.living-recap.hidden { display: none; }
.ls-recap-head { display: flex; align-items: center; gap: 10px; }
.ls-recap-kick {
  flex: 1; font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--gold-text);
}
.ls-recap-close {
  flex: none; background: none; border: none; cursor: pointer;
  color: var(--ink3); font-size: 13px; line-height: 1;
  min-width: 32px; min-height: 32px; border-radius: 8px; transition: color .15s;
}
.ls-recap-close:hover { color: var(--ink); }
.ls-recap-title { margin-top: 4px; font-size: 16px; font-weight: 600; color: var(--ink); }
.ls-recap-teaser {
  margin: 6px 0 0; font-family: var(--font-serif); font-style: italic;
  font-size: 14.5px; line-height: 1.5; color: var(--ink2);
}
.ls-recap-list { list-style: none; margin: 11px 0 0; padding: 0; }
.ls-recap-list li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 6px;
  padding: 5px 0; font-size: 13.5px; color: var(--ink2);
  border-top: 1px dashed var(--line);
}
.ls-recap-ch { flex: none; min-width: 46px; font-size: 11.5px; color: var(--ink3);
  font-variant-numeric: tabular-nums; }
.ls-recap-list b { font-weight: 600; color: var(--ink); }
.ls-recap-pressure { margin-top: 11px; padding-top: 9px; border-top: 1px dashed var(--line); }
.ls-recap-sub {
  display: block; margin-bottom: 4px;
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3);
}
.ls-recap-pressure p {
  margin: 0 0 3px; font-family: var(--font-serif); font-style: italic;
  font-size: 13.5px; line-height: 1.5; color: var(--ink2);
}
.ls-recap-act { margin-top: 12px; }
.ls-recap-back { font-size: 13px; }

/* ---- A-8: бейдж непрочитанной главы + тизер на карточке библиотеки ---- */
.ready-pill {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--gold-text); font-weight: 600;
}
/* Глиф-лист статичен: пульсация в карточке уже занята «пишется…»
   (.writing-pill), два бьющихся индикатора рядом читаются как ошибка. */
.ready-pill::before {
  content: ""; flex: none; width: 8px; height: 8px;
  border-radius: 0 100% 0 100%; transform: rotate(-45deg); background: var(--gold);
}
.card-teaser {
  padding: 4px 2px 0; font-family: var(--font-serif); font-style: italic;
  font-size: 13px; line-height: 1.45; color: var(--ink2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .drawer-tab.has-beat::after { animation: none; }
}

/* ---- P1-3: «Прогресс истории» под главой ----
   Колонка прозы (640px) — как у .ls-roads и .living-recap. Карточка тише
   рекапа намеренно: это справка «где я», а не событие, и спорить за внимание
   с ходом мира и развилкой ниже она не должна. Hover-состояний у неё нет
   вовсе — кликать в ней нечего. */
.season-card {
  max-width: 640px; margin: 20px auto 0; padding: 12px 15px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--state-neutral-bg);
}
.season-bar { display: flex; align-items: center; gap: 12px; }
.season-track {
  flex: 1; height: 3px; border-radius: 2px; background: var(--line2); overflow: hidden;
}
.season-fill { display: block; height: 100%; border-radius: 2px; background: var(--gold); }
.season-count {
  flex: none; font-size: 12px; color: var(--ink2); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.season-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px;
  margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--line);
  font-size: 12.5px; color: var(--ink3);
}
.season-ico { flex: none; font-size: 11px; color: var(--gold-text); }
.season-turn, .season-num { color: var(--ink2); font-variant-numeric: tabular-nums; }
.season-promise, .season-stake {
  font-family: var(--font-serif); font-style: italic; color: var(--ink2);
}
.season-promise {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* «Что на кону» — цельная фраза в 1-2 строки, поэтому она занимает всю
   ширину под подписью, а не жмётся в один ряд с ней. */
.season-stake { flex: 1 1 100%; line-height: 1.45; }

/* ---- P2 «Финальный рывок»: строка-оффер сразу под прогрессом ----
   Ряд, а не карточка: вплотную к .season-card вторая рамка того же веса
   читалась бы как второй блок справки, а здесь одно предложение и одна
   кнопка. Золотая подложка (родня .ls-world-move) поднимает строку над
   прогрессом, но не доводит её до веса развилки — спорить за внимание с
   «Что произойдёт дальше?» оффер не должен. */
.finale-offer {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
  max-width: 640px; margin: 10px auto 0; padding: 10px 15px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--gold-soft);
}
.finale-offer-ico { flex: none; font-size: 12px; color: var(--gold-text); }
.finale-offer-text {
  flex: 1 1 auto;
  font-family: var(--font-serif); font-style: italic;
  font-size: 13.5px; line-height: 1.5; color: var(--ink2);
}
.finale-offer-btn { flex: none; font-size: 12.5px; }

/* ---- P2-7 «Сезон завершён»: конец книги как начало следующей ----
   Карточка тише финальной виньетки намеренно: «Конец» — это событие книги, а
   она служебная и стоит НИЖЕ. Ряд действий центрирован по колонке чтения, как
   и сама виньетка над ним. */
.season-done {
  max-width: 640px; margin: 26px auto 0; padding: 18px 18px 20px;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--state-neutral-bg); text-align: center;
}
.season-done-kick {
  font-family: var(--font-serif); font-size: 17px; font-weight: 600; color: var(--ink);
}
.season-done-facts { margin-top: 6px; font-size: 12.5px; color: var(--ink3); }
.season-done-num { color: var(--ink2); font-variant-numeric: tabular-nums; }
/* Хроника пути (ревью механик 2026-08-20): рубрики итога под фактом «Глав
   написано N». Каждая — заголовок-надпись и одна-две строки данных; всё
   центрировано по колонке карточки, как и её CTA. Длинные цитаты выбора и
   заголовки нитей режутся многоточием: карточка обязана остаться карточкой. */
.season-done-chron {
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 12px;
}
.sd-h {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--ink3); margin-bottom: 6px;
}
.sd-rels { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.sd-rel {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 8px 4px 10px; border: 1px solid var(--line2); border-radius: 999px;
}
.sd-rel-name { font-size: 12.5px; color: var(--ink2); }
/* Пилюля уровня приезжает из панели отношений (.rel-pill) — здесь ей не нужен
   её же margin-left:auto, растягивающий строку карточки. */
.sd-rels .rel-pill { margin-left: 0; }
.sd-gone {
  flex: 1 1 100%; margin-top: 4px;
  font-size: 12px; color: var(--ink3);
}
.sd-gone-one { margin-left: 8px; }
.sd-nums { font-size: 12.5px; color: var(--ink2); display: flex;
  flex-wrap: wrap; justify-content: center; gap: 6px; }
.sd-num { color: var(--ink); font-variant-numeric: tabular-nums; }
.sd-quote {
  margin-top: 4px; font-family: var(--font-serif); font-size: 12.5px;
  color: var(--ink3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.season-done-cta {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: 10px; margin-top: 16px;
}
/* Кнопка «возобновить и дописать» приезжает из editor.js своим блоком
   (.write-own-block): в ряду карточки его центрирование и вертикальные поля
   лишние — выравнивание держит сам ряд. */
.season-done-cta .write-own-block { margin: 0; }

@media (max-width: 719px) {
  /* Кнопка уходит под фразу целиком: обрезанное «Завершить историю за…» —
     единственное в этом ряду, что терять нельзя. Тем же приёмом обе кнопки
     карточки финала становятся полноширинными, как «Скачать книгу» над ними. */
  .finale-offer-btn { flex: 1 1 100%; }
  .season-done-cta { display: block; }
  .season-done-cta > * { width: 100%; }
  /* Кнопка возобновления приезжает завёрнутой в свой .write-own-block —
     ширину надо задать ей самой, обёртке её не передать. */
  .season-done-cta .write-own-block > button { width: 100%; }
  .season-done-cta .season-done-new { margin-bottom: 10px; }
}

/* ============================================================
   «Живая история», фазы B и C (docs/LIVING_STORY_PLAN_2026-08-04.md §4-§5)
   B-3 «Мир сделал ход», C-1 часы агенды, C-2 хроника, C-3 молва.
   Те же правила, что у блока фазы A: только токены темы (светлая
   получает всё через :root[data-theme="light"]), никакой анимации
   сверх той, что гасится prefers-reduced-motion.
   ============================================================ */

/* ---- B-3: баннер над развилкой (родня .gen-teaser и .living-recap) ---- */
.ls-world-move {
  /* Отступ сверху крупнее прочих карточек намеренно (макет Reader Chapter
     Interactive 2026-08-21): между итогами главы и ходом мира проходит шов
     «что было» / «что будет», и пауза здесь — часть чтения. */
  max-width: 640px; margin: 44px auto 0; padding: 14px 17px;
  border: 1px solid var(--gold); border-radius: 14px;
  background: var(--gold-soft); color: var(--ink);
}
.ls-wm-kick {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .16em; color: var(--gold-text); margin-bottom: 6px;
}
.ls-wm-text {
  margin: 0; font-family: var(--font-serif); font-style: italic;
  font-size: 14.5px; line-height: 1.6; color: var(--ink);
}
/* Баннер стоит вплотную над блоком выборов — верхний отступ там уже свой. */
.ls-world-move + .choices-block { margin-top: 14px; }

/* ---- B-3: компактная пометка на прошлой главе ---- */
.ls-wm-note {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 7px;
  max-width: 640px; margin: 20px auto 0;
  font-size: 13px; color: var(--ink3);
}
.ls-wm-ico { flex: none; color: var(--gold-text); font-size: 11px; }
.ls-wm-note-kick {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3);
}
.ls-wm-note-text {
  width: 100%; font-family: var(--font-serif); font-style: italic;
  line-height: 1.5; color: var(--ink2);
}

/* ---- вкладка «Мир»: общее ----
   Макет Reader Chapter Interactive (2026-08-21): рубрика секции — золотая, а
   счётчик рядом — прямоугольный бейдж. Правила держим ЛОКАЛЬНЫМИ для
   .ls-world-sec: общий .pad h4 / .mem-group h4 делят между собой все панели
   шторки, и переписывать их ради одной вкладки нельзя. */
.ls-world-sec + .ls-world-sec { margin-top: 22px; }
.ls-world-sec h4 { margin: 0 0 10px; color: var(--gold-text); }
.ls-world-sec h4::after { content: none; }
.ls-world-sec h4 .count { border-radius: 6px; padding: 0 6px; font-size: 10px; }
.ls-world-note { margin: 10px 0 0; font-size: 12px; color: var(--ink3); font-style: italic; }

/* Полоска-сегменты (макет 2026-08-21) вместо глифов ▮▯ — только во вкладке:
   на поверхности чтения полоска остаётся текстовой (см. clockSegBarHtml).
   Сегменты ужимаются: у часов их бывает до 12, а ширина шторки на телефоне
   не резиновая. */
.ls-segs {
  display: inline-flex; gap: 3px; margin-left: auto;
  flex: 0 0 auto; max-width: 55%;
}
/* Ужимаются САМИ сегменты внутри отведённой полоске половины строки, а не
   полоска целиком: иначе на телефоне двенадцать сегментов вылезали бы поверх
   числа. Ширина сегмента задана через width, а НЕ через flex-basis: с basis
   Chrome считает max-content вложенного флекса по минимумам детей, и полоска
   схлопывалась до 4px на сегмент даже там, где места хватало. */
.ls-segs i {
  flex: 0 1 auto; width: 14px; min-width: 4px; height: 5px;
  border-radius: 2px; background: var(--line2);
}
.ls-segs i.on { background: var(--gold); }
.ls-segs-num {
  flex: none; font-size: 11.5px; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}

/* ---- C-1: часы агенды NPC ---- */
.ls-clock {
  border-bottom: 1px dashed var(--line); padding: 9px 0;
  display: flex; flex-direction: column; gap: 4px;
}
/* Последняя строка секции: подпись .ls-world-note ниже — не строка списка,
   и пунктир перед ней читался бы как обрыв. */
.ls-clock:last-of-type, .ls-current:last-of-type { border-bottom: none; }
.ls-clock-line { display: flex; align-items: center; gap: 10px; font-size: 13.5px; color: var(--ink); }
/* Имя ужимается первым (полоска — содержание строки, а не украшение).
   Жир снят по макету (Reader Chapter Interactive, 2026-08-21): в строке агенды
   выделять нечем — имя там И ЕСТЬ строка, а полужирным оно перекрикивало и
   заголовок секции, и соседнюю примету стадии. Тег <b> оставлен: он держит
   смысл «вот сама агенда», а не начертание. Течение — близнец часов по строению
   (см. .ls-current ниже), и расходиться им нельзя. */
.ls-clock-line b, .ls-current-line b {
  flex: 1 1 auto; font-weight: 400; color: var(--ink);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ls-clock-note {
  margin: 0; font-family: var(--font-serif); font-style: italic;
  font-size: 12.5px; line-height: 1.5; color: var(--ink3);
}
.ls-clock-bar {
  font-size: 15px; line-height: 1; letter-spacing: .22em;
  color: var(--gold); font-variant-numeric: tabular-nums;
}

/* ---- C-2: хроника «тем временем…» ---- */
.ls-chron-list { margin-bottom: 4px; }
.ls-chron {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 10px;
  border-bottom: 1px dashed var(--line); padding: 9px 0;
}
.ls-chron:last-child { border-bottom: none; }
.ls-chron-ch {
  flex: none; min-width: 34px; font-size: 11px; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}
/* Запись хроники — не цитата, а факт: по макету 2026-08-21 она набрана
   интерфейсным шрифтом, курсив остаётся у молвы и примет течений. */
.ls-chron p {
  flex: 1; min-width: 0; margin: 0; font-size: 13.5px; line-height: 1.5;
  color: var(--ink2);
}

/* ---- C-3: карточки молвы ---- */
.ls-rumor {
  margin-top: 9px; padding: 10px 12px; border-radius: 10px;
  border: 1px dashed var(--line2); background: var(--state-neutral-bg);
}
.ls-rumor-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 7px;
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3);
}
.ls-rumor-head .ls-ico { font-size: 12px; letter-spacing: normal; }
.ls-rumor-ch {
  margin-left: auto; font-weight: 400; letter-spacing: normal;
  font-variant-numeric: tabular-nums;
}
.ls-rumor-text {
  margin: 6px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--ink2);
  font-family: var(--font-serif); font-style: italic;
}
.ls-rumor.s-confirmed { border-style: solid; border-color: var(--gold); background: var(--gold-soft); }
.ls-rumor.s-confirmed .ls-rumor-head { color: var(--gold-text); }
.ls-rumor.s-debunked { opacity: .72; }
.ls-rumor.s-debunked .ls-rumor-text { text-decoration: line-through; text-decoration-thickness: 1px; }

/* ---- C-2 «Прилив мира»: течения во вкладке «Мир» (П28) ----
   Строение карточки — как у часов агенды (.ls-clock), и полоска у них общая:
   обе отвечают на «сколько ещё осталось». Отличается только третья строка —
   примета текущей стадии, написанная архитектором книги. */
.ls-current {
  border-bottom: 1px dashed var(--line); padding: 9px 0;
  display: flex; flex-direction: column; gap: 6px;
}
.ls-current-line { display: flex; align-items: center; gap: 10px; font-size: 13.5px; color: var(--ink); }
/* Примета стадии — карточка в рамке (макет 2026-08-21): это единственная в
   секции фраза, написанная архитектором книги, и она стоит того, чтобы её
   обвели. */
.ls-current-stage {
  margin: 2px 0 0; padding: 11px 13px;
  border: 1px solid var(--line2); border-radius: 12px;
  font-size: 13px; line-height: 1.55; color: var(--ink2);
  font-family: var(--font-serif); font-style: italic;
}

/* ---- П28: строка-индикатор «Мир» под прозой ----
   Кнопка, а не карточка: это ВХОД во вкладку «Мир», а не блок с содержанием, и
   вес у неё соответствующий — одна строка на колонке чтения, тише прогресса
   сезона. Появляется только в главах, где мир изменился (worldPulseFor), и
   поэтому имеет право быть кликабельной: постоянная строка на каждой главе
   превратилась бы в мебель. */
.world-line {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px;
  max-width: 640px; width: 100%; min-height: var(--tap);
  margin: 18px auto 0; padding: 8px 14px;
  border: 1px dashed var(--line2); border-radius: 999px;
  background: none; color: var(--ink3);
  font-family: var(--font-ui); font-size: 12.5px; text-align: left;
  cursor: pointer;
}
.wl-kick {
  flex: none; font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--gold-text);
}
.wl-part { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.wl-part b {
  font-weight: 600; color: var(--ink2);
  max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Полоска в строке мельче, чем во вкладке: там она содержание, здесь — намёк. */
.world-line .ls-clock-bar { font-size: 12px; letter-spacing: .16em; }
.wl-dot { flex: none; color: var(--line2); }

/* ---- П29 «С чем вы придёте к развязке» ----
   Рубрики внутри — те же .sd-* , что у «Хроники пути» на экране финала
   (карточка обёрнута в .season-done-chron): это одна и та же сводка, показанная
   на два шага раньше, и второй облик у неё завёлся бы только ради новизны.
   Рамка золотая — в отличие от справочного .season-card над ней: развязка на
   пороге, и это единственное место книги, где сводка чем-то грозит. */
.reckon-card {
  max-width: 640px; margin: 14px auto 0; padding: 16px 18px 18px;
  border: 1px solid var(--gold); border-radius: 14px;
  background: var(--gold-soft); text-align: center;
}
.reckon-kick {
  font-family: var(--font-serif); font-size: 16px; font-weight: 600; color: var(--ink);
}
.reckon-sub {
  margin: 6px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--ink3);
}
/* Обёртка рубрик приезжает от экрана финала со своей верхней чертой — здесь
   она отделяет рубрики от подзаголовка ровно так же. */
.reckon-card .season-done-chron { border-top-color: var(--gold); }
.reckon-clock {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-size: 12.5px; color: var(--ink2);
}

@media (hover: hover) {
  /* Свой блок, а не общий выше: на телефоне :hover залипает после тапа, а
     общий блок правят три разные волны сразу (прецедент «Каталога v3»). */
  .world-line:hover { border-color: var(--gold); color: var(--ink2); }
}

/* ============================================================
   ЖИВЫЕ СЕРИАЛЫ (docs/LIVE_SERIALS_DESIGN.md §6)
   Бюллетень голосования в конце главы (#serial-ballot, web/serials.js) и
   бейдж расписания на карточке каталога. Карточки вариантов переиспользуют
   .choice-card — это тот же жест «выбрать ход», и заводить ему второй облик
   значило бы учить читателя двум языкам ради одного действия.
   Hover-состояния живут в общем @media (hover: hover) блоке выше.
   ============================================================ */
.serial-ballot {
  max-width: 640px; margin: 0 auto 40px;
  padding: 20px 18px 18px;
  border: 1px solid var(--line2); border-radius: 14px;
  background: var(--surface);
  font-family: var(--font-ui);
}
.serial-ballot[hidden] { display: none; }
.serial-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* Пилюля «Сериал» — золотая, в отличие от нейтральной .rating-pill: она
   сообщает не свойство книги, а то, что здесь прямо сейчас что-то решается. */
.serial-pill {
  display: inline-flex; align-items: center;
  border: 1px solid var(--gold); border-radius: 5px; padding: 2px 7px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  color: var(--gold-text); background: var(--gold-soft); white-space: nowrap;
}
.serial-title {
  margin-top: 12px;
  font-size: 15.5px; font-weight: 600; line-height: 1.4; color: var(--ink);
}
.serial-timer { margin-top: 6px; font-size: 13px; color: var(--ink2); }
.serial-timer b { font-weight: 600; color: var(--gold-text); font-variant-numeric: tabular-nums; }
/* Полоска времени раунда: тонкая и без анимации — она информирует, а не
   развлекает, и обновляется раз в 30 секунд одним изменением width. */
.serial-clock {
  display: block; height: 2px; border-radius: 1px;
  margin-top: 8px; background: var(--line2); overflow: hidden;
}
.serial-clock-fill { display: block; height: 100%; background: var(--gold); }
.serial-options { margin-top: 16px; }
.serial-option { position: relative; overflow: hidden; }
/* Закрытый бюллетень рисуется теми же карточками, но не кнопками: курсор-рука
   на нежимаемом варианте — обещание, которого интерфейс не выполнит. */
.serial-option:not(button) { cursor: default; }
.serial-option.is-mine { border-color: var(--gold); background: var(--gold-soft); }
.serial-option-text { flex: 1; min-width: 0; }
.serial-option-pct {
  flex: none; font-family: var(--font-ui); font-size: 13px; font-weight: 600;
  color: var(--gold-text); font-variant-numeric: tabular-nums;
}
/* Доля голосов — подложкой у нижней кромки карточки: не отвлекает от текста
   хода, но даёт мгновенное «кто ведёт» одним взглядом по колонке. */
.serial-tally {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 3px; background: transparent; pointer-events: none;
}
.serial-tally-fill { display: block; height: 100%; background: var(--gold); opacity: .55; }
.serial-voted {
  margin-top: 12px; font-size: 13.5px; font-weight: 600; color: var(--good);
}
.serial-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.serial-btn { min-height: 40px; font-size: 13px; }
.serial-hint, .serial-reason {
  margin-top: 12px; font-size: 13px; line-height: 1.45; color: var(--ink2);
}
.serial-reason {
  padding: 10px 12px; border-radius: 10px;
  border: 1px dashed var(--line2); background: var(--state-neutral-bg);
}
.serial-votes {
  margin-top: 12px; font-size: 12.5px; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}
/* Обещание «голос равен любому другому» стоит там, где человек кликает: увезти
   его в справку значит не дать ему сработать (§6.2). */
.serial-fair {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line);
  font-size: 12px; line-height: 1.45; color: var(--ink3);
}
.serial-fork { margin-top: 14px; }
.serial-fork-btn { width: 100%; min-height: 46px; font-size: 13px; line-height: 1.35; }

/* ---- плашка итога прошлого раунда ---- */
.serial-result {
  margin-top: 16px; padding: 12px 14px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--raised);
}
.serial-result.is-new { border-color: var(--gold); background: var(--gold-soft); }
.serial-result-head {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3);
}
.serial-result.is-new .serial-result-head { color: var(--gold-text); }
.serial-result-move {
  margin-top: 6px; font-family: var(--font-serif); font-size: 14.5px;
  line-height: 1.5; font-style: italic; color: var(--ink);
}
.serial-result-mine { margin-top: 8px; font-size: 13px; font-weight: 600; color: var(--ink2); }
.serial-result-mine.is-win { color: var(--good); }
.serial-result-votes {
  margin-top: 6px; font-size: 12px; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}

/* ---- бейдж сериала на карточке каталога ---- */
.serial-badge {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
  padding: 2px 2px 0; font-size: 12px; color: var(--ink2);
}
.serial-badge .serial-clock { flex: 1 1 100%; margin-top: 4px; }
.serial-badge-state { font-weight: 600; }
.serial-badge.is-open .serial-badge-state { color: var(--gold-text); }
.serial-badge.is-writing .serial-badge-state { color: var(--ink2); }
.serial-badge.is-closed .serial-badge-state { color: var(--ink3); }
.serial-badge-left, .serial-badge-votes { color: var(--ink3); font-variant-numeric: tabular-nums; }

/* ============================================================
   П32/П33 (ревью механик 2026-08-20): открытость меры, неизменяемый архив,
   возвратная петля и эпилог сезона. Всё ниже — ТИШЕ бюллетеня: главный экран
   конца главы остаётся ходом, который решается сейчас, а история, подписка и
   прожектор стоят под ним и не спорят с ним за внимание.
   ============================================================ */

/* «Защита от накрутки сняла голосов: N». Мера, применённая молча, ничем не
   отличается от подкрутки — поэтому строка есть везде, где есть счёт. Цвет
   служебный (не тревожный): читателя это не обвиняет ни в чём. */
.serial-guard {
  margin-top: 6px; font-size: 12px; line-height: 1.4; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}

/* Подписка прямо в бюллетене: единственный канал возврата при окне 12-24 ч. */
.serial-follow { margin-top: 12px; }
.serial-follow .serial-btn { width: 100%; }
.serial-follow .serial-btn.is-on { color: var(--gold-text); border-color: var(--gold); }

/* Прожектор читателя — чипы нитей памяти. */
.serial-spot {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
}
.serial-spot-head {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3);
}
.serial-spot-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.serial-spot-chip {
  flex: 0 1 auto; max-width: 100%; padding: 8px 12px; min-height: 38px;
  border-radius: 999px; border: 1px solid var(--line2); background: var(--raised);
  color: var(--ink2); font-size: 12.5px; line-height: 1.3; text-align: left;
  cursor: pointer;
}
.serial-spot-chip.is-on {
  border-color: var(--gold); background: var(--gold-soft); color: var(--gold-text);
  cursor: default;
}
.serial-spot-note { margin-top: 8px; font-size: 12px; color: var(--good); }

/* Архив голосований: строки истории, аннулированные попытки — ПРИГЛУШЁННО.
   Приглушены, но не спрятаны: попытка была, и архив о ней говорит. */
.serial-arc { margin-top: 14px; }
.serial-arc > .serial-btn { width: 100%; }
.serial-arc-head {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--ink3); margin-bottom: 8px;
}
.serial-arc-item {
  padding: 10px 0; border-top: 1px solid var(--line);
}
.serial-arc-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  font-size: 12.5px; color: var(--ink2);
}
.serial-arc-ch { font-weight: 600; color: var(--ink2); font-variant-numeric: tabular-nums; }
.serial-arc-move {
  flex: 1 1 100%; font-family: var(--font-serif); font-style: italic;
  font-size: 13.5px; line-height: 1.45; color: var(--ink);
}
.serial-arc-meta { color: var(--ink3); font-variant-numeric: tabular-nums; }
.serial-arc-sign { color: var(--ink3); }
.serial-arc-row.is-annulled {
  margin-top: 6px; padding-left: 10px; border-left: 2px solid var(--line2);
  opacity: .62; font-size: 12px;
}

/* Эпилог сезона. Главное здесь — последняя строка: место, где начинается
   личная ветка читателя (П33.7). */
.serial-recap {
  margin-top: 6px; padding: 14px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--raised);
}
.serial-recap-head {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--gold-text);
}
.serial-recap-stats {
  display: flex; flex-direction: column; gap: 4px; margin-top: 10px;
  font-size: 13px; color: var(--ink2); font-variant-numeric: tabular-nums;
}
.serial-recap-list { margin-top: 12px; }
.serial-recap-sub {
  font-size: 12px; font-weight: 600; color: var(--ink3); margin-bottom: 6px;
}
.serial-recap-against {
  display: flex; flex-direction: column; gap: 2px; padding: 8px 0;
  border-top: 1px solid var(--line); font-size: 12.5px; color: var(--ink2);
}
.serial-recap-mine, .serial-recap-won { color: var(--ink3); }
.serial-recap-fork { margin-top: 14px; }
.serial-recap-fork .serial-btn { width: 100%; min-height: 46px; }

@media (min-width: 720px) {
  .serial-ballot { padding: 24px 22px 20px; }
  .serial-recap-stats { flex-direction: row; flex-wrap: wrap; gap: 4px 18px; }
}

/* ============================================================
   ПЕРСОНЫ И ВИТРИНА МИРОВ (docs/PERSONA_DESIGN.md §7, §8)
   Кабинет персон, редактор ядра, лист входа, хроника, лента миров.
   Hover-состояния — в общем @media (hover: hover) блоке выше.
   ============================================================ */

/* «Сохранить как персону» на карточке кастомного персонажа мастера (§7.4).
   Тише «Создать облик»: собрать лицо хочет почти каждый, сохранить героя на
   аккаунт — тот, кто уже понял, зачем. Строка .char-canon-row переносит по
   wrap, поэтому на телефоне кнопка встаёт своей строкой, а не режет соседнюю. */
.c-persona-btn {
  flex: none; font-size: 12.5px; padding: 8px 13px; min-height: 40px;
  white-space: nowrap;
}

/* ---- кабинет: сетка карточек ----
   ПЕРЕЕХАЛА В КОНЕЦ ФАЙЛА, секция «v2 (2026-08-12): Персоны». Здесь её больше
   нет НАМЕРЕННО, а не по недосмотру: старый блок нёс
   `@media (min-width:720px){ .persona-ava{width:96px} }`, а медиазапрос не
   добавляет специфичности — оставленный тут, он молча вернул бы кружок v2 со
   118px обратно на 96 на любом экране шире планшета. Вместе с ним уехали
   мёртвые теперь `.persona-head` (обёртки «лицо + подпись в строку» в разметке
   нет) и `.persona-default-pill` (пилюля переименована в `.persona-main-pill`).
   Hover-хвост (.persona-card/.persona-add/.persona-counts) остался в общем
   @media (hover: hover) выше — эти селекторы не менялись. */

/* ---- редактор персоны ---- */
.persona-forge-row {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 14px; margin-bottom: 16px;
  background: var(--raised); border: 1px solid var(--line); border-radius: 12px;
}
.persona-ava-lg { width: 56px; height: 56px; font-size: 22px; margin-right: 0; }
.persona-forge-label { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.persona-forge-label em { font-style: normal; font-size: 12px; color: var(--ink3); }
.persona-forge-row button { margin-left: auto; flex: none; }
/* Четыре оси решений — плитками, а не столбиком: их ровно четыре, и вместе они
   читаются как один выбор, а не как четыре независимых вопроса. */
.persona-axes-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px;
}
.persona-axes-grid label {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 11px; min-height: var(--tap);
  border: 1px solid var(--line2); border-radius: 10px;
  font-size: 13px; color: var(--ink2); cursor: pointer;
}
.persona-axes-grid input { accent-color: var(--gold); }
.persona-axes-grid input:checked + span { color: var(--gold-text); font-weight: 600; }

/* ---- лист входа ---- */
/* «Играть существующим» гасит блок персоны: сервер отвечает 422 на persona_id
   вместе с canon_hero, и живой на вид блок обманывал бы (§4.3). */
.persona-entry-fields.is-off { opacity: .45; }
.persona-modes { border: none; margin: 16px 0 0; padding: 0; }
.persona-modes legend {
  padding: 0 0 8px; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .1em; color: var(--ink3);
}
.persona-mode {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px; margin-bottom: 8px;
  border: 1px solid var(--line2); border-radius: 12px; cursor: pointer;
  font-size: 13.5px; color: var(--ink);
}
.persona-mode input { margin-top: 2px; accent-color: var(--gold); }
/* :has поддерживают все целевые браузеры 2026 года; там, где его нет, рамка
   просто не подсветится — выбор всё равно виден по самой радиокнопке. */
.persona-mode:has(input:checked) { border-color: var(--gold); background: var(--gold-soft); }
.persona-mode-hint { display: block; margin-top: 2px; font-size: 12.5px; color: var(--ink3); }
.persona-entry-note {
  margin: 16px 0 0; padding-top: 12px; border-top: 1px solid var(--line);
  font-family: var(--font-serif); font-style: italic; font-size: 13px; color: var(--ink3);
}

/* ---- хроника ---- */
.persona-chron-list { display: flex; flex-direction: column; gap: 8px; }
.persona-chron-row {
  display: flex; flex-direction: column; gap: 3px; width: 100%;
  padding: 11px 13px; text-align: left;
  background: var(--raised); border: 1px solid var(--line); border-radius: 12px;
  font-family: var(--font-ui); color: var(--ink); cursor: pointer;
  transition: border-color .15s;
}
/* Надгробие удалённой книги — штатный путь, а не ошибка: приглушаем и снимаем
   курсор, но строку оставляем (у привязки нет FK на stories именно ради этого). */
.persona-chron-row.is-gone { cursor: default; color: var(--ink3); border-style: dashed; }
.persona-chron-title { font-family: var(--font-serif); font-size: 14.5px; font-weight: 600; }
.persona-chron-fandoms { font-size: 12px; font-style: italic; color: var(--ink3); }
.persona-chron-meta {
  display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 2px;
  font-size: 12px; color: var(--ink3); font-variant-numeric: tabular-nums;
}

/* ---- витрина миров в каталоге ----
   УДАЛЕНО 2026-08-13 (Каталог v3). Здесь жила горизонтальная лента .worlds-rail
   со снапом и её карточка (.world-card/.world-title/.world-pitch/.world-meta/
   .world-acts/.world-btn + медиа-хвост 720px). Ленты в v3 нет вовсе: миры
   разъехались на две поверхности — чипы-книжки в секции-приглашении на главном
   под-виде и сетка карточек в под-виде «Миры». Правила пришлось снять
   ФИЗИЧЕСКИ, а не перекрыть: они стояли ВЫШЕ секции каталога и при равной
   специфичности (.world-card против .world-card) побеждал бы порядок — то есть
   старая лента. Замена — секции 7-8 блока «Каталог v3» ниже; .worlds-rail не
   печатает больше ни один js. */

/* ============================================================
   STORY ROOMS (docs/STORY_ROOMS_DESIGN.md §9)
   Приватные комнаты: список, экран комнаты, панель раунда в читалке,
   модалка приглашения. Все цвета — существующие токены; hover-состояния
   живут в общем @media (hover: hover) блоке выше.
   ============================================================ */

/* ---- список комнат (§9.1) ---- */
.rooms-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
.room-head-acts { display: flex; gap: 8px; flex-wrap: wrap; }
.room-card {
  display: flex; align-items: flex-start; gap: 14px; width: 100%;
  padding: 14px 15px; text-align: left;
  background: var(--raised); border: 1px solid var(--line2); border-radius: 12px;
  color: inherit; font: inherit; cursor: pointer;
  transition: border-color .15s;
}
/* Карточка списка v2 больше не рисует корешок .book-cover и обёртки
   .room-card-top / -foot / -left — их правила удалены 2026-08-12 вместе с
   редизайном (разметку не печатает ни один web/*.js). Уцелевшие здесь
   .room-card-body / -title / -people ЖИВЫ: их перекрывает секция «Комнаты v2»
   ниже по файлу, а базовые значения остаются фолбэком. */
.room-card-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1; }
.room-card-title {
  font-family: var(--font-serif); font-size: 15.5px; font-weight: 600;
  overflow-wrap: anywhere;
}
.room-card-people { font-size: 12px; color: var(--ink3); font-variant-numeric: tabular-nums; }
.room-role {
  flex: none; padding: 1px 8px; border-radius: 999px;
  border: 1px solid var(--line2); font-size: 11.5px; color: var(--ink3);
}
.room-role.is-host { border-color: var(--gold); color: var(--gold-text); }
/* Бейдж статуса. «Ваш ход» — единственный жёлтый: он один требует действия,
   и если пожелтеет что-то ещё, он перестанет читаться как призыв. */
.room-badge {
  padding: 2px 9px; border-radius: 999px;
  font-size: 12px; border: 1px solid var(--line2); color: var(--ink3);
}
.room-badge.is-turn {
  background: var(--state-warn-bg); border-color: transparent; color: var(--state-warn-ink);
  font-weight: 600;
}
.room-badge.is-live {
  background: var(--state-good-bg); border-color: transparent; color: var(--state-good-ink);
}
.room-badge.is-wait, .room-badge.is-off { background: transparent; }
.room-badge.is-off { opacity: .8; }

/* ============================================================
   ЭКРАН ОДНОЙ КОМНАТЫ — «СТОЛ» (docs/ROOMS_PLAYABLE_2026-08-29/REDESIGN_SPEC.md)

   ЧТО ЭТОТ БЛОК ЗАМЕНИЛ. Прежние «экран комнаты (§9.2)» и «карточка раунда»
   были функциональной заготовкой 2026-07, которую редизайн v2 не тронул вовсе
   (это было записано прямым текстом ниже по файлу: «Экран одной комнаты
   (#view-room) и панель раунда в читалке НЕ ТРОГАЕМ — макет только про
   список»). Именно её и видел владелец, когда сказал «выглядит ужасно».

   ГЛАВНЫЙ ДЕФЕКТ, КОТОРЫЙ ЧИНИТ ПЕРВОЕ ЖЕ ПРАВИЛО. `.book-cover` объявлен
   `width:100%; aspect-ratio:2/3` — он рассчитан на миниатюру полки. В шапке
   комнаты ему не задавали ни `flex`, ни `width`, и обложка занимала 335×503 на
   375 и 751×1126 на 1200: полтора экрана КАРТИНКИ без единого действия, а поле
   хода начиналось на y=1168. Теперь она 64 px и стоит в строке-якоре.

   Порядок узлов тоже обратный прежнему: шапка фазы → зона действия → стол →
   люди → админ. Раньше было обложка → приглашение → участники → раунд.

   Цвета — ТОЛЬКО токены (`:root` в начале файла). Ни одного сырого хекса.
   ============================================================ */

/* Под нижней навигацией: .lownav фиксирована, и без отступа последняя кнопка
   экрана уезжает под неё. */
#room-screen { padding-bottom: 88px; }

/* ---- .room-bar: якорь возврата, обложка-миниатюра, код-кнопка ---- */

/* РЕШЁТКА, А НЕ ПЕРЕНОС ПО ФЛЕКСУ. У строки-якоря четыре разных по природе
   узла (возврат, обложка, название, код), и `flex-wrap` расставлял их на 375
   в случайном порядке — код уезжал под кнопку возврата и вставал слева.
   На узком: «← Комнаты» и код в первой строке по краям, обложка и название —
   во второй. На широком всё в одну строку. */
.room-bar {
  display: grid; align-items: center; gap: 10px;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "back  .     code"
    "cover main  main";
  margin-bottom: 16px;
}
.room-back {
  grid-area: back; justify-self: start;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: var(--tap); padding: 0 12px; border-radius: 10px;
  background: transparent; border: 1px solid var(--line2); color: var(--ink2);
  font-family: var(--font-ui); font-size: 13px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.room-bar-cover { grid-area: cover; }
/* ВОТ ЭТО ПРАВИЛО И ЕСТЬ ГЛАВНАЯ ПРАВКА РЕДИЗАЙНА (см. шапку блока).
   Без него `width:100%` из `.book-cover` даёт 335 px на 375 и 751 px на 1200. */
#view-room .book-cover { flex: none; width: 64px; margin-bottom: 0; padding: 8px 6px; }
/* Название ВНУТРИ миниатюры не печатается: 17 px в колонке шириной 64 px
   разваливается в вертикальную лесенку по одной букве, а рядом, в 20 px
   правее, то же название уже стоит заголовком <h1>. Скринридер тоже читал его
   дважды. Остаются рамка-«тиснение» и виньетка — этого хватает, чтобы прямо-
   угольник читался корешком книги, а не пустой плашкой. */
#view-room .book-cover .cover-title, .room-book .book-cover .cover-title { display: none; }
#view-room .book-cover .cover-mark, .room-book .book-cover .cover-mark { font-size: 9px; }
.room-bar-main { grid-area: main; min-width: 0; }
.room-bar-code { grid-area: code; justify-self: end; }
@media (min-width: 720px) {
  .room-bar {
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    grid-template-areas: "back cover main code";
  }
}
.room-bar-title {
  margin: 0; font-family: var(--font-serif); font-size: 18px; font-weight: 600;
  line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.room-bar-host { font-size: 12.5px; color: var(--ink3); }
/* Код — КНОПКА копирования, а не подпись с кнопкой рядом: приглашение занимало
   три строки на 375 (подпись → чип → кнопка). */
.room-bar-code {
  min-height: var(--tap); padding: 0 14px; border-radius: 10px;
  background: var(--gold-soft); border: 1px solid var(--line2);
  color: var(--gold-text);
  font-family: var(--font-ui); font-size: 13px; font-weight: 600;
  letter-spacing: .06em; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* ---- раскладка: одна колонка, на широком — стол слева, люди справа ---- */

.room-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
.room-main, .room-side {
  display: flex; flex-direction: column; gap: 14px; min-width: 0;
}
@media (min-width: 900px) {
  .room-grid { grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; }
  /* Люди и настройки не уезжают при прокрутке стола: они справка к тому, что
     происходит слева. 88px — высота залипшей шапки продукта. */
  .room-side { position: sticky; top: 88px; align-self: start; }
}

/* ---- .room-stage: фазовая шапка ---- */

.room-stage {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 16px; border: 1px solid var(--line2); border-radius: 14px;
  background: var(--raised);
}
.room-stage-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Пилюля фазы того же кроя, что на карточке списка: один элемент продукта на
   двух экранах обязан выглядеть одинаково. */
#view-room .room-badge, .room-round-panel .room-badge {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 12px; border-radius: 999px;
  font-size: 12.5px; font-weight: 600;
}
/* НИ ОДНОГО МНОЖИТЕЛЯ `opacity` ПОВЕРХ ПРИГЛУШЁННОГО ЦВЕТА. Базовое
   `.room-badge.is-off { opacity: .8 }` (блок STORY ROOMS выше) превращало
   --ink3 #9A8E77 на --raised #26221D в #837865 — 3.64:1, провал AA, пойман
   axe на состоянии «Комната спит». Это ровно тот класс дефекта, который
   эпик UX/a11y 2026-08-28 уже вычищал тринадцать раз.
   И цвет здесь --ink2, а не --ink3: НАЗВАНИЕ ФАЗЫ — главное слово шапки, а не
   приглушённая подпись. */
#view-room .room-badge.is-wait, #view-room .room-badge.is-off,
.room-round-panel .room-badge.is-wait, .room-round-panel .room-badge.is-off {
  border-color: var(--line2); color: var(--ink2); opacity: 1;
}
.room-stage-left {
  margin-left: auto; display: inline-flex; align-items: baseline; gap: 6px;
  font-size: 12px; color: var(--ink3);
}
.room-stage-left .room-left {
  color: var(--ink2); font-weight: 600; font-variant-numeric: tabular-nums;
}
.room-stage-bar {
  display: block; height: 3px; border-radius: 2px;
  background: var(--line); overflow: hidden;
}
.room-stage-bar > span { display: block; height: 100%; background: var(--gold); border-radius: 2px; }
.room-stage-tally {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--ink2);
}
.room-stage-tally .char-ava {
  width: 26px; height: 26px; margin-right: 0; margin-left: -7px;
  font-size: 12px; border: 2px solid var(--raised);
}
.room-waiting { font-size: 12.5px; color: var(--ink3); }

/* ---- .room-act: зона действия, одна на состояние ---- */

.room-act {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 16px; border: 1px solid var(--line2); border-radius: 14px;
  background: var(--surface);
}
.room-act-head {
  margin: 0; display: flex; align-items: center; gap: 8px;
  font-family: var(--font-serif); font-size: 17px; font-weight: 600;
}
/* Мишень возврата фокуса после «Отправить ход» (tabindex="-1" в разметке):
   без видимого кольца человек с клавиатуры не поймёт, куда его вернули. */
.room-act-head:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; border-radius: 6px; }
.room-act-head .leaf { flex: none; width: 9px; height: 9px; }
.room-act-note { font-size: 13.5px; color: var(--ink2); line-height: 1.55; }
/* ПРАВИЛО ИГРЫ — ЭТО ПРАВИЛО, А НЕ ОГОВОРКА. Раньше «Свой ход не голосуется» и
   «Побеждает ход, набравший больше голосов» стояли курсивом 12.5px в --ink3,
   то есть подавались как сноска мелким шрифтом. */
.room-act-rule {
  padding-left: 10px;
  border-left: 2px solid color-mix(in oklab, var(--gold) 45%, transparent);
  font-size: 13px; color: var(--ink2); line-height: 1.5;
}
.room-acts { display: flex; gap: 8px; flex-wrap: wrap; }
.room-btn { min-height: var(--tap); }
/* Необратимое — единственная кнопка продукта, которая обязана выглядеть иначе. */
.room-btn.danger {
  background: transparent; border: 1px solid var(--danger);
  color: var(--state-danger-ink);
}
.room-wake { display: flex; flex-direction: column; gap: 10px; }

/* поле хода */
.room-move { display: flex; flex-direction: column; gap: 8px; }
.room-move-label { font-size: 13.5px; color: var(--ink2); }
.room-move-text {
  width: 100%; padding: 10px 12px; resize: vertical;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line2); border-radius: 10px;
  font-family: var(--font-serif); font-size: 15px; line-height: 1.5;
}
.room-move-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
/* `flex: none` + nowrap: без них длинная подпись кнопки («Отправить ход и
   разбудить комнату» в спящей комнате) отжимала счётчик, и «0 / 300»
   разваливалось на две строки. */
.room-move-count {
  flex: none; white-space: nowrap;
  font-size: 12px; color: var(--ink3); font-variant-numeric: tabular-nums;
}
/* Последние двадцать символов — предупреждение, а не сюрприз на отправке. */
.room-move-count.is-near { color: var(--state-danger-ink); font-weight: 600; }
.room-move-text-ro {
  padding: 10px 12px; border-radius: 10px;
  background: var(--gold-soft); border: 1px solid var(--line2);
  font-family: var(--font-serif); font-size: 15px; line-height: 1.5;
}

/* ---- .room-choices: заготовленные ветки главы в форме хода (QA5 раздел E) ----
   СВОЙ КОНТЕЙНЕР, А НЕ .choices-block. Книжная одёжка развилки заперта на
   .choices-block намеренно (см. комментарий у неё): те же .choice-card
   переиспользуют бюллетень сериалов и переигрывание выбора, и у каждого свой
   набор детей. Комната — четвёртый такой потребитель, и одевается она здесь.
   Общий облик карточки (рамка, буква, подвал, порог) остаётся общим — это и
   есть смысл переиспользования: одна ветка обязана выглядеть одинаково там,
   где её выбирает одиночный читатель, и там, где её выбирает комната. */
.room-choices { display: flex; flex-direction: column; gap: 8px; }
.room-choices-title {
  font-size: 11.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--ink2);
}
/* Имя персонажа в заголовке набора («Ходы персонажа: <Имя>») из капса выводится:
   имя — это не метка интерфейса, а собственное имя, и «ГЕРМИОНА ГРЕЙНДЖЕР»
   разрядкой в .14em читается как вывеска, а не как «чей это набор». */
.room-choices-title b {
  text-transform: none; letter-spacing: .02em; color: var(--ink);
}
.room-choices-list { display: flex; flex-direction: column; gap: 8px; }
/* Строка = карточка + счётчик «этот ход выбрали трое». Счётчик стоит СНАРУЖИ
   кнопки (см. rooms.js::roomChoiceWrap): внутри он попал бы в её доступное имя
   и менял бы формулировку ветки каждый раз, когда присоединяется сосед. */
.room-choice-row { display: flex; flex-direction: column; gap: 4px; }
/* Та же сетка, что у книжной развилки: подвал под текстом, а не в колонке
   шириной 34px, куда его уронила бы авто-раскладка. Отступы плотнее — здесь
   развилка стоит НАД полем хода, а не занимает конец главы. */
.room-choices .choice-card {
  display: grid; grid-template-columns: 30px minmax(0, 1fr);
  column-gap: 12px; row-gap: 6px; align-items: start;
  border-radius: 12px; padding: 12px 14px 10px; min-height: var(--tap);
  font-size: 15px;
}
.room-choices .choice-card > * { grid-column: 2; }
.room-choices .choice-letter {
  grid-column: 1; grid-row: 1; width: 30px; height: 30px; font-size: 12px;
}
.room-choices .choice-card:active { transform: scale(.995); }
/* ВЫБРАНО, НО ЕЩЁ НЕ ОТПРАВЛЕНО. Не `.primary` и не «сделано»: золотая заливка
   на кнопке действия читается как «главное действие экрана», а здесь это всего
   лишь «вот это я и отправлю». Состояние держится рамкой и фоном — теми же,
   которыми читалка помечает нажатую ветку. */
.room-choices .choice-card.is-chosen {
  border-color: var(--gold); background: var(--gold-soft);
}
.room-choice-tally {
  padding-left: 42px; font-size: 12px; color: var(--ink2);
  font-variant-numeric: tabular-nums;
}
/* Пустая развилка (неинтерактивная книга, ручная глава) — подпись, а не пустое
   место: человек, знающий про ветки, решил бы, что экран сломался. */
.room-choices.is-empty { gap: 0; }
.room-choices-note { font-size: 12.5px; color: var(--ink3); font-style: italic; }
/* Тот же узел под списком карточек — подпись про общий «Запас сил» книги.
   Отступ нужен только там: у пустой развилки подпись стоит одна. */
.room-choices-list + .room-choices-note { line-height: 1.45; }
/* «Ход из книги» над сданным каноническим ходом: формулировка ветки — обычная
   фраза, и отличить её от авторской по одному тексту нельзя, а разница есть —
   у ветки своя цена и свой порог. */
.room-move-src {
  font-size: 11.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; color: var(--gold-text);
}

/* полосы состояния внутри зоны действия */
.room-error, .room-act-stale, .room-newch {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px; border-radius: 10px; font-size: 13px;
}
.room-error { background: var(--state-danger-bg); color: var(--state-danger-ink); }
.room-act-stale { background: var(--state-warn-bg); color: var(--state-warn-ink); }
.room-newch {
  background: var(--gold-soft); color: var(--gold-text);
  border: 1px solid color-mix(in oklab, var(--gold) 45%, transparent);
}
.room-newch .leaf { flex: none; width: 9px; height: 9px; }
.room-newch b { font-variant-numeric: tabular-nums; }
.room-error-retry, .room-newch-go, .room-act-stale .room-btn { margin-left: auto; }

/* «Глава пишется» — самый важный момент продукта. Был абзацем серого текста. */
.room-writing {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 14px; border-radius: 12px;
  background: var(--state-good-bg); color: var(--state-good-ink);
}
.room-writing-pulse { flex: none; width: 10px; height: 10px; margin-top: 5px; }
.room-writing-head { font-family: var(--font-serif); font-size: 15px; font-weight: 600; }
.room-writing-note { margin-top: 3px; font-size: 12.5px; line-height: 1.45; }
.room-decided { font-size: 12.5px; color: var(--ink3); }

/* ---- .room-table: выложенные ходы ---- */

.room-table { display: flex; flex-direction: column; gap: 10px; }
.room-table-head { margin: 0; font-family: var(--font-serif); font-size: 15px; font-weight: 600; }
.room-table-empty {
  padding: 16px 14px; text-align: center;
  border: 1.5px dashed var(--line2); border-radius: 12px;
  color: var(--ink3); font-size: 13px;
}
.room-move-card {
  display: block; padding: 12px 14px;
  border: 1px solid var(--line2); border-radius: 12px; background: var(--surface);
}
.room-move-card.is-winner { border-color: var(--gold); }
.room-move-card.is-voted { border-color: color-mix(in oklab, var(--gold) 55%, transparent); }
/* «ГермионаГолосов 2» — имя и счёт склеивались без пробела: у ряда не было
   gap, а правила `.room-prop-votes` в файле не было вовсе. */
.room-move-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.room-move-card .char-ava { flex: none; width: 30px; height: 30px; margin-right: 0; font-size: 14px; }
.room-move-card-name { font-family: var(--font-serif); font-size: 14.5px; }
/* Победитель получает СЛОВО, а не только золотую рамку: для скринридера рамки
   не существует вовсе. */
.room-move-card-flag { padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 600; }
.room-move-card-flag.is-winner { background: var(--state-good-bg); color: var(--state-good-ink); }
.room-move-card-flag.is-voted { background: var(--gold-soft); color: var(--gold-text); }
/* Главное число голосования было ТУСКЛЕЕ имени соседа (`muted` = --ink3). */
.room-move-card-votes {
  margin-left: auto; color: var(--ink); font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}
.room-move-card-votes b { font-weight: 600; }
.room-move-card-text {
  margin: 8px 0 0; color: var(--ink2);
  font-family: var(--font-serif); font-size: 14.5px; line-height: 1.55;
  overflow: hidden;
  /* Четыре строки, а не две: на экране комнаты, в отличие от панели в читалке,
     место есть. */
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
}
.room-move-card-text.is-open { display: block; -webkit-line-clamp: unset; }
/* Раскрытие — ОТДЕЛЬНАЯ кнопка. Раньше кнопкой была вся проза хода: доступное
   имя длиной в 320 символов и тач-зона 22 px при --tap 44 px. */
.room-move-more { min-height: var(--tap); margin-top: 6px; padding: 0 14px; font-size: 12.5px; }
/* Панель в читалке не имеет права занять пол-экрана над самой книгой. */
.room-round-panel .room-move-card-text { -webkit-line-clamp: 2; }
.room-round-panel .room-move-card-text.is-open { -webkit-line-clamp: unset; }

/* ---- .room-log: лента прошлых раундов ---- */

.room-log, .room-people, .room-admin {
  border: 1px solid var(--line2); border-radius: 12px; background: var(--raised);
}
.room-log-head, .room-people-head, .room-admin-head {
  display: flex; align-items: center; gap: 8px;
  min-height: var(--tap); padding: 10px 14px; cursor: pointer;
  font-family: var(--font-serif); font-size: 15px; font-weight: 600;
}
.room-log-n { color: var(--ink3); font-weight: 400; }
.room-log-list {
  margin: 0; padding: 0 14px 12px; list-style: none;
  display: flex; flex-direction: column; gap: 8px;
}
.room-log-item {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding-top: 8px; border-top: 1px solid var(--line);
  font-size: 12.5px; color: var(--ink2);
}
.room-log-no { flex: none; color: var(--ink3); font-variant-numeric: tabular-nums; }
.room-log-who { flex: none; font-family: var(--font-serif); }
.room-log-text {
  flex: 1 1 180px; min-width: 0; color: var(--ink3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.room-log-how { flex: none; color: var(--ink3); }
.room-log-go { margin-left: auto; min-height: var(--tap); padding: 0 12px; font-size: 12.5px; }

/* ---- .room-people: участники и свободные роли ---- */

.room-members { padding: 0 14px 4px; display: flex; flex-direction: column; gap: 8px; }
.room-member {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; border: 1px solid var(--line2); border-radius: 10px;
  background: var(--surface);
}
.room-member.is-me { border-color: var(--gold); }
.room-member .char-ava { flex: none; width: 30px; height: 30px; margin-right: 0; font-size: 14px; }
.room-member-name { font-family: var(--font-serif); font-size: 14.5px; }
.room-member-user {
  flex: 1; min-width: 0; font-size: 12.5px; color: var(--ink3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* «Ход сдан» и «Ход ещё не сдан» были ОДНИМ И ТЕМ ЖЕ --ink3 в 12 px без иконки:
   самый важный сигнал раунда («кого ждём») визуально не существовал. */
.room-member-flag { flex: none; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
.room-member-flag.is-done { color: var(--ink2); }
.room-member-flag.is-wait { color: var(--ink3); }
.room-flag-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; }
.room-member-flag.is-done .room-flag-dot { background: var(--good); }
.room-member-flag.is-wait .room-flag-dot { border: 1px solid var(--line2); }
.room-kick {
  flex: none; min-width: 34px; min-height: 34px; padding: 0 8px;
  font-size: 12px; line-height: 1;
}
.room-free { padding: 4px 14px 14px; }
.room-free-head { margin: 0 0 8px; font-family: var(--font-serif); font-size: 14px; font-weight: 600; }
.room-seats { display: flex; flex-direction: column; gap: 8px; }
/* Пунктирная рамка — язык «здесь пусто, но может быть занято»: тот же приём,
   что у кнопки добавления персонажа в мастере. */
.room-seat {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; border-radius: 10px;
  border: 1px dashed var(--line2); background: transparent;
  color: var(--ink2); font: inherit; text-align: left;
}
button.room-seat { cursor: pointer; min-height: var(--tap); transition: border-color .15s, color .15s; }
.room-seat .char-ava { flex: none; width: 30px; height: 30px; margin-right: 0; font-size: 14px; }
.room-seat-name { font-family: var(--font-serif); font-size: 14px; }
.room-seat-cta { margin-left: auto; font-size: 12px; color: var(--gold-text); }
.room-seat-free { margin-left: auto; font-size: 12px; color: var(--ink3); }
/* Мёртвая пунктирная строка превращается в приглашение НА КОНКРЕТНУЮ роль:
   у хозяина роли рендерились неинтерактивными всегда (у него есть персонаж). */
.room-seat-invite { margin-left: auto; min-height: var(--tap); padding: 0 12px; font-size: 12.5px; }

/* ---- .room-admin: настройки и необратимое ---- */

.room-setting {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 14px; font-size: 13px; color: var(--ink2);
}
.room-setting-key { flex: none; color: var(--ink3); }
.room-setting b { margin-left: auto; color: var(--ink); }
.room-hours { display: inline-flex; gap: 6px; margin-left: auto; }
.room-hour { min-height: 36px; padding: 0 10px; font-size: 12.5px; }
.room-hour.is-on { border-color: var(--gold); color: var(--gold-text); background: var(--gold-soft); }
/* ЗОНА НЕОБРАТИМОГО. «Закрыть комнату» — единственное необратимое действие
   продукта; оно делило строку пополам с «Читать книгу» и стояло вплотную к
   денежной «Пополнить». Теперь оно стоит одно и под своим заголовком. */
.room-danger { margin: 6px 14px 14px; padding-top: 12px; border-top: 1px solid var(--line2); }
.room-danger-head {
  margin-bottom: 8px; font-size: 11.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink3);
}
.room-danger-note { margin-top: 6px; font-size: 12px; color: var(--ink3); line-height: 1.45; }

/* ---- пустая вкладка «Комнаты» ---- */

/* Была одна курсивная строка и полтысячи пикселей пустоты (UX-07 аудита).
   Дашед-рамка — тот же язык, что у карточки «Создать комнату» в конце полки:
   это одно и то же приглашение, просто в разных обстоятельствах. */
.rooms-empty {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 32px 24px; margin-bottom: 20px; text-align: center;
  border: 1.5px dashed var(--line2); border-radius: 16px; background: transparent;
}
.rooms-empty .leaf { width: 12px; height: 12px; }
.rooms-empty-head { margin: 0; font-family: var(--font-serif); font-size: 19px; font-weight: 600; }
.rooms-empty-note {
  margin: 0; max-width: 420px; font-size: 13.5px; line-height: 1.55; color: var(--ink2);
}
.rooms-empty-acts { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.rooms-empty-acts .room-btn { padding: 0 18px; }
@media (max-width: 719px) {
  .rooms-empty { padding: 24px 16px; }
  .rooms-empty-acts { flex-direction: column; align-self: stretch; }
  .rooms-empty-acts .room-btn { width: 100%; }
}

/* Стрелка раскрытия. `display:flex` на <summary> убирает нативный треугольник
   (Chrome), а секция без единого признака «здесь можно раскрыть» читается как
   мёртвый заголовок. Content — генерируемый, в DOM его нет, словарю не мешает. */
.room-log-head, .room-people-head, .room-admin-head { list-style: none; }
.room-log-head::-webkit-details-marker,
.room-people-head::-webkit-details-marker,
.room-admin-head::-webkit-details-marker { display: none; }
.room-log-head::after, .room-people-head::after, .room-admin-head::after {
  content: "\25BE"; margin-left: auto; color: var(--ink3);
  font-size: 12px; font-weight: 400; transition: transform .15s;
}
details[open] > .room-log-head::after,
details[open] > .room-people-head::after,
details[open] > .room-admin-head::after { transform: rotate(180deg); }

/* Hover'ы — одним блоком, как во всём файле: на телефоне :hover залипает
   после тапа, и «Закрыть комнату» осталась бы подсвеченной. */
@media (hover: hover) {
  .room-back:hover, .room-bar-code:hover { border-color: var(--gold); }
  .room-move-card:hover { border-color: color-mix(in oklab, var(--gold) 40%, var(--line2)); }
  button.room-seat:hover { border-color: var(--gold); color: var(--ink); }
  .room-btn.danger:hover { background: var(--state-danger-bg); }
}
@media (prefers-reduced-motion: reduce) {
  .room-act-head .leaf, .room-newch .leaf, .room-writing-pulse { animation: none; }
  .room-log-head::after, .room-people-head::after, .room-admin-head::after { transition: none; }
}

/* ---- панель раунда в читалке (§9.3) и остатки прежней разметки ---- */

.room-round {
  padding: 16px; border: 1px solid var(--line2); border-radius: 14px;
  background: var(--raised);
}
.room-round-panel { margin-bottom: 18px; display: flex; flex-direction: column; gap: 12px; }
.room-round-empty { padding: 14px 0; color: var(--ink3); font-size: 13.5px; }
/* .room-note остаётся: её печатает модалка создания комнаты. */
.room-note { margin-top: 10px; font-size: 12.5px; color: var(--ink3); }

/* ---- модалка (приглашение, создание, вход по коду) ---- */
.room-modal-card .room-invite-body { display: flex; gap: 14px; align-items: flex-start; }
.room-modal-card .room-invite-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
/* `.room-title` и `.room-host` жили в блоке экрана комнаты и переехали вместе
   с ним в `.room-bar-title` / `.room-bar-host`. Превью приглашения — ВТОРАЯ
   поверхность с теми же двумя строками, и без этих правил они остались бы
   голым текстом 15.5 px посреди модалки. */
.room-modal-card .room-title {
  font-family: var(--font-serif); font-size: 18px; font-weight: 600;
  line-height: 1.25; overflow-wrap: anywhere;
}
.room-modal-card .room-host { font-size: 13px; color: var(--ink2); }
.room-books { display: flex; flex-direction: column; gap: 8px; max-height: 46vh; overflow-y: auto; }
.room-book {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 10px;
  background: transparent; border: 1px solid var(--line2); border-radius: 10px;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: border-color .15s, background .15s;
}
/* ШИРИНА ОБЯЗАТЕЛЬНА, А НЕ ТОЛЬКО `flex: none` (UX-08 аудита, снимок
   `modal-create__w375__dark__view.png`). `flex:none` запрещает СЖАТИЕ, а
   `width:100%` из `.book-cover` при этом берёт всю ширину модалки: обложка
   вырастала до ~270×400, название книги и счётчик глав выдавливались в ноль, а
   вторая книга уезжала за `max-height: 46vh`. Человек видел одну книгу без
   названия и решал, что выбирать не из чего. */
.room-book .book-cover { flex: none; width: 40px; margin-bottom: 0; }
.room-book-title { flex: 1; min-width: 0; font-family: var(--font-serif); font-size: 14.5px; }
.room-book-meta { flex: none; font-size: 12px; color: var(--ink3); font-variant-numeric: tabular-nums; }
.room-settings { margin-top: 14px; display: flex; flex-direction: column; gap: 4px; }
.room-setting { font-size: 13px; color: var(--ink2); }

/* ---- мобильная вёрстка (§9.6) ---- */
@media (max-width: 719px) {
  .room-head-acts button { flex: 1 1 auto; }
  /* КНОПКИ В КОЛОНКУ, PRIMARY ПЕРВОЙ. Прежнее `flex: 1 1 auto` ставило их
     рядом пополам — и «Закрыть комнату» получала ровно половину главной
     строки, наравне с «Читать книгу» и вплотную к денежной «Пополнить».
     На 320 главное действие обязано занимать всю ширину. */
  .room-acts { flex-direction: column; align-items: stretch; }
  .room-acts .room-btn { width: 100%; }
  /* Настройки хозяина — не «действия», а строки формы: их в колонку не гоним,
     иначе три кнопки длины раунда встанут башней. */
  .room-hours { margin-left: 0; }
  .room-setting { align-items: flex-start; flex-direction: column; }
  .room-setting b { margin-left: 0; }
  /* Панель раунда в читалке не должна быть выше половины экрана: под ней
     остаётся книга, ради которой человек и открыл читалку. */
  .room-round-panel { max-height: 52vh; overflow-y: auto; }
}
@media (min-width: 720px) {
  .rooms-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
}

/* ============================================================
   УВЕДОМЛЕНИЯ И «ПОХОЖИЕ КНИГИ» (docs/RETENTION_RECSYS_DESIGN.md §8.5)
   Цвета — только токены. Hover-состояния живут в общем блоке
   @media (hover: hover) выше: на телефоне :hover залипает после тапа.
   ============================================================ */

/* Панель висит на .topbar-right — значит ей нужен якорь позиционирования.
   Правило стоит здесь, а не рядом с базовым .topbar-right: единственная его
   причина — колокольчик, и вернуть его вместе с фичей должно быть одним
   действием. Раскладку шапки position:relative не меняет. */
.topbar-right { position: relative; }

.notif-bell {
  flex: none; position: relative;
  width: 38px; height: 38px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; border: 1px solid var(--line2); background: transparent;
  color: var(--ink2); font-size: 15px; line-height: 1; cursor: pointer;
  transition: color .15s, border-color .15s;
  -webkit-tap-highlight-color: transparent;
}
/* Кружок остаётся 38px (иначе он был бы больше соседнего переключателя темы), а
   тач-таргет добирается прозрачной областью до var(--tap): палец промахивается
   мимо 38px заметно чаще, чем мышь. */
.notif-bell::after {
  content: ""; position: absolute;
  inset: calc((var(--tap) - 38px) / -2);
  border-radius: inherit;
}
/* «Механики нет в этом режиме» (503 из легаси-контура). СВОЙ класс, а не общий
   .hidden: .hidden ставит и снимает syncAuthNavigation по состоянию входа, и
   первый же ре-рендер юзербокса вернул бы колокольчик, за которым ничего нет. */
.notif-bell.notif-gone { display: none; }
.notif-bell .ico { flex: none; }
/* Бейдж макета v3 — ЗОЛОТАЯ ТОЧКА 8px, БЕЗ ЧИСЛА. Кольцо цвета фона отделяет
   её от контура колокольчика. Число непрочитанных не потерялось: его пишет
   notifications.js::notifSyncBell в title/aria-label кнопки — то есть оно есть
   и у мыши, и у скринридера, но не мозолит глаз в шапке.
   ПАРА ПО КЭШУ ОБЯЗАТЕЛЬНА: со старым notifications.js в этой точке окажется
   цифра («жирная точка с числом»), с новым и старым CSS — пустая пилюля 18px. */
.notif-badge {
  position: absolute;
  /* Макет даёт top:7/right:8 при кружке 38px. Считаем от ЦЕНТРА, а не от края:
     на телефоне кружок 44px, и жёсткие 7/8 увели бы точку с плеча колокольчика
     на пустое поле кнопки. */
  top: calc(50% - 12px); right: calc(50% - 11px);
  display: block; width: 8px; height: 8px; padding: 0;
  border-radius: 50%;
  background: var(--gold);
  box-shadow: 0 0 0 2px var(--bg);
  pointer-events: none;
}

.notif-panel {
  position: absolute; z-index: 60;
  top: calc(100% + 6px); right: 0;
  /* Ширина зажата вьюпортом, а не медиа-запросом: панель висит у правого края
     шапки, и на узком экране 360px её левый край иначе уехал бы за границу. */
  width: min(360px, calc(100vw - 32px));
  max-height: min(70vh, 560px); overflow-y: auto;
  padding: 6px;
  background: var(--raised); border: 1px solid var(--line2); border-radius: 14px;
  box-shadow: var(--shadow);
}
.notif-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 8px 10px 10px;
}
.notif-head-t { font-family: var(--font-serif); font-size: 16px; font-weight: 600; color: var(--ink); }
.notif-all { flex: none; font-size: 12px; }
.notif-empty { padding: 22px 12px 26px; text-align: center; font-size: 13.5px; color: var(--ink3); }
.notif-list { display: flex; flex-direction: column; }

.notif-item {
  display: flex; align-items: flex-start; gap: 9px;
  width: 100%; padding: 10px; border: none; border-radius: 10px;
  background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: background .15s;
}
.notif-item + .notif-item { box-shadow: inset 0 1px 0 var(--line); }
/* Точка непрочитанного занимает место ВСЕГДА (у прочитанного она прозрачна):
   иначе гашение строки дёргало бы весь список на девять пикселей влево. */
.notif-mark {
  flex: none; width: 7px; height: 7px; margin-top: 7px;
  border-radius: 50%; background: transparent;
}
.notif-item.unread { background: var(--gold-soft); }
.notif-item.unread .notif-mark { background: var(--gold); }
.notif-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.notif-line { display: flex; align-items: center; gap: 6px; min-width: 0; }
.notif-title {
  min-width: 0; font-family: var(--font-ui); font-size: 13.5px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.notif-count {
  flex: none; padding: 1px 6px; border-radius: 999px;
  background: var(--state-warn-bg); color: var(--state-warn-ink);
  font-size: 10.5px; font-weight: 700; font-variant-numeric: tabular-nums;
}
/* Одна строка и ни строкой больше: панель — список поводов вернуться, а не
   их пересказ. Ужимается ПЕРВЫМ название книги (оно длиннее всех и легче
   узнаётся по началу), номер главы и разделитель не сжимаются никогда. */
.notif-sub {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: nowrap;
  min-width: 0; font-size: 12.5px; color: var(--ink2);
}
.notif-book, .notif-actors {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.notif-book { font-weight: 600; color: var(--ink2); }
.notif-sep, .notif-chapter { flex: none; white-space: nowrap; }
.notif-sep { color: var(--ink3); }
.notif-actors { color: var(--ink3); }
/* Чужая реплика — цитата в одну строку: инбокс не читалка, полный текст всегда
   в одном клике. */
.notif-excerpt {
  font-family: var(--font-serif); font-size: 12.5px; font-style: italic; color: var(--ink3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.notif-ago {
  flex: none; padding-top: 2px;
  font-size: 11px; color: var(--ink3); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.notif-more { width: 100%; margin-top: 4px; font-size: 12.5px; }

/* ---- полка «Похожие книги» (§8.3) ---- */
.similar-shelf { margin-top: 34px; padding-top: 22px; border-top: 1px solid var(--line); }
.similar-head { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
.similar-title { margin: 0; font-family: var(--font-serif); font-size: 19px; font-weight: 600; }
.similar-rule { flex: 1; height: 1px; background: var(--line); }
/* Горизонтальная лента, а не сетка: полка стоит ВНУТРИ колонки чтения, и
   вторая сетка книг под текстом читалась бы как «каталог приехал в книгу».
   Прокрутка со снапом — родная для телефона и безопасна для мыши. */
.similar-row {
  display: flex; gap: clamp(12px, 3vw, 22px);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding-bottom: 8px; scrollbar-width: thin;
}
.similar-row .similar-card { flex: 0 0 clamp(132px, 42vw, 168px); scroll-snap-align: start; }
/* Карточка полки уже каталожной, и в одну строку название с пилюлей
   тональности не помещаются — обрезается при этом именно НАЗВАНИЕ, то есть
   единственное, ради чего карточку читают. Здесь они друг под другом, а
   название переносится на вторую строку вместо многоточия на пятом символе. */
.similar-card .card-top { flex-direction: column; align-items: flex-start; gap: 3px; }
/* Клэмп СТОИТ НА ОБОИХ УЗЛАХ (QA4 п.12). Заголовок карточки полки — теперь
   кнопка `.card-open`, а она приносит СВОИ nowrap+ellipsis (:1512-1518):
   правило на одном `<h3>` до неё не достаёт, и название схлопывалось бы в одну
   строку с многоточием на ~14 символах при ширине карточки clamp(132px…)
   (:6276). Правило только на `.card-open` — обратная промашка: у карточки есть
   ФОЛБЭК-ветка без кнопки (`<span data-no-i18n>`, reader.js::similarCardHtml и
   recsys.js::recsCardHtml — на случай оболочки из кэша без library.js), и там
   текст сидит прямо в `<h3>`. Поэтому оба селектора, а не один.
   Ховер :3796 менять не надо — .card-open объявлен color: inherit. */
.similar-card .card-top h3,
.similar-card .card-top h3 .card-open {
  white-space: normal; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.similar-empty { padding: 18px 2px 22px; font-size: 13.5px; color: var(--ink3); }
/* margin-top:auto прижимает чипы к низу карточки, а лента растягивает карточки
   до общей высоты — иначе объяснения стояли бы на разной высоте у соседей и
   читались бы как свойство карточки, а не как один ряд ответов «почему». */
.similar-why { margin-top: auto; display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 2px 0; }
/* Объяснение — обязательная часть выдачи (§7.3), а не украшение: оно и есть
   причина, по которой карточку открывают. Поэтому чип тихий, но всегда виден. */
.similar-why-chip {
  padding: 2px 7px; border-radius: 999px;
  background: var(--state-info-bg); color: var(--state-info-ink);
  font-size: 10.5px; font-weight: 600; white-space: nowrap;
}

@media (max-width: 719px) {
  /* Выпадашка уведомлений не имеет права занять весь экран под шапкой. */
  .notif-panel { max-height: 74vh; }
  .similar-shelf { margin-top: 26px; }
}

/* ============================================================
   П19 «ЗАПАС СИЛ» и П21 «ПРОВАЛ = КОНТЕНТ» (ревью механик 2026-08-20).
   Полоска ресурса, метка цены на кнопке выбора, карточка «обернулось против
   вас» и секция «Шрамы истории».

   Секция стоит здесь — ПЕРЕД «макетами экранов» и, значит, задолго до .lownav
   (тот обязан оставаться последним в файле, см. его комментарий).
   ============================================================ */

/* ---- П19: полоска «Запас сил» ----
   Живёт в двух местах: рядом с прогрессом книги (конец главы) и в HUD рядом со
   здоровьем (игровой режим). Правило одно на оба, поэтому геометрия внутренняя
   (flex по своей ширине), а не привязана к колонке прозы: карточка-хозяин сама
   задаёт ширину. Тише прогресса намеренно — это состояние героя, а не событие,
   и спорить за внимание с развилкой ниже ему нечем. */
.resolve-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0 8px;
  margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--line);
  font-size: 12.5px; color: var(--ink3);
}
/* В HUD рамки-хозяина нет, и верхняя пунктирная линия читалась бы обрывком
   таблицы — там ряд просто отступает от полоски здоровья. */
.hud-hp + .resolve-row { margin-top: 12px; padding-top: 0; border-top: none; }
/* Макет Reader Chapter Interactive (2026-08-21): в дневнике запас сил стоит
   ПОД здоровьем и читается с ним как пара шкал — 4px и золото вместо 3px и
   тусклого var(--gold). Тоньше полосы здоровья он остаётся намеренно: ресурс
   второй по важности. Правило стоит ВЫШЕ .resolve-row.low/.empty (равная
   специфичность решается порядком) — «на исходе» и «на нуле» обязаны
   перекрашивать полосу и здесь. */
.hud-hp + .resolve-row .resolve-track { height: 4px; }
.hud-hp + .resolve-row .resolve-fill { background: var(--gold-text); }
.resolve-ico { flex: none; font-size: 11px; color: var(--gold-text); }
.resolve-track {
  flex: 1; min-width: 60px; height: 3px; border-radius: 2px;
  background: var(--line2); overflow: hidden;
}
.resolve-fill {
  display: block; height: 100%; border-radius: 2px; background: var(--gold);
  transition: width .25s ease;
}
.resolve-count {
  flex: none; font-size: 12px; color: var(--ink2); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* Запас на исходе и запас на нуле — разные новости, и цвет обязан их
   различать: жёлтое «осталось мало» ещё предупреждение, красное «на нуле»
   уже цена. Кнопки при этом НЕ блокируются нигде и никогда (см. докстринг
   arc_state.spend_resolve): полоска сообщает, а не запрещает. */
.resolve-row.low .resolve-fill { background: var(--state-warn-ink); }
.resolve-row.empty .resolve-fill { background: var(--danger); }
.resolve-row.empty .resolve-count { color: var(--state-danger-ink); }
.resolve-note {
  flex: 1 1 100%; margin-top: 4px; line-height: 1.45;
  font-family: var(--font-serif); font-style: italic; color: var(--state-danger-ink);
}

/* ---- П19: метка цены на кнопке выбора ----
   Ровно тот же вес и то же место, что у глифа оси (.choice-axis выше): цена —
   вторая тихая пометка ставки, а не второй акцент. Стоит ЛЕВЕЕ оси, поэтому
   отступ вешаем на саму метку, а не на соседа. */
.choice-cost {
  flex: none; margin-left: 6px; font-size: 12px; letter-spacing: -.06em;
  /* opacity: .85 убран (P1-7): --gold-text × .85 = 4.02:1 в светлой теме. */
  color: var(--gold-text);
}
.choice-card .rel-hints + .choice-cost { margin-left: auto; }

/* ---- П21: карточка «обернулось против вас» ----
   Родня .ls-fired, но не золотая: «выстрелило» — награда за выбор, «обернулось
   против» — счёт за него, и одинаковая рамка стёрла бы разницу ровно там, где
   она и есть весь смысл механики. */
.ls-fired.backfired { border-color: var(--danger); background: var(--state-danger-bg); }
.ls-fired.backfired .ls-kick,
.ls-fired.backfired .ls-kick-meta { color: var(--state-danger-ink); }

/* ---- П21: строка дневника «обернулось против вас» ---- */
.ls-echo-row.s-backfired .ls-echo-st { color: var(--state-danger-ink); }

/* ---- П21: секция «Шрамы истории» в панели «Память» ----
   Соседка «Эха ваших выборов» и по виду тоже: те же пунктирные разделители и
   тот же кегль. Отличие одно — глиф вида следа слева, потому что рана, утрата
   и неудача читаются по-разному, а текст у всех троих одинаково короткий. */
.ls-scar {
  display: flex; align-items: baseline; gap: 8px;
  border-bottom: 1px dashed var(--line); padding: 9px 0; font-size: 13.5px;
}
.ls-scar .ls-ico { flex: none; font-size: 13px; filter: grayscale(.35); }
.ls-scar-ch {
  flex: none; font-size: 11.5px; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}
.ls-scar-txt {
  flex: 1; line-height: 1.5; color: var(--ink2);
  font-family: var(--font-serif); font-style: italic;
}
.ls-scar.s-setback .ls-ico { color: var(--state-danger-ink); filter: none; }

/* ============================================================
   ПОВЕРХНОСТЬ РАЗВИЛКИ: П20 «цена на кнопке», П22 «честный порог»,
   П24 «предмет с происхождением» (ревью механик 2026-08-20).
   Секция стоит ПЕРЕД «макетами экранов» и, значит, задолго до .lownav —
   тот обязан оставаться последним в файле (см. комментарий ниже).
   ============================================================ */

/* ---- П20: категория цены-последствия ----
   Тот же вес и то же место, что у .choice-cost и .choice-axis: на кнопке уже
   есть две тихих пометки, и третья обязана встать в тот же ряд, иначе строка
   выбора превращается в приборную панель.
   ЦВЕТ РАЗВОДИТ СМЫСЛЫ. ⚡ (cost) золотая — это ставка ЧИТАТЕЛЯ, его запас сил.
   ⚑ (price_weight) чернильная — это счёт от МИРА, и золотом он быть не может:
   награда и цена не имеют права выглядеть одинаково. Тяжёлая ступень чуть
   плотнее лёгкой, но обе остаются пометками на полях, а не акцентом: точная
   величина последствия читателю не обещана и обещана быть не может.
   ПРОЗРАЧНОСТЬ ПОДНЯТА ДО МАКЕТНОЙ (Reader Chapter Interactive, 2026-08-21):
   там обе пометки подвала непрозрачны, ⚑ — просто цвет --ink3. Прежние .55 у
   лёгкой ступени на тёмном фоне читались не как «тише», а как грязь на экране,
   при том что лёгкая ступень и рисуется чаще. Ступень сохранена (тяжёлая
   плотнее лёгкой), но обе теперь по эту сторону читаемости. */
/* Ступень «лёгкая/тяжёлая» переехала с прозрачности на токены (P1-7):
   --ink3 × .85 давало 3.57:1 в светлой теме и 4.28:1 на --surface тёмной.
   Теперь лёгкая — --ink3 (4.76 / 5.10 / 5.41), тяжёлая — --ink2 (6.63 / 7.11 /
   7.54): тяжёлая по-прежнему плотнее лёгкой, но обе по эту сторону AA. */
.choice-price {
  flex: none; margin-left: 5px; font-size: 12px; line-height: 1.7;
  color: var(--ink3);
}
.choice-price.heavy { color: var(--ink2); }
.choice-card .rel-hints + .choice-price { margin-left: auto; }

/* ---- П22: ветка с честным порогом ----
   Кнопка НЕ выключена и выключена быть не может (см. engine/schemas.py::
   Choice.gated): недоступная ветка — это контент, которого читатель не увидит.
   Поэтому вид не «серый и мёртвый», а «тяжёлый»: левая грань темнее, причина
   написана под текстом. Читатель обязан считать «дороже», а не «нельзя». */
.choice-card.gated { border-left: 3px solid var(--ink3); }
.choice-gate {
  display: block; margin-top: 5px; font-size: 12px; line-height: 1.45;
  font-family: var(--font-serif); font-style: italic; color: var(--ink3);
}

/* ---- П20: карточка «мир ответил — но цену ещё не взял» ----
   Родня .ls-fired, но НЕ золотая и НЕ красная. Золотая («выстрелило») —
   награда, красная («обернулось против») — беда; здесь ни то, ни другое:
   счёт открыт и ждёт. Нейтральная рамка и приглушённая подпись — самое честное,
   что можно сказать, не обещая, когда именно мир своё возьмёт. */
.ls-fired.unpaid { border-color: var(--line2); background: var(--state-neutral-bg); }
.ls-fired.unpaid .ls-kick,
.ls-fired.unpaid .ls-kick-meta { color: var(--ink3); }
/* Строка дневника «цена не взята»: вид оплаченного долга ей запрещён —
   золотая подпись под невыполненным обещанием и есть та ложь, ради которой
   статус заводился (web/reader.js::arcEchoSectionHtml). */
.ls-echo-row.s-price_unpaid .ls-echo-st { color: var(--ink3); }

/* ---- П24: «предмет сыграл» ----
   Своего правила больше нет: с макета Reader Chapter Interactive (2026-08-21)
   это ЧИП (.ls-seed с глифом ✦), а не карточка — сыграла вещь, а не решение
   читателя, и золотая рамка уровня «Выстрелило» ей велика. Имя вещи стоит в
   .ls-seed-txt без кавычек (это не цитата выбора), главу получения несёт
   .ls-chip-meta. Стили обоих — в блоке чипов «ещё аукнется» выше. */

/* ============================================================
   МАКЕТЫ ЭКРАНОВ — Каталог, Комнаты, Персоны.
   Каталог переведён на макет v3 (2026-08-13), блок v2 удалён физически;
   Комнаты и Персоны остаются на v2 (2026-08-12).
   Секции идут ПЕРЕД блоком нижней навигации намеренно: .lownav обязан
   оставаться последним в файле (его пункты носят класс .tab и спорят с
   вкладочными правилами при равной специфичности — там решает порядок).
   Внутри каждой секции сохранён комментарий её автора: там объяснено, какие
   старые правила она перекрывает и почему именно порядком, а не !important.
   ============================================================ */

/* ------------------------------------------------------------ v3 (2026-08-13): Каталог */
/* ============================================================
   Turnleaves — КАТАЛОГ v3 (design_import/«Каталог v3.dc.html»).
   Заменил собой блок «Каталог v2» целиком. Разметку всего, что здесь описано,
   печатает web/library.js — без этих правил экран рассыпается в столбик
   нестилизованных блоков.

   ЧТО УДАЛЕНО ВМЕСТЕ С v2 И ПОЧЕМУ (правила сняты ФИЗИЧЕСКИ, а не перекрыты:
   при равной специфичности исход решал бы порядок склейки, и половина старой
   секции продолжала бы побеждать):
     • .cat-follow / .cat-follow.on / .cat-follow:active — ⊕ подписки с обложки.
       В v3 подписка живёт в герое («⊕ На полку») и в панели книги.
     • .cat-cover-foot / .cat-cover-tone / .cat-cover-lang — накладки тональности
       и языка на переплёте. Тональность видна на странице книги, язык — в
       панели фильтров; переплёт в v3 держит только название, автора и ленту.
     • .cat-card-acts / .cat-fork — строка «Создать свою ветку» под карточкой.
       Форк не потерян: кнопка живёт в панели книги (social.js).
     • .cat-cover { overflow: hidden } — из-за него лента «ЗАВЕРШЕНА» (left:-1px)
       обрезалась бы по краю обложки.
     • .cat-card-author и его hover — автор переехал НА переплёт (.cover-author).
     • .shelf-rule как ЛИНИЯ — в v3 это пустая распорка (см. ниже).
   Все перечисленные классы больше не печатает ни один js (проверено grep'ом по
   web/*.js и web/index.html), поэтому они удалены без замены.

   ------------------------------------------------------------
   ЦВЕТА — ТОЛЬКО ТОКЕНЫ
   ------------------------------------------------------------
   Макет v3 нарисован чуть темнее нашей темы (#100E0C против --bg #141210).
   Тему глобально НЕ перекрашиваем: карта макета уложена в существующие токены
   (#100E0C→--bg, #181511→--surface, #1B1713→--raised, #14110E/#17130F —
   полутона color-mix от --bg/--surface, #EEE7D9→--ink, #B0A38D→--ink2,
   #7D7261→--ink3 (сам токен с 2026-08-28 равен #9A8E77 — прежний не добирал
   4.5:1, см. блок :root), .09/.17→--line/--line2, #C7A360→--gold, #D9B878→--gold-text,
   #191510→--on-gold, #EFE6D2→--cover-ink, золотые полутона рамок →
   color-mix(in oklab, var(--gold) N%, transparent)).
   Светлая тема (html[data-theme="light"]) переопределяет ТЕ ЖЕ имена — сырой
   хекс палитры в новом правиле сломал бы её молча, без единой ошибки в консоли.
   ИСКЛЮЧЕНИЕ — светотень НА ПЕРЕПЛЁТЕ: rgba(255,255,255,…)/rgba(0,0,0,…) на
   обложках оставлены (прецедент .book-cover). Переплёт театрально тёмен в ОБЕИХ
   темах: --spine приходит из палитры корешков, а --cover-ink равен #EFE6D2 и в
   тёмной, и в светлой, поэтому блик и тень на нём от темы не зависят.

   Mobile-first: база — телефон, min-width-медиа — планшет/десктоп.
   ============================================================ */

/* ------------------------------------------------------------ 0. под-виды */
/* Под-вид уезжает АТРИБУТОМ на корне вида (library.js::renderCatalog), а не
   классами на каждом блоке: сводку активных фильтров перерисовывает
   renderFilterSummary ПОСЛЕ renderCatalog, и снятый ею класс мы бы не удержали.
   На «Мирах» искать нечего — миры не книги, общий фильтр к ним неприменим. */
#view-catalog[data-catview="worlds"] .cat-search-row,
#view-catalog[data-catview="worlds"] #cat-active-filter { display: none; }

/* ------------------------------------------------------------ 1. герой «Книга недели» */
/* Полоса на всю ширину КОЛОНКИ: отрицательные поля гасят паддинги <main>, так
   что на телефоне и планшете (где main шире вьюпорта не бывает) герой доходит
   до краёв экрана, как в макете, а на десктопе останавливается на 1120px.
   Через 100vw это НЕ делается: html{overflow-y:scroll} держит трек скроллбара
   всегда, 100vw его включает, и страница получила бы горизонтальный скролл. */
.cat-hero {
  position: relative;
  /* Full-bleed по ВЬЮПОРТУ, а не по колонке main (скрин владельца 2026-08-13:
     на широких экранах полоса, обрезанная колонкой 1120px, читалась плавающей
     карточкой, а макет ведёт фон героя от края до края). margin-inline
     50% − 50vw тянет секцию из центрированной колонки к краям окна; лишние
     ~полскроллбара, на которые 50vw шире клиентской ширины, гасит
     overflow-x: clip на html (см. комментарий там же) — горизонтального
     скролла это не создаёт. padding-inline возвращает контент на место:
     max(50vw − 50%, …) = расстояние до колонки на широких экранах и обычные
     поля макета на узких, где колонка и так упирается в края. */
  margin: calc(-1 * clamp(18px, 4vw, 44px)) calc(50% - 50vw) clamp(18px, 3vw, 30px);
  padding: clamp(30px, 5vw, 58px) max(calc(50vw - 50%), clamp(14px, 4vw, 44px)) clamp(34px, 5vw, 62px);
  background:
    radial-gradient(120% 140% at 78% -20%,
      color-mix(in oklab, var(--gold) 14%, transparent), transparent 55%),
    linear-gradient(180deg, color-mix(in oklab, var(--surface) 55%, var(--bg)), var(--bg));
  border-bottom: 1px solid var(--line);
}
.cat-hero-in {
  display: grid; grid-template-columns: 1fr auto;
  gap: clamp(24px, 5vw, 72px); align-items: center;
}
.cat-hero-body { min-width: 0; max-width: 560px; }
.cat-hero-kicker {
  display: flex; align-items: center; gap: 12px; margin-bottom: 18px;
  font-size: 11px; font-weight: 600; letter-spacing: .22em;
  text-transform: uppercase; color: var(--gold);
}
.cat-hero-rule { flex: none; height: 1px; width: 34px; background: var(--gold); }
.cat-hero-title {
  margin: 0 0 12px; font-family: var(--font-serif);
  font-size: clamp(32px, 5vw, 52px); font-weight: 600;
  letter-spacing: -.015em; line-height: 1.08; overflow-wrap: anywhere;
}
.cat-hero-pitch {
  margin: 0 0 14px; font-family: var(--font-serif); font-style: italic;
  font-size: clamp(15px, 1.8vw, 17.5px); line-height: 1.6;
  color: var(--ink2); text-wrap: pretty;
}
.cat-hero-meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 18px;
  margin-bottom: 24px; font-size: 13px; color: var(--ink3);
}
.cat-hero-meta .cat-rate { color: var(--ink2); gap: 7px; }
.cat-hero-meta .stars { font-size: 14px; letter-spacing: .06em; }
.cat-hero-prog { font-variant-numeric: tabular-nums; }
.cat-hero-by {
  background: none; border: none; padding: 0;
  font-family: inherit; font-size: inherit; font-style: italic;
  color: var(--ink3); cursor: pointer;
}
.cat-hero-acts { display: flex; gap: 10px; flex-wrap: wrap; }
.cat-hero-acts button { min-height: 48px; border-radius: 12px; }
.cat-hero-read { padding: 0 26px; font-size: 15px; font-weight: 600; }
.cat-hero-follow { padding: 0 20px; font-size: 14px; }
/* «Слежу»: кнопка перестаёт звать и становится индикатором — она всё ещё
   кликабельна, повторный клик отписывает. */
.cat-hero-follow.on {
  color: var(--gold-text);
  border-color: color-mix(in oklab, var(--gold) 50%, transparent);
}
.cat-hero-cover-wrap { flex: none; width: clamp(210px, 22vw, 270px); }
/* Переплёт героя: тот же материал, что у карточек, но крупнее, с наклоном и
   глубокой тенью — книга, которую положили на стол, а не поставили на полку. */
.cat-hero-cover {
  margin-bottom: 0; gap: 18px; padding: 26px 20px;
  border-radius: 8px 16px 16px 8px; transform: rotate(2.2deg);
  background-color: var(--raised);
  background-image:
    linear-gradient(90deg, rgba(0,0,0,.4), rgba(0,0,0,0) 12px),
    linear-gradient(105deg, rgba(255,255,255,.14), rgba(255,255,255,0) 30%),
    linear-gradient(200deg, var(--spine), color-mix(in oklab, var(--spine) 62%, black));
  box-shadow: var(--shadow);
  cursor: pointer;
}
.cat-hero-cover .cover-frame {
  inset: 11px; border-radius: 5px 11px 11px 5px;
  border-color: color-mix(in oklab, var(--cover-ink) 32%, transparent);
}
.cat-hero-cover .cover-title { font-size: clamp(20px, 2vw, 25px); }
.cat-hero-cover .cover-mark { font-size: 12px; letter-spacing: .4em; opacity: .65; }
.cat-hero-cover .cover-author { font-size: 12.5px; opacity: .7; }
@media (max-width: 719px) {
  .cat-hero-in { grid-template-columns: 1fr; }
  /* Обложка по центру и заметно уже: на телефоне колонка одна, и переплёт в
     полную ширину сжирает экран раньше кнопки «Читать». */
  .cat-hero-cover-wrap { width: min(58vw, 240px); margin: 0 auto; }
  /* «← Каталог» — единственная дорога назад из под-вида: 40px по букве макета
     ниже тач-минимума, а ряд шапки под-вида и так переносится. */
  .cat-back { min-height: var(--tap); }
}

/* ------------------------------------------------------------ 2. шапка под-вида */
.cat-subhead {
  display: flex; align-items: center; flex-wrap: wrap; gap: 16px;
  margin: 6px 0 22px;
}
.cat-back {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 40px; padding: 0 16px;
  background: none; border: 1px solid var(--line2); border-radius: 999px;
  color: var(--ink2); font-family: var(--font-ui); font-size: 13px; font-weight: 600;
  cursor: pointer; white-space: nowrap; transition: border-color .15s, color .15s;
}
.cat-sub-title {
  margin: 0; font-family: var(--font-serif);
  font-size: clamp(26px, 4vw, 36px); font-weight: 600; letter-spacing: -.01em;
}
.cat-sub-note { font-size: 13px; color: var(--ink3); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ 3. строка поиска */
/* Поле поиска — ВХОД в существующую панель фильтров (#open-search), а не второй
   ввод: модель запроса одна (viewFilter("catalog").q), печатается он в #sp-query.
   Поэтому это <button>, стилизованная под поле, а не <input>. Кнопка «Фильтры»
   ведёт в ту же панель — макет v3 просто дал ей отдельную цель на телефоне. */
.cat-search-row { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 12px; }
.cat-search {
  flex: 1 1 280px; min-width: 0;
  display: flex; align-items: center; gap: 10px;
  min-height: 48px; padding: 0 18px;
  background: var(--surface); border: 1px solid var(--line2); border-radius: 999px;
  color: inherit; font-family: var(--font-ui); font-size: 14.5px;
  text-align: left; cursor: text;
  transition: border-color .15s, box-shadow .15s;
}
.cat-search:focus-visible { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px var(--gold-soft); }
.cat-search-glyph { flex: none; color: var(--ink3); font-size: 16px; line-height: 1; }
/* Плейсхолдер — приглушён, введённый запрос — обычными чернилами: строка одна,
   а состояний два, и по цвету должно быть видно, фильтрует каталог или нет. */
.cat-search-text {
  flex: 1; min-width: 0; color: var(--ink3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cat-search-text.is-query { color: var(--ink); }
.cat-sort {
  flex: 0 1 176px; width: auto; min-height: 48px;
  padding: 0 16px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--line2);
  font-size: 13.5px; color: var(--ink2);
}
.cat-filter-btn {
  flex: none; display: inline-flex; align-items: center; gap: 8px;
  min-height: 48px; padding: 0 18px;
  background: var(--surface); border: 1px solid var(--line2); border-radius: 999px;
  color: var(--ink2); font-family: var(--font-ui); font-size: 13.5px;
  cursor: pointer; white-space: nowrap; transition: border-color .15s, color .15s;
}
.cat-filter-btn svg { flex: none; }

/* ------------------------------------------------------------ 4. чипы (фандомы миров) */
/* Быстрых чипов НАД КАТАЛОГОМ в v3 нет — фильтры уехали в панель. Класс жив
   ради фандомного ряда витрины миров (#worlds-chips), он же .cat-chips. */
.cat-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 16px; }
.cat-chips.hidden { display: none; }
.cat-chip {
  display: inline-flex; align-items: center; min-height: 34px;
  padding: 8px 14px; border-radius: 999px;
  font-family: var(--font-ui); font-size: 12.5px; font-weight: 500; line-height: 1;
  background: transparent; color: var(--ink2); border: 1px solid var(--line2);
  cursor: pointer; white-space: nowrap;
  transition: color .15s, border-color .15s, background .15s;
}
.cat-chip.on {
  background: var(--gold-soft); color: var(--gold-text);
  border-color: var(--gold); font-weight: 600;
}

/* ------------------------------------------------------------ 5. полки и доска */
.cat-shelves.hidden, .shelf-head.hidden { display: none; }
.cat-shelf { margin: 34px 0 0; }
.shelf-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 20px; }
.shelf-title {
  margin: 0; font-family: var(--font-serif);
  font-size: clamp(20px, 2.4vw, 26px); font-weight: 600; letter-spacing: -.01em;
}
.shelf-note { font-size: 12.5px; color: var(--ink3); white-space: nowrap; font-style: italic; }
/* Распорка макета: пустое место между подписью и «Все →». Линии здесь больше
   нет — v3 держит шапку полки чистой. Узел остаётся ради разметки. */
.shelf-rule { flex: 1; height: 0; }
.shelf-all {
  flex: none; background: none; border: none; padding: 4px 0;
  font-family: var(--font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--gold-text); cursor: pointer; white-space: nowrap;
}
/* Курсивная подпись полки уходит с телефона (макет: .om-desktop): рядом с
   заголовком в 20px она отнимает у «Все →» последнюю строку. */
@media (max-width: 719px) {
  .shelf-note { display: none; }
  /* «Все →» — ЕДИНСТВЕННЫЙ вход в под-вид «Все истории» с телефона (быстрых
     чипов над сеткой v3 не рисует, полной сетки под полками нет), а по букве
     макета это 12.5px текста с padding 4px, то есть ≈27px тач-таргета. Тянем до
     var(--tap): baseline-выравнивание .shelf-head от этого не едет — у inline-flex
     базовой линией остаётся строка текста, лишняя высота уходит в поля. */
  .shelf-all {
    display: inline-flex; align-items: center;
    min-height: var(--tap); padding: 0 4px;
  }
}

/* Сетка витрины + физическая «доска полки» под ней.
   НА ТЕЛЕФОНЕ КОЛОНКИ НАМЕРЕННО НЕ ПЕРЕОПРЕДЕЛЕНЫ: ниже 720px работает базовое
   `.library { minmax(150px, 1fr) }`, то есть на 375px витрина идёт В ДВЕ
   колонки по ~164px. Макетное minmax(176px) дало бы там ОДНУ колонку — экран
   на одну обложку, и полка из восьми книг превращается в восемь экранов
   скролла. Это ОСОЗНАННОЕ расхождение с макетом, а не забытая медиа-точка:
   не «чините» его до одной колонки. */
.cat-grid { position: relative; gap: 26px 16px; padding-bottom: 22px; }
@media (min-width: 720px) {
  .cat-grid { grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: 26px 22px; }
}
/* Планка выходит за колонки на 8px с каждой стороны — иначе книги стоят не НА
   полке, а над обрывом. Абсолютный узел внутри grid-контейнера из потока
   выпадает и колонок не занимает. */
.shelf-board {
  position: absolute; left: -8px; right: -8px; bottom: 0;
  height: 10px; border-radius: 3px; pointer-events: none;
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--raised) 78%, var(--bg)),
    color-mix(in oklab, var(--bg) 88%, var(--raised)));
  box-shadow: 0 6px 14px color-mix(in oklab, var(--scrim) 75%, transparent);
}

/* ------------------------------------------------------------ 6. карточка каталога */
.cat-card { gap: 6px; }
/* Переплёт v3: двухтоновый градиент от цвета корешка (тёмный конец —
   color-mix со «чёрным», то есть затемнение, а не вторая палитра), глянец и
   тень-корешок слева одним background-image. Цвет и глянец разнесены с
   background-color НАМЕРЕННО: --spine приходит из инлайна карточки, и если его
   однажды не будет, невалидный var() обнулил бы весь фон-шорткат — светлая тема
   получила бы светлые буквы на светлом. */
.cat-card .book-cover {
  margin-bottom: 1px; gap: 10px; padding: 18px 13px;
  border-radius: 5px 11px 11px 5px;
  background-color: var(--raised);
  background-image:
    linear-gradient(90deg, rgba(0,0,0,.38), rgba(0,0,0,0) 8px),
    linear-gradient(105deg, rgba(255,255,255,.12), rgba(255,255,255,0) 30%),
    linear-gradient(200deg, var(--spine), color-mix(in oklab, var(--spine) 68%, black));
  box-shadow: var(--shadow-soft);
}
.cat-card .cover-frame {
  inset: 8px; border-radius: 3px 7px 7px 3px;
  border-color: color-mix(in oklab, var(--cover-ink) 30%, transparent);
}
.cat-card .cover-title { font-size: clamp(15px, 1.55vw, 17.5px); }
.cat-card .cover-mark { font-size: 10px; letter-spacing: .4em; opacity: .6; }
/* Автор НА переплёте (макет v3). Кнопка, а не текст: профиль автора — рабочая
   ссылка, и терять её вместе с подписью под обложкой нельзя. */
.cover-author {
  max-width: 100%; padding: 0; border: none; background: none;
  font-family: var(--font-serif); font-style: italic; font-size: 11px;
  /* .65 → .72 (P1-7). Здесь множитель ОСТАЁТСЯ: фон — не токен темы, а
     --spine, восемь тонов переплёта из library.js::COVER_COLORS, и «тише
     заголовка» на них даёт только прозрачность. Но .65 давала 4.45:1 на самом
     светлом тоне («мох» #32473A) — под порогом при кегле 11px. На .72 худший
     тон даёт 5.07:1, весь набор — 5.07…5.68. Считано по всем восьми
     (scratchpad/contrast.py --alpha). */
  color: var(--cover-ink); opacity: .72; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: opacity .15s;
}
/* «ЗАВЕРШЕНА» — золотая ЛЕНТА слева, а не пилюля v2: это наклейка на корешке
   библиотечного тома. Прописные даёт CSS, чтобы ключ словаря остался прежним
   («завершена» уже переведена в четырёх локалях). */
.cat-cover-done {
  position: absolute; left: -1px; top: 14px; z-index: 1;
  padding: 4px 10px 4px 12px; border-radius: 0 4px 4px 0;
  background: var(--gold); color: var(--on-gold);
  font-family: var(--font-ui); font-size: 10px; font-weight: 700;
  letter-spacing: .04em; line-height: 1.3; text-transform: uppercase;
  box-shadow: 0 4px 10px var(--scrim);
}

/* --- подпись под обложкой --- */
.cat-card-title {
  font-family: var(--font-ui); font-size: 14.5px; font-weight: 600; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: color .15s;
}
.cat-card-by {
  display: block; min-width: 0;
  font-size: 12.5px; font-style: italic; color: var(--ink3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cat-card-fandom {
  background: none; border: none; padding: 0; font: inherit; color: inherit;
  cursor: pointer; transition: color .15s;
}
.cat-card-line {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 12.5px; color: var(--ink2);
}
.cat-rate { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.cat-rate.none { color: var(--ink3); font-size: 12px; }
.cat-star { color: var(--gold); font-size: 12px; line-height: 1; }
.cat-card-prog {
  margin-left: auto; flex: none;
  color: var(--ink3); font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Метки под обложкой: «слежу» и «ваша книга». Кнопки подписки на карточке v3
   нет (⊕ ушёл с переплёта в героя и в саму книгу), но ЗНАТЬ, что книга уже на
   полке, читатель обязан — иначе он открывает её, чтобы это выяснить. */
.cat-card-marks {
  display: flex; flex-wrap: wrap; align-items: center; gap: 3px 10px;
  font-size: 11.5px; color: var(--ink3); min-width: 0;
}
.cat-card-mine { font-size: 11.5px; color: var(--ink3); }

/* ------------------------------------------------------------ 7. секция «Войдите в готовый мир» */
/* Приглашение, а не второй каталог: золотистая рамка, тёплый отсвет и ряд
   чипов-книжек. Полный список миров — за пунктирной пилюлей «Все миры →». */
.worlds-strip {
  margin: 40px 0 0; padding: clamp(20px, 3vw, 30px);
  border: 1px solid color-mix(in oklab, var(--gold) 22%, transparent);
  border-radius: 18px;
  background:
    radial-gradient(90% 120% at 8% 0%,
      color-mix(in oklab, var(--gold) 9%, transparent), transparent 60%),
    color-mix(in oklab, var(--surface) 30%, var(--bg));
}
.worlds-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 14px;
  margin-bottom: 16px;
}
.worlds-strip .worlds-head h2 {
  margin: 0; font-family: var(--font-serif);
  font-size: clamp(19px, 2.2vw, 24px); font-weight: 600;
}
.worlds-strip .worlds-note { margin: 0; font-size: 12.5px; color: var(--ink3); font-style: italic; }
.worlds-chipbooks { display: flex; flex-wrap: wrap; gap: 8px; }
.world-chip {
  display: inline-flex; align-items: center; gap: 10px; max-width: 100%;
  min-height: 46px; padding: 9px 16px 9px 10px;
  background: color-mix(in oklab, var(--ink) 3%, transparent);
  border: 1px solid var(--line2); border-radius: 999px;
  color: var(--ink); font-family: var(--font-ui); font-size: 13.5px;
  cursor: pointer; white-space: nowrap; transition: border-color .15s;
}
/* Мини-корешок: тот же материал, что у обложек книг (глянец поверх --spine), —
   мир и книга обязаны читаться как один продукт. */
.world-chip-spine {
  flex: none; width: 17px; height: 25px; border-radius: 2px 4px 4px 2px;
  background-color: var(--raised);
  background-image:
    linear-gradient(105deg, rgba(255,255,255,.14), rgba(255,255,255,0) 40%),
    linear-gradient(var(--spine), var(--spine));
  box-shadow: 0 3px 8px var(--scrim);
}
.world-chip-title { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.world-chip-ch { flex: none; font-size: 11.5px; color: var(--ink3); font-variant-numeric: tabular-nums; }
.world-all {
  display: inline-flex; align-items: center; min-height: 46px; padding: 0 18px;
  background: none; border: 1px dashed color-mix(in oklab, var(--gold) 50%, transparent);
  border-radius: 999px; color: var(--gold-text);
  font-family: var(--font-ui); font-size: 13px; font-weight: 600;
  cursor: pointer; white-space: nowrap; transition: border-color .15s, color .15s;
}

/* ------------------------------------------------------------ 8. под-вид «Миры» */
.worlds-page .worlds-chips { margin: 0 0 16px; }
.worlds-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  gap: 18px;
}
.world-card {
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line2); border-radius: 16px;
  box-shadow: var(--shadow-soft); transition: border-color .15s;
}
/* Шапка-переплёт мира: кураторский цвет из манифеста витрины (worlds.json).
   Цвет и глянец разнесены по разным свойствам по той же причине, что на
   карточке книги (см. выше) — невалидный var() обнулил бы шорткат целиком. */
.world-cap {
  position: relative; display: flex; align-items: flex-end;
  height: 110px; padding: 14px 16px;
  background-color: var(--raised);
  background-image:
    linear-gradient(160deg, rgba(255,255,255,.10), rgba(0,0,0,.30)),
    linear-gradient(var(--spine), var(--spine));
}
.world-cap-frame {
  position: absolute; inset: 8px; border-radius: 8px; pointer-events: none;
  border: 1px solid color-mix(in oklab, var(--cover-ink) 22%, transparent);
}
.world-cap .world-title {
  font-family: var(--font-serif); font-size: 19px; font-weight: 600; line-height: 1.25;
  color: var(--cover-ink); text-shadow: 0 1px 8px rgba(0,0,0,.45);
}
.world-body { display: flex; flex-direction: column; gap: 8px; flex: 1; padding: 14px 16px 16px; }
.world-body .world-desc { font-size: 13px; font-style: italic; color: var(--ink2); line-height: 1.5; }
.world-body .world-meta {
  display: flex; align-items: center; flex-wrap: wrap; margin: 0;
  font-size: 12px; color: var(--ink3);
}
.world-fandom { white-space: nowrap; }
.world-body .world-acts { display: flex; gap: 8px; margin-top: auto; padding-top: 10px; }
.world-body .world-btn {
  flex: 1; min-height: 42px; font-size: 13px; font-weight: 600;
  border-radius: 10px; white-space: nowrap;
}
/* «С чистого листа» — призрачная и УЖЕ: главное действие секции вход персоной,
   и равные по весу кнопки стирают эту разницу. */
.world-body .world-btn-ghost { flex: none; padding: 0 14px; font-weight: 400; }

/* ------------------------------------------------------------ 9. hover / reduced-motion */
@media (hover: hover) {
  .cat-search:hover, .cat-sort:hover, .cat-filter-btn:hover { border-color: var(--gold); }
  .cat-filter-btn:hover { color: var(--ink); }
  .cat-back:hover { border-color: var(--gold); color: var(--ink); }
  .cat-chip:hover { border-color: var(--gold); color: var(--ink); }
  .cat-chip.on:hover { color: var(--gold-text); }
  .shelf-all:hover { color: var(--gold); }
  /* Книга «снимается с полки» — подъём с лёгким поворотом и рост тени. */
  .cat-card:hover .book-cover { transform: translateY(-6px) rotate(-.4deg); box-shadow: var(--shadow); }
  .cat-card:hover .cat-card-title { color: var(--gold-text); }
  .cat-card-fandom:hover { color: var(--gold-text); }
  .cover-author:hover { opacity: 1; }
  .cat-hero-by:hover { color: var(--gold-text); }
  .world-card:hover { border-color: var(--gold); }
  .world-chip:hover { border-color: var(--gold); }
  .world-all:hover { border-color: var(--gold); color: var(--gold); }
}
@media (prefers-reduced-motion: reduce) {
  .cat-card .book-cover { transition: none; }
  .cat-card:hover .book-cover { transform: none; }
}

/* ------------------------------------------------------------ v2 (2026-08-12): Комнаты */
/* ============================================================
   КОМНАТЫ v2 — СПИСОК (design_import/«Комнаты v2.dc.html»).
   Патч поверх блока «STORY ROOMS» в web/styles.css.

   СБОРЩИКУ: вставлять ПОСЛЕ существующего блока STORY ROOMS (и после его
   мобильных медиа-хвостов). Правила списка намеренно скоплены под #view-rooms
   — карточка комнаты рисуется ТОЛЬКО там, а лишняя специфичность здесь не
   украшательство: старые `.room-card` / `.room-badge` / `.room-card-people`
   из styles.css живут дальше (их читают экран комнаты и модалка приглашения),
   и при равной специфичности исход решал бы порядок склейки файлов.

   Экран одной комнаты (#view-room) и панель раунда в читалке НЕ ТРОГАЕМ —
   макет только про список.

   Цвета — только токены. Сырые rgba остались ровно в двух местах, и оба не
   про палитру: неокрашенная светотень градиента обложки (тот же приём, что у
   .book-cover) и тень под названием на ней.

   СВЕТЛАЯ ТЕМА — ОСОБЫЙ СЛУЧАЙ, тот же, что у накладок каталога. Шапка карточки
   ТЁМНАЯ В ОБЕИХ темах (--spine из палитры корешков, --cover-ink светлый в
   обеих), поэтому всё, что лежит НА НЕЙ, обязано брать цвета, которые с темой
   не переворачиваются: подложка — из --scrim (единственный тёмный в обеих
   темах токен), чернила — из --cover-ink. Токен, который переворачивается
   (--bg, --surface, --raised, --ink2, --gold-text), даёт в светлой теме
   светлое по светлому — и заметит это не автор правки, а человек.

   Осиротевшие правила прежней карточки (.room-card-top / -foot / -left и
   `.room-card .book-cover` с hover-парой) УДАЛЕНЫ из блока STORY ROOMS
   2026-08-12 — искать под них разметку больше не нужно.
   ============================================================ */

/* ---- шапка вкладки: поле кода со встроенной кнопкой + «Создать комнату» ---- */

#view-rooms .room-head-acts {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}

/* НЕ <label>: подпись обёрткой сделала бы текст кнопки «Войти» частью
   доступного имени поля («Код комнаты Войти»). Имя даёт aria-label. */
.room-code-field {
  display: flex; align-items: center; gap: 8px;
  min-height: var(--tap); padding: 0 6px 0 14px;
  background: var(--surface); border: 1px solid var(--line2); border-radius: 12px;
  transition: border-color .15s;
}
.room-code-field:focus-within { border-color: var(--gold); }
/* min-height: 0 обязателен. Глобальное `input, select, textarea { min-height:
   48px }` (начало файла) иначе распирает ОБЁРТКУ до 50px, и поле кода встаёт
   выше соседней кнопки «Создать комнату» (var(--tap) = 44px) — в макете оба
   контрола одной высоты. Сброса padding для этого мало: высоту диктует не он. */
.room-code-field input {
  width: 110px; min-width: 0; min-height: 0; padding: 0;
  background: none; border: none; outline: none;
  color: var(--ink); font-family: var(--font-ui); font-size: 13.5px;
  letter-spacing: .06em;
}
/* Разрядка ставится ради КОДА (его диктуют по буквам); на подсказке она
   растянула бы обычную фразу в непрочитываемую строку. */
.room-code-field input::placeholder { color: var(--ink3); letter-spacing: normal; }
.room-code-go {
  flex: none; min-height: 32px; padding: 0 12px;
  font-family: var(--font-ui); font-size: 12.5px; font-weight: 600;
  background: var(--gold-soft); color: var(--gold-text);
  border: 1px solid var(--gold); border-radius: 8px; cursor: pointer;
  white-space: nowrap;
  transition: background .15s, color .15s;
  -webkit-tap-highlight-color: transparent;
}
#view-rooms .room-create-btn {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: var(--tap); padding: 0 18px; border-radius: 12px;
  font-size: 14px; font-weight: 600; white-space: nowrap;
}

/* ---- баннер «вас ждут» ---- */

.rooms-turn {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 16px; margin: 0 0 20px;
  background: var(--gold-soft);
  border: 1px solid color-mix(in oklab, var(--gold) 45%, transparent);
  border-radius: 12px;
  font-size: 13.5px; color: var(--gold-text);
}
.rooms-turn-leaf { width: 9px; height: 9px; }
.rooms-turn b { font-weight: 600; }
.rooms-turn-go {
  margin-left: auto; min-height: 32px; padding: 0;
  background: none; border: none; color: var(--gold-text);
  font-family: var(--font-ui); font-size: 12.5px; font-weight: 600;
  cursor: pointer; white-space: nowrap;
  transition: color .15s;
  -webkit-tap-highlight-color: transparent;
}

/* ---- полка ---- */

/* minmax(min(100%, 320px), 1fr) — одна формула на все ширины: на телефоне
   колонка ужимается до 100 %, и отдельный min-width-хвост из styles.css
   становится не нужен (он перекрыт этой же специфичностью). */
#view-rooms .rooms-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
  gap: 16px;
}

/* ---- карточка комнаты ---- */

#view-rooms .room-card {
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  width: 100%; padding: 0; text-align: left;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 16px; overflow: hidden;
  color: inherit; font: inherit; cursor: pointer;
  box-shadow: var(--shadow-soft);
  transition: border-color .15s, box-shadow .18s, transform .18s;
  -webkit-tap-highlight-color: transparent;
}
/* Комната, где ход за читающим, — единственная в золотой рамке. Ровно та же
   причина, что у жёлтого бейджа: подсветится что-то ещё — и «здесь ждут вас»
   перестанет читаться с одного взгляда. */
#view-rooms .room-card.is-turn {
  border-color: color-mix(in oklab, var(--gold) 55%, transparent);
  box-shadow: var(--shadow-soft), 0 0 0 1px var(--gold-soft);
}

.room-card-head {
  position: relative; display: flex; align-items: flex-end;
  min-height: 108px; padding: 14px 16px;
  /* Светотень поверх цвета переплёта — не палитра, а объём; приём и синтаксис
     те же, что у .book-cover. --spine приезжает инлайном из rooms.js. */
  background:
    linear-gradient(160deg, rgba(255,255,255,.10), rgba(0,0,0,.3)),
    var(--spine, var(--raised));
}
.room-card-frame {
  position: absolute; inset: 8px; pointer-events: none;
  border: 1px solid color-mix(in oklab, var(--cover-ink) 22%, transparent);
  border-radius: 8px;
}
#view-rooms .room-card-title {
  position: relative;
  font-family: var(--font-serif); font-size: 19px; font-weight: 600;
  line-height: 1.25; color: var(--cover-ink); overflow-wrap: anywhere;
  text-shadow: 0 1px 8px rgba(0,0,0,.45);
}
/* Пилюля роли живёт НА ШАПКЕ-ПЕРЕПЛЁТЕ, а она тёмная в обеих темах. Поэтому
   подложка — --scrim, а не color-mix от --bg: --bg переворачивается вместе с
   темой (#141210 → #F3EEE2), и в светлой теме получалась почти белая плашка на
   тёмной обложке с тёмными чернилами (замер: 2.93:1 у хозяина, ~2.7:1 у гостя
   при кегле 11px/600 — ниже порога AA для мелкого текста). Чернила гостя по
   той же причине --cover-ink, а не --ink2. */
#view-rooms .room-role {
  position: absolute; top: 12px; right: 12px;
  padding: 3px 10px; border: none; border-radius: 999px;
  background: var(--scrim);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  font-size: 11px; font-weight: 600; color: var(--cover-ink);
}
#view-rooms .room-role.is-host { border: none; color: var(--gold-text); }
/* Хозяину нужен ЗОЛОТОЙ, а --gold-text в светлой теме тёмный (#7A5A20) и на
   затемняющей пелене тонет (4.2:1 при кегле 11px/600 — ниже AA). Переворачиваем
   ПОДЛОЖКУ, а не чернила — тот же приём, что у .cat-cover-done. Плашка здесь
   НЕПРОЗРАЧНАЯ намеренно: сквозь полупрозрачную просвечивает цвет переплёта, а
   он у каждой книги свой (палитра корешков), и контраст гулял бы от карточки к
   карточке. Непрозрачная даёт ровно #EFE6D2 против #7A5A20 = 5.1:1 на ЛЮБОЙ
   обложке. Отдельного «светлого золота для обеих тем» в палитре нет, а заводить
   сырой хекс ради одной пилюли значило бы завести палитру мимо токенов. */
html[data-theme="light"] #view-rooms .room-role.is-host {
  background: var(--cover-ink);
}

#view-rooms .room-card-body {
  display: flex; flex-direction: column; gap: 11px; flex: 1; min-width: 0;
  padding: 14px 16px 16px;
}

/* строка статуса: пилюля раунда + прогресс */
.room-card-status { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#view-rooms .room-badge {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 12px; border-radius: 999px;
  font-size: 12.5px; font-weight: 600;
}
#view-rooms .room-badge.is-wait { border-color: var(--line2); color: var(--ink3); }
#view-rooms .room-badge.is-off { border-color: var(--line2); color: var(--ink3); }
.room-badge-leaf { width: 8px; height: 8px; }
.room-badge-sep { opacity: .55; }
.room-card-progress {
  margin-left: auto; font-size: 12px; color: var(--ink3);
  font-variant-numeric: tabular-nums;
}

/* ряд соавторов */
.room-card-people-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Компенсация отрицательного margin первого кружка: без неё ряд «съезжал» бы
   влево на 7px относительно всего остального в карточке. */
.room-avas { display: inline-flex; padding-left: 7px; }
.room-ava-dot {
  flex: none; width: 30px; height: 30px; margin-left: -7px;
  border-radius: 50%; border: 2px solid var(--surface);
}
#view-rooms .room-card-people {
  font-size: 12.5px; color: var(--ink3); font-variant-numeric: tabular-nums;
}
.room-card-people-cap { font-size: 12.5px; color: var(--ink3); }
.room-card-free { margin-left: auto; font-size: 12px; color: var(--gold-text); }

/* остаток раунда + полоска */
.room-card-clock { display: flex; flex-direction: column; gap: 5px; }
.room-card-clock-row {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 11.5px; color: var(--ink3);
}
.room-card-rest { color: var(--ink2); font-weight: 600; font-variant-numeric: tabular-nums; }
.room-card-bar {
  display: block; height: 3px; border-radius: 2px;
  background: var(--line); overflow: hidden;
}
.room-card-bar > span { display: block; height: 100%; background: var(--gold); border-radius: 2px; }

/* CTA — <span>, а не кнопка: вложенная кнопка внутри кнопки-карточки
   невалидна и разорвала бы карточку на два узла (см. rooms.js::roomCardCta). */
.room-cta {
  display: flex; align-items: center; justify-content: center;
  margin-top: auto; min-height: 42px; padding: 0 12px; border-radius: 10px;
  font-size: 13.5px; font-weight: 600; text-align: center;
  background: transparent; color: var(--ink2); border: 1px solid var(--line2);
}
.room-cta.is-primary { background: var(--gold); color: var(--on-gold); border-color: transparent; }

/* ---- дашед-карточка «Создать комнату» ---- */

.room-new {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; min-height: 240px; padding: 24px 18px; text-align: center;
  background: transparent;
  /* Ровно var(--line2), как у .persona-add: это ОДИН элемент продукта на двух
     экранах, и обводка у него обязана быть одна. Прежний color-mix 85 %
     ослаблял и без того слабую линию, уводя её от макета в другую сторону, чем
     у персон, — а рядом на одном экране их не видно, и разъезд жил бы долго. */
  border: 1.5px dashed var(--line2);
  border-radius: 16px;
  color: var(--ink2); font-family: var(--font-ui); font-size: 13.5px; cursor: pointer;
  transition: border-color .15s, background .15s;
  -webkit-tap-highlight-color: transparent;
}
.room-new-plus {
  display: inline-flex; align-items: center; justify-content: center;
  width: 56px; height: 56px; border-radius: 50%;
  border: 1.5px dashed var(--gold); color: var(--gold);
  font-size: 24px; line-height: 1;
}
.room-new-title { font-weight: 600; color: var(--ink); }
.room-new-note { max-width: 220px; font-size: 12px; line-height: 1.45; color: var(--ink3); }

/* ---- мобильная вёрстка ---- */
@media (max-width: 719px) {
  #view-rooms .room-head-acts { width: 100%; }
  .room-code-field { flex: 1 1 170px; }
  .room-code-field input { width: 100%; }
  /* Существующее `.room-head-acts button { flex: 1 1 auto }` растянуло бы и
     встроенную кнопку «Войти» — поле кода схлопнулось бы до половины. */
  .room-code-field .room-code-go { flex: none; }
  #view-rooms .room-create-btn { flex: 1 1 100%; justify-content: center; }
  .room-new { min-height: 180px; }
}

/* Hover'ы вкладки живут ОДНИМ блоком под @media (hover: hover) — правило дома:
   на телефоне :hover залипает после тапа, и карточка осталась бы подсвеченной
   после возврата из комнаты. Блок идёт ПОСЛЕДНИМ: у него та же специфичность,
   что у .is-turn выше, и порядок здесь и есть разрешение спора. */
@media (hover: hover) {
  #view-rooms .room-card:hover { border-color: var(--gold); transform: translateY(-2px); }
  .room-new:hover { border-color: var(--gold); background: var(--gold-soft); }
  .room-code-go:hover { background: var(--gold); color: var(--on-gold); }
  .rooms-turn-go:hover { color: var(--gold); }
}

/* Пульс листика — приглашение, а не индикатор: гасим его тем, кто просил
   меньше движения. Пилюля и золото остаются, новость не теряется. */
@media (prefers-reduced-motion: reduce) {
  .room-badge-leaf, .rooms-turn-leaf { animation: none; }
  #view-rooms .room-card:hover { transform: none; }
}

/* ------------------------------------------------------------ v2 (2026-08-12): Персоны */
/* ============================================================
   Turnleaves — v2: кабинет персон (макет design_import/«Персоны v2.dc.html»).
   Разметку карточек рисует web/personas.js (personaCardHtml/renderPersonas).

   ЧТО ЭТОТ БЛОК ЗАМЕНЯЕТ в web/styles.css: секцию «---- кабинет: сетка карточек
   ----» ЦЕЛИКОМ, от `.persona-grid` до `.personas-note` включительно (сейчас
   ~строки 4218-4288). Дальше по файлу идёт редактор персоны
   (`.persona-forge-row` и ниже), лист входа и хроника — их НЕ трогать: макет
   про сетку кабинета, а модалки ведут себя как раньше.

   ТРИ СТАРЫХ ПРАВИЛА ОБЯЗАНЫ УЕХАТЬ ВМЕСТЕ С БЛОКОМ, иначе ломается молча:
     • `@media (min-width: 720px) { .persona-ava { width: 96px; … } }` —
       медиазапрос не добавляет специфичности, но стоит ПОЗЖЕ, и он вернул бы
       кружок с 118px обратно на 96 на любом экране шире планшета. Здесь размер
       один на всех ширинах, поэтому селектор ниже намеренно `.persona-card
       .persona-ava` (0,2,0) — он переживёт и забытый медиазапрос.
     • `.persona-head` — обёртки «лицо + подпись в строку» в разметке больше
       нет, карточка выкладывается одной колонкой по центру.
     • `.persona-default-pill` — пилюля переименована в `.persona-main-pill`
       вместе с подписью («по умолчанию» → «основная»).

   Хвост hover-состояний в общем `@media (hover: hover)` блоке styles.css
   (`.persona-card:hover` / `.persona-add:hover` / `.persona-counts:hover`)
   остаётся рабочим — эти селекторы не менялись. Ниже добавлен только hover
   квадратной кнопки-карандаша, которой раньше не было.

   ЦВЕТА — ТОЛЬКО ТОКЕНАМИ. Макет нарисован ровно в них (#1C1916 = var(--surface)
   и т.д.), а светлая тема переопределяет те же токены на :root[data-theme=
   "light"]. Сырой хекс из тёмной палитры остался бы в светлой теме дырой, и
   заметил бы это не автор правки, а пользователь.
   ============================================================ */

/* Счётчик «N из M персон» в шапке вида. Живёт прямо в .view-head рядом с
   заголовком — её flex-wrap сам уводит счётчик под заголовок на узком экране,
   своей медиа-точки не нужно. tabular-nums, чтобы число не дёргало соседей при
   переходе 9 → 10. */
.personas-count {
  flex: none;
  font-size: 12.5px;
  color: var(--ink3);
  font-variant-numeric: tabular-nums;
}

/* ---- кабинет: сетка карточек (v2) ---- */
/* Своя сетка, а не .library: у книги обложка-корешок и вертикальная карточка,
   у персоны — лицо во всю ширину колонки. minmax(min(100%, 250px), 1fr), а не
   minmax(250px, 1fr): на экране УЖЕ 250px вторая форма выпирает за вьюпорт и
   даёт горизонтальный скролл всей странице. */
.persona-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 250px), 1fr));
  gap: 18px;
}

/* Карточка — «портрет в рамке»: всё по центру, лицо главное. */
.persona-card {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 26px 18px 18px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 16px;
  min-width: 0;
  text-align: center;
  box-shadow: var(--shadow-soft);
  transition: border-color .15s;
}

/* Кружок и пилюля «основная».
   Обёртка нужна ровно затем, что пилюля обязана жить СНАРУЖИ .char-ava:
   renderCharAva (wizard.js) перерисовывает кружок через innerHTML, когда
   приезжает лицо, и вложенная пилюля пропала бы именно в этот момент; плюс у
   .char-ava overflow:hidden, который срезал бы её свисающий угол. */
.persona-ava-wrap { position: relative; flex: none; display: inline-flex; }
/* 118px по макету: в кабинете лицо — то, по чему человек узнаёт свою персону,
   а не иллюстрация к имени. margin-right базового .char-ava снимаем —
   раскладку держит gap колонки. */
.persona-card .persona-ava {
  width: 118px; height: 118px; font-size: 44px; margin-right: 0;
  box-shadow: 0 0 0 3px var(--bg), 0 0 0 4.5px var(--line2);
}
/* Основная персона — золотое кольцо. Внутренние 3px цветом ФОНА, а не карточки:
   так кольцо читается как отдельный контур, а не как ободок портрета. */
.persona-ava-wrap.is-main .persona-ava {
  box-shadow: 0 0 0 3px var(--bg), 0 0 0 4.5px var(--gold);
}
.persona-main-pill {
  position: absolute; right: -2px; bottom: -2px;
  padding: 3px 9px; border-radius: 999px;
  background: var(--gold); color: var(--on-gold);
  font-family: var(--font-ui); font-size: 10px; font-weight: 700;
  line-height: 1.4; white-space: nowrap;
}

.persona-ident {
  display: flex; flex-direction: column; gap: 1px;
  min-width: 0; max-width: 100%;
}
.persona-name {
  font-family: var(--font-serif); font-size: 20px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.persona-pronouns { font-size: 12.5px; color: var(--ink3); }

/* Характер — две строки с многоточием: карточка кабинета показывает человека,
   а не читает его биографию (полный текст живёт в редакторе). */
.persona-temperament {
  margin: 0; font-family: var(--font-serif); font-size: 13.5px; line-height: 1.5;
  color: var(--ink2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* «Характер не описан» — курсивом и тише: это ОТСУТСТВИЕ текста, а не текст,
   и выглядеть как чей-то характер оно не должно. */
.persona-temperament.muted { color: var(--ink3); font-style: italic; }

/* Оси решений — пилюли без подписи «Любимые решения»: на карточке 250px
   подпись занимала строку, а сам чип уже читается как «во что эта персона
   играет». Полная формулировка осталась в редакторе. */
.persona-axes {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 5px; margin: 0;
}
.persona-axis {
  display: inline-flex; padding: 5px 11px; border-radius: 999px;
  background: var(--raised); border: 1px solid var(--line2);
  color: var(--ink2); font-size: 11.5px; line-height: 1;
}

/* Строка-вход в хронику. margin-top:auto прижимает низ карточки к низу ячейки,
   поэтому в ряду кнопки «Войти в мир» стоят на одной линии независимо от того,
   у кого длиннее характер. У свежей персоны этой строки НЕТ вовсе — тогда auto
   переезжает на сами кнопки (правило ниже), и низ всё равно выравнивается. */
.persona-counts {
  width: 100%; margin-top: auto;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 8px 10px;
  background: none; border: none; border-top: 1px solid var(--line);
  color: var(--ink3); font-family: var(--font-ui); font-size: 12.5px;
  font-variant-numeric: tabular-nums; cursor: pointer;
  transition: color .15s;
}
/* Мини-корешки миров (макет). Это ДЕКОРАЦИЯ СЧЁТЧИКА, как ряд кружков на
   карточке комнаты: цвет детерминирован id персоны (personas.js::
   personaSpinesHtml берёт палитру у library.js::coverColor), правду про число
   несёт соседний текст, весь ряд aria-hidden. Форма — корешок книги, а не
   точка: тот же скруглённый слева-направо профиль, что у .book-cover. */
.persona-spines { display: inline-flex; flex: none; gap: 3px; }
.persona-spine { width: 10px; height: 14px; border-radius: 1px 3px 3px 1px; }
.persona-acts { display: flex; gap: 8px; width: 100%; margin-top: auto; }
/* Оба margin-top:auto одновременно поделили бы свободное место пополам и
   оторвали строку хроники от кнопок — поэтому при живой хронике auto у кнопок
   снимается. */
.persona-counts ~ .persona-acts { margin-top: 0; }
.persona-btn { flex: 1 1 auto; min-height: var(--tap); font-size: 13.5px; font-weight: 600; }
/* Квадрат ровно в тач-таргет: смысл кнопки держат title/aria-label, а не
   подпись, поэтому размер обязан оставаться пальцевым. */
.persona-btn-icon {
  flex: none; width: var(--tap); min-height: var(--tap); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1;
}

/* Карточка-«плюс» ростом с соседей: сетка не должна оседать на последней
   ячейке. min-height берётся от роста карточки персоны с двумя строками
   характера — так «Создать» не выглядит обрубком в ряду с живыми персонами. */
.persona-add {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; min-height: 280px; padding: 24px 18px;
  background: transparent;
  border: 1.5px dashed var(--line2); border-radius: 16px;
  color: var(--ink2); font-family: var(--font-ui); font-size: 13.5px;
  text-align: center; cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.persona-add[disabled] { opacity: .45; cursor: not-allowed; }
.persona-add-glyph {
  display: inline-flex; align-items: center; justify-content: center;
  width: 64px; height: 64px; border-radius: 50%;
  border: 1.5px dashed var(--gold); color: var(--gold);
  font-size: 28px; line-height: 1;
}
.persona-add-title { font-weight: 600; color: var(--ink); }
.persona-add-hint { max-width: 210px; font-size: 12px; line-height: 1.45; color: var(--ink3); }

/* Подпись под сеткой снимает главный вопрос до того, как он задан (§7.1).
   По центру — она относится ко всей сетке, а не к первой колонке. */
.personas-note {
  margin: 20px 0 0; font-family: var(--font-serif); font-style: italic;
  font-size: 13.5px; color: var(--ink3); text-align: center;
}

/* Hover только там, где есть настоящий курсор: на тачскрине :hover залипает
   после тапа (общее правило styles.css). */
@media (hover: hover) {
  .persona-btn-icon:hover { border-color: var(--gold); color: var(--gold-text); }
}

/* ============================================================
   ФОНОВАЯ МУЗЫКА ЧИТАЛКИ (BGM, 2026-08-20) — web/music.js
   Кнопка «Музыка» и ползунок громкости жили в .book-actions рядом со
   «Слушать»; с 2026-08-21 (макет Reader Chapter Interactive) они переехали
   в аудио-пилюлю #tts-bar, и её геометрию задаёт блок TTS-бара выше по файлу.
   Здешние правила остались общими (фон/цвет включённой музыки, ширина
   ползунка) — они верны в любом контейнере и переживают переезд.
   Блок стоит ПЕРЕД нижней навигацией намеренно: .lownav обязан
   оставаться ниже всего, что может с ним конкурировать, — его пункты носят
   класс .tab и выигрывают у общих вкладочных правил только порядком. Здесь
   поэтому НЕТ ни одного правила на .tab и ни одного на голый .ba-btn:
   селекторы сужены до .music-btn / .music-vol, иначе этот блок переопределил
   бы геометрию всех кнопок панели книги.
   ============================================================ */
.music-btn.playing { background: var(--gold-soft); color: var(--gold-text); }
.music-btn.playing .ba-glyph { opacity: 1; }
/* Ползунок появляется только вместе с включённой музыкой (music.js::musicSyncBtn).
   Ширина маленькая: это подстройка фона, а не главный орган управления. */
.music-vol {
  width: 76px; align-self: center; margin: 0 10px 0 2px;
  accent-color: var(--gold); background: none; cursor: pointer;
}
@media (max-width: 719.9px) {
  /* На телефоне ряд действий и так переносится (flex-wrap) — ползунок сужаем,
     чтобы он не утаскивал на новую строку кнопку «⋯». */
  .music-vol { width: 60px; margin: 0 6px 0 2px; }
}

/* ============================================================
   РЕЖИМ ПРОДУКТА (аудит 2026-08-23, onboarding_ids)
   Человек ответил на вопрос первого входа, и лишние пункты меню исчезли.
   Основной механизм — JS (auth.js::syncModeNavigation ставит .hidden);
   эти правила — ВТОРОЙ пояс на случай, когда index.html свежий, а
   onboarding.js приехал из кэша браузера старым. Гейт зеркалит гостевой
   (body.guest-mode / body.email-unverified): те же ворота, тот же приём.

   Компенсации padding НЕ меняются намеренно: бар никогда не пустеет —
   «Каталог» и «Библиотеку» не прячет ни один режим, минимум два пункта
   остаются, и высота бара (--lownav-h) прежняя.
   ============================================================ */
/* Исключение `:not(.has-rooms)` — решение владельца роя №13 (эпик комнат
   2026-08-29): пункт «Комнаты» виден в ЛЮБОМ режиме тому, у кого комната уже
   есть. Класс на body ставит onboarding.js::ffModeSyncBody по одностороннему
   флагу rooms.js. Без этой строки приглашённый друг, ответивший «читать
   истории», попадал в свою комнату только по ссылке из мессенджера. */
body.mode-reader .tab[data-view="new"],
body.mode-reader:not(.has-rooms) .tab[data-view="rooms"],
body.mode-reader .tab[data-view="personas"],
body.mode-creator:not(.has-rooms) .tab[data-view="rooms"],
body.mode-creator .tab[data-view="personas"],
body.mode-author:not(.has-rooms) .tab[data-view="rooms"],
body.mode-author .tab[data-view="personas"],
body.mode-play .tab[data-view="personas"] { display: none !important; }

/* ============================================================
   НИЖНЯЯ НАВИГАЦИЯ ТЕЛЕФОНА (макеты v2, 2026-08-12)
   Пять пунктов вместо ряда вкладок в шапке: Библиотека ▤ · Персоны ◔ ·
   Новая ✦ · Комнаты ⧉ · Каталог ❖. Разметка — <nav id="lownav"> в конце
   index.html, пункты — обычные .tab с data-view (одна проводка в app.js,
   одна подсветка в state.js::showView).
   Блок стоит В КОНЦЕ ФАЙЛА НАМЕРЕННО: пункты носят класс .tab и ловят на
   себя ВСЕ его правила — вкладочную геометрию (flex/padding/подчёркивание
   активной) и, для пункта «Новая», золотую CTA .tab[data-view="new"] вместе
   с её hover-правилом из общего @media (hover:hover). Специфичности у них
   те же, что у здешних, поэтому побеждает порядок: выше по файлу этот блок
   молча проиграл бы, и пункт «Новая» приехал бы золотой кнопкой посреди
   бара, а активный пункт — с подчёркиванием вместо золотого цвета.
   ============================================================ */
/* Высота бара БЕЗ safe-area: 6+6 padding + 52 пункт. Ею отодвигаются от низа
   все остальные нижние поверхности телефона (панель мастера, плеер озвучки),
   иначе они лягут ровно под бар и станут ненажимаемыми. */
:root { --lownav-h: 64px; }
/* ≥720px бара нет вовсе: навигация живёт вкладками шапки. */
.lownav { display: none; }

@media (max-width: 719.9px) {
  /* Гостю бар не показываем: приватные пункты ему скрывает тот же
     auth-only-nav, что и вкладки шапки, и остался бы бар из одного
     «Каталога» — при том, что гостевая шапка и так однострочная и вкладку
     «Каталог» в ней видно (правила body.guest-mode .topbar выше).
     НЕПОДТВЕРЖДЁННАЯ ПОЧТА — ровно тот же случай, и его надо было назвать
     отдельно: `body.email-unverified .auth-only-nav { display: none
     !important }` гасит все ЧЕТЫРЕ приватных пункта так же, как вкладки шапки,
     и бар схлопывался в один «Каталог», растянутый на всю ширину, отъедая
     64px нижней трети экрана. Гейт по гостю этого не ловит: аккаунт есть,
     .guest-mode не стоит. */
  /* ФОН БАРА НЕПРОЗРАЧНЫЙ — и это про контраст, а не про вкус (эпик
     2026-08-28, P1-7).

     Было `color-mix(in oklab, var(--bg) 92%, transparent)` + блюр. 8% чужого
     цвета звучит как ничто, но контраст считается по СОСТАВНОМУ фону, а под
     баром проезжает произвольный контент — обложки, золотые полосы прогресса,
     тёмные карточки. Живой замер axe на библиотеке дал фон #EBE4D4 (под баром
     стояло золото) вместо ожидаемого #F3EEE2, и подпись пункта, честные 4.76:1
     к --bg, проваливалась до 4.35. Худший случай (чёрное под баром) — 3.99.
     Подписи здесь 10.5px, послабления для крупного текста нет, а какой именно
     контент окажется снизу, CSS не знает — гарантировать 4.5:1 при
     полупрозрачном баре нельзя в принципе.
     Блюр убран вместе с прозрачностью: за непрозрачным фоном ему нечего
     размывать, и это лишний слой композитинга на каждом кадре прокрутки.
     Шапка (.topbar, 82% + blur 16px) осталась стеклянной намеренно — там тот
     же риск, но это подпись макета v3 и решение владельца; замеры и разбор — в
     docs/UXA_FIX_2026-08-28/notes/theme_contrast_done.md §«Открытые вопросы». */
  body:not(.guest-mode):not(.email-unverified) .lownav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    display: flex; align-items: stretch;
    background: var(--bg);
    border-top: 1px solid var(--line);
    padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
    transition: transform .25s ease;
  }
  /* Пункт — колонка «глиф + подпись»; подпись отдельным текстовым узлом
     (i18n переводит узлы, а не элементы), глиф — под data-no-i18n. */
  .lownav .tab {
    flex: 1; min-width: 0; min-height: 52px;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
    margin: 0; padding: 4px 2px;
    background: none; border: none; border-radius: 0; box-shadow: none;
    font-size: 10.5px; font-weight: 600; line-height: 1.15;
    color: var(--ink3);
    -webkit-tap-highlight-color: transparent;
  }
  .lownav .tab.active { color: var(--gold-text); box-shadow: none; }
  .lownav .tab:active { color: var(--gold-text); }
  /* Пункт «Новая» — такой же тихий, как соседи: золотая кнопка из шапки в
     ряду из пяти пунктов читалась бы как единственное действие экрана. */
  .lownav .tab[data-view="new"] {
    flex: 1; align-self: auto;
    margin: 0; padding: 4px 2px;
    background: none; color: var(--ink3);
    border-radius: 0; box-shadow: none;
    font-size: 10.5px; font-weight: 600;
  }
  .lownav .tab[data-view="new"].active { color: var(--gold-text); box-shadow: none; }
  .lownav-ico { font-size: 19px; line-height: 1; }

  /* Контент не должен уходить под бар (в макете main { padding-bottom: 96px }).
     Гейт у ВСЕХ компенсаций ровно тот же, что у самого бара: отступ под бар,
     которого нет, — это 96px мёртвого места внизу экрана, а поднятая на 64px
     панель мастера просто висит над пустотой. Расходиться этим двум условиям
     нельзя, поэтому :not(.email-unverified) дописан всюду разом. */
  body:not(.guest-mode):not(.email-unverified) main { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }
  /* Мастер: его панель «Назад / Далее» тоже прибита к низу. Поднимаем её НАД
     баром, а не прячем бар: на телефоне бар — единственная навигация, и без
     него человек остался бы в мастере без выхода. Отступ вида растёт на ту же
     высоту, иначе последний вопрос шага уедет под панель. */
  body:not(.guest-mode):not(.email-unverified) .wiz-nav {
    bottom: calc(var(--lownav-h) + env(safe-area-inset-bottom));
    padding-bottom: 10px;
  }
  body:not(.guest-mode):not(.email-unverified) #view-new {
    padding-bottom: calc(96px + var(--lownav-h) + env(safe-area-inset-bottom));
  }
  /* Плеер озвучки — по той же причине; в читалке при скролле вниз бар уезжает
     (body.chrome-hidden), и плеер возвращается на своё штатное место. */
  body:not(.guest-mode):not(.email-unverified) .tts-bar {
    bottom: calc(14px + var(--lownav-h) + env(safe-area-inset-bottom, 0px));
  }
  body:not(.guest-mode):not(.email-unverified).chrome-hidden .tts-bar {
    bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  }
  /* Список музыкальных тем едет за пилюлей: он всплывает НАД ней, и, оставшись
     на месте, наполовину ушёл бы под нижнюю навигацию. */
  body:not(.guest-mode):not(.email-unverified) .music-theme-menu {
    bottom: calc(80px + var(--lownav-h) + env(safe-area-inset-bottom, 0px));
  }
  body:not(.guest-mode):not(.email-unverified).chrome-hidden .music-theme-menu {
    bottom: calc(80px + env(safe-area-inset-bottom, 0px));
  }
  /* .audiobar-on — пилюля стоит внизу и БЕЗ озвучки (макет Reader Chapter
     Interactive, 2026-08-21), значит и отступ нужен всё время, пока она видна. */
  body:not(.guest-mode):not(.email-unverified).tts-on #view-book,
  body:not(.guest-mode):not(.email-unverified).audiobar-on #view-book { padding-bottom: calc(120px + var(--lownav-h)); }
  /* Запас автопрокрутки озвучки — с высотой нижней навигации: пилюля здесь
     поднята на var(--lownav-h) (см. .tts-bar выше), и без добавки читаемый
     абзац снова оказался бы под ней. chrome-hidden отдельно не ужимаем:
     больший запас лишь ставит абзац чуть выше — это не перекрытие. */
  body:not(.guest-mode):not(.email-unverified).tts-on #prose [data-p] {
    scroll-margin-bottom: calc(88px + var(--lownav-h) + env(safe-area-inset-bottom, 0px));
  }
}

@media (hover: hover) and (max-width: 719.9px) {
  /* Перебиваем общие hover-правила вкладок: .tab:hover красит в --ink, а
     .tab[data-view="new"]:hover ещё и подсвечивает золотую кнопку. */
  .lownav .tab:hover { color: var(--gold-text); }
  .lownav .tab[data-view="new"]:hover { color: var(--gold-text); filter: none; }
}

@media (max-width: 419.9px) {
  /* ДВУХСТРОЧНАЯ ПИЛЮЛЯ НАД НИЖНЕЙ НАВИГАЦИЕЙ (эпик 2026-08-27 §2.4).

     Компенсации выше по файлу (`body.tts-on #view-book { padding-bottom: 168px }`)
     на телефоне проигрывают: блок нижней навигации стоит ПОСЛЕ них и его
     селекторы с тремя :not() старше по специфичности. Поэтому запас под вторую
     строку повторяется здесь — тем же гейтом, что и у самого бара, и после
     него по порядку файла. Расходиться этим правилам нельзя: пилюля поднята на
     var(--lownav-h), и без добавки конец главы уехал бы под неё. */
  body:not(.guest-mode):not(.email-unverified).tts-on #view-book,
  body:not(.guest-mode):not(.email-unverified).audiobar-on #view-book {
    padding-bottom: calc(168px + var(--lownav-h));
  }
  body:not(.guest-mode):not(.email-unverified).tts-on #prose [data-p] {
    scroll-margin-bottom: calc(136px + var(--lownav-h) + env(safe-area-inset-bottom, 0px));
  }
  /* Список тем — над двухстрочной пилюлей, поднятой на нижнюю навигацию. */
  body:not(.guest-mode):not(.email-unverified) .music-theme-menu {
    bottom: calc(122px + var(--lownav-h) + env(safe-area-inset-bottom, 0px));
  }
  body:not(.guest-mode):not(.email-unverified).chrome-hidden .music-theme-menu {
    bottom: calc(122px + env(safe-area-inset-bottom, 0px));
  }
}

/* ============================================================
   МИНИ-БАР ГЕНЕРАЦИИ (глобальный индикатор «глава пишется»)
   Узла в разметке нет — панель строит web/state.js::ffGenMiniNode.
   ============================================================ */
/* z-index 64 — НАД нижней навигацией телефона (60) и ПОД всем, что человек
   открыл сам: скрим панели (65), сама панель (70/80), список уведомлений (80),
   модалки и экран входа (100+). Фоновый индикатор не имеет права перекрыть ни
   одну поверхность, которую вызвали кликом. */
.gen-mini {
  position: fixed; z-index: 64;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px 9px 14px;
  background: var(--raised);
  border: 1px solid var(--line2); border-radius: 14px;
  box-shadow: var(--shadow-soft);
  font-size: 13px; color: var(--ink2); line-height: 1.35;
  animation: gen-mini-in .25s ease;
}
/* Готовность — единственное состояние, ради которого сюда смотрят: подсвечиваем
   рамкой золотом, а не цветом всей панели (панель живёт поверх любого вида и
   кричать не должна). */
.gen-mini.is-ready { border-color: var(--gold); color: var(--ink); }
.gen-mini.is-ready .gen-mini-ico { color: var(--gold-text); }
.gen-mini-ico { font-size: 15px; line-height: 1; flex: none; }
/* Текст сжимается первым: на узком экране кнопка «Вернуться» важнее подписи. */
.gen-mini-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gen-mini-eta { color: var(--ink3); }
.gen-mini-back { flex: none; padding: 7px 12px; min-height: 36px; font-size: 12.5px; }
@keyframes gen-mini-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ≥720px: левый нижний угол. Правый занят тостами (#toasts), и делить его с
   ними значило бы перекрывать сообщение об ошибке панелью про генерацию. */
@media (min-width: 720px) {
  .gen-mini { left: 18px; bottom: 18px; max-width: 380px; }
}
/* <720px: панель уезжает ПОД ШАПКУ, а не вниз. Внизу телефона и так тесно:
   нижняя навигация (--lownav-h), плеер озвучки, фиксированная панель мастера
   «Назад / Далее» и стопка тостов — любая из них либо перекрыла бы бар, либо
   стала бы ненажимаемой под ним. Сверху свободно и панель видно сразу. */
@media (max-width: 719.9px) {
  .gen-mini {
    left: 10px; right: 10px;
    top: calc(var(--tb-h, 64px) + 8px);
  }
}

/* ============================================================
   БАТЧ «90-секундное ожидание: саспенс вместо удержания» (2026-08-12)
   Экран ожидания перестал быть каруселью: карточка принятого выбора, две
   честные оценки вместо псевдопроцента, «что уже сдвинулось», тизер по
   фрагментам, прожектор истории и тонкая полоса, в которую ожидание
   сворачивается, когда пошёл первый связный абзац.
   ============================================================ */

/* «Выбор принят» — первое, что видит читатель: его собственная ставка на
   историю целиком, а не буква варианта. Тише тизера (никакой золотой рамки):
   это напоминание, а не новость. */
.gen-choice {
  width: min(440px, 100%); box-sizing: border-box; text-align: left;
  border: 1px solid var(--line); background: var(--raised);
  border-radius: 14px; padding: 13px 17px;
  animation: ff-fade-up .4s ease both;
}
.gen-choice-kicker {
  font-size: 10.5px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink3); margin-bottom: 6px;
}
.gen-choice-text {
  font-family: var(--font-serif); font-style: italic;
  font-size: 15px; line-height: 1.55; color: var(--ink);
}

/* Две честные оценки. Числа — tabular-nums: они обновляются на месте, и
   прыгающая ширина цифр читалась бы как дёрганье интерфейса. */
.gen-eta {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 22px;
  width: min(440px, 100%);
}
.gen-eta-item { display: inline-flex; align-items: baseline; gap: 6px; font-size: 12.5px; }
.gen-eta-label { color: var(--ink3); }
.gen-eta-value, .gen-eta-unit { color: var(--ink2); font-weight: 600; font-variant-numeric: tabular-nums; }

/* «Что уже сдвинулось»: последствия ПРОШЛОГО выбора, пока пишется следующая
   глава. Наследует рамку тизера (.gen-teaser), меняет только раскладку строк. */
.gen-aftermath { display: flex; flex-direction: column; gap: 7px; }
.gen-aftermath-row { display: flex; align-items: baseline; gap: 10px; font-size: 13.5px; line-height: 1.5; }
.gen-aftermath-label {
  flex: none; min-width: 82px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  color: var(--gold-text);
}
.gen-aftermath-text { color: var(--ink2); font-variant-numeric: tabular-nums; }

/* Тизер по фрагментам: каждый новый кусок проявляется сам, уже показанные не
   перерисовываются (см. _paintTeaser — ключ по числу фрагментов). */
.gen-teaser-part { display: block; animation: ff-fade-up .5s ease both; }
.gen-teaser-part + .gen-teaser-part { margin-top: 6px; }

/* Подпись прожектора: обещание, которое мы реально держим (нить получит
   движение), без обещания исхода. */
.gen-bet-note { font-size: 12px; color: var(--ink3); font-style: italic; }

/* Свёрнутое ожидание: полоса над прозой. Живёт СНАРУЖИ #prose (текст
   переписывается по токенам каждые ~500мс) и потому не перезапускает анимаций. */
.gen-live-strip {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 18px; padding: 0 2px;
  animation: ff-fade-up .35s ease both;
}
.gen-live-strip-track {
  flex: 1; height: 3px; border-radius: 999px; background: var(--line); overflow: hidden;
}
.gen-live-strip-fill {
  display: block; height: 100%; width: 0; border-radius: 999px; background: var(--gold);
  transition: width .6s ease;
}
.gen-live-strip-label {
  flex: none; font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink3); font-style: italic;
}

/* Итог прожектора — карточка в конце главы вместо тоста: тост жил три секунды
   и улетал мимо человека, который в этот момент читал первый абзац. */
.gen-spot-outcome {
  border: 1px solid var(--line); background: var(--raised);
  border-radius: 14px; padding: 14px 17px; margin: 0 0 16px;
  animation: ff-fade-up .4s ease both;
}
.gen-spot-kicker {
  font-size: 10.5px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase;
  color: var(--gold-text); margin-bottom: 8px;
}
.gen-spot-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  padding: 4px 0; font-size: 13.5px; line-height: 1.5;
}
.gen-spot-title { font-family: var(--font-serif); font-style: italic; color: var(--ink); }
.gen-spot-verdict { color: var(--ink3); }
.gen-spot-verdict.hit { color: var(--gold-text); font-weight: 600; }
.gen-spot-ach { margin-top: 8px; font-size: 12.5px; color: var(--ink2); }
.gen-spot-ach-n { font-variant-numeric: tabular-nums; color: var(--ink3); }
.gen-spot-note { margin-top: 8px; font-size: 12px; color: var(--ink3); font-style: italic; }

/* Панель «Лица персонажей» (аудит 2026-08-23, ядро-2) — блок в рабочем месте
   модератора. Своей палитры не заводит: только сетка и моноширинный вывод. */
.mod-identity { margin-top: 24px; border-top: 1px solid var(--line); padding-top: 16px; }
.mod-identity .ci-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px 20px; }
.mod-identity .ci-row { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; border-bottom: 1px dashed var(--line); }
.mod-identity .ci-k { opacity: .75; }
.mod-identity .ci-v { font-variant-numeric: tabular-nums; }
.mod-identity .ci-table { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 14px; }
.mod-identity .ci-table th, .mod-identity .ci-table td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--line); }
.mod-identity .ci-alias { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; align-items: center; }
.mod-identity .ci-out { white-space: pre-wrap; width: 100%; margin: 0; font-size: 13px; opacity: .85; }

/* ============================================================
   РЕЖИМ «БЫСТРОЕ ЧТЕНИЕ · FLASH» (эпик flash_prefetch4, 2026-09-10)

   Пока человек читает главу, сервер готовит четыре продолжения. На экране от
   этого ровно две строки — и обе НАМЕРЕННО тихие: решение владельца роя Р17
   запрещает подсвечивать готовые ветки и называть числа («3 из 4 готовы»
   превращает выбор истории в игру «жми готовое»). Своей палитры блок не
   заводит: те же --ink3 и .note, что у остальных сносок читалки.
   ============================================================ */
.prefetch-note {
  margin: 0 0 12px; font-size: 12.5px; line-height: 1.5;
  color: var(--ink3); font-style: italic;
}
/* Строка стоит в разметке ещё до первого ответа сервера: `aria-live` объявляет
   изменения только в УЖЕ СУЩЕСТВУЮЩЕЙ живой области, а вставленный целиком
   абзац скринридер молча пропустит.

   ПОЭТОМУ ПУСТАЯ СТРОКА ПРЯЧЕТСЯ ПО-ЗРЯЧЕМУ, А НЕ `display:none`. Здесь стояло
   именно оно — и отменяло приём выше целиком: `display:none` выкидывает узел из
   дерева доступности, то есть до первого ответа живой области для диктора НЕ
   СУЩЕСТВОВАЛО, а появлялась она ровно тем присваиванием, которое и должна была
   объявить (`generation.js::prefetchPaint`). Регион, зарегистрированный вместе с
   содержимым, NVDA/JAWS штатно молчат — и «Продолжения готовятся, пока вы
   читаете» незрячий читатель не слышал ни разу; сообщение это часто
   ЕДИНСТВЕННОЕ (на терминальных статусах тикер гаснет и второй покраски не
   будет). Приём — тот же, что у `.sr-only` (reading_prefs.css): узел остаётся в
   дереве, но не занимает ни пикселя; `clip` и `clip-path` оба намеренно (старые
   WebKit не знают второго). Пришла подпись — `:empty` перестаёт совпадать, и
   строка возвращается в поток обычным абзацем. */
.prefetch-note:empty {
  position: absolute;
  width: 1px; height: 1px;
  margin: 0; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Честная подпись у своего варианта: заранее готовятся только предложенные
   ветки. Блочная и вплотную к полю — она объясняет ИМЕННО это поле. */
.custom-choice .prefetch-custom-note { display: block; margin: 8px 0 12px; }
/* Второй блок настроек книги под «Тоном» (шторка «Память»). Тумблер здесь
   мельче мастерского по той же причине, что и счётчик тона: в шторке это
   сноска к книге, а не шаг создания. */
#panel-memory .book-genmode .switch { margin-top: 6px; }
#panel-memory .book-genmode .switch-text { font-size: 13px; }
#panel-memory .book-genmode .note { display: block; margin-top: 6px; line-height: 1.45; }
