/*
 * Компоненты. Ни одного цвета — только переменные из tokens.css.
 * Состояния переключаются классами is-*, чтобы JS не трогал свойства.
 */

/* ---------- Кнопки ---------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--tap-target);
    padding: 0 var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    transition: background var(--transition), border-color var(--transition), transform var(--transition);
}

.btn:hover { background: var(--bg-subtle); text-decoration: none; }
.btn:active { transform: translateY(1px); }

.btn--primary {
    background: var(--accent-fill);
    border-color: var(--accent-fill);
    color: var(--accent-on-fill);
}

.btn--primary:hover { background: var(--accent-hover); }

.btn--gradient {
    background: var(--accent-gradient);
    border-color: transparent;
    color: var(--accent-on-fill);
}

.btn--ghost { background: transparent; border-color: transparent; color: var(--accent); }
.btn--ghost:hover { background: var(--accent-soft); }

.btn--danger { background: var(--danger); border-color: var(--danger); color: var(--on-danger); }

.btn--sm { min-height: 34px; padding: 0 var(--space-3); font-size: var(--text-xs); }

/*
 * Пальцем 34 пикселя не попасть. Мелкая кнопка нужна в плотных таблицах на
 * десктопе, но там, где ввод сенсорный, она дорастает до зоны нажатия.
 * Планшет — главное устройство, и это правило важнее плотности.
 */
@media (pointer: coarse) {
    .btn--sm { min-height: var(--tap-target); }
    .tabs__item { min-height: 38px; }
}
.btn--lg { min-height: 52px; padding: 0 var(--space-6); font-size: var(--text-base); }
.btn--wide { width: 100%; }

/* Две кнопки во всю ширину подряд — «Войти» и «Я на уроке». Без зазора
   они читаются как одна кнопка с двумя надписями, и палец на планшете
   бьёт не туда. Отступ здесь, а не в разметке: правило одно на все
   такие пары. */
.btn--wide + .btn--wide { margin-top: var(--space-3); }

.btn[disabled],
.btn.is-disabled { opacity: .45; pointer-events: none; }

/* ---------- Карточки ---------- */

/*
 * Карточка.
 *
 * Радиус крупнее и отступ больше: платформа для детей, а не панель
 * администратора. Шестнадцать точек по кругу при заголовке в двадцать
 * — это текст, прижатый к краю; двадцать четыре дают воздух, ради
 * которого карточка и существует.
 */
.card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
    box-shadow: var(--shadow-sm);
}

.card--flat { box-shadow: none; }
.card--clickable { cursor: pointer; transition: border-color var(--transition), box-shadow var(--transition); }
.card--clickable:hover { border-color: var(--accent-border); box-shadow: var(--shadow-md); }

.card__icon {
    width: 38px;
    height: 38px;
    border-radius: var(--radius-sm);
    background: var(--accent-soft);
    color: var(--accent);
    display: grid;
    place-items: center;
    margin-bottom: var(--space-3);
}

.card__title { font-size: var(--text-sm); font-weight: var(--weight-semibold); margin-bottom: var(--space-1); }
.card__text { font-size: var(--text-sm); color: var(--text-secondary); margin: 0; }
.card__meta {
    font-size: var(--text-xs);
    color: var(--text-muted);
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}

/* ---------- Статистика ---------- */

/*
 * Счётчик.
 *
 * Число — фирменным цветом. Это и есть ответ на «от смены гаммы ничего
 * не меняется»: цвет жил в двух местах — активный пункт меню и главная
 * кнопка, — а числа на всех сводках оставались серыми. Теперь на любом
 * экране со счётчиками цвет виден сразу.
 *
 * Оттенок берётся читаемый (`--accent`), а не заливочный: фирменный
 * лаймовый на белом даёт 1.4:1, и цифру пришлось бы угадывать.
 */
.stat {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-5) var(--space-6);
}
.stat__value {
    font-size: var(--size-stat-value);
    font-weight: var(--weight-bold);
    line-height: 1.1;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}
.stat__label {
    font-size: var(--text-sm);
    color: var(--text-secondary);
    margin-top: var(--space-2);
}
.stat--warning .stat__value { color: var(--warning); }
.stat--danger .stat__value { color: var(--danger); }

/* Изменение против прошлой недели. Цветом и знаком сразу: только цвет
   не читается на чёрно-белой печати отчёта, только знак — теряется
   среди чисел. */
.stat__trend {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    margin-top: var(--space-1);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
}

.stat__trend span { color: var(--text-muted); font-weight: var(--weight-normal); font-size: var(--text-xs); }
.stat__trend--up   { color: var(--success); }
.stat__trend--down { color: var(--warning); }
.stat__trend--flat { color: var(--text-muted); }

/* ---------- Таблицы ---------- */

.table-wrap {
    overflow-x: auto;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }

.table th {
    text-align: left;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
    font-weight: var(--weight-semibold);
    padding: var(--space-3) var(--space-4);
    background: var(--bg-subtle);
    border-bottom: 1px solid var(--border-strong);
    white-space: nowrap;
}

.table td { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); vertical-align: middle; }
.table tr:last-child td { border-bottom: 0; }
.table__num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.table__name { font-weight: var(--weight-semibold); }

/*
 * Телефон: таблица разбирается в карточки.
 *
 * Пять колонок в 375 точек не помещаются никак — список людей уезжал
 * вбок на 174 точки, и логин приходилось выкручивать пальцем. Шапка
 * прячется, а подпись колонки печатается перед значением из data-label,
 * поэтому разметка остаётся одной таблицей и переводы не дублируются.
 *
 * Правило общее для всех .table: следующий экран получит телефон даром.
 */
@media (max-width: 639px) {
    .table-wrap { overflow-x: visible; border: 0; background: none; }

    .table,
    .table tbody,
    .table tr,
    .table td { display: block; width: auto; }

    /* Шапка убирается от глаз, но остаётся для чтения с экрана. */
    .table thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .table tr {
        background: var(--bg-card);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        padding: var(--space-3) var(--space-4);
        margin-bottom: var(--space-3);
    }

    .table td {
        border-bottom: 0;
        padding: var(--space-1) 0;
        display: flex;
        gap: var(--space-3);
        align-items: baseline;
        justify-content: space-between;
    }

    .table td[data-label]::before {
        content: attr(data-label);
        flex: none;
        color: var(--text-muted);
        font-size: var(--text-xs);
    }

    /* Имя — заголовок карточки, подпись ему не нужна. */
    .table__name {
        font-size: var(--text-base);
        padding-bottom: var(--space-2);
        margin-bottom: var(--space-2);
        border-bottom: 1px solid var(--border);
    }

    /* Кнопки строки — во всю ширину карточки, под большой палец.
       Растягиваем прямых детей .row: «Удалить» лежит внутри своей формы,
       и без этого форма не тянется, а кнопка выходит вдвое уже. */
    .table__num { justify-content: stretch; padding-top: var(--space-3); }
    .table__num > *,
    .table__num .row > * { flex: 1 1 0; }
    .table__num .row { flex-wrap: nowrap; }
    .table__num .btn { width: 100%; justify-content: center; }
}

/* ---------- Метки ---------- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    padding: 3px var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--bg-subtle);
    color: var(--text-secondary);
}

.badge--accent  { background: var(--accent-soft);  color: var(--accent); }
.badge--success { background: var(--success-soft); color: var(--success); }
.badge--danger  { background: var(--danger-soft);  color: var(--danger); }
.badge--warning { background: var(--warning-soft); color: var(--warning); }
.badge--info    { background: var(--info-soft);    color: var(--info); }

/* ---------- Сообщения ---------- */

.alert {
    border: 1px solid var(--border);
    border-left: 3px solid var(--text-muted);
    border-radius: var(--radius-sm);
    padding: var(--space-3) var(--space-4);
    font-size: var(--text-sm);
    background: var(--bg-subtle);
}

.alert--success { background: var(--success-soft); border-left-color: var(--success); }
.alert--danger  { background: var(--danger-soft);  border-left-color: var(--danger); }
.alert--warning { background: var(--warning-soft); border-left-color: var(--warning); }
.alert--info    { background: var(--info-soft);    border-left-color: var(--info); }

/* ---------- Поля ---------- */

.field { display: block; margin-bottom: var(--space-4); }
.field__label { display: block; font-size: var(--text-sm); font-weight: var(--weight-semibold); margin-bottom: var(--space-2); }
.field__hint { display: block; font-size: var(--text-xs); color: var(--text-muted); margin-top: var(--space-1); }
.field__error { display: block; font-size: var(--text-xs); color: var(--danger); margin-top: var(--space-1); }

/* Поле в строке фильтров: подпись сверху, но отступа снизу нет —
   иначе строка с кнопкой разъезжается по вертикали. */
.field--inline { margin-bottom: 0; }
.field--inline .field__label { margin-bottom: var(--space-1); }

/* Два-три поля рядом. Ужимаются до одного столбца сами.

   Фамилия, имя и отчество — поля на одно слово, а занимали всю ширину
   карточки каждое. Форма от этого вытягивалась вниз, и то, что лежит
   под ней — «Добавить родителя», состав класса, — уходило за нижний
   край экрана: владелец его просто не находил.

   Сетка, а не проценты: на узком экране поля встают друг под друга
   сами, без отдельного медиазапроса. */
.row--fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); gap: 0 var(--space-4); }
.row--fields .field { margin-bottom: var(--space-4); }

/* Совсем короткие поля: язык, число дней, буква класса. При 170 точках
   на поле три таких переносились на второй этаж и добавляли экрану
   88 точек ровно там, где он не влезал. */
.row--fields--tight { grid-template-columns: repeat(auto-fit, minmax(min(100%, 132px), 1fr)); }

/* ---------- Серия дней и ближайшая цель (кабинет ученика) ----------
   Два блока, которые отвечают на «что дальше», а не на «сколько
   всего». Оба показываются, только когда есть что показать: «0 дней
   подряд» — упрёк, а не мотивация. */
.streak,
.goal {
    display: flex;
    align-items: center;
    gap: var(--space-4);
}

.streak__flame,
.goal__icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 56px;
    height: 56px;
    border-radius: var(--radius-md);
    background: var(--accent-soft);
    color: var(--accent);
}

.streak__num {
    font-size: var(--size-stat-value);
    font-weight: var(--weight-bold);
    line-height: 1.05;
    letter-spacing: -.02em;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}

.streak__word { color: var(--text-secondary); font-size: var(--text-sm); }

.goal__label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--text-muted);
}

.goal__name { font-size: var(--text-lg); font-weight: var(--weight-bold); }
.goal__hint { color: var(--text-secondary); font-size: var(--text-sm); margin-top: 2px; }

/* ---------- Заголовок столбца с сортировкой ----------
   Ссылка внутри th: порядок это адрес, который сохраняют в закладки.
   Наследует размер и регистр шапки таблицы, чтобы сортируемый столбец
   не отличался от обычного ничем, кроме стрелки. */
.th-sort {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: inherit;
    font: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    white-space: nowrap;
}

.th-sort:hover { color: var(--accent); text-decoration: none; }
.th-sort.is-active { color: var(--accent); }
.th-sort__dir { font-size: var(--text-xs); }

/* ---------- Строка фильтров над отчётом ----------
   Класс и даты в одну строку с кнопкой. Обычная форма с GET, а не
   скрипт: отчёт должен открываться по адресу, который сохраняют в
   закладки и присылают коллеге. */
.filters {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.filters .field { margin-bottom: 0; flex: 0 1 auto; }
.filters .input,
.filters .select { min-width: 9rem; }

/* Кнопка прижата к правому краю: глаз идёт по полям слева направо и
   упирается в неё последней. */
.filters__go { display: flex; gap: var(--space-2); margin-left: auto; }

@media (max-width: 639px) {
    /* На телефоне поля в столбик и во всю ширину: три поля в строку на
       375 точках дают по сантиметру на каждое. */
    .filters { flex-direction: column; align-items: stretch; }
    .filters__go { margin-left: 0; }
    .filters__go .btn { flex: 1 1 0; }
}

/* Поле или подсказка во всю ширину строки: почта, полное название. */
.row--fields .is-wide { grid-column: 1 / -1; }

/* Второстепенное значение в таблице: «не назначен», «нет данных». */
.muted { color: var(--text-muted); }

.input,
.select,
.textarea {
    width: 100%;
    min-height: var(--tap-target);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--text-sm);
}

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

/* Поле в строке рядом с кнопками: поиск, фильтр. Не на всю ширину. */
.input--inline,
.select--inline { width: auto; min-width: 200px; max-width: 100%; }

.input:focus,
.select:focus,
.textarea:focus { outline: none; border-color: var(--accent); box-shadow: var(--shadow-focus); }

.input.is-invalid,
.select.is-invalid { border-color: var(--danger); }

.checkbox { display: flex; align-items: center; gap: var(--space-3); min-height: var(--tap-target); cursor: pointer; font-size: var(--text-sm); }

/*
 * Флажки и переключатели рисуем сами.
 *
 * Родной переключатель с заданным accent-color в невыбранном состоянии
 * отрисовывается сплошным тёмным кругом и читается как выбранный — то есть
 * показывает ровно обратное тому, что есть. Своя отрисовка предсказуема
 * во всех браузерах и в обеих темах.
 */
.checkbox input[type="checkbox"],
.checkbox input[type="radio"] {
    appearance: none;
    -webkit-appearance: none;
    width: 20px;
    height: 20px;
    flex: none;
    margin: 0;
    border: 2px solid var(--border-strong);
    background: var(--bg-card);
    cursor: pointer;
    display: grid;
    place-content: center;
    transition: border-color var(--transition), background var(--transition);
}

.checkbox input[type="checkbox"] { border-radius: 6px; }
.checkbox input[type="radio"]    { border-radius: 50%; }

.checkbox input[type="checkbox"]::before,
.checkbox input[type="radio"]::before {
    content: "";
    transform: scale(0);
    transition: transform var(--transition);
}

.checkbox input[type="checkbox"]::before {
    width: 10px;
    height: 10px;
    box-shadow: inset 1em 1em var(--accent-on-fill);
    clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}

.checkbox input[type="radio"]::before {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    box-shadow: inset 1em 1em var(--accent-on-fill);
}

.checkbox input:checked {
    background: var(--accent-fill);
    border-color: var(--accent-fill);
}

.checkbox input:checked::before { transform: scale(1); }

.checkbox input:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Поле, которое нужно только при одном выборе: своё значение пароля.
 *
 * Видно по умолчанию, прячет его скрипт. Направление именно такое,
 * потому что без скрипта форма обязана работать целиком: пряталось по
 * умолчанию — и когда `form.js` оказался не подключён ни к одной
 * странице, «задать свой пароль» перестало открывать поле вовсе, а
 * сохранение падало с ошибкой про пустой пароль.
 *
 * Правило общее: то, что нужно человеку, показывается разметкой;
 * скрипт только убирает лишнее. Тогда его отсутствие делает страницу
 * многословнее, а не сломанной. */
.field__conditional { display: block; margin-top: var(--space-2); }
.field.is-auto .field__conditional { display: none; }

/* ---------- Вкладки и пилюли ---------- */

/*
 * Вкладки не переносятся на новую строку, а прокручиваются вбок.
 * Фильтр периода в «Результатах» — четыре пункта; на телефоне перенос
 * превращает их в три этажа и ломает шапку.
 */
.tabs {
    display: flex;
    gap: var(--space-1);
    background: var(--bg-subtle);
    padding: var(--space-1);
    border-radius: var(--radius-sm);
    flex-wrap: nowrap;
    overflow-x: auto;
    max-width: 100%;
    scrollbar-width: none;
}

.tabs::-webkit-scrollbar { display: none; }

.tabs__item {
    border: 0;
    background: none;
    padding: var(--space-2) var(--space-4);
    border-radius: 6px;
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text-secondary);
    cursor: pointer;
    white-space: nowrap;
    flex: none;
}

.tabs__item.is-active { background: var(--bg-card); color: var(--text-primary); box-shadow: var(--shadow-sm); }

.pill {
    min-height: 38px;
    padding: 0 var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    cursor: pointer;
}

.pill.is-active { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--accent-on-fill); }

/* ---------- Прогресс ---------- */

/*
 * Ширина полосы — не оформление, а данные. Поэтому она приходит одной
 * переменной --value, а не свойством width: цвет, высота и скругление
 * остаются здесь, а во вьюхе появляется только число.
 */
.progress { height: 8px; background: var(--bg-sunk); border-radius: var(--radius-pill); overflow: hidden; }
.progress__bar {
    --progress-value: 0%;
    height: 100%;
    width: var(--progress-value);
    background: var(--accent-gradient);
    border-radius: var(--radius-pill);
}

/* Страница входа живёт в auth.css целиком. Держать её куски ещё и здесь
   нельзя: правила с одинаковыми именами перекрывают друг друга по порядку
   подключения, и раскладка ломается тихо. Один компонент — один файл. */

/* ---------- Выданные доступы ---------- */

.credentials { max-width: 560px; }
.credentials__name { font-size: var(--text-lg); font-weight: var(--weight-semibold); }

/* Подпись — значение — кнопка одной строкой, и значение растягивается.

   Перенос по `flex-wrap` разложил их в три этажа на телефоне: подпись
   слева, значение прижато вправо, кнопка снизу. Три этажа на каждую из
   двух строк — и экран, где всего два значения, не помещался целиком.

   Сетка вместо переноса: подпись фиксированной ширины, значение
   забирает остаток, кнопка по содержимому. На узком экране подпись
   уходит наверх своей строкой, а значение и кнопка остаются рядом. */
.credentials__row {
    display: grid;
    grid-template-columns: 90px 1fr auto;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}

@media (max-width: 640px) {
    .credentials__row {
        grid-template-columns: 1fr auto;
    }
    .credentials__label { grid-column: 1 / -1; }
}

.credentials__row:last-of-type { border-bottom: 0; }
.credentials__label { min-width: 90px; font-size: var(--text-sm); color: var(--text-secondary); }

.credentials__value {
    /* Значение не режется и не тянет строку вбок: логин бывает
       длинным, а пароль читают вслух по слогам. */
    overflow-wrap: anywhere;
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    padding: var(--space-2) var(--space-3);
    user-select: all;
}

/* ---------- Нижняя панель действий ---------- */

.bar {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-top: var(--space-5);
    padding: var(--space-3) var(--space-4);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
}

/* На телефоне липкая полоса отступает от нижнего меню: иначе кнопка
   «Сохранить» оказывается под пунктом «Настройки». Правило стоит
   здесь, а не в layout.css: components.css подключается позже и
   перебил бы его — один компонент, один файл. */
@media (max-width: 639px) {
    .bar { bottom: var(--bottom-nav-h); }
}

.bar .info { font-size: var(--text-sm); color: var(--text-secondary); }
.bar .sp { margin-left: auto; display: flex; gap: var(--space-2); flex-wrap: wrap; }

.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); gap: var(--space-4); }

.ro {
    background: var(--bg-subtle);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    font-size: var(--text-sm);
    color: var(--text-secondary);
}

/* ---------- Пустое состояние ---------- */

.empty { text-align: center; padding: var(--space-10) var(--space-4); color: var(--text-secondary); }
.empty__title { font-weight: var(--weight-semibold); color: var(--text-primary); margin-bottom: var(--space-2); }
.empty__text { font-size: var(--text-sm); max-width: 46ch; margin: 0 auto var(--space-4); }

/* ---------- Выбор фирменного цвета ---------- */

/*
 * Образец цвета. Сам цвет приходит переменной --swatch из данных —
 * это значение из brand.css, а не оформление, придуманное во вьюхе.
 * Поэтому здесь нет ни одного цвета, а во вьюхе нет ни одного стиля.
 */
.swatches {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 128px), 1fr));
    gap: var(--space-2);
}

.swatch {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--tap-target);
    padding: var(--space-2);
    border: 2px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--text-sm);
    transition: border-color var(--transition), background var(--transition);
}

.swatch:hover { background: var(--bg-subtle); }
.swatch.is-active { border-color: var(--accent-fill); background: var(--accent-soft); }

/* Кружок с цветом пресета. Радио прячем: сам образец и есть кнопка. */
.swatch input { position: absolute; opacity: 0; width: 0; height: 0; }

.swatch__dot {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    flex: none;
    background: var(--swatch);
    border: 1px solid var(--border-strong);
}

.swatch__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.swatch input:focus-visible ~ .swatch__dot { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------- Логотип организации ---------- */

/* Образец логотипа — мелкий и в строке с полем загрузки.
   Крупный квадрат на 120 точек занимал этаж ради того, что и так видно
   в шапке на каждой странице: здесь он нужен только убедиться, что
   загрузился нужный файл. */
.logo-preview {
    flex: none;
    display: grid;
    place-items: center;
    width: 64px;
    height: 48px;
    padding: var(--space-1);
    background: var(--bg-subtle);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.logo-preview img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }

/* Поле выбора файла в строке с образцом: без этого оно тянется во всю
   ширину карточки и выталкивает образец на свою строку. */
.input--file { flex: 1 1 200px; min-width: 0; }

/* ---------- Готовый адрес с кнопкой «Скопировать» ----------
   Строкой, а не карточкой, и наверху экрана. Была карточкой внизу, под
   кнопкой «Сохранить», — то есть за пределами первого экрана, и её
   никто не находил. */
.linkbar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.linkbar__text { display: flex; flex-direction: column; gap: 2px; margin-right: auto; }
.linkbar__text b { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.linkbar__text .muted { font-size: var(--text-xs); }

.linkbar__url {
    padding: var(--space-2) var(--space-3);
    background: var(--bg-subtle);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    /* Длинный адрес на телефоне рвётся, а не уезжает за край. */
    overflow-wrap: anywhere;
}

@media (max-width: 639px) {
    .linkbar__url { width: 100%; }
}

/* ---------- Связанные люди: родитель и ребёнок ---------- */

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

.linked__item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding: var(--space-2) var(--space-3);
    background: var(--bg-subtle);
    border-radius: var(--radius-sm);
    min-height: var(--tap-target);
}

.linked__name { font-weight: var(--weight-semibold); font-size: var(--text-sm); }
.linked__login { font-size: var(--text-xs); color: var(--text-secondary); }
.linked__item .btn { margin-left: auto; }

/* ---------- Печать ---------- */

/*
 * Лист с логинами и паролями раздают по классу, отрезая по строке.
 * Значит с бумаги должно уйти всё, что бумаге не нужно: меню, шапка,
 * кнопки. И цвета — принтер чаще всего чёрно-белый, а серый текст на
 * сером фоне на нём пропадает совсем.
 */
@media print {
    .sidebar,
    .topbar,
    .no-print,
    .bar { display: none; }

    .app { display: block; padding: 0; }
    .page { padding: 0; max-width: none; }

    .table-wrap { border: 0; box-shadow: none; overflow: visible; }
    .table { font-size: var(--text-xs); }
    .table th { border-bottom: 1px solid var(--border-strong); }
    .table td { border-bottom: 1px solid var(--border); }

    /* Строку не рвём между страницами: логин на одной, пароль на другой
       — это лист, который придётся печатать заново. */
    .table tr { break-inside: avoid; page-break-inside: avoid; }

    .alert { border: 1px solid var(--border-strong); }
}

/* ---------- Каталог тренажёров ---------- */

.topic { text-decoration: none; color: inherit; }
.topic:hover { text-decoration: none; }
.topic__code { font-size: var(--text-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* Обложка тренажёра. Пропорция 16:9 зафиксирована, чтобы сетка не
   прыгала, пока картинки грузятся, и не съезжала, если одной нет. */
.preset__card {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--bg-subtle);
    display: block;
}

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

.params__row {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    justify-content: space-between;
    font-size: var(--text-xs);
}

.params__key { color: var(--text-muted); }
.params__value { font-size: var(--text-xs); }

/* ---------- Конструктор задания ---------- */

/*
 * Два шага сверху. Не украшение: педагог должен видеть, где он и
 * сколько уже набрано, — иначе «Дальше» нажимается на пустой корзине.
 */
.steps { display: flex; gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-5); }

.step {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-card);
    flex: 1 1 220px;
}

.step.is-active { border-color: var(--accent-fill); background: var(--accent-soft); }

.step__n {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--bg-subtle);
    font-weight: var(--weight-bold);
    font-size: var(--text-sm);
    flex: none;
}

.step.is-active .step__n { background: var(--accent-fill); color: var(--accent-on-fill); }
.step__t { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.step__t span { display: block; font-weight: var(--weight-normal); color: var(--text-muted); font-size: var(--text-xs); }

/*
 * Источники слева, корзина справа. Корзина липкая: педагог набирает
 * состав из разных тем, и «сколько уже набрано» нужно видеть, не
 * прокручивая обратно.
 */
.work { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: var(--space-4); align-items: start; }

.basket {
    position: sticky;
    top: var(--space-4);
    display: flex;
    flex-direction: column;
    max-height: calc(100dvh - var(--space-10));
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}

.basket__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
}

.basket__count { font-size: var(--text-xs); color: var(--text-muted); }
.basket__body { overflow-y: auto; padding: var(--space-3); flex: 1; }
.basket__empty { font-size: var(--text-sm); color: var(--text-muted); margin: 0; }

.basket__foot {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border);
}

.basket__foot .btn--primary { margin-left: auto; }

.bgrp { border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: var(--space-2); }

.bgrp__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: var(--bg-subtle);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

.bgrp__title { font-size: var(--text-xs); font-weight: var(--weight-semibold); flex: 1; }

.bit {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-top: 1px solid var(--border);
    font-size: var(--text-xs);
}

/* Выключенная строка остаётся видимой: её вернут одним нажатием. */
.bit.is-off { opacity: .45; }
.bit__body { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.bit__count { display: flex; gap: var(--space-1); align-items: center; }
.input--tiny { width: 62px; min-width: 0; padding: var(--space-1) var(--space-2); min-height: 32px; }

.crumbs { font-size: var(--text-sm); margin-bottom: var(--space-3); }

/* Планшет и телефон: корзина уезжает вниз, липкость снимается —
   на узком экране она съела бы весь экран. */
@media (max-width: 1119px) {
    .work { grid-template-columns: 1fr; }
    .basket { position: static; max-height: none; }
}

/* Подпись активного шага лежит на фирменной подложке, а не на карточке:
   приглушённый цвет там даёт 3.9 при пороге 4.5. Берём следующий по
   контрасту — он и на подложке читается. */
.step.is-active .step__t span { color: var(--text-secondary); }

/* Строка состава: название тренажёра сверху, настройка мелким снизу.
   Без второй строки два одинаковых тренажёра с разными параметрами
   выглядят одинаково, и педагог не понимает, что он набрал. */
.bit__body b { display: block; font-size: var(--text-xs); }
.bit__body span { display: block; color: var(--text-muted); font-size: 11px; }

/* ---------- Проекторный свод ---------- */

/*
 * Экран для проектора: читается с задней парты. Размеры здесь крупнее
 * обычных намеренно и не зависят от размера окна — проектор один и тот
 * же, а окно педагог может свернуть как угодно.
 */
.board {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-5);
}

.board__head { display: flex; align-items: flex-start; gap: var(--space-4); margin-bottom: var(--space-4); }
.board__title { font-size: var(--text-2xl); margin: 0; }
.board__sub { color: var(--text-secondary); font-size: var(--text-sm); }

.board__live {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-secondary);
    white-space: nowrap;
}

.board__live i {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--success);
    animation: board-pulse 2s infinite;
}

/* Связь пропала — пульс гаснет. Числа на экране остаются: устаревшие
   лучше пустоты посреди урока, но педагог должен видеть, что они
   устаревшие. */
.board__live i.is-stale { background: var(--warning); animation: none; }

@keyframes board-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }

.board__rows { display: flex; flex-direction: column; gap: var(--space-2); }
.board__loading { color: var(--text-muted); padding: var(--space-6); text-align: center; }

.brow {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-sm);
    background: var(--bg-subtle);
}

.brow.is-top    { background: var(--accent-soft); }
.brow.is-stuck  { background: var(--warning-soft); }
.brow.is-done   { opacity: .7; }

.brow__pos { width: 2ch; text-align: right; font-size: var(--text-lg); font-weight: var(--weight-bold); color: var(--text-muted); }

.brow__av {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--bg-card);
    font-weight: var(--weight-bold);
    flex: none;
}

.brow__name { flex: 1; min-width: 0; font-size: var(--text-lg); font-weight: var(--weight-semibold); }
.brow__state { display: block; font-size: var(--text-sm); font-weight: var(--weight-normal); color: var(--text-secondary); }

.brow__bar { width: 30%; height: 12px; border-radius: var(--radius-pill); background: var(--bg-card); overflow: hidden; flex: none; }

/* Ширина приходит переменной данных, а не назначается скриптом:
   правило 7 — JS переключает классы, а не свойства. */
.brow__bar i { display: block; height: 100%; width: var(--progress-value, 0%); background: var(--accent-fill); }

.brow__num { width: 5ch; text-align: right; font-size: var(--text-lg); font-weight: var(--weight-bold); font-variant-numeric: tabular-nums; }

@media (max-width: 639px) {
    .brow__bar { display: none; }
    .brow__name { font-size: var(--text-base); }
}

/* ---------- Задания на планшете ученика ---------- */

/*
 * Карточка задания — главная кнопка урока. На планшете по ней бьют
 * пальцем, поэтому она вся целиком ссылка, а не строка со ссылкой
 * «открыть» в углу.
 */
.task-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4);
    border: 2px solid var(--accent-border);
    border-radius: var(--radius-md);
    background: var(--accent-soft);
    color: var(--text-primary);
    text-decoration: none;
    min-height: 120px;
}

.task-card:hover { border-color: var(--accent); }

.task-card__mode {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--accent);
    font-weight: var(--weight-bold);
}

.task-card__title { font-size: var(--text-xl); font-weight: var(--weight-semibold); }
.task-card__meta  { font-size: var(--text-sm); color: var(--text-secondary); }
.task-card__go    { margin-top: auto; font-weight: var(--weight-semibold); color: var(--accent); }

/*
 * Полоса «педагог выдал задание». Не модальное окно намеренно: ребёнок
 * может доигрывать, и отнимать у него экран посреди примера незачем.
 */
.task-alert {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    border-radius: var(--radius-md);
    background: var(--success-soft);
    border: 1px solid var(--success);
}

.task-alert__text { flex: 1; min-width: 12rem; font-weight: var(--weight-semibold); }

/*
 * Проектор. От ноутбука и шире свод растёт: его читают не с клавиатуры,
 * а с задней парты — восемь метров от экрана. Восемнадцать пикселей на
 * таком расстоянии не читаются, и весь смысл экрана пропадает.
 *
 * Размеры заданы шагом токенов, а не своими числами: телевизор в классе
 * увеличивает всю шкалу, и свод обязан вырасти вместе с ней.
 */
@media (min-width: 1120px) {
    .brow { padding: var(--space-4) var(--space-5); gap: var(--space-5); }
    .brow__pos { font-size: var(--text-xl); }
    .brow__av { width: 56px; height: 56px; font-size: var(--text-xl); }
    .brow__name { font-size: var(--text-xl); }
    .brow__state { font-size: var(--text-base); }
    .brow__num { font-size: var(--text-xl); }
    .brow__bar { height: 18px; }
}

/* ---------- Доска почёта ---------- */

/*
 * Пьедестал. Первое место посередине и выше остальных — так в прототипе
 * и так во всём мире: ряд одинаковых карточек читается как список, а не
 * как пьедестал, и ребёнок не понимает, что он выиграл.
 */
.podium {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    align-items: end;
    gap: var(--space-3);
    margin: var(--space-4) 0;
}

.pod {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-4) var(--space-2);
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
    text-align: center;
}

.pod--first { background: var(--accent-soft); padding-top: var(--space-6); }

.pod__medal { font-size: var(--text-2xl); line-height: 1; }

.pod__av {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--bg-card);
    font-weight: var(--weight-bold);
}

.pod__name  { font-weight: var(--weight-semibold); }
.pod__group { font-size: var(--text-sm); color: var(--text-secondary); }
.pod__stars { font-weight: var(--weight-bold); color: var(--accent); }

/*
 * Полоска в ячейке таблицы. Ширина приходит переменной данных, как и
 * везде: во вьюхе нет оформления, а JS ничего не назначает.
 */
.meter {
    display: inline-block;
    vertical-align: middle;
    width: 90px;
    height: 6px;
    margin-right: var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--bg-sunk);
    overflow: hidden;
}

.meter i { display: block; height: 100%; width: var(--progress-value, 0%); background: var(--accent-fill); }

/* Карточка целиком ссылка: по ней бьют пальцем, а не по слову в углу. */
.card--link { display: flex; flex-direction: column; gap: var(--space-1); text-decoration: none; color: inherit; }
.card--link:hover { border-color: var(--accent); }
.card__go { margin-top: auto; color: var(--accent); font-weight: var(--weight-semibold); }

/* Вкладки периода мельче основных: это фильтр, а не разделы экрана. */
.tabs--sm .tabs__item { font-size: var(--text-sm); padding: var(--space-2) var(--space-3); }

.row--between { justify-content: space-between; align-items: baseline; }

@media (max-width: 639px) {
    /* На телефоне пьедестал в ряд не читается: три имени в узких
       колонках рвутся по слогам. Столбиком, победитель сверху. */
    .podium { grid-template-columns: 1fr; }
    .pod--first { order: -1; padding-top: var(--space-4); }
    .meter { width: 60px; }
}

/* ---------- Уровень и награды ---------- */

/*
 * Полоса уровня. Главный элемент экрана ребёнка: он открывает «Мои
 * успехи», чтобы увидеть, сколько осталось до следующего, — значит это
 * и должно быть самым крупным.
 */
.level {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    padding: var(--space-5);
    border-radius: var(--radius-md);
    background: var(--accent-soft);
    border: 1px solid var(--accent-border);
    margin-bottom: var(--space-4);
}

.level__badge {
    display: grid;
    place-items: center;
    width: 88px;
    height: 88px;
    flex: none;
    border-radius: 50%;
    background: var(--bg-card);
    border: 2px solid var(--accent);
}

.level__num  { font-size: var(--text-2xl); font-weight: var(--weight-bold); line-height: 1; color: var(--accent); }
.level__word { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--text-secondary); }

.level__body  { flex: 1; min-width: 0; }
.level__stars { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-xl); font-weight: var(--weight-bold); }
.level__hint  { font-size: var(--text-sm); color: var(--text-secondary); margin-top: var(--space-2); }

.level__bar {
    height: 12px;
    margin-top: var(--space-3);
    border-radius: var(--radius-pill);
    background: var(--bg-card);
    overflow: hidden;
}

.level__bar i { display: block; height: 100%; width: var(--progress-value, 0%); background: var(--accent-fill); }

/*
 * Награды. Незаработанные показываются приглушённо, а не прячутся:
 * скрытая награда не мотивирует — ребёнок должен видеть, к чему идти.
 */
.awards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr));
    gap: var(--space-3);
}

.award {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-4) var(--space-2);
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
    border: 1px solid var(--border);
    text-align: center;
    /* Незаработанное приглушено, но читаемо: подпись под ним объясняет,
       что нужно сделать, и её должно быть видно. */
    opacity: .55;
}

.award.is-earned { opacity: 1; background: var(--accent-soft); border-color: var(--accent-border); }
.award__icon     { color: var(--text-muted); }
.award.is-earned .award__icon { color: var(--accent); }
.award__name     { font-weight: var(--weight-semibold); font-size: var(--text-sm); }
.award__hint     { font-size: var(--text-xs); color: var(--text-secondary); }
/* Дата лежит на фирменной подложке заработанной награды, а не на
   карточке: приглушённый цвет даёт там 3.9 при пороге 4.5. */
.award__when     { font-size: var(--text-xs); color: var(--text-secondary); margin-top: auto; }

/* Шапка печатного отчёта: видна только на бумаге. */
.report-head { display: none; }

@media print {
    .report-head {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        padding-bottom: var(--space-2);
        margin-bottom: var(--space-4);
        border-bottom: 1px solid var(--border-strong);
    }

    .report-head__org { font-weight: var(--weight-bold); }

    /* На бумаге приглушать нечего: печатается только заработанное. */
    .award { opacity: 1; }
}

@media (max-width: 639px) {
    .level { flex-direction: column; align-items: stretch; text-align: center; gap: var(--space-4); }
    .level__badge { margin: 0 auto; }
    .level__stars { justify-content: center; }
}

/* ---------- Панель сложности в конструкторе ---------- */

/*
 * `<details>`, а не выпадающее окно: работает без JavaScript, как и весь
 * конструктор, и не закрывается от случайного клика мимо — педагог
 * настраивает её, поглядывая на класс.
 */
.knobs {
    margin: calc(var(--space-2) * -1) 0 var(--space-2);
    padding-left: var(--space-6);
}

.knobs__head {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    cursor: pointer;
    font-size: var(--text-sm);
    color: var(--text-secondary);
    padding: var(--space-1) 0;
    list-style: none;
}

.knobs__head::-webkit-details-marker { display: none; }
.knobs__head:hover { color: var(--accent); }
.knobs[open] .knobs__head { color: var(--accent); }

.knobs__form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    padding: var(--space-3);
    margin-top: var(--space-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-subtle);
}

.knob { display: flex; flex-direction: column; gap: 2px; }
.knob__label { font-size: var(--text-xs); color: var(--text-secondary); }
.knob__value { font-size: var(--text-sm); }

.knob--flag { flex-direction: row; align-items: center; gap: var(--space-2); }
.knob--flag .knob__label { font-size: var(--text-sm); color: var(--text-primary); }

/* Неправимое видно, но приглушено: это справка, а не поле. */
.knob--fixed { opacity: .7; }

.select--tiny { min-width: 8rem; padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }

@media (max-width: 639px) {
    .knobs { padding-left: 0; }
    .knobs__form { gap: var(--space-2); }
    .knob { flex: 1 1 8rem; }
}


/* =====================================================================
   ЦЕРЕМОНИЯ НАГРАЖДЕНИЯ
   =====================================================================
   Экран смотрят классом с проектора, а не в упор. Отсюда всё:
   крупные размеры, тёмный фон независимо от темы кабинета, отсутствие
   меню и хлебных крошек.

   Тёмный намеренно: проектор в классе светит на белую доску, и светлый
   экран на ней превращается в засвеченное пятно, где не читается
   ничего. Тот же выбор, что у игрового поля, и по той же причине.
   ===================================================================== */

.page.page--celebrate {
    padding: 0;
    max-width: none;
    gap: 0;
}

.cel {
    /* Экран идёт без кабинета, поэтому высоту берёт от окна сам. */
    min-height: 100vh;
    min-height: 100dvh;
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-6);
    padding: var(--space-6) var(--space-5) var(--space-5);
    background:
        radial-gradient(ellipse at 50% 0%, rgba(var(--brand-rgb), 0.18) 0%, transparent 60%),
        var(--cel-bg);
    color: var(--cel-text);
}

.cel__head { text-align: center; }
.cel__kicker {
    font-size: var(--text-sm);
    text-transform: uppercase;
    letter-spacing: .12em;
    color: var(--cel-dim);
}
.cel__title {
    margin: var(--space-2) 0 0;
    font-size: clamp(28px, 4vw, 52px);
    line-height: 1.1;
    color: var(--cel-text);
}

/* ---------- Пьедестал ---------- */

.cel__podium {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    gap: var(--space-5);
    flex-wrap: wrap;
    width: 100%;
}

.cel-pod {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-5) var(--space-4);
    border-radius: var(--radius-md);
    background: var(--cel-card);
    border: 1px solid var(--cel-line);
    min-width: 200px;

    /* До появления карточка не занимает места глазом, но занимает его
       в раскладке: иначе пьедестал прыгал бы при каждом выходе. */
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 500ms ease, transform 500ms cubic-bezier(.34, 1.56, .64, 1);
}
.cel-pod.is-in { opacity: 1; transform: none; }

/* Первое место выше и шире — его видно с задней парты без подписи. */
.cel-pod--1 {
    padding: var(--space-6) var(--space-5);
    min-width: 260px;
    border-color: var(--cel-gold);
    box-shadow: 0 0 0 1px var(--cel-gold), var(--cel-shadow);
}

.cel-pod__medal { font-size: 44px; line-height: 1; }
.cel-pod--1 .cel-pod__medal { font-size: 60px; }

.cel-pod__av {
    display: grid;
    place-items: center;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: var(--accent-fill);
    color: var(--accent-on-fill);
    font-size: 30px;
    font-weight: var(--weight-bold);
}
.cel-pod--1 .cel-pod__av { width: 92px; height: 92px; font-size: 38px; }

.cel-pod__name {
    font-size: clamp(18px, 2vw, 26px);
    font-weight: var(--weight-semibold);
    text-align: center;
}
.cel-pod__stars {
    font-size: clamp(20px, 2.4vw, 30px);
    font-weight: var(--weight-bold);
    color: var(--cel-gold);
    font-variant-numeric: tabular-nums;
}
.cel-pod__acc { font-size: var(--text-sm); color: var(--cel-dim); }

/* ---------- Остальной класс ---------- */

.cel__rest {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
    gap: var(--space-2);
    width: min(100%, 900px);
}

.cel-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--cel-card);
    border: 1px solid var(--cel-line);
    font-size: var(--text-base);

    opacity: 0;
    transform: translateX(-12px);
    transition: opacity 300ms ease, transform 300ms ease;
}
.cel-row.is-in { opacity: 1; transform: none; }

.cel-row__num { color: var(--cel-dim); min-width: 2ch; font-variant-numeric: tabular-nums; }
.cel-row__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cel-row__stars { color: var(--cel-gold); font-weight: var(--weight-semibold); }

/* ---------- Низ ---------- */

.cel__foot {
    margin-top: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
}
.cel__total { font-size: clamp(16px, 1.6vw, 22px); color: var(--cel-dim); }
.cel__empty { color: var(--cel-dim); font-size: var(--text-lg); }
.cel__actions { display: flex; gap: var(--space-3); flex-wrap: wrap; justify-content: center; }

/* Анимации выключены — показываем всё сразу. Церемония теряется, но
   человека, которому движение мешает, важнее не мутить. */
@media (prefers-reduced-motion: reduce) {
    .cel-pod, .cel-row { opacity: 1; transform: none; transition: none; }
}

/* На телефоне пьедестал становится столбиком, победитель сверху. */
@media (max-width: 640px) {
    .cel__podium { flex-direction: column; align-items: stretch; }
    .cel-pod, .cel-pod--1 { min-width: 0; }
    .cel-pod--1 { order: -1; }
}
/* ---------- Выбор фирменного цвета ----------
   Два способа рядом: готовая гамма для тех, кому всё равно, и пипетка
   для школы с брендбуком. Переключатель — обычный радио, поэтому всё
   работает без JavaScript, как и остальные настройки. */
.pickmode {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: var(--weight-semibold);
    min-height: var(--tap-target);
}

.field--color { flex-direction: column; align-items: flex-start; gap: var(--space-2); }

.colorpick {
    /* Компактный образец. Был во всю ширину карточки — цвет от этого
       понятнее не становится, а экран съедает. Высота остаётся зоной
       нажатия: по нему бьют пальцем на планшете. */
    width: 96px;
    max-width: 96px;
    height: var(--tap-target);
    padding: 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    cursor: pointer;
}

/* Список выбора с поиском: показываем сразу несколько строк, а не одну.
   Владелец набирает класс из двадцати пяти детей подряд — каждый раз
   раскрывать выпадашку значит двадцать пять лишних нажатий. */
.select[size] {
    height: auto;
    padding: var(--space-2);
}
.select[size] option { padding: var(--space-2); border-radius: var(--radius-sm); }

/* Состав класса прокручивается внутри карточки, а не тянет страницу.
   В классе бывает тридцать человек: список из тридцати строк уводит
   кнопку «Сохранить» и поиск за нижний край, то есть ровно то, ради
   чего состав сюда и переехал. */
.linked--scroll {
    max-height: 320px;
    overflow-y: auto;
    margin: 0;
}

/* ---------- Кружок с буквой ----------

   Список людей был чистым текстом: двадцать пять строк одинаковой серой
   массой, и найти в них нужного можно только чтением подряд. Буква в
   фирменном кружке даёт зацепку глазу и заодно доносит цвет школы до
   экрана, который открывают чаще всех остальных.

   Буква приходит атрибутом, а не вычисляется в CSS: `content` умеет
   только показать готовое значение, а взять первый символ строки не
   умеет. Зато так буква считается один раз на сервере, а не на каждой
   перерисовке. */
.table__name[data-initial],
.linked__name[data-initial] {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.table__name[data-initial]::before,
.linked__name[data-initial]::before {
    content: attr(data-initial);
    display: grid;
    place-items: center;
    flex: none;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: var(--text-sm);
    font-weight: var(--weight-bold);
    /* Буква в кружке — украшение и подсказка, а не содержание строки.
       Читалке она не нужна: имя стоит рядом словами. */
    speak: never;
}

/* На телефоне таблица разбирается в карточки, и кружок там мешает:
   строка и так подписана, а место дорого. */
@media (max-width: 640px) {
    .table__name[data-initial]::before { display: none; }
}

/* ---------- Карточка ребёнка в кабинете родителя ----------
   Родитель открывает её с телефона вечером, поэтому крупно и по делу:
   имя, что задано, как прошла неделя. */
.kid { display: flex; align-items: center; gap: var(--space-3); }

.kid__av {
    display: grid;
    place-items: center;
    flex: none;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: var(--text-lg);
    font-weight: var(--weight-bold);
}
.kid__name { font-size: var(--text-lg); font-weight: var(--weight-semibold); }

/* Домашнее задание — единственная строка, по которой родитель может
   что-то сделать сегодня. Поэтому она в рамке, а не в общем списке. */
.kid__hw {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    font-weight: var(--weight-medium);
}
.kid__hw--todo { background: var(--warning-soft); color: var(--warning); }
.kid__hw--calm { background: var(--success-soft); color: var(--success); }

.kid__week { font-size: var(--text-sm); color: var(--text-secondary); }

/* Значок в строке текста остаётся в строке.

   В базовых стилях `svg { display: block }` — так картинка не оставляет
   лишнего отступа снизу. Но иконка внутри строки от этого уезжает на
   свою строку: «40» и звёздочка под ним. Здесь она снова строчная и
   выровнена по тексту. */
.card__meta svg,
.kid__week svg,
.linked__item svg,
.stat__label svg {
    display: inline-block;
    vertical-align: -.15em;
}

/* ---------- Аватар ----------
   Фото показывается там, где оно одно: шапка, профиль, карточка ребёнка
   у родителя. В списках людей остаётся кружок с буквой — он рисуется CSS
   и не стоит ни одного запроса, а фото там означало бы двадцать пять
   процессов PHP на страницу. */
.who__avatar--photo { object-fit: cover; padding: 0; border-radius: 50%; }

.avatar-preview {
    display: grid;
    place-items: center;
    flex: none;
    /* 64, а не 96: своё лицо человек узнаёт и мелким, а образец нужен
       только убедиться, что загрузился нужный файл. */
    width: 64px;
    height: 64px;
    border-radius: 50%;
    object-fit: cover;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: var(--text-lg);
    font-weight: var(--weight-bold);
}

/* Сохранение набора в корзине: поле и кнопка в строку. */
.basket__save {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border);
}
.basket__save .input { min-width: 0; }

/* =====================================================================
   ВХОД НА УРОКЕ
   =====================================================================
   Экран открывает первоклассник на планшете, глядя на доску. Отсюда
   всё: крупные цифры, крупные плитки, ничего лишнего вокруг. */

.join {
    display: grid;
    place-items: center;
    min-height: 100vh;
    min-height: 100dvh;
    padding: var(--space-5);
    background: var(--bg-page);
}

.join__card {
    width: min(420px, 100%);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-8);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
    border: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
}
.join__card--wide { width: min(860px, 100%); }

.join__title { margin: 0; font-size: var(--text-xl); text-align: center; }
.join__lede  { margin: 0; text-align: center; color: var(--text-secondary); }
.join__foot  { margin: 0; text-align: center; font-size: var(--text-sm); }

/* Код: крупно и моноширинным. Шесть цифр набираются вслепую, глядя на
   доску, а моноширинный не даёт спутать единицу с семёркой. */
.join__code {
    font-family: var(--font-mono);
    font-size: 34px;
    letter-spacing: .3em;
    text-align: center;
    padding: var(--space-4);
    height: auto;
}

/* ---------- Плитки класса ---------- */

.tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr));
    gap: var(--space-3);
}

.tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding: var(--space-4) var(--space-2);
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
    background: var(--bg-subtle);
    color: var(--text-primary);
    font: inherit;
    cursor: pointer;
    transition: border-color var(--transition), background var(--transition);
}
.tile:hover { border-color: var(--accent-border); background: var(--accent-soft); }

.tile__face {
    display: grid;
    place-items: center;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    object-fit: cover;
}
.tile__face--letter {
    background: var(--accent-soft);
    color: var(--accent);
    font-size: var(--text-xl);
    font-weight: var(--weight-bold);
}

.tile__name {
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    text-align: center;
    line-height: 1.25;
}

/* ---------- Код урока на своде ---------- */

.board__code { text-align: center; }
.board__code-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-caps);
    color: var(--text-muted);
}
.board__code-value {
    font-family: var(--font-mono);
    font-size: clamp(28px, 3vw, 44px);
    font-weight: var(--weight-bold);
    letter-spacing: .12em;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}


/* ---------- Плашка установки приложения ---------------------------
   Рисует её pwa.js, здесь только вид. Плашка липнет к низу окна и
   поэтому обязана знать про нижнее меню на телефоне: без этого она
   ложится ровно на пункт «Настройки». Ту же ошибку уже ловили на
   2.3d с полосой сохранения — высота меню вынесена в токен именно
   для таких случаев. */
.pwa {
    position: fixed;
    left: var(--space-3);
    right: var(--space-3);
    bottom: var(--space-3);
    z-index: 40;
}

.pwa--bar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    max-width: 640px;
    margin: 0 auto;
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-card);
    box-shadow: var(--shadow-lg);
}

.pwa__row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex: 1;
    min-width: 0;
}

.pwa__app {
    flex: none;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-sm);
}

.pwa__text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.pwa__text b { font-weight: var(--weight-semibold); }

.pwa__text span {
    color: var(--text-muted);
    font-size: var(--text-sm);
}

/* Значок внутри фразы: «нажмите [значок] Поделиться». Строчный, иначе
   уедет на свою строку — ту же ошибку уже ловили со звездой в счёте. */
.pwa__mini {
    display: inline-flex;
    vertical-align: -3px;
}

.pwa__mini .pwa__icon { width: 16px; height: 16px; }

.pwa__icon { width: 20px; height: 20px; }

.pwa__go { flex: none; }

.pwa__min {
    flex: none;
    display: grid;
    place-items: center;
    width: var(--tap-target);
    height: var(--tap-target);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-muted);
    cursor: pointer;
}

.pwa__min:hover { background: var(--bg-subtle); color: var(--text-primary); }

/* Свёрнутая плашка: одна пилюля в углу. Закрыть насовсем нельзя —
   установку откладывают, а потом не находят, куда нажать. */
.pwa--pill {
    left: auto;
    display: flex;
    justify-content: flex-end;
}

.pwa__pill {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap-target);
    padding: 0 var(--space-4);
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-card);
    color: var(--text-primary);
    font: inherit;
    font-weight: var(--weight-semibold);
    box-shadow: var(--shadow-lg);
    cursor: pointer;
}

@media (max-width: 639px) {
    /* Нижнее меню телефона занимает низ экрана. Плашка встаёт над ним,
       а не поверх: перекрытый пункт меню выглядит как поломка. */
    .pwa { bottom: calc(var(--bottom-nav-h) + var(--space-2)); }

    .pwa--bar,
    .pwa__row { flex-wrap: wrap; }

    .pwa__text { flex: 1 1 200px; }
    .pwa__go   { flex: 1 1 160px; }
}

@media print {
    .pwa { display: none; }
}

/* ---------- Действия в строке ученика у педагога ----------
   Три действия подряд: пароль, убрать, перевести. Без своей раскладки
   каждая форма — блок, и они встают друг под другом столбиком: на
   шести учениках это шесть этажей по четыре элемента.

   Перевод устроен так, что выбор класса и есть действие: `data-autosubmit`
   отправляет форму сразу. Кнопка рядом нужна тем, у кого JavaScript
   выключен — весь кабинет работает без него, и этот угол не исключение. */
.row--acts { gap: var(--space-2); align-items: center; flex-wrap: nowrap; }
.row--acts form { display: inline-flex; }
.row--move { gap: 4px; flex-wrap: nowrap; }
.row--move .select { min-width: 116px; padding-block: 6px; font-size: var(--text-sm); }
.row--move .input { padding-block: 6px; font-size: var(--text-sm); }
.row--move .btn { padding-inline: var(--space-3); }

@media (max-width: 639px) {
    /* На телефоне строка разбирается в карточку, и действия встают в
       ряд по ширине: палец бьёт по кнопке, а не по её краю. */
    .table__num .row--acts { flex-wrap: wrap; }
    .table__num .row--move { flex: 1 1 100%; }
    .table__num .row--move .select { flex: 1 1 auto; }
}

/* ---------- Админка платформы ----------
   Своя полоса вместо кабинетного меню: у сотрудника платформы нет
   организации, а значит нет ни логотипа школы, ни её цвета. И это
   к лучшему — по одному взгляду видно, что ты над школами, а не
   внутри одной. */
.admin__bar {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    padding: var(--space-3) var(--space-5);
    background: var(--bg-inverse);
    color: var(--text-on-inverse);
}

.admin__brand { font-weight: var(--weight-bold); color: var(--text-on-inverse); }
.admin__nav { display: flex; gap: var(--space-4); flex: 1; }
.admin__link { color: var(--text-muted-on-inverse); padding: var(--space-2) 0; }
.admin__link.is-active,
.admin__link:hover { color: var(--text-on-inverse); }
.admin__me { display: flex; align-items: center; gap: var(--space-3); color: var(--text-muted-on-inverse); }
.admin__main { max-width: var(--page-max); margin: 0 auto; padding: var(--space-6) var(--space-5); }

/* Заголовок и действие — в одну строку, и только здесь.
   В кабинете экран листают, в админке по нему работают: строка,
   потраченная на кнопку «Найти» под заголовком, — это строка воронки,
   которую не видно. Правило заведено под `.admin`, а не для всех:
   в кабинетах заголовок остаётся отдельным этажом, как и был. */
.admin .page__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
}

/* Полоса вкладок и то, что рядом с ней (обычно «+ Заявка»). */
.admin__tabs { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.admin__tabs .tabs { flex: 1 1 auto; }

@media (max-width: 639px) {
    .admin__bar { flex-wrap: wrap; gap: var(--space-3); }
    .admin__me span { display: none; }
}

/* ---------- Панель на месте, вместо ухода вглубь ----------
   Правка заявки идёт прямо в строке списка: этап списком, остальное в
   раскрывающейся панели под кнопкой. Проваливаться в карточку ради
   смены этапа — лишний переход на каждый звонок, а звонков за день
   десяток.

   На `<details>`, а не на всплывающем окне: работает без JavaScript,
   не закрывается от случайного клика мимо и не уводит со страницы. */
.pop { position: relative; display: inline-block; }
.pop > summary { list-style: none; cursor: pointer; }
.pop > summary::-webkit-details-marker { display: none; }

.pop__panel {
    position: absolute;
    z-index: var(--z-overlay);
    min-width: 280px;
    margin-top: var(--space-2);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-card);
    box-shadow: var(--shadow-lg);
}

/* У правого края таблицы панель раскрывается влево: иначе она уезжает
   за экран, и половину полей не видно. */
.pop--end .pop__panel { right: 0; }

/* Панель на семь полей. В одну колонку они дают 860 точек — панель
   уходит за нижний край, и «Сохранить» не видно. В две помещается
   целиком, а 520 — та ширина, на которой поле не сжимается уже
   читаемого. */
.pop__panel--wide { min-width: 520px; }

/* Меню из двух пунктов. Общие 280 точек рассчитаны на форму с полями;
   под «Профиль» и «Выйти» это пустая плашка в треть шапки. */
.pop__panel--menu { min-width: 180px; padding: var(--space-3); }

/* Панель лежит внутри таблицы, а `.table-wrap` прокручивается вбок.
   Прокрутка обрезает всё абсолютное внутри себя — панель оказывалась
   срезанной по нижнему краю, и половины полей не было видно. На
   широком экране таблица воронки помещается целиком, и обрезать
   здесь нечего. */
.admin .table-wrap { overflow: visible; }

@media (max-width: 1200px) {
    /* Уже — таблица едет вбок, и прокрутка снова нужна. Тогда панель
       раскрывается в потоке: строка становится выше, зато видна вся.
       Панель поверх прокручиваемой области всё равно была бы срезана. */
    .admin .table-wrap { overflow-x: auto; }
    .admin .table-wrap .pop__panel { position: static; min-width: 0; }
}

/* Плотная таблица: воронку читают строками, а не карточками. Лишний
   воздух здесь — это меньше строк на экране и больше прокрутки. */
.table--tight th,
.table--tight td { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.table--tight .small { display: block; font-size: var(--text-xs); }

@media (max-width: 639px) {
    /* На телефоне панель занимает ширину карточки: узкая всплывашка на
       375 точках всё равно не помещается. */
    .pop { display: block; }
    .pop__panel { position: static; min-width: 0; }

    /* Кроме шапки. Она узкая и липкая: панель, вставленная в её строку,
       растягивает шапку и сжимается до полутора слов. Меню из двух
       пунктов помещается всплывающим и на 375. */
    .topbar .pop { display: inline-block; }
    .topbar .pop__panel { position: absolute; min-width: 160px; }
}

/* ---------- Коммерческое предложение ----------
   Страница, которую директор открывает с телефона по ссылке из
   переписки. Отсюда три решения: одна колонка на любом экране,
   крупный текст и доступы первым блоком.

   Оформление берётся из палитры школы, собранной по её цвету, — тот
   же файл, что подключает страница школы. Своих цветов здесь нет ни
   одного, поэтому белый лейбл доезжает и сюда. */
.kp {
    max-width: 760px;
    margin: 0 auto;
    padding: var(--space-5) var(--space-4) var(--space-8);
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}

.kp__top {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.kp__logo { max-height: 48px; width: auto; }
.kp__school { display: flex; flex-direction: column; line-height: 1.25; }
.kp__by { margin-left: auto; color: var(--text-muted); font-size: var(--text-xs); }

.kp__hero h1 { font-size: var(--size-page-title); margin-bottom: var(--space-3); }
.kp__lede { font-size: var(--text-lg); color: var(--text-secondary); margin: 0; }

.kp__card {
    padding: var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
}

/* Блок с доступами и блок «что дальше» — на фирменной подложке: это
   два места, ради которых страницу открывают, и глаз должен находить
   их без чтения. */
.kp__card--accent {
    background: var(--accent-soft);
    border-color: var(--accent-border);
}

/* Приглушённый текст внутри — на ступень контрастнее.
   `--text-muted` подобран под страницу и карточку, а на фирменной
   подложке даёт 4.18 при пороге 4.5. Это четвёртый раз за проект,
   когда цвет проверяется не на том фоне; здесь фон известен заранее,
   поэтому и поправка местная, а не новая роль в токенах. */
.kp__card--accent .muted { color: var(--text-secondary); }

.kp__grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}

.kp__accounts {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
    margin: var(--space-4) 0;
}

.kp__account {
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-card);
}

.kp__role { font-weight: var(--weight-bold); margin-bottom: var(--space-1); }

.kp__cred { display: grid; grid-template-columns: auto 1fr; gap: 2px var(--space-3); margin: var(--space-3) 0 0; }
.kp__cred dt { color: var(--text-muted); font-size: var(--text-xs); }
.kp__cred dd { margin: 0; }

.kp__enter { display: flex; flex-direction: column; gap: var(--space-2); align-items: flex-start; }

.kp__steps { margin: 0; padding-left: var(--space-5); display: grid; gap: var(--space-3); }

.kp__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}

@media print {
    /* На бумаге ссылка «Открыть» и кнопка печати бессмысленны, а место
       занимают. Доступы остаются: за ними лист и уносят. */
    .kp__print,
    .kp__enter .btn { display: none; }
    .kp { max-width: none; padding: 0; gap: var(--space-4); }
}

/* Счётчик очереди: сколько написано из дневной нормы. */
.queue__count {
    font-size: var(--text-sm);
    font-weight: var(--weight-bold);
    color: var(--text-secondary);
    white-space: nowrap;
}

.queue__count.is-done { color: var(--warning); }

/* Страница ухода в WhatsApp: она живёт секунду, но не должна выглядеть
   сломанной, если обновление в браузере отключено. */
.go { max-width: 420px; margin: 15vh auto; text-align: center; display: grid; gap: var(--space-4); }
.go__t { font-size: var(--text-lg); font-weight: var(--weight-bold); margin: 0; }
.go__s { margin: 0; color: var(--text-secondary); }
