/*
 * ИИАТОМ-Маркет — components.css
 *
 * Универсальные компоненты, используемые и публичной частью, и кабинетом. Каждый
 * интерактивный компонент несёт hover/focus-visible/active/disabled — половина набора
 * состояний считается недоделанным компонентом (CONTRACT.md, «Дизайн»). Цвет никогда не
 * единственный признак состояния: там, где это касается компонента, рядом есть текст/иконка.
 *
 * Только var(--am-*). Нужен оттенок, которого нет в tokens.css, — см. отчёт агента, а не
 * литерал здесь.
 */

/* =========================================================================================
 * 1. Кнопки
 * =======================================================================================*/

.am-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--am-s-2);
    border: 0;
    border-radius: var(--am-r-md);
    padding: var(--am-s-3) var(--am-s-5);
    font-family: var(--am-font);
    font-size: var(--am-fs-sm);
    font-weight: var(--am-fw-bold);
    line-height: 1;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color var(--am-dur-base) var(--am-ease-out),
                border-color var(--am-dur-base) var(--am-ease-out),
                color var(--am-dur-base) var(--am-ease-out),
                transform var(--am-dur-fast) var(--am-ease-out),
                opacity var(--am-dur-base) var(--am-ease-out);
}

.am-btn:active {
    transform: translateY(1px);
}

/* disabled и [aria-disabled] — ссылки-кнопки не понимают атрибут disabled. */
.am-btn[disabled],
.am-btn[aria-disabled="true"] {
    opacity: .45;
    cursor: not-allowed;
    pointer-events: none;
    transform: none;
}

/* Конвенция загрузки: <button class="am-btn ..." aria-busy="true"><span class="am-btn__label">Текст</span></button>
 * Текст гаснет, крутится кольцо — ширина кнопки не прыгает, потому что подпись остаётся в потоке
 * (visibility:hidden, не display:none). */
.am-btn[aria-busy="true"] {
    position: relative;
    pointer-events: none;
}

.am-btn[aria-busy="true"] .am-btn__label {
    visibility: hidden;
}

.am-btn[aria-busy="true"]::after {
    content: '';
    position: absolute;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid currentColor;
    border-right-color: transparent;
    opacity: .8;
    /* Длительность — из токена, не своя (CONTRACT.md §0/§10): 260ms×3 ≈ прежние 0.8s,
       для бесконечного вращения точность не критична, а лишней переменной в tokens.css
       заводить нельзя — этот файл чужой. */
    animation: am-spin calc(var(--am-dur-slow) * 3) linear infinite;
}

/* Primary — лайм, главный CTA бренда. */
.am-btn--primary {
    background: var(--am-action);
    color: var(--am-action-text);
    box-shadow: var(--am-sh-action);
}

/* Цвет текста в :hover повторяется намеренно. Кнопки рендерятся тегом <a>, а base.css задаёт
   глобальное `a:hover { color: var(--am-link-hover) }` (фиолетовый). У правил ниже специфичность
   класса без color этот глобальный hover не перебивает — и текст кнопки с заливкой на наведении
   уезжал в фиолетовый: на синем фоне читался почти как фон (замечено владельцем на стенде 30.07).
   Варианты без заливки (secondary/ghost/danger) свой color в :hover уже задают. */
.am-btn--primary:hover {
    background: var(--am-action-hover);
    color: var(--am-action-text);
    transform: translateY(-1px);
}

/* Primary — тёмный. Фон — инверсный слой (tokens.css, блок «Тёмная тема»): navy в светлой
 * теме, светлый в тёмной — иначе в тёмной теме кнопка сольётся со страницей. Этот фон
 * повторяется в нескольких компонентах (кнопка, активная пилюля, чекбокс) — не опечатка,
 * один и тот же приём инверсии применён к каждому. */
.am-btn--primary-dark {
    background: var(--am-surface-inverse);
    color: var(--am-text-on-inverse);
}

/* Заливка на hover — СЫРОЙ --am-blue, не семантический --am-link: последний в тёмной теме
 * светлеет ради читаемости КАК ТЕКСТ (tokens.css) и тогда белый текст поверх него перестаёт
 * держать контраст. Заливке кнопки нужен цвет, который НЕ меняется по теме — сырой токен
 * подходит для этого по определению (см. пояснение у --am-link в tokens.css). */
.am-btn--primary-dark:hover {
    background: var(--am-blue);
    color: var(--am-white);
}

/* Primary — градиент. */
.am-btn--primary-gradient {
    background: var(--am-grad-primary);
    color: var(--am-white);
}

/* Наведение делает градиент насыщеннее и чуть ГЛУБЖЕ, а не светлее: прежний brightness(1.1)
   поднимал светлоту фона и ронял контраст с белой подписью — кнопка на наведении читалась хуже,
   чем в покое, ровно наоборот от смысла состояния. */
.am-btn--primary-gradient:hover {
    filter: saturate(1.12) brightness(.94);
    color: var(--am-white);
}

/* Secondary outline — на инверсной поверхности (hero, модалки): полупрозрачная заливка от
 * текста этой поверхности, а не от фиксированного белого — иначе в тёмной теме, когда
 * поверхность светлеет, кнопка осталась бы белой-на-белом. */
.am-btn--secondary-dark {
    background: color-mix(in srgb, var(--am-text-on-inverse) 10%, transparent);
    color: var(--am-text-on-inverse);
    border: 1px solid var(--am-border-inverse);
}

.am-btn--secondary-dark:hover {
    border-color: var(--am-text-on-inverse-muted);
    color: var(--am-text-on-inverse);
}

/* Secondary outline — светлый фон. */
.am-btn--secondary {
    background: var(--am-surface);
    color: var(--am-text);
    /* --am-border-dashed указывает на тот же #C4CDF0, что и в макете у сплошной рамки этой
     * кнопки — имя токена говорит "dashed", но значение то самое; своего названия под
     * "сильную сплошную рамку" в семантическом слое нет. */
    border: 1px solid var(--am-border-dashed);
}

.am-btn--secondary:hover {
    border-color: var(--am-link);
    color: var(--am-link);
}

/* Ghost — текстовая кнопка-ссылка, без фона и рамки. */
.am-btn--ghost {
    background: none;
    padding: 0;
    color: var(--am-link);
}

.am-btn--ghost:hover {
    color: var(--am-link-hover);
}

/* Ghost на ИНВЕРСНОЙ поверхности. --am-link (#2A56FF) рассчитан на светлый фон; на тёмно-синей
 * шапке он даёт 3.14:1 при норме 4.5 — замер контраста поймал это на всех пяти публичных
 * страницах в СВЕТЛОЙ теме («Войти» рядом с лаймовой кнопкой). Инверсная поверхность константна
 * в обеих темах, поэтому и роль ссылки на ней одна. */
.am-site-header .am-btn--ghost,
.am-app-topbar .am-btn--ghost {
    color: var(--am-link-on-inverse);
}

.am-site-header .am-btn--ghost:hover,
.am-app-topbar .am-btn--ghost:hover {
    color: var(--am-text-on-inverse);
}

/* Danger — переиспользует семантику attention/error, отдельного «розового» токена нет и не нужно. */
.am-btn--danger {
    background: var(--am-surface);
    color: var(--am-state-attention-fg);
    border: 1px solid var(--am-state-attention-fg);
}

.am-btn--danger:hover {
    background: var(--am-state-attention-bg);
}

/* Иконка-кнопка: квадрат без текста, обязателен aria-label на самом <button>. */
.am-btn--icon {
    padding: var(--am-s-2);
    width: 36px;
    height: 36px;
    border-radius: var(--am-r-sm);
    background: transparent;
    color: var(--am-text-muted);
}

.am-btn--icon:hover {
    background: var(--am-surface-muted);
    color: var(--am-text);
}

/* Пилюля / сегментированный таб — активный сегмент передаёт состояние через aria-selected,
 * а не только цветом (текст остаётся читаемым в обеих цветовых схемах). */
.am-btn--pill {
    border-radius: var(--am-r-pill);
    padding: var(--am-s-2) var(--am-s-4);
    background: var(--am-surface);
    color: var(--am-text-label);
    border: 1px solid var(--am-border);
    font-weight: var(--am-fw-semi);
}

.am-btn--pill:hover {
    border-color: var(--am-link);
}

.am-btn--pill[aria-selected="true"],
.am-btn--pill.is-active {
    background: var(--am-surface-inverse);
    color: var(--am-text-on-inverse);
    border-color: var(--am-surface-inverse);
}

/* =========================================================================================
 * 2. Табы (группа пилюль)
 * =======================================================================================*/

.am-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--am-s-2);
    overflow-x: auto;
}

/* =========================================================================================
 * 3. Поля ввода
 * =======================================================================================*/

.am-field {
    display: block;
}

.am-field + .am-field {
    margin-top: var(--am-s-4);
}

.am-field__label {
    display: block;
    margin-bottom: var(--am-s-2);
    font-size: var(--am-fs-2xs);
    font-weight: var(--am-fw-bold);
    color: var(--am-text-label);
}

.am-field__hint {
    margin-top: var(--am-s-1);
    font-size: var(--am-fs-2xs);
    color: var(--am-text-subtle);
}

.am-field__input,
.am-field__textarea,
.am-field__select {
    width: 100%;
    /* Ширина колонки — источник правды: без min-width:0 длинная опция растягивает сам select
     * в flex/grid-родителе (сайдбар настроек) и выдавливает его за край панели. */
    min-width: 0;
    padding: var(--am-s-3) var(--am-s-4);
    border-radius: var(--am-r-md);
    border: 1px solid var(--am-border);
    background: var(--am-surface-sunken);
    color: var(--am-text);
    font-family: inherit;
    font-size: var(--am-fs-xs);
    /* Страховка на случай длинной подписи: закрытый select текст не переносит, и без этой пары
     * он обрывается на полуслове (скриншот селекта модели, 11.08.2026). Первично подпись
     * укорачивает сама витрина (parts/settings-panel.php, $am_model_short) — это второй рубеж. */
    text-overflow: ellipsis;
    transition: border-color var(--am-dur-base) var(--am-ease-out),
                background-color var(--am-dur-base) var(--am-ease-out);
}

.am-field__textarea {
    resize: vertical;
    min-height: 96px;
}

.am-field__input::placeholder,
.am-field__textarea::placeholder {
    color: var(--am-text-subtle);
}

.am-field__input:hover,
.am-field__textarea:hover,
.am-field__select:hover {
    border-color: var(--am-border-dashed); /* тот же #C4CDF0, что и сплошная рамка secondary-кнопки */
}

.am-field__input:focus-visible,
.am-field__textarea:focus-visible,
.am-field__select:focus-visible {
    border-color: var(--am-link);
    background: var(--am-surface);
    outline: 2px solid var(--am-focus);
    outline-offset: 1px;
}

.am-field__input[disabled],
.am-field__textarea[disabled],
.am-field__select[disabled] {
    background: var(--am-surface-muted);
    color: var(--am-text-subtle);
    cursor: not-allowed;
}

/* Ошибка поля — рамка + обязательный текст рядом (.am-field__error), не только цвет. */
.am-field--error .am-field__input,
.am-field--error .am-field__textarea,
.am-field--error .am-field__select {
    border-color: var(--am-state-attention-fg);
}

.am-field__error {
    display: flex;
    align-items: baseline;
    gap: var(--am-s-1);
    margin-top: var(--am-s-1);
    font-size: var(--am-fs-2xs);
    font-weight: var(--am-fw-semi);
    color: var(--am-state-attention-fg);
}

/* 29.08.2026 — «мягкая» подсказка (задача «звёздочка + подсветка по галочкам», п.б.2): поле,
 * которое отмеченный блок конструктора читает, но не требует жёстко (assets/js/app-tool-run.js
 * ::setSoftHint()) — НЕ красная, обычный приглушённый текст, чтобы не приучать игнорировать
 * настоящие предупреждения (.am-field__error выше) ложными срабатываниями. */
.am-field__softhint {
    margin-top: var(--am-s-1);
    font-size: var(--am-fs-2xs);
    font-style: italic;
    color: var(--am-text-subtle);
}

/* Инверсный вариант — модалка входа, форма обратной связи: живёт ВНУТРИ .am-modal/.am-drawer
 * и обязана перевернуться вместе с ними в тёмной теме, иначе поле останется тёмным на
 * посветлевшей модалке. */
.am-field--on-dark .am-field__label {
    color: var(--am-text-on-inverse);
}

.am-field--on-dark .am-field__input,
.am-field--on-dark .am-field__textarea,
.am-field--on-dark .am-field__select {
    background: var(--am-surface-inverse-2);
    border-color: var(--am-border-inverse);
    color: var(--am-text-on-inverse);
}

.am-field--on-dark .am-field__input::placeholder,
.am-field--on-dark .am-field__textarea::placeholder {
    color: var(--am-text-on-inverse-subtle);
}

.am-field--on-dark .am-field__input:focus-visible,
.am-field--on-dark .am-field__textarea:focus-visible,
.am-field--on-dark .am-field__select:focus-visible {
    border-color: var(--am-action);
    background: var(--am-surface-inverse-2);
}

/* Select — нативный, без кастомной стрелки (см. DESIGN_SYSTEM.md §2.5). */
.am-field__select {
    cursor: pointer;
}

/* =========================================================================================
 * 4. Чекбоксы
 * =======================================================================================*/

.am-checkbox {
    display: flex;
    align-items: flex-start;
    gap: var(--am-s-3);
    cursor: pointer;
    font-size: var(--am-fs-xs);
    color: var(--am-text);
}

.am-checkbox input[type="checkbox"] {
    appearance: none;
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    margin: 0;
    border: 1px solid var(--am-border-dashed);
    border-radius: var(--am-r-xs);
    background: var(--am-surface);
    cursor: pointer;
    position: relative;
    transition: background-color var(--am-dur-fast) var(--am-ease-out),
                border-color var(--am-dur-fast) var(--am-ease-out);
}

.am-checkbox input[type="checkbox"]:hover {
    border-color: var(--am-link);
}

.am-checkbox input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--am-focus);
    outline-offset: 2px;
}

.am-checkbox input[type="checkbox"]:checked {
    /* -2, не -inverse: чекбокс этого же тона живёт и ВНУТРИ инверсных поверхностей
     * (.am-cookie-banner .am-checkbox) — на фоне, который сам уже surface-inverse, чек слился
     * бы с контейнером. -2 на слой светлее/темнее (по теме) и остаётся отличимым в обоих
     * случаях: на светлой карточке формы и на инверсной поверхности cookie-баннера. */
    background: var(--am-surface-inverse-2);
    border-color: var(--am-surface-inverse-2);
}

.am-checkbox input[type="checkbox"]:checked::after {
    content: '';
    position: absolute;
    left: 6px;
    top: 2px;
    width: 5px;
    height: 10px;
    border: solid var(--am-text-on-inverse);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

.am-checkbox input[type="checkbox"][disabled] {
    opacity: .5;
    cursor: not-allowed;
}

.am-checkbox--error input[type="checkbox"] {
    border-color: var(--am-state-attention-fg);
}

/* =========================================================================================
 * 5. Бейджи и чипы
 * =======================================================================================*/

.am-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-1);
    padding: 4px var(--am-s-2);
    border-radius: var(--am-r-sm);
    font-size: var(--am-fs-2xs);
    font-weight: var(--am-fw-bold);
    line-height: 1.3;
    white-space: nowrap;
}

.am-badge--neutral    { background: var(--am-state-neutral-bg);    color: var(--am-state-neutral-fg); }
.am-badge--progress   { background: var(--am-state-progress-bg);   color: var(--am-state-progress-fg); }
.am-badge--attention  { background: var(--am-state-attention-bg);  color: var(--am-state-attention-fg); }
.am-badge--success    { background: var(--am-state-success-bg);    color: var(--am-state-success-fg); }
.am-badge--published  { background: var(--am-state-published-bg);  color: var(--am-state-published-fg); }
.am-badge--warn       { background: var(--am-state-warn-bg);       color: var(--am-state-warn-fg); }

/* Замер 02.08.2026: --am-state-*-fg рассчитан на ОБЫЧНУЮ (переворачивающуюся по теме)
 * поверхность страницы — на постоянно-тёмном hero/шапке (tokens.css, инверсный слой) тёмный fg
 * светлой темы (например #4C6B00 у --success) на такой же затемнённом тинте сливается почти
 * до нечитаемости (2.17:1 у шапки, 1.36:1 у бейджа в hero). Внутри этих поверхностей бейдж
 * переходит на --am-text-on-inverse — тот же приём, что уже применён к --am-eyebrow-on-inverse
 * рядом в tokens.css; заливка состояния остаётся (даёт лёгкий тональный акцент), различие
 * состояний по-прежнему держится текстом лейбла, а не только цветом (раздел 5.5, tokens.css). */
.am-home-hero .am-badge,
.am-catalog-hero .am-badge,
.am-category-hero .am-badge,
.am-tool-hero .am-badge,
.am-group-hero .am-badge,
.am-site-header .am-badge {
    color: var(--am-text-on-inverse);
}

/* Чип — тег/ключевое слово. */
.am-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-1);
    padding: var(--am-s-2) var(--am-s-4);
    border-radius: var(--am-r-pill);
    background: var(--am-surface-sunken);
    border: 1px solid var(--am-border-soft);
    font-size: var(--am-fs-2xs);
    font-weight: var(--am-fw-semi);
    color: var(--am-text-label);
    text-decoration: none;
}

.am-chip--lime {
    background: var(--am-state-success-bg);
    color: var(--am-text); /* тот же #101A3C, но через роль "текст", а не через сырой navy-600 */
    border-color: transparent;
    font-family: var(--am-font-mono);
}

.am-chip a:hover,
a.am-chip:hover {
    border-color: var(--am-link);
}

/* Замер 02.08.2026: та же лаймовая пилюля в шапке (.am-site-header__balance) сидит на
 * постоянно-тёмной инверсной поверхности — тинт --am-state-success-bg над ней даёт тёмную
 * оливу, а --text (тёмный в светлой теме) на ней читается на 2.17:1. Тот же переход на
 * --am-text-on-inverse, что у .am-badge внутри hero/шапки выше. */
.am-site-header .am-chip--lime {
    color: var(--am-text-on-inverse);
}

/* =========================================================================================
 * 6. Карточка
 * =======================================================================================*/

.am-card {
    background: var(--am-surface);
    border: 1px solid var(--am-border-soft);
    border-radius: var(--am-r-lg);
    padding: var(--am-pad-card);
    box-shadow: var(--am-sh-card);
}

.am-card--dark {
    background: var(--am-grad-panel);
    border-color: var(--am-border-inverse);
    color: var(--am-text-on-inverse);
    box-shadow: var(--am-sh-hero);
}

.am-card--interactive {
    transition: border-color var(--am-dur-base) var(--am-ease-out),
                box-shadow var(--am-dur-base) var(--am-ease-out),
                transform var(--am-dur-base) var(--am-ease-out);
}

.am-card--interactive:hover,
.am-card--interactive:focus-visible {
    border-color: var(--am-link);
    box-shadow: var(--am-sh-raised);
    transform: translateY(-2px);
}

/* =========================================================================================
 * 7. Аватар
 * =======================================================================================*/

.am-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: var(--am-r-full);
    background: var(--am-grad-avatar);
    color: var(--am-white);
    font-size: var(--am-fs-2xs);
    font-weight: var(--am-fw-bold);
    flex-shrink: 0;
}

/* =========================================================================================
 * 8. Таблица
 * =======================================================================================*/

.am-table-wrap {
    border: 1px solid var(--am-border-soft);
    border-radius: var(--am-r-lg);
}

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

.am-table th,
.am-table td {
    padding: var(--am-s-3) var(--am-s-4);
    text-align: left;
    border-bottom: 1px solid var(--am-border-soft);
}

.am-table th {
    font-family: var(--am-font-mono);
    font-size: var(--am-fs-2xs);
    text-transform: uppercase;
    letter-spacing: var(--am-ls-eyebrow);
    color: var(--am-text-subtle);
}

.am-table tbody tr:last-child td {
    border-bottom: 0;
}

/* =========================================================================================
 * 9. Аккордеон — на <details>/<summary>: раскрытие, Enter/Space и скринридер идут из браузера.
 * =======================================================================================*/

.am-accordion {
    background: var(--am-surface);
    border: 1px solid var(--am-border);
    border-radius: var(--am-r-lg);
    overflow: hidden;
}

.am-accordion + .am-accordion {
    margin-top: var(--am-s-3);
}

.am-accordion__trigger {
    display: flex;
    align-items: center;
    gap: var(--am-s-4);
    width: 100%;
    padding: var(--am-s-4) var(--am-s-5);
    cursor: pointer;
    list-style: none;
    font-weight: var(--am-fw-bold);
    color: var(--am-text);
}

.am-accordion__trigger::-webkit-details-marker {
    display: none;
}

.am-accordion__trigger:hover {
    background: var(--am-surface-sunken);
}

.am-accordion__icon {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: var(--am-r-sm);
    background: var(--am-surface-muted);
    flex-shrink: 0;
}

.am-accordion__icon::before {
    content: '+';
}

.am-accordion[open] .am-accordion__icon::before {
    content: '\2013'; /* – */
}

.am-accordion__panel {
    padding: 0 var(--am-s-5) var(--am-s-5);
    color: var(--am-text-muted);
}

/* =========================================================================================
 * 10. Модалка и дровер — нативный <dialog>. Один сброс UA-стилей на оба.
 * =======================================================================================*/

dialog.am-modal,
dialog.am-drawer {
    border: 0;
    padding: 0;
    color: var(--am-text-on-inverse);
    background: var(--am-grad-panel);
}

/* Задник — затемняет то, что ПОД модалкой, а не саму модалку: остаётся тёмным в обеих
 * темах намеренно (тот же приём, что и у большинства нативных sheet/dialog — задник глушит
 * контент независимо от темы, поэтому здесь единственное место, где сырой --am-navy-900
 * не переезжает в инверсный слой). */
dialog.am-modal::backdrop,
dialog.am-drawer::backdrop {
    background: color-mix(in srgb, var(--am-navy-900) 72%, transparent);
}

dialog.am-modal {
    width: min(420px, calc(100vw - 32px));
    border-radius: var(--am-r-2xl);
    box-shadow: var(--am-sh-modal);
    padding: var(--am-s-6);
}

.am-modal__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--am-s-4);
}

.am-modal__body {
    margin-top: var(--am-s-4);
}

/* Дровер — off-canvas панель слева, ограничена по ширине, чтобы не выехать за 360px-экран.
 * display ТОЛЬКО под [open]: UA-таблица прячет закрытый <dialog> правилом `:not([open])`,
 * но это правило имеет происхождение user-agent, а любое авторское normal-правило перебивает
 * UA независимо от специфичности — безусловный `display: flex` здесь означал, что закрытый
 * дровер оставался видимым (flex/visible/pointer-events:auto) и перехватывал клики на каждой
 * странице сайта (найдено замером стенда: elementFromPoint отдавал NAV вместо контента). */
dialog.am-drawer {
    position: fixed;
    inset: 0 auto 0 0;
    margin: 0;
    width: min(320px, 86vw);
    height: 100%;
    max-height: none;
}

dialog.am-drawer[open] {
    display: flex;
    flex-direction: column;
}

.am-drawer__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--am-s-4) var(--am-s-5);
    border-bottom: 1px solid var(--am-border-inverse);
}

.am-drawer__logo {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-2);
    color: var(--am-text-on-inverse);
    font-weight: var(--am-fw-black);
    letter-spacing: var(--am-ls-eyebrow);
}

.am-drawer__nav {
    padding: var(--am-s-4) var(--am-s-5);
    overflow-y: auto;
    flex: 1;
}

.am-drawer__menu {
    display: flex;
    flex-direction: column;
    gap: var(--am-s-1);
}

.am-drawer__menu a {
    display: block;
    padding: var(--am-s-3) var(--am-s-2);
    border-radius: var(--am-r-md);
    color: var(--am-text-on-inverse);
    font-weight: var(--am-fw-semi);
}

.am-drawer__menu a:hover,
.am-drawer__menu a:focus-visible {
    background: var(--am-border-inverse);
    color: var(--am-text-on-inverse);
}

.am-drawer__foot {
    display: flex;
    flex-direction: column;
    gap: var(--am-s-3);
    padding: var(--am-s-5);
    border-top: 1px solid var(--am-border-inverse);
}

/* =========================================================================================
 * 11. Тост
 * =======================================================================================*/

.am-toast-root {
    position: fixed;
    left: 50%;
    bottom: var(--am-s-6);
    transform: translateX(-50%);
    z-index: var(--am-z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--am-s-2);
    align-items: center;
    width: min(360px, calc(100vw - 32px));
}

.am-toast {
    display: flex;
    align-items: center;
    gap: var(--am-s-3);
    padding: var(--am-s-3) var(--am-s-5);
    border-radius: var(--am-r-lg);
    background: var(--am-surface-inverse);
    /* Полупрозрачная лаймовая рамка из макета — color-mix() от токена, а не свой rgba-литерал. */
    border: 1px solid color-mix(in srgb, var(--am-action) 40%, transparent);
    box-shadow: var(--am-sh-toast);
    color: var(--am-text-on-inverse);
    font-weight: var(--am-fw-semi);
    font-size: var(--am-fs-sm);
    animation: am-toast-in var(--am-dur-slow) var(--am-ease-out-soft);
}

.am-toast::before {
    content: '';
    width: 8px;
    height: 8px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--am-action);
}

/* Ошибка — тот же корпус тоста, но текст говорит об ошибке (см. app.js AM.toast), а точка
 * дополнительно красная: цвет не единственный признак, текст всегда описывает суть. */
.am-toast--error {
    border-color: var(--am-state-attention-fg);
}

.am-toast--error::before {
    background: var(--am-state-attention-fg);
}

@keyframes am-toast-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Общий fade+slide-up для hero-панелей публичных страниц (home.css, pricing.css) — раньше
 * был продублирован под двумя именами (am-home-hero-in/am-pricing-hero-in), одна и та же
 * анимация. Один keyframe здесь, страницы ссылаются на него по имени. */
@keyframes am-fade-up-in {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* =========================================================================================
 * 12. Скелетон загрузки
 * =======================================================================================*/

.am-skeleton {
    display: block;
    border-radius: var(--am-r-md);
    background: linear-gradient(90deg, var(--am-gray-200) 25%, var(--am-gray-100) 37%, var(--am-gray-200) 63%);
    background-size: 400% 100%;
    /* Длительность — из токена, не своя (см. am-spin выше): 260ms×5 ≈ прежние 1.4s. */
    animation: am-shimmer calc(var(--am-dur-slow) * 5) ease infinite;
}

.am-skeleton--text {
    height: 1em;
    width: 100%;
}

@keyframes am-shimmer {
    from { background-position: 100% 50%; }
    to   { background-position: 0 50%; }
}

@keyframes am-spin {
    to { transform: rotate(360deg); }
}

/* =========================================================================================
 * 13. Пустое состояние
 * =======================================================================================*/

.am-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--am-s-3);
    padding: var(--am-s-10) var(--am-s-5);
    background: var(--am-surface);
    border: 1px solid var(--am-border-soft);
    border-radius: var(--am-r-xl);
}

.am-empty-state--dashed {
    background: transparent;
    border: 1.5px dashed var(--am-border-dashed);
}

.am-empty-state__mascot {
    margin-bottom: var(--am-s-2);
    flex-shrink: 0;
}

/*
 * Пропорции маскота не должны зависеть от того, как его растянет флекс-контейнер:
 * в пустом состоянии дашборда картинку 640×960 сплющивало до 104×268 — узкий силуэт
 * с мыльными краями. Высота считается от ширины, object-fit страхует на случай,
 * когда родитель всё-таки задаёт обе стороны.
 */
.am-empty-state__mascot img {
    display: block;
    height: auto;
    max-width: 100%;
    object-fit: contain;
}

.am-empty-state__title {
    font-size: var(--am-fs-h3);
    font-weight: var(--am-fw-bold);
    color: var(--am-text);
}

.am-empty-state__desc {
    max-width: 42ch;
    color: var(--am-text-muted);
}

.am-empty-state__action {
    margin-top: var(--am-s-2);
}

/* =========================================================================================
 * 14. Хлебные крошки
 * =======================================================================================*/

/*
 * Кегль крошек — 14px, а не минимальные 12px. Крошки это навигация: по ним кликают и по ним
 * понимают, где находятся. Набранные предельно мелким шрифтом, они читались как случайная
 * техническая подпись над заголовком.
 */
.am-breadcrumbs__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--am-s-1);
    font-size: var(--am-fs-sm);
    color: var(--am-text-subtle);
}

.am-breadcrumbs__item {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-1);
}

.am-breadcrumbs__link {
    color: var(--am-text-subtle);
}

.am-breadcrumbs__link:hover {
    color: var(--am-link);
}

.am-breadcrumbs__current {
    color: var(--am-text-label);
    font-weight: var(--am-fw-semi);
}

/* =========================================================================================
 * 15. Блок цены запуска (parts/price-note.php)
 * =======================================================================================*/

.am-price-note {
    background: var(--am-surface-sunken);
    border: 1px solid var(--am-border-soft);
    border-radius: var(--am-r-lg);
    padding: var(--am-s-4);
}

.am-price-note__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--am-s-3);
}

.am-price-note__label {
    color: var(--am-text-muted);
    font-size: var(--am-fs-xs);
}

.am-price-note__value {
    font-family: var(--am-font-mono);
    font-weight: var(--am-fw-bold);
    color: var(--am-link-hover); /* см. .am-eyebrow в base.css — тот же пробел токенов */
}

.am-price-note__breakdown,
.am-price-note__remainder {
    margin-top: var(--am-s-2);
    font-size: var(--am-fs-2xs);
    color: var(--am-text-subtle);
}

.am-price-note--insufficient {
    border-color: var(--am-state-attention-fg);
}

.am-price-note__remainder--warn {
    color: var(--am-state-attention-fg);
    font-weight: var(--am-fw-semi);
}

/* =========================================================================================
 * 16. Cookie-баннер (parts/cookie-banner.php)
 *
 * Переехал из assets/css/pages/legal.css: раньше единственной причиной, по которой legal.css
 * подключался на КАЖДОЙ публичной странице (inc/class-am-assets.php, enqueue_public()), был
 * именно этот блок — юридический css ехал в общий трафик всего сайта. Компонент общий для всех
 * страниц → его место здесь, а legal.css остаётся только про сами юр.документы.
 * =======================================================================================*/

.am-cookie-banner {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: var(--am-z-toast);
    background: var(--am-surface-inverse);
    color: var(--am-text-on-inverse);
    box-shadow: var(--am-sh-sticky);
    border-top: 1px solid var(--am-border-inverse);
}

.am-cookie-banner[hidden] {
    display: none;
}

.am-cookie-banner__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--am-s-5);
    padding-block: var(--am-s-5);
}

.am-cookie-banner__text {
    flex: 1 1 320px;
}

.am-cookie-banner__title {
    font-weight: var(--am-fw-bold);
    margin-bottom: var(--am-s-1);
}

.am-cookie-banner__text .am-small {
    color: var(--am-text-on-inverse-muted);
}

.am-cookie-banner__text a {
    color: var(--am-text-on-inverse);
}

.am-cookie-banner__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--am-s-3);
    margin-left: auto;
}

.am-cookie-banner__settings {
    flex-basis: 100%;
    padding: var(--am-s-4);
    margin-top: var(--am-s-2);
    /* Оттенок от текста поверхности, не свой rgba(255,255,255,...): в тёмной теме текст
     * инверсной поверхности тёмный, и тот же приём даёт тёмный (а не белый-невидимый) оттенок. */
    background: color-mix(in srgb, var(--am-text-on-inverse) 5%, transparent);
    border: 1px solid var(--am-border-inverse);
    border-radius: var(--am-r-md);
    display: flex;
    flex-direction: column;
    gap: var(--am-s-3);
}

.am-cookie-banner__settings[hidden] {
    display: none;
}

.am-cookie-banner__save {
    align-self: flex-start;
}

.am-cookie-banner .am-checkbox {
    color: var(--am-text-on-inverse);
}

.am-cookie-banner .am-checkbox .am-small,
.am-cookie-banner .am-checkbox span {
    color: var(--am-text-on-inverse-muted);
}

.am-cookie-banner .am-checkbox input[type="checkbox"] {
    background: transparent;
    border-color: var(--am-border-inverse);
}

/* ≤700px (замер стенда — 390×844): раньше баннер занимал ~65% высоты экрана — крупные отступы
 * и три кнопки в три ряда одна под другой. Компактно: меньше вертикальных отступов, кнопки
 * в один ряд (равная ширина через flex:1, перенос текста внутри кнопки — не через контейнер),
 * текст мельче на шаг, но не ниже --am-fs-2xs (граница читаемости, tokens.css). */
@media (max-width: 700px) {
    .am-cookie-banner__inner {
        flex-direction: column;
        align-items: stretch;
        gap: var(--am-s-3);
        padding-block: var(--am-s-4);
    }

    /* Базовое flex: 1 1 320px (десктоп, строчная раскладка) в колоночной mobile-раскладке
     * читается как ВЫСОТА, а не ширина — блок текста растягивался на 320px сам по себе,
     * это и было основным виновником "баннер съедает 65% экрана", не только кнопки. */
    .am-cookie-banner__text {
        flex: none;
    }

    .am-cookie-banner__title {
        font-size: var(--am-fs-sm);
    }

    .am-cookie-banner__text .am-small {
        font-size: var(--am-fs-2xs);
    }

    .am-cookie-banner__actions {
        flex-wrap: nowrap;
        margin-left: 0;
        gap: var(--am-s-2);
    }

    .am-cookie-banner__actions .am-btn {
        flex: 1 1 0;
        min-width: 0;
        padding: var(--am-s-2) var(--am-s-2);
        font-size: var(--am-fs-2xs);
    }
}

@media print {
    .am-cookie-banner {
        display: none;
    }
}

/* =========================================================================================
 * 17. Шапка сайта
 * =======================================================================================*/

.am-site-header {
    position: sticky;
    top: 0;
    z-index: var(--am-z-sticky);
    /* Макет держит хедер полупрозрачным (rgba(8,11,32,.94) + blur) — семантического токена
     * под "тёмная поверхность с альфа-каналом" в tokens.css нет, поэтому фон сплошной.
     * surface-inverse, не сырой navy: хедер — тот же класс поверхности, что hero/модалка/тост,
     * и обязан перевернуться в тёмной теме тем же приёмом, иначе сольётся с новым --am-bg. */
    background: var(--am-surface-inverse);
    border-bottom: 1px solid var(--am-border-inverse);
}

.am-site-header__inner {
    max-width: var(--am-container);
    margin-inline: auto;
    display: flex;
    align-items: center;
    gap: var(--am-s-6);
    padding: var(--am-s-3) var(--am-s-5);
}

.am-site-header__logo {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-2);
    color: var(--am-text-on-inverse);
    font-family: var(--am-font-mono);
    font-weight: var(--am-fw-bold);
    letter-spacing: var(--am-ls-eyebrow);
    flex-shrink: 0;
}

.am-site-header__logo img {
    filter: drop-shadow(0 6px 16px color-mix(in srgb, var(--am-link-hover) 50%, transparent));
}

.am-site-header__nav {
    display: none;
}

.am-site-header__menu {
    display: flex;
    align-items: center;
    gap: var(--am-s-6);
}

.am-site-header__menu a {
    color: var(--am-text-on-inverse-muted);
    font-size: var(--am-fs-sm);
    font-weight: var(--am-fw-semi);
}

.am-site-header__menu a:hover {
    color: var(--am-text-on-inverse);
}

.am-site-header__actions {
    display: flex;
    align-items: center;
    gap: var(--am-s-3);
    margin-left: auto;
}

.am-site-header__profile {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-2);
    color: var(--am-text-on-inverse);
}

.am-site-header__profile-name {
    font-size: var(--am-fs-sm);
    font-weight: var(--am-fw-semi);
}

.am-burger {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 4px;
    width: 36px;
    height: 36px;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: var(--am-r-sm);
    cursor: pointer;
}

.am-burger span {
    display: block;
    width: 18px;
    height: 2px;
    background: var(--am-text-on-inverse);
    border-radius: var(--am-r-xs);
}

.am-burger:hover {
    background: var(--am-border-inverse);
}

/* ≥980px: полное десктоп-меню, бургер прячется (DESIGN_SYSTEM.md §1.6). */
@media (min-width: 980px) {
    .am-site-header__nav {
        display: block;
    }

    .am-burger {
        display: none;
    }
}

/* ≤700px (не 480 — см. ниже): обе гостевые кнопки уходят в дровер — места мало, а дровер их
 * содержит (.am-drawer__foot дублирует "Попробовать бесплатно", парт site-header.php).
 * ⛔ Найдено проверкой scrollWidth: раньше здесь пряталась только .am-site-header__guest-link
 * ("Войти") при ≤480px — вторая, более длинная кнопка .am-site-header__guest-cta ("Попробовать
 * бесплатно") оставалась и вместе с бургером выталкивала хедер за пределы вьюпорта (+117px на
 * 360px, +28px вплоть до ~650px — граница 480px саму проблему не закрывала). Порог поднят до
 * существующего брейкпоинта 700px (тот же, что у .am-site-header__profile-name ниже), а не
 * подобран заново — второго значения для соседней проблемы этот файл не заводит. */
@media (max-width: 700px) {
    .am-site-header__guest-link,
    .am-site-header__guest-cta {
        display: none;
    }
}

@media (max-width: 700px) {
    .am-site-header__profile-name {
        display: none;
    }
}

/* ⛔ Найдено той же проверкой scrollWidth (02.08.2026, дефект тёмной темы): у авторизованного
 * пользователя шапка на ≤360px переполнялась на 35px — .am-site-header__balance (лаймовая
 * пилюля с балансом) оставалась рядом с аватаром и бургером, места не хватало. От темы не
 * зависит (переполнение одинаковое в light/dark), но попало в тот же прогон. Тот же приём,
 * что уже применён выше к гостевым кнопкам: баланс дублирован в .am-drawer__foot
 * (parts/site-header.php) — прятать в шапке нечего терять, дровер даёт то же самое действие. */
@media (max-width: 700px) {
    .am-site-header__balance {
        display: none;
    }
}

/* =========================================================================================
 * 18. Подвал сайта
 * =======================================================================================*/

.am-site-footer {
    background: var(--am-surface);
    border-top: 1px solid var(--am-border);
    margin-top: var(--am-gap-section);
}

.am-site-footer__inner {
    display: flex;
    flex-wrap: wrap;
    gap: var(--am-s-8);
    padding-block: var(--am-s-10);
}

.am-site-footer__col {
    flex: 1 1 200px;
    min-width: 0;
}

.am-site-footer__col--brand {
    flex-basis: 260px;
}

.am-site-footer__logo {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-2);
    color: var(--am-text);
    font-weight: var(--am-fw-black);
}

.am-site-footer__desc {
    margin-top: var(--am-s-3);
    color: var(--am-text-muted);
    font-size: var(--am-fs-sm);
}

.am-site-footer__domain {
    margin-top: var(--am-s-2);
    font-family: var(--am-font-mono);
    font-size: var(--am-fs-2xs);
    color: var(--am-text-subtle);
}

.am-site-footer__title {
    font-family: var(--am-font-mono);
    font-size: var(--am-fs-eyebrow);
    letter-spacing: var(--am-ls-eyebrow);
    text-transform: uppercase;
    color: var(--am-text-subtle);
    margin-bottom: var(--am-s-3);
}

.am-site-footer__list {
    display: flex;
    flex-direction: column;
    gap: var(--am-s-2);
}

.am-site-footer__list a {
    color: var(--am-text-muted);
    font-size: var(--am-fs-sm);
}

.am-site-footer__list a:hover {
    color: var(--am-link);
}

.am-site-footer__bottom {
    padding-block: var(--am-s-5);
    border-top: 1px solid var(--am-border-soft);
    color: var(--am-text-subtle);
    font-size: var(--am-fs-2xs);
}

/* =========================================================================================
 * 19. Форма поиска
 * =======================================================================================*/

.am-search-form {
    display: flex;
    gap: var(--am-s-2);
}

.am-search-form__input {
    flex: 1;
}

/* =========================================================================================
 * 20. Экраны каталога и запуска — новые блоки под волну 1 (CONTRACT.md §9, заявка от Ф2/Ф3).
 *     Все шесть переиспользуют существующие примитивы (.am-card/.am-btn/.am-badge), а не
 *     заводят параллельный набор — новый визуальный язык здесь не нужен, нужны недостающие
 *     компоновки.
 * =======================================================================================*/

/* 20.1 Карточка категории — счётчик инструментов и минимальная цена. Разметка (Ф2 собирает):
 *   <a class="am-card am-card--interactive am-category-card" href="/<slug>/">
 *     <span class="am-category-card__icon">…</span>
 *     <span class="am-category-card__name">Нейросеть для фото</span>
 *     <p class="am-category-card__desc">…</p>
 *     <span class="am-category-card__meta">
 *       <span class="am-category-card__count">9 инструментов</span>
 *       <span class="am-category-card__price">от 12 токенов</span>
 *     </span>
 *   </a>
 * .am-card уже даёт фон/рамку/тень/радиус — здесь только внутренняя раскладка. */
.am-category-card {
    display: flex;
    flex-direction: column;
    gap: var(--am-s-3);
    text-decoration: none;
    color: inherit;
}

.am-category-card__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: var(--am-r-md);
    background: var(--am-state-progress-bg);
    color: var(--am-state-progress-fg);
    flex-shrink: 0;
}

.am-category-card__name {
    font-size: var(--am-fs-h4);
    font-weight: var(--am-fw-bold);
    color: var(--am-text);
}

.am-category-card__desc {
    color: var(--am-text-muted);
    font-size: var(--am-fs-sm);
}

.am-category-card__meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--am-s-3);
    margin-top: auto;
    padding-top: var(--am-s-3);
    border-top: 1px solid var(--am-border-soft);
    font-size: var(--am-fs-xs);
}

.am-category-card__count {
    color: var(--am-text-subtle);
}

.am-category-card__price {
    font-family: var(--am-font-mono);
    font-weight: var(--am-fw-semi);
    color: var(--am-text-label);
}

/* 20.2 Плитка пресета «до/после» — пресет выбирается кликом по картинке, не выпадающим
 * списком (прямое требование макета). Разметка:
 *   <div class="am-preset-grid" role="radiogroup" aria-label="Пресет">
 *     <button type="button" class="am-preset-tile" role="radio" aria-checked="true">
 *       <span class="am-preset-tile__compare">
 *         <img class="am-preset-tile__half" src="before.jpg" alt="До">
 *         <img class="am-preset-tile__half" src="after.jpg" alt="После">
 *       </span>
 *       <span class="am-preset-tile__label">Плёнка 90-х</span>
 *     </button>
 *     …
 *   </div>
 * <button role="radio"> — клавиатура и скринридер получают семантику выбора одного варианта
 * из браузера, второй набор обработчиков заводить не нужно (тот же принцип, что у .am-accordion
 * на <details>). */
.am-preset-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
    gap: var(--am-s-3);
}

.am-preset-tile {
    display: flex;
    flex-direction: column;
    gap: var(--am-s-2);
    padding: var(--am-s-2);
    background: var(--am-surface);
    border: 2px solid var(--am-border-soft);
    border-radius: var(--am-r-lg);
    cursor: pointer;
    text-align: left;
    transition: border-color var(--am-dur-base) var(--am-ease-out),
                transform var(--am-dur-base) var(--am-ease-out);
}

.am-preset-tile:hover {
    border-color: var(--am-border-dashed);
    transform: translateY(-1px);
}

.am-preset-tile:focus-visible {
    outline: 2px solid var(--am-focus);
    outline-offset: 2px;
}

.am-preset-tile[aria-checked="true"] {
    border-color: var(--am-link);
    background: var(--am-selected-bg);
}

.am-preset-tile__compare {
    display: flex;
    aspect-ratio: 1;
    border-radius: var(--am-r-md);
    overflow: hidden;
    position: relative;
}

.am-preset-tile__half {
    width: 50%;
    height: 100%;
    object-fit: cover;
    flex-shrink: 0;
}

/* Тонкий разделитель «до | после» посередине плитки — не подпись, а визуальная граница. */
.am-preset-tile__compare::after {
    content: '';
    position: absolute;
    inset: 0 0 0 50%;
    width: 1px;
    background: color-mix(in srgb, var(--am-white) 60%, transparent);
}

.am-preset-tile__label {
    font-size: var(--am-fs-2xs);
    font-weight: var(--am-fw-semi);
    color: var(--am-text-label);
}

/* Галочка выбранного пресета — вместо рамки-only, цвет не единственный признак состояния. */
.am-preset-tile[aria-checked="true"] .am-preset-tile__label::before {
    content: '✓ ';
    color: var(--am-link);
}

/* 20.3 «Спишется N, останется M» — тонкая строка прямо над кнопкой запуска. Отдельно от
 * .am-price-note (раздел 15, полноразмерная карточка расчёта) — здесь только итог перед CTA.
 * Разметка: <p class="am-run-summary">Спишется <b>27</b> · Останется <b>946</b></p> */
.am-run-summary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--am-s-2);
    font-size: var(--am-fs-xs);
    color: var(--am-text-muted);
}

.am-run-summary b {
    font-family: var(--am-font-mono);
    font-weight: var(--am-fw-bold);
    color: var(--am-text);
}

.am-run-summary__sep {
    color: var(--am-border-dashed);
}

/* Баланса не хватает — тот же приём, что у .am-price-note--insufficient: цвет + текст, не
 * только цвет (CONTRACT.md, «Дизайн»). */
.am-run-summary--insufficient b:last-of-type {
    color: var(--am-state-attention-fg);
}

/* 20.4 Панель результата — «Ещё вариант / Другой стиль / Повторить». Ряд вторичных кнопок,
 * ничего нового кроме раскладки: сами кнопки — существующие .am-btn--secondary/.am-btn--ghost.
 * Разметка: <div class="am-result-actions"><button class="am-btn am-btn--secondary">Ещё
 * вариант</button>…</div> */
.am-result-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--am-s-2);
}

.am-result-actions .am-btn {
    flex: 1 1 auto;
}

/* 20.5 Бейдж срока хранения — «доступно до 16 августа». Строится на .am-badge (раздел 5),
 * добавляет только точку-индикатор вместо цветной заливки: срок хранения — не статус
 * успеха/ошибки, самостоятельный смысловой ряд. */
.am-retention-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--am-s-1);
    font-size: var(--am-fs-2xs);
    color: var(--am-text-subtle);
}

.am-retention-badge::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--am-state-neutral-fg);
    flex-shrink: 0;
}

/* Меньше 3 дней до удаления — предупреждение, тем же приёмом «точка + текст», текст рядом
 * тоже обязан назвать срок словами (шаблон в parts/, не здесь). */
.am-retention-badge--soon {
    color: var(--am-state-warn-fg);
    font-weight: var(--am-fw-semi);
}

.am-retention-badge--soon::before {
    background: var(--am-state-warn-fg);
}

/* 20.6 Состояния результата — «истекло» и «модель просит дописать данные». Оба — человеческий
 * текст без служебных слов и прочерков (CONTRACT.md §10, п. 6), оба строятся на компактном
 * варианте .am-empty-state (раздел 13), а не заводят третий набор отступов/типографики.
 * Разметка:
 *   <div class="am-result-state am-result-state--expired">
 *     <p class="am-result-state__title">Результат больше недоступен</p>
 *     <p class="am-result-state__desc">Срок хранения истёк 2 августа. Мы храним файлы 14 дней,
 *       потом удаляем без возможности восстановить.</p>
 *   </div>
 *   <div class="am-result-state am-result-state--needs-input">
 *     <p class="am-result-state__title">Нужно немного уточнить</p>
 *     <p class="am-result-state__desc">Модели не хватило деталей — опишите, что должно быть
 *       на фото, и запустите ещё раз.</p>
 *     <button class="am-btn am-btn--primary am-result-state__action">Дополнить и повторить</button>
 *   </div> */
.am-result-state {
    display: flex;
    flex-direction: column;
    gap: var(--am-s-2);
    padding: var(--am-s-5);
    border-radius: var(--am-r-lg);
    background: var(--am-surface-sunken);
    border: 1px solid var(--am-border-soft);
}

.am-result-state__title {
    font-weight: var(--am-fw-bold);
    color: var(--am-text);
}

.am-result-state__desc {
    color: var(--am-text-muted);
    font-size: var(--am-fs-sm);
}

.am-result-state__action {
    align-self: flex-start;
    margin-top: var(--am-s-1);
}

/* Истёкший результат — приглушённый тон, действие не нужно (файла больше нет). */
.am-result-state--expired {
    background: var(--am-state-neutral-bg);
}

/* Модели не хватило данных — не ошибка провайдера (--am-state-attention), а рабочий шаг:
 * нужен один дозаполненный ответ, а не переотправка задачи. Отдельный, более мягкий тон. */
.am-result-state--needs-input {
    background: var(--am-state-progress-bg);
    border-color: transparent;
}

/* =========================================================================================
 * 21. Скролл-reveal (.am-observe/.am-inview) — пакет W2 «Анимации + вызывающие CTA».
 *
 * Прогрессивное улучшение: assets/js/animations.js добавляет .am-observe секциям публичных
 * страниц ПЕРЕД тем, как начать их наблюдать — без выполнения этого скрипта класс никогда не
 * появляется, а значит без JS ничего не прячется, всё видно сразу. .am-inview добавляется
 * IntersectionObserver'ом один раз, когда секция входит во вьюпорт (наблюдение снимается сразу
 * после — повторный вход в кадр анимацию не повторяет, это не декоративная петля).
 *
 * Ступенчатая задержка карточек внутри конкретной секции (например .am-home-directions__grid)
 * задаётся page-css самой секции через :nth-child от УЖЕ существующих классов ниже — второго
 * наблюдателя на каждую карточку заводить не нужно.
 * =======================================================================================*/

.am-observe {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity var(--am-dur-slow) var(--am-ease-out-soft),
                transform var(--am-dur-slow) var(--am-ease-out-soft);
}

/* will-change — точечно: только пока секция ждёт появления, не постоянно на странице. */
.am-observe:not(.am-inview) {
    will-change: opacity, transform;
}

.am-observe.am-inview {
    opacity: 1;
    transform: none;
}

/* =========================================================================================
 * 22. prefers-reduced-motion — замена анимаций осознанным статичным видом, а не просто
 *     заморозкой на первом кадре (глобальный сброс длительностей уже есть в tokens.css).
 * =======================================================================================*/

@media (prefers-reduced-motion: reduce) {
    .am-btn:active {
        transform: none;
    }

    .am-btn[aria-busy="true"]::after {
        animation: none;
        opacity: 1;
    }

    .am-toast {
        animation: none;
    }

    .am-skeleton {
        animation: none;
        background: var(--am-gray-200);
    }

    .am-card--interactive:hover {
        transform: none;
    }

    .am-btn--primary:hover {
        transform: none;
    }

    /* СТРОГО статично: даже если .am-observe каким-то образом успел появиться до того, как
       animations.js прочитал prefers-reduced-motion (скрипт и сам не запускает наблюдение
       в этом случае — см. докблок) — секция всё равно видна целиком, без сдвига. */
    .am-observe {
        opacity: 1;
        transform: none;
        transition: none;
        will-change: auto;
    }
}

/* Выбор голоса с прослушиванием (29.08.2026) — селект и кнопка в одну строку.
 * На узком экране кнопка уходит под селект: в 320 px они рядом не помещаются,
 * а обрезанная подпись «Послуш…» хуже переноса. */
.am-voice-pick__row {
    display: flex;
    gap: var(--am-s-2);
    align-items: center;
    flex-wrap: wrap;
}

.am-voice-pick__row .am-field__select {
    flex: 1 1 220px;
    min-width: 0;
}

/* В боковой панели настроек колонка узкая (~140 px) — там селект и кнопка становятся друг
 * под другом на всю ширину. Иначе подпись голоса обрезается до «Ровный, уни…», и выбор
 * снова превращается в угадывание, ради устранения которого всё и делалось. */
.am-settings-panel .am-voice-pick__row {
    flex-direction: column;
    align-items: stretch;
}

.am-settings-panel .am-voice-pick__row .am-field__select,
.am-settings-panel .am-voice-pick__play {
    width: 100%;
}

.am-voice-pick__play {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* Плитка звукового пресета (30.08.2026): вместо пары «До / После», которая для звука
 * ничего не значит, — одна иконка волны по центру. */
.am-presetpicker__preview-sound {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    color: var(--am-text-muted, #6b7688);
}
