/*
  Визуальный слой админки AquaGrace.
  Схема «Лагуна», пара «Аква» (Prata + Golos Text), стандартная плотность.
  Палитра и шкала плотности взяты из утверждённых владельцем таблиц задачи 06 релиза 0.1.0 и
  «на глаз» здесь не подбираются: правки палитры и плотности — только новым решением владельца.
  Задача «UI-система админки» (2026-08-08) добавила поверх них 8-кратную шкалу отступов, группы
  меню с иконками, свёрнутое меню, потолок ширины карточек и компоненты badge / toast / modal /
  pagination — не трогая ни одного утверждённого цвета и ни одного значения плотности.
*/

/* --- шрифты: локальные файлы, без CDN (требование задачи 06) --- */

/* Prata — только заголовочный: вордмарк в шапке и заголовок страницы входа. Один вес. */
@font-face {
    font-family: 'Prata';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/prata-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
    font-family: 'Prata';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/prata-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Golos Text — вариативный (ось wght 400–900), поэтому один файл на все веса интерфейса. */
@font-face {
    font-family: 'Golos Text';
    font-style: normal;
    font-weight: 400 900;
    font-display: swap;
    src: url('/fonts/golos-text-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
    font-family: 'Golos Text';
    font-style: normal;
    font-weight: 400 900;
    font-display: swap;
    src: url('/fonts/golos-text-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- токены --- */

:root {
    /* палитра «Лагуна» */
    --primary: #0F575D;
    --primary-hover: #0C4A50;
    --on-primary: #FFFFFF;
    --accent: #0097B1;
    --soft: #E0F5F7;
    --soft-line: #BEE5E9;
    --row-hover: #F2FAFB;
    /*
      Рабочее поле под белыми блоками без рамки (`Дизайн-система.md` §1, пункт 1). Голубой
      #E9F5F7 пятого круга (21.09) владелец 22.09 вернул к прежнему #F6FBFC — «слишком
      выделяется». Блок от такого поля отличается едва (1,04:1), поэтому верхний край блока
      держит тень `--shadow-block`, а не цвет.
    */
    --page: #F6FBFC;
    --card: #FFFFFF;
    --thead: #FBFDFE;
    --line: #C4D6DC;
    --line-soft: #E7F1F3;
    --tx: #23465D;
    --tx2: #55707F;
    /* #8CA3AE на белом даёт 2,64:1 — ниже AA, поэтому живёт только в неактивных элементах */
    --tx-disabled: #8CA3AE;
    --ok: #0E7D6C;
    --danger: #B23274;
    --danger-hover: #97285F;
    --danger-soft: #FCE9F4;
    --danger-line: #F2C6DC;
    --pink: #ED5297;
    /*
      Янтарная тройка — для состояний «требует внимания» (просроченный хвост, неподтверждённая
      оплата) этапов 2–5. В этапе 1 такого статуса нет, поэтому здесь она заведена и не применяется:
      разделу этапа 2 останется взять готовое, а не подбирать свой оранжевый.
    */
    --warn: #9A6212;
    --warn-soft: #FDF3E3;
    --warn-line: #F0DDBC;
    --focus-ring: rgba(0, 151, 177, .18);
    /* Вход не меняется (кадр 60:73): его голубой — своим значением, а не ссылкой на фон страниц. */
    --login-bg: #E9F5F7;
    /* Тень плотнее прежней .05 (отв. 11): на фоне #F6FBFC верхний край блока держит только она. */
    --shadow-block: 0 1px 2px rgba(15, 87, 93, .09), 0 2px 8px rgba(15, 87, 93, .09);
    --shadow-login: 0 12px 34px rgba(15, 87, 93, .10);
    --shadow-float: 0 14px 38px rgba(15, 87, 93, .18);

    /*
      Типографика и шкала стандартной плотности — задача 06, значения владельца.
      Резиновая часть (решение владельца 2026-08-08): кегль и поля растут вместе с экраном,
      но НЕ пропорционально ему. Клампы подобраны так, что до 1440 px включительно выходят ровно
      утверждённые значения (15 / 14,5 / 22 / 26 px) — согласованная плотность не меняется, — а
      выше экран отдаёт прибавку. Это не autoscale: на 1920 видно БОЛЬШЕ записей, а не те же
      записи крупнее; правило само отрабатывает и на 2560 без нового медиазапроса.
    */
    --f-head: 'Prata', Georgia, serif;
    --f-text: 'Golos Text', system-ui, sans-serif;
    --fs: clamp(15px, 0.35vw + 9.96px, 17px);
    --fs-ui: clamp(14.5px, 0.34vw + 9.6px, 16.5px);
    --fs-small: 12.5px;
    --fs-micro: 11.5px;
    --fs-h1: clamp(22px, 0.5vw + 14.8px, 26px);
    --fs-h2: 17px;
    --fs-brand: 21px;

    /*
      Знак стоит верхом боковой панели, а не в шапке, и там он немного крупнее: 40 → 48 → 52 px.
      Панель поднялась до верха окна (правка 2026-08-14), знак получил под себя всю полосу шапки
      в 72 px — и в ней 48 px читались мелко, оставляя по 12 px пустоты сверху и снизу.
    */
    --logo: 52px;
    /* Ширина меню тоже резиновая: кегль пункта растёт с экраном, и «Мониторинг диалогов»
       на 1920 перестал помещаться в фиксированные 236 px. До 1440 включительно — те же 236. */
    --nav-w: clamp(248px, 14vw, 300px);
    /* Высота пункта меню — компонент «Nav / пункт» (13:89): 36 px, пункты стоят вплотную. */
    --nav-item-h: 36px;
    --pad-x: clamp(26px, 2vw, 48px);
    /*
      Верхнее поле содержимого. С первым пунктом меню оно больше не спаривается: у панели
      появился свой верх — знак и переключатель ширины (правка 2026-08-14), и список разделов
      начинается под ним. Общей у шапки, крошек, заголовка и содержимого осталась левая линия.
    */
    --main-py: 34px;
    --gap: 20px;
    --row-py: 13px;
    /* Поле блока: 20 px на экране, 16 на телефоне (переопределено в медиазапросе телефона). */
    --card-pad: 20px;
    --btn-py: 10px;
    --r-s: 8px;
    /*
      Средний радиус — у фотографий (инструктор, хвост). Токен объявляется здесь, потому что
      обе они на него ссылались, а объявления не было вовсе: `var(--r-m)` без значения — это
      невалидное свойство, и оба снимка выходили с прямыми углами. Замерено 2026-08-23:
      `border-radius` у `.instructor-profile-photo` вычислялся в 0px.
    */
    --r-m: 10px;
    --r-l: 12px;

    /* шкала отступов: одна восьмикратная сетка вместо случайных 5 / 11 / 14 / 17 / 46 px */
    --s1: 4px;
    --s2: 8px;
    --s3: 12px;
    --s4: 16px;
    --s5: 24px;
    --s6: 32px;
    --s7: 48px;

    /*
      Высота шапки: 60 px было тесно двум строкам названия, 76 px оказалось слишком много для
      полосы, в которой почти ничего нет (замечание владельца 2026-08-08). 68 px держались
      знаком логотипа; знак уехал в панель, и высота поднята до 72 px — решение владельца
      2026-08-14 внутри диапазона Ксюши 72–80. От этого же токена считается прилипший
      заголовок таблицы, поэтому число живёт здесь и нигде больше.
    */
    --head-h: 72px;
    --nav-w-collapsed: 60px;
    /*
      Потолка ширины у содержимого нет вовсе (замечание владельца 2026-08-18: «белые блоки не
      везде одинаковые» — журнал и учётные записи шли во всю ширину, «Обзор» и карточка события
      обрывались на 1280 px). Один белый блок на всю админку читается только так: ширину задаёт
      колонка содержимого, а не порода страницы. Читаемость держит не потолок блока, а потолок
      самого поля ввода — он и остаётся: два поля по 520 px читаются лучше, чем одно на 700,
      а колонки «Информации» раскладывает `.fields-columns`.
    */
    --field-max: 520px;
    /* Колонка короткой формы — двух-трёх полей одной колонкой (`.form-single`, отв. 7, Ч4). */
    --field-max-wide: 640px;
    /*
      Высота однострочного поля — одна на все поля одного типа (критерий 17). Своих метрик у
      текстового поля, даты, времени и списка выбора разные: без этого токена они приезжали
      42 / 44 / 44,8 / 44 px, и ряд полей формы читался неровным. 44 px — ещё и палец на
      телефоне (цель нажатия не меньше 44 px).
    */
    --field-h: 44px;
    /*
      Колонка подписей в карточке объекта одна на все смысловые группы: `max-content` считал
      её внутри своей группы, и значения соседних групп стояли на разных вертикалях
      (замер 14.08: пять разных на одной карточке). Ширина взята по самой длинной подписи
      админки — «Последний вход», 101 px.
    */
    --field-label-w: 112px;
    /* Ниже этой ширины таблицы показываются карточками — листать строку вбок не приходится. */
    --cards-below: 1024px;
}

/* --- основа --- */

body {
    margin: 0;
    font-family: var(--f-text);
    font-size: var(--fs);
    line-height: 1.5;
    color: var(--tx);
    background: var(--page);
}

/* Заголовок страницы — самый крупный текст экрана; ниже по иерархии идёт заголовок карточки. */
/*
  Заголовок страницы — самый крупный текст экрана. `text-wrap: balance` делит его на строки
  ровно, а не «длинная плюс одно слово», и заодно снимает висящий предлог в конце строки:
  подбирать переносы руками бессмысленно — ширин четыре (375 / 1100 / 1280 / 1920).
*/
h1 {
    margin: 0;
    font-size: var(--fs-h1);
    font-weight: 600;
    letter-spacing: -.005em;
    text-wrap: balance;
}

h2 {
    margin: 0 0 var(--s3);
    font-size: var(--fs-h2);
    font-weight: 600;
    text-wrap: balance;
}

p {
    margin: 0 0 var(--s3);
}

a {
    color: var(--primary);
}

a:hover {
    color: var(--accent);
}

/* Видимый фокус с клавиатуры — один на все интерактивные элементы. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* --- шапка --- */

.app-header {
    /* Шапка остаётся на месте при прокрутке: под ней прилипает и меню, и заголовок таблицы. */
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: var(--s3);
    /* border-box — иначе граница добавляется к высоте, и меню прилипает на пиксель выше шапки. */
    box-sizing: border-box;
    height: var(--head-h);
    padding: 0 var(--pad-x);
    background: var(--card);
    border-bottom: 1px solid var(--line-soft);
}

/*
  Одна левая линия на весь экран (критерий 18): бренд отодвигается на ширину колонки меню и
  встаёт ровно над крошками, заголовком страницы и содержимым. Слева от него — сама панель,
  поэтому и нижняя черта шапки рисуется только над содержимым: без неё левая колонка читается
  одним объектом сверху донизу, а не пустой ячейкой над меню. Черта — псевдоэлемент, а не
  граница: граница шла бы через всю ширину. Высота шапки от этого не меняется — значит,
  прилипший заголовок таблицы (top: var(--head-h)) остаётся на своём месте.
*/
.app-header-nav {
    padding-left: calc(var(--nav-w) + var(--pad-x));
    border-bottom: none;
}

.nav-collapsed .app-header-nav {
    padding-left: calc(var(--nav-w-collapsed) + var(--pad-x));
}

.app-header-nav::after {
    content: '';
    position: absolute;
    left: var(--nav-w);
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--line-soft);
}

.nav-collapsed .app-header-nav::after {
    left: var(--nav-w-collapsed);
}

.app-header .brand {
    display: flex;
    align-items: center;
    gap: var(--s3);
    margin-right: auto;
    color: var(--primary);
    text-decoration: none;
    white-space: nowrap;
}

.brand-text {
    display: flex;
    flex-direction: column;
}

/* Вордмарк — единственное место Prata в админке, кроме заголовка входа. */
.brand-name {
    font-family: var(--f-head);
    font-size: var(--fs-brand);
    line-height: 1.1;
    letter-spacing: .02em;
}

/* Подпись под вордмарком: человек с первого взгляда видит, что это рабочая панель, а не сайт школы. */
.brand-panel {
    font-size: var(--fs-small);
    line-height: 1.2;
    color: var(--tx2);
}

.app-user {
    display: flex;
    align-items: center;
    gap: var(--s4);
    font-size: var(--fs-ui);
    /* min-width: 0 — иначе длинное имя распирает шапку и она уезжает за край телефона. */
    min-width: 0;
}

/* Имя вошедшего бывает длинным: оно сжимается и обрезается многоточием, а не двигает шапку. */
.app-user-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--tx2);
}

.app-user form {
    flex-shrink: 0;
    margin: 0;
}

/*
  «Выйти» в шапке — обведённая кнопка, а не подчёркнутая ссылка: рядом нет других действий.
  Размер s (36 px), как в компонентах «Шапка / ноутбук» 13:397 и «Шапка / телефон» 13:404:
  шапка — не форма и не строка действий страницы, и кнопка m в ней перетягивала бы взгляд
  с названия раздела. Поля — те же, что у `.button-s`.
*/
.app-user .button-link {
    min-height: 36px;
    margin: 0;
    padding: var(--s1) var(--s3);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-s);
    color: var(--primary);
    text-decoration: none;
}

.app-user .button-link:hover {
    background: var(--soft);
    border-color: var(--accent);
}

/* --- навигация по разделам --- */

.app-body {
    display: flex;
    align-items: stretch;
    min-height: calc(100vh - var(--head-h));
}

/*
  Панель вышла из потока, и место под неё держит поле содержимого. Класс вешает th:classappend
  по menu != null: на экране обязательной смены пароля панели нет вовсе, и держать место не под что.
*/
.app-body-nav {
    padding-left: var(--nav-w);
}

.nav-collapsed .app-body-nav {
    padding-left: var(--nav-w-collapsed);
}

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

  Позицией, а не местом в дереве: <nav> обязан идти в разметке ПОСЛЕ шапки — кнопка «Меню»
  телефона управляет им через aria-controls, и порядок чтения с клавиатуры должен остаться
  «кнопка → панель». Перестановку дерева уже пробовали 14.08 и откатили.

  Уровень выше шапки (30): в левом верхнем углу они накладываются, и выигрывать обязана панель,
  иначе фон и черта шапки прошли бы поверх знака.
*/
.app-nav {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 40;
    width: var(--nav-w);
    height: 100vh;
    /* Сверху поля нет: его место занимает строка знака ровно в полосу шапки (--head-h). */
    padding: 0 var(--s2) var(--s4);
    box-sizing: border-box;
    background: var(--card);
    border-right: 1px solid var(--line-soft);
    /* Меню не уезжает вверх вместе со страницей и прокручивается само. */
    overflow-y: auto;
}

/*
  Верх панели: знак и переключатель её ширины. Высота — ровно полоса шапки, поэтому знак стоит
  на одной горизонтальной линии со словесной частью бренда справа от панели (замечание владельца
  2026-08-14: до этого знак висел отдельной полосой на 48 px ниже слова). Геометрия — компонента
  «Nav / меню развёрнутое» (13:90): знак на 12 px от края экрана, у иконок разделов, значок
  переключателя — над шевронами групп; первый пункт — на 76 px от верха окна (строка 72 и зазор
  --s1). flex-shrink: 0 — иначе длинное меню сплющивает эту строку.
*/
.nav-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    box-sizing: border-box;
    height: var(--head-h);
    flex-shrink: 0;
    margin-bottom: var(--s1);
    padding: 0 0 0 var(--s1);
}

/*
  Знак — тоже ссылка внутри панели, и поля пункта меню (.app-nav a) с него сняты. Это не
  косметика: замер 14.08 показал знак на 32 px от края экрана при иконках разделов на 20 —
  поле --s3 у .nav-head складывалось со своим полем ссылки, и вертикальная ось Ж-1.1
  расходилась ровно на эти --s3. Отступ в 2 px ниже строки сдвигал знак на пиксель выше
  словесной части бренда. Ссылка при этом не шире своего знака: иначе на пустое место справа
  от него нажималось бы «на главную». Скругление — чтобы подложка наведения совпала со знаком,
  а не была квадратом под кругом.

  Селектор с элементом обязателен: `.app-nav a` весит 0-1-1, один класс — 0-1-0, и поля пункта
  меню перебили бы правило знака. Та же ловушка, что у `.ss > .ss-input`. Свойство display
  сюда не пишется намеренно: `.nav-collapsed .nav-mark` (0-2-0) прячет знак в свёрнутой панели,
  и правило потяжелее его бы отменило.
*/
.app-nav a.nav-mark {
    margin: 0;
    padding: 0;
    border-radius: 50%;
}

.brand-mark {
    width: var(--logo);
    height: var(--logo);
    border-radius: 50%;
    flex-shrink: 0;
}

/* Переключатель ширины меню: иконка без заливки, чтобы не спорить со знаком рядом. */
.nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    /* Свой квадрат, а не общий минимум кнопки: 44 px — про цель нажатия на телефоне, а на
       телефоне вся панель скрыта, и здесь минимум растянул бы значок в прямоугольник. */
    min-height: 36px;
    margin: 0;
    padding: 0;
    color: var(--tx2);
    background: none;
    border: 1px solid transparent;
    border-radius: var(--r-s);
    flex-shrink: 0;
}

.nav-toggle svg {
    width: 20px;
    height: 20px;
}

.nav-toggle:hover {
    color: var(--primary);
    background: var(--soft);
    border-color: transparent;
}

/* Имя вошедшего живёт в меню только на телефоне — на десктопе оно стоит в шапке. */
.nav-user {
    display: none;
}

/*
  Группы меню (`Дизайн-система.md` §9; компонент 13:90): «Обзор» без группы, затем «Занятия»,
  «Клиентки», «Школа», «Бот», «Система» — одна за другой, «Система» идёт последней тем же
  порядком: к низу панели её больше не прижимают и чертой не отделяют (в кадрах черты нет).
  Группа — `<details>`: заголовок сворачивает её пункты и без скриптов. Отступы — шкалой (§1):
  в компоненте между группами 14 px и заголовок 29 px, здесь 12 и 30,75 — сумма та же, и пункты
  стоят по высоте ровно там же, где в компоненте.
*/
.nav-top + .nav-group,
.nav-group + .nav-group {
    margin-top: var(--s3);
}

/*
  Заголовок группы — `<summary>`: 12,5 / 600 капсом с разрядкой .04em цветом --tx2 (§2, «Заголовок
  группы меню»; в компоненте 13:90 разрядка .06em — код держит правило), справа шеврон 16 px:
  у раскрытой группы смотрит вниз, у свёрнутой — вправо. Подпись — на 20 px от края экрана,
  правее иконок пунктов, как в компоненте; поля шкалой: 4 сверху, 8 снизу и справа, 12 слева.
*/
.nav-group-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    padding: var(--s1) var(--s2) var(--s2) var(--s3);
    border-radius: var(--r-s);
    font-size: var(--fs-small);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--tx2);
    cursor: pointer;
    list-style: none;
}

.nav-group-title::-webkit-details-marker {
    display: none;
}

.nav-group-title > .field-icon {
    position: static;
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    margin: 0;
}

.nav-group[open] > .nav-group-title > .field-icon {
    transform: rotate(90deg);
}

/*
  Свёрнутое вручную прячется ДО отрисовки: nav-collapsed.js ставит на <html> класс
  `nav-closed-<группа>` из памяти браузера, пока разметки группы ещё нет, — а сервер рисует все
  группы раскрытыми. Без этого правила на каждой загрузке было бы видно, как свёрнутая группа
  сначала раскрыта, а потом схлопывается. admin.js затем закрывает сами `<details>` и снимает
  классы. Группу открытой страницы (nav-group-current) не прячет ничто. Группы перечислены
  поимённо: связать слово из класса <html> с атрибутом группы стиль сам не умеет.
*/
.nav-closed-events [data-group="events"]:not(.nav-group-current) > .nav-list,
.nav-closed-clients [data-group="clients"]:not(.nav-group-current) > .nav-list,
.nav-closed-school [data-group="school"]:not(.nav-group-current) > .nav-list,
.nav-closed-bot [data-group="bot"]:not(.nav-group-current) > .nav-list,
.nav-closed-system [data-group="system"]:not(.nav-group-current) > .nav-list {
    display: none;
}

.nav-closed-events [data-group="events"]:not(.nav-group-current) > .nav-group-title > .field-icon,
.nav-closed-clients [data-group="clients"]:not(.nav-group-current) > .nav-group-title > .field-icon,
.nav-closed-school [data-group="school"]:not(.nav-group-current) > .nav-group-title > .field-icon,
.nav-closed-bot [data-group="bot"]:not(.nav-group-current) > .nav-group-title > .field-icon,
.nav-closed-system [data-group="system"]:not(.nav-group-current) > .nav-group-title > .field-icon {
    transform: none;
}

.nav-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

/*
  Пункт — «Nav / пункт» (13:89): 36 px, пункты вплотную, иконка 20, подпись на 44 px от края
  экрана, как в компоненте (иконка — на 16 px вместо 14: поля шкалой, §1). Поля справа нет, как
  и в компоненте: «Подарочные сертификаты» (≈188 px) иначе не помещалось в 248 px и обрезалось.
  Высота — минимумом, а не полями: на широком экране кегль пункта растёт (--fs-ui до 16,5),
  а строка остаётся той же.
*/
.app-nav a {
    display: flex;
    align-items: center;
    gap: var(--s2);
    box-sizing: border-box;
    min-height: var(--nav-item-h);
    padding: 0 0 0 var(--s2);
    border-radius: var(--r-s);
    font-size: var(--fs-ui);
    color: var(--tx);
    text-decoration: none;
}

.nav-icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    color: var(--tx2);
}

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

.app-nav a:hover {
    background: var(--soft);
    color: var(--tx);
}

.app-nav a:hover .nav-icon {
    color: var(--primary);
}

/*
  Активный пункт — пункт раздела открытой страницы, и на её карточке и форме тоже (aria-current
  "page" или "true"): светлая подложка, текст и иконка --primary, 500 (§9, «Nav / пункт»
  state=active). Сплошная тёмная заливка сюда не идёт: она перетягивала на себя весь экран;
  полоски у левого края в компоненте нет.
*/
.app-nav a[aria-current] {
    background: var(--soft);
    color: var(--primary);
    font-weight: 500;
}

.app-nav a[aria-current] .nav-icon {
    color: var(--primary);
}

/*
  Свёрнутое меню — «Nav / меню свёрнутое» (13:308): одни иконки через 38 px, группы разделены
  чертой 24 px цветом --line-soft, заголовков нет; название раздела остаётся во всплывающей
  подсказке (title из admin.js). Группы здесь раскрыты все — свернуть их в колонке значков нечем,
  а закрытая спрятала бы свои значки (admin.js держит их открытыми, заголовок не нажимается).
*/
.nav-collapsed .app-nav {
    width: var(--nav-w-collapsed);
}

/*
  В 60 px знак не помещается рядом с кнопкой, а развернуть меню без кнопки нечем — поэтому знак
  прячется, а кнопка встаёт ровно на его место и по той же оси, что иконки разделов ниже.
  Первая иконка — на 88 px от верха окна (в компоненте 90: зазор под строкой — шкалой, --s2).
*/
.nav-collapsed .nav-mark {
    display: none;
}

.nav-collapsed .nav-head {
    justify-content: center;
    margin-bottom: var(--s2);
    padding: 0;
}

.nav-collapsed .nav-text,
.nav-collapsed .nav-group-title > * {
    display: none;
}

.nav-collapsed .app-nav a {
    justify-content: center;
    margin-bottom: 2px;
    padding-left: 0;
    padding-right: 0;
}

.nav-collapsed .nav-top + .nav-group,
.nav-collapsed .nav-group + .nav-group {
    margin-top: 0;
}

/* Заголовок группы в колонке значков — черта между группами: 8 px над ней, 10 под ней. */
.nav-collapsed .nav-group-title {
    display: block;
    height: 19px;
    padding: var(--s2) 0 0;
    box-sizing: border-box;
    pointer-events: none;
}

.nav-collapsed .nav-group-title::before {
    content: '';
    display: block;
    width: 24px;
    height: 1px;
    margin: 0 auto;
    background: var(--line-soft);
}

/* Кнопка-меню появляется только на узком экране. */
.nav-button {
    display: none;
}

/*
  Колонка справа от меню: всплывашка из layout стоит НАД содержимым страницы, а не рядом с ним.
  Обёртка нужна именно потому, что <main> подставляется целиком из шаблона раздела и вложить
  в него что-то из layout нельзя. Свой блок обёртка не оставляет: сообщения лежат в ней прямо,
  и после переезда в закреплённый стек (admin.js) в потоке страницы не остаётся пустого места.
*/
.app-main {
    /* min-width: 0 — иначе широкая таблица растягивает флекс-элемент и страница уезжает за край. */
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
}

.app-main > .toast {
    box-sizing: border-box;
    margin: var(--main-py) var(--pad-x) 0;
}

main {
    flex: 1;
    min-width: 0;
    padding: var(--main-py) var(--pad-x);
}

/*
  Всё содержимое занимает колонку целиком — и таблицы, и карточки, и формы, и блок дел
  (замечание владельца 2026-08-18). Своего потолка ширины ни у одной породы блоков нет:
  раньше списки шли во всю ширину, а карточки обрывались на 1280 px, и по админке гуляли
  белые блоки двух разных ширин. Читаемость держится ниже — на потолке поля ввода
  (`--field-max`) и на колонках `.fields-columns` / `.form-grid` внутри блока.
*/
main > .card {
    box-sizing: border-box;
}

/* Экран обязательной смены пароля меряет свои 404 px по содержимому — как утверждено задачей 06. */
.centered-main > .card {
    box-sizing: content-box;
}

/* Экран без меню (обязательная смена пароля) — одна карточка по центру, тем же прямоугольником, что вход. */
.centered-main {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Поля и кнопка занимают всю карточку: ограничивать их внутри неё незачем. */
.centered-main form input[type="password"] {
    max-width: none;
}

/* --- страница входа --- */

.centered-page {
    display: flex;
    min-height: 100vh;
    align-items: center;
    justify-content: center;
    padding: var(--s7) var(--s5);
    box-sizing: border-box;
    background: var(--login-bg);
}

/* Один прямоугольник на оба экрана без меню: вход и обязательная смена пароля. */
.centered-page .card,
.centered-main .card {
    width: 100%;
    max-width: 404px;
    margin: 0;
    padding: var(--s6);
    box-shadow: var(--shadow-login);
}

.login-logo {
    display: block;
    width: 180px;
    max-width: 100%;
    margin: 0 auto;
}

.login-title {
    margin: var(--s4) 0 var(--s5);
    text-align: center;
    font-family: var(--f-head);
    font-size: 24px;
    font-weight: 400;
    line-height: 1.25;
    letter-spacing: .01em;
    color: var(--primary);
}

.centered-page button[type="submit"],
.centered-main button[type="submit"] {
    width: 100%;
}

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

/*
  Белая панель на фоне страницы. `.block` — имя дизайн-системы (`Дизайн-система.md` §5,
  `<section class="block">`), `.card` — прежнее имя той же панели: правило одно, второго такого же
  нет (задача 21 релиза 0.5.0). Экраны переходят на `.block` своими задачами.
*/
.card,
.block {
    margin: 0 0 var(--gap);
    padding: var(--card-pad);
    background: var(--card);
    box-shadow: var(--shadow-block);
    border-radius: var(--r-l);
}

.card > *:last-child,
.block > *:last-child {
    margin-bottom: 0;
}

.card > h1 {
    margin-bottom: var(--s4);
}

/* --- язык блоков (задача 21 релиза 0.5.0; `Дизайн-система.md` §5) --- */

/*
  Шапка блока (компонент «Блок / шапка» 14:286): заголовок, счётчик тихим и одно действие блока
  тихой ссылкой у правого края. Высота — кнопки s (36), от шапки до содержимого — --s3.
*/
.block-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px var(--s2);
    min-height: 36px;
    margin: 0 0 var(--s3);
}

.block-title {
    margin: 0;
}

/* Счётчик — роль «счётчик в шапке блока»: кегль заголовка, обычный вес, тихий цвет (§2). */
.block-count {
    font-size: var(--fs-h2);
    color: var(--tx2);
    font-variant-numeric: tabular-nums;
}

/* Сводка свёрнутого блока («1 отмена · 1 перенос») и действие блока — у правого края. */
.block-meta,
.block-header .block-action {
    margin-left: auto;
}

.block-meta {
    font-size: var(--fs-ui);
    color: var(--tx2);
}

/* Пустой блок: вместо списка одна тихая строка (§2 — «пустое состояние», 12,5, --tx2). */
.block-empty {
    margin: 0;
    padding: var(--s3) 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

/*
  Empty section lists (§5): one block with a centred quiet line, padded like `.deposit-empty`. Nine lists put the
  line straight in `.block-list`: clients, bookings, certificates, gifts, directions, instructors, locations,
  courses and audit (owner's decision).
*/
.block-list > .block-empty {
    padding: var(--s6) 0;
    text-align: center;
}

/*
  «Показать все N» — тихая ссылка в подвале длинного списка (14:289; §3 — тихая ссылка
  подчёркнута). Остальные строки раскрываются на месте: `<details>` без скриптов, строки после
  предела помечены `row-more`, и пока подвал закрыт, их не видно. Раскрытый подвал уходит —
  раскрывать больше нечего. Черту над подвалом рисует последняя видимая строка списка.
*/
.block-footer > summary {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 40px;
    padding-top: var(--s1);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--primary);
    cursor: pointer;
    list-style: none;
}

.block-footer > summary::-webkit-details-marker {
    display: none;
}

.block-footer > summary > span {
    text-decoration: underline;
}

.block-footer > summary:hover {
    color: var(--accent);
}

.block-footer .field-icon {
    position: static;
    width: 16px;
    height: 16px;
    margin: 0;
    color: currentColor;
    transform: rotate(90deg);
}

.block:has(> .block-footer:not([open])) .row-more {
    display: none;
}

.block-footer[open] {
    display: none;
}

/*
  Свёрнутый блок (14:286, closed / open): вся шапка — `<summary>`, шеврон слева смотрит вправо,
  у раскрытого — вниз. Так живёт «История» занятия.
*/
.block-collapsible > summary {
    margin-bottom: 0;
    cursor: pointer;
    list-style: none;
}

.block-collapsible > summary::-webkit-details-marker {
    display: none;
}

.block-collapsible[open] > summary {
    margin-bottom: var(--s3);
}

.block-collapsible > summary > .field-icon {
    position: static;
    width: 20px;
    height: 20px;
    margin: 0;
    color: var(--tx2);
}

.block-collapsible[open] > summary > .field-icon {
    transform: rotate(90deg);
}

/*
  Сетка страницы из блоков (§5 «Сетка и телефон»). `.blocks` — от 1100 px главная колонка
  (растягивается) и боковая 360 (данные человека, справка), уже — одна колонка; главная с длинной
  формой или таблицей и боковая справка выравниваются только по верху (пункт 29). Блок, стоящий
  один в ряду, идёт во всю ширину — `.block-full`. `.blocks-row` — ряд из двух-трёх равных
  коротких блоков одной высоты: ряд тянет их до самого высокого, пустое место остаётся внизу
  блока (пункт 29, Ч3). Порядок на телефоне — порядок разметки.
*/
.blocks,
.blocks-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap);
    margin: 0 0 var(--gap);
}

.blocks {
    align-items: start;
}

.blocks-row {
    align-items: stretch;
}

/*
  Главная и боковая — одной высоты: у карточки сертификата «Для сборки сертификата» и «Информация» — ряд
  коротких блоков (кадр 354:7562: 600 и 360, высота по самому высокому; §5, Ч3), а не главная колонка
  со справкой.
*/
.blocks.blocks-even {
    align-items: stretch;
}

.blocks > .block,
.blocks > .card,
.blocks-row > .block,
.blocks-row > .card {
    margin: 0;
}

@media (min-width: 1100px) {
    .blocks {
        grid-template-columns: minmax(0, 1fr) 360px;
    }

    .blocks > .block-full {
        grid-column: 1 / -1;
    }

    .blocks-row {
        grid-template-columns: none;
        grid-auto-flow: column;
        grid-auto-columns: minmax(0, 1fr);
    }

    /*
      Фото первым блоком ряда — колонкой 240, остальные блоки делят остаток поровну (кадр карточки
      хвоста 358:9954: 240 · 350 · 350; карточка инструктора — «Фото · Информация», задача 24).
      Высоту ряда задают соседи, фото заполняет свой блок по высоте.
    */
    .blocks-row:has(> .block-photo:first-child) {
        grid-template-columns: 240px;
    }

    /* Квадратное фото в ряду: 240 × 240 с полями блока, пока соседи ниже; выше соседи — тянется с ними. */
    .blocks-row > .block-photo-square {
        min-height: calc(240px - 2 * var(--card-pad));
        aspect-ratio: auto;
        max-width: none;
    }

    /* Фото хвоста в ряду — белым блоком, снимок заполняет его по высоте ряда (кадр 358:9954). */
    .blocks-row > .block-photo-tail {
        min-height: 240px;
        padding: var(--card-pad);
        background: var(--card);
        box-shadow: var(--shadow-block);
    }

    .blocks-row > .block-photo-tail > img {
        position: absolute;
        width: calc(100% - 2 * var(--card-pad));
        height: calc(100% - 2 * var(--card-pad));
        aspect-ratio: auto;
        margin: 0;
    }
}

/*
  Фото в блоке заполняет его по высоте (`object-fit: cover`; «Поле / фото · снимок» 167:5026):
  высоту ряда задают соседние блоки, а не снимок, поэтому он лежит поверх поля блока, а сам блок
  держит только минимум — на телефоне, где блок стоит один, это и есть высота снимка.
*/
.block-photo {
    position: relative;
    min-height: 240px;
}

.block-photo > img {
    position: absolute;
    inset: var(--card-pad);
    width: calc(100% - 2 * var(--card-pad));
    height: calc(100% - 2 * var(--card-pad));
    object-fit: cover;
    border-radius: var(--r-m);
    background: var(--soft);
}

/*
  Таблица внутри блока (§7: «таблица живёт внутри блока»). Текст ячеек стоит на линии поля
  блока, там же, где заголовок блока и текст шапки страницы (§1: «отступ от левого края блока до
  текста одинаков у заголовка блока, строк таблицы и полей карточки»): у первой колонки своего
  поля нет, между колонками — --s3, последняя кончается на правом поле блока. Кадры списков
  справочников (356:7639) рисуют ячейки с полем 16 внутри поля блока — это расходится с §1,
  правило сильнее; кадры этапа 5 (353:6655, 352:6905) стоят по правилу. Подсветка строки под
  курсором выходит в поле блока тенью крайних ячеек, чтобы не прилипать к тексту.
*/
.block table {
    min-width: 0;
}

.block th,
.block td {
    padding-right: 0;
    padding-left: var(--s3);
}

.block th:first-child,
.block td:first-child {
    padding-left: 0;
}

.block tbody tr:hover td:first-child {
    box-shadow: calc(-1 * var(--card-pad)) 0 0 var(--row-hover);
}

.block tbody tr:hover td:last-child {
    box-shadow: var(--card-pad) 0 0 var(--row-hover);
}

/* Обёртка строк-карточек внутри блока — не вторая панель: панель уже блок. */
.block .table-scroll {
    margin-bottom: 0;
    background: none;
    box-shadow: none;
    border-radius: 0;
}

/*
  Сводка сущности (106:7211; §5 «Карточка сущности»): итог-фраза слева, вертикальная черта,
  факты тихим текстом, события кнопками справа; на телефоне — столбиком.
*/
.summary {
    display: flex;
    align-items: center;
    gap: var(--gap);
}

.summary-total {
    display: flex;
    flex: 0 0 auto;
    align-items: baseline;
    gap: var(--s2);
    margin: 0;
    color: var(--tx2);
    white-space: nowrap;
}

.summary-total .figure {
    color: var(--tx);
}

.summary-divider {
    flex: 0 0 1px;
    align-self: center;
    height: 32px;
    background: var(--line-soft);
}

.summary-facts {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    color: var(--tx2);
}

.summary-facts > .badge {
    margin-right: var(--s3);
}

/* Человек в сводке — ссылкой на свою карточку, стиль «ссылка в тексте» (body-link, А28). */
.summary-person {
    color: var(--primary);
    text-decoration: underline;
}

.summary-actions {
    display: flex;
    flex: 0 0 auto;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-left: auto;
}

.summary-actions form {
    margin: 0;
}

.summary-actions button,
.summary-actions .button {
    margin-top: 0;
}

/*
  Факт, который раскрывается по нажатию, — «свободных хвостов: 12» с разбивкой по размерам.
  Раскрытое встаёт поверх страницы под фактом, как меню «Ещё», и не раздвигает сводку.
*/
.summary-fact {
    position: relative;
    display: inline-block;
}

.summary-fact > summary {
    display: inline;
    cursor: pointer;
    list-style: none;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
}

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

.summary-fact > :not(summary) {
    position: absolute;
    z-index: 25;
    top: calc(100% + var(--s1));
    left: 0;
    margin: 0;
    padding: var(--s3);
    color: var(--tx);
    white-space: nowrap;
    background: var(--card);
    border-radius: var(--r-s);
    box-shadow: var(--shadow-float);
}

/*
  Атомарный факт (раскрывающийся `details.summary-fact`, кнопка «Привязать к клиентке») и «&nbsp;·» за ним —
  одним куском: после inline-block и inline-flex Chromium переносит строку и перед неразрывным пробелом (замер
  26.09), и «·» вставал в начало строки. Без переноса — только стык: сам факт внутри куска переносится как
  раньше, а раскрывающийся оставляет место под «&nbsp;·» (0,55em у Golos Text) — иначе в узкой колонке (107 px
  у отменённого занятия на 768) кусок вылезал за край. Перед куском — обычный пробел.
*/
.summary-keep {
    white-space: nowrap;
}

.summary-keep > * {
    white-space: normal;
}

.summary-keep > .summary-fact {
    max-width: calc(100% - 0.6em);
}

/*
  «Служебное» — одной строкой (126:12642; пункт 19): слева даты и «История изменений →» через
  « · », справа у края блока «Удалить» или тихая причина, почему удалить нельзя.
*/
.service-block {
    display: flex;
    align-items: center;
    gap: var(--s5);
}

.service-dates {
    display: flex;
    flex: 1 1 auto;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1) var(--s2);
    min-width: 0;
    margin: 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

/*
  «·» — в конце части, а не в начале следующей: ряд переносится (flex-wrap), и прежний `* + *::before` вставал
  в начало второй строки. Знак строчный, не inline-block: к последнему слову части он приклеен и один на
  строку не уходит, даже когда часть сжата (замер 26.09).
*/
.service-dates > *:not(:last-child)::after {
    content: "·";
    margin-left: var(--s2);
    font-size: var(--fs-small);
    font-weight: 400;
    color: var(--tx2);
}

.service-history {
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--primary);
    text-decoration: none;
}

.service-history:hover {
    color: var(--accent);
    text-decoration: underline;
}

.service-obstacle {
    flex: 0 1 auto;
    margin: 0;
    font-size: var(--fs-small);
    color: var(--tx2);
    text-align: right;
}

/* «Отменить занятие» of the lesson card stands here like «Удалить», after the reason or before it (owner 27.09). */
.service-cancel,
.service-delete {
    flex: 0 0 auto;
    margin: 0;
}

.service-cancel button,
.service-delete button {
    margin-top: 0;
}

/*
  Without scripts the body of the cancelling window stays in its form, and a form that does not shrink takes the
  warning on one line (≈1 100 px in a block of 940): the dates were squeezed to nothing and the page scrolled
  sideways. Then «Служебное» is a column: the dates, the reason, the form across the block, «Удалить». With scripts
  the body moves into <dialog> (admin.js) and the row stays a row.
*/
.service-block:has(> .service-cancel > .confirm-body) {
    flex-direction: column;
    align-items: stretch;
}

/* --- формы --- */

/*
  Подпись поля — заголовок, а подсказка под полем — пояснение, и различаются они видом
  (правка владельца 2026-08-25: «пометки и заголовки у нас везде одинаковые», подписи
  «Размер» и «Комплектность» читались продолжением подсказки предыдущего поля). Прежде обе
  строки шли одним кеглем приглушённым цветом и отличались только весом 500 против 400 —
  на 12,5 px эта разница не видна. Теперь заголовок набран основным цветом и полужирным,
  подсказка осталась приглушённой и обычной; размер у них общий — иначе форма рассыпается
  на четыре ступени текста вместо двух.
*/
label {
    display: block;
    margin-top: var(--s4);
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--tx);
}

input[type="text"],
input[type="password"],
input[type="date"],
input[type="time"],
textarea,
select {
    width: 100%;
    box-sizing: border-box;
    margin-top: var(--s1);
    padding: var(--btn-py) var(--s3);
    font-family: inherit;
    /* 16px — иначе Safari на телефоне приближает страницу при фокусе в поле. */
    font-size: 16px;
    color: var(--tx);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-s);
}

/* Native single lists share the field chevron; multiple lists stay listboxes. */
select:not([multiple]) {
    appearance: none;
    padding-right: calc(var(--s3) + 20px + var(--s2));
    background-image: url("/img/select-chevron.svg");
    background-repeat: no-repeat;
    background-position: right var(--s3) center;
    background-size: 20px;
}

/*
  Однострочные поля берут высоту из одного токена (критерий 17). Своё внутреннее устройство
  у даты, времени и списка выбора разное, поэтому одинаковые поля от одинаковых полей ползли
  на 1–3 px; вертикальный отступ заменяется высотой, горизонтальный остаётся прежним.
  Многострочное «Описание» под правило не попадает — оно тянется по высоте намеренно.
*/
input[type="text"],
input[type="password"],
input[type="date"],
input[type="time"],
select:not([multiple]) {
    height: var(--field-h);
    padding-top: 0;
    padding-bottom: 0;
}

/*
  Карточки тянутся на ширину контента, а поля — нет: строка ввода в 1000 px нечитаема. Потолок
  поля живёт своим токеном (решение Ж-2.3): он поднят до 520 px под двухколоночную форму —
  два поля по 520 читаются лучше, чем одно на 700.
*/
main form input[type="text"],
main form input[type="password"],
main form input[type="date"],
main form input[type="time"],
main form textarea,
main form select {
    max-width: var(--field-max);
}

/*
  Описание события — единственное многострочное поле; тянется только по высоте.
  `display: block` обязателен: строчный элемент оставляет под собой место под нижние выносные
  знаки, и подсказка отходила от поля на 9,5–11 px вместо общих 4.
*/
textarea {
    display: block;
    min-height: 5.5em;
    resize: vertical;
}

/*
  Комментарий, заметка, примечание — одна строка, которая растёт по мере набора (пятый круг
  21.09, замечание 6: «зачем такой большой комментарий, там может быть одно слово»). Высота
  одной строки — та же, что у соседних полей (`--field-h`). Там, где браузер не умеет
  `field-sizing` (Safari), высоту подгоняет `admin.js` — `setUpGrowingFields`.
*/
textarea.grow {
    min-height: var(--field-h);
    padding-top: calc((var(--field-h) - 1.5em - 2px) / 2);
    padding-bottom: calc((var(--field-h) - 1.5em - 2px) / 2);
    line-height: 1.5;
    field-sizing: content;
    resize: none;
    overflow: hidden;
}

/* Несколько инструкторов на одном событии: список выбора показывает сразу четыре строки. */
select[multiple] {
    padding: var(--s2) var(--s3);
}

input[type="text"]:focus,
input[type="password"]:focus,
input[type="date"]:focus,
input[type="time"]:focus,
textarea:focus,
select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--focus-ring);
}

input:disabled,
select:disabled {
    color: var(--tx-disabled);
    background-color: var(--line-soft);
    cursor: not-allowed;
}

/*
  Поле только для чтения — значение видно и уходит с формой, но не правится (начало номера и «начать с
  номера» курса, по которому выданы сертификаты, задача 11 релиза 0.5.0): фон выключенного поля, текст —
  обычным цветом, его читают.
*/
input[readonly] {
    background: var(--line-soft);
    cursor: default;
}

label.checkbox {
    display: flex;
    align-items: center;
    gap: var(--s2);
    font-size: var(--fs);
    font-weight: 400;
    color: var(--tx);
}

label.checkbox input {
    width: auto;
    margin: 0;
    accent-color: var(--primary);
}

/*
  Галочка админки — квадрат 20, как компонент «Галочка» (22:298, `Дизайн-система.md` §4; форма
  направления 356:7735, задача 24 релиза 0.5.0): родная галочка браузера в 13 px терялась рядом
  с текстом в 15. Сама галочка остаётся родной (`accent-color`): клавиатура, фокус и работа без
  стилей — от браузера. Вход не меняется (кадр 60:73), поэтому правило — только внутри админки.
*/
.app-main label.checkbox input[type="checkbox"],
.modal label.checkbox input[type="checkbox"] {
    flex: none;
    width: 20px;
    height: 20px;
}

/*
  Выпадающий список с поиском — фрагмент `searchSelect`. Настоящий <select> остаётся в форме
  и уходит на сервер: с включённым JS он только не показывается, без JS показывается он один.
  Своих цветов и размеров компонент не заводит — всё из токенов.
*/
.ss-native {
    display: none;
}

/* Потолок тот же, что у нативных полей: два поля одного типа не могут быть разной ширины. */
.ss {
    position: relative;
    max-width: var(--field-max);
    margin-top: var(--s1);
}

/*
  Отступ поля от подписи уже задан обёрткой — второй раз он не нужен. Селектор обязан быть
  не слабее `input[type="text"]`, иначе базовые 4 px складываются с отступом обёртки и список
  с поиском отходит от своей подписи на 8 px, а соседнее поле — на 4. Справа — место под шеврон
  (ниже): длинная подпись выбранного кончается «…» перед ним, а не режется краем поля.
*/
.ss > .ss-input {
    margin-top: 0;
    padding-right: calc(var(--s3) + 20px + var(--s2));
    text-overflow: ellipsis;
}

/*
  Шеврон закрытого поля — «Поле / список» (компонент 22:283, кадры 349:5922 и 348:5232); строит его скрипт,
  размер и поворот — как у поля «Клиентка» формы записи. Якорь снизу держит знак на линии поля, и под чипами
  множественного выбора тоже. Остальное даёт `.field-icon`: цвет, щелчок проходит в поле, раскрытый список
  (`.ss-list`, 20) — поверх знака (1).
*/
.ss > .ss-chevron {
    top: auto;
    right: var(--s3);
    bottom: calc((var(--field-h) - 20px) / 2);
    left: auto;
    width: 20px;
    height: 20px;
    margin: 0;
    transform: rotate(90deg);
}

.ss-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-bottom: var(--s2);
    font-size: var(--fs-ui);
}

/*
  Пока не выбрано ничего, полосы чипов нет вовсе: пустая всё равно отдавала свои 8 px, и поле
  множественного выбора стояло на 8 px ниже соседних полей — замер 14.08 (12 px против 4).
*/
.ss-chips:empty {
    display: none;
}

/* Список раскрывается поверх страницы: иначе соседние поля разъезжались бы при каждом вводе. */
.ss-list {
    position: absolute;
    z-index: 20;
    right: 0;
    left: 0;
    max-height: 240px;
    margin: var(--s1) 0 0;
    padding: var(--s1) 0;
    overflow-y: auto;
    list-style: none;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-s);
    box-shadow: var(--shadow-float);
}

.ss-option {
    padding: var(--s2) var(--s3);
    cursor: pointer;
}

.ss-option:hover,
.ss-active {
    background: var(--row-hover);
}

/* Отмеченное видно в самом списке — при множественном выборе иначе не понять, что уже взято. */
.ss-option[aria-selected="true"] {
    font-weight: 500;
    background: var(--soft);
}

.ss-nothing {
    padding: var(--s2) var(--s3);
    font-size: var(--fs-small);
    color: var(--tx2);
}

/*
  Три уровня кнопок и ни одним больше: основное действие — заливка, второстепенное — белая
  с границей, необратимое — фирменный розовый из палитры (алый с макета в схему не входит).

  Высота берётся тем же токеном, что и у однострочного поля (критерий 17). Своей вёрсткой
  кнопка набирала 42,3 px: на телефоне это ниже цели нажатия в 44 px, а в полосе фильтров она
  стояла ниже соседних полей (дефект P-25 прогона QA 2026-08-26). Задан минимум, а не высота, —
  подпись в две строки на 375 px обязана поместиться целиком; содержимое центрируется флексом,
  иначе добавленные пиксели легли бы под текст одной стороной.
*/
button,
.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    min-height: var(--field-h);
    margin-top: var(--s4);
    padding: var(--btn-py) var(--s4);
    font-family: inherit;
    font-size: var(--fs-ui);
    font-weight: 500;
    line-height: 1.4;
    color: var(--on-primary);
    background: var(--primary);
    border: 1px solid var(--primary);
    border-radius: var(--r-s);
    text-decoration: none;
    cursor: pointer;
}

button:hover,
.button:hover {
    background: var(--primary-hover);
    border-color: var(--primary-hover);
    color: var(--on-primary);
}

.button-secondary {
    color: var(--primary);
    background: var(--card);
    border-color: var(--line);
}

.button-secondary:hover {
    color: var(--primary);
    background: var(--soft);
    border-color: var(--accent);
}

/*
  Необратимое — белая кнопка с розовым текстом и розовой рамкой, и в диалоге подтверждения тоже
  (решение владельца 21.09, пятый круг, ответ 8): розовая заливка спорила с единственной
  бирюзовой заливкой экрана и читалась тревогой там, где это просто «Удалить».
*/
.button-danger {
    color: var(--danger);
    background: var(--card);
    border-color: var(--danger-line);
}

.button-danger:hover {
    color: var(--danger-hover);
    background: var(--danger-soft);
    border-color: var(--danger);
}

button:disabled {
    color: var(--tx-disabled);
    background: var(--line-soft);
    border-color: var(--line-soft);
    cursor: not-allowed;
}

/*
  Скрытая кнопка обязана исчезать: авторское `button { display: inline-flex }` выше перебивает
  браузерное `[hidden] { display: none }` — та же грабля, что у `.filters-apply[hidden]` и
  `.field-pair[hidden]`. Без этой строки «Убрать выбранную фотографию» оставалась на экране,
  когда убирать уже нечего (замечание владельца 2026-08-26).
*/
button[hidden],
.button[hidden] {
    display: none;
}

/*
  Кнопки формы — своей строкой под полями. Без этой обёртки кнопка встаёт справа от последнего
  поля: поле шириной 420 px оставляет рядом место, и браузер тянет туда следующий инлайновый элемент.
  «Сохранить» и «Отмена» стоят тесной парой (Ж-4.6): малый зазор внутри пары и большой до
  соседнего блока — разнесённые кнопки читаются как две несвязанные вещи.
*/
.form-actions {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin-top: var(--s5);
}

.form-actions button,
.form-actions .button,
.form-actions .button-link {
    margin: 0;
}

/*
  Второстепенное действие рядом с основным — текстом, а не второй заливкой. Геометрия повторена
  здесь целиком, потому что «Отмена» — это <a>, а «Выйти» — <button>: на ссылку правило кнопок
  выше не распространяется, и без этих строк два одинаковых с виду действия расходились бы по
  высоте, а на телефоне ссылка не дотягивала бы до цели нажатия в 44 px (дефект P-25).
*/
.button-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    min-height: var(--field-h);
    margin-left: var(--s3);
    padding: var(--btn-py) 0;
    border: none;
    background: none;
    color: var(--primary);
    text-decoration: underline;
}

.button-link:hover {
    background: none;
    color: var(--accent);
}

/*
  Кнопка s — 36 px вместо 44 (`Дизайн-система.md` §3, компонент `Button` m · s): у всех видов
  кнопок, но только в строке таблицы и в раскрытой строке — шапка страницы, форма и диалог
  остаются на m. Поле по бокам на 4 px уже, чем у m, как в компоненте. Тихая ссылка поля по
  бокам не получает и на s. На телефоне s снова становится m — см. медиазапрос телефона.
*/
.button-s {
    min-height: 36px;
    padding: var(--s1) var(--s3);
}

.button-link.button-s {
    padding: var(--s1) 0;
}

/*
  Форма события — две колонки (макет владельца 14.08, Ж-1.5): слева занятие во времени, справа
  кто и почём. Одна вертикальная колонка полей и создавала «пусто справа» — не величина потолка
  ширины. Колонки по высоте не выравниваются: у них разное число полей.

  Колонка меряется полем, а не блоком: `1fr` растягивал её вместе с блоком, и после снятия
  потолка ширины (замечание владельца 2026-08-18) на 1920 px между левым и правым полем зияло
  266 px пустоты — сами-то поля остаются по 520 px. Колонка ровно в поле держит пару вместе на
  любой ширине, а свободное место уходит вправо, как у всех остальных блоков админки.
*/
.form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, var(--field-max)));
    gap: 0 var(--s6);
    align-items: start;
}

/*
  Колонка формы в две колонки: первое поле стоит сразу под шапкой блока — зазор даёт шапка (--s3),
  свой верхний отступ подписи и группы переключателей здесь лишний (кадр 348:5232). Переключатель
  с подсказкой под ним: подсказка стоит под полосой так же близко, как под полем ввода.
*/
.form-col > :first-child,
.form-col > :first-child > label:first-child,
.form-col > fieldset:first-child {
    margin-top: 0;
}

.form-col .segmented {
    margin-bottom: 0;
}

/* Блок на всю ширину под колонками — «Описание»: заметке нужна строка, а не колонка. */
.form-wide {
    grid-column: 1 / -1;
}

.form-wide textarea {
    max-width: none;
}

/*
  Короткая форма — два-три поля одной колонкой `--field-max-wide` (640), не в две колонки и не
  во всю ширину блока (`Дизайн-система.md` §6, пункт 15; отв. 7, Ч4): направление, локация,
  курс сертификата, учётная запись. Поле занимает всю колонку, как в двухколоночной форме —
  свою, поэтому потолок `--field-max` здесь снят: потолок — сама колонка.
*/
.form-single {
    max-width: var(--field-max-wide);
}

.form-single input[type="text"],
.form-single input[type="password"],
.form-single input[type="date"],
.form-single input[type="time"],
.form-single textarea,
.form-single select {
    max-width: none;
}

/* Первое поле короткой формы стоит сразу под краем блока (кадры 356:7735, 357:8937). */
.form-single > :first-child > label:first-child {
    margin-top: 0;
}

/*
  Подсказка ко всему блоку формы — строкой под шапкой блока, до полей (форма клиентки, кадры
  353:7032 и 353:7165, задача 26 релиза 0.5.0): «Обязательны только ФИО и телефон…», «Дату,
  редакцию текста…». От шапки её отделяет зазор шапки (--s3), колонки полей стоят на --s4 ниже;
  строки согласий отступают от неё своим полем. У блока без шапки она — первая строка блока
  («Пароль система выдаст сама…», новая учётная запись, кадр 363:12348, задача 27).
*/
.block-hint {
    margin: 0;
}

.block-hint + .form-grid,
.block-hint + .form-single {
    margin-top: var(--s4);
}

/*
  Согласия строками (`Дизайн-система.md` §6, пункт 26 «Добавить в код»; кадры 353:7165 и 353:7241):
  подпись, дата последнего изменения тихим и переключатель s «Получено · Не получено». Строки — одна
  сетка на весь список (`subgrid`): столбец дат и столбец переключателей у всех строк общие, поэтому
  дата «Фото и видео», у которого переключателя нет, стоит в том же столбце, что и у остальных, а место
  переключателя пустует. Между строками — тонкая черта. Подпись, дата и текст переключателя — на одной
  базовой линии. На телефоне дата — справа от названия, переключатель — во всю ширину под ними
  (медиазапрос телефона).
*/
.consent-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr) max-content max-content;
    column-gap: var(--s4);
}

.consent-row {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: baseline;
    padding: var(--s2) 0;
}

.consent-row + .consent-row {
    border-top: 1px solid var(--line-soft);
}

.consent-title {
    grid-column: 1;
}

.consent-date {
    grid-column: 2;
    font-size: var(--fs-small);
    color: var(--tx2);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.consent-row > .segmented {
    grid-column: 3;
}

/* «Фото и видео»: под названием — пояснение, места галочками и отказ формы, во всю ширину строки. */
.consent-row > .consent-hint,
.consent-row > .photo-places,
.consent-row > .field-error {
    grid-column: 1 / -1;
}

.consent-row > .consent-hint {
    margin: var(--s2) 0 0;
}

/*
  Места публикации — в строку, пока помещаются, и переносятся сами; на телефоне — столбцом (кадр
  353:7241). Галочка стоит у ПЕРВОЙ строки подписи, а не посередине двух: длинная подпись на телефоне
  переносится, и галочка посередине уезжала бы в междустрочье и переставала читаться галочкой этой строки.
*/
.photo-places {
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--s4);
    margin-top: var(--s1);
}

.photo-places > label.checkbox {
    align-items: flex-start;
    margin: 0;
    padding: var(--s2) 0;
}

.photo-places > label.checkbox input {
    margin-top: calc((var(--fs) * 1.5 - 20px) / 2);
}

/* Начало и окончание — одна пара в одну строку: их читают и правят вместе. */
.field-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s3);
    max-width: var(--field-max);
}

/*
  Скрытие задаётся явно: авторское `display: grid` выше перебивает браузерное
  `[hidden] { display: none }`, и одного атрибута мало. Без этой строки переключатель
  «Взрослый / Детский» в форме хвоста не работал вовсе — скрипт ставил атрибут, а на экране
  оставались обе пары полей сразу (замечание владельца 2026-08-25: «ты так и не сделал
  взрослый детский размер»). Та же причина, что у `.filters-apply[hidden]`.
*/
.field-pair[hidden] {
    display: none;
}

/*
  Поле суммы (22:270; §6, пункт 16): число по левому краю, как в остальных полях, «₽» справа
  внутри поля. В форме — во всю колонку (А25), в строке рядом с другими полями — 144 (части
  оплаты, отмена записи, «Кого предупредить»).
*/
/* В строке рядом с другими полями поле не распирает строку своим минимумом. */
.money-field {
    min-width: 0;
}

.money-control {
    position: relative;
    max-width: var(--field-max);
    margin-top: var(--s1);
}

.money-control input {
    margin-top: 0;
    padding-right: calc(var(--s3) + 1em);
    text-align: left;
}

.money-sign {
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    display: flex;
    align-items: center;
    font-size: 16px;
    color: var(--tx2);
    pointer-events: none;
}

.form-single .money-control {
    max-width: none;
}

.money-row .money-control {
    width: 144px;
}

/*
  Поле с иконкой внутри (макет владельца 14.08). Обёртка нужна и выпадающему списку с поиском:
  скрипт кладёт свою разметку рядом с настоящим <select>, то есть внутрь этой же обёртки,
  и иконка остаётся на месте. Иконка центрируется по самому полю, а не по обёртке: сверху
  у поля есть отступ от подписи, и без вычета иконка вставала бы на два пикселя выше.
*/
.field-control {
    position: relative;
    max-width: var(--field-max);
}

/*
  z-index обязателен: выпадающий список с поиском строит себе позиционированную обёртку `.ss`,
  она идёт в разметке ПОСЛЕ иконки и с тем же нулевым уровнем — и белое поле закрашивало
  булавку у «Локации». Уровень ниже раскрытого списка (`.ss-list`, 20), поэтому список
  по-прежнему поверх всего.
*/
.field-icon {
    position: absolute;
    z-index: 1;
    top: var(--s1);
    bottom: 0;
    left: var(--s3);
    width: 18px;
    height: 18px;
    margin: auto 0;
    color: var(--tx2);
    pointer-events: none;
}

/* Отступ под иконку — только у поля, у которого она есть: иначе значение без иконки уезжает вправо. */
.field-control.has-icon input,
.field-control.has-icon .ss-input {
    padding-left: calc(var(--s3) + 18px + var(--s2));
}

/*
  `text-wrap: pretty` — не балансировка, а запрет «сироты»: одного слова на последней строке
  абзаца. Заголовки балансируются целиком (`balance`), а подсказке важна только последняя
  строка — балансировать многострочный абзац значит сузить его без причины.
*/
.hint,
.requirements {
    font-size: var(--fs-small);
    color: var(--tx2);
    text-wrap: pretty;
}

.hint {
    margin: var(--s1) 0 0;
}

.requirements {
    margin: 0 0 var(--s3);
    padding-left: var(--s5);
}

/* Ошибка показывается под своим полем, а не одним сообщением наверху формы. */
.field-error {
    margin: var(--s1) 0 0;
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--danger);
}

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

input.invalid:focus,
textarea.invalid:focus,
select.invalid:focus {
    border-color: var(--danger);
    box-shadow: 0 0 0 3px var(--danger-soft);
}

/*
  «Показать» у поля пароля: одноразовый пароль набирают с бумажки или из сообщения, и опечатку
  в точках не видно. Кнопка стоит внутри поля и не сдвигает раскладку формы; собственный отступ
  поля перенесён на обёртку, иначе он вылез бы наружу и сбил вертикальный центр кнопки.
*/
.password-field {
    position: relative;
    margin-top: var(--s1);
}

.password-field input {
    margin-top: 0;
    padding-right: 92px;
}

.password-toggle {
    position: absolute;
    top: 50%;
    right: var(--s2);
    transform: translateY(-50%);
    margin: 0;
    padding: var(--s1) var(--s2);
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--primary);
    background: none;
    border: none;
    border-radius: var(--r-s);
    cursor: pointer;
}

.password-toggle:hover {
    color: var(--accent);
    background: var(--soft);
}

.message {
    margin-bottom: var(--gap);
    padding: var(--s3) var(--s4);
    font-size: var(--fs-ui);
    background: var(--soft);
    border: 1px solid var(--soft-line);
    border-radius: var(--r-s);
}

.message-error {
    color: var(--danger);
    background: var(--danger-soft);
    border-color: var(--danger-line);
}

/*
  Сообщение в форме трёх видов (компонент «Сообщение» 28:328; §8): отказ, предупреждение,
  которое не запрещает, и успех — знак слева, текст цветом вида. Разметка фрагмента `message`
  — со знаком и `message-text`; простые `.message` входа и смены пароля (старый вид, этап 5 их не
  меняет) это правило не задевает.
*/
.message:has(> .message-text) {
    display: flex;
    align-items: flex-start;
    gap: 10px;
}

.message > .field-icon {
    position: static;
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    margin: 0;
    color: currentColor;
}

.message-text {
    flex: 1 1 auto;
    min-width: 0;
}

.message-ok {
    color: var(--ok);
}

.message-warn {
    color: var(--warn);
    background: var(--warn-soft);
    border-color: var(--warn-line);
}

/*
  Anketa status line of «Выгрузка данных» (frame 362:11686, «Анкета — состояние»): a white strip with a hairline,
  not a shadowed block — it reports, it holds no actions. The icon colour carries the state; the text stays --tx.
*/
.form-status {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 0 0 var(--s4);
    padding: var(--s3) var(--s4);
    font-size: var(--fs-ui);
    color: var(--tx);
    background: var(--card);
    border: 1px solid var(--line-soft);
    border-radius: var(--r-l);
}

.form-status > .field-icon {
    position: static;
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    margin: 0;
}

.form-status-ok > .field-icon {
    color: var(--ok);
}

.form-status-lost > .field-icon {
    color: var(--danger);
}

.form-status-text {
    min-width: 0;
}

.form-status-hint {
    display: block;
    margin-top: var(--s1);
    color: var(--tx2);
}

/* --- всплывающие сообщения --- */

/*
  Без JS toast остаётся обычным блоком над содержимым — тем же, чем раньше был `.message`.
  admin.js собирает такие блоки в закреплённый стек в углу экрана.
*/
.toast {
    position: relative;
    margin-bottom: var(--gap);
    padding: var(--s3) var(--s7) var(--s3) var(--s4);
    font-size: var(--fs-ui);
    background: var(--soft);
    border: 1px solid var(--soft-line);
    border-radius: var(--r-s);
}

/* Ссылка внутри всплывашки («показать занятия») стоит рядом с текстом, а не строкой ниже. */
.toast a {
    margin-left: var(--s1);
    white-space: nowrap;
}

.toast-ok {
    background: var(--soft);
    border-color: var(--soft-line);
}

.toast-error {
    color: var(--danger);
    background: var(--danger-soft);
    border-color: var(--danger-line);
}

.toast-stack {
    position: fixed;
    right: var(--s5);
    bottom: var(--s5);
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    width: min(400px, calc(100vw - 2 * var(--s5)));
}

.toast-stack .toast {
    margin: 0;
    box-shadow: var(--shadow-float);
}

.toast-close {
    position: absolute;
    top: 50%;
    right: var(--s2);
    transform: translateY(-50%);
    width: 28px;
    height: 28px;
    margin: 0;
    padding: 0;
    font-size: 19px;
    line-height: 1;
    color: inherit;
    background: none;
    border: none;
    border-radius: var(--r-s);
    cursor: pointer;
}

.toast-close:hover {
    color: inherit;
    background: rgba(35, 70, 93, .08);
}

/* --- подтверждение необратимого действия --- */

.confirmation .warning,
.modal-warning {
    padding: var(--s3) var(--s4);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--danger);
    background: var(--danger-soft);
    border: 1px solid var(--danger-line);
    border-radius: var(--r-s);
}

/*
  Окно подтверждения и диалог — 560 по центру поверх затемнения (§11); на телефоне — лист снизу
  (медиазапрос телефона). Главная кнопка справа, тихая «Отмена» слева от неё (§3).
*/
.modal {
    box-sizing: border-box;
    width: min(560px, calc(100vw - 2 * var(--s5)));
    padding: var(--s5);
    color: var(--tx);
    background: var(--card);
    border: 1px solid var(--line-soft);
    border-radius: var(--r-l);
    box-shadow: var(--shadow-float);
}

.modal::backdrop {
    background: rgba(35, 70, 93, .42);
}

.modal-title {
    margin-bottom: var(--s4);
}

.modal-body .confirmation,
.modal-body .confirm-body {
    margin: 0;
}

.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--s3);
    margin-top: var(--s5);
}

.modal-actions button {
    margin-top: 0;
}

/*
  Диалог-страница (фрагмент `dialogForm`): без скриптов это обычный белый блок формы в ширину
  окна, со скриптом тот же блок встаёт внутрь `<dialog class="modal">` и своей панели уже не
  рисует — панель там окно.
*/
.dialog {
    box-sizing: border-box;
    max-width: 560px;
}

.modal .dialog {
    max-width: none;
    margin: 0;
    padding: 0;
    background: none;
    box-shadow: none;
}

.dialog-title {
    margin: 0 0 var(--s4);
}

.dialog-actions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: var(--s3);
    margin-top: var(--s5);
}

.dialog-actions button {
    margin-top: 0;
}

.dialog-actions .button-link {
    margin-left: 0;
}

/* --- шапка раздела --- */

.section-header {
    margin-bottom: var(--gap);
    /*
      Шапка стоит на фоне, но с полями блока (`Дизайн-система.md` §1 и §5, пункт 23; Т2, Т3):
      крошки, заголовок и строка под ним начинаются на линии текста белых блоков, а кнопки
      кончаются на линии кнопок в блоках. `--card-pad` сам становится 16 на телефоне.
    */
    padding: 0 var(--card-pad);
}

/*
  Строка шапки — заголовок слева, кнопки справа по центру её высоты (компонент «Шапка
  страницы», 14:301); крошки стоят над строкой, подсказка — под ней. Пока строка помещается,
  кнопки на одной линии с заголовком; не помещается — переносятся под него целиком. На телефоне
  заголовок сначала переносится по словам рядом с кнопками — `.section-row > h1` в его медиазапросе.
*/
.section-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
    /*
      Высота строки не зависит от того, есть ли справа кнопка: иначе заголовок раздела
      с кнопкой и заголовок раздела без неё стоят на разной высоте, и первый пункт меню
      совпадает то с одним, то с другим. Значение — ровно высота кнопки, а она с 26.08 равна
      высоте однострочного поля, поэтому берётся тот же токен.
    */
    min-height: var(--field-h);
}

.section-header .button {
    margin-top: 0;
}

/* Строка-подсказка под заголовком страницы: одно расстояние на все разделы. */
.section-hint {
    margin: var(--s2) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
    text-wrap: pretty;
}

/*
  Зона действий рядом с заголовком (Ж-1.3): очевидный следующий шаг состояния, правка и меню
  редких действий. Отдельной карточки «Действия» в разделах больше нет — она отбирала половину
  ширины у самой информации и стояла ниже того, ради чего человек на страницу пришёл.
*/
.section-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s2);
}

.section-actions form {
    margin: 0;
}

/*
  Одиночный редкий переход встаёт кнопкой в шапку, а не в «Ещё» с одним пунктом (пятый круг
  21.09). Его подсказка написана для раскрытого меню, в строке кнопок ей места нет: там она
  разорвала бы строку. Подсказки внутри диалога (`.confirm-body`) это правило не задевает.
*/
.section-actions > form > .hint {
    display: none;
}

/*
  QA5-C-01: without scripts the body of the window of «Закрыть» stays in its form, and in the header the pink
  warning spread across the row with the button under it. In the header «Закрыть» is one button: the server asks
  no checkbox for closing, and closing is undone by «Вернуть в работу». Only the close form: `form > .confirm-body`
  would also hide the required checkbox of blocking a service account, and blocking would become impossible
  without scripts. The footer of the review keeps its warning. With scripts the body is inside <dialog>.
*/
.section-actions > .close-form > .confirm-body {
    display: none;
}

.section-actions button,
.section-actions .button {
    margin-top: 0;
}

/*
  Тихий текст на месте главной кнопки шапки — «Мест нет» у заполненного занятия (Б4, Н8):
  не ссылка и не кнопка, слева от остальных кнопок, по центру их высоты (это даёт строка
  действий). Зазор до кнопок — 12: общий зазор группы 8 плюс свои 4.
*/
.section-note {
    margin-right: var(--s1);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--tx2);
}

/*
  Меню «Ещё» построено на <details>: с выключенными скриптами оно раскрывается само, а формы
  внутри уходят на сервер как обычно. Своей механики у него нет вовсе — в admin.js под меню
  не добавлено ни строчки, и это условие, а не совпадение.
*/
.more {
    position: relative;
}

/* Стрелка-маркер убирается: у <summary> здесь вид обычной второстепенной кнопки. */
.more > summary {
    margin: 0;
    list-style: none;
    user-select: none;
}

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

/*
  Панель раскрывается поверх страницы, а не раздвигает её: это меню, и содержимое под ним
  не должно прыгать. Правый край панели совпадает с правым краем кнопки — на любой ширине
  она остаётся внутри содержимого и не возвращает боковую прокрутку.
*/
.more-body {
    position: absolute;
    z-index: 25;
    top: calc(100% + var(--s2));
    right: 0;
    display: grid;
    gap: var(--s3);
    /* border-box: иначе к ширине добавляются внутреннее поле и рамка — панель выходит на 34 px шире */
    box-sizing: border-box;
    width: 300px;
    max-width: calc(100vw - var(--pad-x) * 2);
    padding: var(--card-pad);
    text-align: left;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-l);
    box-shadow: var(--shadow-float);
}

/* Единственная анимация админки — раскрытие меню (Ж-4.8), и она уважает системную настройку. */
.more[open] > .more-body {
    animation: more-open 120ms ease-out;
}

@keyframes more-open {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .more[open] > .more-body {
        animation: none;
    }
}

.more-body form {
    margin: 0;
}

/*
  Пункт меню занимает его ширину: вразнобой обрезанные по тексту кнопки читаются как мусор.
  Флекс, а не блок: display из базового правила перебивается, и с `block` подпись пункта
  встала бы к верхнему краю добавленной высоты вместо середины.
*/
.more-body button,
.more-body .button {
    display: flex;
    width: 100%;
    margin-top: 0;
    text-align: center;
}

.more-body .hint {
    margin-top: var(--s2);
}

/*
  Крошки, которым не хватает строки, переносятся целыми словами на следующую строку: без переноса
  флекс сжимал крошку, и на телефоне «Занятия» у формы правки рвалось посреди слова — «Заня / тия»
  (снимок задачи 23 релиза 0.5.0, крошка «Занятия / Синхронное плавание, 27.09.2026 / Изменить»).
*/
.breadcrumbs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px var(--s2);
    margin-bottom: var(--s1);
    font-size: var(--fs-small);
    color: var(--tx2);
}

.breadcrumbs a {
    text-decoration: none;
}

.breadcrumbs a:hover {
    text-decoration: underline;
}

/* Разделитель — тот же вторичный цвет, что и крошки: --tx-disabled на белом ниже AA. */
.breadcrumb-sep {
    color: var(--tx2);
}

/* --- фильтры и чипы --- */

/* Подпись стоит перед своим полем в одну строку, поэтому выравнивание по центру, а не по низу. */
.filters {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
    margin-bottom: var(--gap);
    padding: var(--card-pad);
    background: var(--card);
    box-shadow: var(--shadow-block);
    border-radius: var(--r-l);
}

.filters select {
    width: auto;
    max-width: 100%;
    margin-top: 0;
}

.filters label {
    margin: 0;
}

/* Кнопка-ссылка в полосе фильтров (выбор состояния) стоит в одну строку с остальными. */
.filters button,
.filters .button {
    margin-top: 0;
}

/*
  Кнопка применения — запасной вариант для страницы без JS; со скриптом её прячет
  `setUpAutoFilters`. Скрытие задаётся здесь явно: авторское `button { display: inline-flex }`
  выше перебивает браузерное `[hidden] { display: none }`, и одного атрибута мало.
*/
.filters-apply[hidden],
.list-size-apply[hidden] {
    display: none;
}

/*
  Поле ввода в полосе живёт по правилам полосы, а не формы: ширину ему задаёт колонка сетки,
  а общий потолок формы (--field-max) здесь только мешал бы колонке сжиматься.
*/
.filters input[type="text"] {
    width: 100%;
    max-width: 100%;
    margin-top: 0;
}

/* «Размер» каталога хвостов — одно поле: ступень и, у детского хвоста, рост стоят в одной ячейке. */
.filter-size-row {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-width: 0;
}

.filter-size-row select {
    min-width: 0;
}

/* Роста хватает трёх цифр; поле не тянется, чтобы список ступеней остался читаемым. */
.filter-size-row input[type="text"] {
    flex: 0 0 5.5rem;
}

/* Применённые фильтры видны отдельно от полей: каждый снимается своим крестиком. */
.chips {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-bottom: var(--gap);
    font-size: var(--fs-ui);
}

.chips-label {
    font-size: var(--fs-small);
    color: var(--tx2);
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) var(--s1) var(--s1) var(--s3);
    background: var(--soft);
    border: 1px solid var(--soft-line);
    border-radius: 999px;
}

/* Крестик чипа: ссылкой в применённых фильтрах, кнопкой в выпадающем списке с поиском. */
.chip a,
.chip button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    font-size: 16px;
    line-height: 1;
    text-decoration: none;
}

.chip button {
    margin: 0;
    padding: 0;
    color: var(--primary);
    background: none;
    border: none;
}

.chip a:hover,
.chip button:hover {
    color: var(--danger);
    background: var(--card);
}

.chips-reset {
    font-size: var(--fs-small);
}

/*
  Полоса фильтров строками (компоненты «Фильтры» 95:4949 и «Фильтры / телефон» 95:4977;
  `Дизайн-система.md` §7, пункт 18; задача 21 релиза 0.5.0). Строка — `.filters-row`: её поля
  делят ширину без пустого края, поиск (`.filters-wide`) — две доли, остальные по одной, два
  поля — пополам; вторая строка сегментных фильтров (`.filters-row-fit`) делит ширину по
  содержимому. Своей панели у полосы нет: она стоит внутри блока списка. Подписей над полями
  нет — название фильтра пишет само поле («Размер: любой»).

  Это `<form class="filters filters-rows">`: автоприменение и запоминание фокуса `admin.js`
  ищет по `form.filters`. Прежней сетки равных колонок `form.filters` больше нет: последним на
  строки перешёл журнал действий (задача 27 релиза 0.5.0).
*/
form.filters-rows {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s2);
    margin: 0 0 var(--s3);
    padding: 0;
    background: none;
    box-shadow: none;
    border-radius: 0;
}

/*
  Строка полосы — флекс с переносом: поля делят её поровну, но ни одно не уже своей подписи.
  Не помещается — поле уходит на следующую строку, и каждая строка тянется во всю ширину
  (`Дизайн-система.md` §7: «не влезает в строку — вторая строка, тоже во всю ширину»). Прежняя
  сетка равных колонок (задача 21 релиза 0.5.0) не знала подписей и обрезала их в узкой доле: на
  1100 с развёрнутым меню «Ближайшая нед…» у занятий, «Состояние: в обор…» у хвостов (правки
  итерации 3 того же релиза).

  Равные доли — от общей основы `--s7`: флекс прибавляет долю к основе, а основа шире собственных
  поля и рамки списка (26 px) и у всех полей одна — доли выходят равными, как колонки сетки (с
  основой 0 список получал долю поверх своих 26 px: 198,8 против 172,8). Поиск — две основы и
  зазор между ними, как две колонки сетки: на 1280 полоса та же, что у сетки, — 466 / 229 / 229.
*/
.filters-row {
    display: flex;
    flex-wrap: wrap;
    align-items: start;
    gap: var(--s2);
    min-width: 0;
}

.filters-row > * {
    flex: 1 1 var(--s7);
}

.filters-row > .filters-wide {
    flex: 2 1 calc(2 * var(--s7) + var(--s2));
}

/*
  Поле не уже своего содержимого (`min-width: auto` флекса) — сильнее `min-width: 0` обёрток
  `.period-field` и `.filter-size-row`: тот нужен внутри них, а не строке. `max-width` — одно
  поле не шире строки: длинная подпись не уведёт страницу вбок. Содержимое списка — выбранный
  вариант (`field-sizing: content` ниже), а не самый длинный: иначе длинное название направления
  переносило бы поле и там, где выбрано «Все направления».
*/
form.filters-rows .filters-row > * {
    min-width: auto;
    max-width: 100%;
}

/*
  Строка сегментных фильтров (`.filters-row-fit`) — по содержимому, остаток ширины поровну:
  равных долей у сегментов с названием нет (§4). Три фильтра каталога хвостов (задача 25) — около
  905 px, и в полосе 760 px на 1100 с развёрнутым меню третий уходит на новую строку.
*/
.filters-row-fit > * {
    flex: 1 1 auto;
}

.filters-rows select,
.filters-rows input[type="text"] {
    width: 100%;
    max-width: none;
    margin-top: 0;
}

/*
  Содержимое списка полосы — выбранный вариант, а не самый длинный: по нему строка решает, уйти
  ли полю на следующую (минимум выше). Ширину самого поля это не меняет — её задают строка и
  `width`. Браузер без `field-sizing` (Safari, Firefox) меряет список по самому длинному варианту:
  переносит поле раньше, но подпись не обрезает.
*/
.filters-rows select {
    field-sizing: content;
}

/*
  Текст полей полосы — кеглем интерфейса, как у компонентов «Фильтры» и «Поле / период» (14,5):
  поле пишет своё название вместе со значением («Состояние: в обороте»), и 16 px обрезали бы его
  в доле строки. На телефоне остаются 16 px — иначе Safari приближает страницу при фокусе в поле.
*/
@media (min-width: 761px) {
    .filters-rows select,
    .filters-rows input {
        font-size: var(--fs-ui);
    }
}

.filters-rows .field-control {
    max-width: none;
}

.filters-rows .field-control .field-icon {
    top: 0;
    width: 16px;
    height: 16px;
}

.filters-rows .segmented {
    margin: 0;
}

.filters-rows > .filters-apply {
    align-self: flex-start;
}

/* «Фильтры» раскрывают остальные поля — только на телефоне (медиазапрос телефона). */
.filters-toggle {
    display: none;
}

/* «Выгрузить хвосты» в полосе каталога — только на телефоне; на ноутбуке кнопка в шапке (задача 28). */
.filters-export {
    display: none;
}

/*
  Полоса над блоками списка — на фоне страницы, а не внутри блока (список занятий, кадры 348:4860
  и 348:4968): от блоков её отделяет зазор блоков.
*/
main > form.filters-rows {
    margin-bottom: var(--gap);
}

/*
  Поле периода (95:4948; §7): выпадающий список пишет выбранное, при «Свой период…» под ним два
  поля дат. Без скриптов даты видны всегда; скрытие со скриптом — атрибутом hidden, а сетка
  перебивает браузерное `[hidden] { display: none }`, поэтому оно повторено явно.
*/
.period-field {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    min-width: 0;
}

.period-field select {
    width: 100%;
    margin-top: 0;
}

.period-dates {
    display: grid;
    gap: var(--s2);
}

.period-dates[hidden] {
    display: none;
}

.period-date {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin: 0;
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--tx2);
}

.period-date > span {
    flex: 0 0 1.75em;
}

.period-date input {
    flex: 1 1 auto;
    min-width: 0;
    margin-top: 0;
}

.period-field .field-error {
    margin: 0;
}

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

/*
  Своей прокрутки у таблицы нет (замечание владельца 2026-08-08: полоса прокрутки уезжала
  в самый низ страницы, а вертикальные стрелки без ползунка выглядели поломкой). Страница
  листается целиком, а заголовок таблицы прилипает к странице под шапкой — это возможно
  ровно потому, что контейнер больше не создаёт собственный контекст прокрутки.
  Чтобы таблица при этом никуда не вылезала, ниже 1024 px она показывается карточками,
  а её текстовые колонки умеют переносить строки.
*/
.table-scroll {
    margin-bottom: var(--gap);
    background: var(--card);
    box-shadow: var(--shadow-block);
    border-radius: var(--r-l);
}

table {
    border-collapse: collapse;
    width: 100%;
    min-width: 560px;
    font-size: var(--fs-ui);
}

th,
td {
    text-align: left;
    white-space: nowrap;
    border-bottom: 1px solid var(--line-soft);
}

/* Заголовок прилипает под шапкой: z-index ниже её, поэтому уезжает под шапку, а не поверх. */
th {
    position: sticky;
    top: var(--head-h);
    z-index: 1;
    padding: var(--s3) var(--card-pad);
    font-size: var(--fs-small);
    font-weight: 500;
    letter-spacing: .02em;
    color: var(--tx2);
    background: var(--thead);
}

td {
    padding: var(--row-py) var(--card-pad);
}

/*
  Колонки делят ширину пропорционально, а не отдают весь остаток одной. До правки последняя
  колонка забирала всё: на 1920 под одну строку текста уходило 872 px, и таблица читалась как
  сжатая влево. Проценты стоят на текстовых колонках; короткие («Роль», «Действие», «Статус»)
  остаются по содержимому. Правило резиновое — работает на любой ширине без нового медиазапроса.
*/
/* Адрес локации — та же доля, что у самой широкой текстовой колонки: иначе он переносится
   на вторую строку, когда справа в таблице ещё половина пустого места. */
.col-changes,
.col-address {
    width: 29%;
}

.col-who {
    width: 20%;
}

.col-what {
    width: 18%;
}

.col-login {
    width: 16%;
}

.col-name {
    width: 26%;
}

.col-when {
    width: 13%;
}

/*
  Телефон — одной строкой, как его ввели (в 50 знаков он укладывается). Доли колонок базы клиентов —
  свои, по кадру (`.table-clients`, задача 26 релиза 0.5.0); эти остаются витрине компонентов.
*/
.col-phone,
.col-telegram {
    width: 16%;
}

/*
  Колонки-счётчики справочников: переносится подпись, а не число. «Проведено занятий» и «Впереди
  занятий» вдвоём длиннее всей остальной строки, и без переноса пятиколоночный список локаций
  уходит за край на 1100 px с развёрнутым меню — самой узкой ширине, где он ещё таблица.
*/
th.col-count {
    white-space: normal;
}

/*
  Колонки, куда попадает набранный человеком текст: название, адрес, логин, имя, объект журнала
  и перечень изменений. Поле принимает 200 знаков (адрес — 300), и ничто не мешает набрать их
  без единого пробела: при общем `white-space: nowrap` такая строка растягивала таблицу далеко
  за экран и возвращала странице боковую прокрутку, запрещённую каркасом (замер прогона QA
  14.08: scrollWidth 3658 при clientWidth 1920 на /locations).

  Разрыв слова здесь `anywhere`, а не `break-word`: при обычной раскладке таблицы (`auto`)
  колонка никогда не уже своего самого длинного неразрывного куска, и только `anywhere`
  учитывается при расчёте этого минимума — то есть только он и даёт таблице сузиться.
  `break-word` работает у списка событий лишь потому, что там раскладка фиксированная.
  Короткие колонки (дата, время, роль, счётчики, состояние) остаются неразрывными: их
  содержимое собирает система, а не человек. Телеграм — тоже набранный человеком текст,
  поэтому стоит в этом же списке; в базе клиентов он, ФИО и почта с задачи 26 релиза 0.5.0
  обрезаются «…» (`col-truncate`), а не переносятся.
*/
.col-name,
.col-address,
.col-login,
.col-who,
.col-what,
.col-changes,
.col-telegram {
    white-space: normal;
    overflow-wrap: anywhere;
}

/*
  То же правило на всё содержимое раздела, и по той же причине: имя в 200 знаков без пробелов
  стоит не только в ячейке таблицы, но и в заголовке карточки, в крошках и в значении поля —
  там оно расширяло страницу ровно так же (замер 14.08: /locations/{id} — scrollWidth 4261 при
  clientWidth 1920). Свойство наследуется, поэтому одного правила на `main` хватает; на обычный
  текст оно не влияет вовсе — разрыв случается только со словом, которое иначе не помещается.
*/
main {
    overflow-wrap: anywhere;
}

/*
  Выравнивание колонок — одно правило на все таблицы, включая журнал и учётные записи
  (`Дизайн-система.md` §7; Н2, отв. 8 второго захода — оно заменило «короткие отметки по
  центру» пятого круга). Слева — всё, что читают как текст: колонка, по которой строку узнают,
  свободный текст, даты и время, роль, отметки состояния, «Да / Нет». Справа, табличными
  цифрами, — суммы и счётчики: «Цена», «Занято» («N из M»), «Проведено занятий» (`col-num`).
  По центру в таблицах не стоит ничего: центрированная колонка не даёт глазу края, за который
  зацепиться, а числа в ней не встают разряд под разряд.

  Выравнивание несёт класс колонки, и он стоит и на шапке, и на ячейках: подпись обязана быть
  выровнена так же, как значения под ней, иначе на широкой колонке они разъезжаются. Это
  держит тест `TableColumnsTest`. Своей ширины класс не задаёт вовсе — доли колонок остаются
  на своих `col-*`, а `min-width` таблицам не задаётся нигде (см. абзац выше).

  На карточках узкого окна правило не действует: там ячейка — блок во всю ширину карточки
  с раскладкой «подпись слева / значение справа», и её правила (`.table-cards td`) и весом,
  и порядком в файле сильнее этого.
*/
.col-num {
    text-align: right;
}

/*
  Цифры набираются табличными (Ж-4.3 задачи 2026-08-13). У пропорциональных знаков «1» уже
  остальных, и соседние числа колонки пляшут по горизонтали на 1–2 px — колонка выглядит
  неаккуратной даже при идеальном выравнивании. Правило накрывает всё, где числа стоят друг
  под другом или меняются на месте: суммы и счётчики, дату и время в таблицах (`col-when`,
  `col-time`), в делах «Обзора» и в сетке недели, номера страниц, значения карточки объекта.
*/
.col-num,
.col-when,
.col-time,
.todo-when,
.week-hour,
.week-head,
.week-tile-time,
.page-link,
.fields dd {
    font-variant-numeric: tabular-nums;
}

/*
  Текстовым колонкам разрешены переносы: без них таблица упирается в сумму самых длинных строк
  и на узком окне вылезает из страницы. С переносами она ужимается сама, и боковая прокрутка
  не нужна вовсе. Короткие колонки («Роль», «Действие», «Статус», дата) остаются одной строкой.
*/
td.col-who,
td.col-what,
td.col-name {
    white-space: normal;
}

tbody tr:last-child td {
    border-bottom: none;
}

tbody tr:hover td {
    background: var(--row-hover);
}

/* Карточку открывает вся строка, а не только ссылка на логине. */
tr.row-link {
    cursor: pointer;
}

td a {
    font-weight: 500;
    text-decoration: none;
}

td a:hover {
    text-decoration: underline;
}

/*
  Внутри кликабельной строки ссылка ведёт ровно туда же, куда и вся строка, поэтому видом
  отдельной цели она не притворяется: цвет обычного текста и никакого подчёркивания при
  наведении (замечание владельца 2026-08-18 — «направление выделено и подчёркивается, а
  открывает то же самое, что и любое другое место строки»). Начертание остаётся: по этой
  колонке строку узнают, и на телефоне она же — заголовок карточки. Сама ссылка из разметки
  не уходит: на ней держатся клавиатура, «открыть в новой вкладке» и работа списка без JS.
*/
tr.row-link td a {
    color: inherit;
}

tr.row-link td a:hover {
    text-decoration: none;
}

/*
  Главная колонка — та, по которой строку узнают: имя, название, код, номер, логин
  (`Дизайн-система.md` §2 и §7, Б2). Одна на таблицу, 500, цветом текста строки и без
  подчёркивания даже под курсором: строка ведёт в карточку целиком, и ссылка в ячейке отдельной
  целью не притворяется. Из разметки ссылка не уходит — на ней держатся клавиатура, «открыть
  в новой вкладке» и переход без JS (`admin.js`). Класс ставят задачи экранов вместе с разметкой
  своих списков; до них главная колонка — это единственная ссылка строки, и так же выглядит
  (`td a` выше).
*/
.col-main {
    font-weight: 500;
}

td.col-main a,
td.col-main a:hover {
    color: inherit;
    text-decoration: none;
}

/*
  Исключение из правил выше: контакты ведут НЕ туда, куда строка, — телефон в вызов, Телеграм
  в мессенджер, почта в буфер обмена. Поэтому они и выглядят ссылкой: цвет `--primary`
  и подчёркивание всегда, в списке и в карточке (отв. 10, `Дизайн-система.md` §7). Начертание
  обычное: по контакту строку не узнают, и спорить с главной колонкой за внимание он не должен.
*/
.contact,
tr.row-link td a.contact {
    color: var(--primary);
    font-weight: 400;
    text-decoration: underline;
}

.contact:hover,
tr.row-link td a.contact:hover {
    color: var(--accent);
    text-decoration: underline;
}

/*
  Серая строка — то, что осталось в списке, но уже не в работе: использованный сертификат,
  нулевой депозит (`Дизайн-система.md` §7). Стоит внизу списка — порядок даёт слой сервисов,
  здесь только цвет: весь текст строки, и главной колонки тоже, — тихий `--tx2`. Так же
  и в строке-карточке телефона: цвет наследуется, своего у заголовка карточки нет.
*/
tr.row-muted {
    color: var(--tx2);
}

/*
  Вторая строка ячейки — тихим мелким под значением: «в записи на 14.10» под кодом, способ
  оплаты под суммой. Своя строка, а не продолжение первой: значение остаётся узнаваемым целиком.
*/
.cell-sub {
    display: block;
    font-size: var(--fs-small);
    font-weight: 400;
    color: var(--tx2);
}

/*
  Строка остаётся строкой (`Дизайн-система.md` §7, пункт 8): длинное имя, ник, название в
  списках этапа 5 обрезаются «…» в одну строку, целиком — в раскрытии или в карточке. Ширину
  колонке задаёт её доля (`col-*`), а не содержимое: `max-width: 0` не даёт тексту раздвинуть
  таблицу, поэтому боковой прокрутки обрезка не возвращает (дефект 5 прогона QA 14.08). Списки
  этапов 1–4 остаются на переносе `anywhere`, пока их задачи не решат иначе. Селектор с тегом —
  чтобы перебить перенос текстовых колонок (`td.col-name` выше) той же колонки.
*/
th.col-truncate,
td.col-truncate {
    max-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Номер не разрывается посередине: половина телефона на второй строке нечитаема. */
.contact-phone {
    white-space: nowrap;
}

/*
  Почта копируется по клику, поэтому она кнопка, а не текст: иначе до неё не добраться
  с клавиатуры. От кнопки остаётся только поведение — вид у неё ссылки.
*/
button.contact-copy {
    /*
      Общий вид кнопки снимается целиком, как у крестика чипа: почта обязана стоять в строке
      ровно там же, где стояла бы текстом. Каждое обнуление отвечает за свой пиксель — кнопка
      по умолчанию флекс высотой 44 px с верхним отступом и своим межстрочным интервалом
      (сокращение `font` ставит его в `normal`), и без обнуления почта висела на 5 px ниже
      соседей по строке (замер 04.09).
    */
    display: inline;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    line-height: inherit;
    text-align: left;
    cursor: pointer;
}

/* Подтверждение копирования: живёт полторы секунды, ставит его admin.js. */
.copied-note {
    margin-left: var(--s2);
    font-size: var(--fs-small);
    color: var(--tx2);
    white-space: nowrap;
}

/*
  Состояние объекта: точка и подпись. Компонент общий — этапы 2–5 берут его на статусы
  событий, хвостов и записей, а не заводят свои цвета.
*/
.badge {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    white-space: nowrap;
    font-weight: 500;
}

.badge::before {
    content: '';
    width: 7px;
    height: 7px;
    border-radius: 50%;
    flex-shrink: 0;
}

.badge-ok {
    color: var(--ok);
}

.badge-ok::before {
    background: var(--ok);
}

/* Точка — фирменный розовый из костюмов школы, подпись — тёмный оттенок ради контраста. */
.badge-danger {
    color: var(--danger);
}

.badge-danger::before {
    background: var(--pink);
}

.badge-warn {
    color: var(--warn);
}

.badge-warn::before {
    background: var(--warn);
}

.badge-neutral {
    color: var(--tx2);
}

.badge-neutral::before {
    background: var(--tx-disabled);
}

/*
  Число итог-фразы — «Занято 5 из 8», «Остаток 8 000 ₽» (`Дизайн-система.md` §2, пункт 13).
  Крупное число на экране одно на блок; слова вокруг него — обычный текст, их вид задаёт место.
  Цифры табличные: итог меняется на месте и не должен дёргаться по ширине. На телефоне — 18.
*/
.figure {
    font-size: 20px;
    font-weight: 600;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
}

/*
  Заголовок колонки с сортировкой (95:4919, sort; §7, А19): подпись и значок справа от неё —
  одна ссылка; нажатие меняет направление. Цвет — заголовка колонки, под курсором — ссылки.
*/
.sort-link {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    color: inherit;
    text-decoration: none;
}

.sort-link:hover {
    color: var(--primary);
}

.sort-link .field-icon {
    position: static;
    width: 16px;
    height: 16px;
    margin: 0;
    color: currentColor;
}

/*
  Список с раскрытием строки (Н26; участницы, разбор, журнал): `<details>` строкой `<table>` быть
  не может, поэтому это сетка `div` — шапка и строки делят одну сетку колонок `--row-columns`,
  которую задаёт класс списка (на телефоне — `--row-columns-phone`, колонки `col-wide` там не
  показываются), последняя узкая колонка — шеврон. Текст стоит на линии поля блока, как у
  таблиц (§1), а рамка раскрытой строки и подсветка под курсором выходят в поле блока на --s3 —
  внутренние поля строки ровно это возвращают. Раскрытая строка белая, цветной заливки нет.
*/
.row-list {
    margin: 0;
}

.row-list-head,
.row-summary {
    display: grid;
    grid-template-columns: var(--row-columns, minmax(0, 1fr)) 20px;
    align-items: center;
    column-gap: var(--s3);
}

/* Доли колонок таблиц (`col-name` — 26 %) здесь не действуют: ширину даёт сетка списка. */
.row-list-head > *,
.row-summary > * {
    width: auto;
}

.row-list-head {
    padding: var(--s3) 0;
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--tx2);
    border-bottom: 1px solid var(--line-soft);
}

.row-details {
    margin: 0 calc(-1 * var(--s3));
    padding: 0 calc(var(--s3) - 1px);
    border: 1px solid transparent;
    border-radius: var(--r-s);
}

.row-details:not([open]):hover {
    background: var(--row-hover);
}

.row-details[open] {
    margin-block: var(--s2);
    border-color: var(--line);
}

.row-summary {
    box-sizing: border-box;
    min-height: var(--field-h);
    padding: var(--s1) 0;
    font-size: var(--fs-ui);
    border-bottom: 1px solid var(--line-soft);
    cursor: pointer;
    list-style: none;
}

.row-summary::-webkit-details-marker {
    display: none;
}

.row-details[open] > .row-summary {
    border-bottom-color: transparent;
}

.row-summary > [role="cell"] {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.row-summary > .field-icon {
    position: static;
    width: 20px;
    height: 20px;
    margin: 0;
    justify-self: end;
    color: var(--tx2);
}

.row-details[open] > .row-summary > .field-icon {
    transform: rotate(90deg);
}

.row-body {
    padding: var(--s1) 0 var(--s3);
}

/* Строка действий раскрытой строки: кнопки s (§3). */
.row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-top: var(--s3);
}

.row-actions form {
    margin: 0;
}

.row-actions button,
.row-actions .button {
    margin-top: 0;
}

/*
  Замок в строке — «Скрыто в боте» у занятия (задача 13): знак набора полей без абсолютного
  позиционирования, обычным знаком рядом с текстом.
*/
.lock-mark {
    display: inline-flex;
    vertical-align: middle;
    color: var(--tx2);
}

.lock-mark .field-icon {
    position: static;
    width: 16px;
    height: 16px;
    margin: 0;
}

/*
  Название со знаком «Скрыто в боте» (задача 13; кадры 348:4860, 348:4968, 348:5110): замок стоит
  сразу за названием и не сжимается — длинное название уступает ему место (в списке и плитке
  недели обрезается «…»), короткое держит его рядом с собой. Обёртка стоит у названия всегда,
  с замком и без: строка одна и та же, меняется только знак. Разрыв — `--s1` шкалы (в кадрах 4–6).
*/
.name-line {
    display: flex;
    align-items: center;
    gap: var(--s1);
    min-width: 0;
}

.name-line > :first-child {
    min-width: 0;
}

.name-line > .lock-mark {
    flex: none;
}

/* Строка списка занятий: название обрезается само, а не ячейкой, — иначе «…» съело бы и замок. */
td.col-truncate .name-line > a {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- карточка объекта --- */

/*
  Колонка подписей — не уже `--field-label-w`, и одна на все группы карточки, пока подписи
  в неё помещаются: значения соседних групп стоят на одной вертикали. Подпись в одну строку
  (`Дизайн-система.md` §1, пункт 31; П1): если в группе есть подпись длиннее, её колонка
  растёт по самой длинной, а не переносит подпись на вторую строку. Зазоры — компонента «Поле
  карточки» (14:300): 12 от подписи до значения и между строками.
*/
.fields {
    display: grid;
    grid-template-columns: minmax(var(--field-label-w), max-content) minmax(0, 1fr);
    align-items: baseline;
    gap: var(--s3);
    margin: 0;
}

.fields dt {
    color: var(--tx2);
    font-size: var(--fs-small);
    font-weight: 500;
    white-space: nowrap;
}

.fields dd {
    margin: 0;
}

/*
  «Информация» карточки в две колонки (`Дизайн-система.md` §5: «на широком экране — в две колонки
  внутри блока»; кадры справочников 356:7703, 356:7820, 357:8903): слева данные объекта и его
  состояние, справа «Проведено занятий» и «Впереди занятий». Колонка подписей у каждой своя — по
  самой длинной подписи (пункт 31). На телефоне колонки встают одна под другой с тем же зазором,
  что между полями стопкой (`.fields dd + dt`), — читаются одним списком.
*/
.fields-columns {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
    gap: var(--s5) var(--s6);
}

/* Снимок в колонке предпросмотра формы: квадрат, как его сохраняет система (512 × 512). */
.instructor-photo-preview {
    display: block;
    aspect-ratio: 1;
    border-radius: var(--r-m);
    object-fit: cover;
    background: var(--soft);
}

/*
  Фото инструктора в карточке (задача 24 релиза 0.5.0) — квадратным блоком: снимок у инструктора
  квадратный (система хранит 512 × 512). Уже 1100 ряд встаёт столбиком, и квадрат держит сам блок —
  растянутый во всю ширину, он резал бы портрет в полосу; общий минимум высоты фото-блока здесь снят,
  иначе квадрата не вышло бы. От 1100 — см. ряд с фото в `.blocks-row`.
*/
.block-photo-square {
    min-height: 0;
    aspect-ratio: 1;
    max-width: 240px;
}

/* Одноразовый пароль и кнопки — одной строкой: блок занимает высоту одной строки, а не трёх. */
.one-time-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s3);
}

.one-time-row form {
    margin: 0;
}

.one-time-row button,
.one-time-row .button {
    margin-top: 0;
}

.one-time-row .button-link {
    margin-left: 0;
    padding: 0;
}

.one-time-password {
    flex: 1 1 220px;
    margin: 0;
    padding: var(--s2) var(--s3);
    font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
    font-size: 18px;
    letter-spacing: 2px;
    word-break: break-all;
    color: var(--primary);
    background: var(--soft);
    border: 1px solid var(--soft-line);
    border-radius: var(--r-s);
}

.one-time .hint {
    margin-top: var(--s3);
}

/* --- обзор --- */

/*
  «Обзор» — сводка недели (задача 17 релиза 0.5.0; кадры 347:4860 и 347:5053, «дел нет» 87:5448). Белые
  блоки задачи 21; своих цветов и кеглей нет — только токены. От 1100 px — сетка с именованными местами:
  пары блоков одной высоты (ряд тянет их до самого высокого) и неделя во всю ширину; боковая колонка — 340,
  как в кадре (у `.blocks` — 360: там справка, здесь пара блоков сводки). Уже — столбик в порядке разметки.
  Полоски и столбики — один элемент с градиентом по доле `--share`, без скриптов и библиотек.
*/
.overview-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap);
}

.overview-grid > .block {
    min-width: 0;
    margin: 0;
}

@media (min-width: 1100px) {
    .overview-grid {
        grid-template-columns: minmax(0, 1fr) 340px;
        grid-template-areas: "todo money" "today clients" "week week";
    }

    /* Дел нет — «Сегодня» встаёт в пару к деньгам, на его место — «Завтра» (кадр 87:5448). */
    .overview-grid.overview-calm {
        grid-template-areas: "today money" "tomorrow clients" "week week";
    }

    .overview-todo {
        grid-area: todo;
    }

    .overview-today {
        grid-area: today;
    }

    .overview-tomorrow {
        grid-area: tomorrow;
    }

    .overview-money {
        grid-area: money;
    }

    .overview-clients {
        grid-area: clients;
    }

    .overview-week {
        grid-area: week;
    }
}

/* Шапка блока сводки: заголовок, тихая подпись по базовой линии и действие стрелкой у правого края. */
.overview-header {
    align-items: baseline;
    column-gap: 10px;
}

.overview-meta {
    font-size: var(--fs-small);
    color: var(--tx2);
    font-variant-numeric: tabular-nums;
}

.overview-header > .overview-action {
    margin-left: auto;
}

/* Действие блока — ссылка со стрелкой без подчёркивания (кадр): «Расписание дня →», «Все занятия недели →». */
.overview-action {
    font-size: var(--fs-ui);
    font-weight: 500;
    text-decoration: none;
}

.overview-footer {
    margin: 0;
    padding-top: var(--s2);
}

.overview .badge {
    font-size: var(--fs-ui);
}

/* Ссылка-дело янтарным: «Отмены — решить про деньги: 1 →», «Не оплатили: 5 записей →». */
.overview-warn-link {
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--warn);
}

/* «Ждут решения» — строки занятий карточками в рамке, «Разобрать →» справа (кадр 347:4860). */
.overview-todo {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
}

/* Шапка без действия — высотой заголовка: строка отмен в 10 px под ним (кадры 347:5198 / 347:5203). */
.overview-todo > .block-header {
    min-height: 0;
    margin: 0;
}

.todo-note {
    margin: 0;
    font-size: var(--fs-ui);
    color: var(--tx2);
    text-wrap: pretty;
}

.todo-list {
    display: grid;
    align-self: stretch;
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.todo-item {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "when name action" "where where action";
    align-items: baseline;
    gap: 2px 10px;
    padding: 10px var(--s4);
    font-size: var(--fs-ui);
    color: var(--tx);
    text-decoration: none;
    border: 1px solid var(--soft-line);
    border-radius: var(--r-s);
}

.todo-item:hover {
    color: var(--tx);
    background: var(--row-hover);
    border-color: var(--accent);
}

.todo-when {
    grid-area: when;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Название и локация — свои места сетки строки; на телефоне — одной строкой через «·» (кадр 347:5053). */
.todo-what {
    display: contents;
}

.todo-name {
    grid-area: name;
    overflow: hidden;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.todo-where {
    grid-area: where;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.todo-action {
    grid-area: action;
    align-self: center;
    font-weight: 500;
    color: var(--primary);
    white-space: nowrap;
}

.todo-item:hover .todo-action {
    color: var(--accent);
}

.todo-all {
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--tx);
}

/*
  Строки «Сегодня» и «Завтра»: время, название с локацией, занятые места полоской с подписью и отметка
  справа. Колонки — по кадру (104 · название · 120 · 128); где блок уже (1100 px с развёрнутым меню),
  полоска и отметка переносятся строкой ниже. Строка — ссылка на карточку занятия целиком.
*/
.overview-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.overview-lesson {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s4);
    padding: 18px 0;
    color: var(--tx);
    text-decoration: none;
}

/* Шаг строк — 79, как в кадре 347:4860; первая строка ближе к шапке. */
.overview-list > li:first-child > .overview-lesson {
    padding-top: 6px;
}

li + li > .overview-lesson {
    border-top: 1px solid var(--line-soft);
}

.overview-lesson:hover,
.overview-free:hover {
    color: var(--tx);
    background: var(--row-hover);
    box-shadow: calc(-1 * var(--card-pad)) 0 0 var(--row-hover), var(--card-pad) 0 0 var(--row-hover);
}

.overview-lesson-time {
    flex: 0 0 auto;
    min-width: 104px;
    font-size: var(--fs-ui);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

.overview-lesson-main {
    display: grid;
    flex: 1 1 120px;
    gap: 2px;
    min-width: 0;
}

.overview-lesson-name,
.overview-lesson-where {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.overview-lesson-name {
    font-size: var(--fs-ui);
    font-weight: 500;
}

.overview-lesson-where {
    font-size: var(--fs-small);
    color: var(--tx2);
}

.overview-load {
    display: grid;
    flex: 0 0 120px;
    gap: 6px;
}

.overview-load-text {
    font-size: var(--fs-small);
    color: var(--tx2);
    font-variant-numeric: tabular-nums;
}

.overview-lesson-mark {
    display: flex;
    flex: 0 0 128px;
    justify-content: flex-end;
    margin-left: auto;
}

/* Полоска доли занятых мест: заливка --primary на подложке --soft, длина — `--share`. */
.overview-bar {
    display: block;
    height: 6px;
    background: linear-gradient(to right, var(--primary) var(--share, 0%), var(--soft) var(--share, 0%));
    border-radius: 3px;
}

/*
  «Деньги недели»: крупное «получено», полоска оплаченных и ждущих оплаты записей (по числу, без рублей —
  В11), подпись полоски и ссылка «Не оплатили» внизу блока — ряд тянет блок до высоты соседа.
*/
.overview-money {
    display: flex;
    flex-direction: column;
}

.overview-received {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: var(--s2) 0 var(--s5);
}

.overview-caption {
    font-size: var(--fs-small);
    color: var(--tx2);
}

.overview-split {
    display: flex;
    gap: 2px;
    height: 10px;
    margin-bottom: var(--s5);
}

.overview-split > span {
    flex-basis: 0;
    border-radius: 5px;
}

.overview-split-paid {
    background: var(--ok);
}

.overview-split-unpaid {
    background: var(--warn);
}

.overview-legend {
    display: grid;
    gap: 6px;
    margin: 0 0 var(--s5);
}

.overview-legend > div {
    display: flex;
    justify-content: space-between;
    gap: var(--s3);
}

.overview-legend dt {
    display: flex;
    align-items: center;
    gap: var(--s2);
    font-size: var(--fs-ui);
    color: var(--tx2);
}

.overview-legend dt::before {
    content: '';
    width: 8px;
    height: 8px;
    border-radius: 50%;
}

.overview-dot-ok::before {
    background: var(--ok);
}

.overview-dot-warn::before {
    background: var(--warn);
}

.overview-legend dd {
    margin: 0;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

.overview-unpaid {
    align-self: flex-start;
    margin-top: auto;
}

/*
  «Новые клиентки»: linked name, when/source below, «Не записана» on the right. Both lines keep an ellipsis like
  «Сегодня» and «Где есть места» (§7); otherwise a long name wraps to three lines.
*/
.overview-client {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: 9px 0;
    border-top: 1px solid var(--line-soft);
}

.overview-client-main {
    display: grid;
    gap: 2px;
    min-width: 0;
}

.overview-client-name,
.overview-client-line {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.overview-client-name {
    font-size: var(--fs-ui);
    font-weight: 500;
}

.overview-client-line {
    font-size: var(--fs-small);
    color: var(--tx2);
}

/*
  «Неделя вперёд»: семь столбиков — доля занятых мест снизу вверх, полный день темнее (--primary), день без
  занятий — пунктирная рамка. Сегодня — заливкой --soft. Столбик — ссылка на «Занятия · день».
*/
.overview-days {
    display: flex;
    gap: var(--gap);
    margin: 0 0 var(--s4);
    padding: 0;
    list-style: none;
}

.overview-days > li {
    flex: 1 1 0;
    min-width: 0;
}

.overview-day {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 10px 8px;
    color: var(--tx);
    text-align: center;
    text-decoration: none;
    border-radius: var(--r-s);
}

.overview-day:hover {
    color: var(--tx);
    background: var(--row-hover);
}

.overview-day-today,
.overview-day-today:hover {
    background: var(--soft);
}

.overview-day-label {
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--tx2);
    white-space: nowrap;
}

.overview-day-today .overview-day-label {
    font-weight: 600;
    color: var(--primary);
    letter-spacing: .5px;
    text-transform: uppercase;
}

.overview-column {
    --fill: var(--accent);
    --track: var(--soft);
    display: block;
    box-sizing: border-box;
    width: 36px;
    height: 88px;
    background: linear-gradient(to top, var(--fill) var(--share, 0%), var(--track) var(--share, 0%));
    border-radius: 6px;
}

.overview-day-today .overview-column {
    --track: var(--card);
}

.overview-column-full {
    --fill: var(--primary);
}

.overview-column-empty {
    background: none;
    border: 1px dashed var(--line);
}

.overview-day-value {
    font-size: var(--fs-ui);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.overview-day-none {
    color: var(--tx-disabled);
}

.overview-day-count {
    font-size: var(--fs-small);
    color: var(--tx2);
}

/* «Где есть места» — ближайшие занятия со свободными местами: когда · название · полоска · «свободно 4». */
.overview-free-header {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 10px;
    margin-bottom: var(--s2);
}

.overview-subtitle {
    margin: 0;
    font-size: var(--fs);
    font-weight: 500;
}

.overview-free {
    display: grid;
    grid-template-columns: 150px minmax(0, 1fr) 160px 96px;
    align-items: center;
    gap: var(--s4);
    padding: 18px 0;
    font-size: var(--fs-ui);
    color: var(--tx);
    text-decoration: none;
    border-top: 1px solid var(--line-soft);
}

.overview-free-when {
    color: var(--tx2);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.overview-free-name {
    overflow: hidden;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.overview-free-count {
    font-weight: 500;
    text-align: right;
    white-space: nowrap;
}

/* Подписи только для телефона (кадр 347:5053: «18/24», «своб. 4»); широкие прячет медиазапрос телефона. */
.overview .overview-phone {
    display: none;
}

/* --- журнал действий --- */

/*
  Журнал действий (задача 27 релиза 0.5.0, кадры 362:11737 и 362:11794) — список с раскрытием строки
  (`.row-list`, задача 21). Доли колонок — по кадру (940 px: 150 · 150 · 120 · 210 · 100 · 210); дата и
  время не уже самого значения — «05.10.2026 20:41» кеглем ячейки занимает около 116 px и не
  обрезается ни на 1100 px с развёрнутым меню, ни на 1025, где список ещё строкой. Остальное длинное —
  одной строкой с «…» (правило ячеек `.row-summary`), целиком — в раскрытой строке. «Кто» и «Объект» —
  ссылками обычного вида: журнал и контакты — единственные таблицы со ссылками (§7, А41).
*/
.audit-list {
    --row-columns: minmax(128px, 150fr) minmax(0, 150fr) minmax(0, 120fr) minmax(0, 210fr) minmax(0, 100fr) minmax(0, 210fr);
}

/* Вторая и третья строки карточки узкого окна — только там: на ноутбуке это колонки той же строки. */
.audit-line {
    display: none;
}

/*
  Раскрытая строка (кадр 362:11737): «Объект» и «Что изменилось» целиком, в несколько строк, — две
  колонки поровну, подпись тихим над значением. Уже 1024 px — одной колонкой.
*/
.audit-details {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s3) var(--s6);
    margin: 0;
}

.audit-details dt {
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--tx2);
}

.audit-details dd {
    margin: var(--s1) 0 0;
    font-size: var(--fs);
    overflow-wrap: anywhere;
}

/*
  Список занятий (задача 23 релиза 0.5.0, кадр 348:4860): семь колонок в блоке — время,
  направление, инструктор, локация, «Занято», цена и состояние. Текст ячеек стоит на линии поля
  блока (`.block th`, `.block td` выше): прежнее своё поле ячеек в 8 px ставило время в 8 px от
  края блока, а заголовок дня — в 20 (находка задачи 20), теперь у них одна линия.

  Ширина таблицы не зависит от содержимого — `table-layout: fixed`. При обычном алгоритме
  (`auto`) колонка никогда не уже своего самого длинного неразрывного куска, и на «ожидает
  закрытия» сумма минимумов перерастала контейнер — таблица вылезала за край и возвращала боковую
  прокрутку (замер 12.08). При фиксированной раскладке ширина таблицы тождественно равна ширине
  блока, а доли ниже решают только, где случится перенос. Вместе с этим обязательна пара
  «переносы + разрыв длинного слова»: неразрывное содержимое таблицу больше не расширяет.
*/
.table-events table {
    table-layout: fixed;
}

.table-events th,
.table-events td {
    white-space: normal;
    overflow-wrap: break-word;
}

/*
  Направление, инструктор и локация — одной строкой с «…» (`col-truncate`, кадр 348:4860), а не
  переносом: правило выше весит столько же и стоит позже, поэтому обрезка повторена здесь.
*/
.table-events th.col-truncate,
.table-events td.col-truncate {
    white-space: nowrap;
}

/*
  Доли колонок — по кадру (940 px: 140 · 186 · 130 · 130 · 80 · 90 · 184), «Занятым» чуть больше:
  «12 из 12» не переносится (числа держит неразрывный пробел) и обязано встать целиком на 1100 px
  с развёрнутым меню — самой узкой ширине, где список ещё таблица. Направление, инструктор
  и локация обрезаются «…» (`col-truncate`), целиком — в карточке занятия. Доли живут под
  `.table-events` — общие `.col-*` справочников и журнала они не трогают.
*/
.table-events .col-time {
    width: 15%;
}

.table-events .col-name {
    width: 19.5%;
}

.table-events .col-who {
    width: 14%;
}

.table-events .col-what {
    width: 14%;
}

.table-events .col-seats {
    width: 10%;
}

.table-events .col-price {
    width: 9.5%;
}

.table-events .col-state {
    width: 18%;
}

/*
  Тихая строка подробностей под названием — «инструктор · локация · занято · цена» — только
  у строки-карточки телефона (кадр 348:4968): на ноутбуке это колонки той же строки.
*/
.event-details {
    display: none;
}

/*
  Подпись состояния переносится — единственное значение этой таблицы, которому это разрешено.
  Остальные значения («18:15 — 19:45», «1 500 ₽», дата) от переноса защищены не запретом,
  а долей: на 1100 px и шире они помещаются в свою колонку целиком.

  Одного `white-space: normal` для переноса не хватает. Badge — inline-flex, и подпись внутри
  него становится анонимным flex-элементом: он переносится по словам, но уже своей ширины —
  самого длинного слова плюс точка и отступ — не становится, а вся конструкция остаётся
  атомарным inline-боксом. Замер 12.08 на 1100 px с развёрнутым меню: «в расписании» занимало
  99,6 px при колонке 91,9 px и вылезало на 15,7 px правее таблицы. Поэтому здесь badge —
  обычный поток: точка становится inline-блоком, подпись — обычным текстом ячейки, и к ней
  применяется та же пара «перенос + разрыв длинного слова», что и к остальным ячейкам таблицы.
  Строки после первой при этом берут всю ширину колонки, а не остаток рядом с точкой.

  Точка остаётся на первой строке рядом с началом подписи: `vertical-align: middle` повторяет
  её положение из flex с точностью до десятой пикселя. Отступ 7 px — тот же, что `gap`
  у компонента; у inline-бокса `gap` не действует.

  Слово шире колонки таблица всё же разрывает — это цена долей, а не переноса, поэтому
  колонка состояния и получила самую большую долю: «расписании» просит 85,2 px, и под
  подпись остаётся 97,1 px на 1100 px и 86,3 px на 1025 px.
*/
.table-events .badge {
    display: inline;
    white-space: normal;
}

.table-events .badge::before {
    display: inline-block;
    margin-right: 7px;
    vertical-align: middle;
}

/*
  Полоса дня (Н4, кадр 348:4860): «Понедельник, 12 октября · сегодня» — полосой --soft над строками
  своего дня внутри той же таблицы, иначе колонки соседних дней не совпали бы по ширине. Текст —
  на линии поля блока, как у ячеек (§1): фон выходит в поле блока тенью по бокам и не сдвигает
  текст — так же, как подсветка строки и рамка раскрытой строки (задача 21). От края блока полоса
  стоит на --s2. Прилипание снято: липнет только заголовок таблицы, иначе две полосы наезжают друг
  на друга. Вес 500: 600 бывает ровно у трёх ролей (§2).
*/
th.day-cell {
    position: static;
    padding: var(--s2) 0;
    font-size: var(--fs-ui);
    font-weight: 500;
    letter-spacing: normal;
    color: var(--tx);
    background: var(--soft);
    border-bottom: none;
    box-shadow: calc(var(--s2) - var(--card-pad)) 0 0 var(--soft), calc(var(--card-pad) - var(--s2)) 0 0 var(--soft);
}

/* --- списки справочников --- */

/*
  Списки направлений, инструкторов и локаций (задача 24 релиза 0.5.0, кадры 356:7639, 356:7770,
  357:8849): фильтр состояния и таблица — одним блоком (`.block-list`), от фильтра до таблицы — --s3.
  Раскладка таблицы фиксированная, доли — по кадру (940 px): «Нужен хвост» и «Состояние» — 140,
  счётчики — 160 (у локаций 150), адрес — 240, название берёт остаток. Доля считается вместе с полем
  ячейки (`border-box`), иначе поле между колонками прибавлялось бы к каждой, и название теряло бы
  по 12 px на колонку. Название и адрес — одной строкой с «…» (`col-truncate`, `Дизайн-система.md`
  §7, пункт 8), целиком — в карточке: при фиксированной раскладке длинное слово таблицу не
  раздвигает. Общие доли `.col-name` (26 %) и `.col-address` (29 %) здесь перебиты — свои у
  занятий, клиенток, учётных записей и журнала.
*/
.block-list > .segmented-links {
    margin-bottom: var(--s3);
}

.table-catalogue table {
    table-layout: fixed;
}

.table-catalogue th,
.table-catalogue td {
    box-sizing: border-box;
}

.table-catalogue .col-name {
    width: auto;
}

.table-catalogue .col-tail,
.table-catalogue .col-state {
    width: 14.9%;
}

.table-catalogue .col-count {
    width: 17%;
}

.table-catalogue .col-address {
    width: 25.5%;
}

.table-locations .col-count {
    width: 16%;
}

/* Строка подробностей под названием — только у строки-карточки телефона: на ноутбуке это колонки. */
.catalogue-details {
    display: none;
}

/*
  Скрытая запись справочника (кадры 356:7639, 356:7770, 357:8849): значения строки тихим --tx2,
  а название остаётся цветом текста — по нему строку узнают. Вся строка серой — у использованного
  сертификата и нулевого депозита (`tr.row-muted`), у скрытого справочника в кадрах — нет.
*/
tr.row-hidden td:not(.col-main) {
    color: var(--tx2);
}

/*
  База клиентов (задача 26 релиза 0.5.0, кадры 353:6948 и 353:7005): полоса поиска и таблица одним
  блоком списка, доли колонок — по кадру (940 px): ФИО 260, телефон 180, Телеграм 200, почта 300;
  доля считается вместе с полем ячейки (`border-box`). ФИО, Телеграм и почта — одной строкой с «…»
  (`col-truncate`), целиком — в карточке. Телефон виден целиком всегда (`Дизайн-система.md` §7),
  поэтому раскладка таблицы обычная, а не фиксированная: колонка телефона не бывает уже номера,
  сжимаются обрезаемые соседи.
*/
.table-clients th,
.table-clients td {
    box-sizing: border-box;
}

.table-clients .col-name {
    width: 27.66%;
}

.table-clients .col-phone {
    width: 19.15%;
}

.table-clients .col-telegram {
    width: 21.28%;
}

.table-clients .col-email {
    width: 31.91%;
}

/*
  Почта в базе — текстом, как в кадре: ссылкой выглядят только телефон и Телеграм (§7, отв. 10), а
  почта по нажатию копируется в буфер (`data-copy`, `admin.js`). Кнопка неделима, и обрезка ячейки
  спрятала бы её целиком за «…» — поэтому длинный адрес обрывается «…» внутри самой кнопки.
*/
.table-clients button.contact-copy {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: top;
    color: inherit;
}

/* Вторая строка карточки телефона — только там: на ноутбуке телефон, Телеграм и почта стоят колонками. */
.client-contacts {
    display: none;
}

/* --- сертификаты русалок --- */

/*
  Реестр «Сертификаты» (задача 11 релиза 0.5.0, кадры 354:7488 и 354:7534): полоса поиска и таблица
  одним блоком списка, доли колонок — по кадру (940 px): номер 148, курс 208, клиентка 220, дата 138,
  занятие 226; доля считается вместе с полем ячейки (`border-box`). Курс, клиентка и занятие — одной
  строкой с «…» (`col-truncate`), целиком — в карточке. Номер и дата видны целиком всегда, поэтому
  раскладка таблицы обычная, как у базы клиентов: длинный номер своего курса (начало номера — до 20
  знаков) раздвигает свою колонку, место уступают обрезаемые соседи. Аннулированный — `tr.row-muted`.
*/
.table-certificates th,
.table-certificates td {
    box-sizing: border-box;
}

.table-certificates .col-number {
    width: 15.74%;
}

.table-certificates .col-course {
    width: 22.13%;
}

.table-certificates .col-name {
    width: 23.4%;
}

.table-certificates .col-when {
    width: 14.68%;
}

.table-certificates .col-lesson {
    width: 24.04%;
}

/* «· ФИО» за номером и строка «курс · дата» — только у строки-карточки телефона: на ноутбуке это колонки. */
.cert-phone-name,
.cert-details {
    display: none;
}

/*
  «Каталог сертификатов» (кадр 359:10619) — список справочника (`table-catalogue`, задача 24) со своими
  долями по кадру (940 px): название 460 (остаток), начало номера 180, выдано 140, состояние 160. Начало
  номера бывает до 20 знаков — обрезается «…», целиком — в карточке курса.
*/
.table-courses .col-prefix {
    width: 19.15%;
}

.table-courses .col-count {
    width: 14.89%;
}

.table-courses .col-state {
    width: 17.02%;
}

/* --- подарочные сертификаты --- */

/*
  Список подарочных (задача 12 релиза 0.5.0, кадры 354:7606 и 354:7663): полоса «Код или имя · Все · Активные ·
  Использованные» и таблица одним блоком, доли колонок — по кадру (940 px): код 96, кому 162, на что 158, цена
  96, куплен 102, действует до 112, состояние 214; доля считается вместе с полем ячейки (`border-box`). Кому и на
  что — одной строкой с «…», целиком — в карточке. Использованный — `tr.row-muted`, срок вышел — дата янтарём.
  Под отметкой состояния — тихая строка «в записи на 14.10».
*/
.table-gifts th,
.table-gifts td,
.table-gift-uses th,
.table-gift-uses td {
    box-sizing: border-box;
}

.table-gifts .col-code {
    width: 10.21%;
}

.table-gifts .col-name {
    width: 17.23%;
}

.table-gifts .col-purpose {
    width: 16.81%;
}

.table-gifts .col-price {
    width: 10.21%;
}

.table-gifts .col-bought {
    width: 10.85%;
}

.table-gifts .col-until {
    width: 11.91%;
}

.table-gifts .col-state {
    width: 22.77%;
}

.gift-expired {
    color: var(--warn);
}

.gift-next {
    display: block;
    margin-top: 2px;
}

/* «Кому», отметка и строка «на что · до … · в записи на …» внутри кода — только у строки-карточки телефона. */
.gift-phone-name,
.gift-phone-state,
.gift-details,
.gift-use-details {
    display: none;
}

/*
  «Где использован» (кадр 354:7696): дата 112, занятие 334, клиентка 232, сумма 102 справа, «что с занятием» 160
  — доли по кадру (940 px).
*/
.table-gift-uses .col-when {
    width: 11.91%;
}

.table-gift-uses .col-lesson {
    width: 35.53%;
}

.table-gift-uses .col-name {
    width: 24.68%;
}

.table-gift-uses .col-sum {
    width: 10.85%;
}

.table-gift-uses .col-fate {
    width: 17.02%;
}

/*
  Сводка карточки: «Клиентка не привязана» тихим текстом и белая s «Привязать к клиентке» рядом, в строке фактов
  (кадр 354:7696: у привязанного на этом месте — клиентка ссылкой).
*/
.gift-unlinked {
    margin-right: var(--s2);
}

.summary-facts .gift-link {
    margin: 0 var(--s2) 0 0;
    vertical-align: middle;
}

/*
  Форма подарочного (кадр 354:7753) — две колонки по полю, как `.form-grid`, но поля — парами по строкам: у каждой
  ячейки своя колонка (`.form-left`, `.form-right`), поэтому пара стоит на одной линии, даже когда соседняя
  ячейка спрятана («На сумму» прячет поля занятия, «На занятие» — «Осталось»). Во всю ширину — «Пожелание» и
  «Комментарий» (`.form-wide`). На телефоне — одной колонкой в порядке разметки.
*/
.gift-form {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, var(--field-max)));
    column-gap: var(--s6);
    align-items: start;
}

.gift-form > .form-left {
    grid-column: 1;
}

.gift-form > .form-right {
    grid-column: 2;
}

.gift-form > .form-cell {
    min-width: 0;
    margin: 0;
}

.gift-form > .form-wide textarea {
    max-width: none;
}

.gift-form .segmented {
    margin-bottom: 0;
}

/* Chrome ignores a rendered legend's top margin; the fieldset carries the label offset. */
.gift-form > fieldset.gift-state,
.gift-form > fieldset.form-cell:not(:first-child) {
    margin-top: var(--s4);
}

/* Without the first-code cell, purpose and fin share the first desktop row under the block title. */
.gift-form > fieldset.form-cell:first-child + fieldset.form-right {
    margin-top: 0;
}

.gift-form:has(input[name="kind"][value="AMOUNT"]:checked) .gift-lesson,
.gift-form:has(input[name="kind"][value="LESSON"]:checked) .gift-amount-only {
    display: none;
}

/* «Куплен» и «Действует до» — пара полей в одной ячейке, подсказка про год — под обоими (кадр 354:7753). */
.gift-dates {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--s4);
}

.gift-dates input[type="date"] {
    max-width: none;
}

.gift-dates-hint {
    grid-column: 1 / -1;
}

.gift-fixed {
    display: grid;
    grid-template-columns: minmax(var(--field-label-w), max-content) minmax(0, 1fr);
    gap: var(--s2) var(--s4);
    margin-top: var(--s4);
}

.gift-fixed dd {
    margin: 0;
}

/* --- депозиты --- */

/*
  Список депозитов (задача 18 релиза 0.5.0, кадры 355:7574 и 355:7604): полоса «Поиск по клиентке · Все · С
  остатком · Без остатка» и таблица одним блоком. Отбор — по своему содержимому, сегменты одной ширины по самому
  широкому с полем --s4, как у сегментного фильтра полосы (§4), поиск забирает остаток строки: в кадре 569 и 363.
  Доли колонок — по кадру (940 px) вместе с полем ячейки: клиентка 432, остаток 172, последняя операция 336.
  Нулевой депозит — `tr.row-muted`, внизу.
*/
.filters-row > .deposit-balances {
    flex: 0 0 auto;
}

.deposit-balances .segment > span {
    padding: var(--s1) var(--s4);
    white-space: nowrap;
}

.table-deposits th,
.table-deposits td,
.table-deposit-ops th,
.table-deposit-ops td {
    box-sizing: border-box;
}

.table-deposits .col-name {
    width: 45.96%;
}

.table-deposits .col-balance {
    width: 18.30%;
}

.table-deposits .col-when {
    width: 35.74%;
}

/* Остаток рядом с именем и «последняя операция …» — только у строки-карточки узкого окна. */
.deposit-phone-balance,
.deposit-details {
    display: none;
}

/* Пустой раздел (§5): тихая строка по центру и «Добавить депозит» тихой ссылкой под ней. */
.deposit-empty {
    display: grid;
    justify-items: center;
    gap: var(--s2);
    padding: var(--s6) 0;
    text-align: center;
}

/*
  Карточка депозита (кадр 355:7633): в сводке — клиентка ссылкой и комментарий через « · »; «История операций» —
  дата 104, сумма 112 справа, что это 182, занятие и запись 200, способ 96, комментарий 162, кто внёс 84 — доли по
  кадру (940 px), раскладка фиксированная: длинный комментарий не раздвигает свою колонку за счёт соседних.
  Пришло — сумма зелёным; у поправки под видом тихо «было … → стало …» (длинные суммы — на вторую строку по
  пробелу у стрелки). Комментарий переносится целиком (§7: в «Истории» перенос разрешён) — общее
  `white-space: nowrap` ячеек здесь снято, иначе он уходил за край и возвращал странице боковую прокрутку. Занятие — текстом, как в кадре: строка ведёт в его карточку, а ссылка внутри
  остаётся для клавиатуры и страницы без скриптов. Способ и строка «дата · кто» под занятием — только у
  строки-карточки.
*/
.table-deposit-ops table {
    table-layout: fixed;
}

.table-deposit-ops .deposit-op-lesson {
    font-weight: inherit;
}

.table-deposit-ops .col-when {
    width: 11.06%;
}

.table-deposit-ops .col-sum {
    width: 11.91%;
}

.table-deposit-ops .col-kind {
    width: 19.36%;
}

.table-deposit-ops .col-lesson {
    width: 21.28%;
}

.table-deposit-ops .col-method {
    width: 10.21%;
}

.table-deposit-ops .col-comment {
    width: 17.23%;
    white-space: normal;
    overflow-wrap: anywhere;
}

.table-deposit-ops .col-by {
    width: 8.94%;
}

.deposit-amount {
    white-space: nowrap;
}

.deposit-amount-in {
    color: var(--ok);
}

.deposit-correction {
    margin-top: 2px;
    white-space: normal;
}

.deposit-op-method,
.deposit-op-meta {
    display: none;
}

/*
  Формы депозита (кадр 355:7690 и собранные по нему): под шапкой блока — чей депозит и сколько на нём сейчас.
  У выбранной клиентки с депозитом «Комментарий депозита» не нужен — его правят в «Изменить»: поле прячет
  отметка `data-has-deposit` выбранного кружка, без скриптов.
*/
.deposit-form:has(.client-option input[data-has-deposit]:checked) .deposit-own-comment {
    display: none;
}

.deposit-whose {
    color: var(--tx2);
}

/* Строка найденной клиентки с депозитом — «уже есть депозит 2 000 ₽ — сумма добавится к нему», под контактом. */
.client-option-note {
    font-size: var(--fs-small);
    color: var(--tx2);
}

.client-options-pick .client-option-note {
    grid-column: 2;
}

/* Поиск клиентки в короткой форме (формы депозитов) — во всю колонку, как соседние поля (§6). */
.form-single .client-pick-field,
.form-single .client-options {
    max-width: none;
}

/* --- все записи --- */

/*
  «Все записи» (задача 15 релиза 0.5.0, кадр 352:6905): полоса в две строки, таблица и итог одним блоком.
  В первой строке период берёт 1,1 доли — «Ближайшая неделя» помещается целиком (поиск — две, остальные
  по одной); вторая строка — три списка поровну (общее правило строки).

  Доли колонок — по кадру (940 px, поле ячейки 12 слева входит в долю у всех, кроме первой): дата и время
  104, занятие 132, клиентка 114, хвост 76, запись 274, сумма 122, оплата 118. Раскладка обычная, а не
  фиксированная: отметка «Запись» не уже себя — «Отменена с частичным возвратом» не обрезается (§7), и на
  1100 с развёрнутым меню ширину ей отдают занятие, клиентка и хвост («…»). Сумма — справа, под ней
  способы тихой строкой («сертификат + перевод», `.cell-sub`), длинное — «…»: ячейка не шире своей доли
  (`max-width: 0`). Отметка оплаты — мелкая и тихая, точка своего цвета (кадр: Badge 12,5 / 400).
*/
.bookings-filters .filters-row > .period-field {
    flex-grow: 1.1;
}

/*
  Заготовка поиска длинная («Клиентка: имя, телефон или Телеграм», 273 px): поле не уже её — по ней строка
  решает, уйти ли соседям на следующую строку, как у списков полосы. На 1100 с развёрнутым меню инструкторы
  уходят второй строкой во всю ширину (§7), а не обрезают заготовку.
*/
.bookings-filters .filters-wide {
    field-sizing: content;
}

/* The phone search icon must not change the laptop field's intrinsic minimum. */
.bookings-filters > .field-icon {
    display: none;
}

.table-bookings th,
.table-bookings td {
    box-sizing: border-box;
}

.table-bookings .col-when {
    width: 11.06%;
}

.table-bookings .col-lesson {
    width: 14.04%;
}

.table-bookings .col-name {
    width: 12.13%;
}

.table-bookings .col-tail {
    width: 8.09%;
}

.table-bookings .col-state {
    width: 29.15%;
}

.table-bookings .col-sum {
    width: 12.98%;
}

.table-bookings td.col-sum {
    max-width: 0;
}

.table-bookings .col-pay {
    width: 12.55%;
}

/* Сумма строки не переносится: «10 000 ₽» — одно значение. */
.table-bookings .booking-amount {
    white-space: nowrap;
}

.table-bookings .booking-methods {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.table-bookings .col-pay .badge {
    font-size: var(--fs-small);
    font-weight: 400;
    color: var(--tx2);
}

/* «Свой хвост» — тихим: хвоста школы у записи нет (кадр 352:6905). */
.booking-own {
    color: var(--tx2);
}

/* Дата, отметка оплаты и строка подробностей внутри ячейки клиентки — только у строки-карточки телефона. */
.booking-phone-date,
.booking-phone-pay,
.booking-details {
    display: none;
}

/* --- учётные записи --- */

/*
  Список учётных записей (задача 27 релиза 0.5.0, кадры 362:11825 и 362:11857): таблица в блоке, доли
  колонок — по кадру (940 px: логин 200, имя 260, роль 160, состояние 140, последний вход 180), всё
  слева (§7), только роли отдано 30 px имени: кадр рисовал её по «Администратору», а «Главный
  администратор» занимает 171 px — в колонке 160 он наезжал на отметку состояния. Логин и имя — одной
  строкой с «…» (`col-truncate`), целиком — в карточке. Раскладка таблицы обычная, а не фиксированная,
  как у базы клиентов: отметка «Заблокирована» (126 px) и дата входа не бывают уже своего значения —
  на 1100 с развёрнутым меню доля состояния (113 px) им мала, и место уступают обрезаемые логин и имя.
  Доля считается вместе с полем ячейки (`border-box`), иначе поле между колонками прибавлялось бы к
  каждой.
*/
.table-accounts th,
.table-accounts td {
    box-sizing: border-box;
}

.table-accounts .col-login {
    width: 21.28%;
}

.table-accounts .col-name {
    width: 24.47%;
}

.table-accounts .col-role {
    width: 20.21%;
}

/*
  Роль не обрезается посреди слова — как отметка «Запись» (§7): на 1280 «Главный администратор» стоит
  одной строкой, на 1100 с развёрнутым меню, где колонке 140 px, переносится по словам.
*/
.table-accounts td.col-role {
    white-space: normal;
}

.table-accounts .col-state {
    width: 14.89%;
}

.table-accounts .col-when {
    width: 19.15%;
}

/* Вторая строка карточки телефона — «имя · роль · вход …»: на ноутбуке это колонки той же строки. */
.account-details {
    display: none;
}

/*
  «Выдать новый пароль» в «Информации» учётной записи (кадр 363:12313): белая кнопка s и справа от неё,
  по центру высоты, строка «Старый пароль перестанет действовать.»; не помещается — строка уходит под
  кнопку (на телефоне кнопка во всю ширину, строка — под ней, кадр 363:12330).
*/
.password-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s3);
    margin: var(--s3) 0 0;
}

.password-row button {
    margin: 0;
}

.password-row .hint {
    margin: 0;
}

/*
  Значение, которое форма показывает, но не меняет, — логин в «Изменить» учётной записи: подпись —
  как у поля формы, значение — текстом на месте поля.
*/
.form-fixed {
    margin: 0;
}

.form-fixed dt {
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--tx);
}

.form-fixed dd {
    margin: var(--s1) 0 0;
}

/* --- виды «День» и «Неделя» --- */

/*
  Второй и третий виды раздела «Занятия»: неделя — дни колонками, время по вертикали, занятия
  плитками (задача 09); день — список занятий дня (задача 23 релиза 0.5.0, до неё — та же сетка
  одной колонкой). Своих цветов и кеглей виды не заводят — всё из токенов; собственная величина
  одна, высота часа, и она взята со шкалы отступов. Геометрию плиток считает слой сервисов
  и передаёт переменными --start / --length / --lane / --lanes: в разметке нет ни одной формулы.
*/

.view-switch,
.week-nav,
.day-nav {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-bottom: var(--gap);
}

.view-switch .button,
.week-nav .button,
.day-nav .button {
    margin-top: 0;
}

/* «Текущая неделя» и «Сегодня» — тихие ссылки: заливка на экране одна — «Добавить» (кадры 348:5046, 348:5110). */
.week-nav .button-link,
.day-nav .button-link {
    margin-left: 0;
}

/* «12 — 18 октября 2026» — рядом с переходом, тем же весом, что дата дня. */
.week-title {
    margin-left: var(--s2);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--tx);
}

/*
  Дата дня стоит между стрелками (решение 9) и в переносе не нуждается. На ноутбуке она полная —
  «Понедельник, 12 октября 2026», на телефоне короткая — «Пн, 12.10.2026» (кадры 348:5046,
  348:5078): у коротких русских сокращений дней недели две буквы, ширина подписи постоянная, и
  стрелки от даты к дате не дёргаются. Вторая форма скрыта, а не пересказана: читающая с экрана
  программа видит одну.
*/
.day-title {
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--tx);
    white-space: nowrap;
}

.day-title-short {
    display: none;
}

/*
  Стрелка — кнопка ровно в палец шириной: в ней один знак, а общее поле кнопки раздувало её
  до 82 px. Замер на 375 px: под полосу остаётся 351 px, и «← дата → Сегодня» такими стрелками
  требовали 384 — «Сегодня» уезжало вторым рядом. Ширина считается по внешнему краю
  (border-box), иначе --s7 задал бы содержимое и кнопка вышла бы 66 px, а полосе не хватило бы
  одного пикселя. С 48 px полоса встаёт в 316 px и остаётся одним рядом.
*/
.day-arrow {
    box-sizing: border-box;
    min-width: var(--s7);
    padding-left: var(--s2);
    padding-right: var(--s2);
    text-align: center;
}

/*
  Сетка недели стоит в белом блоке (кадр 348:5110): панель — сам блок, у сетки своей нет.
  Колонок столько, сколько показывает вид: неделя не задаёт --days и берёт запасные семь.
*/
.week {
    /* Час сетки: получасовое занятие остаётся видимой плиткой, а вечер недели влезает в экран. */
    --week-hour: calc(var(--s6) * 2);
    --week-minute: calc(var(--week-hour) / 60);
    display: grid;
    grid-template-columns: calc(var(--s7) + var(--s5)) repeat(var(--days, 7), minmax(0, 1fr));
}

.week-corner,
.week-head {
    padding: var(--s2);
    font-size: var(--fs-small);
    color: var(--tx2);
    border-bottom: 1px solid var(--line-soft);
}

.week-head {
    text-align: center;
    border-left: 1px solid var(--line-soft);
}

/*
  Сегодняшний день отмечен так же, как текущая страница в меню, — цветом, а не рамкой: подпись
  --primary кеглем интерфейса, колонка под ней — подсветкой строки (кадр 348:5110). Вес 500:
  600 бывает ровно у трёх ролей (§2).
*/
.week-head.week-today {
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--primary);
}

.week-corner-untimed,
.week-untimed {
    padding: var(--s2);
    font-size: var(--fs-micro);
    color: var(--tx2);
    background: var(--card);
    border-bottom: 1px solid var(--line-soft);
}

.week-untimed {
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: var(--s1);
    border-left: 1px solid var(--line-soft);
}

/* Занятие без времени — короткой плашкой: в сетке ему места нет, а из недели пропасть оно не должно. */
.week-chip {
    padding: var(--s1) var(--s2);
    font-size: var(--fs-micro);
    color: var(--tx);
    background: var(--soft);
    border: 1px solid var(--soft-line);
    border-radius: var(--r-s);
    text-decoration: none;
}

.week-chip:hover {
    background: var(--row-hover);
    border-color: var(--accent);
}

.week-axis,
.week-day {
    position: relative;
    height: calc(var(--minutes) * var(--week-minute));
}

.week-hour {
    position: absolute;
    top: calc(var(--offset) * var(--week-minute));
    right: var(--s2);
    font-size: var(--fs-micro);
    color: var(--tx2);
}

/* Часовые полосы рисуются фоном: отдельных строк-разделителей в разметке для этого не нужно. */
.week-day {
    border-left: 1px solid var(--line-soft);
    background-image: repeating-linear-gradient(to bottom,
    var(--line-soft) 0, var(--line-soft) 1px, transparent 1px, transparent var(--week-hour));
}

.week-day.week-today {
    background-color: var(--row-hover);
}

.week-tile {
    position: absolute;
    top: calc(var(--start) * var(--week-minute));
    /* Плитки не смыкаются: пара пикселей по низу и правому краю отделяет соседние занятия. */
    height: calc(var(--length) * var(--week-minute) - 2px);
    left: calc(var(--lane) * 100% / var(--lanes));
    width: calc(100% / var(--lanes) - var(--s1));
    box-sizing: border-box;
    overflow: hidden;
    /*
      Плитка — заливка --soft со скруглением, без рамки (кадр 348:5110). По вертикали поле 3 px:
      64-пиксельный час оставляет часовой плитке 62 px, внутри — 56, как было с рамкой 1 px и
      полем 2 px, поэтому пороги строк плитки (`EventService`, 42 / 59 минут) не сдвигаются:
      три строки содержимого занимают 54,75 (замер перепроверки QA 15.08: при меньшем запасе
      нижний отступ уезжал за край).
    */
    padding: 3px var(--s1) 3px var(--s2);
    font-size: var(--fs-small);
    color: var(--tx);
    background: var(--soft);
    border-radius: var(--r-s);
    text-decoration: none;
}

/* Под курсором плитка светлеет и получает кромку — размер при этом не меняется. */
.week-tile:hover {
    background: var(--card);
    box-shadow: inset 0 0 0 1px var(--accent);
}

/*
  Строки плитки не переносятся, а обрезаются многоточием: в неделе с двумя параллельными
  занятиями на колонку дня приходится около полусотни пикселей, и разорванное «Рус | ало»
  читается хуже, чем «Русало…». Полностью занятие называет подсказка самой плитки.
  The time line carries the place after the time. The measured 1074/1089 time-prefix fit is nominal:
  ellipsis of the full row can still cover part of that prefix; the tooltip keeps the full span.
*/
.week-tile-time,
.week-tile-meta,
.week-tile-name {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.week-tile-time,
.week-tile-meta {
    font-size: var(--fs-micro);
    color: var(--tx2);
}

.week-tile-name {
    font-weight: 500;
}

/*
  The state mark ends with «…» inside the tile, like the rows above (QA5-E-01, `Дизайн-система.md` §10): the
  shared .badge is an inline-flex, and text-overflow never reaches its label — an anonymous flex item — so the
  tile edge cut it mid-word, «• В расп». Here it is a block. Its line stays the tile text line, 12.5 × 1.5 =
  18.75, as the inline-flex had it: the 59-minute third-row threshold of `EventService` stands on the three rows'
  54.75 px. The third row shows occupancy or the state; there is no fourth row.
  A block of the micro size alone takes 17.25. nowrap and the weight come from .badge.
*/
.week-tile .badge {
    display: block;
    overflow: hidden;
    font-size: var(--fs-micro);
    line-height: calc(var(--fs-small) * 1.5);
    text-overflow: ellipsis;
}

/* In a block the dot keeps its size only as inline-block; the margin stands in for the gap of the flex. */
.week-tile .badge::before {
    display: inline-block;
    margin-right: 7px;
    vertical-align: middle;
}

/* Замок в плитке — по кегелю плитки: 14, а не 16 строки списка (кадр 348:5110). */
.week-tile .lock-mark .field-icon {
    width: 14px;
    height: 14px;
}

/* Отменённое занятие видно, но не спорит с расписанием: подпись «отменено» у него своя, в badge. */
.week-off {
    color: var(--tx2);
    background: var(--page);
}

.week-off .week-tile-name {
    font-weight: 400;
}

/*
  Линия текущего времени. Смещение --now приходит с сервера в минутах от верха сетки и живёт
  на самой линии: без JS она стоит на времени загрузки страницы, с ним `admin.js` переставляет
  её раз в минуту. Линия не анимируется — она именно переставляется, поэтому и уважать
  prefers-reduced-motion нечем. Сегмент рисуется в каждой колонке дня и заходит на её границу
  слева, чтобы через всю сетку шла одна линия без разрывов; поверх плиток — иначе идущее
  занятие её бы и скрыло, ровно когда она нужна.
*/
.week-now {
    position: absolute;
    top: calc(var(--now) * var(--week-minute));
    left: -1px;
    right: 0;
    z-index: 2;
    height: 2px;
    background: var(--danger);
    pointer-events: none;
}

/* Абзац для узкого окна — белым блоком: на широком экране его нет, там показывается сама сетка. */
.week-narrow {
    display: none;
    color: var(--tx2);
}

/*
  Вид «День» (задача 23 релиза 0.5.0, кадры 348:5046 и 348:5078): список занятий дня в блоке.
  Строка — ссылка на карточку целиком: время, название с тихой строкой подробностей и отметка
  справа, строки — под чертой; первая черта стоит сразу под шапкой блока. Подсветка под курсором
  выходит в поле блока, а текст остаётся на его линии — как у строк таблицы.
*/
.day-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.block-header + .day-list {
    margin-top: calc(-1 * var(--s3));
}

.day-lesson {
    display: grid;
    grid-template-columns: calc(var(--s7) * 2.5) minmax(0, 1fr) auto;
    align-items: baseline;
    gap: var(--s1) var(--s5);
    padding: var(--s3) 0;
    color: var(--tx);
    text-decoration: none;
    border-top: 1px solid var(--line-soft);
}

.day-lesson:hover {
    color: var(--tx);
    background: var(--row-hover);
    box-shadow: calc(-1 * var(--card-pad)) 0 0 var(--row-hover), var(--card-pad) 0 0 var(--row-hover);
}

.day-lesson-time,
.day-lesson-name {
    font-size: var(--fs-ui);
    font-weight: 500;
}

.day-lesson-time {
    font-variant-numeric: tabular-nums;
}

.day-lesson-main {
    display: grid;
    gap: var(--s1);
    min-width: 0;
}

.day-lesson-details {
    font-size: var(--fs-small);
    color: var(--tx2);
}

/* Пустой список — не голая строка, а видимое состояние раздела. */
.empty {
    margin-bottom: var(--gap);
    padding: var(--s7) var(--s5);
    text-align: center;
    color: var(--tx2);
    background: var(--card);
    box-shadow: var(--shadow-block);
    border-radius: var(--r-l);
}

/* --- каталог хвостов (этап 3; вид — задача 25 релиза 0.5.0) --- */

/*
  Выдача каталога — сетка карточек, а не таблица (правка владельца 2026-08-25): хвост узнают по
  фотографии. Ширина карточки задана снизу, число колонок считает браузер (`auto-fill`): три в ряд
  на 1280, четыре на широком экране, одна на телефоне (кадры 358:9912 и 358:9936, Н25). Минимум
  колонки — `min(300px, 100%)`: на 375 px доступные 343 px меньше 300 не бывают, но жёсткий минимум
  шире экрана увёл бы сетку за правый край.
*/
.tail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
    gap: var(--gap);
}

/*
  Карточка хвоста (компонент «Хвост · карточка каталога» 168:5004, Т7, `Дизайн-система.md` §6):
  снимок слева, справа код со статусом, три пары и нижняя строка. Одна высота у всех держится не
  выравниванием ряда, а самой карточкой: у каждой ровно пять строк по одной строке текста (значения
  не переносятся, длинные обрываются «…»), нижняя строка есть всегда и держит место даже пустой.
  Поле карточки — 16 (`--s4`), как в компоненте, а не поле блока: высота компонента — 167 у всех.

  Карточка целиком — ссылка на хвост. Наведение и фокус меняют тень, но не размеры: подрастающая
  под курсором карточка сдвигала бы соседей по ряду. Анимации у админки нет (Ж-4.8).
*/
.tail-card {
    display: grid;
    grid-template-columns: 4.5rem minmax(0, 1fr);
    gap: var(--s4);
    align-items: start;
    padding: var(--s4);
    color: var(--tx);
    text-decoration: none;
    background: var(--card);
    box-shadow: var(--shadow-block);
    border-radius: var(--r-l);
}

.tail-card:hover,
.tail-card:focus-visible {
    color: var(--tx);
    box-shadow: var(--shadow-float);
}

/*
  Снимок 72 × 96 — ровно 3 : 4, той пропорции, к которой сервер приводит каждый кадр белым полем
  (`TailPhotoService`): кадр показан целиком и не обрезается (критерий 8 этапа 3). Фон `--soft` виден
  только под картинкой каталога без своего снимка — она прозрачная, как в компоненте.
*/
.tail-card-photo {
    display: block;
    width: 4.5rem;
    height: 6rem;
    object-fit: contain;
    background: var(--soft);
    border-radius: var(--r-m);
}

/* Строки карточки — через 6 px, как в компоненте: на шкале отступов такого шага нет. */
.tail-card-body {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

/*
  Код и статус — одна строка: статус коротко справа от кода и под код не переносится; длинный
  номер с названием обрывается «…», целиком — в подсказке и в карточке хвоста.
*/
.tail-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    min-width: 0;
}

.tail-card-code {
    min-width: 0;
    overflow: hidden;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tail-card-head .badge {
    flex: none;
    font-size: var(--fs-ui);
}

/* Пара «подпись — значение»: подписи колонкой 64, значение — в одну строку, длинное обрывается «…». */
.tail-fact {
    display: flex;
    align-items: flex-start;
    gap: var(--s2);
    min-width: 0;
}

.tail-fact-label {
    flex: none;
    width: 4rem;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.tail-fact-value {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/*
  Нижняя строка (Т7): причина вывода и примечание тихим текстом через «·» (`TailRow.footnote`),
  длинное обрывается «…», целиком — в карточке хвоста. Высота строки своего кегля держится и у
  пустой: без неё карточка без примечания была бы ниже соседей.
*/
.tail-card-foot {
    min-height: 1.5em;
    overflow: hidden;
    font-size: var(--fs-small);
    color: var(--tx2);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/*
  Карточка хвоста (задача 14 релиза 0.5.0; кадры 358:9954 / 358:10041): верхний ряд «Фото · Информация
  · Состояние» — общий `.blocks-row` (240 · поровну), фото заполняет свой блок (`.block-photo`, `cover`).
  Внизу «Состояния» — сроки недоступности плашкой вида warn (П1, ДС 32): «Недоступен: 01.10–07.10 ·
  аренда» — нажатие открывает правку срока, ✕ справа снимает его; под плашкой тихой строкой — записи,
  которые попали в срок, ссылками. Без срока внизу пусто.
*/
.tail-count {
    font-variant-numeric: tabular-nums;
}

/*
  Уже 1100, где ряд встаёт столбиком, фото хвоста — снимком 3 : 4 высотой 240 по центру прямо на фоне,
  без белого блока (кадр 358:10041): вертикальный снимок в блоке во всю ширину пришлось бы резать
  в полосу. Белый блок возвращает ряд от 1100 (`.blocks-row > .block-photo-tail`).
*/
.block-photo-tail {
    min-height: 0;
    padding: 0;
    background: none;
    box-shadow: none;
}

.block-photo-tail > img {
    position: static;
    display: block;
    width: auto;
    height: 240px;
    aspect-ratio: 3 / 4;
    margin: 0 auto;
}

.tail-periods {
    display: grid;
    gap: var(--s2);
    margin-top: var(--s3);
}

.tail-period {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-height: var(--field-h);
    box-sizing: border-box;
    padding: var(--s1) var(--s1) var(--s1) var(--s3);
    color: var(--warn);
    background: var(--warn-soft);
    border: 1px solid var(--warn-line);
    border-radius: var(--r-s);
}

.tail-period-label {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--fs-ui);
    font-weight: 500;
    color: inherit;
    text-decoration: none;
}

a.tail-period-label:hover {
    color: inherit;
    text-decoration: underline;
}

.tail-period-clear {
    flex: 0 0 auto;
    margin: 0;
}

/* ✕ — знак без рамки цветом плашки; нажимается квадратом кнопки s. */
.tail-period-clear button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    min-height: 36px;
    margin: 0;
    padding: 0;
    color: var(--warn);
    background: none;
    border: none;
}

.tail-period-clear button:hover {
    background: var(--warn-line);
}

.tail-period-clear .field-icon {
    position: static;
    width: 20px;
    height: 20px;
    margin: 0;
    color: currentColor;
}

/* Записи внутри срока: тихой строкой под плашкой, занятия — ссылками в «Заменить хвост» (задача 08). */
.tail-period-note {
    margin: 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.tail-period-note a {
    color: var(--primary);
    text-decoration: underline;
    white-space: nowrap;
}

/*
  Списки занятий хвоста (кадр 358:9954): «Дата · Занятие · Клиентка · Состояние», доли по кадру (940):
  дата 110, клиентка 180, отметка 294 — вместе с полем 12 перед текстом; занятие берёт остаток. Раскладка фиксированная: длинное имя
  и название обрезаются «…» и таблицу не раздвигают; отметка — «Отменена с частичным возвратом» —
  помещается целиком (§7: колонка не уже самой длинной отметки). Короткая дата в начале названия —
  только у строки-карточки телефона.
*/
.table-tail-lessons table {
    table-layout: fixed;
}

/*
  Шапки колонок в кадре нет — значения понятны сами. Она остаётся в разметке и в потоке таблицы — нулевой
  высоты и без знаков на экране: по её ячейкам фиксированная раскладка берёт доли колонок, а чтение
  с экрана — подписи (вынутая из потока шапка отдавала бы колонкам равные доли).
*/
.table-tail-lessons thead th {
    height: 0;
    padding-top: 0;
    padding-bottom: 0;
    font-size: 0;
    line-height: 0;
    border-bottom: 0;
}

.table-tail-lessons th,
.table-tail-lessons td {
    box-sizing: border-box;
}

.table-tail-lessons .col-when {
    width: 11.7%;
}

.table-tail-lessons .col-name {
    width: auto;
}

.table-tail-lessons .col-who {
    width: 19.1%;
}

.table-tail-lessons .col-state {
    width: 31.3%;
}

.tail-lesson-date {
    display: none;
}

/*
  Карточка клиентки (задача 16 релиза 0.5.0; кадр 353:6655). Сводка двумя строками: телефон, Телеграм
  и размер со знаками (контакты — ссылками, промежуток 24); ниже итог-фраза тихим с числами ролью «число
  итог-фразы» и справа, на той же линии, депозит.
*/
.client-summary {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
}

.client-facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s5);
    margin: 0;
    font-size: var(--fs-ui);
}

.client-fact {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}

.client-fact .field-icon,
.client-deposit .field-icon {
    position: static;
    flex: none;
    width: 20px;
    height: 20px;
    margin: 0;
    color: var(--tx2);
}

a.client-fact .field-icon {
    color: var(--primary);
}

.client-total-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s2) var(--s5);
}

.client-total {
    margin: 0;
    color: var(--tx2);
}

.client-total .figure {
    color: var(--tx);
}

.client-deposit {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    margin: 0;
}

/*
  Депозит в сводке карточки клиентки — ссылкой в его карточку (задача 18, кадр 353:6655): «Депозит: 8 000 ₽»
  цветом ссылки и подчёркнут, знак кошелька и шеврон — рядом; у клиентки без депозита — тихая «Добавить
  депозит» (кадр 353:6933).
*/
.client-deposit-link {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    color: var(--primary);
    text-decoration: none;
}

.client-deposit-text {
    text-decoration: underline;
    text-underline-offset: 3px;
}

.client-deposit-link .figure {
    color: inherit;
}

.client-deposit-add .button-link {
    margin: 0;
}

/*
  «Клиентка»: другие карточки на этом же телефоне — тихой строкой под полями, на линии значений (строка
  списка полей с пустой подписью), имена ссылками.
*/
.client-same-phone {
    font-size: var(--fs-small);
    color: var(--tx2);
}

.client-same-phone a {
    color: var(--primary);
    text-decoration: underline;
}

/*
  «Согласия» строками (кадр 353:6655): короткое название — «…» в одну строку, справа колонкой отметка
  состояния, дата и время последней отметки тихим; места публикации «Фото и видео» — строкой во всю ширину под
  ним. Между строками — тонкая черта. The column is 228 and not the frame's 210: «Не получено» with date and
  time measures 219.6 (snapshot client-card-consents; criterion 7 of stage 4 brought the time back, owner's
  decision 27.09); 228 is the width of the picture the owner approved and keeps a margin for font rendering.
*/
.consent-mark {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 228px;
    align-items: baseline;
    column-gap: var(--s3);
    padding: 9px 0;
    border-bottom: 1px solid var(--line-soft);
}

.consent-mark:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.consent-mark-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.consent-mark-state {
    display: flex;
    align-items: baseline;
    gap: 6px;
    white-space: nowrap;
}

.consent-mark-date {
    font-size: var(--fs-small);
    color: var(--tx2);
    font-variant-numeric: tabular-nums;
}

/*
  Who marked the consent, under the title across the row (criterion 7 of stage 4; owner's decision 27.09, variant Б),
  one line with «…». The moment stands beside the state here; on the phone it moves to the right end of this line
  (`.consent-mark-when`), because there the state column holds the mark alone (frame 353:6825).
*/
.consent-mark-by {
    grid-column: 1 / -1;
    display: flex;
    justify-content: space-between;
    gap: var(--s3);
    margin: var(--s1) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.consent-mark-author {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.consent-mark-when {
    display: none;
    flex: none;
    font-variant-numeric: tabular-nums;
}

.consent-mark-places {
    grid-column: 1 / -1;
    margin: var(--s1) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

/*
  Занятия клиентки (кадр 353:6655): «Дата · Занятие · Хвост · Запись · Оплата · Посещение», доли по кадру (940,
  вместе с полем 12 перед текстом): дата 96, хвост 92, запись 284 — «Отменена с частичным возвратом» целиком
  (§7), оплата 198, посещение 112; занятие берёт остаток. Раскладка фиксированная: длинное название и код
  обрезаются «…». Способ оплаты — тихим рядом с отметкой суммы; «Удержано школой 500 ₽» и «не отмечено» —
  тихим текстом. Короткая дата в начале названия, отметка и вторая строка — только у строки-карточки телефона.
*/
.table-client-lessons table,
.table-client-certificates table,
.table-client-gifts table {
    table-layout: fixed;
}

.table-client-lessons th,
.table-client-lessons td,
.table-client-certificates th,
.table-client-certificates td,
.table-client-gifts th,
.table-client-gifts td {
    box-sizing: border-box;
}

.table-client-lessons .col-when {
    width: 10.2%;
}

.table-client-lessons .col-name {
    width: auto;
}

.table-client-lessons .col-tail {
    width: 9.8%;
}

.table-client-lessons .col-state {
    width: 30.2%;
}

.table-client-lessons .col-pay {
    width: 21.1%;
    white-space: nowrap;
}

.table-client-lessons .col-visit {
    width: 11.9%;
    white-space: nowrap;
}

.client-pay-note,
.client-visit-note {
    color: var(--tx2);
}

.badge + .client-pay-note {
    margin-left: 6px;
}

.client-lesson-date,
.client-row-phone,
.client-row-details {
    display: none;
}

/*
  Сертификаты клиентки — «Номер · Курс · Дата · Состояние» (100 · 150 · 68 · 122 из 440) и подарочные —
  «Код · На что · До · Состояние» (80 · 142 · 96 · 122): доли по кадру, но колонка состояния — по самой длинной
  отметке («Аннулирован», «Использован», §7), курс и «На что» берут остаток и обрезаются «…». Номер и код —
  главная колонка строки.
*/
.table-client-certificates .col-number {
    width: 22.7%;
}

.table-client-certificates .col-course {
    width: auto;
}

.table-client-certificates .col-when {
    width: 15.5%;
}

.table-client-certificates .col-state,
.table-client-gifts .col-state {
    width: 27.7%;
}

.table-client-gifts .col-code {
    width: 18.2%;
}

.table-client-gifts .col-purpose {
    width: auto;
}

.table-client-gifts .col-until {
    width: 21.8%;
}

.client-row-title {
    white-space: nowrap;
}

/*
  Окна карточки хвоста (задача 14; 358:10137, 358:10156, 358:10168): под заголовком — тихая строка
  «VK12 · Ариэль», вплотную к нему; поля «С» и «По» — рядом, поровну (на телефоне — столбиком).
*/
.dialog:has(.dialog-sub) > .dialog-title {
    margin-bottom: var(--s1);
}

.dialog-sub {
    margin: 0 0 var(--s4);
    font-size: var(--fs-small);
    color: var(--tx2);
}

.dialog-sub ~ .message {
    margin-bottom: 0;
}

.dialog-pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 var(--s4);
}

.dialog-text {
    margin: 0;
}

.tail-photo-preview {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 4;
    border-radius: var(--r-m);
    object-fit: contain;
    background: var(--soft);
}

/*
  Снимок в форме — отдельная колонка справа от зоны загрузки, со своим заголовком над ним
  (макеты `forma-redaktirovaniya-instruktora-foto-sprava.png` и
  `forma-novogo-instruktora-posle-vybora-foto.png`, правки владельца 2026-08-25).

  До этой правки снимок и зона стояли флексом слева направо и делили ширину по запросу:
  у хвоста снимок просил `width: 100%` без потолка и забирал её всю, а зона схлопывалась
  в ноль (замер на /tails/1/edit при 1280 px: `.photo-field-body` 0 px при 456 доступных,
  подпись зоны 12 px в ширину и 563 в высоту — «по одной букве на строку»). У инструктора
  снимок был ограничен, но стоял ПЕРЕД своей подписью и выталкивал зону на 208 px правее
  поля «Имя» — «выезжает за сетку».

  Колонка снимку задана числом, а не содержимым: выбор файла не двигает и не сужает левую
  колонку, а пустая колонка у нового объекта просто стоит пустой.
*/
.photo-side {
    display: grid;
    gap: var(--s2);
    align-content: start;
}

/* Скрытие явное: авторское `display: grid` перебивает браузерное `[hidden] { display: none }`. */
.photo-side[hidden] {
    display: none;
}

/*
  Заголовок колонки — та же ступень, что подпись поля, но не <label>: подписи здесь не к чему
  звать, поле выбора файла лежит в левой колонке и свою подпись уже имеет.
*/
.photo-side-title {
    /* Верхний отступ тот же, что у подписи поля: заголовки двух колонок стоят на одной линии. */
    margin: var(--s4) 0 0;
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--tx);
}

/* Снимок занимает колонку целиком: свой потолок ширины ему задаёт она, а не он сам. */
.photo-side .instructor-photo-preview,
.photo-side .tail-photo-preview {
    width: 100%;
    margin: 0;
}

/* Кнопка стоит вплотную под снимком и делит с ним левый и правый края. */
.photo-side button {
    width: 100%;
    margin: 0;
}

/*
  Пометка «удалить снимок» — флажок, одетый кнопкой: щёлкается всей площадью, как кнопка, но
  состояние у него настоящее и уезжает на сервер вместе с формой даже без скриптов. Приём тот
  же, что у `.segment`: сам `input` лежит поверх прозрачным слоем, видим — `span`.
*/
.photo-remove {
    position: relative;
    display: grid;
    margin: 0;
}

.photo-remove[hidden] {
    display: none;
}

.photo-remove input {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.photo-remove > span {
    display: flex;
    box-sizing: border-box;
    align-items: center;
    justify-content: center;
    min-height: var(--field-h);
    padding: var(--btn-py) var(--s4);
    font-size: var(--fs-ui);
    font-weight: 500;
    line-height: 1.4;
    text-align: center;
    border: 1px solid transparent;
    border-radius: var(--r-s);
}

/*
  Пометки нет — предлагается необратимое: вид `.button-danger`, белая с розовым текстом и
  розовой рамкой (`Дизайн-система.md` §3, пункт 20; «Поле / фото · снимок» 167:5026) — залитой
  розовой кнопки в админке нет. Селектор с родителем обязателен: `.photo-remove > span` выше
  ставит прозрачную рамку и одним классом не перебивается — у белой кнопки рамка и есть её край
  (у прежней розовой заливки это было не видно, а «Вернуть фотографию» стояла без рамки).
*/
.photo-remove > .photo-remove-off {
    color: var(--danger);
    background: var(--card);
    border-color: var(--danger-line);
}

.photo-remove input:hover ~ .photo-remove-off {
    color: var(--danger-hover);
    background: var(--danger-soft);
    border-color: var(--danger);
}

/* Пометка стоит — предлагается вернуть как было, действие спокойное: вторая по весу кнопка. */
.photo-remove > .photo-remove-on {
    color: var(--primary);
    background: var(--card);
    border-color: var(--line);
}

.photo-remove input:hover ~ .photo-remove-on {
    background: var(--soft);
    border-color: var(--accent);
}

.photo-remove input:focus-visible ~ span {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/*
  Видна ровно одна подпись, и какая — решает сам флажок, без единой строки скриптов. Селекторы
  сильнее одного класса не по вкусу, а по необходимости: `.photo-remove > span` выше уже задал
  обеим `display: flex`, и правило в один класс ему проигрывало бы (замер: обе подписи стояли
  друг под другом, площадка 88 px вместо 44).
*/
.photo-remove > .photo-remove-on,
.photo-remove input:checked ~ .photo-remove-off {
    display: none;
}

.photo-remove input:checked ~ .photo-remove-on {
    display: flex;
}

/*
  Зона загрузки снимка (правка владельца 2026-08-25). Настоящее поле выбора файла лежит поверх
  зоны прозрачным слоем: щелчок в любую точку попадает именно в него, перетаскивание файла —
  тоже, и всё это без единой строки скриптов. Кнопки «Выберите файл» браузера при этом не
  видно: её ширину и подпись задаёт сам браузер, выглядела она чужой, а в колонке формы ещё
  и вылезала за край страницы (замер 2026-08-23: /tails/{id}/edit на 1100 px — scrollWidth
  1106 при clientWidth 1085).
*/
.dropzone {
    position: relative;
    display: block;
    margin-top: var(--s2);
}

.dropzone input[type="file"] {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    opacity: 0;
    cursor: pointer;
}

.dropzone-face {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s1);
    padding: var(--s5) var(--s4);
    text-align: center;
    background: var(--page);
    border: 1px dashed var(--soft-line);
    border-radius: var(--r-m);
}

/* Поле лежит поверх зоны, поэтому её состояния читаются с него, а не с самой зоны. */
.dropzone input[type="file"]:hover ~ .dropzone-face,
.dropzone.dropzone-over .dropzone-face {
    background: var(--soft);
    border-color: var(--accent);
}

.dropzone input[type="file"]:focus-visible ~ .dropzone-face {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.dropzone input[type="file"].invalid ~ .dropzone-face {
    border-color: var(--danger);
}

.dropzone-icon .field-icon {
    position: static;
    width: 32px;
    height: 32px;
    margin: 0;
    color: var(--accent);
}

.dropzone-title {
    font-weight: 600;
}

.dropzone-hint {
    font-size: var(--fs-small);
    color: var(--tx2);
}

/* Не кнопка, а её вид: настоящая кнопка внутри зоны перехватывала бы щелчок у поля файла. */
.dropzone-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    /* Тот же минимум, что у настоящих кнопок и у пометки удаления рядом: иначе в колонке
       снимка стояли бы две одинаковые с виду кнопки разной высоты. */
    min-height: var(--field-h);
    margin-top: var(--s2);
    padding: var(--btn-py) var(--s5);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--primary);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-s);
}

/*
  Форма инструктора (кадры 357:8778 и 357:8811, задача 24 релиза 0.5.0): в блоке слева «Имя»,
  зона загрузки и её подсказка, справа колонка снимка 320 (20 rem) вровень с «Именем» по верхнему
  краю, между ними --s6. Кнопки — строкой под блоком, поэтому строк у сетки одна.

  Ширина левой колонки — ровно поле ввода (`--field-max`), правой — 20 rem. Обе заданы числом,
  и выбор файла не двигает левую колонку: место под снимок стоит занятым с самого начала.
  Первое поле и заголовок колонки снимка стоят сразу под краем блока — своего верхнего отступа
  у них нет, иначе они встали бы ниже поля блока.
*/
.instructor-form {
    display: grid;
    grid-template-columns: minmax(0, var(--field-max)) minmax(0, 20rem);
    gap: 0 var(--s6);
    align-items: start;
}

.instructor-form-main > :first-child > label:first-child,
.instructor-form .photo-side-title {
    margin-top: 0;
}

/*
  Форма хвоста (кадры 359:10444 и 359:10529, Н24, задача 25 релиза 0.5.0): две колонки, зона загрузки —
  последней в левой, снимок — под полями правой. Колонки высотой независимы: зона встаёт сразу под
  «Примечанием», снимок — сразу под «Хвостом в боте». Сеткой так не выложить — строки сетки общие
  у колонок, и зона встала бы под правой, более высокой. А в разметке зона и снимок стоят после обеих
  колонок: на телефоне форма идёт одной колонкой в порядке разметки (`Дизайн-система.md` §5), и снимок
  должен стоять сразу под зоной (359:10574). Поэтому части колонок — обтеканием: левые прижаты влево
  под предыдущие левые (`clear: left`), правые — вправо под правые. Ширина колонок — как у
  `.form-grid`: по полю, не шире `--field-max`, между ними `--s6`. Снимок — 224 (14 rem, 3 : 4):
  на всю колонку кадр вырастал бы до 605 px в высоту.
*/
.tail-form {
    display: flow-root;
    max-width: calc(2 * var(--field-max) + var(--s6));
}

.tail-form-left,
.tail-form-right {
    width: calc((100% - var(--s6)) / 2);
}

.tail-form-left {
    float: left;
    clear: left;
}

.tail-form-right {
    float: right;
    clear: right;
}

.tail-form .photo-side {
    max-width: 14rem;
}

/*
  Узкое окно: колонки встают друг под друга в порядке разметки — поля, под ними снимок (кадр
  357:8831), от зоны загрузки до заголовка снимка --s5. Пока снимка нет, строки под него нет вовсе.
*/
@media (max-width: 900px) {
    .instructor-form {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--s5) 0;
    }

    .instructor-form .photo-side {
        max-width: 20rem;
    }
}

/* Имя выбранного файла дописывает `admin.js`: без него о выборе говорил бы только предпросмотр. */
.dropzone-file {
    font-size: var(--fs-small);
    color: var(--tx2);
    overflow-wrap: anywhere;
}

.dropzone-file:empty {
    display: none;
}

/*
  Группа переключателей формы. Рамку и поля fieldset снимаем: смысловые блоки админки
  разделены отступами и подписями, а не рамками, — вторая рамка внутри карточки читалась бы
  как вложенная карточка.
*/
main form fieldset {
    /*
      Верхний отступ группы — тот же, что у одиночной подписи (`label`), плюс промежуток
      под подсказку предыдущего поля: без него «Размер» вставало вплотную к строке «Как
      в каталоге школы: „VK12“, „0001“», и две строки читались одним абзацем (правка
      владельца 2026-08-25). Нижнего отступа группа не заводит — его даёт `.segmented`.
    */
    margin: var(--s5) 0 0;
    padding: 0;
    border: none;
}

/* Заголовок группы переключателей — та же ступень, что подпись поля: вид у них общий. */
main form legend {
    padding: 0;
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--tx);
}

/*
  Переключатель из двух кнопок «по заливке» вместо столбика радиокнопок (правка владельца
  2026-08-25). Внутри — те же настоящие переключатели одной группы: без стилей и без скриптов
  форма остаётся рабочей, с клавиатуры группа обходится стрелками, а выбор уходит на сервер
  прежним полем. Само поле не прячется `display: none` — так оно перестало бы получать фокус;
  оно растянуто прозрачным слоем поверх своей половины.

  Кнопки равной ширины (`grid-auto-columns: 1fr`), а не по тексту: «Полная (лиф + хвост)»
  и «Неполная (хвост)» разной длины, и половинки разного размера читались бы как разные
  по весу варианты.
*/
.segmented {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 3px;
    max-width: var(--field-max);
    margin: var(--s2) 0 var(--s4);
    padding: 3px;
    background: var(--page);
    border: 1px solid var(--line-soft);
    border-radius: var(--r-s);
}

/* Сетка, а не блок: короткая половина растягивается до высоты длинной, а не висит на своей. */
.segment {
    position: relative;
    display: grid;
    margin: 0;
}

.segment input {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

/*
  Высота задана минимумом, а не числом: «Полная (лиф + хвост)» на 375 px не помещается в строку
  и переносится на две (замер 2026-08-25: содержимому нужно 40 px при высоте кнопки 36), а
  жёсткая высота обрезала бы вторую строку. Переключатель растёт, обе половины остаются равными.
*/
.segment > span {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Минимум считается вместе с полями: снаружи переключатель обязан выйти ровно в --field-h,
       как соседние поля формы (критерий 17), — 36 px кнопки плюс 3 px рамки полосы с двух сторон. */
    box-sizing: border-box;
    min-height: calc(var(--field-h) - 8px);
    padding: var(--s1) var(--s2);
    font-size: var(--fs-ui);
    line-height: 1.25;
    font-weight: 500;
    color: var(--tx2);
    text-align: center;
    border-radius: calc(var(--r-s) - 2px);
}

.segment input:hover ~ span {
    color: var(--primary);
}

/*
  Те же сегменты из ссылок — там, где выбор уходит адресом (фильтр состояния справочников,
  вид раздела «События»): форма не нужна, ссылка работает и без JS. Полоса по ширине своих
  подписей, а не поля формы: это фильтр над списком, а не поле. Геометрия подписи — та же,
  что у `.segment > span`, чтобы оба вида читались одним элементом.
*/
.segmented-links {
    display: inline-grid;
    max-width: none;
    margin: 0 0 var(--gap);
}

.segment-link {
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    min-height: calc(var(--field-h) - 8px);
    padding: var(--s1) var(--s4);
    font-size: var(--fs-ui);
    line-height: 1.25;
    font-weight: 500;
    color: var(--tx2);
    text-decoration: none;
    white-space: nowrap;
    border-radius: calc(var(--r-s) - 2px);
}

.segment-link:hover {
    color: var(--primary);
}

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

.view-switch .segmented-links {
    margin: 0;
}

/*
  Выбранный сегмент — белая плашка на полосе, а не бирюзовая заливка (решение владельца 21.09,
  вопрос 3 пятого круга): заливка на экране одна и принадлежит главной кнопке, а в форме хвоста
  три переключателя подряд дали бы три заливки рядом с «Сохранить».
*/
.segment input:checked ~ span,
.segment-link[aria-current="true"] {
    color: var(--primary);
    background: var(--card);
    box-shadow: var(--shadow-block);
}

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

/*
  Переключатель в форме — во всю свою колонку, сегменты поровну (пункт 17, А23): в короткой форме
  колонка шире потолка поля, и потолок здесь снимается, как у её полей.
*/
.form-single .segmented {
    max-width: none;
}

/*
  Размер s (36 px, `Переключатель` size=s 160:5328): строка списка — согласия в форме клиентки.
  По самому широкому сегменту, на телефоне — во всю ширину блока, поровну (А16, Н16).
*/
.segmented-s {
    display: inline-grid;
    max-width: none;
    margin: 0;
}

.segmented-s .segment > span {
    min-height: 28px;
    padding: 0 var(--s3);
    white-space: nowrap;
}

/*
  Фильтр с названием в первом сегменте («Ласта: любая · Mini · Maxi»): сегменты по длине
  подписи, первый шире остальных, остаток ширины — поровну; подписи целые (§4, В40, В41).
*/
.segmented-fit {
    display: flex;
    max-width: none;
}

.segmented-fit .segment {
    flex: 1 1 auto;
}

.segmented-fit .segment > span {
    padding: var(--s1) var(--s4);
    white-space: nowrap;
}

/* --- страницы --- */

.pagination {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s1);
    margin-bottom: var(--gap);
}

.page-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 34px;
    height: 34px;
    padding: 0 var(--s2);
    box-sizing: border-box;
    font-size: var(--fs-ui);
    color: var(--primary);
    background: var(--card);
    border: 1px solid var(--line-soft);
    border-radius: var(--r-s);
    text-decoration: none;
}

.page-link:hover {
    color: var(--primary);
    background: var(--soft);
    border-color: var(--accent);
}

.page-current,
.page-current:hover {
    color: var(--on-primary);
    background: var(--primary);
    border-color: var(--primary);
    font-weight: 500;
}

/*
  Footer of a long list (owner's decisions R3–R4 of 03.10.2026, `Дизайн-система.md` §7 «Таблицы»): the range and
  «На странице» at the left, the page links at the right, under a thin line closing the rows. Only lists longer than
  the smallest page have it (`PageSize.shownFor`).
*/
.list-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding-top: var(--s3);
    border-top: 1px solid var(--line-soft);
}

.list-size {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s3);
}

.list-range {
    font-size: var(--fs-ui);
    color: var(--tx2);
    font-variant-numeric: tabular-nums;
}

.list-size select {
    width: auto;
}

.list-footer .pagination {
    margin: 0;
}

.page-gap {
    padding: 0 var(--s1);
    color: var(--tx2);
}

/* --- узкое окно и планшет: списки карточками --- */

/*
  Ниже 1024 px таблица показывается карточками (правка владельца 2026-08-08): листать строку
  вбок, чтобы прочитать одну запись, — не работа, а полоса прокрутки контейнера уезжала в самый
  низ страницы. Разметка та же самая, меняется только раскладка: подпись колонки берётся из
  data-label, поэтому дублировать шаблон не нужно. Побочный и главный эффект — на десктопе
  таблице больше не нужна собственная прокрутка, и её заголовок прилипает прямо к странице.
  Разделы этапов 2–5 получают это же поведение, добавив класс table-cards и подписи.
*/
@media (max-width: 1024px) {
    .table-cards {
        background: none;
        border: none;
        border-radius: 0;
        box-shadow: none;
    }

    .table-cards table,
    .table-cards tbody,
    .table-cards tr,
    .table-cards td {
        display: block;
        width: auto;
        min-width: 0;
    }

    /*
      Доли колонок списка событий на карточках не действуют: ячейка стала блоком во всю ширину
      карточки. Правило нужно именно здесь и именно с этим весом — доли заданы парой классов
      (`.table-events .col-*`) и без него перебивают `width: auto` соседнего правила, отчего
      значения на телефоне вжимаются в колонку шириной с десятую часть карточки.
    */
    .table-cards.table-events td {
        width: auto;
    }

    .table-cards thead {
        display: none;
    }

    .table-cards tr {
        margin-bottom: var(--s3);
        padding: var(--s3) var(--s4);
        background: var(--card);
        box-shadow: var(--shadow-block);
        border-radius: var(--r-l);
    }

    .table-cards td {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: var(--s4);
        padding: var(--s1) 0;
        white-space: normal;
        text-align: right;
        border: none;
    }

    .table-cards td::before {
        content: attr(data-label);
        flex: 0 0 auto;
        font-size: var(--fs-small);
        color: var(--tx2);
        text-align: left;
    }

    /*
      Вторая строка ячейки на карточке — своей строкой под значением, у правого края: ячейка
      здесь флекс «подпись / значение», и без переноса она встала бы третьим столбцом.
    */
    .table-cards td:has(> .cell-sub) {
        flex-wrap: wrap;
    }

    .table-cards td > .cell-sub {
        flex: 0 0 100%;
    }

    /*
      Обрезка «…» на карточке: доли колонки здесь нет — ячейка во всю ширину карточки. Ячейки —
      флекс «подпись / значение», и их значение переносится, как у всех карточных ячеек.
    */
    .table-cards td.col-truncate {
        max-width: none;
    }

    /* Подсветка строки под курсором на карточках не нужна — они и так отделены друг от друга. */
    .table-cards tbody tr:hover td {
        background: none;
    }


    /*
      Строки-карточки таблицы внутри блока — строки под чертой, а не отдельные панели с тенью:
      панель уже сам блок (кадры 375 списков, например 352:7027, 356:7678). Подсветка крайних
      ячеек под курсором здесь не нужна, как и у карточек.
    */
    .block .table-cards tr {
        margin: 0;
        padding: var(--s3) 0;
        background: none;
        box-shadow: none;
        border-radius: 0;
        border-bottom: 1px solid var(--line-soft);
    }

    .block .table-cards tr:last-child {
        border-bottom: none;
    }

    .block .table-cards tbody tr:hover td {
        box-shadow: none;
    }

    /*
      Полоса дня на строках-карточках — своей полосой над группой, со скруглением (кадр 348:4968):
      карточкой она выглядела бы таким же занятием, как настоящие. Текст — на линии поля блока,
      фон выходит в поле, как на ноутбуке; черты под полосой нет — она сама разделитель.
    */
    .block .table-cards tr.day-row {
        padding: var(--s1) 0;
        border-bottom: none;
    }

    .block .table-cards tbody + tbody tr.day-row {
        padding-top: var(--s4);
    }

    .table-cards th.day-cell {
        display: block;
        margin: 0 calc(var(--s2) - var(--card-pad));
        padding: var(--s2) calc(var(--card-pad) - var(--s2));
        border-radius: var(--r-s);
        box-shadow: none;
    }

    /*
      Строка занятия карточкой (кадр 348:4968): «время · отметка» первой строкой, название 15 / 500
      второй, тихая строка «инструктор · локация · занято · цена» третьей — её собирает слой сервисов,
      поэтому колонки инструктора, локации, мест и цены здесь не показываются. Подписи колонок не
      нужны: значения понятны сами (§7).
    */
    .table-events tbody tr:not(.day-row) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-events td {
        display: block;
        padding: 0;
        text-align: left;
    }

    .table-events td::before {
        display: none;
    }

    .table-events td.col-time {
        font-weight: 500;
    }

    .table-events td.col-state {
        grid-column: 2;
        grid-row: 1;
    }

    .table-events td.col-name {
        grid-column: 1 / -1;
        white-space: normal;
    }

    /* Название — одной строкой «…» (`td.col-truncate .name-line > a`), замок скрытого — сразу за ним. */
    .table-events td.col-name .name-line {
        font-size: var(--fs);
    }

    .table-events .event-details {
        display: block;
        margin-top: var(--s1);
    }

    .table-events td.col-who,
    .table-events td.col-what,
    .table-events td.col-seats,
    .table-events td.col-price {
        display: none;
    }

    /*
      Строка справочника карточкой (кадры 356:7678, 356:7799, 357:8882): название 15 / 500 одной
      строкой с «…», справа от него отметка — только у скрытой записи, под названием тихая строка
      подробностей («нужен хвост · проведено 24 · впереди 6», у локации — адрес); её переносить
      можно. Location details stop after two lines with an ellipsis: an address is unbounded.
      Остальные колонки здесь не показываются, подписи колонок не нужны (§7).
    */
    .table-catalogue tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-catalogue td {
        display: block;
        padding: 0;
        text-align: left;
    }

    .table-catalogue td::before {
        display: none;
    }

    .table-catalogue td.col-main {
        font-size: var(--fs);
    }

    .table-catalogue td.col-main > a {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-catalogue .catalogue-details {
        display: block;
        margin-top: var(--s1);
    }

    .table-locations .catalogue-details {
        display: -webkit-box;
        overflow: hidden;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        line-clamp: 2;
    }

    .table-catalogue td:not(.col-main):not(.col-state),
    .table-catalogue tr:not(.row-hidden) td.col-state {
        display: none;
    }

    /*
      Строка занятия хвоста карточкой (кадр 358:10041): «14.10 Русалки · группа, 14.10» первой строкой
      15 / 500, под ней клиентка слева и отметка справа; длинное имя клиентки обрезается «…», отметка —
      нет. Колонки даты здесь нет: короткая дата стоит в начале первой строки. Подписи колонок не нужны.
      The lesson title also stays one line with an ellipsis (owner's decision).
    */
    .table-tail-lessons tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-tail-lessons td {
        display: block;
        width: auto;
        padding: 0;
        text-align: left;
    }

    .table-tail-lessons td::before {
        display: none;
    }

    .table-tail-lessons td.col-when {
        display: none;
    }

    .table-tail-lessons td.col-name {
        grid-column: 1 / -1;
        font-size: var(--fs);
        white-space: nowrap;
    }

    .table-tail-lessons .tail-lesson-date {
        display: inline;
        margin-right: var(--s2);
    }

    .table-tail-lessons td.col-who {
        max-width: none;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /*
      Строка базы клиентов карточкой (кадр 353:7005): ФИО 15 / 500 одной строкой с «…», под ним тихая
      строка «телефон · Телеграм», у кого Телеграма нет — «телефон · почта» (`client-contacts`) — one line
      too; телефон и Телеграм — ссылками, как в строке. Колонки контактов здесь не показываются, подписи
      колонок не нужны (§7); доли колонок не действуют — ячейка во всю ширину карточки.
    */
    .table-cards.table-clients td {
        display: block;
        width: auto;
        padding: 0;
        text-align: left;
    }

    .table-clients td::before {
        display: none;
    }

    .table-clients td.col-main {
        font-size: var(--fs);
    }

    .table-clients td.col-main > a {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /*
      The second line is always one line (QA5-Q-01; owner's rule 27.09: 581 of 581 real clients fit one line at 375):
      the phone whole, a nick or an email ends with «…». Contacts are flex items picked by kind, not by position —
      showCopiedNote (admin.js) appends «Скопировано» after the email, and the note must not be the one to shrink. The
      email takes only the room left (basis 0, up to its own width), so «Телеграм · почта» cuts the email first and
      the nick only when it does not fit by itself — shrink weights would still take a fraction of a pixel from the
      nick and put «…» on it. «·» with its spaces is an anonymous item, and `pre` keeps those spaces. An ellipsis on a
      link needs a flex item with min-width 0: inline, it has no width of its own.
    */
    .table-clients .client-contacts {
        display: flex;
        align-items: baseline;
        margin-top: var(--s1);
        overflow: hidden;
        white-space: pre;
    }

    .table-clients .client-contacts > * {
        flex: none;
    }

    .table-clients .client-contacts > a.contact:not(.contact-phone) {
        flex: 0 1 auto;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-clients .client-contacts > button.contact-copy {
        flex: 1 1 0;
        min-width: 0;
        max-width: max-content;
    }

    .table-clients td:not(.col-main) {
        display: none;
    }

    /*
      Строка реестра сертификатов карточкой (кадр 354:7534): «номер · ФИО» 15 / 500 одной строкой с «…»,
      под ней тихая строка «курс · дата» (`cert-details`). Остальные колонки здесь не показываются,
      подписи колонок не нужны (§7); у аннулированного вся карточка тихая — цвет наследуется от строки.
    */
    .table-cards.table-certificates td {
        display: block;
        width: auto;
        padding: 0;
        text-align: left;
    }

    .table-certificates td::before {
        display: none;
    }

    .table-certificates td.col-main {
        font-size: var(--fs);
    }

    .table-certificates .cert-title {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-certificates .cert-phone-name {
        display: inline;
    }

    .table-certificates .cert-details {
        display: block;
        margin-top: var(--s1);
    }

    .table-certificates td:not(.col-main) {
        display: none;
    }

    /*
      Строка подарочного карточкой (кадр 354:7663): «Код 1067  Ксения Громова» и отметка справа, под ними тихо
      «Русалки · групповое, Mini · до 02.09.2026» или «· в записи на 14.10». Ячейка кода растворяется в сетке
      строки (`display: contents`), остальные колонки не показываются, подписи колонок не нужны (§7).
    */
    .table-gifts tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-gifts td {
        text-align: left;
    }

    .table-cards.table-gifts td.col-main {
        display: contents;
    }

    .table-gifts td::before {
        display: none;
    }

    .table-gifts .gift-title {
        overflow: hidden;
        font-size: var(--fs);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-gifts .gift-phone-name {
        display: inline;
        margin-left: var(--s3);
        font-weight: 400;
    }

    .table-gifts .gift-phone-state {
        display: block;
    }

    .table-gifts .gift-details {
        display: block;
        grid-column: 1 / -1;
        margin: 0;
    }

    .table-gifts td:not(.col-main) {
        display: none;
    }

    /*
      Строка «Всех записей» карточкой (кадр 352:7027): «12.10  Анна Орлова» одной строкой с «…» и отметка
      оплаты справа — у оплаченной сумма, у остальных оплата словами; ниже во всю ширину тихо «19:00 · Русалки
      · группа · VK14 · подтверждена». Ячейка клиентки растворяется в сетке строки (`display: contents`),
      остальные колонки не показываются, подписи колонок не нужны (§7).
    */
    .table-bookings tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-bookings td {
        text-align: left;
    }

    .table-cards.table-bookings td.col-main {
        display: contents;
    }

    .table-bookings td::before {
        display: none;
    }

    .table-bookings .booking-title {
        overflow: hidden;
        font-size: var(--fs);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-bookings .booking-phone-date {
        display: inline;
        margin-right: var(--s2);
        font-size: var(--fs-ui);
    }

    .table-bookings .booking-phone-pay {
        display: block;
    }

    .table-bookings .booking-details {
        display: block;
        grid-column: 1 / -1;
        margin: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-bookings td:not(.col-main) {
        display: none;
    }

    /* «Где использован» на телефоне: занятие, под ним «клиентка · сумма» и отметка «что с занятием». */
    .table-cards.table-gift-uses td {
        display: block;
        width: auto;
        padding: 0;
        text-align: left;
    }

    .table-gift-uses td::before {
        display: none;
    }

    .table-gift-uses .gift-use-details {
        display: block;
        margin-top: var(--s1);
    }

    .table-gift-uses td.col-when,
    .table-gift-uses td.col-name,
    .table-gift-uses td.col-sum {
        display: none;
    }

    .table-gift-uses td.col-fate {
        margin-top: var(--s1);
    }

    /*
      Карточка клиентки строками-карточками (кадр 353:6825): занятие — «14.10 Русалки · группа» 15 / 500 с «…»
      и отметкой справа (у будущей — оплата, у прошедшей — посещение, у отменённой — отмена), ниже во всю ширину
      тихо «19:00 · VK12 · Подтверждена · перевод»; сертификат — одной строкой «MI-1-0117 Mermaid Inside ·
      05.10» и отметка. Главная ячейка растворяется в сетке строки (`display: contents`), остальные колонки
      не показываются, подписи колонок не нужны (§7).
    */
    .table-client-lessons tbody tr,
    .table-client-certificates tbody tr,
    .table-client-gifts tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-client-certificates tbody tr,
    .table-client-gifts tbody tr {
        grid-template-columns: auto minmax(0, 1fr) auto;
    }

    .table-cards.table-client-lessons td,
    .table-cards.table-client-certificates td,
    .table-cards.table-client-gifts td {
        text-align: left;
    }

    .table-cards.table-client-lessons td.col-main,
    .table-cards.table-client-certificates td.col-main,
    .table-cards.table-client-gifts td.col-main {
        display: contents;
    }

    .table-client-lessons td::before,
    .table-client-certificates td::before,
    .table-client-gifts td::before {
        display: none;
    }

    .table-client-lessons td:not(.col-main),
    .table-client-certificates td:not(.col-main),
    .table-client-gifts td:not(.col-main) {
        display: none;
    }

    .table-client-lessons .client-lesson-title,
    .table-client-certificates .client-row-title,
    .table-client-gifts .client-row-title {
        overflow: hidden;
        font-size: var(--fs);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-client-lessons .client-lesson-date {
        display: inline;
        margin-right: var(--s2);
        font-size: var(--fs-ui);
    }

    .table-client-lessons .client-row-phone,
    .table-client-certificates .client-row-phone,
    .table-client-gifts .client-row-phone {
        display: block;
    }

    .table-client-lessons .client-row-details {
        display: block;
        grid-column: 1 / -1;
        margin: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-client-certificates .client-row-details,
    .table-client-gifts .client-row-details {
        display: block;
        overflow: hidden;
        font-size: var(--fs-ui);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /*
      Строка учётной записи карточкой (кадр 362:11857): логин 15 / 500 одной строкой с «…» и отметка
      состояния справа, под ними во всю ширину — тихая строка «имя · роль · вход …» (`account-details`).
      Ячейка логина растворяется в сетке строки (`display: contents`): иначе вторая строка упиралась бы
      в отметку. Остальные колонки здесь не показываются, подписи колонок не нужны (§7).
    */
    .table-accounts tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-accounts td {
        display: block;
        width: auto;
        padding: 0;
        text-align: left;
    }

    .table-cards.table-accounts td.col-main {
        display: contents;
    }

    .table-accounts td::before {
        display: none;
    }

    .table-accounts td.col-main > a {
        grid-column: 1;
        grid-row: 1;
        overflow: hidden;
        font-size: var(--fs);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-accounts td.col-state {
        grid-column: 2;
        grid-row: 1;
    }

    .table-accounts .account-details {
        display: block;
        grid-column: 1 / -1;
        grid-row: 2;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-accounts td:not(.col-main):not(.col-state) {
        display: none;
    }

    /*
      Журнал строками-карточками (кадр 362:11794) — на той же ширине, что таблицы (`--cards-below`):
      уже шесть колонок не читаются. Шапки колонок нет; строка — объект ссылкой, под ним «действие ·
      что изменилось» одной строкой с «…» и тихое «когда · кто · роль» — их несёт ячейка объекта
      (`audit-line`), остальные ячейки здесь не показываются. Шеврон — справа от объекта.
    */
    .audit-list .row-list-head {
        display: none;
    }

    .audit-list .row-summary {
        grid-template-columns: minmax(0, 1fr) 20px;
        align-items: start;
        padding: var(--s3) 0;
    }

    .audit-list .row-summary > [role="cell"]:not(.col-what) {
        display: none;
    }

    .audit-list .row-summary > .col-what {
        white-space: normal;
    }

    .audit-list .col-what > :not(.audit-line) {
        display: block;
        overflow: hidden;
        font-size: var(--fs);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .audit-list .audit-line {
        display: block;
        margin-top: var(--s1);
        overflow: hidden;
        color: var(--tx2);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* Кто и когда — тихим мелким, переносится: длинное имя не прячет роль (кадр 362:11794). */
    .audit-list .audit-meta {
        font-size: var(--fs-small);
        white-space: normal;
    }

    .audit-details {
        grid-template-columns: minmax(0, 1fr);
    }

    /*
      Строка депозита карточкой (кадр 355:7604): «Мария Соколова» с «…» и остаток справа, ниже тихо «последняя
      операция 05.10». Ячейка клиентки растворяется в сетке строки (`display: contents`), остальные колонки не
      показываются, подписи колонок не нужны (§7).
    */
    .table-deposits tbody tr {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-deposits td {
        text-align: left;
    }

    .table-cards.table-deposits td.col-main {
        display: contents;
    }

    .table-deposits td::before {
        display: none;
    }

    .table-deposits .deposit-title {
        overflow: hidden;
        font-size: var(--fs);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-deposits .deposit-phone-balance {
        display: block;
        font-variant-numeric: tabular-nums;
    }

    .table-deposits .deposit-details {
        display: block;
        grid-column: 1 / -1;
        margin: 0;
    }

    .table-deposits td:not(.col-main) {
        display: none;
    }

    /*
      Операция депозита карточкой (кадр 355:7667): «−4 000 ₽» 500 и «в оплату записи» (у пополнения — «пополнение ·
      перевод») строкой, ниже тихо занятие и «05.10.2026 · Ангелина · комментарий». Колонки даты, способа,
      комментария и автора не показываются — их слова стоят в тихой строке.
    */
    .table-deposit-ops tbody tr {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-items: baseline;
        gap: var(--s1) var(--s3);
    }

    .table-cards.table-deposit-ops td {
        display: block;
        width: auto;
        max-width: none;
        padding: 0;
        text-align: left;
    }

    .table-deposit-ops td::before {
        display: none;
    }

    .table-deposit-ops td.col-when,
    .table-deposit-ops td.col-method,
    .table-deposit-ops td.col-comment,
    .table-deposit-ops td.col-by {
        display: none;
    }

    .table-deposit-ops td.col-sum {
        font-weight: 500;
    }

    .table-deposit-ops td.col-lesson {
        grid-column: 1 / -1;
        overflow: visible;
        white-space: normal;
    }

    .table-deposit-ops .deposit-op-method {
        display: inline;
    }

    .table-deposit-ops .deposit-op-none {
        display: none;
    }

    .table-deposit-ops .deposit-op-lesson {
        display: block;
        overflow: hidden;
        font-size: var(--fs-small);
        color: var(--tx2);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .table-deposit-ops .deposit-op-meta {
        display: block;
        margin-top: 2px;
        overflow-wrap: anywhere;
    }
}

/*
  Календарная сетка недели показывается только на широком экране (решение задачи 09; порог
  поднят правкой ревью 2026-08-12 — на 768 px сетка рисовалась, но колонка дня выходила 58 px
  и в плитке не читалось ничего).

  This screen keeps its own documented threshold (DS §1). With the menu expanded,
  the time line has (layout width − 412) / 7 − 17 px: menu 248, outer padding 52, block padding 40,
  time axis 72, then the tile's border/gap/padding. The time prefix fits from 1074 px (1089 with a
  15-px scrollbar); below that it ends with «…» under DS §10, and the tile's title gives it whole.
  The grid itself remains visible from 1060 px; the default collapsed menu has room for the time.

  Вместе с сеткой уходят переход по неделям и кнопка «Неделя» переключателя. Сам переключатель
  остаётся: узкому окну доступны список и вид «День», и прятать выбор между ними не за чем.
  Скрывается блок недели целиком (`.week-block`) — сетка или строка «занятий нет».
*/
@media (max-width: 1059px) {
    .week-block,
    .week-nav,
    .view-week {
        display: none;
    }

    .week-narrow {
        display: block;
    }
}


/*
  Участницы карточки занятия (задача 07 релиза 0.5.0, кадры 349:5922 / 349:6028) — список с раскрытием
  строки (`.row-list`, задача 21). Доли колонок — по кадру (940 px: имя 250 · хвост 90 · телефон 140 ·
  Телеграм 140 · сумма 84 · оплата 124); телефон, сумма и отметка не уже себя — они не обрезаются,
  имя и ник — «…» (правило ячеек `.row-summary`).
*/
.participants-list {
    --row-columns: minmax(0, 250fr) minmax(0, 90fr) minmax(max-content, 140fr) minmax(0, 140fr)
        minmax(max-content, 84fr) minmax(max-content, 124fr);
}

/* «· нужна ласта школы» за именем — тихим, как в кадре; на телефоне её несёт хвост («Свой + ласта»). */
.participant-fin {
    color: var(--tx2);
}

/* Хвост строки: на ноутбуке «Свой хвост», на телефоне «Свой + ласта» — показывается один из двух. */
.participant-phone {
    display: none;
}

/* Свой хвост — тихим: хвоста школы у записи нет (кадры 349:5922, 349:6028). */
.participant-own {
    color: var(--tx2);
}

/* Места публикации под отметкой съёмки — тихой мелкой строкой, как в кадре 349:5940. */
.participant-fields .consent-places {
    font-size: var(--fs-small);
    color: var(--tx2);
}

/*
  Open row (349:5940): from 1100 px (the two-column threshold, design system §5) the participant fields are on
  the left and the 540 px «Оплата» panel on the right, top-aligned. Narrower, it is one column with the panel
  under the fields at full width, as on the phone (349:6028): below 1100 a 540 px track takes the whole row and
  squeezes the fields column to 0–144 px. Without the panel the fields take the full width.
*/
.participant-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--s3);
}

.participant-body > :only-child {
    grid-column: 1 / -1;
}

@media (min-width: 1100px) {
    .participant-body {
        grid-template-columns: minmax(0, 1fr) minmax(0, 540px);
        gap: var(--s5);
    }
}

/* «VK14 Заменить» — две ссылки одной строкой (кадр: хвост ссылкой в карточку хвоста, рядом — замена). */
.participant-tail {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s2);
}

/* Панель «Оплата» — рамка без тени внутри белой раскрытой строки (кадр «Оплата (4.3)», 349:5965). */
.payment-panel {
    padding: var(--s4);
    border: 1px solid var(--line-soft);
    border-radius: var(--r-m);
}

.payment-panel-title {
    margin: 0 0 var(--s3);
    font-size: var(--fs-h2);
    font-weight: 600;
}

/*
  Части оплаты (задача 09 релиза 0.5.0; кадры 349:5922, 349:6099, 349:6112). Строка части — «сумма 144 ·
  способ» (в правке — ещё дата и ✕) одной линией; «+ ещё часть» слева, кнопки справа на той же строке
  (§3, А2). Скрытое атрибутом `hidden` прячется явно: `display` правил ниже перебил бы его.
*/
.payment-form {
    margin: 0;
}

.payment-form[hidden],
.payment-actions[hidden] {
    display: none;
}

.payment-parts {
    display: grid;
    gap: var(--s2);
}

.payment-part {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.payment-part .money-control,
.payment-part select,
.payment-part input[type="date"] {
    margin-top: 0;
}

.payment-part .money-field {
    flex: none;
}

/*
  A long method ends with «…» before the arrow, like a list field (component 22:283; QA5-S-02 / QA5-T-02): the
  native list cut «Подарочный сертифика» mid-word. The widths of the row stay as they were.
*/
.payment-part .payment-method {
    flex: 1 1 auto;
    min-width: 0;
    max-width: none;
    text-overflow: ellipsis;
}

.payment-part .payment-date {
    flex: 0 0 136px;
    width: 136px;
}

/*
  Сертификат части (задача 12, кадр 349:5922): список «Код 1…» справа от способа — только при способе
  «Подарочный сертификат»; поле уходит с каждой строкой, чтобы параллельные поля строк не разъехались.
*/
.payment-part .payment-certificate {
    display: none;
    flex: 0 0 110px;
    width: 110px;
    min-width: 0;
    max-width: none;
}

.payment-part:has(.payment-method option[value="GIFT_CERTIFICATE"]:checked) .payment-certificate {
    display: block;
}

/* Without scripts the certificate is this native list: «Код 1081 · …» ends with «…» like the method (owner, 27.09). */
.payment-certificate select {
    text-overflow: ellipsis;
}

/*
  Список с поиском поверх <select> сертификата (ревью релиза 0.5.0, R2-01): поле — во всю ширину своей ячейки,
  без отступа от подписи (подписи нет). На ноутбуке поле узкое (110 px), и раскрытый список — по правому краю
  поля и не уже его, шириной по самой длинной подписи «Код 1081 · Ксения Громова · клиентка Анна Орлова ·
  осталось …», но не шире окна. На телефоне поле во всю строку — список по его ширине, как у любого списка с
  поиском: правым краем к полю он вышел бы за левый край окна (замер снимка 375: x = −11).
*/
.payment-certificate .ss {
    margin-top: 0;
    max-width: none;
}

@media (min-width: 761px) {
    .payment-certificate .ss-list {
        left: auto;
        min-width: 100%;
        width: max-content;
        max-width: min(28rem, calc(100vw - 2rem));
    }
}

/*
  «Изменить» of a paid booking on the laptop (QA5-S-03, owner's decision 27.09): a part paid by certificate puts the
  certificate on a second line, from the left edge of the row to the right edge of the date; the first line is
  amount · method · date · ✕, as in frame 349:6112. Five fields in one row of 506 px left the method 56 px — a single
  «П». The method grows from zero: its own width would push it onto a line of its own. The phone wraps the
  certificate already; the rows without a date keep it beside the method.
*/
@media (min-width: 761px) {
    .payment-part-dated:has(.payment-method option[value="GIFT_CERTIFICATE"]:checked) {
        flex-wrap: wrap;
    }

    .payment-part-dated:has(.payment-method option[value="GIFT_CERTIFICATE"]:checked) .payment-method {
        flex-basis: 0;
    }

    .payment-part-dated .payment-certificate {
        order: 1;
        flex: 0 0 calc(100% - 28px - var(--s2));
        width: auto;
    }
}

/* ✕ у части в правке: отметка «снять», часть уходит по «Сохранить» (о. 3 доп.). */
.part-remove {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 28px;
    min-height: 36px;
    margin: 0;
    padding: 0;
    color: var(--primary);
    background: none;
    border: none;
    cursor: pointer;
}

.part-remove:hover {
    color: var(--accent);
    background: none;
}

.part-remove input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}

.part-remove:has(input:focus-visible) {
    border-radius: var(--r-s);
    box-shadow: 0 0 0 3px var(--focus-ring);
}

.part-remove .field-icon {
    position: static;
    width: 20px;
    height: 20px;
    margin: 0;
    color: currentColor;
}

/* Отмеченная ✕ часть — погашена: снимется по «Сохранить», «Отмена» её вернёт. */
.payment-part:has(.part-remove input:checked) > :not(.part-remove) {
    opacity: .45;
}

.payment-part:has(.part-remove input:checked) .part-remove {
    color: var(--danger);
}

/* «запись станет неоплаченной» — только когда сняты все части и новых нет. */
.payment-unpaid-note {
    display: none;
    margin: var(--s2) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.payment-edit:not(:has(.payment-part-existing .part-remove input:not(:checked))):not(:has(.payment-part-new)) .payment-unpaid-note {
    display: block;
}

.payment-form > .message {
    margin: var(--s3) 0 0;
}

.payment-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2) var(--s3);
    margin-top: var(--s3);
}

.payment-actions button,
.payment-actions .button,
.payment-actions .button-link {
    margin: 0;
}

.payment-actions-start {
    justify-content: flex-start;
}

.payment-actions-end {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--s3);
    margin-left: auto;
}

.payment-edit-hint {
    margin: var(--s2) 0 0;
}

/* Подсказка зачёта — плашка --soft со знаком и кнопкой-текстом (§8, о. 4): «На депозите 5 000 ₽ — зачесть». */
.payment-hint {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 0 0 var(--s3);
    padding: 10px var(--s3);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--primary);
    background: var(--soft);
    border: 1px solid var(--soft-line);
    border-radius: var(--r-s);
}

.payment-hint .field-icon {
    position: static;
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    margin: 0;
    color: currentColor;
}

/* Подсказка сертификата — та же плашка кнопкой целиком (кадр 349:5922): нажатие подставляет часть сертификатом. */
a.payment-hint-gift {
    text-decoration: none;
}

a.payment-hint-gift:hover {
    background: var(--row-hover);
}

.payment-hint > .nav-icon {
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
}

/* Части оплаченной записи — строками, тихим текстом; итоговой строки нет (Н6). */
.payment-lines {
    display: grid;
    gap: var(--s2);
    margin: 0;
    padding: 0;
    font-size: var(--fs-ui);
    color: var(--tx2);
    list-style: none;
}

.payment-lines + .payment-form,
.payment-lines + .payment-form + .payment-actions,
.payment-lines + .payment-actions {
    margin-top: var(--s3);
}

.payment-free-note {
    margin: 0;
    font-size: var(--fs-ui);
    color: var(--tx2);
}

/* «Бесплатно (приглашённая)» и «Снять отметку» — тихой ссылкой под строками. */
.payment-free {
    margin: var(--s1) 0 0;
}

.payment-free .button-link {
    margin: 0;
}

/*
  «История» занятия (задача 09, кадры 349:6131 / 349:6144): строки одна под другой через черту; клиентка
  ссылкой, остальное тихим; «Решить про деньги» — справа (на телефоне — под текстом).
*/
.history-anchor {
    scroll-margin-top: var(--gap);
}

.history-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.history-row {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s3) 0;
    border-top: 1px solid var(--line-soft);
}

.history-row:first-child {
    padding-top: 0;
    border-top: none;
}

.history-text {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    font-size: var(--fs-ui);
    color: var(--tx2);
    overflow-wrap: anywhere;
}

.history-who {
    color: var(--tx2);
}

.history-decide {
    flex: none;
    margin: 0;
}

/*
  Разбор занятия (задача 10 релиза 0.5.0, кадры 351:6871 / 351:6953): те же «Участницы», но первой
  колонкой круг посещения, вместо телефона и Телеграма — «Способ». Доли колонок — по кадру (940 px:
  круг 24 · имя 316 · хвост 90 · сумма 84 · способ 190 · оплата 124); способ — тихим, «…».
*/
.review-list {
    --row-columns: 24px minmax(0, 316fr) minmax(0, 90fr) minmax(max-content, 84fr) minmax(0, 190fr)
        minmax(max-content, 124fr);
}

.review-list .col-method {
    color: var(--tx2);
}

/* Кольцо фокуса круга не обрезается ячейкой: у ячеек строки `overflow: hidden` ради «…». */
.review-list .row-summary > .col-attendance {
    display: flex;
    overflow: visible;
}

/*
  Круг посещения — кнопка без вида кнопки (как почта-кнопка у контактов): 24 × 24, знак цветом отметки.
  Нажатие по кругу строку не раскрывает — у кнопки своё действие, `<summary>` его не перехватывает.
*/
.attendance-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    min-height: 0;
    margin: 0;
    padding: 0;
    color: var(--tx-disabled);
    background: none;
    border: 0;
    border-radius: 50%;
}

button.attendance-mark:hover {
    color: var(--primary);
    background: none;
    border: 0;
}

button.attendance-mark:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.attendance-mark .field-icon {
    position: static;
    width: 24px;
    height: 24px;
    margin: 0;
    color: currentColor;
}

.attendance-none {
    color: var(--tx-disabled);
}

.attendance-yes,
button.attendance-yes:hover {
    color: var(--ok);
}

.attendance-no,
button.attendance-no:hover {
    color: var(--danger);
}

/* «Пришла · Не пришла» в раскрытой строке — сегменты s из кнопок: выбранный — белая плашка (§4). */
.attendance-choice {
    margin: 0;
}

.attendance-segments {
    margin: 0;
}

button.segment-link {
    min-height: 28px;
    margin: 0;
    padding: 0 var(--s3);
    background: none;
    border: 0;
}

button.segment-link:hover {
    color: var(--primary);
    background: none;
    border: 0;
}

.segment-link[aria-pressed="true"] {
    color: var(--primary);
    background: var(--card);
    box-shadow: var(--shadow-block);
}

/* «Отметить всех пришедшими» — действие шапки блока тихой ссылкой (кадр 351:6871); на телефоне — ниже. */
.block-header .review-mark-all {
    margin: 0 0 0 auto;
}

/*
  Подвал разбора (кадр 351:6871): слева «не отмечено у 1» с пустым кругом, справа итог-фраза и «Закрыть
  занятие» заливкой, по центру высоты кнопки.
*/
.review-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3) var(--s4);
    margin-top: var(--s3);
}

.review-unmarked {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    font-size: var(--fs-ui);
    font-weight: 500;
}

.review-unmarked .field-icon {
    position: static;
    width: 16px;
    height: 16px;
    margin: 0;
    color: var(--tx-disabled);
}

.review-footer-end {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--s3) var(--s4);
    margin-left: auto;
}

.review-tally {
    margin: 0;
    color: var(--tx2);
}

.review-tally .figure {
    color: var(--tx);
}

.close-form,
.close-form button {
    margin: 0;
}

/*
  «Кого предупредить» (задача 10, кадр 351:7013): строка человека в ширину — слева кто и чем платила (406),
  в середине деньги (330), последним столбцом по верху «предупредила» (140); у строки без денег левая часть
  занимает и середину. Строки через тонкую черту, как список «Истории».
*/
.warn-list {
    margin: 0;
}

.warn-row {
    display: grid;
    grid-template-columns: minmax(0, 406fr) minmax(0, 330fr) minmax(max-content, 140fr);
    align-items: start;
    gap: var(--s3) var(--s6);
    padding: var(--s3) 0 var(--s4);
    border-top: 1px solid var(--line-soft);
}

.warn-row:first-child {
    border-top: 0;
    padding-top: 0;
}

.warn-row:not(:has(.warn-money)) .warn-who {
    grid-column: 1 / 3;
}

.warn-done {
    grid-column: 3;
    min-height: 36px;
    margin: 0;
    font-size: var(--fs-ui);
}

/* Имя, контакты и зачёты — роль «ui» 14,5 (кадр 351:7013), чем платила — мелким тихим. */
.warn-name {
    margin: 0;
    overflow: hidden;
    font-size: var(--fs-ui);
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.warn-name a {
    color: var(--primary);
}

/* Телефон и Телеграм одной строкой; длинный ник — «…», телефон целиком (кадр 351:7013). */
.warn-contacts {
    display: flex;
    gap: var(--s1) var(--s4);
    margin: var(--s1) 0 0;
    min-width: 0;
    font-size: var(--fs-ui);
}

.warn-contacts > .contact-phone {
    flex: none;
}

.warn-contact {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    max-width: 100%;
}

.warn-contact > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.warn-contact .field-icon {
    position: static;
    flex: none;
    width: 20px;
    height: 20px;
    margin: 0;
    color: var(--primary);
}

.warn-paid {
    margin: var(--s1) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.warn-money {
    display: grid;
    gap: var(--s2);
    min-width: 0;
}

.warn-back {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin: 0;
    font-size: var(--fs-ui);
}

.warn-back .field-icon {
    position: static;
    flex: none;
    width: 20px;
    height: 20px;
    margin: 0;
    color: var(--ok);
}

.warn-fields {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s3);
}

.warn-fields label {
    margin-top: 0;
}

.warn-kept {
    flex: 1 0 100%;
    margin: 0;
    font-size: var(--fs-ui);
    color: var(--tx2);
}

.warn-kept[hidden] {
    display: none;
}

.warn-actions {
    margin-top: var(--s3);
}

/*
  Диалог отмены записи (задача 09; кадры 351:6630 — 351:6839): шапка — клиентка и занятие ссылками,
  «Оплачено: …» мелким; строки денег — подпись слева, поле суммы 144 справа; «+ на депозит» слева и
  «удержано школой» справа на одной строке (§11, А6); итог — плашкой --soft.
*/
.dialog-head + label {
    margin-top: var(--s3);
}

.dialog-who {
    margin: 0;
}

.dialog-note {
    margin: var(--s1) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.cancel-money {
    margin-top: var(--s3);
}

.cancel-money-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
}

.cancel-money-title {
    font-size: var(--fs-small);
    font-weight: 600;
}

.cancel-money-head .button-link {
    margin: 0;
}

.cancel-money-head .button-link[hidden] {
    display: none;
}

.cancel-rows {
    display: grid;
    gap: var(--s2);
    margin-top: var(--s2);
}

.cancel-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
}

.cancel-row[hidden] {
    display: none;
}

.cancel-row-label {
    min-width: 0;
}

.cancel-row-label label {
    margin: 0;
    font-size: var(--fs);
    font-weight: 400;
}

.cancel-row-label .hint {
    margin: 2px 0 0;
}

.cancel-row .money-field {
    flex: none;
}

.cancel-row .money-control {
    margin-top: 0;
}

.cancel-row-links {
    min-height: 36px;
}

/* Сертификат на занятие в отмене — галочкой «вернуть сертификат Код 1094» (задача 12). */
.cancel-row-check {
    justify-content: flex-start;
    min-height: 36px;
}

.cancel-row-check .checkbox {
    margin: 0;
}

.cancel-row-links .button-link {
    margin: 0;
}

.cancel-row-links .button-link[hidden] {
    display: none;
}

.cancel-kept {
    margin-left: auto;
    font-size: var(--fs-ui);
    font-weight: 600;
}

.cancel-kept[hidden] {
    display: none;
}

.cancel-later {
    margin-top: var(--s3);
}

.cancel-later-hint {
    display: none;
    margin: var(--s2) 0 0;
}

/* «Деньги — решу позже»: строки денег гаснут и не нажимаются, под галочкой — что будет (§11). */
.cancel-money:has(.cancel-later input:checked) .cancel-rows,
.cancel-money:has(.cancel-later input:checked) [data-no-refund] {
    opacity: .45;
    pointer-events: none;
}

.cancel-money:has(.cancel-later input:checked) .cancel-later-hint {
    display: block;
}

.cancel-total {
    margin-top: var(--s3);
    padding: var(--s3) var(--s4);
    background: var(--soft);
    border-radius: var(--r-s);
}

.cancel-total[hidden] {
    display: none;
}

.cancel-total-line {
    margin: 0;
    font-size: var(--fs-ui);
    font-weight: 600;
}

.cancel-total-kind {
    margin: 2px 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.cancel-total-kind:empty {
    display: none;
}

.dialog .message {
    margin: var(--s3) 0 0;
}

.cancel-transfer {
    display: inline-block;
    margin-top: var(--s3);
    font-weight: 500;
}

/*
  «Перенести запись» (задача 09, кадры 352:6643 / 352:6676): занятия направления — строками выбора, как
  список хвостов формы записи (§6): рамка --line, выбранное — рамка --primary и фон --soft; скрытое в
  боте — замок и «скрыто в боте» второй строкой.
*/
.transfer-title {
    margin: var(--s4) 0 0;
    font-size: var(--fs-small);
    font-weight: 600;
}

.transfer-targets {
    display: grid;
    gap: var(--s2);
    margin-top: var(--s1);
}

.transfer-target {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px var(--s3);
    padding: var(--s3) var(--s4);
    color: var(--tx2);
    text-decoration: none;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-s);
}

.transfer-target:hover {
    color: var(--tx2);
    border-color: var(--accent);
}

.transfer-target[aria-current="true"] {
    background: var(--soft);
    border-color: var(--primary);
}

.transfer-target-when {
    font-weight: 600;
    color: var(--tx);
    font-variant-numeric: tabular-nums;
}

.transfer-target-hidden {
    display: flex;
    flex-basis: 100%;
    align-items: center;
    gap: var(--s1);
    font-size: var(--fs-small);
}

/* Строки выбора, строка о хвосте и причина — во всю ширину блока (кадр 352:6643: 580 из 620). */
main form.transfer-form textarea {
    max-width: none;
}

/* Описание и причина отмены занятия — тихими строками под фактами сводки. */
.summary-note {
    margin: var(--s1) 0 0;
}

/*
  Итог-фраза под списком — «оплачено: 12 500 ₽» (кадр 349:6024): справа, слова тихим, число — ролью
  «число итог-фразы». Стоит у правого края колонки «Оплата» — левее шеврона строки.
*/
.block-total {
    margin: var(--s3) 0 0;
    padding-right: calc(20px + var(--s3));
    color: var(--tx2);
    text-align: right;
}

.block-total .figure {
    color: var(--tx);
}

/* --- форма записи и «Заменить хвост» (задача 08 релиза 0.5.0; кадры 350:5956 / 350:6006) --- */

/*
  Справка «Занятие»: от 1100 — боковой блок справа от формы, по верху (`.blocks`, Ч3), уже — одна
  строка над формой (кадр 350:6006): занятие ссылкой и «Занято 5 из 8 · свободных хвостов: 12».
*/
.lesson-brief {
    display: none;
}

.lesson-line {
    display: grid;
    gap: var(--s1);
    padding-top: var(--s3);
    padding-bottom: var(--s3);
}

.lesson-line > a,
.lesson-brief-name {
    font-weight: 500;
    color: var(--primary);
    text-decoration: underline;
}

.lesson-line > p {
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--s3);
    margin: 0;
    color: var(--tx2);
}

.lesson-line .figure,
.lesson-brief .figure {
    color: var(--tx);
}

.lesson-brief > p {
    margin: var(--s2) 0 0;
}

.lesson-brief-where {
    color: var(--tx2);
}

.lesson-brief-seats {
    padding-top: var(--s2);
    border-top: 1px solid var(--line-soft);
    color: var(--tx2);
}

.lesson-brief-tails > summary {
    display: flex;
    align-items: center;
    gap: var(--s1);
    margin-top: var(--s2);
    color: var(--tx2);
    cursor: pointer;
    list-style: none;
}

.lesson-brief-tails > summary::-webkit-details-marker {
    display: none;
}

.lesson-brief-tails > summary > .field-icon {
    position: static;
    width: 20px;
    height: 20px;
    margin: 0;
    transform: rotate(90deg);
}

.lesson-brief-tails > p {
    margin: var(--s2) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

@media (min-width: 1100px) {
    .lesson-line {
        display: none;
    }

    .lesson-brief {
        display: block;
    }
}

/*
  Поля формы записи — одним шагом, как в кадре: подпись, под ней поле, группы через --s4. Переключатели
  — во всю колонку блока (§4, А23), поля ввода и список хвостов — в потолок поля (кадр: 560 и 520).
*/
/* Первое поле — сразу под краем блока (первым ребёнком формы стоит скрытое поле защиты от подделки). */
.booking-entry > .client-pick > label:first-child {
    margin-top: 0;
}

.booking-entry fieldset {
    margin-top: var(--s4);
}

.booking-entry .segmented {
    max-width: none;
    margin: var(--s1) 0 0;
}

.booking-entry > .message {
    margin: var(--s4) 0 0;
}

/* «Хвост школы · Свой хвост» — развилка формы: вторая ветка прячется без скриптов (`:has`). */
.booking-entry:has(input[name="tailMode"][value="own"]:checked) .booking-school,
.booking-entry:not(:has(input[name="tailMode"][value="own"]:checked)) .booking-own {
    display: none;
}

/*
  Размер одной строкой — «Взрослый · Детский» и список ступеней или поле роста справа (кадр: 432 + 120
  из 560, на телефоне 215 + 96). Форма записи и форма клиентки. Второе поле прячет переключатель.
*/
.size-pick-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(96px, 21.5%);
    gap: var(--s2);
    align-items: center;
    margin-top: var(--s1);
}

.size-pick-row > .segmented {
    max-width: none;
    margin: 0;
}

main form .size-pick-row > select,
main form .size-pick-row > input {
    max-width: none;
    margin-top: 0;
}

.size-pick:has(input[name="sizeMode"][value="child"]:checked) .size-pick-adult,
.size-pick:not(:has(input[name="sizeMode"][value="child"]:checked)) .size-pick-child {
    display: none;
}

/*
  Клиентка — поле поиска с шевроном, как у списка (кадр: «Поле / список»); найденные — выпадающим
  списком поверх формы; выбранная — строкой «на депозите 8 000 ₽» под полем.
*/
.client-pick {
    position: relative;
}

.client-pick-field {
    position: relative;
    max-width: var(--field-max);
}

main form .client-pick-field > input {
    padding-right: calc(var(--s3) + 20px + var(--s2));
    /* Long unfocused placeholders and selected names end before the reserved chevron space. */
    text-overflow: ellipsis;
}

.client-pick-field > .field-icon {
    left: auto;
    right: var(--s3);
    width: 20px;
    height: 20px;
    transform: rotate(90deg);
}

.client-pick-note {
    display: flex;
    align-items: center;
    gap: calc(var(--s1) + 2px);
    margin: var(--s2) 0 0;
    font-size: var(--fs-small);
    color: var(--tx2);
}

.client-pick-note > .nav-icon {
    flex: none;
    width: 20px;
    height: 20px;
}

.client-options {
    position: absolute;
    z-index: 20;
    left: 0;
    display: grid;
    gap: var(--s1);
    box-sizing: border-box;
    width: 100%;
    max-width: var(--field-max);
    margin-top: var(--s1);
    padding: var(--s3);
    background: var(--card);
    border-radius: var(--r-s);
    box-shadow: var(--shadow-float);
}

.client-options-empty {
    margin: 0 0 var(--s2);
    color: var(--tx2);
}

.client-options > .button {
    justify-self: start;
    margin: 0;
}

.client-option {
    display: grid;
    gap: 2px;
    padding: var(--s2) var(--s3);
    color: var(--tx);
    text-decoration: none;
    border-radius: var(--r-s);
}

.client-option:hover,
.client-option:focus-visible {
    background: var(--row-hover);
}

.client-option-name {
    font-weight: 500;
}

.client-option-contact,
.client-options-more {
    font-size: var(--fs-small);
    color: var(--tx2);
}

.client-options-more {
    margin: 0;
    padding: var(--s1) var(--s3) 0;
}

/*
  Поиск клиентки без занятия (задача 12): найденные — кружками внутри своей формы, списком в потоке, а не
  выпадающим поверх неё — форма (или окно) растёт вниз, выбранное видно до отправки.
*/
.client-options-pick {
    position: static;
    padding: var(--s2);
    background: var(--card);
    border: 1px solid var(--line-soft);
    box-shadow: none;
}

.client-options-pick .client-option {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--s2);
    margin: 0;
    font-size: var(--fs);
    font-weight: 400;
    cursor: pointer;
}

.client-options-pick .client-option input {
    grid-row: 1 / span 2;
    margin: 3px 0 0;
    accent-color: var(--primary);
}

.client-options-pick .client-option:has(input:checked) {
    background: var(--soft);
}

/*
  Выбор хвоста (§6, з. 8): строка «фото 40 × 40 · код · размер · ласта · пометка» в потолок поля;
  выбранная — рамка --primary и фон --soft. Сам переключатель спрятан, строку выбирает нажатие по ней.
  Пометка на телефоне переносится под код (кадр 350:6006).
*/
.tail-choices {
    display: grid;
    gap: calc(var(--s2) - 2px);
    margin-top: var(--s4);
}

.tail-choices > .message {
    margin: 0;
}

/*
  Подпись списка и «Выгрузить хвосты» справа от неё — одной строкой во всю колонку формы (кадр 350:5956,
  задача 28): ссылка кончается на правом краю полосы «Ласта». На телефоне ссылка уходит под список.
*/
.tail-choices-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    min-height: 36px;
}

/* Тихая ссылка (§3): 14,5 / 500, высота строки подписи — 36, как в кадре, и на телефоне тоже. */
.tail-choices-export {
    flex: 0 0 auto;
    min-height: 36px;
    margin-left: 0;
    padding: var(--s1) 0;
    font-size: var(--fs-ui);
    font-weight: 500;
}

.tail-choices-title {
    margin: 0;
    font-size: var(--fs-small);
    font-weight: 500;
    color: var(--tx2);
}

.tail-choice {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--s3);
    box-sizing: border-box;
    max-width: var(--field-max);
    margin: 0;
    padding: calc(var(--s3) - 2px) calc(var(--s4) - 2px);
    font-size: var(--fs-ui);
    font-weight: 400;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-s);
    cursor: pointer;
}

.tail-choice:hover {
    background: var(--row-hover);
}

.tail-choice:has(input:checked) {
    background: var(--soft);
    border-color: var(--primary);
}

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

.tail-choice > input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
}

.tail-choice > img {
    flex: none;
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: var(--r-s);
}

.tail-choice-text {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px var(--s3);
    min-width: 0;
}

.tail-choice-code {
    font-weight: 500;
}

.tail-choice-meta {
    color: var(--tx2);
}

.tail-choice-hint {
    display: inline-flex;
    align-items: baseline;
    gap: calc(var(--s1) + 2px);
    min-width: 0;
    font-size: var(--fs-small);
    color: var(--warn);
}

.tail-choice-hint > .field-icon {
    position: static;
    flex: none;
    align-self: center;
    width: 16px;
    height: 16px;
    margin: 0;
    color: currentColor;
}

.tail-choice-hint a {
    color: var(--primary);
    text-decoration: underline;
}

.tail-choice-hidden {
    font-size: var(--fs-small);
    color: var(--tx2);
}

/* «Заменить хвост»: чья запись и какой хвост сейчас — первой строкой блока. */
.tail-change-who {
    margin: 0;
    color: var(--tx2);
}

.tail-change-name {
    font-weight: 500;
    color: var(--tx);
}

/* --- телефон --- */

@media (max-width: 760px) {
    :root {
        --pad-x: 12px;
        --main-py: 16px;
        --card-pad: 16px;
        /* Шапка телефона — 60 px (компонент «Шапка / телефон» 13:404): под ней открывается меню. */
        --head-h: 60px;
    }

    /*
      Шапка телефона — три ячейки: «Меню» (три черты) слева, название по центру, «Выйти» справа.
      Крайние колонки равны (1fr), поэтому название стоит по центру экрана, а не по центру
      остатка. Имя вошедшего отсюда убрано (правка владельца 2026-08-08): в узкой шапке оно
      обрезалось до одной буквы и отбирало место у названия — теперь оно первой строкой в меню.
    */
    .app-header {
        display: grid;
        grid-template-columns: 1fr auto 1fr;
        align-items: center;
        gap: var(--s2);
    }

    /*
      Отодвигать бренд не от чего: колонки меню на телефоне нет вовсе. Без этого сброса шапка
      получила бы левое поле в ширину развёрнутого меню и уехала бы за край экрана.
      Сбрасывать обязательно ОБА состояния: класс `nav-collapsed` висит на <html> и на телефоне
      (скрипт в <head> ставит его по ширине окна), а `.nav-collapsed .app-header-nav` весит
      больше одиночного класса и перебивал бы сброс. Замер 2026-08-14: шапка держала левое поле
      в 72 px под панель, которой тут нет, и «AquaGrace» стояло на 30 px правее центра экрана —
      ровно на половину этой лишней колонки.
    */
    .app-header-nav,
    .nav-collapsed .app-header-nav {
        padding-left: var(--pad-x);
        border-bottom: 1px solid var(--line-soft);
    }

    .app-header-nav::after {
        display: none;
    }

    .app-header .brand {
        margin-right: 0;
        justify-self: center;
        gap: var(--s2);
    }

    /* Верх боковой панели на телефоне не показывается: меню тут — раскрывающийся список, а не колонка. */
    .nav-head {
        display: none;
    }

    .app-user {
        justify-self: end;
    }

    .app-user-name {
        display: none;
    }

    /* Телефон: навигация свёрнута в кнопку-меню и раскрывается на всю ширину. */
    .app-body {
        display: block;
    }

    /*
      Колонки меню на телефоне нет вовсе — держать под неё место нечем и незачем. Без сброса
      содержимое получило бы левое поле в ширину развёрнутого меню и уехало бы за край экрана.
    */
    .app-body-nav,
    .nav-collapsed .app-body-nav {
        padding-left: 0;
    }

    /* Первая строка раскрытого меню — чей это аккаунт. */
    .nav-user {
        display: block;
        margin: 0 0 var(--s3);
        padding: 0 var(--s3) var(--s3);
        font-size: var(--fs-h2);
        font-weight: 600;
        border-bottom: 1px solid var(--line-soft);
    }

    /* Пункт меню — по левому краю (так собирается взгляд), но крупнее и выше под палец. */
    .app-nav a {
        padding: var(--s3);
        font-size: var(--fs);
    }

    /* Заголовок группы — тоже цель пальца: не ниже поля ввода (44 px). */
    .nav-group-title {
        min-height: var(--field-h);
        box-sizing: border-box;
    }

    /*
      «Меню» — квадрат 44 × 44 с тремя чертами (компонент «Шапка / телефон» 13:404): цель
      пальца и при этом легче «Выйти» рядом, а не крупнее её (замечание владельца 2026-08-14
      к прежней кнопке-слову). Жмётся по себе, а не растягивается на всю крайнюю колонку сетки.
      Это ссылка, поэтому вид кнопки задан здесь целиком — правило кнопок на неё не действует.
    */
    .nav-button {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        justify-self: start;
        box-sizing: border-box;
        width: var(--field-h);
        height: var(--field-h);
        color: var(--tx2);
        background: var(--card);
        border: 1px solid var(--line);
        border-radius: var(--r-s);
    }

    .nav-button > .field-icon {
        position: static;
        width: 20px;
        height: 20px;
        margin: 0;
        color: inherit;
    }

    .nav-button:hover {
        color: var(--primary);
        background: var(--soft);
        border-color: var(--accent);
    }

    /*
      Меню телефона — панель под шапкой, стоящая относительно окна, а не документа (дефект
      владельца 2026-09-20: пролистал список вниз, нажал «Меню» — панель открылась вверху страницы,
      вне экрана, и нажатие выглядело нерабочим). Шапка прилипает к верху окна, панель — сразу
      под ней, поэтому открывается там, куда смотрят, на любой прокрутке; длинное меню
      прокручивается само, а страница под ним при этом не едет (overscroll-behavior). Высота —
      по видимой области окна (dvh), без запасной — по окну целиком. Свёрнутое состояние
      десктопа на телефон не переносится — там раскрытый список с подписями.

      Открывает её класс `opened` (admin.js) или без скриптов — адрес #app-nav, на который
      ведёт сама кнопка «Меню» (:target): закрывается тогда выбором раздела или «Назад».
    */
    .app-nav,
    .nav-collapsed .app-nav {
        display: none;
        top: var(--head-h);
        right: 0;
        bottom: auto;
        width: auto;
        height: auto;
        max-height: calc(100vh - var(--head-h));
        max-height: calc(100dvh - var(--head-h));
        padding: var(--s3) var(--s2);
        overscroll-behavior: contain;
        border-right: none;
        border-bottom: 1px solid var(--line-soft);
        box-shadow: var(--shadow-float);
    }

    .app-nav.opened,
    .app-nav:target {
        display: flex;
    }

    .nav-collapsed .nav-text {
        display: inline;
    }

    .nav-collapsed .app-nav a {
        justify-content: flex-start;
        margin-bottom: 0;
        padding-left: var(--s3);
        padding-right: var(--s3);
    }

    .nav-collapsed .nav-top + .nav-group,
    .nav-collapsed .nav-group + .nav-group {
        margin-top: var(--s3);
    }

    .nav-collapsed .nav-group-title {
        display: flex;
        height: auto;
        padding: var(--s1) var(--s2) var(--s2) var(--s3);
        pointer-events: auto;
    }

    .nav-collapsed .nav-group-title > * {
        display: block;
    }

    .nav-collapsed .nav-group-title::before {
        content: none;
    }

    /* Группа меню над заглушкой раздела на телефоне не показывается (кадр 362:11679). */
    .breadcrumbs-group {
        display: none;
    }

    /* Та же причина, по которой в шапке нет полного названия: вторая строка в узкой шапке не читается. */
    .brand-panel {
        display: none;
    }

    .toast-stack {
        right: var(--s3);
        left: var(--s3);
        bottom: var(--s3);
        width: auto;
    }

    .centered-page {
        padding: var(--s5) var(--s3);
    }

    .centered-page .card,
    .centered-main .card {
        padding: var(--s6) var(--s5);
    }

    .form-grid {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Форма хвоста на телефоне — одной колонкой в порядке разметки: поля, зона, снимок (359:10574). */
    .tail-form-left,
    .tail-form-right {
        float: none;
        width: auto;
    }

    /*
      Шапка на телефоне (кадры 375; журнал Figma «заголовок рядом с кнопкой переносится»): кнопки справа,
      заголовок переносится по словам; под него кнопки уходят, только когда рядом не встаёт самое
      длинное слово — слово ради кнопок не рвётся. break-word, а не anywhere от main: не занижает
      минимальную ширину до буквы; max-width — слово длиннее экрана рвётся в строке.
    */
    .section-row > h1 {
        flex: 1 1 0;
        max-width: 100%;
        overflow-wrap: break-word;
    }

    /*
      The lesson card is the exception (owner 27.09, `Дизайн-система.md` §3 «Шапка карточки занятия»): its title is
      always on top, a short one too, and «Изменить · Записать» always start the line under it. A single «Вернуть
      в черновик» / «Вернуть в работу» takes its own line below them; the form spans the line, the button inside it
      keeps the width of its words. «Вернуть в расписание» has no `rare-transition` and stays beside «Изменить».
    */
    .lesson-head .section-row > h1 {
        flex-basis: 100%;
    }

    .lesson-head .section-actions > .rare-transition {
        flex-basis: 100%;
    }

    /*
      «Кого предупредить» as frame 351:7129 (owner 27.09: «как в кадре»): the title on top, «Открыть группу» under it
      across the column, to the edges of the white blocks below. The header stands inside the padding of the blocks
      (`.section-header`), so the row of the button takes that padding back — without it the button is 32 px narrower.
    */
    .warn-head .section-row > h1 {
        flex-basis: 100%;
    }

    .warn-head .section-actions {
        flex: 1 0 auto;
        margin: 0 calc(-1 * var(--card-pad));
    }

    .warn-head .section-actions > .button {
        flex: 1 1 auto;
    }

    /*
      На телефоне панель меню идёт во всю ширину под строкой кнопок: прижатая правым краем
      к «Ещё», она уезжала за ЛЕВЫЙ край экрана, где её уже не достать (прокрутки влево
      не бывает). Ширину и оба края панель берёт у строки заголовка — `.section-header`
      становится её содержащим блоком, а сам <details> остаётся обычным элементом строки.
      Растягивать на всю строку сам <details> (`flex: 1 0 100%` на `.more[open]`, правка 7c)
      нельзя: во всю ширину уходит и <summary>, и при раскрытии кнопка «Ещё» перескакивала
      на вторую строку — замер 2026-08-14 дал у неё 124,41 / 155,75 закрытой против
      12 / 206,05 раскрытой. Кнопка обязана оставаться на месте, ехать вправе только панель.
    */
    .section-header {
        position: relative;
    }

    .more {
        position: static;
    }

    .more-body {
        left: 0;
        right: 0;
        width: auto;
        max-width: none;
    }

    /*
      На телефоне подпись стоит над значением (кадры 375 — «Поле карточки», layout=stack,
      например 356:7720): в колонке шириной с экран подпись в 112 px и значение рядом делили
      строку пополам, и длинное значение вставало столбиком из одного-двух слов.
    */
    .fields {
        grid-template-columns: minmax(0, 1fr);
        gap: 0;
    }

    .fields dd {
        margin: var(--s1) 0 0;
    }

    .fields dd + dt {
        margin-top: var(--s5);
    }

    /* Две колонки «Информации» — одна под другой: один список полей (кадры 356:7720, 357:8921). */
    .fields-columns {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Кнопка s в строке и раскрытой строке на телефоне — снова m во всю ширину: под палец. */
    .button-s {
        min-height: var(--field-h);
        width: 100%;
    }

    .button-link.button-s {
        width: auto;
    }

    .figure {
        font-size: 18px;
    }

    /* Сводка сущности на телефоне — столбиком, без вертикальной черты (106:7200). */
    .summary {
        flex-direction: column;
        align-items: flex-start;
        gap: 10px;
    }

    .summary-divider {
        display: none;
    }

    .summary-actions {
        margin-left: 0;
    }

    /* Раскрытый факт на телефоне — строкой под фактом в потоке: плашка поверх уезжала бы за край. */
    .summary-fact {
        max-width: 100%;
    }

    .summary-fact > :not(summary) {
        position: static;
        padding: var(--s1) 0 0;
        white-space: normal;
        background: none;
        box-shadow: none;
    }

    /* «Служебное» на телефоне: даты столбиком, под ними кнопка или причина у правого края (126:12634). */
    .service-block {
        flex-direction: column;
        align-items: stretch;
        gap: var(--s3);
    }

    .service-dates {
        flex-direction: column;
        align-items: flex-start;
        gap: 6px;
    }

    .service-dates > *:not(:last-child)::after {
        display: none;
    }

    .service-obstacle,
    .service-cancel,
    .service-delete {
        align-self: flex-end;
    }

    /*
      Полоса фильтров на телефоне (95:4977): сверху поиск, под ним две белые кнопки пополам —
      поле периода (пишет выбранное) и «Фильтры», остальные поля под ними. Строки полосы
      расходятся в одну сетку, место каждого поля задаёт порядок. Со скриптом остальные поля
      свёрнуты за «Фильтры» (`filters-collapsed`); без скриптов кнопки нет и всё раскрыто.
    */
    form.filters-rows {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    .bookings-filters {
        position: relative;
    }

    .bookings-filters > .field-icon {
        display: block;
        top: calc((var(--field-h) - 16px) / 2);
        bottom: auto;
        width: 16px;
        height: 16px;
        margin: 0;
    }

    .bookings-filters #search {
        padding-left: calc(var(--s3) + 16px + var(--s2));
    }

    .filters-row,
    .filters-rows .period-field {
        display: contents;
    }

    .filters-row > *,
    .filters-rows > .filters-apply,
    .filters-rows .period-field > * {
        grid-column: 1 / -1;
        order: 3;
    }

    .filters-row > .filters-wide {
        order: 0;
    }

    .filters-rows .period-field > select {
        grid-column: auto;
        order: 1;
        padding: 0 var(--s2);
        font-weight: 500;
        color: var(--primary);
        text-align: center;
        text-align-last: center;
        appearance: none;
        background-image: none;
    }

    /*
      Блок списка на телефоне: полоса фильтров стоит на фоне над белой панелью строк, а не внутри
      неё (кадры 352:7027, 356:7678); на ноутбуке полоса и список — одна панель (352:6905, 356:7639).
      Блок фильтров каталога хвостов на телефоне — так же на фоне (358:9936): панели строк у него нет,
      карточки хвостов стоят под ним сами.
    */
    .block-list,
    .tail-filters {
        padding: 0;
        background: none;
        box-shadow: none;
        border-radius: 0;
    }

    /* Каталог хвостов на телефоне: от полосы до карточек и между карточками — 12, как в кадре 358:9936. */
    .tail-filters {
        margin-bottom: var(--s3);
    }

    .tail-grid {
        gap: var(--s3);
    }

    .block-list > .filters-rows {
        margin-bottom: var(--gap);
    }

    .block-list > .block-list-body {
        padding: var(--card-pad);
        background: var(--card);
        box-shadow: var(--shadow-block);
        border-radius: var(--r-l);
    }

    /* A narrow footer: the range and «На странице» on one line, the page links under them. */
    .list-footer .pagination {
        flex-basis: 100%;
    }

    /* Empty deposits (§5): the same white panel under the filter bar as the rows. */
    .block-list > .deposit-empty {
        padding: var(--s6) var(--card-pad);
        background: var(--card);
        box-shadow: var(--shadow-block);
        border-radius: var(--r-l);
    }

    /* The other nine empty lists use that panel too (owner's decision). */
    .block-list > .block-empty {
        padding: var(--s6) var(--card-pad);
        background: var(--card);
        box-shadow: var(--shadow-block);
        border-radius: var(--r-l);
    }

    .filters-rows .filters-toggle:not([hidden]) {
        display: inline-flex;
        grid-column: auto;
        order: 2;
        margin: 0;
    }

    /*
      «Выгрузить хвосты» каталога на телефоне (кадр 358:9936, Ч2): белая кнопка справа от «Фильтров», обе
      пополам; кнопка шапки уходит. Без скриптов «Фильтров» нет — кнопка во всю ширину.
    */
    .filters-rows .filters-export {
        display: inline-flex;
        grid-column: auto;
        order: 2;
        margin: 0;
    }

    .filters-rows .filters-toggle[hidden] ~ .filters-export {
        grid-column: 1 / -1;
    }

    .section-actions .tails-export {
        display: none;
    }

    /* Форма записи на телефоне (кадр 350:6006): подпись над списком, «Выгрузить хвосты» — под ним. */
    .tail-choices-head {
        display: contents;
    }

    .tail-choices-export {
        order: 1;
        justify-self: start;
    }

    .filters-collapsed .filters-row > :not(.filters-wide):not(.period-field) {
        display: none;
    }

    /* Сегменты во всю ширину, поровну; фильтр с названием — по длине подписи, остаток поровну. */
    .segmented-s {
        display: grid;
    }

    /* Keep all three completeness labels whole even in a 288px filter bar. */
    .segmented-fit .segment > span {
        padding: var(--s1);
    }

    /* Список с раскрытием строки: колонки телефона, колонки только для широкого экрана не показываются. */
    .row-list-head,
    .row-summary {
        grid-template-columns: var(--row-columns-phone, var(--row-columns, minmax(0, 1fr))) 20px;
    }

    .row-list .col-wide {
        display: none;
    }

    /*
      Участницы на телефоне (кадр 349:6028): «имя · хвост · отметка» одной линией без шапки колонок
      и без шеврона; код хвоста и отметка — по себе, остаток — имени («…»).
    */
    .participants-list .row-list-head {
        display: none;
    }

    /* Отметка — колонкой 118 (кадр 349:6028): коды хвостов разных строк стоят на одной линии. */
    .participants-list .row-summary {
        grid-template-columns: minmax(0, 1fr) max-content minmax(max-content, 118px);
    }

    /* Код хвоста школы на телефоне — жирным: по нему собирают хвосты к занятию (кадр 349:6028). */
    .participants-list .col-tail:not(.participant-own) {
        font-weight: 600;
    }

    .participants-list .row-summary > .field-icon,
    .participant-fin,
    .participant-wide {
        display: none;
    }

    .participant-phone {
        display: inline;
    }

    /* Поля раскрытой строки — подпись слева, как в кадре; телефон и Телеграм — только здесь (з. 3б). */
    .participant-fields {
        grid-template-columns: minmax(var(--field-label-w), max-content) minmax(0, 1fr);
        gap: var(--s2) var(--s3);
    }

    .participant-fields dd {
        margin: 0;
    }

    .participant-fields dd + dt {
        margin-top: 0;
    }

    .participant-fields .participant-phone {
        display: block;
    }

    /*
      «Пришла · Не пришла» goes under its label «Посещение» across the row, the segments equal (§4: an s-size
      switch on the phone takes the full block width). Beside the 112 px label the ≈191 px strip overflows a 320 px
      window. A service account shows a word instead of the switch; that word stays beside the label.
    */
    .participant-fields .attendance-choice:has(.attendance-segments) {
        grid-column: 1 / -1;
    }

    .participant-body {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--s3);
    }

    .payment-panel {
        padding: var(--s3);
    }

    /*
      Панель «Оплата» на телефоне (кадр 349:6028): «+ ещё часть» своей строкой, главная кнопка — во всю
      ширину под ней; в правке дата и ✕ уходят под сумму и способ, «Отмена» — под «Сохранить».
    */
    .payment-actions {
        flex-direction: column;
        align-items: stretch;
    }

    .payment-actions > .button-link {
        align-self: flex-start;
    }

    .payment-actions > button {
        width: 100%;
    }

    .payment-actions-end {
        flex-direction: column-reverse;
        align-items: stretch;
        margin-left: 0;
    }

    .payment-actions-end > .button-link {
        align-self: center;
    }

    .payment-part-dated {
        flex-wrap: wrap;
    }

    /*
      Сертификат части на телефоне — второй строкой во всю ширину: «Код 1…» рядом с суммой и способом не влезает;
      сумма и способ остаются первой строкой. Строки других способов не переносятся.
    */
    .payment-part:has(.payment-method option[value="GIFT_CERTIFICATE"]:checked) {
        flex-wrap: wrap;
    }

    .payment-part:has(.payment-method option[value="GIFT_CERTIFICATE"]:checked) .payment-method {
        flex-basis: calc(100% - 144px - var(--s2));
    }

    /*
      Строка части не шире панели: у списка сертификатов подписи длинные («Код 1081 · Анна Орлова · осталось …»),
      и без нулевого минимума строка сетки частей растягивалась по самой длинной из них — боковая прокрутка на 375.
    */
    .payment-part {
        min-width: 0;
    }

    .payment-part .payment-certificate {
        flex: 1 1 100%;
        width: auto;
    }

    .gift-form {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Отбор списка на телефоне — сегменты по длине подписи (кадр 354:7663): «Использованные» шире «Все». */
    .gift-states {
        grid-auto-columns: auto;
    }

    .gift-form > .form-left,
    .gift-form > .form-right {
        grid-column: auto;
    }

    /* The first pair stacks on the phone, so fin follows purpose with the same label offset. */
    .gift-form > fieldset.form-cell:first-child + fieldset.form-right {
        margin-top: var(--s4);
    }

    .payment-part-dated .payment-method {
        flex-basis: calc(100% - 144px - var(--s2));
    }

    .payment-part-dated .payment-date {
        flex: 1 1 auto;
        width: auto;
    }

    /* «История» на телефоне (349:6144): имя первой строкой, без тире; «Решить про деньги» — под текстом, по себе. */
    .history-row {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--s2);
    }

    .history-who {
        display: block;
    }

    .history-dash {
        display: none;
    }

    .history-row .history-decide {
        width: auto;
    }

    /* Строка выбора занятия в переносе — две строки: когда, затем что (352:6676). */
    .transfer-target {
        flex-direction: column;
    }

    .transfer-target-hidden {
        flex-basis: auto;
    }

    .block-total {
        padding-right: 0;
    }

    /* Переключатель вида на телефоне — во всю ширину, сегменты поровну (кадр 348:4968). */
    .view-switch .segmented-links {
        display: grid;
        flex: 1 1 auto;
    }

    /* Фильтр состояния справочника на телефоне — тоже во всю ширину, на фоне над строками (356:7678, §4). */
    .block-list > .segmented-links {
        display: grid;
    }

    .day-title-long {
        display: none;
    }

    .day-title-short {
        display: inline;
    }

    /* Строка дня на телефоне: «время · отметка», под ними название и подробности (кадр 348:5078). */
    .day-lesson {
        grid-template-columns: minmax(0, 1fr) auto;
        gap: var(--s1) var(--s3);
    }

    .day-lesson-state {
        grid-column: 2;
        grid-row: 1;
    }

    .day-lesson-main {
        grid-column: 1 / -1;
    }

    .day-lesson-name {
        font-size: var(--fs);
    }

    .day-list > li:first-child > .day-lesson {
        border-top: none;
    }

    /*
      Колонки формы на телефоне — одна под другой: первое поле второй колонки отходит от последнего
      поля первой, как любое поле формы. Пустая первая колонка отступа не даёт — поле стоит сразу
      под шапкой блока (так стояла «Цена в боте» до «Занятия в боте» задачи 13).
    */
    .form-col + .form-col > :first-child > label:first-child,
    .form-col + .form-col > label:first-child,
    .form-col:not(:empty) + .form-col > fieldset:first-child {
        margin-top: var(--s4);
    }

    /*
      Согласия на телефоне (353:7241): название и дата — строкой, переключатель — во всю ширину под ними.
      Столбцы названия и даты — без промежутка, как в кадре (253 + 66 = 319): дата прижата вправо, и
      на 375 «Обработка персональных данных» (249 px) встаёт в одну строку, а не переносится.
    */
    .consent-list {
        grid-template-columns: minmax(0, 1fr) max-content;
        column-gap: 0;
    }

    .consent-row > .segmented {
        grid-column: 1 / -1;
    }

    .photo-places {
        flex-direction: column;
    }

    /* Окно на телефоне — лист снизу во всю ширину; главная кнопка сверху во всю ширину, «Отмена» под ней (351:6734). */
    .modal {
        box-sizing: border-box;
        width: 100%;
        max-width: 100%;
        max-height: 90vh;
        margin: auto 0 0;
        padding: var(--s5) var(--card-pad);
        overflow-y: auto;
        border-radius: var(--r-l) var(--r-l) 0 0;
    }

    .modal-actions,
    .dialog-actions {
        flex-direction: column-reverse;
        align-items: stretch;
    }

    .modal-actions .button-link,
    .dialog-actions .button-link {
        align-self: center;
        margin: 0;
    }

    .dialog-pair {
        grid-template-columns: minmax(0, 1fr);
        gap: 0;
    }

    /* Форма записи на телефоне (350:6006): «Записать» во всю ширину, «Отмена» под ней; «Создать карточку
       клиентки» — во всю ширину (350:6229). */
    .booking-entry .form-actions {
        flex-direction: column;
        align-items: stretch;
    }

    .booking-entry .form-actions .button-link {
        align-self: center;
        margin: 0;
    }

    .client-options > .button {
        justify-self: stretch;
    }

    /*
      Разбор на телефоне (кадр 351:6953): «круг · имя · хвост · отметка» одной линией; «Отметить всех
      пришедшими» — белой кнопкой m во всю ширину над списком (Ч2); итог — справа, «Закрыть занятие» — во
      всю ширину под ним.
    */
    .review-list .row-summary {
        grid-template-columns: 24px minmax(0, 1fr) max-content minmax(max-content, 118px);
    }

    .block-header .review-mark-all {
        flex: 1 0 100%;
        order: 3;
        min-height: var(--field-h);
        margin: var(--s2) 0 0;
        padding: var(--btn-py) var(--s4);
        color: var(--primary);
        text-decoration: none;
        background: var(--card);
        border: 1px solid var(--line);
        border-radius: var(--r-s);
    }

    .review-footer-end {
        width: 100%;
    }

    /* Only the footer of the review: «Закрыть» in the header of a lesson is a button by its content (owner 27.09). */
    .review-footer-end > .close-form {
        width: 100%;
    }

    .review-footer-end > .close-form button {
        width: 100%;
    }

    /* Отметка состояния в сводке занятия на телефоне — своей строкой над фактами (кадр 351:6953). */
    .summary-facts > .badge {
        display: flex;
        width: max-content;
        margin: 0 0 var(--s1);
    }

    /* «Кого предупредить» на телефоне (кадр 351:7129) — столбиком: кто, деньги, «предупредила»; длинный ник
       переносится своей строкой. */
    .warn-contacts {
        flex-wrap: wrap;
    }

    .warn-name {
        white-space: normal;
    }

    .warn-row {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--s2);
    }

    .warn-row:not(:has(.warn-money)) .warn-who,
    .warn-done {
        grid-column: auto;
    }

    .warn-actions button {
        width: 100%;
    }

    /*
      Карточка клиентки на телефоне (кадр 353:6825): ФИО — только в заголовке; итог — в две строки, депозит —
      под чертой во всю ширину; у согласий — без даты, отметка у правого края.
    */
    .client-name-field,
    .client-same-phone-gap {
        display: none;
    }

    .fields dd.client-name-field + dt {
        margin-top: 0;
    }

    .fields dd.client-same-phone {
        margin-top: var(--s3);
    }

    .client-total > span {
        display: block;
    }

    .client-total > .client-total-sep {
        display: none;
    }

    /*
      Client facts on the phone follow frame 353:6834: at least 32 high, 6 between rows. A very long nickname can
      still grow.
    */
    .client-facts {
        row-gap: 6px;
    }

    .client-fact {
        min-height: 32px;
    }

    .client-total-row {
        flex-direction: column;
        align-items: stretch;
        gap: var(--s3);
    }

    .client-deposit {
        padding-top: var(--s3);
        border-top: 1px solid var(--line-soft);
    }

    .consent-mark {
        grid-template-columns: minmax(0, 1fr) auto;
    }

    .consent-mark-date {
        display: none;
    }

    /* The moment of the mark — at the right end of the author line (owner's answer 27.09, «предлагаю»). */
    .consent-mark-when {
        display: inline;
    }

    /* Отбор депозитов на телефоне — во всю ширину, сегменты по длине подписи (§4, кадр 355:7604). */
    .deposit-balances {
        grid-auto-columns: auto;
    }

    /* Сводка депозита на телефоне (кадр 355:7667): клиентка и комментарий — своими строками. */
    .summary-facts .deposit-client,
    .summary-facts .deposit-comment {
        display: block;
    }

    .summary-facts .deposit-comment {
        margin-top: var(--s2);
    }

    .deposit-comment-sep {
        display: none;
    }

    /* A linked gift client takes its own phone line before the facts (frame 354:7736). */
    .summary-facts .gift-client {
        display: block;
        margin-bottom: var(--s2);
    }

    .gift-client-sep {
        display: none;
    }

    /* «Привязать к клиентке» — во всю ширину: «·» за кнопкой не нужен, а в куске без переноса вылез бы за край. */
    .gift-link + .gift-link-sep {
        display: none;
    }

    /* Депозит в карточке клиентки на телефоне — во всю ширину под чертой, шеврон у правого края (353:6825). */
    .client-deposit-link {
        display: flex;
        width: 100%;
    }

    .client-deposit-link > .field-icon:last-child {
        margin-left: auto;
    }

    /*
      «Обзор» на телефоне (кадр 347:5053): сегодняшний день над заголовком остаётся — группы меню здесь нет,
      это дата; подписи короче («18/24», «своб. 4»), строки — в две строки, недели — узкие столбики 20 × 56.
    */
    .overview .breadcrumbs-group {
        display: block;
    }

    .overview .overview-wide {
        display: none;
    }

    .overview .overview-phone {
        display: revert;
    }

    .overview .todo-note {
        font-size: var(--fs-small);
    }

    .todo-item {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "when action" "what what";
    }

    .todo-what {
        display: block;
        grid-area: what;
    }

    .todo-name {
        white-space: normal;
    }

    .todo-where {
        font-size: var(--fs-ui);
    }

    /* «·» ends the name, glued by U+00A0: as the place's ::before it could start the second line. */
    .todo-name::after {
        content: "\00a0·";
        font-weight: 400;
        color: var(--tx2);
    }

    .overview-lesson {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        grid-template-areas: "time main main" "load load mark";
        gap: var(--s2) var(--s3);
        padding: 10px 0;
    }

    .overview-list > li:first-child > .overview-lesson {
        padding-top: 0;
    }

    .overview-lesson-time {
        grid-area: time;
        min-width: 0;
    }

    .overview-lesson-main {
        grid-area: main;
    }

    .overview-load {
        display: flex;
        grid-area: load;
        align-items: center;
        gap: var(--s3);
    }

    .overview-load > .overview-bar {
        flex: none;
        width: 96px;
    }

    .overview-lesson-mark {
        grid-area: mark;
    }

    .overview-received {
        flex-flow: row wrap;
        align-items: baseline;
        column-gap: var(--s2);
        margin-bottom: var(--s4);
    }

    .overview-split {
        height: 8px;
        margin-bottom: var(--s4);
    }

    .overview-days {
        gap: var(--s1);
    }

    .overview-day {
        gap: var(--s1);
        padding: 6px 2px;
    }

    .overview-day-today .overview-day-label {
        letter-spacing: 0;
        text-transform: none;
    }

    .overview-column {
        width: 20px;
        height: 56px;
        border-radius: 4px;
    }

    .overview-day-value {
        font-size: var(--fs-small);
    }

    .overview-free {
        grid-template-columns: auto minmax(0, 1fr) auto;
        gap: var(--s3);
        padding: var(--s3) 0;
    }

    /* «Где есть места» на телефоне — три ближайших (кадр 347:5053), дальше ведёт «Все занятия недели →». */
    .overview-week .overview-list > li:nth-child(n + 4) {
        display: none;
    }

    .overview-free-when {
        font-size: var(--fs-small);
    }
}

/* Места публикации под состоянием согласия в раскрытой строке участницы: продолжение ответа, а не отдельное поле. */
.consent-places {
    display: block;
    margin-top: var(--s1);
}
