/* public/assets/css/app.css — дизайн-система бюллетеня.
   Редакционный, журнальный язык (spec §9): крупная типографика вместо иконок,
   свой акцент у каждой темы, ироничный «бланк» в служебных элементах.
   Замена гарнитуры — правка в двух местах: переменная ниже и файлы в
   public/assets/fonts/. Ни разметка, ни шаблоны от выбора шрифта не зависят. */

/* --- Шрифты: самохостинг, внешних запросов нет ни одного (spec §9.2) ------ */
@font-face { font-family: "Rubik"; src: url("../fonts/Rubik-Regular.ttf") format("truetype"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Rubik"; src: url("../fonts/Rubik-Medium.ttf") format("truetype"); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Rubik"; src: url("../fonts/Rubik-Bold.ttf") format("truetype"); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: "PT Serif"; src: url("../fonts/PTSerif-Regular.ttf") format("truetype"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "PT Serif"; src: url("../fonts/PTSerif-Italic.ttf") format("truetype"); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: "PT Serif"; src: url("../fonts/PTSerif-Bold.ttf") format("truetype"); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: "PT Serif"; src: url("../fonts/PTSerif-BoldItalic.ttf") format("truetype"); font-weight: 700; font-style: italic; font-display: swap; }
@font-face { font-family: "PT Mono"; src: url("../fonts/PTMono-Regular.ttf") format("truetype"); font-weight: 400; font-style: normal; font-display: swap; }

/* --- Токены -------------------------------------------------------------- */
:root {
  --font-display: "Rubik", system-ui, sans-serif;
  --font-text: "PT Serif", Georgia, serif;
  --font-mono: "PT Mono", ui-monospace, monospace;

  /* База тёплая, не белая: холодный чистый белый утянул бы макет в ту самую
     корпоративность, от которой мы отталкиваемся (spec §9.3). */
  --paper: oklch(97% 0.012 85);
  --paper-sunk: oklch(94% 0.016 85);
  --ink: oklch(24% 0.02 60);
  --muted: oklch(48% 0.02 60);
  --rule: oklch(84% 0.02 70);

  /* Одна шкала отступов, кратная 4px, четыре ступени вертикального ритма. */
  --step-1: 4px;
  --step-2: 12px;
  --step-3: 28px;
  --step-4: 64px;

  /* Ширина текста ограничивается по ch, не по пикселям. */
  --measure: 48.9rem;
}

/* Девять акцентов задаются формулой, а не подбираются на глаз: фиксированные
   светлота и насыщенность, меняется только тон. Оба свойства живут на одном
   элементе намеренно — подстановка var() внутри пользовательского свойства
   выполняется там, где свойство объявлено, и --accent-h с <body> до :root не
   достанет (spec §9.3). */
body { --accent: var(--ink); }
body[data-topic] { --accent: oklch(58% 0.17 var(--accent-h)); }
.card:not(.card--bonus) .card__link { --accent: oklch(58% 0.17 var(--accent-h)); }

/* Бонусная тема выпадает из акцентной серии сознательно: девять тонов заведено
   ровно по числу ранжированных тем, десятого нет, и набор чернильным цветом и
   есть видимый признак «эта тема вне очереди» (spec §9.3, §9.4). Здесь только
   значение --accent — всё остальное (номер, линейка) считает его само. */
.card--bonus .card__link { --accent: var(--ink); }

@media (prefers-color-scheme: dark) {
  :root {
    --paper: oklch(21% 0.015 70);
    --paper-sunk: oklch(26% 0.017 70);
    --ink: oklch(92% 0.012 85);
    --muted: oklch(70% 0.015 85);
    --rule: oklch(38% 0.02 70);
    /* --annotate-mark-bg/-mark-done-bg (репорт владельца, 2026-09-15): бледная
       жёлтая/зелёная заливка annotate.css (#ffe9a8/#d7ead9, тот же дефолт, что
       и в этом же :root без темы) держит контраст только с тёмным --ink
       светлых тем. Здесь --ink светлый (текст .annotate-mark наследует его
       через color: inherit) — той же бледной заливке не хватит контраста ни
       при какой светлоте, поэтому это не «подвинуть тон», а тёмная заливка
       того же жёлтого/зелёного духа, специально для светлого текста поверх. */
    --annotate-mark-bg: #4a3c12;
    --annotate-mark-done-bg: #1f3d28;
  }
  body[data-topic] { --accent: oklch(72% 0.15 var(--accent-h)); }
  .card:not(.card--bonus) .card__link { --accent: oklch(72% 0.15 var(--accent-h)); }
  .card--bonus .card__link { --accent: var(--ink); }
}

/* --- База ---------------------------------------------------------------- */
/* Полоса прокрутки резервирует место всегда: иначе переключение темы, чуть
   меняющее высоту страницы своими украшениями, на одних темах даёт вертикальный
   скролл, на других нет — и ширина контента дёргается между ними. */
html { scrollbar-gutter: stable; }

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: 0 var(--step-3) var(--step-4);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: 18px;
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
}

h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 700; line-height: 1.1; margin: 0 0 var(--step-2); }
p { margin: 0 0 var(--step-2); }
a { color: inherit; text-decoration-color: var(--accent); text-underline-offset: 3px; }
a:hover { text-decoration-thickness: 2px; }

/* --- Шапка-бланк --------------------------------------------------------- */
.masthead {
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--step-3) 0 var(--step-2);
  border-bottom: 2px solid var(--ink);
}
.masthead__home { text-decoration: none; }
.masthead__name { font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; letter-spacing: -0.01em; }
.masthead__stamp,
.hub__order,
.topic__stamp,
.card__number,
.card__minutes,
.progress {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.masthead__downloads { display: flex; gap: var(--step-2); font-family: var(--font-mono); font-size: 0.78rem; }
/* Ссылка «Войти» — видна только анониму (masthead.twig, {% if identity.get() is null %}),
   исчезает сразу после входа любой из двух дверей (владелец/редактор). Тот же
   mono-регистр, что у .masthead__downloads a, отдельной строкой под ним —
   без text-decoration: none, иначе подчёркивание (и наведённое утолщение
   глобального a:hover) пропадает только у этой ссылки. */
.masthead__login { display: inline-block; margin-top: var(--step-2); font-family: var(--font-mono); font-size: 0.78rem; }

/* --- Счётчик ------------------------------------------------------------- */
/* Тон поощряющий и только: ноль из двадцати — это «начнём» (spec §7.4). */
.progress { margin: var(--step-2) 0; }
.progress__done { color: var(--accent); font-weight: 700; }
/* «Крупнее на хабе» (spec §7.4) — без единой правки схемы данных: страницу темы
   опознаёт data-slug, который стоит на <body> у каждой темы и только у неё
   (topic.twig), а у хаба <body> без атрибутов. Раньше этот пункт спецификации
   был отложен как требующий нового поля; появившийся data-slug закрыл ровно ту
   его половину, которая про размер. Вторая половина — «в каких темах остались
   неотмеченные практики» — по-прежнему ждёт колонки темы в practices.tsv. */
body:not([data-slug]) .progress { font-size: 1.05rem; letter-spacing: 0.02em; }
.reset {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
}

/* --- Хаб ----------------------------------------------------------------- */
body > main { max-width: var(--measure); margin: 0 auto; }
.hub__promise { font-size: clamp(2rem, 6vw, 3.2rem); margin-top: var(--step-4); }

/* Регресс (жалоба владельца: нав в шапке админки «прыгает» вверх-вниз при
   переходе между /admin/editors, /admin/review и /admin/notes) — раньше
   .admin/.admin-editors/.admin-review держали margin-top тремя отдельными
   однострочными правилами, а у .notes-page не было ни одного margin-top
   вовсе: /admin/notes рендерился вплотную к border-bottom шапки (0px),
   тогда как остальные три страницы — с отступом 64px, то есть при
   навигации между ними возникал скачок в 64px. Один общий отступ на все
   четыре страницы устраняет разъезд. */
.admin, .admin-editors, .admin-review, .notes-page, .admin-settings { margin-top: calc(var(--step-4) + var(--step-2)); }
.intro-strip {
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: var(--step-2) 0;
  margin: var(--step-3) 0;
}
.intro-strip__title { font-size: 1.1rem; margin-bottom: var(--step-1); }

/* CTA профиля на хабе — та же анатомия, что у .intro-strip: линейки сверху и
   снизу, ни своего фона, ни своих цветов, чтобы тема просвечивала как обычно.
   Текст (и, при отвеченном профиле, короткая сводка выбранного) пишет
   refreshProfileCta() (app.js); ссылка внутри всегда ведёт на /profile. */
.profile-cta {
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: var(--step-2) 0;
  margin: var(--step-3) 0;
  color: var(--muted);
  font-size: 0.95rem;
}
.profile-cta a { color: var(--ink); }

.cards { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--step-3); }
@media (min-width: 700px) { .cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .cards { grid-template-columns: repeat(3, 1fr); } }

.card__link {
  display: block;
  height: 100%;
  padding: var(--step-2);
  border: 1px solid var(--rule);
  border-top: 4px solid var(--accent);
  background: var(--paper-sunk);
  text-decoration: none;
}
.card__number { display: block; font-size: 2.2rem; line-height: 1; color: var(--accent); }
.card__title { font-size: 1.35rem; }
.card__summary { color: var(--ink); }
/* Верхняя линейка у бонусной карточки приглушена: её номер и заголовок уже
   чернильные через --accent, а сплошная чернильная линейка спорила бы по весу
   с девятью акцентными. Строк «мин» и шкал у карточки нет вовсе — их не
   печатает шаблон, поэтому прятать здесь нечего (spec §9.4). */
.card--bonus .card__link { border-top-color: var(--rule); }
.card__scales { display: flex; gap: var(--step-2); }
.scale { display: inline-flex; gap: 2px; align-items: center; }
.scale__pip { width: 8px; height: 8px; border: 1px solid var(--muted); border-radius: 50%; }
.scale--impact .scale__pip.is-on { background: var(--accent); border-color: var(--accent); }
.scale--effort .scale__pip.is-on { background: var(--muted); }

/* --- Страница темы ------------------------------------------------------- */

/* Плавающее меню перехода между темами (partials/topic-jump.twig) — список
   строит cards(), тот же вызов и тот же порядок, что у сетки хаба.

   Ниже брейкпоинта это обычный свёрнутый <details> НАД статьёй — та же
   анатомия, что у details.howto (акцентная грань слева, утопленный фон,
   свой треугольник вместо системного): свёрнут по умолчанию по той же
   причине, что и howto — развёрнутый список десяти тем заслонил бы саму
   статью на первом экране (см. комментарий у details.howto ниже). Раскрытие
   кликом — встроенное поведение <details>, без JS.

   На широком экране — ДРУГОЙ элемент разметки, не тот же самый
   принудительно раскрытый <details>. Раньше здесь стояла именно эта идея
   (переопределить браузерное `details:not([open]) > :not(summary) {
   display: none }` авторским `.topic-jump__list { display: block }`,
   рассчитывая, что авторские стили бьют user-agent), и в теории порядка
   cascade origin это верно — но живой Playwright-прогон в Chromium 151
   показал, что закрытый <details> прячет содержимое не одним только этим
   UA-правилом: реальный bounding box у .topic-jump на ≥1200px был
   {width:192, height:0} — нулевой высоты, некликабельный, — и высота
   появлялась только после `element.open = true` через JS. Похоже на
   внутреннее подавление рендера (что-то вроде content-visibility) поверх
   обычного display:none, которое `display: block` на потомке не отменяет,
   потому что борется не с тем свойством. Чинить это версией браузера или
   ещё одним CSS-свойством ненадёжно — вместо этого на широком экране
   <details> вообще не используется:
     - .topic-jump__details — тот же свёрнутый <details> со видимым
       <summary>, что и на узком экране, — но виден только <1280px.
     - .topic-jump__pinned — рельс: та же разметка ссылок (partials/
       topic-jump-items.twig, чтобы не дублировать её), но плоским <ol> без
       <details> и без состояния open/closed вообще: смотреть тут нечему,
       значит нечему и не отрендериться. Виден только на >=1280px. Внутри
       него ДВА блока (правка владельца после живого прогона первой версии):
       .topic-jump__list--pinned — список тем, свёрнутый до текущей; ниже,
       за разделителем, .topic-jump__toc-block — оглавление текущей темы.
   Оба — соседи <main class="topic"> (см. topic.twig), не его потомки; на
   любой ширине ровно один из двух виден (`display: none` у второго — это
   заодно и корректный способ убрать его из accessibility-дерева, так что
   двух объявлений одного и того же <nav aria-label> для скринридера не
   возникает).

   ПОЗИЦИОНИРОВАНИЕ РЕЛЬСА — position: fixed, а не float + sticky (правка
   владельца сайта после живого прогона первой версии: «меню не должно
   двигаться, оно должно висеть на одном месте сверху слева»). Разница не
   косметическая: sticky-элемент едет вместе со страницей от загрузки и до
   того момента, когда его верх упрётся в заданный top, — вот эта начальная
   поездка и была тем, на что владелец возражал. fixed не едет никогда.

   Горизонталь считается ТЕМ ЖЕ выражением, что и раньше, — и это не
   совпадение, а арифметика. Было: float + margin-left: calc((100% -
   var(--measure)) / 2 - 12rem - var(--step-4)), где 100% = ширина
   content-box у body, то есть (viewport - скроллбар) - 2 × var(--step-3), а
   сам элемент начинался от левого паддинга body (28px). Стало: left с тем же
   calc, но 100% у fixed-элемента считается от initial containing block, то
   есть от всего viewport без скроллбара. Паддинг body в старой формуле
   сокращался: 28 + (X - 56 - 652)/2 = X/2 - 326 — ровно то же, что
   (X - 652)/2 при X = вьюпорт. Итог обеих формул идентичен до пикселя: на
   живой странице фактический rail.left и пересчитанная тут же по геометрии
   body старая формула дали 138.00 и 138.00 на 1440px; зазор до колонки .topic
   вышел ровно 64px (var(--step-4)) и на 1440px, и на 1280px.

   Почему 100%, а не 100vw: 100vw включает ширину классического скроллбара, а
   старая формула — нет; на 100vw рельс уехал бы вправо на полскроллбара
   относительно прежнего положения. 100% у fixed-элемента резолвится по ICB,
   который скроллбар исключает, — то есть ведёт себя как прежний 100% у body.

   top: var(--step-3) — рельс висит у верхней кромки окна постоянно. На
   нулевом скролле он оказывается на одной высоте с .masthead, но не
   пересекается с ней: шапка сама ограничена max-width: var(--measure) и
   центрирована, то есть в левый гуттер не заходит вовсе (на 1280px правый
   край рельса — ~242px, левый край шапки — ~306px, зазор ровно var(--step-4)).

   Брейкпоинт — не круглое число «на глаз», а прямой расчёт под фактическую
   ширину меню и колонки, и он НЕ меняется вместе со сменой способа
   позиционирования (выражение-то то же):
     гуттер слева от .topic = (contentBox - var(--measure)) / 2,
     где contentBox = 100vw - 2 × var(--step-3) (горизонтальный паддинг body);
     меню — 12rem (192px) плюс зазор var(--step-4) (64px) до колонки = 256px.
   Решаем (100vw - 56px - 652px) / 2 >= 256px ⇒ 100vw >= 1220px. Порог ниже
   взят с запасом ~60px (скругления, скроллбар) — 1280px, а не первое
   попавшееся круглое число. Оба состояния (--details видим/--pinned скрыт
   и наоборот) переключаются одной и той же границей min-width: 1280px, так
   что зазора или наложения ровно на 1279–1280px не возникает.

   Вертикальный бюджет рельса: max-height: calc(100vh - 2 * var(--step-3)) при
   top: var(--step-3) даёт равные поля сверху и снизу. Раскрытый по наведению
   Блок 1 (10 тем) плюс Блок 2 (до 9 пунктов) на ноутбучные 800px не влезают —
   поэтому рельс сделан flex-колонкой, а оба блока получили min-height: 0 и
   собственный overflow-y: auto: при нехватке места они ужимаются
   пропорционально и каждый скроллится сам, вместо того чтобы один вытеснил
   другой за нижнюю кромку экрана без всякого признака прокрутки. В обычном
   (свёрнутом) состоянии ничего не ужимается и скроллбаров нет. */
 .topic-jump { margin: var(--step-3) 0; }
 .topic-jump__pinned { display: none; }
.topic-jump__details {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  background: var(--paper-sunk);
  padding: 0 var(--step-3);
}
.topic-jump__details[open] { padding-bottom: var(--step-2); }
.topic-jump__summary {
  margin: 0 calc(-1 * var(--step-3));
  padding: var(--step-2) var(--step-3);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.15rem;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}
.topic-jump__summary::-webkit-details-marker { display: none; }
.topic-jump__summary::before { content: "\25B8\A0"; color: var(--accent); }
.topic-jump__details[open] .topic-jump__summary::before { content: "\25BE\A0"; }
.topic-jump__list { list-style: none; margin: 0; padding: 0; }
.topic-jump__item + .topic-jump__item { margin-top: var(--step-1); }
/* Не только цвет отличает текущую тему (spec §7.2): жирное начертание и
   левая грань — форма и вес, а не только оттенок акцента, тем более что
   акцентный цвет темы и так уже виден по всей странице (заголовок,
   маркеры). aria-current="page" в разметке — тот же сигнал для
   вспомогательных технологий. */
.topic-jump__link {
  display: block;
  padding: var(--step-1) 0 var(--step-1) var(--step-2);
  border-left: 3px solid transparent;
  text-decoration: none;
  color: var(--muted);
  font-size: 1.1rem;
}
.topic-jump__link:hover { color: var(--ink); }
.topic-jump__link.is-current {
  color: var(--ink);
  font-weight: 700;
  border-left-color: var(--accent);
}
/* Номер темы — тем же mono-начертанием, что у других «штампов» страницы
   (.topic__stamp, .card__number, .masthead__stamp): цвет и вес нарочно не
   задаются здесь отдельно, а наследуются от .topic-jump__link, чтобы номер
   переключался в тон вместе со ссылкой на hover/is-current, а не жил своим
   отдельным, рассинхронизированным цветом. */
.topic-jump__number { font-family: var(--font-mono); }
/* Второй уровень меню — оглавление ТЕКУЩЕЙ темы (Блок 2 рельса,
   partials/topic-jump.twig). Отдельный блок ПОД списком тем, а не вложенный
   <ol> внутри её <li>: так его видно всегда, независимо от того, свёрнут
   сейчас список тем или раскрыт наведением.

   Разметка печатается только для текущей темы. В первой версии оглавления
   рисовались сразу у всех десяти (~17 КБ на страницу), потому что наведение
   на чужой пункт показывало его оглавление; теперь чужие оглавления не
   показываются никогда, и печатать их незачем. Ленивой подгрузки тут не было
   и быть не может: app.js не имеет права ходить в сеть
   (tests/Assets/ScriptContractTest::testNothingIsSentAnywhere). */
.topic-jump__toc-block {
  margin-top: var(--step-2);
  padding-top: var(--step-2);
  border-top: 1px solid var(--rule);
}
/* Зрительный разделитель между блоками — это грань выше плюс собственный
   «штамп» блока тем же mono-начертанием, что у .masthead__stamp и
   .topic-nav__label: читатель должен видеть, что ниже — не продолжение списка
   тем, а содержание одной из них. */
.topic-jump__toc-title {
  margin: 0 0 var(--step-1);
  font-family: var(--font-mono);
  font-size: 0.92rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.topic-jump__toc { list-style: none; margin: 0; padding: 0; }
/* border-left: 3px transparent + тот же padding, что у .topic-jump__link —
   пункты оглавления встают ровно под пунктами тем, а не с произвольным
   отступом, и остаётся готовый крючок под будущую подсветку секции. */
.topic-jump__toc-link {
  display: block;
  padding: var(--step-1) 0 var(--step-1) var(--step-2);
  border-left: 3px solid transparent;
  color: var(--muted);
  text-decoration: none;
  font-family: var(--font-display);
  font-size: 1rem;
  line-height: 1.25;
  overflow-wrap: anywhere;
}
.topic-jump__toc-link:hover { color: var(--ink); }

/* Фокус виден явно: раскрытие списка тем держится на :focus-within, то есть
   на клавиатурном обходе, и без видимой обводки было бы непонятно, какой
   пункт сейчас в фокусе. Обводка — та же идиома, что у остальных
   интерактивных элементов страницы (details.howto, choice-tabs,
   theme-toggle). */
.topic-jump__link:focus-visible,
.topic-jump__toc-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Свёртка Блока 1 до текущей темы и раскрытие всего списка по наведению.

   Базовое состояние — ПОЛНЫЙ список: если ни один пункт не текущий (страница
   вводной главки — cards() исключает kind=intro), :has() не срабатывает и
   видны все темы. Свёртка — надстройка над этим состоянием, а не наоборот.

   display:none, а не visibility/height: свёрнутые пункты должны уходить и из
   дерева доступности тоже. Раскрытие — display: list-item (родной display
   для <li>; маркеры всё равно сняты list-style: none у .topic-jump__list).

   :hover и :focus-within висят на самом БЛОКЕ, а не на отдельном пункте: по
   запросу владельца сайта полный список возвращается при наведении на
   свёрнутый блок целиком. :focus-within здесь — не «на всякий случай», а
   единственный клавиатурный вход, и работает он так: пункт текущей темы виден
   всегда, значит фокусируем всегда; Tab доводит до него → :focus-within на
   блоке → появляются остальные девять → дальше Tab идёт по темам ниже
   текущей, Shift+Tab — по темам выше, и всё это время фокус остаётся внутри
   блока, то есть список остаётся раскрытым. Именно поэтому на ссылках тем
   НЕТ tabindex="-1" (он был в первой версии): он бы отключил единственную
   дверь и сделал девять тем недостижимыми с клавиатуры вовсе.

   Раскрытие не «убегает» из-под курсора: блок прикреплён к верху рельса и
   растёт только вниз, пункты выше текущей вставляются внутрь уже наведённой
   области, а не над ней.

   Медиазапросом правила НЕ обёрнуты: класс --pinned есть только в рельсе, а
   рельс ниже 1280px скрыт целиком (.topic-jump__pinned { display: none }).
   Селектор по классу физически не может дотянуться до узкого <details>. */
.topic-jump__list--pinned:has(.is-current) .topic-jump__item:not(.is-current) { display: none; }
.topic-jump__list--pinned:has(.is-current):hover .topic-jump__item:not(.is-current),
.topic-jump__list--pinned:has(.is-current):focus-within .topic-jump__item:not(.is-current) { display: list-item; }
 @media (min-width: 1280px) {
   .topic-jump__details { display: none; }
   .topic-jump__pinned {
     display: flex;
     flex-direction: column;
     position: fixed;
     top: var(--step-3);
     left: calc((100% - var(--measure)) / 2 - 12rem - var(--step-4));
     width: 12rem;
     max-height: calc(100vh - 2 * var(--step-3));
   }
   /* min-height: 0 снимает автоматический минимум flex-элемента (иначе он не
      ужимается ниже содержимого и вылезает за max-height рельса), overflow-y
      даёт каждому блоку собственную прокрутку. padding-bottom у списка тем
      заодно расширяет площадь наведения свёрнутого (в один пункт) блока до
      разделителя. */
   .topic-jump__list--pinned {
     flex: 0 1 auto;
     min-height: 0;
     overflow-y: auto;
     padding-bottom: var(--step-2);
   }
   .topic-jump__toc-block { flex: 0 1 auto; min-height: 0; overflow-y: auto; }
}

.topic__header { margin: var(--step-4) 0 var(--step-3); }
.topic__title { font-size: clamp(2rem, 7vw, 3.4rem); color: var(--accent); }
.topic__summary { font-size: 1.15rem; color: var(--muted); }
.topic__body section.level2 { margin-top: var(--step-4); }
.topic__body h2 { font-size: 1.6rem; }
.topic__body h2::before { content: ""; display: block; width: 48px; border-top: 3px solid var(--accent); margin-bottom: var(--step-2); }

/* Выносные цитаты на полях (spec §9.4). В источнике это обычные цитатные блоки
   Markdown, поэтому выделение доезжает и до книг без своего механизма. */
.topic__body blockquote {
  margin: var(--step-3) 0;
  padding-left: var(--step-2);
  border-left: 4px solid var(--accent);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.3rem;
  line-height: 1.25;
  color: var(--ink);
}
@media (min-width: 1100px) {
  .topic__body blockquote {
    float: right;
    width: 16rem;
    margin: var(--step-2) calc(-1 * var(--step-4)) var(--step-2) var(--step-3);
  }
}

/* Группа-переключатель «Базовый минимум» / «Роскошный максимум» — та же
   структура, что у .theme-toggle__group: рамка на контейнере, кнопки внутри
   без собственной рамки. Разметку строит setupMaxToggle() (app.js). */
.max-toggle {
  display: inline-flex;
  border: 1px solid var(--accent);
  border-radius: 2px;
  overflow: hidden;
}
.max-toggle__btn {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: none;
  border: none;
  color: var(--muted);
  padding: var(--step-1) var(--step-2);
  cursor: pointer;
}
.max-toggle__btn + .max-toggle__btn { border-left: 1px solid var(--accent); }
.max-toggle__btn.is-active { background: var(--accent); color: var(--paper); }
.max-toggle__hint { font-family: var(--font-text); font-size: 0.88rem; color: var(--muted); margin-top: var(--step-1); }

/* Блок «Сделай это прямо сейчас» — самый заметный элемент первого экрана. */
.topic__body .now {
  background: var(--paper-sunk);
  border: 2px solid var(--accent);
  padding: var(--step-2) var(--step-3);
  margin: var(--step-3) 0;
}
.topic__body .now > p:first-child {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.25rem;
  color: var(--accent);
}
.topic__body ul.task-list { list-style: none; padding-left: 0; }
.topic__body ul.task-list li { margin-bottom: var(--step-2); }
.topic__body ul.task-list input[type="checkbox"] { margin-right: var(--step-1); accent-color: var(--accent); }
/* Статус-иконка вместо чекбокса — для пунктов, у которых есть свой раздел
   «карта + разделы» ниже по странице (setupNowStatusIcon() в app.js): сама
   отметка стоит в конце раздела, а здесь только читается её состояние.
   Ширина фиксирована, чтобы ⏳ и ✅ не сдвигали текст пункта при смене
   состояния; свой цвет иконкам не нужен — у эмодзи он уже есть. */
.topic__body ul.task-list .now__status { display: inline-block; width: 1.2em; margin-right: var(--step-1); text-align: center; }
/* Отмеченный пункт карты приглушается — тем же приёмом, что «сделай сейчас»
   вообще ничего не красит красным/зелёным (spec §7.2): не оценка, а просто
   «этот шаг позади». */
.topic__body ul.task-list li.now__item--done { color: var(--muted); }

/* Кнопка «Отметить как сделанное» внутри раздела «карта + разделы»
   (buildStopButton() в app.js) — та же кнопка, что у self-check-проверок,
   класс .check__submit не дублируем нигде ниже. Раздел живёт внутри
   .topic__body, поэтому и отступы даём здесь же, тем же шагом, что у .max
   и .check. */
.topic__body .now-done { margin: var(--step-3) 0; }

/* Тихая служебная строка внутри «Сделай это сейчас»: приглашение указать
   профиль, либо кнопка «показать то, чего нет в профиле». Не второй призыв к
   действию, а обслуживающая реплика — поэтому мельче и тише самого блока, тем
   же языком, что и .check-note. Разметку строит applyProfile() (app.js). */
.now__meta { font-family: var(--font-mono); font-size: 0.78rem; color: var(--muted); margin: var(--step-2) 0 0; }
.now__meta a { color: var(--muted); }
.now__meta-btn {
  font: inherit;
  background: none;
  border: none;
  padding: 0;
  color: var(--muted);
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-underline-offset: 3px;
  cursor: pointer;
}
.now__meta-btn:hover { text-decoration-color: var(--accent); }

/* «Максимум» всегда в DOM; переключатель управляет только видимостью. */
.topic__body .max, .start-onboarding__example .max {
  border-left: 3px solid var(--accent);
  background: var(--paper-sunk);
  padding: var(--step-2) var(--step-3);
  margin: var(--step-3) 0;
}
.topic__body .max > p:first-child, .start-onboarding__example .max > p:first-child {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent);
}

/* «Получилось —» — критерий успеха шага, визуально выделен: акцентная
   левая грань как у .max, но с собственной галочкой-маркером, чтобы не
   путать с «доп. подробности» — это подтверждение, а не углубление. */
.topic__body .stop__result {
  background: var(--paper-sunk);
  border-left: 3px solid var(--accent);
  padding: var(--step-1) var(--step-2);
  border-radius: 2px;
  margin: var(--step-2) 0;
}
.topic__body .stop__result::before {
  content: "\2713\A0";
  color: var(--accent);
  font-weight: 700;
}

/* Список ссылок на расширение для браузера — то же обрамление, что у .max:
   акцентная грань слева и утопленный фон. setupBrowserExtensionLinks()
   (app.js) подсвечивает подходящую ссылку и подпись рядом с ней, а при
   удачной догадке прячет остальные пункты (hidden, не CSS) и добавляет
   кнопку .browser-extension-links__toggle, которая их возвращает. Все
   шесть пунктов всегда есть в разметке — без JS список остаётся
   полностью кликабельным. */
.topic__body .browser-extension-links {
  border-left: 3px solid var(--accent);
  background: var(--paper-sunk);
  padding: var(--step-2) var(--step-3);
  margin: var(--step-3) 0;
}
.topic__body .browser-extension-links ul { padding-left: var(--step-3); }
.topic__body .browser-extension-links li { margin-bottom: var(--step-1); }
.topic__body .browser-extension-links a.is-detected {
  font-weight: 700;
  text-decoration-color: var(--accent);
}
.browser-extension-links__badge {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--accent);
}
.browser-extension-links__toggle {
  display: block;
  margin-top: var(--step-1);
  padding: 0;
  font: inherit;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: none;
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-underline-offset: 3px;
  cursor: pointer;
}
.browser-extension-links__toggle:hover { text-decoration-color: var(--accent); }

/* --- Плашки «как это сделать» -------------------------------------------- */
/* Пошаговая инструкция для конкретного сервиса (::: {.howto summary="…"} в
   источнике, <details> на выходе). Свёрнута по умолчанию: примеров под одним
   действием бывает несколько, и развёрнутыми они заслонили бы саму тему.
   Прячет их браузер собственным поведением <details>, а не наш JS, поэтому
   префикс html.js здесь не нужен: со скриптами и без них плашка одинаково
   раскрывается по клику, а содержимое всё время лежит в DOM
   (spec §7.5). Рамка повторяет «максимум» — акцентная грань слева и
   утопленный фон, — так что оба блока читаются одной семьёй.
   Своих цветов и радиусов у плашки нет ни одного: всё собрано из токенов,
   и каждая из четырёх тем перекрашивает её вместе с остальным телом темы. */
.topic__body details.howto {
  margin: var(--step-2) 0;
  padding: 0 var(--step-3);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  background: var(--paper-sunk);
}
.topic__body details.howto[open] { padding-bottom: var(--step-2); }
/* Отрицательное поле растягивает кликабельную строку на всю ширину плашки:
   горизонтальные отступы содержимому даёт сам <details>, и без этого по краям
   заголовка оставались бы мёртвые зоны.

   Подпись набрана чернилами, а не акцентом, — в отличие от меток «максимума» и
   «сделай сейчас». Те стоят над текстом, который и так читается чернилами, а
   здесь у свёрнутой плашки подпись — вообще единственное видимое слово, да ещё
   и орган управления. На светлой «искре» акцент (фуксия) к утопленному фону
   даёт всего 2.9:1, чернила — 12.9:1; акцент остаётся на грани и треугольнике,
   где от него не зависит читаемость. */
.topic__body details.howto > summary {
  margin: 0 calc(-1 * var(--step-3));
  padding: var(--step-2) var(--step-3);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}
/* Свой треугольник вместо встроенного: направление у него честное (закрыто —
   вбок, открыто — вниз), а размер и цвет наши. Мелкие «текстовые» треугольники
   U+25B8/U+25BE выбраны намеренно — у них нет эмодзи-начертания, которое в
   части систем подменило бы фирменный цвет плашки. */
.topic__body details.howto > summary::-webkit-details-marker { display: none; }
.topic__body details.howto > summary::before { content: "\25B8\A0"; color: var(--accent); }
.topic__body details.howto[open] > summary::before { content: "\25BE\A0"; }
.topic__body details.howto > summary:hover {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 3px;
}
.topic__body details.howto > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.topic__body details.howto ol,
.topic__body details.howto ul { padding-left: var(--step-3); }
.topic__body details.howto li { margin-bottom: var(--step-1); }
/* Нижний отступ последнего элемента снимаем: вертикальный воздух под ним уже
   отмерен паддингом раскрытой плашки. */
.topic__body details.howto > :last-child { margin-bottom: 0; }

/* Video guides are deliberately visible as a labelled, closed disclosure. The
   iframe stays out of the initial layout and keeps a stable 16:9 shape. */
.topic__body details.video-guide {
  margin: var(--step-3) 0;
  border: 1px solid var(--rule);
  border-left: 4px solid var(--accent);
  background: var(--paper-sunk);
  padding: 0 var(--step-3);
}
.topic__body details.video-guide[open] { padding-bottom: var(--step-2); }
.topic__body details.video-guide > summary {
  padding: var(--step-2) 0;
  font-family: var(--font-display);
  font-weight: 700;
  cursor: pointer;
}
.topic__body details.video-guide > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.video-guide__icon { display: inline-block; margin-left: 0.25em; font-size: 0.85em; line-height: 1; }
.video-guide__frame { aspect-ratio: 16 / 9; margin-bottom: var(--step-2); }
.video-guide__frame iframe { width: 100%; height: 100%; border: 0; }
.video-guide__sound-note { margin: var(--step-1) 0 0; color: var(--muted); font-family: var(--font-mono); font-size: 0.78rem; }
.video-guide__sound-button { margin-right: var(--step-1); padding: var(--step-1) var(--step-2); border: 1px solid var(--accent); background: var(--paper); color: var(--ink); font: inherit; cursor: pointer; }
.video-guide__sound-button:hover { background: var(--paper-sunk); }
.video-guide__sound-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.topic__body details.video-guide > ul,
.topic__body details.video-guide > ol { margin: 0; padding-left: var(--step-3); }
.topic__body details.video-guide > ul li,
.topic__body details.video-guide > ol li { margin-bottom: var(--step-1); overflow-wrap: anywhere; }
.topic__body details.video-guide > ul a:focus-visible,
.topic__body details.video-guide > ol a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.topic__body details.video-guide .video-guide__chapter { text-decoration-thickness: 1px; }
.topic__body details.video-guide > p { margin-bottom: 0; }
@media (max-width: 699px) {
  .topic__body details.video-guide { padding-left: var(--step-2); padding-right: var(--step-2); }
  .topic__body details.video-guide > ul,
  .topic__body details.video-guide > ol { padding-left: var(--step-2); }
}

/* Choice tabs are progressively enhanced: before JS both panels are ordinary
   readable sections, while the ready widget exposes one panel at a time. */
.topic__body .choice-tabs { margin: var(--step-3) 0; }
.choice-tabs__list { display: flex; flex-wrap: wrap; gap: var(--step-1); margin-bottom: var(--step-2); border-bottom: 1px solid var(--rule); }
.choice-tabs__tab { flex: 1 1 16rem; min-width: 12rem; padding: var(--step-2); border: 1px solid var(--rule); border-bottom: 3px solid transparent; background: var(--paper-sunk); color: var(--ink); font: 500 0.95rem/1.2 var(--font-display); cursor: pointer; text-align: left; }
.choice-tabs__tab:hover { border-bottom-color: var(--accent); }
.choice-tabs__tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.choice-tabs__tab.is-active { border-bottom-color: var(--accent); background: var(--paper); }
.choice-tabs__panel { border-left: 3px solid var(--accent); padding: 0 var(--step-3); }
.choice-tabs__panel > :first-child { margin-top: 0; }
html.js .choice-tabs.is-ready .choice-tabs__panel:not(.is-active) { display: none; }

/* Иллюстрации остаются частью статьи без JS, но никогда не расширяют её
   колонку. Рамка и утопленный фон продолжают визуальный язык инструкций. */
.topic__body figure {
  max-width: 100%;
  margin: var(--step-3) 0;
  padding: var(--step-2);
  border: 1px solid var(--rule);
  background: var(--paper-sunk);
}
.topic__body img {
  display: block;
  max-width: 100%;
  height: auto;
}
.topic__body img[role="button"] { cursor: zoom-in; }
.topic__body img[role="button"]:hover { filter: brightness(1.04); }
.topic__body img[role="button"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.image-lightbox {
  width: min(92vw, 90rem);
  max-width: calc(100vw - 2 * var(--step-3));
  max-height: calc(100vh - 2 * var(--step-3));
  padding: var(--step-3);
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
}
.image-lightbox::backdrop { background: rgb(0 0 0 / 78%); }
.image-lightbox__image {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - 7rem);
  width: auto;
  height: auto;
  object-fit: contain;
  margin: 0 auto;
}
.image-lightbox__close {
  display: block;
  margin: 0 0 var(--step-2) auto;
  padding: var(--step-1) var(--step-2);
  border: 1px solid var(--rule);
  background: var(--paper-sunk);
  color: var(--ink);
  font: 1.5rem/1 var(--font-display);
  cursor: pointer;
}
.image-lightbox__close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .topic__body img[role="button"] { transition: none; }
}

/* --- Проверки ------------------------------------------------------------ */
.topic__body .check {
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: var(--step-2) 0;
  margin: var(--step-3) 0;
}
.check__form { display: grid; gap: var(--step-1); margin-bottom: var(--step-2); }
.check__option { display: flex; gap: var(--step-1); align-items: baseline; }
.check__input { font: inherit; padding: var(--step-1); border: 1px solid var(--rule); background: var(--paper); color: var(--ink); }
.check__submit {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  background: var(--accent);
  color: var(--paper);
  border: none;
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  cursor: pointer;
  justify-self: start;
}
/* Неверный ответ не блокирует и не красится красным: без счёта попыток, без
   «неправильно» (spec §7.2). */
.check__verdict { font-family: var(--font-mono); font-size: 0.82rem; color: var(--muted); }
.topic__body .check-note { color: var(--muted); font-style: italic; }

/* --- Места, где что-то прячется, — и все под html.js ---------------------
   Без JS страница остаётся полностью раскрытой: «максимум», ответ проверки и
   контент, сужаемый по профилю читателя, читаются как в книге (spec §7.5).
   Профиль сам по себе — виджет, который строит только JS: без скриптов его
   некому заполнить, поэтому без html.js правилам ниже просто нечего делать.
   А без has-profile на <html> (профиль ещё ни разу не отвечен) сужать тоже
   нечего: решать за читателя раньше, чем он сам нажал хоть одну кнопку, —
   не наше дело. .profile-show-all на <body> — разовое «покажи всё», не
   персистентное, тот же принцип, что и у .max-open. */
html.js body:not(.max-open) .max { display: none; }
html.js .check:not(.is-answered) .check-note { display: none; }
html.js.has-profile body:not(.profile-show-all) [data-tool]:not(.is-mine) { display: none; }
html.js.has-profile body:not(.profile-show-all) .now li:has([data-tool]):not(.is-mine) { display: none; }

/* --- Навигация и подвал -------------------------------------------------- */
.topic-nav { margin: var(--step-4) 0 0; border-top: 2px solid var(--ink); padding-top: var(--step-2); }
.topic-nav__next { display: block; text-decoration: none; }
.topic-nav__label { display: block; font-family: var(--font-mono); font-size: 0.78rem; text-transform: uppercase; color: var(--muted); }
.topic-nav__title { font-family: var(--font-display); font-weight: 700; font-size: 1.6rem; color: var(--accent); }

.footer {
  max-width: var(--measure);
  margin: var(--step-4) auto 0;
  padding-top: var(--step-2);
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-size: 0.9rem;
}
.footer__downloads { display: flex; gap: var(--step-2); font-family: var(--font-mono); font-size: 0.78rem; }
.footer__about { display: flex; flex-wrap: wrap; gap: var(--step-1) var(--step-3); margin-bottom: var(--step-2); align-items: baseline; }
.footer__label { flex-basis: 100%; font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink); }
.info-page { padding-top: var(--step-4); }
.info-page__eyebrow { margin: 0 0 var(--step-1); font-family: var(--font-mono); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.info-page__title { margin: 0; color: var(--accent); font-size: clamp(2rem, 7vw, 3.4rem); }
.info-page__lead { max-width: 42rem; margin: var(--step-2) 0 var(--step-4); font-size: 1.15rem; color: var(--muted); }
.principles-list { margin: 0; padding: 0; list-style: none; border-top: 2px solid var(--ink); }
.principles-list li { padding: var(--step-2) 0; border-bottom: 1px solid var(--rule); }
.principles-list strong { color: var(--ink); }
.bulletin-tree { margin: 0; padding: 0; list-style: none; border-top: 2px solid var(--ink); counter-reset: topic; }
.bulletin-tree__topic { counter-increment: topic; position: relative; padding: var(--step-3) 0 var(--step-4) 3.2rem; border-bottom: 1px solid var(--rule); }
.bulletin-tree__topic::before { content: counter(topic, decimal-leading-zero); position: absolute; left: 0; top: var(--step-3); font-family: var(--font-mono); color: var(--muted); }
.bulletin-tree__title { margin: 0 0 var(--step-1); font-size: 1.45rem; }
.bulletin-tree__title a { color: var(--accent); }
.bulletin-tree__nodes { margin: 0 0 var(--step-2); padding-left: 1.2rem; }
.bulletin-tree__nodes li, .bulletin-tree__format li { margin-bottom: 0.45rem; }
.bulletin-tree__label { display: block; color: var(--muted); font-family: var(--font-mono); font-size: 0.78rem; }
.bulletin-tree__summary { display: block; }
.bulletin-tree__format, .bulletin-tree__videos { margin: var(--step-1) 0 0; font-size: 0.92rem; }
.bulletin-tree__format > span, .bulletin-tree__videos > span, .bulletin-tree__label { font-family: var(--font-mono); font-size: 0.78rem; color: var(--muted); }
.bulletin-tree__videos ul { margin: 0.35rem 0 0; padding-left: 1.2rem; }
.bulletin-tree__videos li { margin-bottom: 0.45rem; }
.bulletin-tree__videos a { overflow-wrap: anywhere; }
@media (max-width: 600px) {
  .info-page { padding-top: var(--step-3); }
  .bulletin-tree__topic { padding-left: 2.4rem; }
  .bulletin-tree__topic::before { font-size: 0.75rem; }
  .footer__about { align-items: flex-start; flex-direction: column; gap: var(--step-1); }
}

/* --- Страница ошибки ----------------------------------------------------- */
.error { margin-top: var(--step-4); }
.error__status { font-family: var(--font-mono); font-size: 3rem; color: var(--muted); }

/* --- Темы: html[data-theme] ------------------------------------------------
   Переключатель оформления (виджет в шапке — masthead.twig,
   setupThemeToggle() в app.js) сохраняет выбор в localStorage под ключом
   'theme' тем же четырём значениям, что читает инлайн-скрипт в layout.twig
   до первой отрисовки. Без сохранённого значения атрибут не выставляется —
   всё работает ровно как раньше: тёплая светлая база + системная тёмная через
   prefers-color-scheme (ничего выше не тронуто).

   Девятитоновая акцентная система на тему (spec §9.3) сохранена в каждой:
   переопределяются только светлота/насыщенность в формуле
   oklch(L% C var(--accent-h)) — сам тон (--accent-h) по-прежнему приходит
   инлайн-стилем с сервера (topic.twig, topic-card.twig) и остаётся одним из
   девяти фиксированных чисел. Тёмные темы поднимают светлоту относительно
   светлой формулы тем же приёмом, что и штатный prefers-color-scheme: dark
   выше (58%→72%, 0.17→0.15); светлые темы держат светлоту у базового
   светлого уровня, варьируя только насыщенность под настроение темы. */

html[data-theme="terminal"] {
  /* Терминал: тёмный «хакерский» терминал, несерьёзная бравада */
  --paper: #0b0f0c;
  --paper-sunk: #121810;
  --ink: #d6ffd0;
  --muted: #6ea87a;
  --rule: #234029;
  --font-display: var(--font-mono);
  --font-text: var(--font-mono);
  --accent-2: #1fae4a;
  /* Тёмная жёлтая/зелёная заливка меток annotate — тот же приём и та же
     причина, что в @media (prefers-color-scheme: dark) выше: светлый --ink
     этой темы не читается на бледной заливке annotate.css по умолчанию. */
  --annotate-mark-bg: #4a3c12;
  --annotate-mark-done-bg: #1f3d28;
  /* Акцент режима редактуры «принадлежит» теме (правка владельца сайта
     2026-09-15: раньше везде был один и тот же чужеродный флэт-красный —
     см. развёрнутый комментарий у общего :root { --annotate-bg: ... } ниже,
     где это решение объясняется и теперь пересматривается). И --accent, и
     --accent-2 терминала — зелёные: переиспользовать любой из них означало
     бы, что бейдж/меню/панель тонут в собственном зелёном свечении темы, а
     не читаются отдельным инструментом. Янтарь — второй канонический цвет
     хакерского терминала (классические amber-мониторы, предупреждение/алерт
     в консоли рядом с обычным зелёным выводом) — тон новый для этой темы, но
     узнаваемо «свой»: 10.8:1 к --paper и 10.8:1 к --annotate-accent-fg. */
  --annotate-accent: #ffb300;
  --annotate-accent-fg: var(--paper);
}
html[data-theme="terminal"] body[data-topic],
html[data-theme="terminal"] .card:not(.card--bonus) .card__link {
  /* светлее и насыщеннее штатной тёмной формулы (72%/0.15) — неоновый
     оттенок калиброван под фирменный accent ≈ #39ff6a этой темы */
  --accent: #39ff6a;
}
/* Едва заметные скан-линии поверх фона — декоративный штрих, не мешает
   чтению: непрозрачность подобрана заведомо низкой. */
html[data-theme="terminal"] body {
  background-color: var(--paper);
  background-image: repeating-linear-gradient(
    180deg,
    rgb(57 255 106 / 4%) 0px,
    rgb(57 255 106 / 4%) 1px,
    transparent 1px,
    transparent 3px
  );
}
/* Неоновое свечение заголовков — фирменная примета «хакерского» терминала.
   Усилено до мокапного значения и распространено на номер и заголовок
   карточки: там тоже акцентный текст, ему та же личность идёт. */
html[data-theme="terminal"] h1,
html[data-theme="terminal"] h2,
html[data-theme="terminal"] h3,
html[data-theme="terminal"] h4,
html[data-theme="terminal"] .card__title,
html[data-theme="terminal"] .card__number {
  text-shadow: 0 0 10px rgba(57, 255, 106, .55), 0 0 2px rgba(57, 255, 106, .7);
}
/* Карточка: верхняя линейка тоньше (2px вместо базовых 4px) и едва заметное
   скругление (2px) — мокап держит терминал почти острым, не буквально
   острым. Кнопки уже наследуют 2px из базовых правил — трогать не нужно. */
html[data-theme="terminal"] .card__link {
  border-radius: 2px;
  border-top-width: 2px;
}

html[data-theme="graphite"] {
  /* Графит: сталь и сдержанность, без декоративного свечения */
  --paper: #14171c;
  --paper-sunk: #1c2028;
  --ink: #e8ebf0;
  --muted: #838c9c;
  --rule: #2a3038;
  --font-text: var(--font-display); /* --font-display уже Rubik в :root */
  --accent-2: #ffa94d;
  /* Та же тёмная заливка меток annotate, что у terminal и у системной тёмной
     темы — светлый --ink здесь тоже не читается на бледной заливке-дефолте. */
  --annotate-mark-bg: #4a3c12;
  --annotate-mark-done-bg: #1f3d28;
  /* Акцент режима редактуры — переиспользуем уже объявленный выше --accent-2
     (оранжевый) вместо нового цвета: он у графита и так существует ровно для
     роли «второй, неконтентный акцент», сейчас почти нигде не занят (только
     пункт шкалы «усилие»), и тёплый оранжевый на стальном холодном фоне даёт
     тот же уверенный контраст, что и голубой --accent контента, но другой
     тон — бейдж не путается с обычными синими ссылками/заголовками.
     9.4:1 к --paper, 9.4:1 текста к заливке. */
  --annotate-accent: var(--accent-2);
  --annotate-accent-fg: var(--paper);
}
html[data-theme="graphite"] body[data-topic],
html[data-theme="graphite"] .card:not(.card--bonus) .card__link {
  /* близко к штатной тёмной формуле (72%/0.15), насыщенность чуть ниже —
     плоский, уверенный тон вместо неонового */
  --accent: #4d8fff;
}
/* Личность темы — в отсутствии украшений: никакого свечения, никакой тени.
   Один жест — линия тоньше базовых 4px, плоская и точная вместо
   акцентно-жирной; скругление чуть заметнее сайтового 2px-по-умолчанию —
   мокап держит графит на 4px. */
html[data-theme="graphite"] .card__link {
  border-top-width: 1px;
  border-radius: 4px;
}
/* Кнопки графита скруглены сильнее прочих (мокап: --radius-btn: 6px). Для
   max-toggle это теперь радиус группы-контейнера — сами кнопки внутри
   остаются прямоугольными, скругляются только внешние углы группы. */
html[data-theme="graphite"] .check__submit,
html[data-theme="graphite"] .max-toggle {
  border-radius: 6px;
}
/* Мокапный графитовый CTA — уверенная заливка акцентом — был рассчитан на
   одну всегда-активную кнопку. У группы из двух состояний ту же роль теперь
   играет активный сегмент: выбранный вариант красится залитым CTA, второй
   остаётся тихим контурным сегментом группы. .reset так и остаётся
   контурным отдельно от этой логики: «сбросить всё» — не про выбор из двух
   состояний. */
html[data-theme="graphite"] .max-toggle__btn.is-active {
  background: var(--accent);
  color: var(--paper);
}
html[data-theme="graphite"] .max-toggle__btn.is-active:hover {
  filter: brightness(1.08);
}

html[data-theme="coral"] {
  /* Коралл: мягкая, пастельная, мятно-коралловая */
  --paper: #f5fbf9;
  --paper-sunk: #e8f5f0;
  --ink: #1f3d38;
  --muted: #6a9088;
  --rule: #d3e6e0;
  --font-text: var(--font-display); /* --font-display уже Rubik в :root */
  --accent-2: #e0946a;
  /* Акцент режима редактуры — тот же зелёный, что и у --accent контента
     (#3f8f7c), но заметно темнее и насыщеннее: буквальный --accent даёт
     лишь 3.69:1 к светлой мятной бумаге, а коралловый --accent-2 (левая
     грань «сделай сейчас») ещё слабее — 2.33:1. Углублённый тон той же
     зелёной семьи держит 7.02:1 — и как заливка к бумаге, и как текст
     заливки (--annotate-accent-fg), контраст здесь симметричен. */
  --annotate-accent: #2a5f52;
  --annotate-accent-fg: var(--paper);
}
html[data-theme="coral"] body[data-topic],
html[data-theme="coral"] .card:not(.card--bonus) .card__link {
  /* Свежий выбор цвета, не производная от штатной формулы (58%/0.17) —
     мягкий мятно-зелёный акцент по утверждённому владельцем макету */
  --accent: #3f8f7c;
}
/* Жирнее нельзя — 700 уже самый тяжёлый начертанный вес Rubik (см. @font-face
   вверху файла), здесь просто подтверждаем его явно для этой темы. */
html[data-theme="coral"] h1,
html[data-theme="coral"] h2,
html[data-theme="coral"] h3,
html[data-theme="coral"] h4 {
  font-weight: 700;
}
/* Самое крупное скругление среди тем (мокап: --radius-card/--radius-btn —
   10px). Верхнюю линейку карточки не трогаем: в мокапе для coral она не
   переопределена, остаётся на базовых 4px. */
html[data-theme="coral"] .card__link {
  border-radius: 10px;
}
html[data-theme="coral"] .check__submit,
html[data-theme="coral"] .max-toggle {
  border-radius: 10px;
}
/* Блок «сделай это сейчас» получает коралловую полосу слева — как в мокапе:
   не весь прямоугольник коралловый, только левый акцент, остальная рамка —
   обычная мятно-зелёная тема. */
html[data-theme="coral"] .topic__body .now {
  border-left-color: var(--accent-2);
  border-left-width: 4px;
}

html[data-theme="warmth"] {
  /* Тепло: тёплая редакционная сдержанность */
  --paper: #faf5f0;
  --paper-sunk: #f3e8de;
  --ink: #33281f;
  --muted: #8a7565;
  --rule: #e8d9c8;
  --font-display: var(--font-text); /* --font-text уже PT Serif в :root */
  --accent-2: #5c7a63;
  /* Акцент режима редактуры — переиспользуем шалфейный --accent-2 (сейчас
     занят только пунктом шкалы «усилие»): холодный зелёный тон против
     тёплого терракотового --accent контента и того же в среднем тепле
     флэт-красного, который раньше почти сливался с терракотой (репорт
     владельца «едва видно в Тепле» до этой правки). Взят на пару тонов
     темнее объявленного --accent-2 ради запаса контраста — 5.9:1 и к
     бумаге, и к тексту заливки (сам --accent-2 держит только 4.4:1, ниже
     порога читаемости жирного мелкого текста бейджа). */
  --annotate-accent: #4a6552;
  --annotate-accent-fg: var(--paper);
}
html[data-theme="warmth"] body[data-topic],
html[data-theme="warmth"] .card:not(.card--bonus) .card__link {
  /* приглушённее штатной светлой формулы (58%/0.17) — по мотивам
     accent ≈ #b5563c, сдержанность важнее эффектности */
  --accent: #b5563c;
}
/* Тонкая редакционная линейка под заголовком темы — только под главным
   заголовком страницы, не под каждым h2/h3: журнальный, не декоративный
   приём. */
html[data-theme="warmth"] .topic__title {
  padding-bottom: var(--step-2);
  border-bottom: 2px solid var(--accent-2);
  margin-bottom: var(--step-3);
}
/* Самая тёплая тема — самая тактильная: скругление 8px и рамка вдвое толще
   базовой (--border-w: 2px против сайтового --rule в 1px); border-width
   разом бьёт и верхнюю линейку (нужна ровно 2px), и боковые/нижнюю грань —
   цвета остаются прежними (акцент сверху, --rule по бокам), меняется
   только толщина. */
html[data-theme="warmth"] .card__link {
  border-radius: 8px;
  border-width: 2px;
  /* мокап поднимает паддинг карточки примерно на 20% (24/22/20 вместо
     20/18/16); ближайший именованный шаг (--step-3: 28px) даёт скачок в
     2.3× — для тесной карточки слишком грубо, поэтому берём шаг поменьше,
     step-2 + step-1, без нового сырого px-значения. */
  padding: calc(var(--step-2) + var(--step-1));
}
html[data-theme="warmth"] .check__submit,
html[data-theme="warmth"] .max-toggle {
  border-radius: 8px;
}

/* Второй акцент (--accent-2) — фирменный «парный» тон темы вне девятитоновой
   ротации: используется для мелкого декоративного акцента (пункты шкалы
   «усилие») и для активной кнопки виджета переключателя тем ниже. */
html[data-theme] .scale--effort .scale__pip.is-on { background: var(--accent-2); }

/* --- Виджет переключателя тем --------------------------------------------
   Разметку строит setupThemeToggle() (app.js) в слот data-theme-toggle-slot
   (masthead.twig) — тот же приём, что у .reset и .max-toggle: без JS слот
   остаётся пустым, а не показывает нерабочие кнопки (spec §7.5). Активная
   кнопка красится основным --accent темы (как в мокапе), не второстепенным
   --accent-2. */
.theme-toggle { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--step-3); margin-top: var(--step-2); }
.theme-toggle__axis { display: inline-flex; align-items: center; gap: var(--step-1); }
.theme-toggle__axis-label { font-family: var(--font-mono); font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.theme-toggle__group { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--rule); border-radius: 2px; overflow: hidden; }
.theme-toggle__btn {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: none;
  border: none;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
}
.theme-toggle__btn + .theme-toggle__btn { border-left: 1px solid var(--rule); }
.theme-toggle__btn.is-active { background: var(--accent); color: var(--paper); }
.theme-toggle__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; position: relative; z-index: 1; }
.language-toggle { margin-top: 0; }
.language-toggle { margin-left: var(--step-1); }
.language-toggle__group { display: inline-flex; border: 1px solid var(--rule); border-radius: 2px; overflow: hidden; }
.language-toggle__btn { font-family: var(--font-mono); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; background: none; border: none; padding: var(--step-1) var(--step-2); color: var(--muted); cursor: pointer; }
.language-toggle__btn + .language-toggle__btn { border-left: 1px solid var(--rule); }
.language-toggle__btn.is-active { background: var(--accent); color: var(--paper); }
.language-toggle__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.profile-page__settings { margin: var(--step-3) 0 var(--step-4); padding: var(--step-2) 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); align-items: flex-start; }
.profile-page__settings .theme-toggle__axis { flex-wrap: wrap; }
@media (max-width: 699px) {
  .profile-page__settings { align-items: stretch; flex-direction: column; }
  .profile-page__settings .theme-toggle__axis { align-items: flex-start; flex-direction: column; }
}
/* Короткая карта выбора на /start: сначала ориентир по глубине, затем обычный
   выбор устройств и сервисов в шапке. */
.start-onboarding { max-width: var(--measure); margin: var(--step-3) auto; padding: var(--step-2) 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.start-onboarding__title { margin: 0 0 var(--step-2); font-size: 1.1rem; }
.start-onboarding__intro { max-width: 36rem; }
.start-onboarding__levels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--step-3); }
.start-onboarding__level { padding: var(--step-2); background: var(--paper-sunk); border: 1px solid var(--rule); }
.start-onboarding__level h3 { margin: 0 0 var(--step-1); font-size: 1rem; }
.start-onboarding__level p { margin: var(--step-1) 0 0; }
.start-onboarding__settings, .start-onboarding__profile { margin-top: var(--step-3); }
.start-onboarding__settings h3, .start-onboarding__profile h3 { font-size: 1.1rem; }
.start-onboarding__setting { margin-top: var(--step-2); }
.start-onboarding__setting + .start-onboarding__setting { margin-top: var(--step-3); }
.start-onboarding__setting-explain { margin: 0 0 var(--step-1); }
.start-onboarding__setting-example { margin: 0 0 var(--step-1); }
.start-onboarding__example { margin-top: var(--step-3); }
.start-onboarding__example-label { font-family: var(--font-mono); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin: 0 0 var(--step-1); }
.profile--start { margin-top: var(--step-2); }
@media (max-width: 699px) { .start-onboarding__levels { grid-template-columns: 1fr; } }
/* Названия элементов интерфейса в тексте — текстовые control-chip, а не кнопки.
   Site-owned labels intentionally stay outside this class and language scope.
   Исключение — живой образец в онбординге: это тоже подпись внешней программы,
   просто вынесенная из темы наверх, поэтому выглядит и переключается так же. */
.start-onboarding__setting-example .ui-label,
.topic__body .ui-label {
  display: inline-block;
  max-width: 100%;
  padding: 0 0.3em;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: var(--paper-sunk);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.86em;
  font-weight: 400;
  line-height: 1.3;
  vertical-align: baseline;
  overflow-wrap: anywhere;
  white-space: normal;
}
.topic__body code { overflow-wrap: anywhere; word-break: break-word; white-space: normal; }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; } }

/* --- Виджет профиля читателя -----------------------------------------------
   Та же анатомия, что у .theme-toggle: рамка на группе кнопок, сами кнопки
   без своей рамки, активная заливается --accent. Отличие от переключателя
   темы — выбор МНОЖЕСТВЕННЫЙ: у .profile__btn нет второй, гасящей её кнопки
   в паре, is-active просто переключается по клику, и группа не получает
   никаких per-темных скруглений (как и .theme-toggle__group — единственная
   постоянная толщина/радиус на всех четырёх темах). Разметку строит
   setupProfile() (app.js) в слоты data-profile-slot (масштед и /profile) —
   тот же приём, что у .reset/.max-toggle/.theme-toggle: без JS слот остаётся
   пустым.

   Данные — два больших раздела (Устройства/Сервисы), в каждом — строки
   подкатегорий (Компьютер/Планшет/…). buildProfileSectionsTable() (app.js)
   рендерит одну и ту же таблицу «подкатегория | выбор» на всю ширину и на
   /profile, и в компактном <details> шапки (data-mode="bar") — раньше шапка
   строила отдельную безтабличную разметку (подпись и кнопки в ряд, с
   переносом), из-за которой подпись подкатегории и её кнопки оказывались в
   одной строке, а две подкатегории — рядом в одной строке; ушла вместе со
   вторым рендерером. В шапке отличается только класс profile__sections--
   compact/profile__section--compact на обёртке (ниже сужает вертикальные
   отступы строк таблицы, чтобы не спорить по весу с переключателем темы
   рядом) и заголовок раздела — <span>, а не <h2> (см. app.js). */
.profile { font-family: var(--font-mono); font-size: 0.78rem; margin-top: var(--step-2); }
.profile__group { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--rule); border-radius: 2px; overflow: hidden; }
.profile__btn {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: none;
  border: none;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
}
.profile__btn::before { content: "☐ "; }
.profile__btn[aria-pressed="true"]::before { content: "☑ "; }
.profile__btn + .profile__btn { border-left: 1px solid var(--rule); }
.profile__btn.is-active { background: var(--accent); color: var(--paper); }

/* Компактный режим в шапке (data-mode="bar"): разделы свёрнуты под <details>,
   чтобы не спорить по весу с переключателем темы рядом. Подпись раздела —
   как штамп даты (.masthead__stamp): мелкая, моно, приглушённая, свой маркер
   вместо встроенного — тот же приём, что у details.howto выше. Внутри —
   та же таблица «подкатегория | выбор», что и на /profile (см. комментарий
   у .profile выше и buildProfileSectionsTable() в app.js), только с более
   тесными строками: полный межстрочный интервал страницы /profile (var(--
   step-2) = 12px сверху/снизу ячейки) в узком <details> и был тем самым
   «слишком широким» интервалом из жалобы читателя — здесь он сужен до
   var(--step-1) = 4px, втрое компактнее, а сама таблица теряет отступ сверху
   от заголовка раздела. Полная страница /profile ничего из этого не
   наследует: .profile__section--compact на неё не попадает. */
.profile__details > summary {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
  list-style: none;
}
.profile__details > summary::-webkit-details-marker { display: none; }
.profile__details > summary::before { content: "\25B8\A0"; color: var(--accent); }
.profile__details[open] > summary::before { content: "\25BE\A0"; }
.profile__details > .profile__sections { margin-top: var(--step-2); }
.profile__sections--compact { display: flex; flex-direction: column; gap: var(--step-3); }
.profile__section--compact .profile__section-title { display: block; margin-bottom: var(--step-1); }
.profile__section--compact .profile__table { margin-top: var(--step-1); }
/* padding-top/-bottom как отдельные свойства, а не шорткат padding: тот
   переписал бы все четыре стороны сразу и обнулил бы padding-right: var(--
   step-3), который .profile__row-label ставит ниже как раз для зазора между
   подписью и кнопками — с шорткатом самая длинная подпись («Мессенджеры»)
   упиралась прямо в кнопки без него (найдено на скриншоте). */
.profile__section--compact .profile__row-label,
.profile__section--compact .profile__row-choices { padding-top: var(--step-1); padding-bottom: var(--step-1); }

/* Полная страница /profile (data-mode="page", класс .profile--page): каждый
   раздел — заголовок и таблица из двух колонок на всю ширину, открывать тут
   нечего, читатель и так за этим пришёл. */
.profile--page { margin-top: var(--step-4); }
.profile--page .profile__sections { display: flex; flex-direction: column; gap: var(--step-4); }
.profile__section-title { font-size: 1.1rem; }
.profile__table { width: 100%; border-collapse: collapse; margin-top: var(--step-2); }
.profile__row-label,
.profile__row-choices { padding: var(--step-2) 0; border-bottom: 1px solid var(--rule); text-align: left; vertical-align: middle; }
.profile__row:last-child .profile__row-label,
.profile__row:last-child .profile__row-choices { border-bottom: none; }
.profile__row-label { font-family: var(--font-display); font-weight: 700; color: var(--ink); white-space: nowrap; padding-right: var(--step-3); width: 1%; }

/* Кнопка «Забыть мои ответы» — тихая, как .reset: тот же контурный стиль и та
   же интонация служебного действия, чтобы её не приняли за призыв к действию. */
.profile-forget {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
  margin-top: var(--step-2);
}

/* Верхний отступ заголовка — та же конвенция, что у .hub__promise и
   .topic__header: --step-4 от нижней линии шапки, иначе h1 садится
   вплотную к border-bottom .masthead (margin: 0 у h1 по умолчанию). */
.profile-page__title { margin-top: var(--step-4); }

/* Завершающая кнопка «Готово» — главный призыв к действию на странице
   профиля, поэтому крупнее и заметнее служебной «Забыть мои ответы» выше:
   по центру, с большим отступом сверху, чтобы явно отделить от настроек. */
.profile-page__done { margin-top: var(--step-4); text-align: center; }
.profile-page__done-link {
  display: inline-block;
  text-decoration: none;
  font-size: 0.95rem;
  padding: var(--step-2) var(--step-3);
}

/* --- Адаптация annotate под дизайн-систему бюллетеня (.task/annotation-mode/
   plan.md §1) -----------------------------------------------------------
   Портируемый модуль читает свои переменные как var(--annotate-*, дефолт);
   мы переопределяем только текст/фон/рамки, чтобы он бесплатно получил
   тёмный режим и палитру всех 4 тем — они уже переопределяют --paper/--ink/
   --rule и т.п. на своих уровнях, здесь достаточно косвенной ссылки через
   var(). --annotate-accent и остальные (accent-fg/done/shadow/radius/
   radius-pill/gap/max-width/z) нарочно не трогаем — инструмент редактора
   должен быть узнаваемо чужеродным на любой странице/теме.

   БАГ (обнаружен и исправлен 2026-09-15, репорт владельца про невидимость
   режима редактуры на всех темах, кроме едва-едва — «Тепло»): в предыдущей
   редакции этого комментария список оканчивался на «radius» со звёздочкой,
   сразу за которой шёл слэш следующего пункта — звёздочка и слэш стояли
   вплотную, без пробела между ними. Ровно эти два символа подряд и есть
   последовательность, закрывающая CSS-комментарий, поэтому браузер читал
   комментарий закрытым прямо там: весь хвост абзаца, настоящая закрывающая
   пара ниже и сам блок `:root { --annotate-bg: ...; }` превращались в
   мусорный CSS-селектор (кириллица, непарная скобка, тире), и всё правило
   целиком отбрасывалось парсером. Из-за этого блок `--annotate-bg/-fg/
   -muted/-border/-font/-font-quote` ниже никогда не применялся ни на одной
   теме: модуль всегда падал на собственные литеральные светлые дефолты из
   annotate.css, вне зависимости от --paper/--ink активной темы. Урок:
   в слэш-разделённом списке внутри такого комментария звёздочка не должна
   стоять вплотную перед слэшем следующего пункта — между ними нужен пробел
   или другой разделитель. */
:root {
  --annotate-bg: var(--paper);
  --annotate-bg-sunk: var(--paper-sunk);
  --annotate-fg: var(--ink);
  --annotate-muted: var(--muted);
  --annotate-border: var(--rule);
  --annotate-font: var(--font-display);
  --annotate-font-quote: var(--font-text);
}

/* Метка заметки внутри залитой кнопки (репорт владельца, 2026-09-13) --------
   CTA сайта уже несёт собственную заливку (background: var(--accent) +
   color: var(--paper)), и плоская заливка метки поверх неё читается не как
   «вот этот кусок выделил редактор», а как «кнопка целиком перекрасилась» —
   особенно когда выделена вся подпись кнопки. Общий случай (метка в обычном
   тексте) остаётся ровно таким, как задуман выше: ни одна --annotate-*
   переменная здесь не переопределяется, чужеродность маркера не размывается.
   Меняется только ГЕОМЕТРИЯ метки в этом одном контексте — та же краска
   модуля, но полоской под текстом, а не заливкой поверх кнопки.

   Полоска двухцветная, и нижняя её половина — currentColor, не ради красоты:
   ни один фиксированный цвет не виден на всём диапазоне заливок сразу. На
   /profile --accent — это --ink (почти чёрный в светлых темах, почти белый в
   тёмных), на странице темы — средний тон девятитоновой формулы; жёлтый
   #ffe9a8 пропадает на светлых заливках ровно так же, как красный #b3261e на
   тёмных. currentColor внутри метки — это color самой кнопки, то есть
   --paper, а его контраст с --accent сайт уже гарантирует: иначе была бы
   нечитаема сама подпись кнопки. Значит полоска видна по построению в любой
   теме, а краска модуля поверх неё остаётся узнаваемо чужеродной.

   Список — не «любой элемент с фоном», а ровно те правила app.css, где стоит
   пара background: var(--accent) + color: var(--paper) и есть текст, который
   редактор может выделить (.scale__pip.is-on из этой пары выпадает: там нет
   текста). У is-active-кнопок селектор снимается вместе с классом — погасшая
   кнопка снова получает обычный жёлтый маркер. */
:is(.check__submit, .editor-form__submit, .max-toggle__btn.is-active,
    .theme-toggle__btn.is-active, .language-toggle__btn.is-active,
    .profile__btn.is-active) .annotate-mark {
  --cta-mark-stripe: var(--annotate-mark-bg, #ffe9a8);
  background: none;
  box-shadow:
    inset 0 -0.12em 0 0 currentColor,
    inset 0 -0.32em 0 0 var(--cta-mark-stripe);
}
:is(.check__submit, .editor-form__submit, .max-toggle__btn.is-active,
    .theme-toggle__btn.is-active, .language-toggle__btn.is-active,
    .profile__btn.is-active) .annotate-mark--done {
  --cta-mark-stripe: var(--annotate-mark-done-bg, #d7ead9);
}
/* Заметка, на которую только что перешли по ссылке журнала: контур модуля
   (annotate.css, .annotate-mark.is-current) остаётся красным и чужеродным, но
   снаружи кнопки ему места нет — у .check__submit вертикальный паддинг всего
   var(--step-1), и контур с offset 1px вылезает за край кнопки, перечёркивая
   её собственную границу. Уводим его внутрь строки. */
:is(.check__submit, .editor-form__submit, .max-toggle__btn.is-active,
    .theme-toggle__btn.is-active, .language-toggle__btn.is-active,
    .profile__btn.is-active) .annotate-mark.is-current {
  outline-offset: -1px;
}

/* Бейдж «Режим редактуры» — fixed-элемент вне потока (annotate.css:57-75):
   top: var(--annotate-gap, 12px), внутри кнопка ~13px×1.4 строки текста
   (line-height наследуется от [data-annotate-root]) + padding 6px×2 +
   border 2px×2 ≈ 34px высоты. Итого от верха вьюпорта до низа бейджа —
   около 46px; добавляем тот же зазор ещё раз сверху шапки, чтобы бейдж не
   соприкасался с рамкой .masthead. .annotate-badge появляется в DOM только
   пока модуль реально активен — buildRootDom() вызывается из activate(),
   а не из init() (annotate.js) — поэтому :has() ловит именно «режим
   реально включён», без единой правки портируемого модуля. */
body:has(.annotate-badge) .masthead { margin-top: calc(var(--annotate-gap, 12px) * 2 + 34px); }

/* Меню бейджа .annotate-menu (по центру вьюпорта, min-width 180px,
   annotate.css:109-124) и правая панель заметок страницы .annotate-side
   (прижата к правому краю, ширина 20rem, annotate.css:378-394) — два
   независимых fixed-блока модуля. На широких экранах между ними много
   воздуха, но на узких их X-диапазоны пересекаются: живой замер в Chromium
   на 375px при открытом меню — меню {top:52,bottom:171,left:98,right:278} к
   панели {top:58,bottom:139,left:43,right:363}, то есть меню целиком ложится
   поверх панели. annotate.css не даёт переменной под вертикальный отступ
   панели (только --annotate-side-width под ширину — см. его докблок:
   переопределять модуль можно только переменными снаружи, не правкой файла),
   так что сдвиг вниз задаём отсюда, своим правилом на класс модуля — тот же
   приём, что и у body:has(.annotate-badge) .masthead чуть выше. :has()
   ограничивает правило именно моментом, когда меню открыто ([hidden] снимает
   toggleMenu() в annotate.js по клику на бейдж) — обычный показ панели
   заметок без открытого меню не трогается, сдвиг виден, только пока
   действительно есть чему наезжать.

   Порог здесь СВОЙ, не 699px, как у соседних мобильных переопределений —
   геометрия столкновения задаётся не привычной точкой перелома вёрстки, а
   формулами самих модульных блоков: правый край меню — vw/2 + 90px (центр
   плюс половина min-width 180px), левый край панели — vw - 332px (20rem +
   правый отступ 12px). Они совпадают ровно на vw = 844px — до этой ширины
   меню и панель обязаны пересекаться при равной высоте, шире — нет. 899px
   (нижняя граница «899px и ниже» = верхняя граница диапазона конфликта плюс
   запас на скроллбар/округление) перепроверено живым замером в Chromium на
   375/390/480/699/900px — конфликта нет нигде до 899px включительно, с
   900px правило уже не применяется, и живой замер там тоже подтверждает
   отсутствие пересечения естественной раскладки. */
@media (max-width: 899px) {
  body:has(.annotate-menu:not([hidden])) .annotate-side {
    top: calc(var(--annotate-gap, 12px) + 46px + 9rem);
  }
}

/* --- Раздел /admin: инструменты владельца (admin-auth-spec.md §4.5) -------
   Стили сайта, а не модуля — --annotate-* сюда сознательно не заходят: этот
   раздел не часть annotate.js, у него собственная серверная разметка
   (index.twig, admin-bar.twig, notes.twig-обёртка, denied.twig). Токены те
   же, что и везде в app.css: --paper/--ink/--rule/--muted/--accent/--step-*. */

/* Дэшборд (admin/index.twig) — та же карточная сетка, что и .cards у хаба,
   свой namespace классов, потому что список — инструментов, а не тем. */
.admin__title { margin-top: 0; }
.admin__tools { list-style: none; margin: var(--step-3) 0 0; padding: 0; display: grid; gap: var(--step-3); }
@media (min-width: 700px) { .admin__tools { grid-template-columns: repeat(2, 1fr); } }
.admin-tool__link {
  display: block;
  height: 100%;
  padding: var(--step-2);
  border: 1px solid var(--rule);
  border-top: 4px solid var(--accent);
  background: var(--paper-sunk);
  text-decoration: none;
  color: inherit;
}
.admin-tool__title { font-size: 1.2rem; }
.admin-tool__summary { color: var(--ink); margin: 0; }

/* Шапка раздела на каждой странице /admin/* (partials/admin-bar.twig):
   нав по всем инструментам владельца + ссылка назад на дэшборд + форма
   выхода. flex-wrap, чтобы на узких экранах нав переносился отдельной
   строкой, а не толкал кнопку «Выйти» за край (~375px). Кнопка — та же
   тихая интонация служебного действия, что у .profile-forget (см. выше). */
.admin-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--step-2);
  padding-bottom: var(--step-2);
  margin-bottom: var(--step-3);
  border-bottom: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: 0.85rem;
}
.admin-bar__nav { display: flex; flex-wrap: wrap; gap: var(--step-2); }
.admin-bar__nav-link { color: var(--muted); text-decoration: none; }
.admin-bar__nav-link.is-current { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.admin-bar__home { color: var(--muted); text-decoration: none; }
.admin-bar__logout { margin: 0 0 0 auto; }
.admin-bar__logout-button {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
}

/* Наг «дверь токена скоро закроется/уже бессрочна» (OwnerCredentialStore::bannerText(),
   partials/admin-bar.twig) — тот же приём рамки-акцента, что и
   .editor-account__warning/.admin-editors__shown-link (предупреждение, не
   рядовой текст), но вне .admin-bar: он flex, notice — блочный элемент над
   ним, чтобы не разъезжался нав. */
.admin-bar__notice {
  margin: 0 0 var(--step-3) 0;
  padding: var(--step-2);
  border: 1px solid var(--accent);
  background: var(--paper-sunk);
  color: var(--ink);
  font-size: 0.92rem;
}
.admin-bar__notice a { color: inherit; }

/* Страница отказа 401 (admin/denied.twig, RequireAdminMiddleware) — та же
   анатомия, что у общей .error, свой namespace, потому что раздел её не
   наследует и не должен зависеть от неё. */
.admin-denied { margin-top: var(--step-4); }
.admin-denied__status { font-family: var(--font-mono); font-size: 3rem; color: var(--muted); }
.admin-denied__lead { color: var(--ink); }
.admin-denied__login { margin-top: var(--step-3); }
.admin-denied__login-button {
  display: inline-block;
  text-decoration: none;
  font-size: 0.95rem;
  padding: var(--step-2) var(--step-3);
  border: 1px solid var(--accent);
  color: var(--accent);
}

/* /admin/editors (multi-editor-review spec.md §5.1, plan.md этап 5) —
   управление аккаунтами редакторов. shown_link — тот же приём, что
   .editor-auth__error (рамка-акцент), потому что это тоже предупреждение,
   не рядовой текст: секрет показывается ровно один раз. */
.admin-editors__shown-link {
  margin: var(--step-3) 0;
  padding: var(--step-2);
  border: 1px solid var(--accent);
  background: var(--paper-sunk);
  color: var(--ink);
}
.admin-editors__shown-link-row { display: flex; flex-wrap: wrap; gap: var(--step-1); align-items: center; margin: var(--step-2) 0; }
.admin-editors__shown-link-input {
  flex: 1 1 20rem;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  padding: var(--step-1) var(--step-2);
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  border-radius: 2px;
}
.admin-editors__shown-link-copy {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
}
.admin-editors__create,
.admin-editors__list { margin: var(--step-4) 0; }
.admin-editors__table { width: 100%; border-collapse: collapse; margin-top: var(--step-2); }
.admin-editors__table th,
.admin-editors__table td { padding: var(--step-2) var(--step-2) var(--step-2) 0; border-bottom: 1px solid var(--rule); text-align: left; vertical-align: top; }
.admin-editors__actions { display: flex; flex-direction: column; gap: var(--step-1); }
.admin-editors__action { display: flex; align-items: center; gap: var(--step-1); margin: 0; }
.admin-editors__action button {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
}

/* На узких экранах (до 699px, тот же порог, что и у остальных мобильных
   переопределений в этом файле) шесть колонок таблицы — Имя, Логин, Пароль,
   Аккаунт, Ссылка, Действия — не помещаются даже с переносом текста внутри
   ячеек: у последней колонки строка формы .admin-editors__action держит
   select и кнопку рядом без переноса, и это задаёт минимальную ширину всей
   таблицы около 570-615px (перепроверено живым замером в Chromium на
   375-390px, замер в mm/dd не нужен — цифра стабильна во всех темах).
   Раньше это раздвигало саму страницу горизонтально (документ шире
   вьюпорта). display: block переключает сам элемент table в собственный
   скролл-контейнер: браузер достраивает вокруг thead/tbody анонимные
   табличные боксы, колонки остаются выровненными как обычная таблица, а
   горизонтально прокручивается только она сама, а не вся страница. */
@media (max-width: 699px) {
  .admin-editors__table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

/* --- /login и /me: единая дверь входа (владелец + редактор, LoginPage) и
   аккаунт редактора (multi-editor-review spec.md §4.1, §6) — первая
   настоящая текстовая форма в шаблонах (не annotate.js, не .check в
   контенте темы), свой namespace классов, те же токены, что и везде:
   --paper/--ink/--rule/--muted/--accent/--step-* / --font-mono. */
.editor-auth { margin-top: var(--step-4); }
.editor-auth__lead { color: var(--muted); }
.editor-auth__error {
  margin: var(--step-2) 0;
  padding: var(--step-2);
  border: 1px solid var(--accent);
  color: var(--ink);
  font-size: 0.92rem;
}
.editor-form { display: grid; gap: var(--step-2); margin: var(--step-3) 0; max-width: 24rem; }
.editor-form__row { display: grid; gap: var(--step-1); }
.editor-form__row label { font-family: var(--font-mono); font-size: 0.78rem; text-transform: uppercase; color: var(--muted); }
.editor-form__row input {
  font: inherit;
  padding: var(--step-1) var(--step-2);
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  border-radius: 2px;
}
.editor-form__submit {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  background: var(--accent);
  color: var(--paper);
  border: none;
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  cursor: pointer;
  justify-self: start;
}

.editor-account { margin-top: var(--step-4); }
.editor-account__section { margin: var(--step-3) 0; padding-bottom: var(--step-3); border-bottom: 1px solid var(--rule); }
.editor-account__section:last-of-type { border-bottom: none; }
.editor-account__warning {
  margin: var(--step-2) 0;
  padding: var(--step-2);
  border: 1px solid var(--accent);
  background: var(--paper-sunk);
  color: var(--ink);
  font-size: 0.92rem;
}
.editor-account__link-status { font-family: var(--font-mono); font-size: 0.9rem; color: var(--ink); }
.editor-account__revoke-button {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
}
.editor-account__logout { margin-top: var(--step-3); }

/* /admin/review и /me/notes (multi-editor-review spec.md §5.3, §6.3,
   plan.md этап 6) — разбор замечаний владельцем и свои заметки редактора.
   Общий вид с .editor-form/.editor-auth__lead выше, свой namespace только
   там, где структура своя (секции по автору, тред, три кнопки решения). */
.admin-review__filters { display: flex; flex-direction: row; flex-wrap: wrap; align-items: end; gap: var(--step-3); max-width: none; }
.admin-review__section { margin: var(--step-4) 0; }
.admin-review__note {
  margin: var(--step-3) 0;
  padding: var(--step-2) var(--step-3);
  border: 1px solid var(--rule);
  border-radius: 2px;
}
.admin-review__quote { font-style: italic; color: var(--muted); }
.admin-review__meta { font-family: var(--font-mono); font-size: 0.82rem; color: var(--muted); }
.admin-review__decisions { display: flex; gap: var(--step-1); margin: var(--step-2) 0; }
.admin-review__decisions form { margin: 0; }
.admin-review__decisions button {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--step-1) var(--step-2);
  color: var(--muted);
  cursor: pointer;
}
.admin-review__thread,
.editor-notes__thread { list-style: none; margin: var(--step-2) 0; padding: 0; display: grid; gap: var(--step-1); }
.admin-review__thread li,
.editor-notes__thread li { padding: var(--step-1) var(--step-2); background: var(--paper-sunk); border-radius: 2px; }
.admin-review__thread-date,
.editor-notes__thread-date { color: var(--muted); font-size: 0.85rem; }
.admin-review__reply textarea,
.editor-notes__reply textarea {
  display: block;
  width: 100%;
  max-width: 32rem;
  min-height: 4rem;
  font: inherit;
  padding: var(--step-1) var(--step-2);
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  border-radius: 2px;
  margin-bottom: var(--step-1);
}

.editor-notes { margin-top: var(--step-4); }
.editor-notes__note {
  margin: var(--step-3) 0;
  padding: var(--step-2) var(--step-3);
  border: 1px solid var(--rule);
  border-radius: 2px;
}
.editor-notes__quote { font-style: italic; color: var(--muted); }
.editor-notes__meta { font-family: var(--font-mono); font-size: 0.82rem; color: var(--muted); }

/* review-actions.js (annotate-review-merge spec.md §9.6) до сих пор не имел
   ни одного CSS-правила ни под одним из своих .review-actions__* классов —
   .review-actions__reactions рисовал два соседних inline <span> без всякого
   зазора между ними, и текст "За: N · Против: N" склеивался с "Счёт: N" без
   пробела. Тот же приём, что и везде в этом файле для строки инлайн-элементов
   (.admin-review__decisions, .theme-toggle__axis) — flex + gap. */
.review-actions__reactions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--step-1); }
