/* Базовые элементы: типографика, поля, кнопки, сообщения, бейджи.
   Компоненты по дизайн-системе: кнопки-пилюли, плоские карточки без теней. */

* { box-sizing: border-box; }

body {
    margin: 0;
    font-family: var(--font);
    color: var(--text);
    background: var(--bg);
    font-variant-numeric: tabular-nums;
}

h1, h2, h3 { font-family: var(--font-head); letter-spacing: .3px; font-weight: 400; }

/* ---- поля ---- */
.field { margin-bottom: 16px; }
.field label { display: block; font-size: var(--fs-base); font-weight: 600; margin-bottom: 6px; }
.field input,
.field select {
    width: 100%;
    height: 44px;
    padding: 0 14px;
    font-family: var(--font);
    font-size: var(--fs-md);
    background: #fff;
    border: 1px solid #DEDEE4;
    border-radius: var(--radius-field);
    /* Свойства перечислены поимённо (/design П7). Сокращение без свойства —
       `transition: 150ms ease-out` — это `transition-property: all` со всеми
       его бедами: браузер следит и за тем, что меняться не должно. */
    transition: border-color var(--t-fast) ease-out, box-shadow var(--t-fast) ease-out;
}
/* :focus-visible вместо :focus (/design П6). Видимой разницы на текстовых
   полях нет и не будет — проверено браузером 18.08: поле ввода и выпадающий
   список браузер подсвечивает при любом способе фокуса, потому что в них
   можно сразу печатать. Правило приведено к общему виду ради единообразия:
   голый :focus + outline: none — это шаблон, из которого рождается поле без
   индикатора вообще. */
.field input:focus-visible,
.field select:focus-visible {
    outline: none;
    border-color: var(--blue);
    box-shadow: 0 0 0 3px rgba(30, 72, 255, .15);
}
.field input.bad { border-color: var(--neg); }
/* Единственный класс пояснения на весь проект (дизайн-система, § Система
   подсказок: закрытый список .hint / .warn-money / .empty-state — локальных
   аналогов не заводить). Внутри поля и карточки доступа — свои отступы ниже. */
.hint { font-size: var(--fs-xs); color: var(--muted); line-height: 1.5; }
.field .hint { margin-top: 5px; line-height: 1.45; }

/* ---- ширины полей в строке формы ----
   Одна шкала на весь проект (/design § Формы ввода → «Ширины полей»). Раньше
   она жила копиями в player-bonuses.css, player-money.css и player-contract.css
   с привязкой к #id раздела — на новом экране половина классов не действовала,
   и поля «болтались» по ширине (поймано пользователем 13.08 на живом кабинете).
   Раздел переопределяет размер только при содержательной причине. */
/* Высота у всех полей строки одна: select по умолчанию выше input на 2px, и
   поля стоят «лесенкой» (/design § Формы ввода → «Ширины полей» п.5) */
.af-row .fld input, .af-row .fld select, .af-row .fld textarea {
    height: 34px;
    box-sizing: border-box;
}
.af-row .fld.w-xs input, .af-row .fld.w-xs select { width: 78px; }
.af-row .fld.w-sm input, .af-row .fld.w-sm select,
.af-row .fld.w-sm .inp > input { width: 104px; }
.af-row .fld.w-md input, .af-row .fld.w-md select { width: 154px; }
.af-row .fld.w-lg input, .af-row .fld.w-lg select { width: 190px; }
.af-row .fld.w-dt input { width: 150px; }
/* Тянущееся поле обязано иметь потолок: без него «Турнир» занимает всю строку,
   и рядом стоят поля 104px и 700px — форма читается как рваная */
.af-row .fld.grow { flex: 1 1 220px; min-width: 150px; max-width: 360px; }
.af-row .fld.grow input { width: 100%; }
/* Комментарий — поле в одну строку, как соседние, а не «резиновый» textarea */
.af-row .fld.w-cm { flex: 1 1 200px; min-width: 160px; max-width: 320px; }
.af-row .fld.w-cm textarea {
    width: 100%;
    height: 34px;
    min-height: 34px;
    padding: 8px 10px;
    border: 1px solid #D8D8DE;
    border-radius: 8px;
    font: inherit;
    font-size: var(--fs-base);
    resize: vertical;
}
.af-row .fld.w-cm textarea:focus-visible {
    outline: none;
    border-color: var(--blue);
    box-shadow: 0 0 0 3px rgba(30, 72, 255, .15);
}
/* На узком экране шкала отключается целиком: иначе форма прокручивается вбок */
@media (max-width: 640px) {
    .af-row .fld.w-xs input, .af-row .fld.w-xs select,
    .af-row .fld.w-sm input, .af-row .fld.w-sm select, .af-row .fld.w-sm .inp > input,
    .af-row .fld.w-md input, .af-row .fld.w-md select,
    .af-row .fld.w-lg input, .af-row .fld.w-lg select,
    .af-row .fld.w-dt input { width: 100%; }
    /* На телефоне строка формы становится столбцом (`player-bonuses.css`), и
       главной осью flex-контейнера становится вертикаль: `flex-basis: 220px`
       у тянущегося поля превращается из ширины в ВЫСОТУ, и между полем и
       кнопкой зияет пустая полоса в две сотни пикселей. Поэтому основу
       сбрасываем, оставляя только снятый потолок ширины. */
    .af-row .fld.grow, .af-row .fld.w-cm { flex: 0 0 auto; max-width: none; }
}

/* ---- поле-заметка: свободный текст на свою строку ----
   Правило «комментарий — поле в одну строку» (`.w-cm` выше) писано под
   комментарий, стоящий РЯДОМ с суммой. Итоги созвона с кандидатом и результат
   внешних проверок — свободный текст на несколько фраз: в поле высотой 34px он
   обрезается на первой же. Поле занимает строку целиком и открыто на три
   строки. Ставится ВМЕСТЕ с `.w-cm` (оттуда вид рамки и шрифт), поэтому и
   правила стоят ниже — в том числе ниже узкого экрана: на телефоне заметка
   тоже занимает всю строку, а не сжимается по содержимому.
   Принято на демо Блока 7 (2026-08-20), перенесено из demo-moderator-players.css. */
/* Потолок ширины обязателен: в карточке во весь раздел строка текста
   растягивалась на 1100px и переставала читаться — глаз не находит начало
   следующей строки (находка ревью 24.08). 760px — та же мера, что у колонки
   текста в карточках кабинета игрока. */
.af-row .fld.w-note { flex: 1 1 100%; min-width: 0; max-width: 760px; }
.af-row .fld.w-note textarea { height: 74px; min-height: 74px; }

/* ---- заголовок группы полей внутри формы ----
   Анкета из пятнадцати полей без заголовков читается сплошняком (владелец
   20.08: «а можно эти блоки выделить, чтобы в глаза бросались?»). Заголовка
   группы ВНУТРИ формы в проекте не было: `.grp-head` живёт только внутри
   таблиц. Вид — тёмный полужирный текст с отбивкой сверху и тонкой чертой:
   группа видна раньше, чем читаются подписи полей. */
.aform .fgroup {
    font-size: var(--fs-md);
    font-weight: 700;
    color: var(--text);
    padding-top: 14px;
    border-top: 1px solid var(--line);
}
/* Первая группа идёт сразу под заголовком формы — своей черты не рисует */
.aform > .ft + .fgroup { padding-top: 4px; border-top: none; }

/* ---- отметка «Скопировано» в строке «подпись → значение» ----
   Отбивка всплывает над значением по его ЛЕВОМУ краю (`copy-value.css`).
   В строке `.info-row` значение прижато к ПРАВОМУ краю карточки, и плашка
   уезжала за её границу. Правило общее: то же самое ждёт любую строку
   «подпись → значение» с копируемым значением. */
.info-value .copy-done { left: auto; right: 0; }

/* ---- поля на телефоне: шрифт не мельче 16px ----
   Safari на iPhone приближает всю страницу, когда палец попадает в поле со
   шрифтом мельче 16px, и обратно масштаб приходится сводить пальцами вручную —
   на каждый ввод суммы. Телефон у нас основной сценарий, так что размер здесь
   функциональный, а не стилистический.
   `!important` — сознательно: base.css подключается раньше всех player-*.css,
   и при равной силе селектора локальные 13-14px разделов перебивают базовые.
   Перечислять эти разделы поимённо значит заводить список, который забудут
   пополнить на следующем экране.
   Десктоп не затронут — правило живёт только внутри медиазапроса. Кнопки,
   флажки и выбор файла исключены: клавиатуру они не открывают, зума нет. */
@media (max-width: 640px) {
    input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=range]):not([type=button]):not([type=submit]),
    select,
    textarea { font-size: 16px !important; }
}

/* ---- кнопки ---- */
.btn-primary,
.btn-ghost {
    font-family: var(--font);
    border-radius: 999px;
    cursor: pointer;
    /* Свойства поимённо (/design П7): сокращение без свойства означает
       `transition-property: all` — браузер тянет и то, что меняться не должно. */
    transition: filter var(--t-fast) ease-out, background var(--t-fast) ease-out,
                border-color var(--t-fast) ease-out, box-shadow var(--t-fast) ease-out;
}
.btn-primary {
    width: 100%;
    height: 46px;
    margin-top: 8px;
    font-size: var(--fs-md);
    font-weight: 700;
    color: #fff;
    background: var(--magenta-btn);
    border: none;
}
.btn-primary:hover:not(:disabled) { filter: brightness(.94); }
/* Нажатие — глубже наведения (/design § Кнопки п.4): без него кнопка кажется
   неотзывчивой на тач-экране, где наведения нет вовсе. */
.btn-primary:active:not(:disabled) { filter: brightness(.88); }
/* Выключенная — СЕРАЯ, а не выцветшая (/design § Кнопки п.4, сверка правил
   с кодом 19.08). Прозрачность оставляла кнопке фирменный цвет действия, и
   она читалась как «главная, просто бледная»; серый читается как «нельзя».
   Правило общее: копии разделов («Главная», «Загрузки», «Сессии», «Контракт»)
   держали его у себя, из-за чего на одном кабинете жили два разных языка. */
.btn-primary:disabled,
.btn-ghost:disabled {
    background: #EDEDF1;
    color: #A5A7AE;
    border-color: #EDEDF1;
    opacity: 1;
    filter: none;
    cursor: not-allowed;
}
.btn-ghost {
    padding: 8px 16px;
    font-size: var(--fs-base);
    background: #fff;
    border: 1px solid #DEDEE4;
}
.btn-ghost:hover:not(:disabled) { filter: brightness(.97); }
/* :not(:disabled) обязателен и здесь: без него выключенная кнопка при нажатии
   заливается и читается как рабочая (/design § Кнопки п.4) */
.btn-ghost:active:not(:disabled) { background: #EAEAEE; }
.btn-primary:focus-visible,
.btn-ghost:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(30, 72, 255, .35); }

/* ---- крестик закрытия формы ----
   Форма, которая ОТКРЫВАЕТСЯ действием, закрывается двумя способами: крестиком
   в углу и кнопкой «Отмена» внизу. Раньше крестик жил только в «Загрузках»
   (класс #pageUploads .uform-close), и на остальных вкладках форму можно было
   убрать единственным способом — доехать до «Отмены» (замечание пользователя
   на приёмке 18.08). Родителю нужен `position: relative`. */
.form-close {
    position: absolute;
    top: 10px;
    right: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: #8A8C93;
    font-size: var(--fs-lg);
    line-height: 1;
    cursor: pointer;
    transition: background var(--t-fast) ease-out, color var(--t-fast) ease-out;
}
.form-close:hover { background: #EAEAEE; color: var(--text); }
.form-close:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(30, 72, 255, .35); }

/* ---- сообщения: первая строка что случилось, вторая что делать ---- */
.msg { font-size: var(--fs-base); padding: 10px 12px; border-radius: var(--radius-field); margin-bottom: 16px; line-height: 1.55; }
.msg b { display: block; font-weight: 600; }
.msg span { display: block; margin-top: 2px; opacity: .85; }
.msg.err { background: var(--badge-danger-bg); color: var(--badge-danger-text); }
.msg.warn { background: var(--badge-pending-bg); color: var(--badge-pending-text); }
.msg.info { background: rgba(30, 72, 255, .08); color: #12308f; }
.msg.ok { background: var(--badge-ok-bg); color: var(--badge-ok-text); }
.msg.hidden { display: none; }

/* ---- бейджи статусов ---- */
/* Предупреждение о денежном последствии у самой кнопки — второй класс из
   закрытого списка пояснений (/design § Система подсказок п.11). До раздела
   «Деньги» его никто не применял, поэтому описания не существовало и текст
   выводился голым (поймано пользователем 11.08). */
.warn-money {
    /* По ширине текста, а не во всю строку: стоит рядом с итогом операции
       в том же ряду (правка 14.08) */
    display: inline-block;
    font-size: var(--fs-sm);
    color: #7a4a00;
    background: #FFF6E5;
    border-radius: var(--radius-field);
    padding: 5px 11px;
    margin: 0;
    line-height: 20px;
}

.badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: var(--fs-xs); }

/* Полный степпер «весь путь заявки» (/design § Статусы): нужен отправителю
   в баннере — он показывает, чья сейчас очередь и что будет дальше. */
.stepper { display: flex; align-items: flex-start; margin: 0 0 16px; max-width: 520px; }
.stepper .step { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 96px; flex-shrink: 0; }
.stepper .dot {
    width: 26px; height: 26px; border-radius: 50%; background: #fff;
    border: 2px solid #C9CBD3; color: #8A8C93; font-size: var(--fs-sm); font-weight: 700;
    display: flex; align-items: center; justify-content: center;
}
/* Единое правило заливки на оба степпера (решение 14.08): пройденное ЗАЛИТО
   зелёным, текущий шаг — контур синим, будущее серое. До этого полный степпер
   красил ровно наоборот (пройденное контуром, текущее заливкой), и одно и то
   же состояние на соседних экранах читалось по-разному. */
.stepper .step.done .dot { background: var(--pos); border-color: var(--pos); color: #fff; }
.stepper .step.active .dot { background: #fff; border-color: var(--blue); color: var(--blue); }
.stepper .s-label { font-size: var(--fs-xs); line-height: 1.35; text-align: center; color: #5c5e66; }
.stepper .step.active .s-label { color: var(--text); font-weight: 600; }
.stepper .s-line { flex: 1; height: 2px; background: #D5D7DE; margin-top: 13px; min-width: 16px; }

/* Мини-степпер «чья очередь» (/design § Статусы): заменяет бейдж в той же
   ячейке. Отказ — красный КВАДРАТ, а не круг: форма несёт смысл наравне
   с цветом, это различимо при дальтонизме. */
.stepper-mini { display: inline-flex; gap: 4px; vertical-align: middle; margin-right: 6px; }
.stepper-mini i { width: 9px; height: 9px; border-radius: 50%; background: #D5D7DE; display: block; }
.stepper-mini i.done { background: var(--pos); }
/* Текущий шаг — НЕ залитая точка в контуре акцентного цвета: пройденное
   отличается от «сейчас здесь» не только цветом, но и заливкой (решение 13.08,
   распространено на полный степпер 14.08 — правило одно на оба) */
.stepper-mini i.active { background: #fff; box-shadow: inset 0 0 0 2px var(--blue); }
.stepper-mini i.fail { background: var(--neg); border-radius: 2px; }
.st-word { font-size: var(--fs-sm); }
.st-word.ok { color: var(--pos); }
.st-word.bad { color: var(--neg); }
.st-word.wait { color: var(--badge-pending-text); }
.badge-approved { background: var(--badge-ok-bg); color: var(--badge-ok-text); }
.badge-pending { background: var(--badge-pending-bg); color: var(--badge-pending-text); }
.badge-danger { background: var(--badge-danger-bg); color: var(--badge-danger-text); }

/* ---- требования к паролю: показываются ДО ввода и отмечаются по мере набора.
       Один компонент на вход и на смену пароля в «Профиле» — правила пароля
       у продукта одни, и выглядеть они должны одинаково. ---- */
.req { list-style: none; padding: 0; margin: 8px 0 0; font-size: var(--fs-xs); line-height: 1.75; }
.req li { position: relative; padding-left: 19px; color: var(--muted); }
.req li::before { content: '○'; position: absolute; left: 3px; top: -1px; }
.req li.ok { color: var(--pos); }
.req li.ok::before { content: '●'; }

/* ---- индикатор ожидания на кнопке ---- */
.spinner {
    display: inline-block;
    width: 13px; height: 13px;
    margin-right: 7px;
    vertical-align: -2px;
    border: 2px solid rgba(255, 255, 255, .45);
    border-top-color: #fff;
    border-radius: 50%;
    animation: spin .7s linear infinite;
}
/* Кружок ожидания живёт ВНУТРИ кнопки, которую на время запроса выключают, —
   а выключенная кнопка светло-серая. Белый кружок на ней исчезает полностью
   (контраст 1.06 при норме 3), и человек не видит, что запрос идёт: он жмёт
   «Сохранить», кнопка гаснет, дальше тишина. Поймано замером 19.08 в четырёх
   разделах. Правило «кружок ожидания НЕ гасим» — /design § Премиум-детали. */
:disabled > .spinner,
:disabled .spinner {
    border-color: rgba(90, 92, 100, .35);
    border-top-color: #5A5C64;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- скелетоны загрузки ---- */
.skel { background: #E4E4EA; border-radius: 8px; height: 22px; animation: skeleton-pulse 1.2s ease-in-out infinite; }
.skel.sm { height: 12px; width: 60%; margin-top: 8px; }
/* Каркас раздела на время загрузки (ui.skeleton): плашки повторяют форму
   будущего содержимого — ряд карточек и строки списка. */
.skel-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin-bottom: 14px; }
.skel-card { padding: 14px 16px; background: var(--card); border-radius: var(--radius-card); }
.skel-card .skel.sm { margin-top: 0; margin-bottom: 10px; }
.skel-row { height: 34px; margin-bottom: 8px; }
@keyframes skeleton-pulse { 0%, 100% { opacity: .6 } 50% { opacity: 1 } }

.hidden { display: none !important; }

/* Атрибут [hidden] обязан прятать элемент, даже если у него задан свой display
   (flex/grid/block перебивают атрибут — тогда ошибки висят под каждым полем сразу).
   Ловушка ловилась дважды: демо 07.08 и Блок 5; правило /design § Формы п.11 без
   этой строки не срабатывало. Страховка — tests/e2e/hidden-attr.spec.js. */
[hidden] { display: none !important; }

/* Текст только для программ чтения экрана: живая область результата действий
   (копирование) должна лежать в разметке заранее и быть пустой. */
.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ---- кнопки внутри строки формы ----
   Базовая `.btn-primary` растянута на всю ширину — она рассчитана на форму
   входа. В строке формы (`.af-row`) кнопка обязана быть по содержимому и
   одного роста с полями, иначе «Сохранить» ложится отдельной полосой.
   Правило общее: разделы игрока держали свои копии (`#pageBonuses` и др.),
   и каждый новый раздел заводил бы ещё одну. Копии специфичнее и продолжают
   побеждать — их не трогаем. */
.af-row .btn-primary,
.af-row .btn-ghost {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: auto;
    height: 34px;
    margin-top: 0;
    padding: 0 16px;
    font-size: var(--fs-base);
    white-space: nowrap;
}
.af-row .btn-primary { font-weight: 700; }
/* Кнопка встаёт на уровень ПОЛЕЙ, а не подписей: у поля первый ряд сетки —
   подпись (16px + 5px зазор), у кнопки его нет, и без отступа она уезжает
   вверх (поймано на демо Блока 2 — форма долива). */
.af-row > .btn-primary,
.af-row > .btn-ghost,
.af-row > .af-calc { margin-top: 21px; }

/* ---- знак валюты внутри поля суммы ----
   /design § Формы ввода п.12: знак стоит ВНУТРИ поля, а не отдельной подписью
   рядом — иначе он липнет к рамке и съезжает от неё при переносе строки.
   Правило было только у раздела «Деньги» игрока (#pageMoney), и новая форма
   получала знак, болтающийся сбоку (поймано на демо Блока 2). */
.inp { position: relative; display: block; }
.inp > i {
    position: absolute;
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
    font-style: normal;
    font-size: var(--fs-base);
    color: #8A8C93;
    pointer-events: none;
}
.inp > input { padding-left: 24px; }

/* ---- итог операции под полями формы ----
   Стабильная строка ПОД полями: не толкает кнопку вниз, переносится на узком
   экране. Отказ по потолку (`.fld-err`) — плашкой В ТОМ ЖЕ РЯДУ, что и жёлтая
   подсказка `.warn-money` (одна выключена, когда включена другая — та же пара
   «предупреждение / блокировка», что и `.warn-money.over` в разделе
   «Загрузки»): полноширинная строка ниже читалась как отдельная ошибка формы,
   а не как пояснение к сумме, и на узком экране уводила таблицу за край
   (правка 19.08, было — демо-гейт Блока 2). Правило общее — копии разделов
   (`#pageMoney`, `#pagePlayerHome`) специфичнее и продолжают побеждать. */
.af-result {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-top: 12px;
    min-height: 22px;
    flex-wrap: wrap;
}

/* 🔴 Серая плашка расчёта — ширина ПО ТЕКСТУ, не во всю строку формы.
   Правило владельца 19.08 («пустая заливка справа читается как здесь что-то
   должно быть») было записано в COMPONENTS.md только для жёлтой `.msg warn`,
   а серая `.af-calc` — эталон вида для всего ряда плашек — его не получила и
   осталась блочной. Замер боевой формы корректировки 07.09: плашка 1482px при
   тексте 354px, четыре пятых — пустая заливка.
   Внутри `.af-result` плашка и так ведёт себя правильно (там гибкий ряд), а
   прямым ребёнком формы — нет; правило адресовано именно этому случаю.
   Замечание владельца 07.09, найдено на демо и подтверждено на бою. */
.aform > .af-calc { display: inline-block; width: auto; }
.af-result .fld-err {
    display: inline-block;
    width: auto;
    flex: 0 1 auto;
    /* 🔴 Типографика всех плашек ряда — ОДНА (решение владельца 27.08,
       повторено 03.09 на демо Блока 3). Эталон — серая плашка расчёта
       `.af-calc` (--fs-base, обычное начертание): красная и жёлтая были на
       ступень мельче, красная ещё и полужирной, и три сообщения об одном и том
       же поле читались как три разных по важности голоса. Разницу несёт ЦВЕТ
       («считаю» / «предупреждаю» / «запрещаю»), а не размер и вес шрифта.
       Правило было записано 27.08, но заперто под `.aform.send-form`
       (moderator-transfers.css:170) — то есть жило ровно на одной форме
       проекта, а каждая следующая рождалась с прежним разнобоем. Здесь оно
       становится общим; копии разделов (`#pageMoney`, `#pagePlayerHome`)
       специфичнее и перекрывают его — их перевод отдельным сводом. */
    font-size: var(--fs-base);
    font-weight: 400;
    line-height: 20px;
    color: #7A1119;
    background: #FDECEA;
    /* 🔴 Совпадать обязан ВЕСЬ вид плашки, а не только шрифт: отступы и
       скругление берутся у эталона `.af-calc` (7px 11px, 8px). Правка 03.09
       по замечанию владельца — он назвал это ВТОРОЙ раз, потому что в первый
       я сверил только размер и вес шрифта, а на экране плашки остались разной
       высоты (30px против 34px) и с разным скруглением. Урок: «выглядит
       одинаково» проверяется сверкой ВСЕХ свойств коробки, а не тех трёх,
       что назвал сам. */
    border-radius: 8px;
    padding: 7px 11px;
    margin: 0;
}
/* Жёлтая плашка ряда — тем же видом целиком (шрифт + коробка) */
.af-result .warn-money {
    font-size: var(--fs-base);
    line-height: 20px;
    border-radius: 8px;
    padding: 7px 11px;
}

/* ---- пустое состояние раздела ----
   Карточка с пунктирной рамкой вместо голой строки «ничего нет»: `.es-t` —
   что именно пусто, `.es-s` — что с этим делать (формула /design § Пустые
   состояния). Правила общие, потому что состояние общее: разделы игрока
   держали свои копии под `#pageMoney` и `#pageContract`, и кабинет модератора
   завёл бы третью. Копии разделов специфичнее и продолжают побеждать —
   их не трогаем (файлы кабинета игрока правит соседняя задача). */
.empty-state {
    background: #fff;
    border: 1px dashed #D8D8DE;
    border-radius: 12px;
    padding: 22px 18px;
    text-align: center;
}
.es-t { font-weight: 600; font-size: var(--fs-md); margin-bottom: 5px; }
.es-s { font-size: var(--fs-base); color: var(--muted); line-height: 1.5; }

/* ---- «уменьшить движение» ----
   Гасим движение адресно, а не глухим `* { animation: none !important }`
   (/design П5). У глухого варианта две беды, обе поймал демо-гейт 18.08:
   1) вместе с украшениями он гасил ПОЛЕЗНОЕ — кружок ожидания на кнопке
      замирал, и человек не видел, что запрос идёт: кнопка выглядела просто
      заблокированной;
   2) `!important` на всё делает невозможным «оставить мягкое проявление
      вместо резкого скачка», а именно это правильная реакция на настройку.
   Настройка означает «меньше движения», а не «интерфейс перестаёт отвечать». */
@media (prefers-reduced-motion: reduce) {
    /* Подъём гасим ЧЕРЕЗ ТОКЕН, а не отменой правил. Причина техническая:
       правила подъёма живут в разделах и написаны через `#pageПравило`, а такой
       селектор сильнее любого правила отсюда — медиа-запрос силы не добавляет,
       и отмена молча не срабатывала (поймано тестом motion.spec 18.08).
       Ноль в токене выключает подъём всюду, где он им задан. */
    :root { --lift-hover: 0px; }

    /* Здесь НЕТ правил вида `.stat { transition: none }` — они были и не
       работали (найдено ревью 18.08): правило из общего файла проигрывает и
       селектору с `#id`, и правилу равной силы из файла раздела, который
       подключён позже. Всё, что нужно погасить в разделах, гасится либо
       токеном выше, либо на месте — через `@media (prefers-reduced-motion:
       no-preference)` вокруг самого перехода. Мёртвое правило хуже
       отсутствующего: оно выглядит как выполненное требование. */

    /* пульсация скелетона — постоянное движение на весь экран загрузки */
    .skel { animation: none; }

    /* Кружок ожидания НЕ выключаем, только замедляем: это не украшение,
       а единственный признак, что кнопка занята делом. */
    .spinner { animation-duration: 1.4s; }
}


/* ══════════════════════════════════════════════════════════════════════════
   ФОРМА: три правила владельца — одна точка исполнения (введено 06.09)
   ══════════════════════════════════════════════════════════════════════════

   Зачем этот блок вообще появился. На Блоке 2 тренера владелец назвал ТРИ
   дефекта форм по три раза каждый (журнал `docs/retro/block2-trainer-remarks.md`,
   строка блока в `docs/retro/workflow-metrics.md`). Каждый раз их чинили ПО
   МЕСТУ ПОКАЗА — правило переписывали под `#pageTrainerStudents` и под тот
   класс формы, на который показал владелец. Соседняя форма того же экрана под
   правку не попадала, и владелец называл дефект снова. Разбор 05.09 назвал это
   главной причиной повторов: «чинилось по месту показа, а не там, где вид
   собирается для всех форм».

   Поэтому правила стоят ЗДЕСЬ, в `base.css`: это единственный файл, который
   подключён на каждой странице продукта — напрямую (`cabinet.html`,
   `leader.html`, `moderator.html`, `index.html`, все demo-*) либо через
   `cabinet-base.css` (`trainer.html`). Классы, на которые они написаны
   (`.aform`, `.af-row`, `.fld`, `.form-close`), общие всем четырём комплектам
   форм проекта — свод разметки 06.09 по `trainer-training-forms.js`,
   `player-money-form-kit.js`, `moderator-transfers-form-kit.js` и
   `leader-money-ops-forms.js` дал 4 из 4.

   🔴 Блок ДОБАВЛЯЕТ правила и не меняет ни одного существующего. Разделы,
   написанные через `#pageЧто-то`, сильнее по силе селектора и продолжают
   побеждать — их вид не поедет. Смысл блока в другом: НОВАЯ форма и форма
   раздела, который своей копии правила не завёл, получают верный вид сразу,
   без третьего замечания владельца.

   Машинная проверка каждого правила — в гейте показа:
   `scripts/demo-gate/form-close-gap.js`, `form-group.js`, `form-rhythm.js`.
   Словами правила записаны в `.claude/skills/design/references/forms.md`
   § Форма: три правила с одной точкой исполнения. */

/* ---- 1. Крестик закрытия не отбирает у формы целую строку ----

   Владелец, замечание 18 (02.09, скриншот со стрелкой вверх — в пустую полосу
   над полями): «перенести этот блок в „Запланировать тренировку“ выше». До
   этого то же самое реплика 2 первой пачки: «где дата, формат, подтип, тема —
   нужно выше, примерно на уровне крестика». И до этого — находка 1 предпоказа,
   где место под крестик отбирал `padding-right: 44px` у всей формы и кнопка
   уезжала на второй ряд.

   Различение, к которому пришли: отдельная строка нужна форме С ЗАГОЛОВКОМ —
   там крестик встаёт на линию заголовка. У формы БЕЗ заголовка первая строка —
   сами поля, и крестик кладётся в угол ПОВЕРХ ряда.

   🔴 Место крестику даёт ПЕРВЫЙ РЯД, а не форма целиком: `padding-right` самой
   формы отобрал бы 32px у КАЖДОЙ строки, включая строку отказа под кнопкой. */
.aform > .form-close + .af-row,
.add-form > .form-close + .af-row { padding-right: 32px; }

/* Форма с заголовком: заголовок обтекает крестик и черты не рисует — он ПЕРВЫЙ
   в форме, отделять его сверху не от чего (то же, что `.aform > .ft + .fgroup`
   выше по файлу, только для действующего носителя `h3`). */
.aform > .form-close + h3 {
    padding-right: 34px;
    padding-top: 0;
    border-top: none;
}

/* На узком экране крестику возвращается своя строка: место в ряду ему там не
   нужно, иначе поля не дотягиваются до правого края (поля на узком идут во всю
   ширину строки — § Ширины полей п.4). */
@media (max-width: 640px) {
    .aform > .form-close + .af-row,
    .add-form > .form-close + .af-row { padding-right: 0; }
}

/* ---- 2. Группу полей в форме отделяет ВОЗДУХ, а не черта ----

   Владелец снял эту полосу дважды подряд одной пачкой — замечание 20 (два
   скриншота: «убрать эту разделительную полосу» и «эту разделительную полосу
   тоже убрать») и замечание 24 («убрать эту полосу»). Третий раз про неё же —
   замечание 6, но там речь была о слипшемся ВИДЕ: «полоса, отделяющая, она
   просто слеплена с блоком. Опять эта ошибка везде вылезает, и мы её повторяем
   раз за разом». Замер тогда дал над чертой 0px — черта стояла вплотную к
   предыдущему полю и читалась КОНЦОМ прошлого блока, а не началом нового.

   Итог обоих требований: черты нет, границу держат отбивка и жирный заголовок.
   Отбивка 18px сверху = 12px общего ритма + 6px своей добавки; 12px под
   заголовком до первого поля — ровно тот же ритм, что между полями.

   🔴 Общее правило `.aform .fgroup` выше по файлу (черта + `padding-top: 14px`)
   НЕ трогаем: оно осталось верным для длинных анкет вне формы. Здесь правило
   той же силы, но написанное на прямого потомка формы — оно и побеждает внутри
   формы, а вне формы поведение прежнее. */
.aform > .fgroup,
.aform > .aform-body > .fgroup {
    margin-top: 18px;
    padding-top: 12px;
    border-top: none;
}

/* Первая группа стоит сразу под заголовком формы — своей отбивки не удваивает */
.aform > .form-close + h3 + .fgroup,
.aform > h3 + .fgroup { margin-top: 12px; }

/* ---- 3. Ритм формы доходит до полей, и форма не едет вбок ----

   Владелец, замечание 5 (02.09): «очень сплюснута вся информация, всё близко,
   неудобно. Должны быть больше отступы между каждым из блоков, которые
   заполняются: „Что разбирали“, „Прогресс по прошлому ДЗ“, „Новое ДЗ“ — вот это
   всё». 🔴 Замер вскрыл не «мало отступа», а ОТСУТСТВИЕ его: все одиннадцать
   зазоров формы отчёта были РОВНО 0px.

   Причина механическая и общая для всего проекта: правило ритма `.aform > * + *`
   до полей НЕ достаёт, когда содержимое завёрнуто в тело формы — прямых детей
   у `.aform` тогда три (заголовок, тело, ряд кнопок), а поля лежат внутри тела.
   Свод 06.09: тело носит класс `.aform-body` только у двух комплектов из
   четырёх (`trainer-training-forms.js:64`, `player-training-forms.js:151`), у
   остальных ряды лежат прямыми детьми `.aform`. Поэтому ритм назначается ОБОИМ
   уровням сразу — тогда правило верно при любой из двух разметок. */
.aform > * + *,
.aform-body > * + * { margin-top: 12px; }

/* Заголовок формы отбивается от содержимого ОДИН раз. Без этого правила отбивка
   считается дважды — `h3` ритмом 12px до тела и тело ещё 12px до первого поля, —
   и получается та самая пустая полоса, про которую замечание 2. */
.aform > h3 + .aform-body { margin-top: 10px; }

/* Между большими полями свободного текста ритм крупнее: поля `.w-note` и `.w-cm`
   открыты на три строки, и общий ритм 12px рядом с ними читается как слипшийся.
   Отбивку даёт ряд, ИДУЩИЙ ЗА большим полем, — тогда правило не раздувает форму
   целиком, а между короткими элементами ритм остаётся прежним. */
.aform > .af-row:has(.w-note) + *,
.aform-body > .af-row:has(.w-note) + * { margin-top: 18px; }

/* 🔴 Форма занимает ширину своего места и НЕ едет вбок.

   Владелец возвращался к этому трижды: замечание 11 («по ширине иначе, чем в
   „Проведено“»), замечание 21 («края с нахлёстом — где отступы потерял») и
   замечание 23 («отступы опять нет от края»). Причина везде одна: ширину формы
   подгоняли ЧИСЛОМ (`style.width` в пикселях), и при любом расхождении с
   фактической шириной места содержимое уезжало за край.

   Лечение — не подгонка числа, а естественная ширина: форма блочная, коробка
   считается вместе с рамкой и полями, ряд полей переносится. Тогда края
   совпадают при ЛЮБЫХ полях ячейки. */
.aform,
.add-form { box-sizing: border-box; max-width: 100%; }
.aform .af-row,
.add-form .af-row { flex-wrap: wrap; }
