/*
 * Компонентные стили публичной поверхности — перенос из
 * `design/prototypes/01-styleguide.html` (Sprint 1.1, Task 8).
 *
 * Токены НЕ дублируются: цвета, радиусы и рейтинговая палитра берутся из
 * `tokens.css` как есть (DESIGN_BRIEF: токены — единственный источник
 * правды, прототип их не переопределяет). Здесь только раскладка
 * компонентов.
 *
 * Три плотности карточки — из брифа: `full` (лента фандома), `mid`
 * (поиск: без описания и тегов), `min` (списки «читаю»/закладки: строка с
 * прогрессом).
 */

body {
    font-family: var(--font-ui);
}

/*
 * ── топбар и подвал (Дизайн-спринт, Task 10) ───────────────────────────
 * `@layer components` (спек §4.2, `assets/css/layers.css`) — здесь и в
 * блоке «карточка работы» ниже (Task 11); остальной `app.css` остаётся вне
 * слоёв (докблок `print.css`, РЕШЕНИЕ 1, объясняет почему для файла в
 * целом). Оба участка — НОВАЯ разметка своих задач, а не перенос старой
 * без правки, и заводятся сразу правильно, а не переносятся в слой
 * отдельной задачей потом (в блоке карточки правило то же — старый
 * `.card` не просто обёрнут, а переименован и переписан целиком, докблок
 * Task 11 там же). Классы шапки/подвала и карточки — `rf-*`, а не
 * литералы: тот же довод, что у примитивов (потолок специфичности 0,2,0),
 * только для страничных компонентов, а не переиспользуемых виджетов.
 */
@layer components {

/*
 * Sticky-топбар — визуальный канон `design/prototypes/02-fandom-feed.html`.
 * `position: sticky` без `inset-block-start: 0` не прилипает вовсе — это
 * не опечатка, а обязательная часть контракта свойства.
 */
.rf-topbar {
    position: sticky;
    inset-block-start: 0;
    z-index: 10;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    padding-block: var(--space-3);
    padding-inline: var(--space-4);
    border-block-end: 1px solid var(--border);
}

/*
 * `scroll-padding-top` НА `html` (фикс-раунд 1, I-1). Ряд разделов страницы
 * автора (`.rf-sections__nav`, `templates/users/author.html.twig`) — первая
 * и единственная якорная навигация внутри страницы во всём дереве; переход
 * по чипу ставил верх целевой секции вровень с верхом окна, а липкий
 * `.rf-topbar` ложился поверх — `<h2>` секции, единственная подсказка
 * читателю, куда его привело, оказывалась ПОЛНОСТЬЮ под шапкой (замер
 * ревью — 1200 и 390; regression-тест — `tests/Visual/anchor-scroll.spec.ts`).
 *
 * НА `html`, А НЕ `scroll-margin-top` НА КАЖДОЙ СЕКЦИИ: одно правило на
 * СКРОЛЛ-КОНТЕЙНЕРЕ обслуживает любой будущий якорь автоматически — вторая
 * якорная навигация в другой задаче не обязана вспоминать про топбар и
 * расставлять `scroll-margin-top` по своим секциям вручную.
 *
 * ЧИСЛО — `calc()` ОТ ТЕХ ЖЕ ТОКЕНОВ, ЧТО СТРОЯТ ВЫСОТУ `.rf-topbar`, А НЕ
 * ЛИТЕРАЛ ТИПА `71px`. Высота шапки — не константа: `padding-block` и `gap`
 * заданы в `--space-3` (`rem`) и растут с кеглем корня, а сама шапка —
 * `display: flex; flex-wrap: wrap` и на узких экранах с увеличенным кеглем
 * переносится в ДВЕ строки (бренд перестаёт помещаться рядом с поиском и
 * блоком пользователя). Замер живой шапки (`getBoundingClientRect`,
 * `document.documentElement.style.fontSize`):
 *
 *   1200 / 16px корень   71px (одна строка)
 *   390  / 16px корень   71px (одна строка)
 *   1200 / 24px корень   83px (одна строка)
 *   390  / 24px корень  145px (ДВЕ строки — единственный застигнутый случай)
 *
 * Формула ниже считает ХУДШИЙ случай — две строки — теми же величинами,
 * что и сама шапка: `padding-block` дважды, зазор между строками `gap`
 * (тот же `--space-3`, он же `row-gap` в перенёсшемся flex), две строки
 * высотой `--rf-tap` (таргет FE-2, минимальная высота каждого элемента
 * шапки) и рамка `border-block-end`. Запас `--space-2` покрывает то, что
 * не каждая строка ровно `--rf-tap` (напр. строка поиска — 46px, на 2px
 * выше таргета, из-за иконки внутри поля): `calc(3 × --space-3 + 2 ×
 * --rf-tap + --space-2 + 1px)` — при корне 24px даёт 155px против
 * измеренных 145px, при корне 16px — 133px против измеренных 71px.
 *
 * ПОЧЕМУ НЕ УЖЕ, МЕДИА-ЗАПРОСОМ ПОД ОДНОСТРОЧНУЮ ШАПКУ. Перенос шапки в две
 * строки — не следствие одного документированного брейкпоинта (тот, что
 * есть, — `.rf-topbar__nav` не переносится ниже 800px, докблок «Finding 1»
 * ниже по файлу, — а не про шапку целиком); это текучий эффект ширины
 * контента, которая зависит и от вьюпорта, и от кегля корня разом.
 * Медиа-запрос под конкретную ширину дублировал бы порог, которого в
 * разметке нет и который придётся поддерживать руками при любой правке
 * содержимого шапки, — тот же класс проблемы, которого просит избежать сам
 * фикс. Расплата — по формуле в однострочном случае (1200/390 при 16px)
 * якорь останавливается на 133px, а не на фактических 71px топбара:
 * лишние ~60px воздуха НАД заголовком раздела при переходе по чипу. Пусто,
 * но не дефект: инвентарь и ревью требуют, чтобы заголовок НЕ БЫЛ скрыт, а
 * не чтобы зазор был минимальным.
 */
html {
    scroll-padding-top: calc(3 * var(--space-3) + 2 * var(--rf-tap) + var(--space-2) + 1px);
}

/*
 * ФОН И РАЗМЫТИЕ ШАПКИ — НА ПСЕВДОЭЛЕМЕНТЕ, А НЕ НА САМОЙ `.rf-topbar`
 * (Дизайн-спринт, Task 15.6). Вид не меняется ни на йоту; меняется то, чьим
 * CONTAINING BLOCK становится шапка.
 *
 * ПОЧЕМУ. `backdrop-filter` (как `filter`, `transform`, `perspective`,
 * `contain: paint`, `will-change` на них) делает элемент containing block'ом
 * для ПОТОМКОВ С `position: fixed` — это требование спецификации Filter
 * Effects, а не причуда браузера. Меню аккаунта — `<details
 * class="rf-sheet-host rf-topbar__account">` ВНУТРИ шапки (`base.html.twig`,
 * блок `site_user`), а панель SSR-листа прибита к экрану `position: fixed` +
 * `inset` из примитива (`ui.css`, `.rf-sheet--bottom`/`--popover`). Пока
 * размытие стояло на самой шапке, `inset` панели считался от её коробки
 * высотой 71px, а не от окна, и меню уезжало ВЫШЕ экрана, поверх
 * собственного аватара. Замер (аноним, разметка меню вставлена в настоящую
 * шапку скриптом — `tests/Visual/sheet-position.spec.ts`, там же почему
 * именно так):
 *
 *   390×844   панель y=-360..70 — 360px срезано сверху, `elementFromPoint`
 *             в центре аватара отдавал содержимое самой панели;
 *   1200×900  панель y=-180..251 — поповер отцентрован по шапке (`50%` от
 *             71px), а не по окну.
 *
 * Задевало КАЖДУЮ страницу вошедшего читателя.
 *
 * ПОЧЕМУ ИМЕННО ПСЕВДОЭЛЕМЕНТ. Размытие — часть канона прототипа, а не
 * случайность: полупрозрачный фон `color-mix(… 88%, transparent)` без него
 * читается как грязь поверх уезжающего контента (замер: снять размытие —
 * до 50 520 различающихся пикселей из 85 200 в полосе шапки на `/search`).
 * Псевдоэлемент сохраняет и фон, и размытие, и ПОРЯДОК ИХ НАЛОЖЕНИЯ
 * (сначала размывается то, что под шапкой, затем поверх ложится
 * полупрозрачный фон — ровно как было, когда оба свойства стояли на одном
 * элементе), но `position: fixed` потомков он не перехватывает: containing
 * block'ом становится он сам, а внутри него потомков нет вовсе. Замер
 * разницы вида: максимум 2/255 по каналу, и только по краям сглаживания
 * СОБСТВЕННЫХ надписей шапки — контент под ней не отличается ни пикселем.
 *
 * `inset: 0` — это padding-box шапки, то есть нижняя граница остаётся
 * снаружи и рисуется самой `.rf-topbar`, как и раньше. `z-index: -1` держит
 * псевдоэлемент под содержимым шапки и не течёт наружу: `.rf-topbar` со
 * своим `z-index: 10` — уже стековый контекст. Абсолютно позиционированный
 * `::before` не становится flex-элементом, поэтому раскладка шапки не
 * сдвигается (проверено замером: коробки всех элементов шапки совпадают
 * до пикселя).
 */
.rf-topbar::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: blur(12px);
}

.rf-topbar__brand {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--rf-tap);
    font-size: var(--fs-lg);
    font-weight: 700;
    letter-spacing: -.02em;
    color: var(--text);
    text-decoration: none;
}

/* «fic» — акцентом, «Ever» — обычным текстом (канон прототипа): одна
 * ссылка, доступное имя которой всё равно «Everfic» целиком — разбивка
 * только визуальная, между буквами нет пробела и второго узла с ролью. */
.rf-topbar__brand-accent {
    color: var(--primary);
}

.rf-topbar__search {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex: 1 1 200px;
    min-inline-size: 0;
    max-inline-size: 420px;
    padding-inline: var(--space-4) var(--space-2);
    border-radius: var(--r-chip);
    border: 1px solid var(--border);
    background: var(--surface);
    transition: border-color var(--dur-fast);
}

.rf-topbar__search:focus-within {
    border-color: var(--primary);
}

.rf-topbar__search-icon {
    display: inline-flex;
    flex-shrink: 0;
    color: var(--text-muted);
    cursor: pointer;
}

.rf-topbar__search-input {
    flex: 1 1 auto;
    min-inline-size: 0;
    min-block-size: var(--rf-tap);
    border: 0;
    background: transparent;
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-base);
}

.rf-topbar__search-input::placeholder {
    color: var(--text-muted);
}

/* Кольцо — на самом поле, а не на пилюле-обёртке: обёртка не элемент
 * управления, фокус получает только `<input>`. */
.rf-topbar__search-input:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
    border-radius: var(--r-chip);
}

/* Группа «Фандомы» + тема + блок пользователя — ОДИН флекс-элемент
 * топбара, прижимается вправо на любой ширине (`margin-inline-start:
 * auto`).
 *
 * ФИКС-РАУНД 1 (Finding 1). Раньше здесь стоял докблок, объяснявший перенос
 * группы на вторую строку как осознанный выбор («не разрывать свои
 * три-пять элементов между двумя строками порознь»). Контроллер отчёта
 * переиграл это решение: канон прототипа (`02-fandom-feed.html`) держит
 * топбар в одну строку на любой ширине, и перенос стоил 71 лишний пиксель
 * на каждом телефонном экране — там, где вертикаль и есть продукт.
 * Правильный ответ — не разрешать перенос, а НЕ ДАВАТЬ группе повода
 * переноситься: «Фандомы» скрывается ниже 800px (докблок у самой ссылки в
 * `base.html.twig`, правило `.rf-topbar__link` ниже), поиск сворачивается
 * в значок ниже 560px (`.rf-topbar__search`, `@media (max-width:
 * 559.98px)` ниже). После этого в группе остаются максимум тема и блок
 * пользователя (2 элемента) — им хватает места на любой измеренной ширине
 * без переноса (замер контроллера 390/560/1200 и `no-horizontal-scroll.spec.ts`
 * это подтверждают). `flex-wrap: wrap` оставлен страховкой на случай
 * будущего роста числа элементов, а не рабочим механизмом текущей
 * раскладки.
 */
.rf-topbar__nav {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-inline-start: auto;
}

/*
 * Видна от 800px (спек §2.1) — тот же брейкпоинт, что у `Sheet` (`ui.css`)
 * и у колонок подвала (`.rf-footer__columns` ниже): переключение вместе с
 * ними в одном `@media (min-width: 800px)` блоке, а не втором отдельном
 * числе. Ниже 800px ссылка не теряется — переезжает в подвал (колонка
 * «Библиотека»: Фандомы/Теги/Поиск, видна на любой ширине). `display: none`
 * убирает ссылку и из потока, и из порядка обхода Tab — второй копии в
 * разметке нет, дублирующий `aria-hidden` не нужен.
 */
.rf-topbar__link {
    display: none;
    align-items: center;
    min-block-size: var(--rf-tap);
    padding-inline: var(--space-2);
    border-radius: var(--r-chip);
    color: var(--text-muted);
    text-decoration: none;
    font-size: var(--fs-base);
    font-weight: 600;
}

.rf-topbar__link:hover { color: var(--primary); }

.rf-topbar__theme,
.rf-topbar__bell {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-block-size: var(--rf-tap);
    min-inline-size: var(--rf-tap);
    border: 0;
    border-radius: var(--r-chip);
    background: transparent;
    color: var(--text-muted);
    font-size: var(--fs-lg);
    text-decoration: none;
    cursor: pointer;
    transition: color var(--dur-fast), background-color var(--dur-fast);
}

.rf-topbar__theme:hover,
.rf-topbar__bell:hover { color: var(--primary); background: var(--surface); }

.rf-topbar__link:focus-visible,
.rf-topbar__theme:focus-visible,
.rf-topbar__bell:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
}

/*
 * `.rf-topbar a.rf-btn { text-decoration: none }` СНЯТО (Дизайн-спринт,
 * Task 17). Правило гасило подчёркивание у «Войти» — единственного места
 * топбара, где `.rf-btn` стоит на `<a>`, — и его собственный докблок
 * (Task 10) называл причину прямо: «примитив не гасит подчёркивание
 * браузера у ссылки — это ЕГО пробел, не повод трогать `ui.css` (файловый
 * список этой задачи его не касается)». Файловый список Task 17 `ui.css`
 * касается, и пробел закрыт там, где живёт (`text-decoration: none` в
 * `.rf-btn`): у него нашёлся пятый потребитель — «Пропустить» в липкой
 * панели онбординга. Держать здесь правило, повторяющее свойство
 * примитива, после этого значило бы завести вторую правду об одном
 * свойстве — ровно то, чего каскадные слои и заведены не допускать.
 */

/*
 * Меню аватара — триггер СВОЙ, а не заимствованный у `.rf-sheet-host >
 * summary` (та геометрия — пилюля с рамкой под ТЕКСТОВЫЙ ярлык, здесь
 * триггер — круглый аватар сам по себе). `@layer components` побеждает
 * `@layer primitives` ПОРЯДКОМ СЛОЯ (спек §4.2), а не специфичностью
 * селектора — поэтому обнулить чужой паддинг/рамку/фон можно этим же
 * простым селектором, без `!important` и без гонки за специфичностью с
 * `.rf-sheet-host[open] > summary` из `ui.css`.
 */
.rf-topbar__account > summary {
    padding: 0;
    min-inline-size: auto;
    border: 0;
    background: transparent;
}

.rf-topbar__account[open] > summary {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: 50%;
}

.rf-menu__name {
    margin: 0 0 var(--space-3);
    font-weight: 600;
    color: var(--text);
}

.rf-menu__links {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.rf-menu__links a {
    display: flex;
    align-items: center;
    min-block-size: var(--rf-tap);
    padding-inline: var(--space-2);
    border-radius: var(--r-chip);
    color: var(--text);
    text-decoration: none;
    font-size: var(--fs-base);
}

.rf-menu__links a:hover { background: var(--bg); color: var(--primary); }

.rf-menu__links a:focus-visible {
    outline: var(--focus-ring);
    outline-offset: -2px;
}

.rf-menu__logout {
    margin-block-start: var(--space-3);
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--border);
}

/* ── подвал ────────────────────────────────────────────────────────── */

.rf-footer {
    border-block-start: 1px solid var(--border);
    background: var(--bg);
    padding-block: var(--space-7);
    padding-inline: var(--space-4);
}

.rf-footer__columns {
    max-inline-size: 860px;
    margin-inline: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}

.rf-footer__column {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.rf-footer__heading {
    margin: 0;
    font-size: var(--fs-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-muted);
}

.rf-footer__brand {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--rf-tap);
    font-size: var(--fs-lg);
    font-weight: 700;
    color: var(--text);
    text-decoration: none;
}

.rf-footer__note {
    margin: 0;
    max-inline-size: 42ch;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.rf-footer__copy {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

/*
 * ЛОГО ПОДВАЛА ИСКЛЮЧЕНО ИЗ ПРАВИЛА ССЫЛОК СПИСКА — И ЭТО НЕ ПЕДАНТИЗМ.
 *
 * `.rf-footer__brand` физически лежит ВНУТРИ первой `.rf-footer__column`
 * (`templates/base.html.twig`), а оба правила стоят в одном
 * `@layer components`. Порядок слоёв здесь ничего не решает — решает
 * специфичность: `.rf-footer__column a` (0,1,1) перебивал `.rf-footer__brand`
 * (0,1,0) по каждому свойству, которое объявляют оба. Замер
 * `getComputedStyle()` до этой правки (Chromium, статичная страница с
 * настоящими CSS-файлами дерева и разметкой подвала, тёмная тема — не
 * живой стенд): лого `font-size: 14.4px` (`--fs-base`) вместо 16.8px
 * (`--fs-lg`), `color: rgb(155, 148, 184)` (`--text-muted`) вместо
 * `rgb(240, 237, 250)` (`--text`), под курсором — `--primary`, как пункт
 * списка. `display` в счёт не идёт: колонка — flex, и `inline-flex` лого
 * блокифицируется в `flex` при любом исходе.
 *
 * Баг прожил с Task 10, и прятали его дважды. Сначала неслоёный `base.css`:
 * оба правила проигрывали его `a { color: var(--primary) }`, и в цвете
 * разницы не было. Потом докблок Task 13.5 ниже по файлу записал лого
 * подвала в «заработавшие as-is» — про цвет из `base.css` это правда, а
 * этот второй конфликт открылся ровно под ней. Канон его тоже не выдаёт:
 * в `design/prototypes/04-shell.html` (и в копиях подвала 07–10) те же два
 * правила стоят в том же порядке с той же специфичностью, так что
 * ОТРИСОВАННЫЙ прототип показывает лого приглушённым — хотя его собственное
 * правило объявляет `--text` и `--fs-lg`, и мёртвыми эти объявления
 * задуманы не были (у лого шапки, `.rf-topbar__brand`, те же два токена).
 *
 * ПОЧЕМУ `:where(:not(…))`, А НЕ ГОЛЫЙ `:not(…)`. Гейт
 * `css-specificity.test.ts` пропустил бы и голый: он считает имена классов
 * (в том числе внутри `:not()`), а псевдоклассы по договорённости не
 * считает — `.rf-footer__column a:not(.rf-footer__brand)` для него два
 * класса, ровно потолок. В этом «ровно» и цена: `:not()` берёт
 * специфичность аргумента, и правило СПИСКА выросло бы с 0,1,1 до 0,2,1 —
 * до самого потолка принципа 4 ради того, чтобы кого-то НЕ описывать.
 * Любое будущее уточнение ссылок подвала тогда либо встаёт на потолок, либо
 * проигрывает. `:where()` обнуляет вклад исключения: правило остаётся
 * 0,1,1, наведение — 0,2,1, ровно как было. Исключение фильтрует, а не
 * выигрывает.
 *
 * ПОЧЕМУ НЕ ДВА ДРУГИХ ПУТИ. Поднять лого до `.rf-footer__column
 * .rf-footer__brand` (0,2,0) — начать ту самую гонку селекторов, от которой
 * заведены слои; наведение списка (0,2,1) всё равно перекрасило бы лого.
 * Сузить список до `nav.rf-footer__column a` — привязать правило к тегу
 * (колонка лого — `<div>`, колонки ссылок — `<nav>`), и первая же правка
 * разметки молча вернула бы баг. Исключение стоит там, где живёт смысл.
 *
 * Кольцо фокуса ниже не тронуто: у лого и списка оно одно и то же, и оба
 * селектора стоят в `FOCUS_RING_REGISTRY` (`css-specificity.test.ts`).
 */
.rf-footer__column a:where(:not(.rf-footer__brand)) {
    display: flex;
    align-items: center;
    min-block-size: var(--rf-tap);
    color: var(--text-muted);
    text-decoration: none;
    font-size: var(--fs-base);
}

.rf-footer__column a:where(:not(.rf-footer__brand)):hover { color: var(--primary); }

.rf-footer__column a:focus-visible,
.rf-footer__brand:focus-visible,
.rf-topbar__account > summary:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
}

/* Три колонки бок о бок только на широком экране — тот же брейкпоинт, что
 * у `Sheet` (`ui.css`, `@media (min-width: 800px)`): одно число геометрии
 * на всё приложение, а не второе, независимо выбранное здесь. */
@media (min-width: 800px) {
    .rf-footer__columns {
        flex-direction: row;
        justify-content: space-between;
        gap: var(--space-7);
    }

    .rf-footer__column {
        flex: 1 1 0;
    }

    .rf-topbar__link {
        display: inline-flex;
    }
}

/*
 * Ниже 560px пилюля поиска сворачивается в значок и разворачивается БЕЗ
 * JS (фикс-раунд 1, Finding 1; спек §2.1: «на <560px пилюля сворачивается
 * в иконку и разворачивается по тапу»). Механизм — `:focus-within`: пока
 * фокус лежит на любом потомке `.rf-topbar__search` (поле или кнопка
 * «Найти»), контейнер считается раскрытым. `<label for="site-search-input">`
 * вокруг значка (`base.html.twig`) переводит туда фокус кликом/тапом —
 * нативное поведение `label[for]`, которое существует без единой строки
 * скрипта; клавиатурный Tab достигает поля тем же путём, каким достигал
 * бы раскрытого поля — оно ни на одной ширине не покидает DOM и не
 * получает `display: none`/`hidden`, значит остаётся в порядке обхода.
 *
 * `max-width`, а не `min-width` (единственное такое исключение в файле —
 * брейкпоинт 800px у `Sheet`/подвала выше устроен наоборот, «от узкого
 * вверх») — сознательно: раскрытое состояние (≥560px) уже верно само по
 * себе (замер контроллера: 71px, одна строка), эта правка добавляет
 * СЖАТОЕ состояние поверх него отдельным блоком, не трогая уже рабочие
 * декларации выше по каскаду.
 */
@media (max-width: 559.98px) {
    .rf-topbar__search {
        flex: 0 0 auto;
        max-inline-size: none;
        padding-inline: var(--space-1);
    }

    /* Тап-таргет метки ≥44px (FE-2) даже когда сама иконка внутри мельче
     * своего em-размера — метка, а не значок, ловит клик/тап. */
    .rf-topbar__search-icon {
        min-inline-size: var(--rf-tap);
        min-block-size: var(--rf-tap);
        justify-content: center;
    }

    .rf-topbar__search-input {
        inline-size: 1px;
        padding-inline-start: 0;
    }

    /*
     * Раскрытому полю нужно куда расти, а бюджет строки на 390px тесен
     * (брейкпоинт §2.1 не даёт «Фандомы» вернуться, но тема+аватар/«Войти»
     * остаются). Общий сиблинг-комбинатор `~` прячет группу темы/
     * пользователя РОВНО пока фокус лежит внутри поиска: `.rf-topbar__search`
     * и `.rf-topbar__nav` — прямые соседи внутри `<nav class="rf-topbar">`
     * (форма поиска в разметке идёт раньше `div.rf-topbar__nav`), поэтому
     * `:has()` не нужен вовсе — тот приём стоял здесь первым чистовиком, но
     * `.rf-topbar:has(...) .rf-topbar__nav` — цепочка из ТРЁХ классов
     * (`.rf-topbar`, `.rf-topbar__search`, `.rf-topbar__nav`) и роняла
     * потолок специфичности 0,2,0 (`css-specificity.test.ts`); `~` считает
     * тем же тестом только сами классы-якоря по обе стороны комбинатора —
     * их два. Группа не теряется навсегда: снова на месте, как только фокус
     * покидает поиск (клик вовне, Tab дальше). Кнопка «Найти» ВНУТРИ формы,
     * поэтому не прячется вместе с группой — раскрытое поле и его отправка
     * остаются на месте целиком.
     */
    .rf-topbar__search:focus-within ~ .rf-topbar__nav {
        display: none;
    }

    .rf-topbar__search:focus-within .rf-topbar__search-input {
        inline-size: min(50vw, 11rem);
        padding-inline-start: var(--space-1);
    }

    /*
     * Кнопка «Найти» — компактной иконкой ВСЕГДА на этой ширине, вне
     * зависимости от того, раскрыто ли поле: форма и так отправляется
     * Enter'ом в поле (фикс-раунд 1, Finding 1, п.3), текстовая подпись
     * занимала бы место без надобности. Текст «Найти» остаётся в разметке
     * (доступное имя кнопки — и без JS, и без CSS), но визуально прячется:
     * ТА ЖЕ схема свойств, что у `.rf-vh` (`ui.css`, «видимый только
     * скринридеру текст») — абсолютное позиционирование в 1px с
     * `clip-path`, не `display: none`, — продублирована здесь СВОИМ
     * классом, а не переиспользованием `.rf-vh`: класс нельзя навесить
     * условно по ширине экрана без скрипта, а подпись обязана прятаться
     * только НИЖЕ 560px, не всегда.
     */
    .rf-topbar__search-submit-label {
        position: absolute;
        inline-size: 1px;
        block-size: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    .rf-topbar__search-submit {
        padding-inline: var(--space-2);
    }
}

@media (prefers-reduced-motion: reduce) {
    .rf-topbar__search,
    .rf-topbar__theme,
    .rf-topbar__bell {
        transition: none;
    }
}

}

/* ── бейджи ─────────────────────────────────────────────────────────── */

.badge {
    font-size: .7rem;
    font-weight: 700;
    border-radius: 6px;
    padding: 3px 8px;
    letter-spacing: .03em;
    display: inline-block;
}

.b-g { background: color-mix(in srgb, var(--rating-g) 18%, transparent); color: var(--rating-g); }
.b-pg13 { background: color-mix(in srgb, var(--rating-pg13) 18%, transparent); color: var(--rating-pg13); }
.b-r { background: color-mix(in srgb, var(--rating-r) 18%, transparent); color: var(--rating-r); }
.b-nc17 { background: color-mix(in srgb, var(--rating-nc17) 18%, transparent); color: var(--rating-nc17); }

.b-dir {
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text-muted);
    font-weight: 600;
}

.b-warn {
    background: color-mix(in srgb, var(--rating-r) 12%, transparent);
    color: var(--rating-r);
    border: 1px dashed color-mix(in srgb, var(--rating-r) 50%, transparent);
}

/*
 * СТАТУС-БЕЙДЖ СТАРОГО СЛОВАРЯ УДАЛЁН ЦЕЛИКОМ (Дизайн-спринт, Task 19).
 * Пять правил — приглушённый бейдж с точкой и три цвета точки по
 * состоянию работы — стояли здесь до этой задачи. Три цветных модификатора
 * остались без единого потребителя ещё в Task 14 (страница работы ушла на
 * `.rf-badge--status`, докблок которого в `assets/css/ui.css` объясняет
 * замену); бареный вариант держали ровно три шаблона, и все три несли им
 * НЕ статус: «приватный» у сборника и «вы заблокировали/замьютили этого
 * автора» на странице автора. Эта задача перевела все три на
 * `.rf-badge--neutral` (`ui.badge_neutral()`), после чего у секции не
 * осталось потребителей ни у одного правила — проверено грепом по
 * `templates/` перед удалением, а не выведено из плана.
 *
 * Соседние правила старого словаря (`.badge`, `.b-{рейтинг}`, `.b-dir`,
 * `.b-warn`) НЕ тронуты: их по-прежнему печатают
 * `templates/compliance/transparency.html.twig` (рейтинг правила
 * видимости) и `templates/tagging/tag.html.twig` (тип тега) — оба вне
 * файлового списка этой задачи.
 */

/*
 * Метка чтения переехала в раздел «карточка работы» ниже (Дизайн-спринт,
 * Task 11, класс `.rf-read-badge`) — card-специфична (единственный
 * потребитель — `work_card.html.twig`), логичнее жить рядом с
 * `.rf-card__top`, куда встаёт в разметке, чем в общей секции бейджей,
 * которую делят страницы, не знающие о карточке вовсе.
 */

/* ── чипы (теги, фильтры) ───────────────────────────────────────────── */

.chip {
    font-size: .8rem;
    font-weight: 500;
    border-radius: var(--r-chip);
    padding: 5px 13px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    font-family: inherit;
    transition: all .15s;
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

.chip:hover { border-color: var(--primary); color: var(--primary); }

/*
 * ── карточка работы (Дизайн-спринт, Task 11) ───────────────────────────
 *
 * Переведена на примитивы (`rf-badge`/`rf-chip`/`rf-vh`, макросы Task 5,
 * `work_card.html.twig`) и на контейнерные запросы — `@layer components`,
 * тем же основанием, что топбар/подвал Task 10: НОВАЯ разметка задачи
 * (класс переименован целиком, `.card` → `.rf-card`), заводится сразу
 * правильно, а не переносится в слой отдельной задачей потом.
 *
 * ГЕОМЕТРИЯ — КОНТЕЙНЕРНАЯ, НЕ ПЛОТНОСТНАЯ. До этой задачи `.card.mid`/
 * `.card.min` несли СВОЙ фиксированный padding — то есть density (сколько
 * данных показываем) заодно решала И геометрию (сколько воздуха), хотя
 * одна и та же full-плотная карточка в контейнере 320px и в контейнере
 * 900px нуждается в разном отступе НЕЗАВИСИМО от того, сколько в ней
 * данных. `--rf-card-pad` и раскладка `.rf-card__meta` ниже реагируют на
 * `@container` — ширину БЛИЖАЙШЕГО предка с `container-type: inline-size`
 * (`.feed`/`.cards`/`.rf-feed`/`.rf-home__feed`/`.rf-home__continue-item`/
 * `.rf-sections__feed` — в соответствующих секциях файла), а не на
 * density-класс и не на ширину
 * окна: карточка живёт в шести местах
 * (лента фандома, поиск, страница автора, закладки, главная, сборники), и
 * то, что видно в окне браузера, ничего не говорит о том, сколько места
 * реально досталось карточке в узкой колонке — там медиа-запрос солгал бы.
 * Плотность (`.rf-card--mid`/`--min`) остаётся СМЫСЛОВОЙ: она по-прежнему
 * решает, сколько данных показывать (см. Twig-условия `work_card.html.twig`),
 * просто больше не решает заодно и отступ.
 */
@layer components {

.rf-card {
    --rf-card-pad: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    padding: var(--rf-card-pad);
    transition: border-color var(--dur-fast);
}

.rf-card:hover { border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); }

/* Тесный контейнер (боковая колонка, узкая ячейка грида сборника) —
   компактный отступ; тот же исход, что раньше давали `.card.mid`/`.card.min`
   каждой своим числом, только теперь это решает РЕАЛЬНАЯ ширина места, а
   не то, сколько данных карточка показывает. */
@container (max-width: 26rem) {
    .rf-card { --rf-card-pad: var(--space-3); }
}

.rf-card__top {
    display: flex;
    gap: var(--space-2);
    align-items: center;
    flex-wrap: wrap;
    margin-block-end: var(--space-2);
}

.rf-card h3 {
    font-size: var(--fs-lg);
    font-weight: 600;
    letter-spacing: -.01em;
    /*
     * Ruling 2 брифа Task 11 (находка Task 8): у заголовка не было точки
     * переноса, если название — одно «слово» без пробелов (стресс-
     * фикстура `longTitle`, 120 символов): заголовок распирал карточку до
     * ~1200px внутри контейнера 640px вместо переноса по буквам (докблок
     * стресс-секции витрины).
     *
     * `overflow-wrap: anywhere`, А НЕ `word-break: break-all`: `anywhere`
     * ломает слово ТОЛЬКО когда больше некуда перенести — обычные
     * многословные названия по-прежнему переносятся по пробелам, как
     * раньше (`break-all` ломал бы их тоже, в любом тесном контейнере, без
     * надобности). И, В ОТЛИЧИЕ от устаревшего `overflow-wrap: break-word`,
     * `anywhere` учитывается при расчёте MIN-CONTENT — именно это
     * позволяет `.rf-card` (grid-элементу `.feed`) сжаться до ширины
     * контейнера вместо того, чтобы раздувать его: `break-word` не меняет
     * вклад слова в минимальную ширину, только его отображение внутри уже
     * широкой коробки.
     */
    overflow-wrap: anywhere;
}
/*
 * Цвет ссылки заголовка — ТЕПЕРЬ ЗДЕСЬ, ВНУТРИ СЛОЯ (Дизайн-спринт, Task
 * 13.5). До этой задачи `base.css` не был обёрнут в `@layer base`, и его
 * `a { color: var(--primary) }` был НЕслоёным — неслоёное правило бьёт
 * ЛЮБОЕ слоёное независимо от специфичности, поэтому правило ниже жило
 * СНАРУЖИ `@layer components` (за его закрытием, там же, где раньше стоял
 * `.rf-card .rf-icon`) и побеждало не порядком слоя, а тем же трюком —
 * специфичностью между двумя неслоёными правилами. Task 13.5 обернула
 * `base.css` в `@layer base`, который проигрывает `primitives`/`components`/
 * `pages` порядком (`assets/css/layers.css`) — переносить правило наружу
 * слоя больше незачем, `@layer components` и так побеждает `@layer base`.
 */
.rf-card h3 a { color: var(--text); text-decoration: none; }
.rf-card h3 a:hover { color: var(--primary); }

.rf-card__by { font-size: var(--fs-sm); color: var(--text-muted); }
/* `color: var(--primary)` СОВПАДАЕТ со значением `a` из `base.css` — до
 * Task 13.5 это было унылое совпадение (правило формально проигрывало
 * неслоёному `a`, но с тем же результатом); после обёртки `base.css` в
 * `@layer base` побеждает уже по-настоящему, порядком слоя. */
.rf-card__by a { color: var(--primary); text-decoration: none; }

.rf-card__fandoms { font-size: var(--fs-sm); color: var(--text-muted); margin-block: var(--space-1); }
.rf-card__fandoms b { color: var(--text); font-weight: 500; }

.rf-card__pairing { font-size: var(--fs-sm); color: var(--text-muted); margin-block-end: var(--space-1); }

.rf-card__summary {
    font-family: var(--font-read);
    font-size: var(--fs-base);
    color: var(--text-muted);
    margin-block: var(--space-2);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    line-height: var(--lh-loose);
    /* Тот же приём, что у `h3` выше (ruling 2, диагностика по ходу этой же
       задачи): `-webkit-box`/`line-clamp` — легаси-флексбокс WebKit, и без
       `overflow-wrap` его MIN-CONTENT считается по НЕПЕРЕНЕСЁННОЙ строке
       целиком, а не по самому длинному слову — это и раздувало
       `#card-density-demo` витрины до 465px в контейнере 350px даже у
       `demoCard` с обычным, коротким названием (докблок Task 8,
       фикс-раунд 2). */
    overflow-wrap: anywhere;
}

.rf-card__tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-block: var(--space-2) var(--space-3); }

.rf-card__meta {
    display: flex;
    gap: var(--space-4);
    font-size: var(--fs-xs);
    color: var(--text-muted);
    align-items: center;
    flex-wrap: wrap;
}

/*
 * Ruling брифа Task 11 (Step 4): просторный контейнер — мета-строка в две
 * колонки, а не один ряд, переносящийся некрасивой лесенкой на средней
 * ширине. 34rem — порог, начиная с которого четырём пунктам меты (слова/
 * главы/лайки/обновлено) хватает места на ДВЕ ровные колонки вместо
 * переноса «в час по чайной ложке».
 */
@container (min-width: 34rem) {
    .rf-card__meta {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--space-1) var(--space-4);
    }
}

.rf-card__meta-likes { color: var(--spark); }

/*
 * Метка чтения (Sprint 3.1, Task 5; SOC-5) — «я это уже читал», переехала
 * сюда из общей секции бейджей (Task 11): card-специфична, единственный
 * потребитель — `.rf-card__top` выше.
 *
 * Форма НЕ такая, как у рейтинга: пунктирная рамка вместо заливки. Метка
 * говорит о ЧИТАТЕЛЕ, а не о работе, и выглядеть должна как пометка на
 * полях, а не как ещё одно свойство текста, которое автор ей приписал.
 *
 * Цвета — только токены (`tokens.css`): своя краска здесь означала бы
 * четвёртую тему, невидимую для машинной проверки контраста
 * (`apps/shared/ui/contrast.test.ts` читает токены, а не этот файл).
 * «Прочитано» — зелёным полюсом рейтинговой палитры, «читаю» — акцентным
 * `--spark` (то же, чем помечена работа «в процессе»), «потом» и «брошено»
 * приглушены: одно ещё не начато, другое уже закончилось.
 */
.rf-read-badge {
    font-size: var(--fs-xs);
    font-weight: 600;
    border-radius: var(--space-2);
    padding: var(--space-1) var(--space-2);
    letter-spacing: .02em;
    display: inline-block;
    border: 1px dashed var(--border);
    color: var(--text-muted);
}

.rf-read-badge--read { color: var(--rating-g); border-color: color-mix(in srgb, var(--rating-g) 45%, transparent); }
.rf-read-badge--reading { color: var(--spark); border-color: color-mix(in srgb, var(--spark) 45%, transparent); }
.rf-read-badge--later,
.rf-read-badge--dropped { color: var(--text-muted); }

/* средняя плотность — выдача поиска: без аннотации (Twig её рисует и в
 * mid — то же HTML во всех плотностях, докблок `work_card.html.twig`/
 * `WorkCardComponentTest`; прячет её именно этот стиль, а не отсутствие в
 * разметке). Тегов в mid нет вовсе (`density == 'full'` условие в шаблоне)
 * — второго правила под `.rf-card--mid .rf-card__tags` не заводим, чтобы
 * не оставлять недостижимую строку (тот же принцип, что убрал мёртвый
 * `.card.mid .tags` до этой задачи). */
.rf-card--mid .rf-card__summary { display: none; }

/*
 * Компакт — списки чтения: строка, а не колонка. Структурная раскладка
 * density (min — всегда «в одну строку», сколько бы места вокруг ни
 * было), а не геометрия — контейнеру здесь решать нечего, поэтому
 * правило не под `@container`.
 *
 * НАХОДКА ГЕЙТА ПРОКРУТКИ (дизайн-спринт, Task 11, обязательная уборка за
 * задачей 8): `flex-wrap: wrap` — без него `#card-density-demo` на 390px
 * снова красит `no-horizontal-scroll.spec.ts`, хотя оба ранее найденных
 * дефекта (перенос заголовка, обрезка фандомов) уже починены. Причина —
 * ТРЕТЬЯ, до этой находки не описанная: `work_card.html.twig` не прячет
 * `.rf-card__top`/`h3`/`.rf-card__by`/`.rf-card__fandoms`/`.rf-card__meta`
 * в плотности `min` (условны только `.rf-card__summary`/`.rf-card__tags`/
 * закладка/меню, докблок компонента) — пять блочных детей в один
 * НЕпереносимый flex-ряд без wrap суммируют свой min-content (замерено:
 * 400.6px у `demoCard` против 350px контейнера на 390px viewport,
 * `card.style.width='min-content'` в изоляции). Само содержимое `min`
 * плотности — не в файловом списке этой задачи (ruling контроллера
 * ограничил её сворачиванием закладки/обрезкой/переносом, не
 * редизайном density-логики), поэтому чинится ГЕОМЕТРИЯ ряда, а не состав
 * содержимого: `flex-wrap: wrap` даёт ряду деградировать в столбец, когда
 * не помещается, вместо того чтобы распирать карточку. На ширинах, где
 * пять пунктов помещаются в строку (типичный случай), перенос не
 * срабатывает, и раскладка не отличается от прежней ни на пиксель —
 * проверено тем же способом (`getBoundingClientRect()` при 640/900px).
 */
.rf-card--min {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3);
}

/*
 * `.continue-item`/`.continue-item .rf-card--min`/`.continue-progress`
 * (Sprint 3.1, Task 17, RDR-5) БЫЛИ ЗДЕСЬ — блок «Читать дальше» на
 * главной. Дизайн-спринт, Task 16 перевела главную на `rf-*` и УНЕСЛА их
 * в секцию «главная» ниже по файлу под именами
 * `.rf-home__continue-item`/`.rf-home__progress`; отступ между карточкой и
 * строкой позиции при этом переехал с `margin-block-end` карточки на
 * `margin-block-start` самой строки — одно правило вместо двух, без
 * потомкового селектора.
 *
 * Это ПЕРЕИМЕНОВАНИЕ, а не алиас (урок Task 3): у старых классов был
 * ровно один потребитель — `templates/home/index.html.twig`, — и он
 * переписан в том же коммите, поэтому правила удалены здесь, а не
 * оставлены рядом с новыми.
 */

}

/*
 * `.rf-card h3 a` (Task 11) И `.rf-card .rf-icon` (Task 13) — были ЗДЕСЬ,
 * НАРОЧНО СНАРУЖИ `@layer components`, двумя точечными заглушками одной
 * семьи багов: `base.css` до Дизайн-спринта, Task 13.5 не был обёрнут ни в
 * какой слой, и его неслоёные `a { color: var(--primary) }` и `img, svg {
 * display: block }` били ЛЮБОЕ слоёное правило независимо от специфичности
 * — заголовок карточки красился акцентным всегда, а иконка сердца/значка
 * рендерилась `display: block` и ломала строку с числом рядом.
 *
 * Task 13.5 вылечила корень (докблок `base.css` объясняет решение целиком):
 * файл обёрнут в `@layer base`, который по объявлению
 * (`assets/css/layers.css`) проигрывает и `primitives`, и `components`
 * порядком слоя, а не специфичностью. Обе заглушки СНЯТЫ:
 *
 * — `.rf-card h3 a`/`:hover` переехала ВНУТРЬ слоя, к `.rf-card h3` (см.
 *   выше по файлу) — там же, где ей и место в обычном каскаде.
 * — `.rf-card .rf-icon { display: inline-block; }` УДАЛЕНА целиком, а не
 *   перенесена: сам примитив `.rf-icon { display: inline-block; }` (`ui.css`,
 *   `@layer primitives`) теперь и без локальной копии честно побеждает
 *   `@layer base` порядком слоя — точечное правило стало избыточным
 *   дублем, а не просто неверно расположенным. Тот же диагноз касался
 *   `.rf-topbar__link`/`.rf-footer__brand`/`.rf-menu__links a` (Task 10) и
 *   `.rf-hero__crumbs a`/`.rf-filterbar__sort a`/`.rf-catalog__link`/
 *   `.rf-filter-chip` (Task 12, блок ниже) — ни один из них не был точечно
 *   пропатчен, все они были просто СЛОМАНЫ (топбар/подвал/меню красились
 *   акцентным вместо приглушённого) до Task 13.5 и заработали as-is одной
 *   правкой корня, без отдельного вмешательства (измерено `getComputedStyle()`
 *   на живом дев-стенде, отчёт Task 13.5). Про `.rf-footer__brand` это
 *   правда только наполовину: цвет из `base.css` ушёл, а под ним остался
 *   второй конфликт — правило ссылок колонки перебивало лого
 *   специфичностью; разбор и починка — докблок у `.rf-footer__column a`.
 */

/*
 * ── лента фандома + каталог (Дизайн-спринт, Task 12) ───────────────────
 *
 * `.rf-hero`/`.rf-filterbar`/`.rf-feed`/`.rf-catalog*` — НОВЫЕ примитивы,
 * потребители — ТОЛЬКО `templates/catalog/fandom.html.twig` и
 * `catalog/index.html.twig` (файловый список этой задачи). Это НЕ
 * переименование `.hero`/`.crumbs`/`.page-shell`/`.subfandoms`/
 * `.category`/`.feed`/`.more`/`.empty` из блока «страница фандома» ниже —
 * те правила остаются как есть, потому что их держат ещё ~28 других
 * шаблонов (поиск, автор, читалка, вход, гейты, подписки, сборники,
 * уведомления и другие — `grep -rn 'class="hero"' templates/`), которых
 * файловый список Task 12 не касается. Три задачи спринта явно называют
 * «hero Task 12» своим потребителем (Task 14 — шапка работы, Task 15 —
 * поиск, Task 19 — автор): каждая мигрирует СВОЙ шаблон на `.rf-hero` в
 * СВОЕЙ задаче, и только когда мигрирует последняя, старый блок ниже
 * становится мёртвым кодом, который снимет финальное ревью (тот же
 * порядок, что уже прошли шапка/подвал и карточка работы — Task 10/11).
 *
 * ЭТО НЕ АЛИАС (урок Task 3: алиас `.old, .new { … }` протекает на
 * неперерисованные страницы — правка нового класса тогда молча красит и
 * старые). Ни одно правило ниже не объединяет `.hero` и `.rf-hero` в один
 * селектор. Значения на первый взгляд дублируют старый блок — это
 * ВРЕМЕННОЕ и ожидаемое следствие миграции по одной странице за раз, а не
 * недосмотр; дубликат исчезнет вместе с последним потребителем `.hero`.
 *
 * ПОТОЛОК `no-magic-space.test.ts` ЭТОЙ ЗАДАЧЕЙ НЕ ОПУЩЕН (докблок
 * теста объясняет почему): всё ниже сразу на шкале (`--space-*`/`--fs-*`),
 * ноль новых магических пикселей, а снять хоть один физический px из
 * старого блока «страница фандома» нельзя ровно по причине выше — его
 * держат страницы, которых нет в файловом списке этой задачи.
 */
@layer components {

.rf-hero {
    background-image: var(--hero-glow);
    border-block-end: 1px solid var(--border);
    padding-block: var(--space-7) var(--space-6);
    padding-inline: var(--space-4);
}

@media (min-width: 800px) {
    .rf-hero { padding-block: var(--space-8) var(--space-7); }
}

.rf-hero__in { max-inline-size: 860px; margin-inline: auto; }

.rf-hero__crumbs {
    margin: 0 0 var(--space-2);
    font-size: var(--fs-xs);
    color: var(--text-muted);
}

/* Цвет ссылки — снова здесь (Task 13.5): раньше жил неслоёным правилом за
   закрытием `@layer components` (та же причина, что у `.rf-card h3 a`,
   докблок выше по файлу) — теперь `base.css` сам в `@layer base` и слой
   `components` бьёт его порядком, переносить цвет наружу больше незачем. */
.rf-hero__crumbs a { text-decoration: none; color: var(--text-muted); }
.rf-hero__crumbs a:hover { color: var(--primary); }

.rf-hero__title {
    font-size: clamp(1.6rem, 5vw, 2.4rem);
    font-weight: 700;
    letter-spacing: -.03em;
    line-height: 1.15;
}

.rf-hero__orig {
    display: inline-block;
    margin-inline-start: var(--space-2);
    font-size: .55em;
    font-weight: 400;
    letter-spacing: 0;
    color: var(--text-muted);
}

.rf-hero__description {
    max-inline-size: 56ch;
    margin-block: var(--space-3) var(--space-4);
    font-family: var(--font-read);
    font-size: var(--fs-base);
    color: var(--text-muted);
}

.rf-hero__stats {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.rf-hero__stats b { color: var(--text); }

.rf-hero__subfandoms {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-start: var(--space-4);
}

/*
 * Фильтр-бар (Task 12) — ССЫЛКИ В `/search`, НЕ ФИЛЬТРАЦИЯ НА МЕСТЕ.
 * Рулинг контроллера спринта (докблок `fandom.html.twig` объясняет
 * подробно): `FandomController::show()` не разбирает ни одного параметра
 * фильтра, кроме курсора, и рисовать здесь РАБОТАЮЩИЕ чипы значило бы
 * новую бэкенд-функциональность в задаче, обязанной её не менять (три
 * названных исключения спринта — это стало бы четвёртым). Секция ниже —
 * только раскладка строки чипов и сортировки; сами чипы печатает
 * `ui.filter_chip()` (Task 4/5, `.rf-filter-chip`, `@layer primitives`) —
 * этот раздел не заводит для них никакого нового класса.
 */
.rf-filterbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    max-inline-size: 860px;
    margin-inline: auto;
    margin-block-start: var(--space-5);
    padding-inline: var(--space-4);
}

.rf-filterbar__chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.rf-filterbar__sort {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-inline-start: auto;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

/* Цвет ссылки — снова здесь (Task 13.5), та же причина, что у
   `.rf-hero__crumbs a` выше. */
.rf-filterbar__sort a { text-decoration: none; color: var(--text-muted); }
.rf-filterbar__sort a:hover { color: var(--primary); }

.rf-feed {
    container-type: inline-size;
    max-inline-size: 860px;
    margin-inline: auto;
    padding-block: var(--space-6) var(--space-8);
    padding-inline: var(--space-4);
    display: grid;
    gap: var(--space-4);
}

/*
 * «ПОКАЗАТЬ ЕЩЁ» — ОДНО РЕШЕНИЕ НА ТРИ ЛЕНТЫ (финальный фикс-раунд по
 * сквозному ревью, I-5).
 *
 * Здесь стоял `.rf-feed__more { justify-self: center; margin-block-start:
 * var(--space-2) }` — класс на самой кнопке ленты фандома и поиска, — а
 * ниже по файлу жил `.rf-sections__more` — абзац-обёртка вне решётки на
 * странице автора. Два механизма и два ритма (16px гриды + 8px = 24px
 * против 16px маргина) для одного действия на трёх соседних лентах, все три
 * перестроены этим спринтом. Оба имени сняты: разметка носит примитив
 * `rf-loadmore` (`assets/css/ui.css`, центрирование и место под живую
 * строку статуса), а страница ставит ЕМУ ТОЛЬКО МЕСТО — шов §3.1.
 *
 * Ритм считается одинаково у обеих, потому что предок у них одной формы:
 * обе ленты — грид с `gap: var(--space-4)`, и `--space-2` добавляется к
 * зазору, а не заменяет его. 16 + 8 = 24px в обоих случаях — то есть кнопка
 * стоит чуть дальше от карточек, чем карточки друг от друга, и это
 * единственное, что правило говорит.
 */
.rf-feed > .rf-loadmore,
.rf-sections__feed > .rf-loadmore { margin-block-start: var(--space-2); }

/* ── каталог фандомов (Task 12) ──────────────────────────────────────── */

.rf-catalog {
    max-inline-size: 860px;
    margin-inline: auto;
    padding-block: var(--space-6) var(--space-8);
    padding-inline: var(--space-4);
}

.rf-catalog__category { padding-block: var(--space-5); border-block-end: 1px solid var(--border); }
.rf-catalog__category:last-child { border-block-end: none; }

.rf-catalog__category-title { margin: 0 0 var(--space-3); font-size: var(--fs-lg); font-weight: 600; }

.rf-catalog__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-5);
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Цвет ссылки — снова здесь (Task 13.5), та же причина, что у
   `.rf-hero__crumbs a` выше. */
.rf-catalog__link { text-decoration: none; font-size: var(--fs-base); color: var(--text-muted); }
.rf-catalog__link:hover { color: var(--primary); }
.rf-catalog__count { margin-inline-start: var(--space-1); color: var(--text-muted); font-size: var(--fs-xs); }

}

/*
 * Цвета ссылок крошек/сортировки/каталога, НАРОЧНО СНАРУЖИ `@layer
 * components`, БЫЛИ ЗДЕСЬ (Task 12) — сняты Task 13.5, объединены назад с
 * `text-decoration` внутри слоя (см. `.rf-hero__crumbs a`/`.rf-filterbar__sort
 * a`/`.rf-catalog__link` выше по файлу): `base.css` теперь в `@layer base`,
 * и `@layer components` бьёт его порядком, специфичность снаружи слоя
 * решать больше нечего.
 *
 * Переопределение `.rf-hero .rf-filter-chip`/`.rf-filterbar .rf-filter-chip`
 * УДАЛЕНО целиком, а не перенесено: значения (`--text-muted`/`--primary` на
 * hover) СОВПАДАЛИ со значениями самого примитива `.rf-filter-chip` (`ui.css`,
 * `@layer primitives`) один в один — правило существовало только чтобы быть
 * неслоёным и обыграть неслоёный `a` из `base.css`; теперь примитив бьёт
 * `@layer base` порядком слоя сам, без дубля здесь.
 */

/*
 * ── шапка работы и обсуждение (Дизайн-спринт, Task 14) ─────────────────
 *
 * Продолжает `.rf-hero`/`.rf-hero__in`/`.rf-hero__crumbs`/`.rf-hero__title`/
 * `.rf-hero__description`/`.rf-hero__stats` (Task 12) — эта задача названа
 * потребителем «hero Task 12» прямо в докблоке выше. Ниже — ТОЛЬКО узлы,
 * которых у фандома/каталога не было: ряд бейджей, атрибуция, серия,
 * предупреждения, оглавление, панель действий.
 *
 * `.tag-groups`/`.subfandoms`/`.pairings`/`.characters`/`.chip-menu`/
 * `.chip` СОЗНАТЕЛЬНО не переехали на `rf-*` этой задачей (докблок
 * `works/show.html.twig` объясняет почему — их проверяют буквально по
 * классу `WorkPairingsTest`/`SubscribeButtonsTest`) — их CSS остаётся ЗА
 * старым блоком «страница фандома» ниже по файлу, здесь для них нет ни
 * одного нового правила.
 */
@layer components {

/*
 * Ряд бейджей (рейтинг/направленность/статус/кроссовер/перевод) — ПЕРВЫМ
 * содержательным блоком шапки после крошек (DESIGN_BRIEF §4.4: «рейтинг и
 * предупреждения считываются мгновенно»). Сами бейджи красит `.rf-badge*`
 * (`ui.css`, `@layer primitives`) — здесь только раскладка строки.
 */
.rf-hero__badges {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-block: var(--space-3);
}

/*
 * Атрибуция «Автор: … · Бета: …» (WRK-22) — вторым планом относительно
 * рейтинга и h1: приглушённый текст, акцентные ссылки на профиль.
 *
 * ОДИН ВИД, ДВА ИМЕНИ — И ЭТО ГРУППИРОВКА, А НЕ КОПИЯ (фикс-раунд 1, M-7).
 * `.rf-profile__handle` (ник страницы автора, «страница автора и серия»
 * ниже по файлу) до этой правки объявлял те же три свойства под своим
 * именем — та же роль «мелкая приглушённая строка», найденная копией
 * ПОСЛЕ того, как этот блок уже был написан, потому что копия завелась не
 * переименованием, а вторым `style=""`-в-CSS с нуля. Раздельные правила
 * успели разойтись бы на первой же правке кегля или цвета — ровно та
 * ловушка, которую спек §4.1 и эта секция уже переживали (см.
 * `.rf-badge--direction, .rf-badge--neutral`, `ui.css`). Ссылки на профиль внутри
 * ника не бывает (`@{{ profile.username }}` — простой текст,
 * `templates/users/author.html.twig`), поэтому `a`/`a:hover` ниже
 * группировку не задевают: они остаются только у байлайна работы.
 * Собственное поведение ника (`overflow-wrap: anywhere` — длинный ник без
 * пробелов не должен распирать колонку) осталось ЕГО правилом на месте
 * `.rf-profile__handle`, а не переехало сюда: оно не часть общего вида,
 * это его личный контракт переноса.
 */
.rf-work-byline,
.rf-profile__handle {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.rf-work-byline a { color: var(--primary); text-decoration: none; }
.rf-work-byline a:hover { text-decoration: underline; }

/* Строка(и) серии («Часть N из M») — та же приглушённая роль, что
   атрибуция: несколько серий возможны (WRK-18), каждая своей строкой. */
.rf-work-series {
    margin-block-start: var(--space-1);
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.rf-work-series p { margin: 0; }
.rf-work-series a { color: var(--primary); text-decoration: none; }
.rf-work-series a:hover { text-decoration: underline; }

/*
 * Предупреждения (`.rf-warnings`, было `.warnings`) — свёрнутый `<details>`
 * ВЫШЕ меты, а не ниже (DESIGN_BRIEF §4.4, бриф задачи): `summary` несёт
 * иконку `warning` и число крупнее и заметнее, чем приглушённая мета-
 * строка ниже по документу, — вместе с рейтингом это ровно то, ради чего
 * взгляд скользит по шапке.
 *
 * Переименование `.warnings` → `.rf-warnings` — ИСКЛЮЧИТЕЛЬНО этой
 * странице принадлежавший класс (`grep -rn 'class="warnings"' templates/`
 * — только здесь), поэтому, в отличие от `.tag-groups`/`.pairings`/
 * `.chip-menu` выше, миграция безопасна: ни один шаблон вне списка задачи
 * его не держит. `print.css` (список `break-inside: avoid`) обновлён тем
 * же коммитом — единственный внешний потребитель имени.
 */
.rf-warnings {
    margin-block: var(--space-4);
}

.rf-warnings > summary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-block-size: var(--rf-tap);
    cursor: pointer;
    list-style: none;
    font-weight: 600;
    font-size: var(--fs-base);
    color: var(--rating-r);
}

.rf-warnings > summary::-webkit-details-marker { display: none; }

.rf-warnings__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-start: var(--space-2);
}

/*
 * Панель действий (`.rf-actionbar`, интерфейс задачи) — ПОСЛЕДНИЙ блок
 * шапки, тем же принципом, что вся страница: числа (мета-строка выше)
 * читаются раньше кнопок, кнопки — это действие, не информация. Одна
 * строка на широком экране, перенос на узком (`flex-wrap`) — та же мера
 * колонки 860px, что у всей шапки (`.rf-hero__in`), собственной ширины
 * панель не заводит.
 */
.rf-actionbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-block-start: var(--space-5);
}

/* ── оглавление (`.rf-toc`, интерфейс задачи) ──────────────────────────
 *
 * Заменяет `.category ul`/`.count` ТОЛЬКО на этой странице: та разметка —
 * инлайн-ряд плоских тег-ссылок (флекс-ряд, `gap`), а оглавление — список
 * СТРОК с несколькими полями на строку (✓, заголовок, дата, слова,
 * минуты) — разная форма данных, а не косметика одной. `.category`
 * (заголовок секции, разделительная линия) заменён на `.rf-work-section`
 * ниже по той же причине, что и везде в этом файле: `.category` держат
 * ~10 других шаблонов (автор, тег, серия, сборник), не входящих в список
 * этой задачи.
 */
.rf-work-section {
    padding-block: var(--space-6);
    border-block-end: 1px solid var(--border);
}

.rf-work-section:last-child { border-block-end: none; }

.rf-work-section h2 {
    margin: 0 0 var(--space-3);
    font-size: var(--fs-lg);
    font-weight: 600;
}

.rf-toc {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: 0;
    padding: 0;
    list-style: none;
}

.rf-toc__item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    padding-block: var(--space-2);
    border-block-end: 1px solid var(--border);
}

.rf-toc__item:last-child { border-block-end: none; }

.rf-toc__link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-block-size: var(--rf-tap);
    color: var(--text);
    text-decoration: none;
    font-weight: 500;
}

.rf-toc__link:hover { color: var(--primary); }

.rf-toc__check { display: inline-flex; color: var(--rating-g); }

.rf-toc__meta { font-size: var(--fs-xs); color: var(--text-muted); }

/*
 * `.rf-thread` (интерфейс задачи — «ветка обсуждения») — ДОБАВЛЕН к
 * прежнему `.comments` (докблок `comments.html.twig`: `social.js` находит
 * контейнер по старому классу, новый ничего не заменяет). Единственная
 * правка раскладки — верхний отступ на шкале вместо магического
 * `padding-top: 4px` из старого `.comments` (тот остаётся как есть — его
 * держит тизер обсуждения читалки, `chapter.html.twig`, вне списка этой
 * задачи): `.rf-thread` объявлен позже по каскаду и побеждает порядком,
 * не специфичностью.
 */
.rf-thread {
    padding-block-start: var(--space-2);
}

}

/*
 * ── поиск и фасеты (Дизайн-спринт, Task 15) ────────────────────────────
 *
 * Продолжает `.rf-hero`/`.rf-hero__in`/`.rf-hero__crumbs`/`.rf-hero__title`/
 * `.rf-hero__description` (Task 12) и `.rf-feed` (вместе с местом для
 * `.rf-loadmore` в нём) — страница поиска названа их потребителем в
 * докблоке выше по файлу. Ниже — ТОЛЬКО
 * узлы, которых у ленты фандома и шапки работы не было: строка поиска, ряд
 * фасетов, чипы снятых фильтров, счётчики выдачи и редакционный блок.
 *
 * ЗДЕСЬ ТОЛЬКО РАСКЛАДКА И МЕСТО. Ни одно правило ниже не переобъявляет
 * свойств примитива (цвет, радиус, рамку, отступ, кегль элемента `rf-*`):
 * `app.css` ещё не обёрнут в каскадный слой целиком, поэтому его правило
 * побеждает `@layer primitives` НАСОВСЕМ и на всех страницах — то есть
 * «поправить чип на поиске» здесь означало бы поправить его везде. Всё,
 * что относится к виду самого примитива (`.rf-facet*`, `.rf-segmented
 * label`, `.rf-field__input--lg`, ссылочный `.rf-chip`), уехало в
 * `assets/css/ui.css` этой же задачей.
 */
@layer components {

/*
 * Строка поиска — ОДНА ФОРМА НА СТРАНИЦУ (инвентарь §7), поэтому в ней
 * лежит всё: запрос, «Искать», сортировка, ряд фасетов и «Применить
 * фильтры». Вертикальная стопка на любой ширине: на 390px ряд из
 * восьми чипов фасетов и так занимает несколько строк, а на 1200
 * колонка hero всё равно ограничена 860px (`.rf-hero__in`).
 */
.rf-searchbar {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.rf-searchbar__row { display: flex; gap: var(--space-2); }

/* Поле тянется, кнопка держит свою ширину. `min-inline-size: 0` — против
   неубираемого минимума flex-элемента: без него длинный `value` в поле
   распирал бы строку шире экрана (FE-3). */
.rf-searchbar__row .rf-field__input { flex: 1 1 auto; min-inline-size: 0; }

.rf-searchbar__mods {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-4);
    font-size: var(--fs-xs);
    color: var(--text-muted);
}

.rf-searchbar__mod {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

/* `<select>` сортировки несёт `.rf-field__input` (как `Select.tsx`), а тот
   растягивается на всю ширину — верно для поля в столбце формы, неверно
   для контрола в строке модификаторов. Ширина — раскладка, ею странице
   распоряжаться можно; вид селекта остаётся за примитивом. */
.rf-searchbar__mod select { inline-size: auto; }

/*
 * Ряд фасетов. Сами чипы и их листы печатает `ui.facet()` и красит
 * `.rf-facet*` (`ui.css`) — здесь только перенос по строкам: на 390px
 * восемь чипов встают в четыре ряда, и это единственное, что должна знать
 * страница.
 */
.rf-facets {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

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

/* «Сбросить всё» — приглушённая ссылка рядом с кнопкой и в строке чипов
   (инвентарь §7 требует её в обоих местах, и расходиться им незачем). */
.rf-reset { font-size: var(--fs-sm); color: var(--text-muted); }
.rf-reset:hover { color: var(--primary); }

/*
 * Колонка над выдачей: плашки подмен, чипы снятых фильтров, «Найдено: N»,
 * «Возможно, вы искали». Ширина и отступы — те же, что у `.rf-feed` ниже,
 * но это ОТДЕЛЬНЫЙ блок, а не вложение: `.rf-feed` несёт
 * `container-type: inline-size` для карточек, и лишний контейнер внутри
 * него сломал бы их контейнерные запросы.
 */
.rf-search {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    max-inline-size: 860px;
    margin-inline: auto;
    padding-block-start: var(--space-5);
    padding-inline: var(--space-4);
}

.rf-search__found { font-size: var(--fs-sm); color: var(--text-muted); }
.rf-search__suggest { font-size: var(--fs-base); }

.rf-active-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.rf-active-filters__title { font-size: var(--fs-xs); color: var(--text-muted); }

.rf-editorial { display: flex; flex-direction: column; gap: var(--space-2); }

.rf-editorial__title {
    margin-block-start: var(--space-2);
    font-size: var(--fs-lg);
    font-weight: 600;
}

.rf-editorial__chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.rf-editorial__note { font-size: var(--fs-xs); color: var(--text-muted); }

}

/*
 * ── главная (Дизайн-спринт, Task 16) ───────────────────────────────────
 *
 * Продолжает `.rf-hero`/`.rf-hero__in`/`.rf-hero__description` (Task 12) и
 * `.rf-searchbar`/`.rf-searchbar__row` (Task 15) — главная названа их
 * ВТОРЫМ потребителем, а не автором второго определения того же узла:
 * hero-lite анонима — это тот же hero, а крупное поле в нём — та же строка
 * поиска, что на `/search`, только без сортировки и фасетов. Ниже — ТОЛЬКО
 * узлы, которых ни у одной прежней страницы не было: разрезанный на знак и
 * строку заголовок, колонка секций витрины и блок «Читать дальше».
 *
 * ЗДЕСЬ ТОЛЬКО РАСКЛАДКА И МЕСТО — то же ограничение и по той же причине,
 * что у секции поиска выше: `app.css` не обёрнут в каскадный слой целиком,
 * его правило побеждает `@layer primitives` НАСОВСЕМ и на всех страницах, и
 * «поправить чип на главной» здесь означало бы поправить его везде.
 * Единственный цвет ниже — у `.rf-home__mark-accent`/`.rf-home__tagline`/
 * `.rf-home__note`, и все три стоят на СВОИХ узлах страницы, а не на
 * элементе `rf-*` из `ui.css`.
 *
 * НИ ОДНОГО `filter`/`backdrop-filter`/`transform` (урок Task 15.6): любой
 * такой предок становится containing block для `position: fixed` потомков,
 * а на главной вошедшего рисуется до девяти листов `.rf-sheet-host`
 * (закладка и «⋯» в каждой карточке `full`) — панель уехала бы считать свой
 * `inset` от секции вместо окна, ровно как уехало меню аккаунта под
 * размытой шапкой.
 *
 * ПОТОЛОК `no-magic-space.test.ts` НЕ ОПУЩЕН: 146 → 146, измерено
 * `magicSpaceCount()` до и после задачи, разница ноль, оставлен как есть
 * НАРОЧНО. Новая секция целиком на шкале (`--space-*`/`--fs-*`) и не
 * добавила ни одного магического пикселя — но это ожидаемо и само по себе
 * отсутствие снижения не объясняет. Причина другая: снять с главной
 * старый словарь и НЕ УДАЛИТЬ его правила — единственное, что здесь можно
 * было сделать. `.category`/`.category h2`/`.category ul` (4 магических
 * пикселя: 22, 10, 8, 18), `.count`, `.more`, `.page-shell` держат ещё
 * десяток шаблонов — прозрачность, гейты, вход, настройки, удаление
 * аккаунта (`grep -rn 'class="category"' templates/`), и удалить их этой
 * задачей значило бы менять вид чужих экранов без их ревью (урок Task 3).
 * Единственные два правила, которые ЭТА страница держала одна
 * (`.continue-item`/`.continue-progress`), переименованы в
 * `.rf-home__continue-item`/`.rf-home__progress` — но их Task 11 уже
 * перевела на шкалу целиком, и магических пикселей в них не было.
 */
@layer components {

/*
 * ЗНАК И СТРОКА — ОДИН `h1`, разрезанный на две части (докблок
 * `templates/home/index.html.twig` объясняет, зачем именно так). Кегль,
 * начертание И МЕЖСТРОЧНОЕ повторяют `.rf-hero__title` (выше в этом же
 * файле, тот же слой), но правило СВОЁ, а не второй класс на том же узле:
 * у заголовка главной есть то, чего у остальных hero'ев нет — внутренняя
 * раскладка из двух элементов с разным кеглем, и `display: flex` на общем
 * `.rf-hero__title` задел бы шапку работы и ленту фандома, где заголовок —
 * простой текст.
 *
 * `line-height: 1.15` — ЛИТЕРАЛОМ, а не `var(--lh-tight)` (фикс-раунд 1,
 * Minor 1): токен даёт 1.25, и первая редакция задачи взяла его «как
 * ближайший», отчего знак главной оказался на 0.1 свободнее всех остальных
 * hero-заголовков площадки и разошёлся со своим же прототипом
 * (`design/prototypes/07-home.html`), где стоит 1.15. Число обязано
 * совпадать с `.rf-hero__title`, а не с ближайшим токеном; перевод ОБОИХ
 * правил на новый токен «плотного заголовка» — правка чужого класса, то
 * есть отдельная задача.
 *
 * `align-items: baseline` — знак и строка сидят на одной базовой линии,
 * пока помещаются в строку; `flex-wrap` уводит строку на вторую при
 * нехватке места (на 390px это происходит всегда).
 */
.rf-home__title {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    font-size: clamp(1.6rem, 5vw, 2.4rem);
    font-weight: 700;
    letter-spacing: -.03em;
    line-height: 1.15;
}

.rf-home__mark { letter-spacing: -.04em; }

/* «fic» — акцентом, «Ever» — обычным текстом: тот же канон, что у знака в
   топбаре (`.rf-topbar__brand-accent`), одним оттенком и одним правилом
   деления слова, а не второй трактовкой логотипа на главной странице. */
.rf-home__mark-accent { color: var(--primary); }

/* Кегль строки — В `em`, а не в токене шкалы: она обязана оставаться
   вдвое мельче ЗНАКА, а тот сам меняется по `clamp()` от ширины экрана.
   Фиксированное значение разошлось бы со знаком на каждой ширине, кроме
   одной. */
.rf-home__tagline {
    font-size: .5em;
    font-weight: 500;
    color: var(--text-muted);
}

/*
 * Колонка секций — 860px, ровно как лента фандома (`.rf-feed`) и выдача
 * поиска (`.rf-search`). Второй геометрии («две колонки на 1200») главная
 * НЕ заводит, и это замеренный выбор, а не экономия: карточка `full`
 * показывает мета-строку в два ряда от 34rem контейнера (`@container` в
 * секции карточки выше), а в ячейке двухколоночного грида на 1200 ей
 * досталось бы ~26rem — то есть широкий экран дал бы карточку БЕДНЕЕ, чем
 * узкий, и вдобавок с компактным отступом `@container (max-width: 26rem)`.
 */
.rf-home {
    max-inline-size: 860px;
    margin-inline: auto;
    padding-block: var(--space-6) var(--space-8);
    padding-inline: var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-7);
}

.rf-home__section { display: flex; flex-direction: column; gap: var(--space-3); }

/*
 * Заголовок секции — крупнее заголовка карточки (`--fs-lg`), потому что у
 * вошенего он остаётся ЕДИНСТВЕННЫМ видимым заголовком страницы (`h1`
 * скрыт визуально, см. докблок шаблона), и совпадать по кеглю с названием
 * работы внутри ленты он не вправе: тогда структура страницы читалась бы
 * как одна плоская простыня.
 */
.rf-home__section-title {
    font-size: var(--fs-xl);
    font-weight: 600;
    letter-spacing: -.01em;
}

.rf-home__chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/*
 * Строка «дальше» под секцией — «Все фандомы» под чипами и «Всё свежее»/
 * «Всё популярное за месяц» под лентами (фикс-раунд 1: у витрины нет
 * курсора, и дверь дальше — ссылка на тот же запрос в поиске, докблок
 * шаблона).
 *
 * ТОЛЬКО КЕГЛЬ, БЕЗ ЦВЕТА (фикс-раунд 1, Minor 3): единственное содержимое
 * абзаца — ссылка, а она берёт свой цвет (`--primary`) и `color:
 * var(--text-muted)` здесь не красил НИЧЕГО. Замер ревью в трёх темах:
 * `#A78BFA`/`#6D53E0`/`#7457BF` — это `--primary`. Правильным оказалось
 * мёртвое объявление снять, а не красить ссылку: «дальше» — навигация, и
 * приглушать единственный переход со страницы значило бы прятать её.
 */
.rf-home__note { font-size: var(--fs-xs); }

/*
 * `container-type: inline-size` — на ОБЁРТКЕ ленты и на элементе «Читать
 * дальше», а не на `.rf-home`: карточка спрашивает ближайшего предка с
 * контейнером, и объяви его на всей колонке, обе ленты и блок прогресса
 * отвечали бы одним и тем же числом независимо от того, сколько места
 * реально досталось карточке (Task 11 завела это правило для шести мест
 * карточки — главная из них).
 */
.rf-home__feed {
    container-type: inline-size;
    display: grid;
    gap: var(--space-4);
}

.rf-home__continue { display: grid; gap: var(--space-3); }

.rf-home__continue-item { container-type: inline-size; }

/*
 * «Читать дальше»: строка позиции — ПОД карточкой, а не внутри неё
 * (`.rf-card--min` уже флекс-строка сама по себе, секция карточки выше), и
 * приглушённой подписью, а не акцентом: позиция — вспомогательная деталь
 * под уже понятной карточкой, а не то, ради чего строка читается первым
 * делом. Правило переехало сюда из `.continue-progress` вместе с
 * переименованием класса; `padding-inline` повторяет отступ карточки,
 * чтобы «глава N · M%» стояла под её текстом, а не под её рамкой.
 */
.rf-home__progress {
    margin: 0;
    margin-block-start: var(--space-1);
    padding-inline: var(--space-4);
    font-size: var(--fs-xs);
    color: var(--text-muted);
}

/*
 * ТЕСНЫЙ КОНТЕЙНЕР — ТОТ ЖЕ ОТСТУП, ЧТО У КАРТОЧКИ (фикс-раунд 1, Minor 5).
 * `--rf-card-pad` объявлен НА `.rf-card` и сестринскому `<p>` не
 * наследуется, поэтому «взять то же значение» нельзя — но можно задать
 * тот же вопрос ТОМУ ЖЕ контейнеру: `.rf-home__continue-item` несёт
 * `container-type: inline-size` (правило выше), и карточка внутри него
 * спрашивает про 26rem ровно его же (секция карточки в этом файле). Порог и
 * пара значений повторяют карточку НАРОЧНО — иначе строка «глава N · M%»
 * стоит на 4px правее текста над ней (замерено на 390: подпись 16px против
 * карточки 12px).
 */
@container (max-width: 26rem) {
    .rf-home__progress { padding-inline: var(--space-3); }
}

}

/* ── страница фандома ───────────────────────────────────────────────── */

/*
 * `--hero-glow` ПЕРЕЕХАЛ в `assets/css/tokens.css` (ревью Task 20) — как и
 * обещал прежний комментарий на этом месте («перенести в токены при следующем
 * обновлении палитры»). Обновление случилось: у палитры появилась третья тема,
 * а здешний блок знал только две и был прибит к `:root`, тогда как темы стали
 * применимыми к контейнеру. Свечение сепии и совместимость со скоупом — там же,
 * где остальные значения; здесь остаётся только ПОТРЕБИТЕЛЬ (`.hero` ниже).
 */

.page-shell { max-width: 860px; margin: 0 auto; padding: 0 16px; }

.hero {
    background-image: var(--hero-glow);
    border-bottom: 1px solid var(--border);
    padding: 36px 16px 24px;
}

.hero-in { max-width: 860px; margin: 0 auto; }

.crumbs { font-size: .75rem; color: var(--text-muted); margin-bottom: 10px; }
.crumbs a { text-decoration: none; color: var(--text-muted); }
.crumbs a:hover { color: var(--primary); }

.hero h1 {
    font-size: clamp(1.6rem, 5vw, 2.4rem);
    font-weight: 700;
    letter-spacing: -.03em;
    line-height: 1.15;
}

.hero .orig { color: var(--text-muted); font-weight: 400; font-size: .55em; letter-spacing: 0; }

.hero p {
    font-family: var(--font-read);
    color: var(--text-muted);
    max-width: 56ch;
    margin: 10px 0 16px;
    font-size: .95rem;
}

.hero .stats { display: flex; gap: 18px; flex-wrap: wrap; align-items: center; font-size: .85rem; color: var(--text-muted); }
.hero .stats b { color: var(--text); }

.subfandoms { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.subfandoms a { text-decoration: none; }

/*
 * `container-type: inline-size` (Дизайн-спринт, Task 11) — «лента фандома»
 * и «поиск» (оба места используют `.feed`) отдают карточке работы
 * (`.rf-card`, `app.css`) свою реальную ширину через `@container`, а не
 * через ширину окна: см. докблок секции «карточка работы».
 */
.feed { container-type: inline-size; display: grid; gap: 14px; padding: 24px 0 40px; }

.more {
    display: inline-block;
    margin: 4px 0 40px;
    padding: 10px 20px;
    border: 1px solid var(--border);
    border-radius: var(--r-chip);
    background: var(--surface);
    color: var(--text);
    text-decoration: none;
    font-size: .88rem;
}

.more:hover { border-color: var(--primary); color: var(--primary); }

.empty { color: var(--text-muted); font-size: .95rem; padding: 24px 0 40px; max-width: 56ch; }

/*
 * ── глобальная шапка ───────────────────────────────────────────────────
 * Структурная раскладка (`.site-head`/`.site-head-in`/`.site-nav a` —
 * бывший `.brand`) переехала в `.rf-topbar*` (Дизайн-спринт, Task 10,
 * раздел «топбар» ниже). `.site-search` держал позиционирование ТОЛЬКО
 * для нужд саджеста — эта роль перешла к `.rf-topbar__search`, а сам
 * класс `.site-search` остаётся на форме БЕЗ единой строки CSS: это
 * поведенческий хук `assets/js/suggest.js`
 * (`document.querySelector('.site-search')`, докблок файла), не
 * оформление, и трогать его нельзя — контракт скрипта этой задачей не
 * меняется.
 */

/* ── саджест (Sprint 1.2, Task 14, SRCH-11) ────────────────────────────── */

.site-search-suggest {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 20;
    margin: 4px 0 0;
    padding: 6px 0;
    max-height: 320px;
    overflow-y: auto;
    list-style: none;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow-2);
}

.site-search-suggest[hidden] { display: none; }

.site-search-suggest-label {
    padding: 4px 12px;
    font-size: .7rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--text-muted);
}

.site-search-suggest [role="option"] {
    padding: 6px 12px;
    font-size: .88rem;
    color: var(--text);
    cursor: pointer;
}

.site-search-suggest [role="option"].active,
.site-search-suggest [role="option"]:hover {
    background: var(--primary);
    color: var(--on-primary);
}

/*
 * ── старый словарь поиска: ПУСТО (Дизайн-спринт, Task 15 и Task 17) ────
 *
 * Здесь стояли свои поля и кнопки поиска (браузерные на тёмной теме дают
 * белые прямоугольники системного контрола) и вся панель фасетов. От
 * раздела не осталось ни одного правила: и `/search`, и онбординг печатают
 * `rf-*`. Заголовок оставлен вместе с обоими разборами ниже — они говорят,
 * ЧТО и ПОЧЕМУ отсюда снято, и следующий автор не заведёт третий словарь
 * на том же месте.
 *
 * `.site-search input[type="search"]`/`.site-search button` СЮДА НЕ
 * ВХОДИЛИ (Дизайн-спринт, Task 10) — урок Task 3 про алиасы старого
 * словаря повторялся буквально: `.site-search` остаётся на форме топбара
 * ТОЛЬКО как поведенческий хук `suggest.js` (докблок раздела «глобальная
 * шапка» выше), а поле и кнопка внутри неё оформлены
 * `.rf-topbar__search-input`/`ui.button()`. Пока здесь стояла групповая
 * запись, НЕСЛОЁНОЕ правило этого файла (`app.css` вне `@layer`) красило
 * кнопку заливкой `--primary` ПОВЕРХ `.rf-btn--ghost` из `@layer
 * primitives` — неслоёный CSS побеждает слоёный всегда, вне зависимости
 * от порядка (тот же принцип, что уже объясняет `print.css`, РЕШЕНИЕ 1).
 */

/*
 * ЧТО ОТСЮДА УДАЛЕНО (Дизайн-спринт, Task 15) и почему это безопасно:
 * `.search-mods`, `.facets`/`.facets-body`/`.facet`/`.facet-title`/
 * `.facet-option`/`.facet-inline`/`.facet-note`/`.facet-actions`,
 * `.active-filters`/`.active-filters-title`, `.chip-on`, `.found`,
 * `.did-you-mean`, `.editorial`/`.editorial-fandoms`, `.reset` — у всех
 * ЕДИНСТВЕННЫМ потребителем была страница поиска, и она переехала на
 * `rf-*` (грепом по `templates/` перед удалением: ни одного вхождения).
 * Мёртвые правила сняты вместе с переездом, а не «когда-нибудь»: именно
 * так и накопились два словаря на один сайт, против которых затеян спринт.
 *
 * `.search-form`/`.search-row` УДАЛЕНЫ ТЕПЕРЬ (Дизайн-спринт, Task 17):
 * Task 15 оставила их «до задачи 17», потому что их держал ЕДИНСТВЕННЫЙ
 * оставшийся потребитель — форма поиска фандома в онбординге
 * (`templates/users/welcome.html.twig`). Она переехала на
 * `.rf-searchbar`/`.rf-searchbar__row` — те же классы, что у `/search` и
 * главной, третий их потребитель, а не третье определение той же строки, —
 * и правила остались без потребителей вовсе (проверено грепом по
 * `templates/` перед удалением). Девять магических пикселей ушли этой
 * правкой, потолок `no-magic-space.test.ts` опущен ровно на них.
 */

.notice {
    margin: 14px 0 0;
    padding: 11px 14px;
    border: 1px solid color-mix(in srgb, var(--spark) 40%, var(--border));
    border-radius: var(--r-card);
    background: color-mix(in srgb, var(--spark) 8%, transparent);
    color: var(--text-muted);
    font-size: .85rem;
    max-width: 68ch;
}

/* Плашка «содержит из вашего „Скрытого“» на странице работы (Sprint 3.1,
   Task 6, SOC-24). Модификатор `.notice`, а не свой блок: это то же
   сообщение «сайт объясняет, что произошло», и расходиться в отступах и
   радиусе им незачем.

   Отличается ЦВЕТОМ, и осознанно: базовый `.notice` окрашен в `--spark` —
   роль «внимание, ограничение» (гео-скрытие, возрастной гейт). Здесь
   ограничения нет вовсе: страница открыта, читатель сам это спрятал, — и
   тревожный оттенок сообщал бы неправду. `--primary` — нейтральная роль
   «это про вас и ваши настройки». */
.notice-blacklist {
    border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
    background: color-mix(in srgb, var(--primary) 7%, transparent);
}

/* ── каталог фандомов ───────────────────────────────────────────────── */

.category { padding: 22px 0; border-bottom: 1px solid var(--border); }
.category h2 { font-size: 1.05rem; font-weight: 600; margin-bottom: 10px; }
.category ul { list-style: none; display: flex; flex-wrap: wrap; gap: 8px 18px; padding: 0; }
.category a { color: var(--text-muted); text-decoration: none; font-size: .9rem; }
.category a:hover { color: var(--primary); }
.category .count { color: var(--text-muted); font-size: .78rem; }

/*
 * ── вход, регистрация (Sprint 2, Task 8) ───────────────────────────────
 * Ник, выход и остальной блок пользователя (`.site-user`/`.site-logout`)
 * переехали в меню аватара топбара (Дизайн-спринт, Task 10) — стиль имени
 * и кнопки выхода там теперь несёт `.rf-menu__name`/`ui.button()`, второй
 * копии этих правил здесь больше нет.
 *
 * ЧТО СНЯТО ОТСЮДА (Дизайн-спринт, Task 17) и почему это безопасно:
 * `.auth` (колонка 46ch страниц входа и регистрации) и `.tg-login`
 * (отступы монтировки виджета) — у обоих ЕДИНСТВЕННЫМИ потребителями были
 * `identity/login.html.twig` и `identity/registration.html.twig`, и они
 * переехали на `.rf-authpage`/`.rf-authcard` (секция «вход, регистрация,
 * возраст, онбординг» ниже). Класс `.tg-login` при этом остался В РАЗМЕТКЕ:
 * это селектор `auth-telegram.js`, а не оформление (§4.4, «JS-хуки
 * неприкосновенны»), — вид ему теперь даёт `.rf-authcard__tg`.
 *
 * ЧТО ОСТАЛОСЬ И ПОЧЕМУ. `.auth-form` держат `social/collections_mine.html.twig`
 * и `social/collection.html.twig`; `.auth-field` — `identity/settings.html.twig`
 * и `identity/delete_account.html.twig`; `.auth-hint` — `settings.html.twig`
 * (строка статуса привязки почты); `.auth-consents`/`.auth-consent` —
 * `delete_account.html.twig` (судьба работ, IDN-20). Ни одного из этих
 * шаблонов нет в файловом списке Task 17, и снять правило, у которого есть
 * живой потребитель, значило бы поменять чужой экран без его ревью (урок
 * Task 3 про алиасы старого словаря). Регистрация мигрировала на
 * `ui.field()` и `.rf-consents`, поэтому временно существуют два словаря
 * для одного понятия — то же переходное состояние, что у `.hero`/`.rf-hero`
 * (докблок секции «лента фандома + каталог»), и оно исчезнет вместе с
 * миграцией настроек и удаления аккаунта.
 */

.auth-form { display: block; margin: 18px 0 0; }

.auth-field { display: block; margin: 0 0 4px; }

.auth-field span {
    display: block;
    font-size: .82rem;
    color: var(--text-muted);
    margin: 0 0 4px;
}

.auth-field input {
    width: 100%;
    padding: 9px 12px;
    border: 1px solid var(--border);
    border-radius: var(--r-chip);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: .92rem;
}

.auth-field input:focus { outline: 2px solid var(--primary); outline-offset: 1px; }

.auth-hint { font-size: .78rem; color: var(--text-muted); margin: 0 0 16px; min-height: 1em; }

.auth-consents { border: 0; margin: 8px 0 0; padding: 0; }

.auth-consents legend {
    font-size: .82rem;
    color: var(--text-muted);
    padding: 0;
    margin: 0 0 8px;
}

.auth-consent {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    font-size: .86rem;
    margin: 0 0 8px;
}

.auth-consent input { accent-color: var(--primary); margin-top: 3px; }

/* ── настройки аккаунта (Sprint 2, Task 9) ──────────────────────────── */

.settings-section {
    padding: 22px 0;
    border-bottom: 1px solid var(--border);
    max-width: 68ch;
}

.settings-section:last-of-type { border-bottom: 0; }

.settings-section h2 { font-size: 1.05rem; font-weight: 600; margin-bottom: 10px; }
.settings-section p { margin: 0 0 10px; font-size: .92rem; }
.settings-section .empty { padding: 0; margin: 0 0 10px; font-size: .82rem; }

.settings-form { margin: 14px 0 4px; }

.settings-form button {
    padding: 9px 16px;
    border: 1px solid var(--border);
    border-radius: var(--r-chip);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: .88rem;
    cursor: pointer;
}

.settings-form button:hover { border-color: var(--primary); color: var(--primary); }
.settings-form button[disabled] { opacity: .6; cursor: default; }

/*
 * Необратимые действия отмечены цветом предупреждения, а не спрятаны: прятать
 * удаление аккаунта — способ заставить человека искать его в поддержке.
 * Подтверждение всё равно требует ввода ника (IDN-19), цвет лишь напоминает.
 */
.settings-danger button,
.settings-form button.danger,
a.more.danger {
    border-color: color-mix(in srgb, var(--rating-nc17) 45%, var(--border));
    color: var(--rating-nc17);
}

.settings-danger button:hover,
.settings-form button.danger:hover,
a.more.danger:hover {
    border-color: var(--rating-nc17);
    color: var(--rating-nc17);
}

/* ── лайк работы/главы (Sprint 3.1, Task 7, SOC-1/SOC-2) ──────────────── */

/*
 * Кнопка стилизована ПОД прежний статичный `<span class="likes">` (карточка,
 * `.meta .likes` выше) — читатель не должен заметить, что «❤ N» стало
 * интерактивным, кроме как наведя курсор. `aria-pressed="true"` (лайк стоит)
 * добавляет вес: тот же приём, что различает «свою» ссылку в шапке
 * (`.site-user`) от обычной, без второго цвета.
 */
.like-btn {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    border: 0;
    background: none;
    padding: 0;
    font: inherit;
    font-size: inherit;
    color: var(--spark);
    cursor: pointer;
}

.like-btn:hover { opacity: .82; }
.like-btn[aria-pressed="true"] { font-weight: 700; }
.like-btn[disabled] { opacity: .5; cursor: default; }

/* Аноним видит счётчик и ссылку на вход — та же роль, что `.notice`, но без
   рамки: это не ограничение и не предупреждение, а обычное приглашение. */
.like-anon { color: var(--text-muted); font-size: .85rem; }
.like-anon a { color: inherit; }

/*
 * Тост (`assets/js/social.js`) — общий каркас для лайков и будущих
 * социальных форм (закладки, комментарии — задачи 8, 11–13 расширяют тот же
 * `form[data-social]`, не заводя второго тоста). Один элемент на страницу,
 * создаётся скриптом при первой ошибке; видимость — классом, а не
 * `display`, чтобы transition отрабатывал на появление и на уход.
 */
.toast {
    position: fixed;
    left: 50%;
    bottom: 24px;
    transform: translate(-50%, 8px);
    max-width: min(90vw, 48ch);
    padding: 10px 16px;
    border-radius: var(--r-chip);
    background: var(--text);
    color: var(--bg);
    font-size: .86rem;
    text-align: center;
    opacity: 0;
    pointer-events: none;
    transition: opacity .2s ease, transform .2s ease;
    z-index: 100;
}

.toast.is-visible {
    opacity: 1;
    transform: translate(-50%, 0);
}

/* ── закладка и статус прочитанного (Sprint 3.1, Task 8, SOC-3/SOC-8) ─── */

/*
 * `<summary>` стилизована ПОД `.chip` (карточка тегов выше) — тот же
 * визуальный язык, что «жанр»/«троп» на странице работы: закладка такая
 * же метка, только интерактивная. Открытый `<details>` (докблок
 * `bookmark_control.html.twig`: компактный вариант открыт ВСЕГДА) не
 * прячет `<summary>`, поэтому маркер списка снят явно — раскрывающийся
 * треугольник браузера здесь неуместен рядом с кнопками статуса.
 */
.bookmark-control summary {
    display: inline-flex;
    align-items: center;
    list-style: none;
    cursor: pointer;
    padding: 3px 10px;
    border: 1px solid var(--border);
    border-radius: var(--r-chip);
    font-size: .78rem;
    color: var(--text-muted);
}

.bookmark-control summary::-webkit-details-marker { display: none; }
.bookmark-control[open] summary { border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); color: var(--text); }

.bookmark-control form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 8px;
}

/* `.bookmark-statuses` — мёртвый код (Task 11). Разметка переписана с
   `.bookmark-statuses` на `.rf-segmented` и `aria-pressed` на общий селектор в
   `ui.css`. Здесь оставались только правила старого компонента, который ни
   один шаблон не печатает с 2026-09-07. Удалено.
   @layer components из Task 10-11 обнял всё, что касается этого компонента. */

.bookmark-field {
    display: flex;
    flex-direction: column;
    gap: 3px;
    font-size: .8rem;
    color: var(--text-muted);
}

.bookmark-field input[type="text"],
.bookmark-field textarea {
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    padding: 6px 9px;
    font: inherit;
    font-size: .85rem;
}

.bookmark-field textarea { min-height: 54px; resize: vertical; }

.bookmark-check { flex-direction: row; align-items: center; gap: 6px; }
.bookmark-check input[type="checkbox"] { accent-color: var(--primary); }

.bookmark-remove {
    align-self: flex-start;
    border: 0;
    background: none;
    padding: 0;
    font-size: .78rem;
    color: var(--text-muted);
    cursor: pointer;
    text-decoration: underline;
}

.bookmark-remove:hover { color: var(--rating-nc17); }

/* ── `/bookmarks` (SOC-8) ───────────────────────────────────────────── */

.bookmark-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 4px 0 16px;
}

.bookmark-tabs a {
    padding: 5px 12px;
    border: 1px solid var(--border);
    border-radius: var(--r-chip);
    font-size: .85rem;
    color: var(--text-muted);
    text-decoration: none;
}

.bookmark-tabs a:hover { border-color: var(--primary); color: var(--primary); }

.bookmark-tabs a.active {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 16%, transparent);
    color: var(--primary);
    font-weight: 600;
}

.bookmark-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-bottom: 20px;
}

.bookmark-filters select,
.bookmark-filters input[type="search"] {
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    padding: 6px 10px;
    font: inherit;
    font-size: .85rem;
}

/*
 * `.cards` — список карточек `/bookmarks` И `/collections/{slug}`
 * (`templates/social/bookmarks.html.twig`, `templates/social/collection.html.twig`).
 * `container-type: inline-size` (Дизайн-спринт, Task 11) — тем же
 * основанием, что `.feed` выше: обёртка ленты отдаёт карточке работы
 * реальную ширину через `@container`, докблок секции «карточка работы».
 */
.cards { container-type: inline-size; }

/*
 * Заглушка недоступной работы (`work: null`, докблок `BookmarkRow`) —
 * СТИЛЕМ приглушённее обычной карточки (`opacity`, без hover-подсветки
 * `.rf-card:hover`), а не другим цветом: это не ошибка и не предупреждение,
 * а обычный факт «эта работа сейчас недоступна», и красный/жёлтый оттенок
 * читался бы тревогой там, где её нет.
 */
.rf-card.bookmark-stub { opacity: .7; }
.rf-card.bookmark-stub:hover { border-color: var(--border); }

/* ── обсуждение (Sprint 3.1, Task 11, SOC-11…SOC-14) ──────────────────── */

.comments { padding-top: 4px; }

.comments-closed { color: var(--text-muted); font-size: .9rem; }

.comment-form {
    display: flex;
    flex-direction: column;
    gap: 6px;
    max-width: 60ch;
    margin-bottom: 18px;
}

.comment-field {
    display: flex;
    flex-direction: column;
    gap: 3px;
    font-size: .8rem;
    color: var(--text-muted);
}

.comment-field textarea,
.comment-edit-area {
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    padding: 8px 10px;
    font: inherit;
    font-size: .9rem;
    min-height: 70px;
    resize: vertical;
}

.comment-form button,
.comment-menu-body button {
    align-self: flex-start;
    border: 1px solid var(--border);
    border-radius: var(--r-chip);
    background: none;
    color: var(--text);
    padding: 5px 14px;
    font: inherit;
    font-size: .85rem;
    cursor: pointer;
}

.comment-form button:hover,
.comment-menu-body button:hover { border-color: var(--primary); color: var(--primary); }

.comment-counter { font-size: .75rem; color: var(--text-muted); }

.comment-list { display: flex; flex-direction: column; gap: 14px; }

/*
 * Один комментарий — БЕЗ карточной рамки `.card` (та роль занята работой):
 * левая полоса-акцент отличает реплики друг от друга внутри списка так же,
 * как отступ у `.comment-replies` ниже отличает ответ от корня, — вертикальный
 * ритм вместо рамки на каждой строке.
 */
.comment {
    padding: 10px 0 10px 14px;
    border-left: 2px solid var(--border);
}

.comment.pinned { border-left-color: var(--spark); }

.comment.collapsed { padding: 6px 0 6px 14px; }
.comment.collapsed > summary { cursor: pointer; color: var(--text-muted); font-size: .85rem; list-style: none; }
.comment.collapsed > summary::-webkit-details-marker { display: none; }

.comment-head { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; font-size: .85rem; }
.comment-author { color: var(--text); }

.comment-badge {
    padding: 1px 8px;
    border-radius: var(--r-chip);
    border: 1px solid var(--border);
    color: var(--text-muted);
    font-size: .7rem;
}

.comment-date { color: var(--text-muted); font-size: .78rem; margin-left: auto; }

.comment-body { margin-top: 4px; font-size: .92rem; line-height: 1.5; white-space: pre-wrap; }

.comment-menu { margin-top: 6px; font-size: .82rem; }

.comment-menu > summary {
    display: inline-flex;
    cursor: pointer;
    list-style: none;
    color: var(--text-muted);
    padding: 2px 8px;
    border-radius: var(--r-chip);
}

.comment-menu > summary::-webkit-details-marker { display: none; }
.comment-menu > summary:hover { color: var(--primary); }

.comment-menu-body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    margin-top: 8px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    background: var(--surface);
}

.comment-menu-body form,
.comment-reply form,
.comment-report form {
    display: flex;
    flex-direction: column;
    gap: 6px;
    width: 100%;
}

.comment-reply,
.comment-report { width: 100%; }

.comment-reply > summary,
.comment-report > summary {
    cursor: pointer;
    list-style: none;
    color: var(--text-muted);
}

.comment-reply > summary::-webkit-details-marker,
.comment-report > summary::-webkit-details-marker { display: none; }

.comment-reply[open] > summary,
.comment-report[open] > summary,
.comment-menu-body button:hover { color: var(--primary); }

.comment-replies {
    margin: 10px 0 0 18px;
    padding-left: 14px;
    border-left: 1px dashed var(--border);
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.comment-replies:empty { display: none; }
/* `.bookmark-stub` без `.card`: класс появляется только на заглушке
 * недоступной работы в `/bookmarks` (`social/bookmarks.html.twig`), второго
 * источника нет — упрощение специфичности (принцип 4), адресат тот же. */
.bookmark-stub .notice { margin: 0; }

/*
 * ── колокольчик и лента уведомлений (Sprint 3.1, Task 15, NTF-4) ───────
 * Сам значок-ссылка (`.bell`) переехал в `.rf-topbar__bell` (Дизайн-
 * спринт, Task 10, раздел «топбар» ниже) — счётчик остаётся здесь: у него
 * нет отношения к раскладке шапки, только к своему бейджу, и его класс
 * (`data-unread`-контракт) `assets/js/social.js` находит по атрибуту,
 * а не по родителю.
 */

.bell-count {
    display: inline-flex;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    align-items: center;
    justify-content: center;
    border-radius: var(--r-chip);
    background: var(--spark);
    color: var(--ink);
    font-size: .68rem;
    font-weight: 700;
}

/*
 * Sprint 3.1, Task 15, фикс-раунд 1: `display: inline-flex` выше — правило
 * АВТОРСКОЙ таблицы, а `[hidden] { display: none }` браузер держит в СВОЕЙ,
 * UA-таблице — авторская побеждает всегда, вне зависимости от специфичности
 * (`.bell-count` и `[hidden]` у неё равные 0-1-0). Без этой строки
 * `social.js` (`badge.hidden = true` после «прочитано по клику», обнулившего
 * счётчик) снимал атрибут, а бейдж оставался на экране пустым кружком.
 */
.bell-count[hidden] { display: none; }

.notifications-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }

.notification {
    display: block;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    background: var(--surface);
}

.notification.unread { border-color: var(--primary); }

.notification a { display: block; color: inherit; text-decoration: none; }
.notification a:hover strong { color: var(--primary); }

.notification p { margin: 4px 0 0; color: var(--text-muted); font-size: .88rem; }

/*
 * Тело уведомления — многострочное у дайджеста (секции через перевод
 * строки, RenderNotificationTask::dailyDigest()). `pre-line`, а не
 * `pre-wrap`: лишние пробелы схлопывать по-прежнему нужно, а вот переводы
 * строк — нет.
 */
.notification-body { white-space: pre-line; }
.notification time { display: block; margin-top: 6px; color: var(--text-muted); font-size: .78rem; }

/* ── настройки уведомлений (Sprint 3.1, Task 15, NTF-7) ───────────────── */

.notification-settings-table { width: 100%; border-collapse: collapse; margin: 0 0 20px; }

.notification-settings-table th,
.notification-settings-table td {
    padding: 8px 10px;
    border-bottom: 1px solid var(--border);
    text-align: left;
    font-size: .88rem;
}

.notification-settings-table .hint { margin: 4px 0 0; font-size: .78rem; color: var(--text-muted); }

.notification-settings-table td.mandatory { color: var(--text-muted); font-style: italic; }

/*
 * ── вход, регистрация, возраст, онбординг (Дизайн-спринт, Task 17) ─────
 *
 * Пять экранов одного семейства и два новых узла: карточка на glow-фоне
 * (`.rf-authpage` + `.rf-authcard`) и липкая панель действий
 * (`.rf-sticky-actions`).
 *
 * КАРТОЧКА ЖИВЁТ НЕ ТОЛЬКО ЗДЕСЬ (Task 18, фикс-раунд 1): её носят ещё
 * два комплаенс-экрана — возрастной гейт и 451 (секция ниже по файлу).
 * Правка любого правила `.rf-authcard*` меняет семь экранов, а не пять;
 * `no-duplicate-primitive.test.ts` держит `rf-authcard` в списке блоков,
 * которые объявляются ровно в одном файле, чтобы следующая копия
 * карточки не завелась молча.
 *
 * Продолжают `.rf-hero`/`.rf-hero__in`/
 * `.rf-hero__title`/`.rf-hero__description` (Task 12) и
 * `.rf-searchbar`/`.rf-searchbar__row` (Task 15) — онбординг назван их
 * потребителем, а не автором второго определения тех же узлов.
 *
 * ЗДЕСЬ ТОЛЬКО РАСКЛАДКА И МЕСТО — то же ограничение и по той же причине,
 * что у секций поиска и главной выше: `app.css` не обёрнут в каскадный
 * слой целиком, его правило побеждает `@layer primitives` НАСОВСЕМ и на
 * всех страницах, и «поправить поле на регистрации» здесь означало бы
 * поправить его везде. Ни одно правило ниже не переобъявляет свойств
 * примитива: всё, что понадобилось самим примитивам (резерв строки у живой
 * подсказки, чип-чекбокс, вид нативно отключённой кнопки), заведено в
 * `assets/css/ui.css` — там же, где эти примитивы живут.
 *
 * НИ ОДНОГО `filter`/`backdrop-filter`/`transform` (урок Task 15.6): любой
 * такой предок становится containing block для `position: fixed` потомков,
 * а панель меню аккаунта в шапке — `fixed`, и она открывается на КАЖДОЙ
 * из этих страниц. Липкая панель онбординга — ровно тот элемент, которому
 * размытие «идёт», и поэтому фон у неё сплошной: см. её докблок ниже.
 *
 * ПОТОЛОК `no-magic-space.test.ts` ОПУЩЕН 146 → 122 (второе реальное
 * снижение за спринт, после Task 15): удалены `.auth` (2) и `.tg-login`
 * (2), `.search-form`/`.search-row` (9) и весь блок
 * `.welcome-*`/`.chip-list` (11) — у всех единственными потребителями были
 * пять шаблонов этой задачи. 24 магических пикселя ушли этой правкой
 * (замерено `magicSpaceCount()` того же теста до и после), потолок опущен
 * ровно на них, без запаса. Новая секция целиком на шкале и не добавила ни
 * одного: единственное место, где хотелось литерала, — сдвиг чекбокса
 * согласия на первую строку, и там стоит `--space-1` (см. правило ниже).
 */
@layer components {

/*
 * ГЛОУ — НА СТРАНИЦЕ, КАРТОЧКА — 420px ПО ЦЕНТРУ. Две коробки, а не одна:
 * подсветка обязана идти во всю ширину окна (иначе это не фон, а пятно
 * шириной 420px), а карточка — держать читаемую меру. 420px — это ровно та
 * мера, которой колонка входа держалась и раньше (`.auth { max-width: 46ch }`
 * ≈ 420px при `--fs-base`), только теперь она названа числом, а не
 * количеством знаков шрифта, который читатель может подменить своим.
 */
.rf-authpage {
    display: flex;
    justify-content: center;
    background-image: var(--hero-glow);
    padding-block: var(--space-7) var(--space-8);
    padding-inline: var(--space-4);
}

.rf-authcard {
    inline-size: 100%;
    max-inline-size: 420px;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-6);
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    background: var(--surface);
}

/* Экран подтверждения 18+ несёт четыре абзаца объяснений вместо одной
   строки: на 420px они превращаются в колонку из двадцати строк. Ту же
   меру и по той же причине носят два комплаенс-экрана — возрастной гейт
   и 451 (Task 18, фикс-раунд 1: они носят ЭТУ карточку, а не свою копию;
   секция «возрастной гейт и 451» ниже по файлу объясняет, почему). */
.rf-authcard--wide { max-inline-size: 560px; }

/* Шапка карточки — своя, более плотная сетка: знак, заголовок и строка
   объяснения читаются одним блоком, а `gap` карточки разносит блоки. */
.rf-authcard__head { display: flex; flex-direction: column; gap: var(--space-2); }

.rf-authcard__crumbs { font-size: var(--fs-xs); color: var(--text-muted); }
.rf-authcard__crumbs a { color: var(--text-muted); text-decoration: none; }
.rf-authcard__crumbs a:hover { color: var(--primary); }

.rf-authcard__mark {
    font-size: var(--fs-lg);
    font-weight: 700;
    letter-spacing: -.03em;
    color: var(--text);
}

.rf-authcard__mark-accent { color: var(--primary); }

/* Кегль — `--fs-2xl` без `clamp()`, в отличие от `.rf-hero__title`:
   заголовок карточки не тянется по ширине окна, потому что и карточка не
   тянется.

   `margin: 0` — Дизайн-спринт, Task 18, фикс-раунд 1 (I-3), и это не
   сброс «на всякий случай». `base.css` не гасит UA-маргины заголовков и
   абзацев, поэтому к `gap` карточки складывалась вторая, невидимая в
   коде шкала: замерено на `/login` — `16.08px` сверху и снизу у
   заголовка и `14.4px` у строки объяснения поверх `gap: var(--space-2)`.
   Докблок карточки выше («отступов здесь нет: их даёт `gap`») описывал
   не то, что происходило. Ноль приехал сюда вместе с комплаенс-экранами
   (гейт 18+ и 451), которые носят ЭТУ карточку и держали ноль у своей
   копии: одно определение — один вертикальный ритм на все пять экранов
   карточки. `.rf-authcard__note` UA-маргины пока сохраняет — этот узел
   на комплаенс-экранах не встречается, и его ритм правится задачей,
   которая владеет входом (строка в `IDEAS.md`). */
.rf-authcard__title {
    margin: 0;
    font-size: var(--fs-2xl);
    font-weight: 700;
    letter-spacing: -.02em;
    line-height: var(--lh-tight);
}

.rf-authcard__description {
    margin: 0;
    font-family: var(--font-read);
    font-size: var(--fs-base);
    color: var(--text-muted);
}

.rf-authcard__form { display: flex; flex-direction: column; gap: var(--space-4); }

.rf-authcard__note { font-size: var(--fs-xs); color: var(--text-muted); }

/*
 * Зона монтировки виджета Telegram. Своей высоты у неё нет, пока не
 * приехал сторонний `<iframe>` с telegram.org, — держим тап-таргет, чтобы
 * карточка не подпрыгивала на его загрузке. Отступов здесь нет: их даёт
 * `gap` карточки (принцип «компонент не знает о странице» — раньше
 * `.tg-login` носила `margin: 18px 0 8px` и тащила чужую вертикальную
 * сетку в каждое место, куда её ставили).
 */
.rf-authcard__tg { min-block-size: var(--rf-tap); }

/*
 * Список согласий. Старый `.auth-consents` остался в `app.css` для
 * удаления аккаунта (докблок секции «вход, регистрация» выше объясняет,
 * почему его нельзя снять этой задачей); здесь — тот же смысл в словаре
 * `rf-*`. Легенда набрана как подпись поля (`.rf-field__label`): три
 * галочки юридически значимы, и «Согласия» — их общий заголовок, а не
 * приглушённая сноска.
 */
.rf-consents { display: flex; flex-direction: column; gap: var(--space-2); border: 0; padding: 0; }
.rf-consents legend { padding: 0; font-size: var(--fs-sm); font-weight: 600; color: var(--text); }

.rf-consent {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

/* Смещение на первую строку текста: чекбокс мельче строки и без сдвига
   висит выше её середины. `--space-1` (4px), а не литеральные 3px старого
   `.auth-consent input`: расчётный сдвиг для строки `--fs-sm` — 3.4px, и
   ближайший шаг шкалы точнее прежнего числа, а не грубее его. */
.rf-consent input { accent-color: var(--primary); margin-block-start: var(--space-1); }

/* ── онбординг: два шага (Sprint 3.1, Task 20, FE-8; вёрстка — Task 17) ── */

/* Та же колонка 860px, что у ленты фандома, выдачи поиска и главной. */
.rf-welcome { max-inline-size: 860px; margin-inline: auto; padding-block-start: var(--space-6); padding-inline: var(--space-4); }

.rf-welcome__chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/*
 * Контейнер саджеста (`[data-suggested]`) рисуется ВСЕГДА, даже пустым:
 * его ищет `welcome.js` до первого набранного символа (докблок шаблона).
 * Правило переехало сюда из `.chip-list:empty` вместе со своим
 * единственным потребителем — и вместе с ним переехало условие, без
 * которого оно не работает: шаблон обязан срезать пробелы внутри
 * контейнера (`{%- for %}`/`{% endfor -%}`), иначе текстовый узел из
 * отступов не даёт селектору `:empty` совпасть.
 *
 * ЧТО ОНО ДАЁТ СЕГОДНЯ — НИЧЕГО, И ЭТО ЗАМЕРЕНО (Task 17): пустой
 * контейнер занимает 0px и без правила, потому что флекс-контейнер
 * выбрасывает пробельные узлы, а форма вокруг — блочная. Правило
 * оставлено как гарантия на будущее: коробка нулевой высоты станет
 * полосой отступа в тот день, когда форма станет `flex`/`grid` с `gap`, —
 * то есть в правку, которая с саджестом никак не связана и причину искать
 * будет не там.
 */
.rf-welcome__chips:empty { display: none; }

.rf-welcome__cards {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding-block: var(--space-6) var(--space-8);
}

/*
 * СВОЯ рамка/фон/паддинг, а не заимствованные у `.rf-card` (рулинг Task 11,
 * сохранён этой задачей): онбординг не карточка работы и не обязан
 * участвовать в её контракте (контейнерные запросы, `--rf-card-pad`,
 * перенос длинного слова). Иконка и текст — строка из двух колонок:
 * заголовок и объяснение стоят одним блоком справа от знака.
 */
.rf-welcome__card {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    background: var(--surface);
    text-decoration: none;
    color: inherit;
    transition: border-color var(--dur-fast);
}

.rf-welcome__card:hover { border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); }

.rf-welcome__card-icon { color: var(--primary); }

.rf-welcome__card-title { display: block; font-size: var(--fs-md); font-weight: 600; color: var(--text); }
.rf-welcome__card-text { display: block; font-size: var(--fs-sm); color: var(--text-muted); }

/*
 * ЛИПКАЯ ПАНЕЛЬ ДЕЙСТВИЙ ПЕРЕЕХАЛА В `assets/css/ui.css` (Дизайн-спринт,
 * Task 22). Причина: у неё появился ВТОРОЙ потребитель — массовые действия
 * экрана импорта в кабинете (`apps/studio/src/import/BulkActions.tsx`), а
 * `apps/studio` не подключает этот файл вовсе (`main.tsx` — `layers.css`,
 * `tokens.css`, `ui.css`, `studio.css`). Оставь правило здесь — и React
 * напечатал бы имя без определения; скопируй в `studio.css` — и панель
 * стала бы второй копией одной коробки, ровно тем, что спринт вылавливает
 * пятый раз (`.rf-stats-card`, `.rf-gatecard`, `.rf-work-row`, `.rf-pill`).
 * Онбординг (`templates/users/welcome.html.twig`) продолжает печатать тот же
 * класс и получает его из `ui.css`, который `base.html.twig` подключает
 * строкой выше `app.css`; имя `rf-sticky-actions` заведено в реестре
 * `no-duplicate-primitive.test.ts`, чтобы второе объявление больше не
 * прошло молча.
 *
 * ЧТО ИЗМЕНИЛОСЬ В КАСКАДЕ: правило было в `@layer components` этого файла,
 * стало в `@layer primitives` — то есть СЛАБЕЕ на один слой. Конфликтовать
 * ему не с чем (второго правила на этот класс в дереве нет; проверено
 * грепом), и это ЗАМЕРЕНО, а не выведено: разметка `/welcome` выгружена из
 * `AuthLayoutTest` и отрисована Chromium'ом дважды — с этим файлом и
 * `ui.css` из `git show HEAD:` и с итоговыми, — все **503** вычисленных
 * свойства панели совпали, коробка тоже (16,823,358,77 на 390 и
 * 186,664,828,77 на 1200). Прогон не пустой: в ОБЕИХ ревизиях панель
 * считает `position: sticky`, `inset-block-end: 0`, `padding-block: 16px`,
 * `margin-block-start: 24px`, рамку 1px и фон `--bg`, то есть правило
 * применяется и там, и там (без него у неё была бы высота одной кнопки,
 * 44px вместо 77).
 */

}

/*
 * ── возрастной гейт и 451 (Дизайн-спринт, Task 18) ─────────────────────
 *
 * Два комплаенс-экрана на одной странице-обёртке `.rf-gatepage`. КАРТОЧКА
 * У НИХ НЕ СВОЯ: это `.rf-authcard rf-authcard--wide` секции «вход,
 * регистрация, возраст, онбординг» выше по файлу. Оба экрана носят одну и
 * ту же коробку с экранами входа: читателю ни в одном из случаев не в чем
 * себя винить, и «доступ закрыт по возрасту», «доступ закрыт правом» и
 * «войдите, чтобы продолжить» обязаны выглядеть одинаково спокойно.
 *
 * ПОЧЕМУ КАРТОЧКА ОБЩАЯ (фикс-раунд 1, I-3). До этого раунда здесь стояла
 * своя `.rf-gatecard` — вторая коробка, собранная копированием правил: у
 * неё и у `.rf-authcard` было по девять объявлений, ВОСЕМЬ совпадали
 * байт-в-байт, а девятое отличалось только мерой — `560px` против `420px`,
 * то есть ровно тем, что `.rf-authcard--wide` уже носит и по той же
 * причине (объяснений здесь три-четыре абзаца плюс список оснований, и на
 * 420px они превращаются в колонку из тридцати строк). Всего с узлами
 * шапки дублировалось 22 объявления. Копия успела РАЗОЙТИСЬ за один
 * коммит — так, как расходятся копии: гейт гасил UA-маргины текстовых
 * узлов (`margin: 0`), карточка входа — нет, и две «сестринские» карточки
 * жили на разном вертикальном ритме (замерено на живых страницах: у
 * `.rf-authcard__title` 16.08px сверху и снизу поверх `gap`, у
 * `__description` — 14.4px). Заметить это можно было только открыв обе
 * рядом, то есть ровно тем способом, против которого спринт и заводил
 * принцип «одно определение» (спек §4.1).
 *
 * Рулинг Task 11 (`.rf-welcome__card`, докблок выше по файлу) сюда НЕ
 * ПЕРЕНОСИТСЯ, хотя прежняя редакция этого докблока на него ссылалась: он
 * про отказ от КОНТРАКТА `.rf-card` — контейнерных запросов,
 * `--rf-card-pad`, переноса длинного слова. У `.rf-authcard` контракта
 * нет, это коробка, и отказываться здесь было не от чего — оставалась
 * копия.
 *
 * `margin: 0` УЕХАЛ В САМИ `.rf-authcard__title`/`__description`: одно
 * определение, один ритм. Карточка входа стала на 32.16px/28.8px плотнее
 * в шапке — и это не побочный ущерб, а приведение её к собственному
 * докблоку («отступов здесь нет: их даёт `gap` карточки»): UA-маргины там
 * никто не выбирал, они протекли из браузера. Замеры до/после — в отчёте
 * задачи.
 *
 * АЛИАСА `.rf-gatecard` НЕ ОСТАЛОСЬ НАРОЧНО, ни строкой. Рулинг Task 3
 * (докблок `no-duplicate-primitive.test.ts`): алиас поверх живого правила
 * — не «одно правило, два имени», а два независимых правила, склеенных
 * порядком каскада, то есть гибрид, которого никто не проектировал.
 *
 * ОБЁРТКА ОСТАЛАСЬ СВОЕЙ, и это единственное, что здесь дублируется
 * осознанно. `.rf-authpage` подсвечивает фон `--hero-glow`, потому что там
 * человека ВСТРЕЧАЮТ; под сообщением о правовом ограничении праздничная
 * подсветка читается насмешкой. Четыре раскладочных объявления двух
 * обёрток при этом совпадают — честная остаточная копия, и она записана
 * строкой в `IDEAS.md`, а не оставлена на память: объединить их значит
 * напечатать модификатор в трёх чужих шаблонах входа, то есть отдельная
 * задача с их ревью.
 *
 * СВОИ УЗЛЫ ЭКРАНОВ — ЭЛЕМЕНТЫ СТРАНИЦЫ, А НЕ КАРТОЧКИ (`.rf-gatepage__*`).
 * Слот метки, заголовки объяснений, список оснований с комментариями, ряд
 * действий и ссылка на правила лежат ВНУТРИ общей карточки, но принадлежат
 * ЭКРАНУ, а не каждому, кто эту карточку носит. Иначе пришлось бы печатать
 * под одним именем два разных узла: `.rf-authcard__mark` — это словесный
 * знак «Everfic» на входе (кегль `--fs-lg`, жирный), а метка комплаенс-
 * экрана — слот под бейдж рейтинга или чип юрисдикции. Два смысла на одно
 * имя развели бы карточки при первой же правке — тем же способом, каким
 * разошлась `.rf-gatecard`.
 *
 * ЗДЕСЬ ТОЛЬКО РАСКЛАДКА И МЕСТО, и причина этому — не каскад, а
 * собственность (фикс-раунд 1, I-1). Секция целиком обёрнута в
 * `@layer components`, поэтому потомковый селектор вроде
 * `.rf-gatepage__mark .rf-badge` остался бы ЛОКАЛЬНЫМ — прежняя редакция
 * этого докблока утверждала обратное, и это было неверно (верно оно
 * только для БАРЕНОГО правила `.rf-badge`, которого никто не предлагал).
 * Правило страницы не переобъявляет собственных свойств примитива не
 * потому, что не может, а потому, что кегль бейджа принадлежит бейджу.
 * Единственное, что этим экранам понадобилось от примитива, — КРУПНЫЙ
 * бейдж, и он взят объявленным входом примитива, ручкой `--rf-badge-fs`
 * (докблок `.rf-gatepage__mark` ниже и докблок `.rf-badge` в
 * `assets/css/ui.css`).
 *
 * НИ ОДНОГО `filter`/`backdrop-filter`/`transform` (урок Task 15.6): любой
 * такой предок становится containing block для `position: fixed`
 * потомков, а панель меню аккаунта в шапке — `fixed`, и она открывается и
 * на этих двух страницах.
 *
 * ПОТОЛОК `no-magic-space.test.ts` ОСТАЁТСЯ 122 (замерено
 * `magicSpaceCount()` до и после — разница ноль, потолок оставлен НАРОЧНО,
 * а не забыт). Новая секция целиком на шкале и не добавила ни одного
 * магического пикселя. Снизить потолок было нечем: старое семейство
 * `.hero`/`.page-shell`/`.category`/`.empty`/`.more`, с которого съехали
 * эти два шаблона, держат ещё РОВНО 20 шаблонов вне файлового списка
 * задачи (посчитано грепом по `templates/` после правки: страница работы,
 * автор, серия, тег и глоссарий, сборники, закладки, подписки, «Скрытое»,
 * уведомления и их настройки, настройки аккаунта и его удаление,
 * обсуждение, помощь с импортом и — та же секция комплаенса —
 * `/transparency`) — снять из него хоть один пиксель значило бы поменять
 * вид всех этих экранов одной правкой без их ревью (тот же довод, что в
 * докблоке Task 12).
 */
@layer components {

/*
 * Обёртка страницы: центрирует карточку и держит поля от края экрана.
 * Больше ничего — ни фона, ни рамки, ни тени: всё, что видно на этих
 * страницах, рисует сама карточка.
 */
.rf-gatepage {
    display: flex;
    justify-content: center;
    padding-block: var(--space-7) var(--space-8);
    padding-inline: var(--space-4);
}

/*
 * СЛОТ МЕТКИ — ОДИН НА ДВА ЭКРАНА: у гейта здесь бейдж рейтинга, у 451 —
 * чип юрисдикции. Обе карточки обязаны отвечать на «почему я это вижу»
 * первым же элементом, и держать под это два разных узла значило бы
 * развести их место в карточке при первой правке. Что бейдж и чип
 * выглядят по-разному — правда, а не рассогласование: это разные
 * сущности, и примитив у каждой свой.
 *
 * КРУПНЫЙ БЕЙДЖ — РУЧКОЙ `--rf-badge-fs`, И ЭТО САНКЦИОНИРОВАННЫЙ ШОВ, А
 * НЕ ОБХОД ЗАПРЕТА (фикс-раунд 1, I-1). Спека §4.3 («Уровень 3 —
 * переменные компонента», `2026-09-07-design-sprint-design.md:264-268`)
 * заводит ровно этот ярус и печатает буквально этот приём с
 * `--rf-btn-bg` прецедентом: примитив читает величину из переменной с
 * фоллбэком, страница переменную ставит. Правило страницы не имеет права
 * переобъявить `font-size` самого бейджа — ни бареным `.rf-badge`, ни
 * локальным `.rf-gatepage__mark .rf-badge`, — потому что кегль бейджа
 * принадлежит бейджу; а поставить его объявленную переменную — ровно то,
 * для чего она объявлена.
 *
 * Класс-модификатор `.rf-badge--lg` был вторым допустимым решением и
 * проиграл по цене РАЗМЕТКИ, а не по каскаду: бейдж печатает макрос
 * `ui.badge_rating()`, и модификатор потребовал бы либо расширить его
 * сигнатуру ради одного экрана, либо набрать бейдж разметкой мимо макроса
 * — то есть завести второй генератор бейджа. (Прежняя редакция ссылалась
 * ещё и на то, что демо модификатора на витрине показывало бы неверный
 * размер из-за stale-бандла. Довод снят как НЕРАЗЛИЧАЮЩИЙ: бандл
 * перекрывает `.rf-badge` без ручки, тем же слоем и позже по порядку
 * источника, — значит на витрине и сама ручка молчаливый no-op, и довод
 * осуждал оба варианта одинаково.)
 *
 * Ручка стоит на СЛОТЕ, а не на самом бейдже: кастомные свойства
 * наследуются, а слот — то место, где решение «здесь метка крупная» и
 * принимается. Чип юрисдикции ручку не читает и остаётся компактным по
 * своему `--static`.
 *
 * РУЧКА ЗАРЕГИСТРИРОВАНА (фикс-раунд 1, I-2):
 * `apps/shared/ui/component-knobs.test.ts` утверждает, что примитив её
 * читает, а этот файл — ставит. Удалить любую из двух половин молча
 * больше нельзя.
 */
.rf-gatepage__mark {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    --rf-badge-fs: var(--fs-md);
}

.rf-gatepage__heading { margin: 0; font-size: var(--fs-lg); font-weight: 600; }

.rf-gatepage__text { margin: 0; font-size: var(--fs-base); }

/*
 * Список правовых оснований — стопка строк без маркеров: у пункта два
 * поля (основание и необязательный комментарий), и точка перед
 * многострочным пунктом читалась бы как «перечисление коротких слов».
 *
 * Кегль — `--fs-base`, тот же, что у объяснений карточки. Без него пункт
 * наследовал бы 16px `<body>` и, набранный `<b>` (разметка зафиксирована
 * инвентарём §9), выходил КРУПНЕЕ и жирнее собственного заголовка
 * «Основание»/«Почему так» — замерено на живой странице: 16px bold против
 * 16.8px semibold, то есть ссылка на норму читалась вровень с
 * заголовком блока, а объяснения под ней — мельче обоих.
 */
.rf-gatepage__grounds {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-base);
}

/* Комментарий к основанию — СВОЕЙ строкой под ним (`display: block`), а не
   в одну строку рядом: основание — это ссылка на норму, комментарий —
   человеческое пояснение к ней, и в одну строку они читаются как одна
   длинная формулировка. */
.rf-gatepage__comment {
    display: block;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

/* Ряд действий: одна строка на широком экране, перенос на узком — та же
   раскладка, что у панели действий страницы работы. */
.rf-gatepage__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/*
 * Ссылка на правила видимости — ТЕКСТОМ, не третьей кнопкой: у карточки
 * уже есть один-два выхода, и третья пилюля уравняла бы «уйти читать
 * правила» с ними по весу. Цвет ссылки приходит из `base.css`
 * (`@layer base` проигрывает этому слою порядком, но `color` здесь никто
 * не переобъявляет — значит, работает reset).
 *
 * ТАП-ТАРГЕТ И КОЛЬЦО ФОКУСА — СВОИ, тем же правилом и в том же месте,
 * что у ссылок подвала (`.rf-footer__column a`, Task 10). Это не
 * украшение и не дубль примитива: `.rf-btn` носит и таргет, и кольцо сам,
 * а текстовая ссылка — единственный узел этих двух экранов, который
 * НЕ примитив, и без правила ниже она (замерено на живой странице)
 * получала UA-кольцо `1px auto rgb(0, 95, 204)` — синюю системную рамку
 * поверх лавандовой карточки — и высоту 20px вместо 44px требуемого
 * таргета (FE-2). В список `FOCUS_VISIBLE_BLOCKS`
 * (`apps/shared/ui/css-specificity.test.ts`) эта строка НЕ добавляется:
 * тот гейт читает только `assets/css/ui.css` и ищет кольца ПРИМИТИВОВ —
 * узел страницы там дал бы вечно красный тест, ровно как его не дают
 * туда кольца подвала и топбара.
 */
.rf-gatepage__more { margin: 0; font-size: var(--fs-sm); }

.rf-gatepage__more a {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--rf-tap);
}

.rf-gatepage__more a:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
}

}

/*
 * ── страница автора и серия (Дизайн-спринт, Task 19) ───────────────────
 *
 * Продолжает `.rf-hero`/`.rf-hero__in`/`.rf-hero__crumbs`/`.rf-hero__title`/
 * `.rf-hero__description`/`.rf-hero__stats` (Task 12) — обе страницы
 * названы потребителем «hero Task 12» в докблоке той секции, — а также
 * `.rf-work-byline` и `.rf-actionbar` (Task 14). Ниже — ТОЛЬКО узлы,
 * которых ни у одного из этих экранов не было: личность автора, ряд
 * разделов, колонка разделов и тумбстоун.
 *
 * ЗДЕСЬ ТОЛЬКО РАСКЛАДКА И МЕСТО. Ни одно правило ниже не переобъявляет
 * свойств примитива (цвет, радиус, рамку, кегль элемента `rf-*`):
 * `app.css` ещё не обёрнут в каскадный слой целиком, поэтому его правило
 * побеждает `@layer primitives` НАСОВСЕМ и на всех страницах. Всё, что
 * относится к виду самих примитивов этой задачи (четвёртый размер
 * аватара, нейтральный бейдж), уехало в `assets/css/ui.css` тем же
 * коммитом.
 *
 * ЧТО ПЕРЕИСПОЛЬЗОВАНО, А НЕ СКОПИРОВАНО (урок Task 18 про вторую
 * карточку): панель действий — `.rf-actionbar` страницы работы, атрибуция
 * серии — `.rf-work-byline` оттуда же, портрет — `.rf-avatar` с новым
 * размером, а не свой круг, чипы разделов — `.rf-chip` в форме ссылки
 * (`ui.chip(label, {href})`, Task 15). Собственных правил у этой задачи
 * 13 (охватывают десять узлов: `.rf-profile`, `__names`, `__handle`,
 * `.rf-sections`, `__nav`, `__section` (плюс `:last-child` и вложенный
 * `h2`), `__feed`, `__list` (плюс вложенный `a`), `__more`,
 * `.rf-tombstone`) — правка M-6, фикс-раунд 1: число было ошибочным
 * («восемь»), сам довод (ни одно правило не повторяет чужой набор
 * объявлений) верен и без изменений — и стал верным ещё и для
 * `.rf-profile__handle` только после M-7 этого же раунда (было
 * дублирование, см. докблок `.rf-work-byline` выше по файлу).
 *
 * ПОТОЛОК `no-magic-space.test.ts` ОПУЩЕН этой задачей — не новой секцией
 * (она целиком на шкале и не добавила ни одного магического пикселя), а
 * удалением старого статус-бейджа выше по файлу; арифметика — в докблоке
 * самого теста.
 */
@layer components {

/*
 * ЛИЧНОСТЬ АВТОРА — портрет и имя ОДНОЙ строкой (спека §3.2: «Avatar 72px
 * (или инициал), `h1` + `@username`»). До этой задачи и круг, и его
 * плейсхолдер были набраны инлайновым `style=""` прямо в шаблоне — восемь
 * объявлений на узел, невидимых ни одному гейту CSS.
 *
 * `align-items: center`, а не `flex-start`: под именем стоит ровно одна
 * короткая строка (`@ник`), и выравнивание по верху оставило бы круг
 * висеть над пустотой при длинном имени в две строки — проверено на 390px
 * с самым длинным ником демо-набора.
 */
.rf-profile {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-block: var(--space-2) var(--space-3);
}

/*
 * `min-inline-size: 0` ОБЯЗАТЕЛЕН, а не гигиеничен: флекс-элемент по
 * умолчанию не сжимается уже своего min-content, и ник без пробелов
 * (`@Северный_Мотылёк` — подчёркивания точкой переноса не считаются)
 * распирал бы строку шире окна. Тот же минимум содержимого, что ловил
 * заголовок карточки, и та же пара к нему — `overflow-wrap` ниже: без
 * него обнулённый минимум ничего не даёт, слово просто вылезло бы за край
 * колонки.
 */
.rf-profile__names {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-inline-size: 0;
}

/*
 * `margin`/`font-size`/`color` — в группированном `.rf-work-byline,
 * .rf-profile__handle` (секция «карточка работы» выше по файлу, докблок
 * там, фикс-раунд 1, M-7). Здесь только то, что принадлежит НИКУ, а не
 * общему виду: перенос длинного слова без пробелов, см. докблок
 * `.rf-profile__names` выше — тот же минимум содержимого, эта половина
 * пары.
 */
.rf-profile__handle {
    overflow-wrap: anywhere;
}

/*
 * КОЛОНКА РАЗДЕЛОВ — 860px, ровно как лента фандома (`.rf-feed`), выдача
 * поиска (`.rf-search`) и главная (`.rf-home`). Один блок на ОБА экрана
 * задачи: у серии раздел один («Работы серии»), у автора до пяти, но
 * разделительная линия, кегль заголовка и решётка карточек у них одни и те
 * же — заводить серии свою копию значило бы развести два экрана, которые
 * спека §3.2 описывает одной фразой «тот же паттерн».
 */
.rf-sections {
    max-inline-size: 860px;
    margin-inline: auto;
    padding-inline: var(--space-4);
    padding-block-end: var(--space-8);
}

/*
 * РЯД РАЗДЕЛОВ — ЧИПЫ-ССЫЛКИ, А НЕ ПОЛОСА ВКЛАДОК, и это решение, а не
 * упрощение. Спека §3.2 называет ряд «табами», но вкладка обещает «одна
 * панель за раз»: у неё есть выбранная вкладка, и переключение меняет
 * содержимое. Здесь переключать нечем и нечего — публичная страница
 * обязана работать без единой строки JS (FE-7), а инвентарь §10 требует,
 * чтобы КАЖДЫЙ непустой раздел стоял на странице. Полоса вкладок без
 * выбранной вкладки и без панелей была бы обещанием, которого интерфейс
 * не выполняет (и `role="tab"` на ссылке-якоре сказал бы скринридеру
 * прямую неправду); чип-ссылка обещает ровно то, что делает, — прыжок к
 * разделу. Ряд рисуется только когда разделов больше одного: ссылка,
 * ведущая на единственный раздел двумя экранами ниже, — навигация ради
 * навигации.
 *
 * Своих правил у чипа здесь нет — только место ряда: сам чип красит
 * `.rf-chip` (`ui.css`, `@layer primitives`), включая 44px тап-таргета и
 * кольцо фокуса.
 */
.rf-sections__nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-start: var(--space-5);
}

.rf-sections__section {
    padding-block: var(--space-6);
    border-block-end: 1px solid var(--border);
}

/* Последний раздел линии не рисует: она отделяет разделы ДРУГ ОТ ДРУГА, а
   под последним читалась бы как граница страницы, которой нет. */
.rf-sections__section:last-child { border-block-end: none; }

.rf-sections__section h2 {
    margin: 0 0 var(--space-3);
    font-size: var(--fs-lg);
    font-weight: 600;
}

/*
 * `container-type: inline-size` — НЕ УКРАШЕНИЕ ОБЁРТКИ, А УСЛОВИЕ РАБОТЫ
 * КАРТОЧКИ: `.rf-card` берёт свой отступ и раскладку мета-строки из
 * `@container` (докблок секции «карточка работы»), то есть меряет
 * БЛИЖАЙШЕГО предка, объявившего себя контейнером. Без этой строки
 * ближайшим оказался бы контейнер другой страницы или вовсе никакой — и
 * карточки страницы автора остались бы с умолчаниями, не отреагировав на
 * ширину своей колонки.
 */
.rf-sections__feed {
    container-type: inline-size;
    display: grid;
    gap: var(--space-4);
}

.rf-sections__list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-base);
    color: var(--text-muted);
}

.rf-sections__list a { text-decoration: none; }

/* «Показать ещё» страницы автора уехало в общее правило `.rf-feed >
   .rf-loadmore, .rf-sections__feed > .rf-loadmore` выше по файлу
   (финальный фикс-раунд, I-5): одно решение на три ленты. */

/*
 * ТУМБСТОУН (410) — колонка уже, чем у разделов, и это единственное, что
 * задаёт правило: внутри ровно одно пустое состояние (`.rf-empty`,
 * `@layer primitives`) и ни одного контрола. Ширина 560px — та же мера,
 * что у широкой карточки входа и комплаенс-экранов: две короткие фразы,
 * растянутые на 860px, читались бы одной строкой поперёк экрана.
 */
.rf-tombstone {
    max-inline-size: 560px;
    margin-inline: auto;
    padding-block: var(--space-8);
    padding-inline: var(--space-4);
}

}
