/* ============================================================
   Админка студии. Mobile-first; палитра приходит из /theme.css.
   Брейкпоинты: базовый — телефон (390px), с 900px — десктоп.
   ============================================================ */

/* Палитра и шрифты приходят отдельным ответом /theme.css — он собирается
   из [theme] в tenants/<slug>.toml (admin/routes/theme.py). Здесь остаётся
   только то, что от вертикали не зависит: ритм, радиусы, тайминги.
   Полупрозрачные слои строятся на автоматических --<токен>-rgb
   (см. admin/routes/theme.py): альфа-слоёв в интерфейсе больше двух десятков,
   перечислять их в конфиге по одному значило бы держать палитру в двух местах.
   Тени берут --shadow-rgb, а не --ink-rgb: в тёмной теме текст светлый,
   и тень на нём превратилась бы в свечение. */
/* ═══ Каркас «Тихая линия» (№34, эталон design-preview/final.html) ═════
   Плоскость держат ЛИНИИ, а не тени: --shadow обнулён, и это не «забыли
   значение», а решение каркаса. Глубину дают обводка, заливка заголовка
   и полоска слева. Кольца фокуса (box-shadow: 0 0 0 Npx) тенями не
   считаются и остаются — это обводка, нарисованная тенью.
   ═══════════════════════════════════════════════════════════════════ */
:root {
  /* ⚠️ Ритм (`--space-*`), радиусы-роли (`--radius-sm/md/lg`), высота
     органов (`--control-h`, `--tap`), `--ease` и `--content-max` живут
     в `/theme.css` (`orvi_admin/routes/theme.py`): их читают и страница
     записи, и публичный сайт, у которых этого файла нет. Здесь остаётся
     только то, что принадлежит админке. */

  /* Штриховка «не рабочее время» — ОДНА точка (№477): её читают сетка
     записей (.dv-col) и график мастеров; вторую копию не заводить. */
  --hatch: repeating-linear-gradient(135deg,
             transparent 0 7px, rgba(var(--ink-rgb), .045) 7px 14px);

  /* Отклик нажатия и наведения: затемнение в серый на ~7%. Один токен,
     чтобы «серость» отклика не разъехалась по файлу оттенками. */
  --press: rgba(var(--ink-rgb), .07);
  /* Запас под ОБВОДКУ выбранного и сфокусированного. Обводка рисуется
     ЗА рамкой элемента (`outline-width` 2 + `outline-offset` 2), и в
     области со своей прокруткой её срезает край: у крайнего образца
     палитры это видно как обрубленную рамку (п.3 седьмого круга №787;
     замер 26.09 — кнопка начинается ровно на границе колонки).
     Контейнеру с прокруткой нужен внутренний запас не меньше этого
     числа; чтобы содержимое при этом не съехало, тем же числом
     он выбирается наружу отрицательным внешним отступом. */
  --ring-reach: 4px;
  /* Затемнение под окнами и боковой панелью (книга: `overlay`). */
  --overlay: rgba(var(--ink-rgb), .45);
  /* Линии: волосяная `--line` делит строки внутри рамки, `--line-strong`
     обводит поля и контурные кнопки. Обе приходят из темы; здесь только
     сокращения, чтобы не писать `1px solid` в сотне правил. */
  --rule: 1px solid var(--line);
  --rule-strong: 1px solid var(--line-strong);
  --raised-glass: rgba(var(--raised-rgb), .92);  /* липкие заголовки */
  /* Теней в системе нет — плоскость держат линии. Единственное исключение
     названо книгой: панель, которая перекрывает страницу. */
  --shadow: none;
  --shadow-panel: 0 8px 32px rgba(var(--ink-rgb), .16);

  --bar-h: 64px;             /* высота нижней панели телефона */
  /* Шапки телефона больше нет (№769): к верху экрана липнут те,
     кто раньше прибивался под неё. Токен остался их адресом. */
  --top-h: env(safe-area-inset-top, 0px);
  --side-w: 240px;           /* ширина борта на десктопе (книга) */

  /* ═══ Лестница слоёв — ОДНО место (№287, решение владельца 03.09).
     Снизу вверх: нижняя панель телефона → окно записи → боковая панель
     со своим скримом → карточка события → модалка → тост → вопрос.
     Новый слой получает СВОЙ токен здесь, а не число в своём правиле —
     иначе следующая правка перевернёт лестницу молча. */
  --z-menu: 40;
  --z-tabbar: 50;
  --z-sheet: 60;
  --z-panel-scrim: 65;
  --z-panel: 70;
  --z-alert: 80;
  --z-success: 120;
  --z-modal: 200;
  /* Выше модалки намеренно: под ней отказ не виден, а сообщить о нём
     больше нечем (случай 12.09 — пять молчаливых 409 подряд). */
  --z-toast: 250;
  --z-ask: 300;
  /* ⚠️ САМЫЙ ВЕРХ — раскрытый список своего выпадающего виджета,
     и место у него здесь не по важности, а по устройству: его кнопка
     может стоять ВНУТРИ ЛЮБОГО слоя снизу — в окне записи (60),
     в боковой панели (70), в модалке (200), в окне вопроса (300).
     Список рисуется в `<body>`, и чтобы быть НАД своей кнопкой, он
     обязан быть над самым верхним из них.

     Боевой дефект 29.09.2026: список стоял на 210 — между модалкой
     и тостом. В окне «Условия сеанса» (`askForm`, слой 300) он
     отрисовывался ПОЗАДИ: пункты просвечивали сквозь модалку, а нажатие
     в пункт попадало в подвал окна (`elementFromPoint` вернул
     `.panel-foot`) — выбрать длительность было нельзя вовсе.

     Плата названа честно: пока список раскрыт, он перекрывает и тост.
     Это принято — список живёт доли секунды и гаснет от любого нажатия
     снаружи, а тост под ним остаётся на экране и дождётся своего часа. */
  --z-select: 310;
  /* Верх окна записи на широком экране — одна константа: первый ряд
     кнопок стоит в одной точке у любой записи. */
  --sheet-top-band: 240px;
}

* { box-sizing: border-box; }

/* Атрибут hidden — это UA-стиль display:none, и любой авторский display его
   перебивает. Дважды из-за этого элемент оставался видимым (модалка продажи,
   меню «Позже»). Universal-правило закрывает класс проблемы: новый скрытый
   элемент прячется по построению, помнить про [hidden] больше не нужно.
   !important здесь оправдан — это не стилизация, а базовая семантика. */
[hidden] { display: none !important; }

/* Гайд на вкладке «Партнёрка»: заголовок раздела обязан читаться как
   НАЧАЛО нового куска, а не как хвост предыдущего абзаца. Принадлежность
   задаёт близость: сверху воздуха заметно больше, чем снизу. У h1–h3
   в проекте `margin: 0`, поэтому без этого правила заголовок прижимался
   к тексту над ним и терял смысл разделителя. Правило локальное —
   общий h3 не трогаем, он живёт на других страницах по своим законам. */
.guide h3 { margin: var(--space-8) 0 var(--space-2); }
.guide h3:first-of-type { margin-top: var(--space-6); }
.guide h3 + p, .guide h3 + ul { margin-top: 0; }
/* Список внутри гайда не должен слипаться со следующим абзацем. */
.guide ul { margin: var(--space-2) 0 0; padding-left: var(--space-4); }
.guide li { margin-bottom: var(--space-1); }

/* Форма, запертая для роли «только просмотр» (№46): поля видны и читаются,
   но не редактируются. `fieldset[disabled]` выключает всё внутри одним
   тегом — перечислять поля поштучно значило бы забыть новое при первой
   же правке формы.

   `display: contents` обязателен: без него обёртка становится собственным
   блоком и ломает раскладку формы, ради сохранности которой всё и затеяно.
   Приглушение — тем же приёмом, что у остальных неактивных элементов. */
/* Группа полей с подписью (`<fieldset><legend>`) — браузерная рамка
   снимается: в системе группу называет `caps`-заголовок, а рамок
   вокруг полей нет (книга, Field). */
fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
fieldset > legend {
  padding: 0; margin-bottom: var(--space-2);
  font-size: var(--text-caps); line-height: var(--line-caps); font-weight: 500;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink2);
}
fieldset.ro-lock { display: contents; }
fieldset.ro-lock :disabled { opacity: .75; cursor: default; }

/* ⚠️ **Место под полосу прокрутки резервируется ВСЕГДА** — иначе появление
   полосы меняет ширину содержимого, и вся центрированная раскладка
   уезжает влево. Владелец поймал это 28.08 в Opera: выбор записи
   открывает панель действий (+283px высоты), страница перестаёт влезать
   в экран, полоса появляется — и сетка дёргается влево, а при закрытии
   возвращается.

   Дефект зависит от высоты окна: страница без панели 1210px, с панелью
   1493px, значит дрожь живёт при высоте окна между ними — замер даёт
   сдвиг на 1300 и 1400, и не даёт на 800, 1000, 1200 (полоса там уже
   есть) и на 1600 (панель влезает). Отсюда «то есть, то нет».

   Гипотеза «панель меняет ширину сетки» тем же замером ОПРОВЕРГНУТА:
   ширина сетки 1132.00 во всех состояниях и на всех высотах; двигается
   только левый край, 262 → 254.5, ровно когда полоса появляется.

   На корне, а не на `body`: полосу страницы рисует корневой элемент,
   и резерв на `body` её не касается. Решение владельца — чинить во всей
   админке, а не на одном экране: то же самое случается везде, где
   содержимое подрастает (списки, фильтры, раскрывающиеся блоки).

   ⚠️ Дрожь ДОКАЗЫВАЕТСЯ браузерным швом — см.
   `tests/browser/test_scrollbar_gutter.py`. Здесь раньше стояло
   «швом это не доказывается, в headless полоса наложенная»: неверно,
   полоса нулевая потому, что playwright передаёт `--hide-scrollbars`,
   и со снятым аргументом дефект виден в пикселях. Граница была
   объявлена без попытки её перейти.

   ⚠️ Известный побочный эффект: жёлоб сужает область просмотра, и слои
   `position: fixed` (фон-подложка `body::before`, модалки, всплывашка
   успеха) идут до 1425 при окне 1440 — у правого края остаётся светлая
   лента 15px. Видно на короткой странице, без всякой модалки. Заведено
   пунктом №373; попытка закрыть её `width: 100vw` ломала больше,
   чем чинила, и откачена. */
/* overscroll-behavior-y на корне — решение владельца 31.08 (разведка
   №375): в запасной ветке ниже overflow корня перестаёт быть `visible`,
   и тогда то же свойство на body (Chromium) перестаёт распространяться
   на весь вьюпорт. Дублируется на body ради явности — конфликта нет,
   свойство аддитивное. */
/* ⚠️ `both-edges`, а не просто `stable` (29.09.2026). `stable` резервирует
   жёлоб под полосу ТОЛЬКО СПРАВА — и весь экран оказывается сдвинут влево
   на его ширину: замер на 390 дал у любой страницы (не только «Диалогов»)
   `.wrap` шириной 380 при окне 390, то есть слева 16px, справа 26.
   Видно это на всём, но заметили на ленте переписки, где симметрия левого
   и правого — сама суть раскладки: входящие слева, исходящие справа.
   `both-edges` резервирует поровну с обеих сторон, и стороны совпадают
   при любом виде полосы; там, где полоса накладная (телефон, macOS),
   жёлоб не резервируется вовсе и цена правки нулевая.
   Прежняя причина `stable` в силе: страница не должна прыгать, когда
   полоса появляется. Запасной путь для старых браузеров — ниже. */
html { scroll-behavior: smooth; scrollbar-gutter: stable both-edges; overscroll-behavior-y: contain; }
/* Своя реакция на касание (ниже, по кнопкам и плашкам) заменяет системную
   вспышку браузера — решение владельца 05.09: синяя подсветка на телефоне
   читалась как чужой, не наш отклик. */
html { -webkit-tap-highlight-color: transparent; }

/* ------------------------------------------------ волна касания (ripple)
   Затемнения фона на `:active` (ниже, по кнопкам) не хватило — палец
   закрывает его собой, а после отрыва отклика не остаётся вовсе (решение
   владельца 05.09, второй заход). Волна рождается в точке касания и живёт
   ПОСЛЕ отпускания пальца: элемент и его позиция и размер ставит app.js
   (делегированный `pointerdown` на документе, одна точка на всю админку —
   `spawnRipple`), здесь только сама анимация и клип по границе кнопки.

   Обёртка (`.btn`, `.act`, `.pick`, `.client-line`) получает контекст
   позиционирования и `overflow: hidden` — клип идёт по УЖЕ ЕЁ СОБСТВЕННОМУ
   скруглению, отдельного радиуса для волны не заводим. `.bk` (плашка
   записи) оба свойства несёт с рождения — ради обрезки длинного текста —
   и волне этого достаточно, добавлять нечего. */
.btn, .act, .pick, .line-row {
  position: relative;
  overflow: hidden;
}
.ripple {
  position: absolute;
  border-radius: var(--radius-full);
  background: currentColor;    /* цвет текста самой кнопки — работает и на тёмной, и на белой */
  opacity: .22;
  transform: scale(0);
  pointer-events: none;
  animation: ripple-grow 320ms ease-out forwards;
}
@keyframes ripple-grow {
  to { transform: scale(1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* Волны нет вовсе — решение владельца; затемнение фона остаётся: оно
     на переходе цвета, не на анимации, движения в нём нет. */
  .ripple { display: none; }
}

/* Запасной путь для браузеров без `scrollbar-gutter` (до Chromium 94,
   Firefox 97, Safari 17). Старый способ: полоса рисуется всегда, поэтому
   и жёлоб есть всегда. Не основной — он показывает пустую полосу даже
   на короткой странице, а `stable` только резервирует место. */
@supports not (scrollbar-gutter: stable) {
  html { overflow-y: scroll; overscroll-behavior-y: contain; }
}

/* Жёлоб `scrollbar-gutter` красится ТОЛЬКО `background-color` (замер
   01.09, №373/№374): картинка, градиент и тень в резерв под полосу
   прокрутки не попадают ни при каком слое. С перерисовкой (№769) это
   перестало быть заботой — фоновых подложек в системе нет вовсе, и
   жёлоб получает тот же плоский `--page`, что страница рядом с ним.
   Ветки `:has()` под скрим, галочку успеха и открытую панель «Ещё»
   сняты той же правкой: они подгоняли цвет жёлоба под картинку,
   которой больше нет.
   ⚠️ Остаётся одно расхождение, и оно принятое: полупрозрачный скрим
   поверх страницы жёлоб не накрывает. Лента 15px у правого края
   против затемнённого экрана — цена отказа от `100vw` (запрет
   владельца, №373). */
html { background: var(--page); }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font);
  /* Тело — 15 (№478); поля ввода держат свои 16px сами — порог
     автозума iOS живёт на них, а не на body. */
  font-size: var(--text-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: contain;  /* без случайного pull-to-refresh */
}

/* Подложки под страницей нет: дизайн-система знает `--page` под всем
   и `--raised` в рамках; третьей поверхности и декоративных пятен
   в ней не предусмотрено. */

/* Шкала дизайн-системы: ЧЕТЫРЕ кегля на экране и ДВА веса — 400 и 500.
   Жирнее нет: `700` снят перерисовкой (№769), потому что заголовок
   отличается от текста размером, а не насыщенностью. */
h1, h2, h3, .h {
  font-family: var(--font);
  font-weight: 500;
  letter-spacing: 0;
  margin: 0;
}
/* ⚠️ ЗАГОЛОВКИ НАБРАНЫ ЗАГОЛОВОЧНЫМ ШРИФТОМ ПАРЫ (№788, 26.09.2026).
   Шрифт темы — пара «заголовочный + текстовый»: заголовки, имя студии
   и большие числа берут первый, всё остальное второй.
   ⚠️ Вес — ТОКЕНОМ `--font-head-weight`, а не числом: у пары
   «Славянская вязь» ровно одно начертание, и `font-weight: 500`
   заставил бы браузер ПОДДЕЛАТЬ полужирный (synthetic bold) — контур
   размазывается, и это видно на первом же заголовке. Число приходит
   вместе с парой из каталога ядра. */
h1, h2, h3 {
  font-family: var(--font-head);
  font-weight: var(--font-head-weight);
}
h1 { font-size: var(--text-title); line-height: var(--line-title); letter-spacing: -.01em; }
h2 { font-size: var(--text-section); line-height: var(--line-section); }
h3 { font-size: var(--text-section); line-height: var(--line-section); }

/* Мелкая надпись над заголовком: раздел, дата, номер карточки. */
.eyebrow {
  font-family: var(--font);
  font-size: var(--text-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink2);
}

.accent {
  font-family: var(--font);
  font-style: normal; font-weight: 500;
  font-weight: 500;
  color: var(--ink2);
}

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

/* ------------------------------------------------ логотип */
.logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px; height: 46px;
  border-radius: var(--radius-lg);
  background: var(--ink);
  color: var(--raised);
  font-family: var(--font);
  font-weight: 500;
  font-size: var(--text-control);
  line-height: .92;
  letter-spacing: .08em;
  text-align: center;
  flex-shrink: 0;
}
.logo span { display: block; }

/* ------------------------------------------------ шапка телефона
   СНЯТА перерисовкой (№769): у страницы пять слотов, и первый из них —
   заголовок с главной кнопкой. Полоса с логотипом, девизом и выходом
   стояла над ним, съедала первый экран телефона и повторяла то, что
   и так есть в борте (десктоп) и в листе «Ещё» (телефон). Разметка
   `.top` осталась в `base.html` ради одного: шапка ленты диалога
   прибивается к `--top-h`, и ноль там — её место.
   ⚠️ `--top-h` при этом не обнуляется: лента липнет к верху экрана,
   а не к несуществующей полосе. */
.top { display: none; }

/* ═══ Боковая панель разделов (десктоп) ══════════════════════════════
   Пришла на место двух рядов вкладок в шапке. Тёмная: это единственная
   тёмная поверхность каркаса, и она же граница между «где я» и «что я
   делаю». Цвета — токенами: на вертикали с тёмной темой борт возьмёт
   её собственные значения, а не останется шоколадным.
   ═══════════════════════════════════════════════════════════════════ */
.shell { display: grid; grid-template-columns: 1fr; min-height: 100vh; }
.side { display: none; }              /* на мобиле разделы живут в .tabbar */
.work { min-width: 0; }

@media (min-width: 1000px) {
  .shell { grid-template-columns: var(--side-w) 1fr; }
  /* Каркас без борта (№385): страница ошибки постороннему, у которого
     пользователя в запросе нет. Колонка борта там не рисуется вовсе,
     и без этого правила рабочая область встала бы в неё — узкой полосой
     шириной с меню. */
  .shell-bare { grid-template-columns: 1fr; }
  .side {
    display: flex; flex-direction: column; gap: 2px;
    position: sticky; top: 0; height: 100vh; overflow: auto;
    padding: var(--space-3) var(--space-2);
    padding-top: calc(var(--space-3) + env(safe-area-inset-top));
    background: var(--sidebar);
    border-right: 1px solid rgba(var(--sidebar-ink-rgb), .12);
  }
  .side-brand {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2); margin-bottom: var(--space-2);
    border-bottom: 1px solid rgba(var(--sidebar-ink-rgb), .12);
  }
  .side-brand .logo { width: 34px; height: 34px; margin: 0; }
  .side-brand .nm {
    /* Имя студии — заголовочным шрифтом пары (№788). */
    font-family: var(--font-head); font-weight: var(--font-head-weight);
    font-size: var(--text-num); color: var(--sidebar-ink); letter-spacing: .02em;
  }
  .side-group {
    font-size: var(--text-micro); letter-spacing: .14em; text-transform: uppercase;
    color: rgba(var(--sidebar-ink-rgb), .45);
    padding: var(--space-3) var(--space-2) 2px;
  }
  /* №185, первый ход: шаг пункта 36px вместо 46 — высота 34 и зазор 2
     (`gap` колонки выше). ТОЛЬКО на десктопе: мышь не требует тап-зоны
     44px, а на ≤999px разделы живут в `.tabbar`, и её пальцем нажимают —
     туда это правило не приходит по построению (медиазапрос).
     В 32px не идти (решение владельца): на 1200×800 меню всё равно
     не помещается, и прокрутка там принята как норма. */
  .side a {
    display: flex; align-items: center; gap: 10px;
    /* ⚠️ `flex: none` обязателен: колонка — flex, и при переполнении
       (1200×800 меню не помещается, это принято) пункты СЖИМАЛИСЬ бы
       до своей `min-height`, а нижний, у которого она меньше, — сильнее
       соседей. Замер 05.09: тот же пункт был 35px на 1440 и 28px на 1200.
       Высота пункта не должна зависеть от высоты окна: пусть колонка
       прокручивается, как и решено. */
    flex: none;
    min-height: 34px; padding: 6px var(--space-2);
    border-radius: var(--radius-md);
    color: var(--sidebar-muted);
    text-decoration: none; font-size: var(--text-control);
  }
  .side a:hover { color: var(--sidebar-ink); background: rgba(var(--sidebar-ink-rgb), .08); }
  /* Активный пункт — подложка `--sidebar-active` и светлый текст
     (книга, блок Sidebar). Полоска акцента слева снята: состояние уже
     названо подложкой, второй признак того же лишний. */
  .side a.active {
    background: var(--sidebar-active); color: var(--sidebar-ink); font-weight: 500;
  }
  .side a svg { width: 18px; height: 18px; stroke-width: 1.7; flex: none; }
  .side .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Счётчик непрочитанных — у правого края пункта, а не под текстом */
  .side a .dot { margin-left: auto; position: static; }
  .side-foot { margin-top: auto; padding-top: var(--space-3);
               border-top: 1px solid rgba(var(--sidebar-ink-rgb), .12); }
  /* Внизу борта — одно имя вошедшего (книга, 25.09): роль и «Выйти»
     оттуда убраны, поэтому правил `.side-role` здесь больше нет —
     мёртвый класс не оставляем, он вернулся бы в разметку «по образцу».
     Выравнивание по левому краю — как у пунктов меню; длинное имя
     уходит многоточием. */
  .side-foot a { justify-content: flex-start; }
  .side-me { display: flex; align-items: baseline; gap: 0;
             min-height: 40px; min-width: 0; }
  .side-me .label { font-weight: 500; color: var(--sidebar-ink);
                     flex: 1 1 auto; min-width: 0; }

  /* №185: редкий раздел уезжает ПОД «Выйти» — порядком в колонке, а не
     переносом в разметке. Элемент остаётся в своём цикле (`row2`),
     в `visible_tabs`, в нижней панели и в «Ещё»: правами и составом меню
     он не отличается от соседей, отличается только местом на экране.
     `order` больше нуля ставит его после `.side-foot` (у той `margin-top:
     auto`, поэтому обе группы жмутся к низу); черта — своя, у этого же
     элемента. Мелкий вид — тем же способом, что «Работа дня»: кегль
     и приглушённость, а не отдельный шрифт. */
  .side a.side-under {
    order: 1;
    margin-top: var(--space-2); padding-top: var(--space-2);
    border-top: 1px solid rgba(var(--sidebar-ink-rgb), .12);
    border-radius: 0;
    min-height: 28px;
    font-size: var(--text-meta); color: rgba(var(--sidebar-ink-rgb), .55);
  }
  .side a.side-under svg { width: 16px; height: 16px; }

  /* Шапка на десктопе не нужна: бренд, разделы, профиль и выход —
     всё в борте. Оставлять её значило бы держать две навигации. */
  .wrap { padding: var(--space-6); padding-bottom: var(--space-8);
          max-width: calc(var(--content-max) + var(--space-6) * 2); }
}

/* ------------------------------------------------ контент */
/* Отступ страницы: `--space-4` на телефоне, `--space-6` на десктопе;
   ширина содержимого — `--content-max` 1120px, борт в неё не входит. */
.wrap {
  padding: var(--space-4);
  padding-bottom: calc(var(--bar-h) + var(--space-8) + env(safe-area-inset-bottom));
  max-width: var(--content-max);
  margin: 0 auto;
}
/* Нет борта — нет и нижней панели (№385), а значит нечего под неё
   резервировать: постороннему на телефоне высота `--bar-h` внизу экрана
   осталась бы мёртвой полосой под коротким текстом ошибки. Отбивка
   `--s6` та же, что даёт странице десктоп, где панели нет тоже.
   Селектор из двух классов бьёт базовый `.wrap` специфичностью,
   а не порядком строк. */
.shell-bare .wrap {
  padding-bottom: calc(var(--space-8) + env(safe-area-inset-bottom));
}

/* ═══ СЛОТЫ СТРАНИЦЫ S1–S5 (книга, блок PageSlots) ═══════════════════
   Страница не рисуется, а заполняется: у каждого вида элемента одно
   место, и оно стоит в одних координатах на каждом экране.

     S1 `.ph`    — шапка: заголовок слева, главная кнопка справа.
     S2/S3 `.ctl` — строка управления: главный орган (поиск, листалка
                   даты, «назад») и срез (сегменты); справа, за распоркой
                   `.ctl-sp`, тихие второстепенные органы.
     S4 `.mets`  — ряд метрик, не больше четырёх.
     S5          — содержимое: группы с `.caps` и строки в рамке.

   Высоты слотов постоянны: кнопки в шапке нет — высота та же, заголовок
   не прыгает при переходе между вкладками. На телефоне S2 и S3 — две
   строки (`.ctl` подряд), на десктопе одна.

   ⚠️ Строка управления НЕ ПЕРЕНОСИТСЯ никогда: не помещается —
   второстепенное уходит в «Ещё», а сам ряд скроллится вбок. Отсюда
   `flex-wrap: nowrap` и `flex: none` у всех органов внутри. */
.ph {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--control-h);
  margin-bottom: var(--space-3);
}
.ph > .page-title { flex: 1; min-width: 0; margin: 0; }

/* ⚠️ ЗАПАС ПОД ОБВОДКУ ФОКУСА — у КАЖДОГО ряда управления, а не у одного
   (решение владельца 29.09.2026, распространение приёма из №134).
   Ряд прокручивается, а значит СРЕЗАЕТ всё, что вышло за его коробку:
   `overflow-x: auto` заставляет браузер вычислить и `overflow-y: auto`.
   Обводка выбранного и сфокусированного рисуется ЗА рамкой (`--ring-reach`
   = ширина плюс отступ), и органу, прижатому к краю ряда, её срезает.
   Случай не редкий: за один день к краям прижаты ряды переключателей
   на восьми экранах, а слева у края стоит кнопка «назад».
   Приём: внутренний запас в это число и та же величина наружу
   отрицательным отступом — содержимое остаётся на месте, коробка
   становится шире на запас с каждой стороны.
   ⚠️ Нижний отступ уменьшен на ту же величину: шаг до следующего блока
   обязан остаться `space-3` 12px. Ряды со СВОИМ нижним отступом
   (`.archive-panel`, `.ev-search`, `.sched-nav`, ряды-переключатели)
   вычитают запас у себя — иначе шаг вырастет на него. */
.ctl {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: var(--control-h);
  padding: var(--ring-reach);
  margin: calc(-1 * var(--ring-reach));
  margin-bottom: calc(var(--space-3) - var(--ring-reach));
  min-width: 0; flex-wrap: nowrap;
  overflow-x: auto; scrollbar-width: none;
}
.ctl::-webkit-scrollbar { display: none; }
.ctl > * { flex: none; }
/* Селект в строке управления — ОРГАН: ширина по содержимому, а не 100%
   от общего правила полей ввода (книга, «Текст в тесноте»). */
.ctl select, .ctl .sort-select { width: auto; }
.ctl-sp { flex: 1 1 auto; min-width: 0; }

/* ⚠️ СТРОКА ПЕРЕКЛЮЧАТЕЛЕЙ экрана со списком — прижата ВПРАВО, одна
   на все их виды: срезы со счётчиками («Все абонементы 100»),
   переключатели вида («Месяц / Год», знаки валют), сортировка (книга,
   «Экран со списком: порядок органов», п.4, решение владельца
   29.09.2026). Слева у этой строки пусто намеренно: срез, вставший
   слева, читается как первая строка списка, а справа — как орган.
   Правый край совпадает с правым краем поиска над ней и списка под ней.
   Второго способа поставить переключатели нет — прежние `.plan-bar`
   (тарифы) и растянутый на всю ширину `.seg-three` («Диалоги») сведены
   сюда, иначе экраны расходятся видом, как уже расходились.

   Три вещи, каждая куплена замером:
   • ПЕРЕНОСА нет — ряд подчиняется `flex-wrap: nowrap` от `.ctl`
     («строка управления не переносится никогда», книга, «Слоты
     страницы»). Первая редакция ставила `wrap`, и на 390 меню «ещё»
     у «Клиентов» ушло на вторую строку, оторвавшись от ленты чипов,
     которой оно принадлежит.
   • ДВОЙНОЙ КЛАСС в селекторе детей — не украшение: лента чипов
     «Клиентов», сегменты «Событий» и `.seg-three` «Диалогов» объявляют
     себе `flex: 1` своими правилами НИЖЕ по файлу, и при равной силе
     побеждал бы порядок файла, а не смысл — ряд снова растянулся бы
     во всю ширину.
   • Снимается только РОСТ, сжатие остаётся. Рост и есть «растянут»
     вместо «прижат вправо»; сжатие нужно всем — лента чипов
     прокручивается внутри себя, ряд срезов «Событий» на узком
     ужимается. Запрети сжатие — и они вытолкнут ряд за край экрана
     вместо того, чтобы сжаться. */
.ctl-switches { justify-content: flex-end; }
/* ⚠️ ЗАПАС ПОД ОБВОДКУ ФОКУСА. Ряд управления прокручивается
   (`.ctl { overflow-x: auto }`, а браузер вычисляет тогда и
   `overflow-y: auto`), и всё, что выходит за его коробку, СРЕЗАЕТСЯ.
   Орган этого ряда прижат к его правому краю вплотную — запас справа
   ровно 0, — а обводка выбранного и сфокусированного рисуется ЗА
   рамкой (2px ширины плюс 2px отступа). Её и срезало: на боевой
   «Статистике» при раскрытом списке рамка кнопки выглядела разорванной
   справа, у стрелки (жалоба владельца 29.09.2026; замер показал, что
   список кнопку не перекрывает — между ними 4px, — а фокус на кнопке
   есть и `:focus-visible` совпадает).

   Лечение — домовое, ради него и заведён `--ring-reach`: ряд получает
   внутренний запас в это число и тем же числом выбирается наружу
   отрицательным внешним отступом, чтобы содержимое осталось на месте.
   Нижний отступ уменьшен на ту же величину: шаг до первого блока
   обязан остаться прежним (12px), а коробка ряда стала на 4px ниже.
   Двойной класс в селекторе — чтобы правило пережило собственные
   `margin` у `.clients-bar` и `.dlg-filter`. */
.ctl.ctl-switches {
  padding: var(--ring-reach);
  margin: calc(-1 * var(--ring-reach));
  margin-bottom: calc(var(--space-3) - var(--ring-reach));
}
.ctl.ctl-switches > * { flex: 0 1 auto; min-width: 0; }

/* ⚠️ УЗКАЯ КОЛОНКА — исключение, и оно названо в книге (решение
   владельца 29.09.2026 по снимку с боя). В колонке списка «Диалогов»
   (360px) ряд, прижатый вправо, оставляет слева дыру шириной почти
   с себя — это читается поломкой, а не порядком. Поэтому там ряд
   занимает ширину колонки целиком. Признак ставится РАЗМЕТКОЙ экрана,
   а не угадывается по ширине: «узкая колонка» — решение о раскладке
   экрана, и принимать его в таблице стилей по числу пикселей значило бы
   прятать решение от того, кто читает разметку. */
.ctl.ctl-switches-fill > * { flex: 1 1 auto; }
.ctl-switches-fill .seg-three { width: 100%; }
.ctl + .caps, .ctl + .list, .ctl + .mets, .ctl + .section,
.ph + .caps, .ph + .list, .ph + .mets { margin-top: var(--space-4); }
/* ⚠️ После РЯДА ПЕРЕКЛЮЧАТЕЛЕЙ шаг до первого блока — `space-3` 12px,
   а не 16 (жалоба владельца 29.09.2026 «на „Зарплатах“ зазор больше,
   чем на остальных экранах»; «свести к общему шагу»).

   Почему 12, а не 16. Правило строкой выше даёт 16 только там, где
   за рядом идёт БРАТ-`<section>`; там, где содержимое лежит внутри той же
   секции («Статистика», «Задачи», «Клиенты», «События», «Абонементы»),
   оно не срабатывает вовсе, и шаг там 12. То есть шестью экранами
   из семи система уже живёт на 12, и «общий шаг» — это он. Книга
   поправлена тем же решением: прежняя строка «перед S4 и S5 —
   space-4 16px» описывала раскладку, которой на экранах нет. */
/* ⚠️ Из шага вычтен запас под обводку (`--ring-reach`): ряд получил
   внутренний отступ, и его коробка стала ниже содержимого на эту
   величину. Без вычета шаг до следующей секции вырос бы на неё —
   замер 29.09.2026 дал 16px на «Зарплатах» и «Платежах» против 12
   на остальных, то есть ровно ту разницу, которую перед этим свели. */
.ctl-switches + .section, .ctl-switches + .caps,
.ctl-switches + .list, .ctl-switches + .mets {
  margin-top: calc(var(--space-3) - var(--ring-reach));
}

/* Заголовок страницы: одна строка, многоточие, кегль `title`. */
.page-title {
  margin: 0 0 var(--space-3);
  font-family: var(--font-head); font-weight: var(--font-head-weight);
  font-size: var(--text-title); line-height: var(--line-title);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Поиск — ОДИН И ТОТ ЖЕ элемент на всех экранах: во всю ширину на
   телефоне, 260px на десктопе. Не сжимается соседями и не растягивается
   на свободное место (книга, «Текст в тесноте»). */
.ctl .search, .ctl .search-field { flex: 1 1 auto; min-width: 0; width: auto; }
@media (min-width: 1000px) {
  .ctl .search, .ctl .search-field { width: 260px; flex: none; }
  /* ⚠️ Экран со списком: поиск — ВО ВСЮ ШИРИНУ списка (книга, «Экран
     со списком: порядок органов», решение владельца 29.09.2026). Те же
     левый и правый края, что у списка под ним и у ряда переключателей.
     Это отменяет прежнее «„Диалоги“ — исключение» (28.09.2026): там
     ровно это и было сделано, но для одного экрана, и «Абонементы»
     с «Диалогами» разошлись видом. Теперь признак общий, и его
     получает каждый переделанный экран; 260px остаётся у тех, где
     поиск ещё делит строку с другими органами. */
  .ctl .search.search-wide,
  .ctl .search-field.search-wide { width: auto; flex: 1 1 auto; }
}

/* Разделитель группы — единственное место прописных в системе.
   Отстоит от рамки своего блока на `space-3` 12px (книга, «Сетка
   и отступы»; решение владельца 25.09.2026 — было 8px). Эталон
   «Способы оплаты» показывал 0px: его инлайновый `margin: var(--s4) 0 10px`
   был МЁРТВ — токена `--s4` нет с переименования в `--space-4`, и браузер
   отбрасывал свойство целиком. Таких мёртвых токенов нашлось 46 в 19
   файлах, вычищены тем же заходом. */
.caps {
  margin: 0 0 var(--space-3);
  font-size: var(--text-caps); line-height: var(--line-caps); font-weight: 500;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink2);
}
.caps + .list { margin-top: 0; }

/* Подпись-пояснение блока (книга, «Сетка и отступы»): часть блока, стоит
   сразу под его рамкой строкой `meta` `ink-2`, и межблочный отступ
   считается ОТ ЕЁ КОНЦА. Отсюда малый отступ сверху (`space-2`, строка
   прижата к своей рамке) и полный снизу (`space-6`): иначе она висела бы
   посередине между блоками и читалась как ничья — ровно то, что книга
   запрещает правилом «между блоками текста нет». */
.block-note {
  margin: var(--space-2) 0 var(--space-6);
  font-size: var(--text-meta); line-height: var(--line-meta);
  color: var(--ink2);
}
.block-note:last-child { margin-bottom: 0; }

/* ⚠️ ДВА ВИДА ОБЪЯСНЕНИЯ, и у каждого своё место (книга, решение
   владельца 29.09.2026; до него книга описывала вид органов, но не место
   объяснений — и текст обычного кегля над полями формально ничего
   не нарушал, а читался вторым заголовком):
   * объяснение к ОДНОМУ полю — `.field-hint`, ПОД этим полем;
   * объяснение ко ВСЕМУ блоку — `.block-hint`, сразу под заголовком
     блока и ДО полей. Законный случай: «Личные ставки {мастеров}»
     относится ко всем сразу и подписью под одним полем быть не может.
   Стиль у обоих ОДИН — мелкий приглушённый; разное только место,
   и потому это два имени, а не одно с двумя отступами: имя говорит,
   куда его ставить.
   Порядок и стиль держит страж установки
   `tests/test_block_hints_stand_in_their_place.py`. */
/* ⚠️ НАЗВАНИЕ БЛОКА ЖИВЁТ ВНУТРИ БЛОКА (книга, решение владельца
   29.09.2026). До него на новом экране названия стояли НАД карточками
   серой капителью — между заголовком страницы и первым блоком вырастал
   третий слой, которого нет ни на одном экране настроек («График
   {мастеров}», «Рабочий день», «Отмены»): там заголовок страницы, сразу
   блок. Название — первая строка ВНУТРИ блока, обычным тёмным, как
   подпись поля (`label`): серый в блоке остаётся только пояснению
   под полем (`field-hint`).
   Порядок держит страж установки
   `tests/test_nothing_stands_between_title_and_block.py`. */
.block-name {
  margin: 0 0 var(--space-3);
  font-size: var(--text-body); line-height: var(--line-body);
  font-weight: 500; color: var(--ink);
}
/* Та же подпись, но ПЕРЕД своим блоком: «показаны 1–50 из 344» над
   списком, вступление к операции над её списком. Отступы зеркальны —
   прижата снизу к своей рамке, сверху отбита межблочным. */
.block-note-lead { margin: 0 0 var(--space-2); max-width: 70ch; }
/* Заголовок раздела внутри страницы — `section` 17/24 (книга, «Одна
   шкала»). Разделитель группы в списке — `caps`; когда заголовок
   раздела И есть разделитель, он несёт класс `.caps` явно. */
.section-head > h2 { font-size: var(--text-section); line-height: var(--line-section); }
.section-head > h2.caps { font-size: var(--text-caps); line-height: var(--line-caps);
                          letter-spacing: .06em; text-transform: uppercase;
                          color: var(--ink2); font-weight: 500; }

.list + .caps { margin-top: var(--space-6); }

/* ═══ Ряд метрик (книга, блок MetricCard, правка 24.09) ══════════════
   Подпись `meta` сверху, число `numeral` снизу; до пяти в ряду в одну
   строку на 1280px, на телефоне сетка 2 колонки. Единица — один раз
   в заголовке ряда, а не в каждой карточке. */
.mets {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  /* п.16 (25.09): зазор между ячейками — ОДИН токен сетки на систему,
     `--space-3` 12px. Было 8px у метрик и 12px у карточек в колонке
     («Профиль»), и на соседних экранах одинаковые с виду ячейки стояли
     с разным зазором. Выбран 12px, а не 8px: карточки в колонке уже
     стоят так на всех экранах, и подгонять их под метрики значило бы
     двигать больше, чем меняешь. */
  gap: var(--space-3); margin: 0 0 var(--space-6);
}
@media (min-width: 1000px) { .mets { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (min-width: 1280px) { .mets { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.met {
  background: var(--raised); border: var(--rule); border-radius: var(--radius-lg);
  padding: var(--space-3) var(--space-4); min-width: 0;
}
.met small {
  display: block; font-size: var(--text-meta); line-height: var(--line-meta);
  color: var(--ink2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.met b {
  display: block; margin-top: 2px;
  /* Большое число — заголовочным шрифтом пары (№788): на экране
     это такой же «заголовок», как имя раздела. */
  font-family: var(--font-head); font-weight: var(--font-head-weight);
  font-size: var(--text-num); line-height: var(--line-num);
  font-variant-numeric: tabular-nums;
}

/* Контакты объекта в S2 (книга, «Контакты»): текст `ink`, волосяное
   подчёркивание, при наведении акцент. Ни иконок-кнопок, ни слов
   «позвонить» — подпись это сам контакт. */
.contacts { display: flex; gap: var(--space-3); flex-wrap: wrap;
            font-size: var(--text-control); min-width: 0; }
.contacts a { color: var(--ink); text-decoration: none;
              border-bottom: 1px solid var(--line-strong); }
.contacts a:hover { color: var(--accent); border-color: var(--accent); }
.contacts span { color: var(--ink3); }

.section { margin-bottom: var(--space-6); }
.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-3);
}
/* Вид заголовка — ОДНОЙ точкой, у слота S1 выше (`.page-title`).
   Исключения «Записей» (дата вместо заголовка, №475) больше нет. */
.section-head > .page-title { margin: 0; }
/* Название блока в строке section-head (карточка клиента, №141):
   отступ вниз даёт сам section-head, второй — двоил бы зазор. */
.section-head > .block-name { margin: 0; }
/* ВОЗВРАТ НА РОДИТЕЛЬСКИЙ ЭКРАН — ОДИН КЛАСС `.back` НА ВСЮ АДМИНКУ
   (№769, заход 2; заведён заходом 1, когда выяснилось, что класс
   стилизован только в book.css публичного сайта и ссылка на странице
   задачи стояла голой).

   Было пять исполнений одной мысли: кнопка `.btn-ghost` с инлайновым
   отступом (карточка клиента, сверка дублей, приглашения), голая
   ссылка (страница задачи), ссылка внутри строки-счётчика (архив
   задач), `a.muted` в шапке раздела (карточка партнёра), ссылка
   в карточке-пояснении (тексты писем). Кнопка среди них была самым
   заметным элементом первого экрана — уход назад выглядел главным
   действием страницы, которым он не является.

   ⚠️ СТРЕЛКУ РИСУЕТ КЛАСС, а не текст ссылки: в разметке её писали
   то «←», то «‹», и разнобой возвращался бы при каждой новой
   странице. Вызывающему остаются только слова.

   Что этим классом НЕ является и сведено быть не должно: листание
   периодов («‹» у сетки, графика, календаря), пагинация архива
   («‹ Новее»), возврат на первый уровень внутри окна («‹ Назад»
   в «Доступах») и `.thread-back` «Диалогов» — иконка мессенджера
   (решение владельца, записано в разборе аудита).

   /book не задет: у него свой `<head>` без just.css и свой `.back`. */
.back {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: var(--tap); padding: 6px 0; margin: 0 0 var(--space-2);
  color: var(--ink2); font-size: var(--text-control);
  text-decoration: none;
}
.back::before { content: "←"; font-size: var(--text-body); line-height: 1; }
.back:hover, .back:active { color: var(--ink); }
.muted { color: var(--ink2); font-size: var(--text-control); }
/* «держателя нет — подарок в пути» на карточке абонемента (№211): состояние,
   а не примечание, поэтому не `.muted` — тот на телефоне ужимается до 12.5px
   правилом `.card .muted`, и владелец по скриншоту не смог его прочесть.
   Цвет тот же, размер — основной текст карточки. */
.sub-holder-none { color: var(--ink2); font-size: var(--text-body); }

/* Сомнение в ставке часа (№683): ставка ниже половины самого дешёвого часа
   прайса — почти наверняка при продаже в поле суммы ввели цену часа.
   Своей строкой, а не `.muted`: это состояние абонемента, по которому
   считается каждое списание, а не примечание (тот же довод, что
   у `.sub-holder-none`, №211). Цвет — предупреждение, не отказ. */
.rate-doubt { display: block; margin-top: 2px; color: var(--wait-ink);
              font-size: var(--text-meta); }

/* Подсказка под полем времени: что система поняла из «600», «10ч», «1:30».
   Ответ приходит с сервера — тем же разбором, каким продаётся абонемент.
   Красный вариант означает «не поняли», и он обязан отличаться цветом,
   а не только словами: продавец читает эту строку боковым зрением. */
.time-hint { margin: 6px 0 0; min-height: 1.2em; color: var(--ink2); }
.time-hint.is-bad { color: var(--alert-ink); }
/* Пустое состояние (книга, блок EmptyState): заголовок `section`,
   одна строка `meta` о том, что здесь появится и откуда, кнопка —
   если действие есть. Ни рамки, ни пунктира, ни иллюстрации: пустота —
   состояние страницы, а не событие. Камни-SVG сняты перерисовкой. */
.empty {
  padding: var(--space-6) 0; color: var(--ink2);
  background: none; border: 0;
}

/* ------------------------------------------------ карточки */
.card {
  background: var(--raised);
  border: var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  margin-bottom: var(--space-3);
}
/* `.card.cream` — АКЦЕНТНАЯ ПОВЕРХНОСТЬ, а не исход операции (граница
   проведена №769, заход 2). Ею остаются: ссылка партнёру в бота,
   сводка денег партнёра, карточка подписки в профиле, пояснение
   «как это работает» на «Статистике», карточки абонементов в карточке
   клиента, плашка «клиент в архиве». Все они говорят «смотрите сюда»,
   а не «получилось / не получилось», и живут на экране постоянно. */
.card.cream { background: var(--accent-soft); border-color: var(--accent); }

/* ИСХОД ОПЕРАЦИИ — ОДИН ВИД НА ВСЮ АДМИНКУ (№769, заход 2).
   Было три исполнения одной мысли: `.card.cream` (успех),
   `.login-error` (ошибка — имя от экрана входа, а жил он на восьми
   рабочих экранах) и `.card.warn` (ошибка на «Партнёрке»).
   ⚠️ У последнего правила в CSS не было ВОВСЕ: отказ партнёрского
   раздела рисовался обычной серой карточкой — четвёртый мёртвый
   класс того же рода, что `.btn-quiet` на «Текстах писем». Найден
   аудитом, снят здесь.

   Две роли и ровно два класса: `.note-ok` — получилось, `.note-error` —
   не получилось. Тон берётся у темы вертикали (`--ok-*`/`--danger-*`),
   своих цветов нет. Экран входа остаётся на своём `.login-error`:
   у него свой каркас без борта, и его плашка — часть той страницы. */
.note {
  margin: 0 0 var(--space-3); padding: var(--space-3);
  border: 1px solid; border-radius: var(--radius-lg);
  font-size: var(--text-control);
}
.note-ok { background: var(--ok-bg); border-color: var(--ok-ink); color: var(--ink); }
.note-error { background: var(--alert-bg); border-color: var(--alert-ink); color: var(--alert-ink); }

.card-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-3);
}

/* Код и время в строке — `body-strong` табличными цифрами. Кегль
   `numeral` остался числу метрики: в списке он кричал громче имени
   (книга, «Одна шкала»). */
.slot-time {
  font-family: var(--font);
  font-weight: 500; font-size: var(--text-body); line-height: var(--line-body);
  font-variant-numeric: tabular-nums;
}
.slot-master {
  font-family: var(--font);
  font-style: normal; font-weight: 500; font-size: var(--text-body); color: var(--ink2);
}

/* ------------------------------------------------ чипы статуса
   ЧЕТЫРЕ смысла и ровно четыре пары цвета (книга, блок StatusChip):
   готово `ok`, ждёт `wait`, проблема `alert`, закрыто `done`. Цвета
   приходят из ядра, темой не меняются. Рамки у чипа нет: смысл несут
   заливка и слово, обводка была пятым признаком на той же плашке.

   Имена классов прежние (`.badge-ok` и соседи) — они адреса разметки
   и швов; сведены к четырём смыслам значения, а не имена. */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: 22px; padding: 0 var(--space-2);
  border: 0; border-radius: var(--radius-sm);
  font-size: var(--text-caps); line-height: var(--line-caps); font-weight: 500;
  white-space: nowrap; flex: none;
}
.badge-ok     { background: var(--ok-bg);    color: var(--ok-ink); }
.badge-warn   { background: var(--wait-bg);  color: var(--wait-ink); }
.badge-wait   { background: var(--wait-bg);  color: var(--wait-ink); }
.badge-danger { background: var(--alert-bg); color: var(--alert-ink); }
.badge-done   { background: var(--done-bg);  color: var(--done-ink); }
/* Остаток абонемента — не статус, а значение: подложка выбранного
   и обычные чернила (та же роль, что у «ожидается» в сетке). */
.badge-sub    { background: var(--accent-soft); color: var(--ink); }

/* ------------------------------------------------ кнопки
   ТРИ ВЕСА (книга, блок Button): заливная `accent` — единственное
   главное действие страницы; контурная — второе; тихая — отмена
   и возврат. Высота `--control-h` 36px; тап-зону на телефоне добивает
   внешний отступ ряда, а не рост самой кнопки.

   ⚠️ Заливная красится АКЦЕНТОМ, а не чернилами. До перерисовки (№769)
   главная кнопка была тёмной `--ink`, и выбор акцента в «Внешнем виде»
   на экране почти не участвовал. Пара `accent`/`on-accent` посчитана
   ядром (`build()` проверяет контраст) и читается в любой теме. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  padding: 0 var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  /* Заливка — акцент, доведённый до 4.5:1 с подписью (см. выше);
     у девятнадцати пар каталога из двадцати это тот же акцент байт
     в байт, доводка срабатывает только там, где подпись не читалась. */
  background: var(--accent-btn, var(--accent));
  color: var(--on-accent);
  font-family: var(--font);
  font-size: var(--text-control); font-weight: 500;
  line-height: var(--line-control);
  white-space: nowrap;              /* орган управления не переносится */
  flex: none;                       /* и не сжимается соседями */
  cursor: pointer;
  text-decoration: none;
  touch-action: manipulation;       /* без 300 мс задержки и зума двойным тапом */
  transition: background var(--ease), opacity var(--ease);
}
/* Наведение и нажатие — ШАГ ТОНА, цвет подписи не меняется (правило
   владельца 25.09.2026). Движения нет (решение 05.09).
   ⚠️ Было `color-mix(accent 88%, ink)`: акцент мешался с чернилами,
   то есть ВСЕГДА темнел. Под тёмной подписью (Коралл, Абрикос, Розовое
   золото — три акцента каталога, где белая подпись не читается) это
   топило её насмерть: контраст падал с 3.90 до 1.12, и текст пропадал
   прямо под курсором. Теперь шаг считает ядро и ведёт его В СТОРОНУ
   РОСТА контраста с подписью (`core/theme.py::accent_hover`), а сама
   заливка доведена до 4.5:1 (`--accent-btn`). */
.btn:hover  { background: var(--accent-hover); }
.btn:active { background: var(--accent-hover); filter: brightness(.96);
              transition-duration: .1s; }
/* Погашенная — белая, серый текст, светлая рамка, БЕЗ заливки.
   Прозрачность не годится: полупрозрачная заливка читается как ещё
   один, четвёртый вид. */
.btn[disabled], .btn[disabled]:hover {
  background: var(--raised); color: var(--ink2);
  border-color: var(--line);
  opacity: 1; cursor: not-allowed;
}

/* Контурная — второе действие страницы. */
.btn-ghost {
  background: var(--raised); color: var(--ink); border-color: var(--line-strong);
}
.btn-ghost:hover  { background: var(--press); }
.btn-ghost:active { background: var(--press); transition-duration: .1s; }

/* Тихая — отмена, возврат, редкий второстепенный орган в строке
   управления («Ещё», «Отменить»).
   ⚠️ Класс ВОЗВРАЩЁН перерисовкой (№769). Он был удалён из CSS 05.09
   как «бежевая заливка без роли», но разметка звала его и после — на
   «Текстах писем» три тёмные кнопки стояли подряд, потому что класс
   не находился и падал в базовую `.btn`. Теперь у него есть роль,
   названная книгой, и заливки нет вовсе.
   ⚠️ «Сегодня» СНЯТА отсюда правкой 24.09 (проверка владельца на lab):
   рядом с ней в строке стоит подпись того же кегля («Воскресенье, 21
   сентября»), и тихая с ней визуально сливалась — не читалась как
   кнопка. Теперь она `.btn-ghost`, книга («Сегодня — контурная
   кнопка»). */
.btn-quiet {
  background: none; border-color: transparent; color: var(--ink2);
  padding: 0 var(--space-2);
}
.btn-quiet:hover  { background: var(--press); color: var(--ink); }
.btn-quiet:active { background: var(--press); transition-duration: .1s; }

/* Вида «опасной» кнопки НЕТ (№146, решение владельца 30.09.2026):
   отсутствие цвета как предупреждение не читалось. Главная кнопка окна
   всегда заливная; необратимое отвечает вторым шагом подтверждения
   (`twoStep` у askDialog), а не цветом. Прежний `.btn-danger` снят
   вместе со всеми использованиями. */

/* Кнопка из одной иконки — только поиск, закрыть, «ещё» и стрелки
   листания периода (40×36, книга, блок DayNav). */
.btn-ico {
  width: 40px; min-width: 40px; padding: 0;
  background: var(--raised); color: var(--ink); border-color: var(--line-strong);
}
.btn-ico:hover { background: var(--press); }

/* Размерный модификатор главной кнопки (во всю ширину формы), не вид. */
.btn-hero { width: 100%; min-height: 44px; font-size: var(--text-body); }
/* ⚠️ В ШАПКЕ СТРАНИЦЫ (S1) кнопка своей ширины, а не во всю строку
   (п.12, 25.09). `.btn-hero` задуман во всю ширину для подвала листа
   и формы; попав в шапку, он растягивался на всю её ширину — заголовок
   сжимался в ноль (`flex: 1; min-width: 0`), а правый край кнопки уезжал
   за край содержимого на 16 px (замер: кнопка 313…1385 при рамках блоков
   297…1369, то есть «колонки на одной линии» ломались обе стороны).
   Правило стоит здесь, а не в шаблоне: шапка — общий слот, и следующая
   страница с заливной кнопкой в S1 не должна повторять ту же поломку. */
.ph > .btn { width: auto; flex: none; }

/* Несохранённое — у СВОЕЙ кнопки (№531): признак ставит `initDirtyForms()`
   ровно тем кнопкам, что отправят начатое. Подпись словами, а не только
   цветом: цвет один читают не все. */
.btn.is-dirty { border-color: var(--accent); }
.btn.is-dirty::after {
  content: "есть несохранённое";
  font-size: var(--text-micro);
  opacity: .8;
}

.btn-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.btn-row .btn { flex: 1 1 auto; }

/* ------------------------------------------------ формы */
label { display: block; font-size: var(--text-body); line-height: var(--line-body);
        font-weight: 500; margin-bottom: var(--space-1); }
/* Поля ввода. ⚠️ Флажки и переключатели перекрывают это правило целиком
   (см. ниже) — им противопоказаны и ширина 100%, и высота в тап-зону:
   именно от них флажок вырастал в большой квадрат браузерного вида, чужой
   палитре и несоразмерный подписи. Лечили точечно шесть раз (задачи, права,
   режимы оплаты, слияние, панель графика, тумблер оценок), седьмой раз —
   в модалке мастера — просто забыли, и владелец увидел тот же квадрат.

   Перекрытие сделано СПЕЦИФИЧНОСТЬЮ, а не исключением через `:not()`:
   `input[type="checkbox"]` весит больше, чем `input`, и работает одинаково
   в любом движке. Вариант с `:not()` весит БОЛЬШЕ правила флажка — то есть
   при малейшей ошибке в селекторе побеждает не тот, кто задуман. */
input, select, textarea {
  width: 100%;
  min-height: var(--control-h);
  padding: 0 var(--space-3);
  font-size: 16px;                  /* без автозума на iOS */
  font-family: inherit;
  line-height: var(--line-control);
  color: var(--ink);
  background: var(--raised);
  /* Граница поля — `--line-strong` (книга: у полей контраст 3:1,
     волосяная `--line` делит строки внутри рамки, но не обводит орган). */
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
}
textarea { padding: var(--space-2) var(--space-3); line-height: var(--line-body); }

/* Флажок и переключатель — один вид на всю админку. Размер соразмерен
   подписи (15–16px), цвет из палитры, курсор указателя. Подпись кликабельна
   тем, что элемент лежит ВНУТРИ `<label>` — так и размечаем. */
input[type="checkbox"], input[type="radio"] {
  width: 18px; height: 18px;
  min-height: 0; padding: 0; margin: 0;
  /* Рамку и фон СБРАСЫВАЕМ явно: от правила полей ввода флажку достаются
     `border` и `background`, и с ними браузер рисует его как коробку,
     а не как флажок. Именно эта пара и делала «квадрат». */
  border: 0; background: none;
  flex: none;
  accent-color: var(--accent);
  cursor: pointer;
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled { cursor: not-allowed; }
/* На телефоне флажок крупнее: там по нему попадают пальцем, а не курсором. */
@media (max-width: 899px) {
  input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
}
/* Подсказка в пустом поле и недоступное поле — разные состояния, и серый
   цвет не должен значить оба сразу. Подсказка: приглушённый текст в живом
   поле — рамка, фон и фокус как у обычного. Недоступное: утопленный фон,
   курсор запрета и заметно ниже контраст, то есть отличается НЕ ТОЛЬКО
   цветом текста (иначе разница читается как оттенок, а не как запрет). */
::placeholder { color: var(--ink2); opacity: .65; }
input:disabled, select:disabled, textarea:disabled {
  background: var(--sunken);
  color: var(--ink2);
  opacity: .5;
  cursor: not-allowed;
}
input[readonly], textarea[readonly] {
  background: var(--sunken);
  cursor: default;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.field { margin-bottom: var(--space-3); }
/* Справка под полем: тон спокойный, не предупреждение — она объясняет
   поведение, а не запрещает. Живёт внутри `.field`, поэтому в паре полей
   («Для кого» + ключ) не разъезжается с колонкой, к которой относится. */
.field-hint {
  color: var(--ink2);
  font-size: var(--text-meta);
  line-height: var(--line-meta);
  margin-top: var(--space-1);
}

/* Настройки студии (№202-соседняя мелкая правка, решение владельца 01.09):
   два столбца от 900px — тот же брейкпоинт, что у `.two-fields`, чтобы
   раскладки форм в админке не расходились по мелочи. */
.settings-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 0 var(--space-4);
}
@media (max-width: 899px) { .settings-grid { grid-template-columns: 1fr; } }
/* Заголовок раздела настроек (№686). Во всю ширину сетки — иначе он встанет
   первой колонкой, и настройка справа от него прочтётся как его часть.
   Сверху воздуха больше, чем снизу: заголовок принадлежит тому, что ПОД ним. */
.settings-group {
  grid-column: 1 / -1; margin: var(--space-6) 0 var(--space-2);
  font-size: var(--text-body); text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink2);
}
.settings-group:first-child { margin-top: 0; }
/* Название и число — в одну строку: было под них по отдельной строке
   каждой, и однозначное число получало ту же ширину, что абзац пояснения
   под ним. Пояснение остаётся строкой ПОД парой, во всю ширину колонки. */
/* Поле формы (книга, блок Field): подпись `body-strong` НАД полем,
   поле высотой `--control-h`, подсказка `meta` под полем. Раскладка
   «подпись слева, число справа» (01.09) снята перерисовкой: у длинных
   подписей она давала две колонки разной высоты, а книга держит один
   порядок чтения — что настраиваем, чем настраиваем, что будет. */
.settings-row {
  display: block;
}
.settings-row label {
  display: block; margin-bottom: var(--space-1);
  font-size: var(--text-body); line-height: var(--line-body); font-weight: 500;
}
.field-num { width: 120px; flex: none; }
/* Текстовая настройка (№320): в той же строке, что подпись, но тянется —
   у текста нет «однозначного-трёхзначного» размера, под который годится
   фиксированная ширина числового поля. `width: auto` обязателен: общее
   правило полей ввода даёт 100 %, и в флексе оно вытесняет подпись.
   Флажку своего правила не нужно — `input[type="checkbox"]` уже несёт
   и размер, и `flex: none`. */
.field-text { width: 100%; min-width: 0; }

/* видимый фокус для клавиатуры */
a:focus-visible, button:focus-visible, .tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-md);
}

/* ------------------------------------------------ нижняя навигация (телефон)
   Книга: на телефоне борт становится нижней панелью из ПЯТИ пунктов
   «Работы дня», «Студия» уезжает за пункт «Ещё». Панель светлая —
   `--raised` с линией сверху, активный пункт акцентом (макет
   design/screens.html): тёмная полоса под светлой страницей читалась
   как вторая, чужая поверхность. */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-tabbar);
  display: flex;
  background: var(--raised);
  border-top: var(--rule);
  padding-bottom: env(safe-area-inset-bottom);
}
.tab {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px;
  min-height: var(--bar-h);
  padding: var(--space-2) 2px;
  color: var(--ink2);
  text-decoration: none;
  font-size: var(--text-micro); font-weight: 400;
  transition: color var(--ease);
}
.tab svg { width: 22px; height: 22px; stroke-width: 1.5; }
.tab.active { color: var(--accent); font-weight: 500; }
.tab-label { display: block; line-height: 1.2; max-width: 100%;
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ------------------------------------------------ календарь записей */
.day-block { margin-bottom: var(--space-6); }
.day-head {
  position: sticky; top: 62px; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  font-family: var(--font);
  font-weight: 500; font-size: var(--text-meta);
 letter-spacing: .05em;
}
.day-head.is-today { background: var(--accent-soft); color: var(--ink); border-color: var(--accent); }
.day-head .count { font-weight: 500; font-size: var(--text-meta); opacity: .8; letter-spacing: 0; }

.master-group { margin-bottom: var(--space-4); }
.master-name {
  font-family: var(--font);
  font-style: normal; font-weight: 500; font-size: var(--text-section);
  margin-bottom: var(--space-2);
  padding-left: 2px;
}

/* десктопная таблица-сетка — на мобиле скрыта */
.grid-desktop { display: none; }

/* ------------------------------------------------ таймлайн касаний */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li {
  position: relative;
  padding: 0 0 var(--space-4) var(--space-6);
  border-left: 2px solid var(--line);
}
.timeline li:last-child { border-left-color: transparent; padding-bottom: 0; }
.timeline li::before {
  content: '';
  position: absolute; left: -7px; top: 4px;
  width: 12px; height: 12px;
  border-radius: var(--radius-full);
  background: var(--raised); border: 2px solid var(--accent);
}
.timeline li.done::before { background: var(--ok-ink); border-color: var(--ok-ink); }
.timeline li.next::before { background: var(--ink); border-color: var(--ink); }
/* №195: каскад, погашенный более новым визитом. Приглушаем, а не прячем —
   строка обязана остаться видимой (иначе смена не увидит, что записи не
   было молча), но не иметь права выглядеть как «в работе» рядом с done/next. */
.timeline li.closed { opacity: .55; }
.timeline li.closed::before { background: var(--raised); border-color: var(--ink2); }
.timeline .t-title { font-weight: 500; font-size: var(--text-body); }
.timeline .t-meta { font-size: var(--text-meta); color: var(--ink2); }

/* ---------- строка задачи (№769) ----------
   Отметка «сделано» — свой орган: круг 20px с галочкой при нажатии.
   Не `input[type=checkbox]`: тот несёт браузерный вид и подпись,
   а здесь нужен ровно один смысл — «дело закрыто». */
.task-check {
  width: 20px; height: 20px; flex: none; padding: 0;
  border: 1.5px solid var(--line-strong); border-radius: var(--radius-full);
  background: var(--raised); cursor: pointer;
}
.task-check:hover { border-color: var(--accent); }
.task-check.is-done { background: var(--ok-bg); border-color: var(--ok-ink); }
.line-row.task.is-done { opacity: .6; }
.line-row.task .task-body, .line-row.task .task-pair { color: var(--ink3); }
.task-menu { flex: none; }

/* ------------------------------------------------ задачи: аккордеон */
/* №476: вид спойлера несёт общий `.fold` (глиф «›», подпись без засечек,
   `.fold-n` справа); срочные группы — модификаторы `.fold-overdue`/
   `.fold-today` там же. Здесь — только свои отступы тела и стопки. */
.acc { margin-bottom: var(--space-3); }
.acc-body { padding: var(--space-4) var(--space-4) var(--space-3); }
/* №481: «Доступы» — строки и окно. Роль справа в строке; группы галочек:
   колонка одна на 380, две на широком (правило владельца). */
.acc-line .acc-role { flex: none; font-size: var(--text-meta); }
.perm-group { margin-bottom: var(--space-3); }
.perm-group-h {
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink2); margin: var(--space-3) 0 var(--space-2);
}
.perm-note { margin: 0 0 var(--space-2); font-size: var(--text-meta); }
.perm-danger .perm-group-h { color: var(--alert-ink); }
.perm-group-grid { display: grid; grid-template-columns: 1fr; gap: 2px var(--space-3); }
@media (min-width: 900px) {
  .perm-group-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.perm-foot { display: flex; gap: var(--space-2); justify-content: space-between; margin-top: var(--space-3); }

/* №482: карточка задачи — скелет и один блок кнопок внизу. Порядок
   постоянный: связь [WhatsApp][Позвонить], исход [Недозвон][Позже]
   [Выполнено]. Широкий — один ряд, зазор между группами; 380 — две
   строки: связь сверху, исход снизу. */
/* Ряд из пяти кнопок в карточке задачи (№482) снят перерисовкой:
   действия живут в меню строки (`.row-menu`), главное — отметкой
   слева. Стили ряда убраны вместе с ним, чтобы мёртвых классов
   не осталось. */

/* ═══ «События» (№768, 24.09): лента видна сразу ═══════════════════════
   Было: карточки задач внутри спойлера дня — два свёрнутых уровня до
   события и восемь чипов с нулями над ними. Стало: полоса управления,
   заголовок дня и строки под ним. Своих цветов тут нет ни одного —
   только токены вертикали (`[theme]` → /theme.css): рамка, фон,
   приглушённый текст.

   Телефон первым: полоса управления переносится, строка кладёт кнопку
   под текст; десктопные уточнения — ниже, от 900px. */

/* Полоса управления: поиск отдельной строкой выше, здесь — период,
   «Показать» и служебные. */
/* Тот же вычет запаса, что у `.ctl`: шаг до ряда срезов остаётся 8px. */
.ev-search { margin-bottom: calc(var(--space-2) - var(--ring-reach)); }
/* ⚠️ `.ev-search input[type="search"] { width: 100% }` снято 29.09.2026:
   ширину поиска задаёт теперь общий признак `search-wide` (книга, «Экран
   со списком: порядок органов», п.3). Экранное правило делало то же
   самое, но в обход общего — и «поиск во всю ширину» существовал бы
   в системе двумя способами: у «Событий» своим, у остальных общим. */
.ev-controls {
  display: flex; align-items: center; gap: var(--space-2);
  flex-wrap: wrap; margin-bottom: var(--space-3);
}
.ev-pick { margin: 0; }
.ev-pick-label { display: inline-flex; align-items: center; gap: var(--space-2); }
/* Служебные — тот же чип, что снятые фильтры: это переключатель, а не
   действие, и кнопкой он читался бы как «сделать что-то».
   ⚠️ `margin-left: auto` снят 29.09.2026: он отталкивал последнюю группу
   к правому краю и разрывал ряд надвое — приём из времён, когда ряд
   управления стоял слева. Теперь ВЕСЬ ряд прижат вправо (`.ctl-switches`,
   книга, «Экран со списком: порядок органов», п.4), и распорка внутри
   него оставляла бы посреди ряда дыру в три сотни точек. */
.ev-active { margin: 0 0 var(--space-3); }
.ev-dates { margin-bottom: var(--space-3); }

/* Лента. Рамка одна на весь список, строки разделены линиями — так
   читается сверху вниз, а не двадцатью отдельными карточками. */
.ev-feed {
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--raised); overflow: hidden;
}
/* Заголовок дня. Липкий на прокрутке: в ленте за неделю день уезжает
   вверх первым, а именно он отвечает на вопрос «когда это было». */
.ev-day-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--raised); border-bottom: 1px solid var(--line);
  font-size: var(--text-meta); font-weight: 500; color: var(--ink2);
  text-transform: uppercase; letter-spacing: .04em;
}
.ev-feed > .ev-day-head:not(:first-child) { border-top: 1px solid var(--line); }
.ev-day-n { margin-left: auto; font-weight: 500; letter-spacing: 0; }

.ev-row {
  display: flex; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-3); border-bottom: 1px solid var(--line);
  cursor: pointer;
}
.ev-row:last-child { border-bottom: 0; }
.ev-row:hover { background: var(--raised); }
.ev-time {
  flex: none; min-width: 42px;
  font-size: var(--text-meta); color: var(--ink2);
  font-variant-numeric: tabular-nums; padding-top: 2px;
}
/* ⚠️ `flex: 1 1 0`, а не `1 1 auto`: при `auto` основой служит ШИРИНА
   СОДЕРЖИМОГО, и длинный заголовок переносил время на свою строку —
   на телефоне соседние строки ленты выравнивались по-разному (поймано
   скриншотом приёмки, не расчётом). С нулевой основой время держит
   свой столбец у всех строк. */
.ev-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 0; }
.ev-title { font-size: var(--text-control); }
.ev-body { font-size: var(--text-meta); color: var(--ink2); }
/* Служебное — одним способом (книга, «Чистота экрана», правка 24.09):
   слово в начале второй строки, тоном ink-3. Только там, где служебные
   строки смешиваются с обычными (сегмент «Служебные» на «Событиях») —
   признак решает `events.py::is_system`, вторая копия правила не
   заводится. */
.ev-sys { color: var(--ink3); }
.ev-meta {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  font-size: var(--text-meta); color: var(--ink2);
}
/* Строка, которой ждут действия: отметка полосой у края, а не цветом
   фона — цвет фона в ленте уже занят наведением, и два смысла на одном
   свойстве читались бы как одно. */
.ev-row.is-todo { border-left: 3px solid var(--accent); padding-left: calc(var(--space-3) - 3px); }
.ev-act { flex: none; }
.ev-act .btn { min-height: 36px; padding: 0 var(--space-3); }

@media (max-width: 899px) {
  /* Кнопка уходит под текст: на узком она отнимала бы у заголовка
     половину строки. Время остаётся слева — по нему лента и читается. */
  .ev-row { flex-wrap: wrap; }
  .ev-act { width: 100%; padding-left: calc(42px + var(--space-3)); }
  .ev-act .btn { width: 100%; }
  /* Сброс `.ev-service { margin-left: 0 }` здесь больше не нужен:
     самой распорки нет (снята 29.09.2026, см. правило выше). */
  /* ⚠️ Прежняя сетка 2×2 у срезов «Событий» снята (№769): в системе
     ряд управления не переносится — он скроллится вбок (`.ctl`).
     Замер, ради которого сетка заводилась (409px при 390), остаётся
     верным, но ответ у системы другой: группа сегментов уезжает
     за край и прокручивается, а не ломается на две строки. */
  .ev-pick .sort-select { min-width: 150px; }
}

/* №465: последняя строка спойлера «Выполненные» — путь в архив. */
.task-archive-link {
  display: flex; align-items: center; justify-content: center;
  min-height: var(--tap); margin-top: var(--space-2);
  border-top: 1px solid var(--line);
  color: var(--ink); font-weight: 500; font-size: var(--text-control);
  text-decoration: none;
}
.task-archive-link:hover { color: var(--accent); }
/* Архив задач: форма фильтра в строку, пагинация по краям. */
/* Нижний отступ вычитает запас под обводку (см. `.ctl`): без этого
   шаг до списка вырос бы на его величину. */
.archive-panel { display: flex; gap: var(--space-2); flex-wrap: wrap;
                 margin-bottom: calc(var(--space-3) - var(--ring-reach)); }
.archive-panel .search { flex: 1 1 200px; }
.archive-panel .sort-select { width: auto; min-width: 160px; }
.archive-pages { display: flex; justify-content: space-between; gap: var(--space-2); margin-top: var(--space-3); }
.archive-pages a:only-child { margin-left: auto; }

/* Задача — СТРОКА СПИСКА (№769): вид, слоты и разделители у неё общие
   (`.line-row`), здесь остаются только её частности. Колоночный скелет
   №482 снят вместе с рядом из пяти кнопок — почему, написано в шапке
   `_task_card.html`. */
.task.is-done .task-title { text-decoration: line-through; }
.task-title { font-weight: 500; font-size: var(--text-body);
              white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Имя клиента в задаче — ссылка, и на телефоне тоже. Тап-зона добита
   псевдоэлементом, а не отступами: отступы раздвинули бы плотную строку
   с бейджем и сроком, а палец всё равно толще курсора. */
.task-client {
  position: relative;
  color: var(--ink);            /* ссылки шоколадом, без подчёркиваний-пилюль */
  font-weight: 500; text-decoration: none;
  border-bottom: 1px solid var(--line);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 14ch;
}
.task-client::after {
  content: ''; position: absolute; left: -6px; right: -6px;
  top: 50%; height: var(--tap); transform: translateY(-50%);
}
.task-client:hover { text-decoration: underline; }
/* Карточка кликабельна целиком, когда ей есть куда вести. */
.task[data-open] { cursor: pointer; }
.task[data-open]:hover { background: var(--sunken); }
.task-meta { font-size: var(--text-meta); color: var(--ink2); }

/* ------------------------------------------------ список клиентов */
/* №458 (вторая половина): СТРОКА СПИСКА — один класс на все списки.
   Клиенты (`.client-line`), услуги (`.svc-line`), абонементы (`.sub-line`)
   несут его вместе со своей доменной меткой: метка — адрес для стилей
   частностей и швов, `.line-row` — общий вид, волна касания и нажатие
   (списки выше по файлу). Новый список — `line-row` плюс своя метка,
   второй копии вида не заводить. */
/* ------------------------------------------------ строка списка
   Книга, блок ListRow: строки лежат В ОДНОЙ РАМКЕ и делятся волосяной
   линией — промежутков между строками нет. Слоты слева направо:
   R1 аватар или время (72px) · R2 две строки текста · R3 статус ·
   R4 значение у правого края · R5 «ещё».

   ⚠️ Рамку несёт КОНТЕЙНЕР `.rows`, а не строка: так строки делит
   линия, а не воздух. Строка без контейнера остаётся читаемой —
   у неё прежняя своя рамка. */
.rows {
  background: var(--raised);
  border: var(--rule);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: var(--space-4);
}
.rows > .line-row {
  border: 0; border-top: var(--rule); border-radius: 0; margin: 0;
}
/* п.23 (25.09): ПОЛОСКА СОСТОЯНИЯ ПОВТОРЯЕТ КОНТУР РАМКИ. Крайние строки
   получают левые скругления рамки — и inset-полоска гнётся по ним сама
   (inset-тень всегда следует радиусу своего элемента), а не упирается
   прямым углом в скруглённый угол списка. Правые скругления тоже, иначе
   подложка наведения и выбора красила бы угол квадратом под кривой.
   ⚠️ Одной `overflow: hidden` у рамки для этого не хватало: она РЕЗАЛА
   полоску по кривой, и у первой строки та начиналась ниже угла —
   именно это владелец и увидел («из контура торчит»). */
.rows > .line-row:first-child {
  border-top: 0;
  border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg);
}
.rows > .line-row:last-child {
  border-bottom-left-radius: var(--radius-lg);
  border-bottom-right-radius: var(--radius-lg);
}

.line-row {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: 56px;
  padding: var(--space-3) var(--space-4);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-2);
  text-decoration: none;
  cursor: pointer;
  touch-action: manipulation;
  /* `box-shadow` в переходе — из-за полоски «ход за студией» у диалогов
     (книга, принцип 4): без него она вспыхивала бы, а не проявлялась. */
  transition: background var(--ease), box-shadow var(--ease);
}
/* Выбранная строка — подложка `--accent-soft` (книга). */
.line-row.is-sel { background: var(--accent-soft); }

/* R1 — время или дата: 72px, две строки по смыслу, каждая без переноса. */
.line-when {
  width: 72px; flex: none; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.line-when b { display: block; font-weight: 500; }
.line-when span { display: block; font-size: var(--text-meta); color: var(--ink3); }
/* R2 — две строки текста, каждая обрезается многоточием. */
.line-tx { flex: 1; min-width: 0; }
.line-tx .n { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.line-tx em {
  font-style: normal; display: block;
  font-size: var(--text-meta); line-height: var(--line-meta); color: var(--ink2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* R4 — значение у правого края: время, сумма, дата. */
.line-val {
  flex: none; color: var(--ink3);
  font-size: var(--text-meta); font-variant-numeric: tabular-nums;
}
/* Аватар с инициалом — единственный круг в системе. */
.line-av {
  width: 36px; height: 36px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-full);
  background: var(--sunken); color: var(--ink2);
  font-size: var(--text-control); font-weight: 500;
}

/* Активный стиль — одна точка, ниже по файлу («кнопки: мягкий подъём»),
   рядом со своим фоновым токеном (`--surface-sunken`, тот же, что у hover). */
.client-line .who { flex: 1; min-width: 0; }
.client-line .nm { font-weight: 500; }
/* Шеврон в строке списка снят перерисовкой (книга, блок ListRow). */

/* ---------------------------------------- клиенты: чипы, группы (№474) */
/* Первый экран на 380 (приёмка владельца): поиск и «+ Добавить» — одной
   строкой, поэтому полю разрешено сжиматься уже общих 260px; подсказка
   в нём усечётся, и это дешевле второй строки шапки. */
.clients-head .search { flex: 1 1 120px; min-width: 120px; }
/* Ряд чипов и меню «ещё»: чипы прокручиваются на узком, «ещё» стоит ВНЕ
   прокрутки — раскрытое меню внутри overflow срезалось бы рамкой ряда. */
.clients-bar { display: flex; align-items: center; gap: var(--space-2); margin: var(--space-2) 0 var(--space-3); }
/* ⚠️ Лента чипов прокручивается САМА и потому срезает своё —
   ей нужен тот же запас под обводку, что и ряду управления: чип
   у её края фокусируется, и обводку ему обрезало бы лентой, даже
   когда у ряда снаружи запас есть. Отрицательный отступ возвращает
   содержимое на место. */
.clients-chips {
  display: flex; align-items: center; gap: var(--space-2);
  flex: 1; min-width: 0;
  overflow-x: auto; scrollbar-width: none;
}
/* ⚠️ Двойной класс: лента чипов — ещё и `.seg`, а тот объявляет свой
   отступ ПОЗЖЕ по файлу и при равном весе побеждал бы. Замер показал
   3px вместо нужных 4 — ровно на этот пиксель обводку и срезало бы. */
.seg.clients-chips {
  padding: var(--ring-reach); margin: calc(-1 * var(--ring-reach));
}
.clients-chips::-webkit-scrollbar { display: none; }
.clients-chips .filter-chip { flex: none; }
.chip-more { position: relative; flex: none; }
.chip-more summary { list-style: none; display: inline-flex; cursor: pointer; }
.chip-more summary::-webkit-details-marker { display: none; }
.chip-menu {
  --pop-align: right;
  position: absolute; right: 0; top: calc(100% + 6px);
  z-index: var(--z-panel);
  min-width: 230px;
  display: flex; flex-direction: column;
  padding: var(--space-2);
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: 0 0 0 1px rgba(var(--ink-rgb), .06), 0 8px 24px rgba(var(--ink-rgb), .14);
}
.chip-menu a {
  display: flex; align-items: center; min-height: var(--tap);
  padding: 0 var(--space-3); border-radius: var(--radius-md);
  color: var(--ink); text-decoration: none; white-space: nowrap;
}
.chip-menu a:hover { background: var(--press); }
.chip-menu .muted { margin: 2px 0 var(--space-1); padding: 0 var(--space-3); font-size: var(--text-meta); }

/* Заголовок группы по дате («Сегодня», «Раньше») — тем же голосом,
   что подписи групп в борте. */
.client-group {
  font-size: var(--text-micro); letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink2);
  margin: var(--space-4) 2px var(--space-2);
}
.client-grid > .client-group:first-child { margin-top: 0; }
/* Вторая строка карточки: источник · визитов · последнее действие. */
.client-line .line-sub {
  display: block;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Правый край строки: когда человек проявлялся последний раз. */
.client-line .when {
  flex: none; font-size: var(--text-meta); color: var(--ink2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Значки каналов у имени (№150). Первая строка становится рядом: имя
   сжимается и усекается многоточием, значки и метка архива не сжимаются
   никогда — имя читается и наполовину, а значок наполовину не читается
   вовсе. Тот же приём и те же числа, что у строки диалога (`.dlg-item`
   выше по файлу): второй раскладки для той же мысли не заводим. */
.client-line .line-tx .n {
  display: flex; align-items: center; gap: 6px; min-width: 0;
}
.client-line .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.client-line .tag-archived { flex: none; }
.chans { display: inline-flex; align-items: center; gap: 4px; flex: none; }
/* ЦВЕТ У ОБОИХ СОСТОЯНИЙ ОДИН — `--ink2`, тот же, которым нарисованы
   значки каналов в «Диалогах» (`.ch-tg`, `.ch-wa`). Статусных цветов
   в строке клиента нет (решение владельца 30.09.2026): статусная палитра
   в списках занята смыслом «ход за студией» / «ведёт человек», и канал
   связи в неё не входит.

   ⚠️ Состояния различает ПРОЗРАЧНОСТЬ, и это третье средство подряд —
   первые два не сработали, каждое по своей причине. Один вес чернил
   (`ink3` против `ink`) на снимке 390px оказался нечитаем: половина
   значков библиотеки — заливные глифы (Telegram, WhatsApp), и светлее
   набранная заливка всё равно читается сплошным пятном той же тяжести.
   Цвет (зелёный против серого) читался хорошо, но приносил в строку
   статусный смысл, которого у канала нет. Прозрачность бьёт ровно туда,
   где отказал вес: она снимает тяжесть САМОГО ПЯТНА и потому действует
   на заливку и на обводку одинаково. */
.chan { display: inline-flex; color: var(--ink2); opacity: .38; }
.chan svg { width: 16px; height: 16px; }
.chan.is-live { opacity: 1; }
.clients-more { width: 100%; }
.clients-found { margin: 0 0 var(--space-2); }

.avatar {
  width: 40px; height: 40px; border-radius: var(--radius-full);
  background: var(--accent-soft); color: var(--ink);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font); font-weight: 500; font-size: var(--text-body);
  flex-shrink: 0;
}

/* ------------------------------------------------ полоса всплывающих (№667)

   ОДНО место на все нижние уведомления вместо трёх независимых углов.
   Раньше тост и карточка события стояли в ОДНОЙ точке (`bottom:
   calc(var(--bar-h) + 16px)`), а «Вышло обновление» обходило только
   карточку (`--alert-h`) — и третий элемент наезжал на кнопки второго.

   Порядок — правилом, а не вкусом: чем ДОЛЬШЕ элемент живёт, тем ниже
   он стоит. `column-reverse` ставит первого ребёнка внизу, поэтому
   в разметке они идут: карточка события, «вышло обновление», тост, —
   а на экране снизу вверх: обновление (не гаснет), событие (гаснет
   по кнопке), тост (гаснет сам). Частое появление тоста двигает только
   его самого, кнопка «Понятно» под пальцем не прыгает.

   ⚠️ `pointer-events: none` на полосе и `auto` на детях: полоса шире
   своих детей (она центрируется по ним), и без этого невидимый столб
   перехватывал бы клики по странице между уведомлениями.

   ⚠️ Поток не трогается вовсе (№570, №642): ни отступов у `body`,
   ни сдвига липкой шапки — страница не двигается ни при появлении,
   ни при исчезновении. Таббар телефона обойдён отступом `--bar-h`. */
.popups {
  position: fixed; left: 50%; bottom: calc(var(--bar-h) + 16px);
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex; flex-direction: column-reverse; align-items: center;
  gap: var(--space-2);
  width: min(420px, calc(100vw - 2 * var(--space-4)));
  pointer-events: none;
}
.popups > * { pointer-events: auto; }

/* ------------------------------------------------ уведомление */
.toast {
  /* Место задаёт полоса `.popups` (№667) — здесь только вид и переход.
     Своё `position: fixed` снято: пока оно стояло, тост жил в той же
     точке, что карточка события, и накрывал её кнопки. */
  transform: translateY(20px);
  max-width: 100%;
  padding: var(--space-3) var(--space-4);
  background: var(--ink); color: var(--raised);
  border-radius: var(--radius-lg);
  font-size: var(--text-control);
  opacity: 0; pointer-events: none;
  transition: opacity var(--ease), transform var(--ease);
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.error { background: var(--alert-ink); }

/* ------------------------------------------------ страница входа */
.login-page {
  min-height: 100dvh;
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-4);
  background: linear-gradient(160deg, var(--accent-soft) 0%, var(--raised) 55%);
}
.login-card {
  width: 100%; max-width: 380px;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-8) var(--space-6);
  text-align: center;
}
.login-card .logo { width: 68px; height: 68px; border-radius: var(--radius-lg); font-size: var(--text-num);
                    margin: 0 auto var(--space-4); }
.login-card h1 { font-size: var(--text-section); margin-bottom: 4px; }
.login-card .accent { display: block; margin-bottom: var(--space-6); font-size: var(--text-section); }
.login-card form { text-align: left; }
.login-card .btn { width: 100%; margin-top: var(--space-2); }
.login-error {
  margin-bottom: var(--space-3); padding: var(--space-3);
  background: var(--alert-bg); border: 1px solid var(--alert-ink); color: var(--alert-ink);
  border-radius: var(--radius-lg); font-size: var(--text-control); text-align: left;
}

/* ============================================================
   Десктоп
   ============================================================ */
@media (min-width: 1000px) {
  /* Порог тот же, что у борта (.shell выше): между 900 и 999 иначе
     пропадала вся навигация — борт ещё не включился, панель уже спряталась. */
  .tabbar { display: none; }

  .wrap { padding: var(--space-6); padding-bottom: var(--space-12); }
  h1 { font-size: var(--text-title); }

  .toast { bottom: 28px; }

  /* записи: настоящая таблица-сетка мастера × дни */
  .cards-mobile { display: none; }
  .grid-desktop { display: block; }

  .grid-scroll { overflow-x: auto; padding-bottom: var(--space-2); }
  table.grid {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    min-width: 900px;
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
  }
  table.grid th, table.grid td {
    border-bottom: 1px solid var(--line);
    border-right: 1px solid var(--line);
    padding: var(--space-2);
    vertical-align: top;
    text-align: left;
  }
  table.grid th {
    background: var(--accent-soft);
    font-family: var(--font);
    font-size: var(--text-meta); font-weight: 500;
    text-transform: uppercase; letter-spacing: .04em;
    position: sticky; top: 0; z-index: 5;
  }
  table.grid th.is-today { background: var(--accent-soft); color: var(--ink); }
  table.grid th:first-child, table.grid td:first-child {
    width: 140px; background: var(--sunken); font-weight: 500;
    position: sticky; left: 0; z-index: 4;
  }
  table.grid th:first-child { z-index: 6; }
  table.grid tr:last-child td { border-bottom: none; }
  table.grid td:last-child, table.grid th:last-child { border-right: none; }

  .cell-empty { color: var(--ink2); opacity: .55; font-size: var(--text-meta); }
  .mini {
    background: var(--sunken);
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-lg);
    padding: var(--space-2);
    margin-bottom: var(--space-2);
    font-size: var(--text-meta);
  }
  .mini:last-child { margin-bottom: 0; }
  .mini.came { border-left-color: var(--ok-ink); }
  .mini.no_show { border-left-color: var(--alert-ink); }
  .mini .t { font-weight: 500; font-variant-numeric: tabular-nums; }
  .mini .btn { min-height: 34px; padding: 4px 10px; font-size: var(--text-meta); width: 100%; }

  .two-col { display: grid; grid-template-columns: 1.15fr .85fr; gap: var(--space-6); align-items: start; }
  /* №474: «Клиенты» — одна колонка и на десктопе (решение владельца:
     единый язык списков; прежняя плитка 320px тут больше не живёт). */
}

/* уважаем системную настройку «меньше движения» */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================
   ПОЛИРОВКА: фоны, микроанимации, состояния
   Всё на CSS/SVG — без внешних библиотек и рантайм-загрузок.
   ============================================================ */

/* ------------------------------------------------ страница входа
   Свой мир у входа остался (аудит №769, п.6), но подложка снята вместе
   с остальными: та же `--page` под карточкой на линии. */
.login-page { background: var(--page); position: relative; }
.login-card { background: var(--raised); border-color: var(--line); }

/* ⚠️ Волна-разделитель под шапкой убрана (волна 2 №34): декоративная
   картинка из /static, которая не знает про палитру вертикали, — на
   barber она осталась бы кремовой. Границу держит обычная линия. */
.wrap { padding-top: var(--space-4); }

/* ⚠️ Всплытие карточек (fade+slide со «лесенкой») УБРАНО в волне 2 №34.
   Оно изображало движением ту самую глубину, от которой каркас отказался:
   карточка приподнималась над фоном, хотя тени под ней больше нет. На
   плоскости это читается как подёргивание списка при каждой загрузке. */

/* ------------------------------------------------ кнопки: мягкий подъём */
.btn {
  transition: background var(--ease), color var(--ease), opacity var(--ease);
}
/* Градиент снят (05.09): hero — РАЗМЕРНЫЙ модификатор тёмной кнопки,
   а свой фон делал её четвёртым видом. */
/* Нажатие — то же затемнение акцента, что у обычной кнопки: вид
   и отклик главной кнопки живут ОДНОЙ точкой, у класса `.btn`. */
@media (hover: hover) {
  /* Подъём при наведении убран (волна 3): каркас плоский, и «приподнятый»
     элемент обещает тень, которой под ним нет. Отклик даёт заливка. */
  .line-row:hover { background: var(--sunken); }
}
/* ⚠️ Прежний `.btn:active { opacity: .9; }` здесь СНЯТ (05.09): та же
   специфичность, что у `.btn:active` выше по файлу (background: var(--ink)),
   и эта, более поздняя, побеждала на равенстве — реальным откликом на
   нажатие была прозрачность, а не задуманное затемнение. Второй копии
   не заводим: активный стиль кнопки — ОДНА точка, у самого класса `.btn`. */
.line-row:active { background: var(--sunken); transition-duration: .1s; }

/* ------------------------------------------------ прогресс-полоска абонемента */
.sub-progress {
  height: 6px;
  margin-top: 10px;
  background: var(--sunken);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.sub-progress i {
  display: block; height: 100%;
  width: var(--fill, 100%);
  border-radius: var(--radius-sm);
  /* ⚠️ ПОЛОСА ОСТАТКА — АКЦЕНТОМ ТЕМЫ (решение владельца 25.09.2026,
     п.13), а не чернилами. Прежнее правило («акцент только кнопке,
     фокусу и пункту борта») здесь пересмотрено владельцем: шкала,
     залитая цветом текста, на всех шести пресетах выглядела одинаково
     тёмной и с темой не менялась вовсе. Цвет доводит ядро
     (`theme.bar_fill`) до 3:1 с подложкой — минимум WCAG для графики. */
  background: var(--bar-fill, var(--accent));
  transition: width 700ms cubic-bezier(.2, .7, .3, 1);
}

/* число остатка: короткая подсветка при изменении */
.num-flash { animation: num-flash 700ms ease-out; }
@keyframes num-flash {
  0%   { transform: scale(1);    color: var(--ink); }
  35%  { transform: scale(1.14); color: var(--ok-ink); }
  100% { transform: scale(1);    color: var(--ink); }
}

/* ------------------------------------------------ галочка успеха (stroke-dashoffset) */
.success-pop {
  position: fixed; inset: 0; z-index: var(--z-success);
  display: grid; place-items: center;
  pointer-events: none;
  background: rgba(var(--ink-rgb), .10);
  opacity: 0;
  animation: pop-fade 1500ms ease forwards;
}
.success-pop .ring {
  width: 108px; height: 108px;
  background: rgba(var(--raised-rgb), .96);
  border-radius: var(--radius-full);
  display: grid; place-items: center;
  transform: scale(.86);
  animation: ring-in 420ms cubic-bezier(.2, .8, .3, 1) forwards;
}
.success-pop svg { width: 56px; height: 56px; }
.success-pop .circle {
  fill: none; stroke: var(--accent); stroke-width: 2.4;
  stroke-dasharray: 151; stroke-dashoffset: 151;
  animation: draw 520ms cubic-bezier(.4, 0, .2, 1) 80ms forwards;
}
.success-pop .tick {
  fill: none; stroke: var(--ink); stroke-width: 3.6;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 36; stroke-dashoffset: 36;
  animation: draw 360ms cubic-bezier(.4, 0, .2, 1) 380ms forwards;
}
@keyframes draw    { to { stroke-dashoffset: 0; } }
@keyframes ring-in { to { transform: scale(1); } }
@keyframes pop-fade {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  75%  { opacity: 1; }
  100% { opacity: 0; }
}

/* ------------------------------------------------ деликатный лоадер автообновления */
.sync {
  position: fixed;
  right: 14px;
  bottom: calc(var(--bar-h) + 16px + env(safe-area-inset-bottom));
  z-index: 45;
  display: flex; align-items: center; gap: 5px;
  padding: 7px 12px;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--ease), transform var(--ease);
  pointer-events: none;
}
.sync.show { opacity: 1; transform: none; }
.sync i {
  width: 6px; height: 6px; border-radius: var(--radius-full);
  background: var(--accent);
  animation: dot 1200ms ease-in-out infinite;
}
.sync i:nth-child(2) { animation-delay: 160ms; }
.sync i:nth-child(3) { animation-delay: 320ms; }
@keyframes dot {
  0%, 100% { opacity: .35; transform: translateY(0) scale(.9); }
  40%      { opacity: 1;   transform: translateY(-3px) scale(1); background: var(--ink); }
}

/* ------------------------------------------------ пустые состояния
   Вид — ОДНОЙ точкой, у класса `.empty` выше: заголовок, строка,
   кнопка; ни рамки, ни пунктира, ни иллюстрации (книга, EmptyState).
   Здешняя вторая редакция (рамка пунктиром и парящие камни) снята
   перерисовкой №769 вместе с самими камнями. */
.empty .say {
  font-family: var(--font);
  font-style: normal; font-weight: 500; font-size: var(--text-section); color: var(--ink2);
  max-width: 34ch; line-height: 1.45;
}
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}

/* ------------------------------------------------ таймлайн: плавное заполнение линии */
.timeline li { --line-fill: 0%; }
.timeline li::after {
  content: '';
  position: absolute; left: -2px; top: 0;
  width: 2px; height: var(--line-fill);
  background: var(--ink);
  transition: height 900ms cubic-bezier(.2, .7, .3, 1) 200ms;
}
.timeline li.done { --line-fill: 100%; }
.timeline li.next { --line-fill: 45%; }
.timeline li::before {
  transition: background var(--ease), border-color var(--ease), transform var(--ease);
}
.timeline li.next::before {
  animation: pulse-dot 2.4s ease-in-out infinite;
}
@keyframes pulse-dot {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--ink-rgb), .26); }
  50%      { box-shadow: 0 0 0 7px rgba(var(--ink-rgb), 0); }
}

/* ------------------------------------------------ тост: чуть заметнее и в тоне бренда */
.toast {
  border: 1px solid rgba(var(--accent-rgb), .35);
  border: var(--rule);
  display: flex; align-items: center; gap: 10px;
}
.toast::before {
  content: '';
  width: 7px; height: 7px; border-radius: var(--radius-full);
  background: var(--accent);
  flex-shrink: 0;
}
.toast.error::before { background: var(--alert-bg); }

/* ------------------------------------------------ мобильные уточнения (390px) */
@media (max-width: 899px) {
  /* тост и лоадер не должны накрывать нижние кнопки и таб-бар */
  .toast { bottom: calc(var(--bar-h) + 14px + env(safe-area-inset-bottom)); }
  .success-pop .ring { width: 96px; height: 96px; }
  .success-pop svg { width: 50px; height: 50px; }
}

@media (min-width: 900px) {
  .sync { bottom: 24px; right: 24px; }
}

/* «меньше движения» — гасим всё, включая новые эффекты */
@media (prefers-reduced-motion: reduce) {
  .card, .client-line, .acc, .day-block,
  .login-card, .login-card .logo,
  .empty svg, .timeline li.next::before { animation: none !important; }
  .sub-progress i, .timeline li::after { transition: none !important; }
  .success-pop { animation: pop-fade-static 1200ms ease forwards; }
  @keyframes pop-fade-static { 0%, 85% { opacity: 1; } 100% { opacity: 0; } }
  .success-pop .circle, .success-pop .tick { stroke-dashoffset: 0; animation: none; }
  .success-pop .ring { transform: scale(1); animation: none; }
}

/* ------------------------------------------------ метка сборки (пруф свежести страницы) */
.build-mark {
  margin-top: var(--space-6);
  text-align: center;
  font-size: var(--text-micro);
  letter-spacing: .06em;
  color: var(--ink2);    /* метку сборки читают с телефона — латте не годится */
  user-select: all;            /* удобно продиктовать или скопировать */
}
.build-mark.login {
  position: fixed; left: 0; right: 0;
  bottom: calc(12px + env(safe-area-inset-bottom));
  margin: 0;
  color: var(--ink2);
  opacity: .6;
}

/* ============================================================
   ДИАЛОГИ, АВТООТВЕТЧИК, ВИТРИНЫ, НАВИГАЦИЯ «ЕЩЁ»
   ============================================================ */

/* ------------------------------------------------ бейдж непрочитанного */
.dot {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: var(--radius-lg);
  background: var(--ink); color: var(--raised);
  font-size: var(--text-micro); font-weight: 500;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.tab .dot { position: absolute; top: 6px; left: 50%; margin-left: 6px; }
.tab { position: relative; }
.tab.active .dot { background: var(--accent); color: var(--on-accent); }

/* ------------------------------------------------ панель «Ещё» */
.more-scrim {
  position: fixed; inset: 0; z-index: 55;
  background: rgba(var(--ink-rgb), .38);
  opacity: 0; pointer-events: none;
  transition: opacity var(--ease);
}
.more-sheet {
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--bar-h) + env(safe-area-inset-bottom));
  z-index: 56;
  background: var(--sidebar);
  border-top: 1px solid rgba(var(--sidebar-ink-rgb), .14);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: var(--space-2) var(--space-3) var(--space-3);
  /* ⚠️ Прячем ниже НИЖНЕГО КРАЯ ЭКРАНА, а не на «110% своей высоты».
     Лист стоит НАД баром (`bottom: bar-h`), поэтому уйти ему нужно на свою
     высоту ПЛЮС высоту бара. С `110%` сдвиг считался только от собственной
     высоты, и чем короче лист, тем выше он оставался: у роли с семью
     разделами в «Ещё» три пункта (высота 176px), закрытый лист накрывал
     36 пикселя из 60 нижней панели — иконки пропадали, а те, что торчали
     из-под него, выглядели выключенными и всё же нажимались. У владельца
     с девятью пунктами (488px) перекрытие было 5px и в глаза не бросалось,
     поэтому баг выглядел «зависящим от роли». Он и зависел — но от ЧИСЛА
     видимых разделов, а не от прав: права на нижние вкладки у обеих ролей
     полные. */
  /* ⚠️ ПОТОЛОК ВЫСОТЫ И СВОЯ ПРОКРУТКА — без них лист любой длины
     обрезается ЖЁСТКО (правка 28.09.2026, дефект в бою у клиента).
     Лист `fixed` и растёт ВВЕРХ от нижнего края: не помещается —
     верхние пункты уходят за верх экрана, и достать их нечем.
     Страничная прокрутка его не двигает (он `fixed`), своей у него
     не было вовсе — ни `max-height`, ни `overflow`.
     ⚠️ Лечение НЕ отступами: уменьшить их значит отложить тот же
     дефект до следующего пункта меню. Ограничение высоты плюс своя
     прокрутка делают проходимым лист ЛЮБОЙ длины на ЛЮБОМ экране.
     Замер до правки (по этим же стилям: пункт 48px, подпись группы 23,
     подвал 36, отступы 20, бар 64): владельцу нужно 886px видимой
     высоты, менеджеру 742 — столько нет ни у одного телефона; смене
     и гостю хватает 358–406, поэтому дефект выглядел «у некоторых».
     ⚠️ Зазор сверху `--space-8` оставлен НАРОЧНО: лист закрывают
     нажатием по затемнению, и без полоски затемнения закрыть его
     было бы нечем, кроме кнопки «Ещё».
     ⚠️ `vh` строкой выше `dvh` — запасной путь: браузер, не знающий
     динамической единицы, возьмёт первую и всё равно получит потолок.
     С `dvh` потолок ещё и переживает выезд адресной строки. */
  max-height: calc(100vh - var(--bar-h) - env(safe-area-inset-bottom) - var(--space-8));
  max-height: calc(100dvh - var(--bar-h) - env(safe-area-inset-bottom) - var(--space-8));
  overflow-y: auto;
  /* Докрутив лист до края, не продолжаем крутить страницу под ним. */
  overscroll-behavior: contain;
  transform: translateY(calc(100% + var(--bar-h) + env(safe-area-inset-bottom)));
  transition: transform var(--ease);
  /* №185, второй ход: колонка — ради `order` у `.more-under`, тем же
     приёмом, что `.side-under` в борте. */
  display: flex; flex-direction: column;
}
.more-toggle:checked ~ .more-scrim { opacity: 1; pointer-events: auto; }
.more-toggle:checked ~ .more-sheet { transform: translateY(0); }

/* Панель «Ещё» лежит на тёмном — значит и текст в ней светлый. Иначе
   шоколад на шоколаде: пункты пропали бы целиком. */
/* №185, второй ход: шаг 48 (тап-зона ≥44), иконка 20, шрифт 15 —
   уплотнение тем же приёмом, что в борте на десктопе. Подписи групп —
   вид `.side-group` борта, но своим правилом: те стили живут в десктопном
   медиазапросе и сюда не приходят. */
.more-sheet .side-group {
  font-size: var(--text-micro); letter-spacing: .14em; text-transform: uppercase;
  color: rgba(var(--sidebar-ink-rgb), .45);
  padding: var(--space-2) var(--space-3) 2px;
}
.more-item {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: 48px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-md);
  color: rgba(var(--sidebar-ink-rgb), .82); text-decoration: none;
  font-weight: 500; font-size: var(--text-body);
}
.more-item svg { width: 20px; height: 20px; stroke-width: 1.7; color: rgba(var(--sidebar-ink-rgb), .6); }
.more-item.active { background: var(--sidebar-active); color: var(--sidebar-ink); }
.more-item.active svg { color: var(--sidebar-ink); }
/* Низ листа — тот же, что низ борта: одно имя вошедшего, больше ничего
   (книга, 25.09). На телефоне этот лист и есть борт. */
.more-foot {
  margin-top: var(--space-2); padding-top: var(--space-2);
  border-top: 1px solid rgba(var(--sidebar-ink-rgb), .14);
}
/* «Возможности и цены» — под черту, мелким (зеркало `.side-under`). */
.more-item.more-under {
  order: 1;
  margin-top: var(--space-2);
  border-top: 1px solid rgba(var(--sidebar-ink-rgb), .12);
  border-radius: 0;
  min-height: 40px;
  font-size: var(--text-meta); color: rgba(var(--sidebar-ink-rgb), .55);
}
.more-item.more-under svg { width: 16px; height: 16px; }
.more-tab { cursor: pointer; }

/* ------------------------------------------------ диалоги: раскладка */
.wrap-dialogs { padding-bottom: var(--space-4); }
.dialogs { display: block; }

/* мобильный режим: либо список, либо лента на весь экран.
   Порог 999px, а не 899 (№213): шапка `.top` и нижняя панель `.tabbar`
   живут до 999px включительно, а десктопная сетка начинается с 1000 —
   в полосе 900–999 (планшет) раньше не действовало ни то, ни другое:
   обе колонки шли стопкой на всю ширину, лента держала `min-height: 60vh`. */
@media (max-width: 999px) {
  /* ⚠️ Заголовок страницы на телефоне ВЕРНУЛСЯ (№769). Он был снят
     07.09 ради первого экрана: «строку занимает, а место называет
     подсвеченная вкладка внизу». Книга держит S1 у каждого экрана,
     и снимок двух соседних вкладок это проверяет; место нашлось
     в другом месте — снята шапка студии с логотипом и девизом,
     которая занимала 45px над заголовком на каждой странице.
     Заголовок на телефоне мельче (кегль `section`): 28px съедали
     строку целиком, и владелец был прав именно про это. */
  .page-title { font-size: var(--text-section); line-height: var(--line-section); }
  .section-head { justify-content: space-between; }
  /* Обёртка кнопок раздела занимает строку целиком, когда в ней есть чип
     фильтра рядом с кнопкой; содержимое прижимается вправо, где кнопки
     и стояли при заголовке. */
  .section-head > .head-actions { justify-content: flex-end; }
  .wrap-dialogs .build-mark { display: none; }
  .dlg-thread { display: none; }
  .wrap-dialogs.thread-open .dlg-list { display: none; }
  .wrap-dialogs.thread-open .dlg-thread { display: flex; flex-direction: column; }
  .thread-blank { display: none; }
}

/* ------------------------------------------------ список диалогов */
/* Строка диалога — та же строка списка (`.line-row`), рамку несёт
   контейнер `.rows`. Свой фон, рамка и отступ между карточками сняты
   перерисовкой №769: список переписок ничем не отличается от других
   списков, кроме содержимого строки. */
.dlg-item {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: 64px;
  padding: var(--space-3) var(--space-4);
  text-decoration: none; color: var(--ink);
}
/* Значок канала перед именем. ⚠️ Имя — своим `span` с многоточием:
   у flex-строки обрезается ЭЛЕМЕНТ, а голый текст рядом со значком
   элементом не является, и длинное имя растягивало бы строку
   (книга, «Текст в тесноте»). */
.dlg-item .line-tx .n { display: flex; align-items: center; gap: 6px; min-width: 0; }
.dlg-item .line-tx .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ═══ ПОЛОСКА СОСТОЯНИЯ У ЛЕВОГО КРАЯ — ОДНА МЕХАНИКА НА СИСТЕМУ ═══════
   Книга, принцип 4: 3px, не двигает содержимое, видна на выбранной строке,
   дублируется подсказкой при наведении. ДВА смысла, оба из СТАТУСНОЙ
   палитры (не из темы): красная `bar-problem` — ход за студией; жёлтая
   `bar-wait` — диалог ведёт человек, не бот. Совпали — красная, и это
   решает РАЗМЕТКА одним классом (п.2, 25.09), а не порядок правил здесь:
   «срочное сильнее принадлежности» — утверждение о смысле, и жить ему
   надо там, где смысл известен.

   ⚠️ `box-shadow`, а не `border-left`: рамка сдвинула бы содержимое строки
   на три пикселя, и строки списка перестали бы стоять в колонку. Второе
   свойство inset-тени и есть то, зачем она здесь: она СЛЕДУЕТ СКРУГЛЕНИЮ
   своего элемента, поэтому полоска гнётся по контуру рамки сама — см.
   левые радиусы у первой и последней строки (`.rows`, п.23). */
.bar-problem, .fold-overdue > summary { box-shadow: inset 3px 0 0 var(--alert-ink); }
.bar-wait { box-shadow: inset 3px 0 0 var(--wait-ink); }

/* Время и рядом значок «кто говорил последним» (R4, книга: «„Авто“
   и „человек“ у диалога — значок у времени с подсказкой, не слово
   в строке»). Слова «БОТ», «ЧЕЛОВЕК», «ЖДЁТ АДМИНИСТРАТОРА» и «авто:»
   из строки убраны: они стояли на каждой строке и ничего не различали. */
.dlg-when { display: inline-flex; align-items: center; gap: 6px; }
.dlg-who { display: inline-flex; color: var(--ink3); }
.dlg-who svg { width: 16px; height: 16px; }

.ch { width: 15px; height: 15px; flex-shrink: 0; }
.ch-tg { color: var(--ink2); }
.ch-wa { color: var(--ink2); }

/* ------------------------------------------------ лента */
.dlg-thread { min-height: 60vh; }

.thread-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-3);
  /* Закреплена сверху (владелец 07.09): на телефоне лента прокручивается
     страницей, и шапка уезжала — смена листала обратно, чтобы вернуть
     диалог боту. `top` — высота шапки приложения `.top` (она sticky, z 40);
     слой ниже неё, фон непрозрачный, иначе лента просвечивает сквозь. */
  position: sticky; top: var(--top-h); z-index: 30;
  background: var(--page);
}
@media (min-width: 1000px) {
  /* Внутри карточки ленты фон карточки; прокручивается сама `.thread`,
     sticky здесь лишь страховка. */
  .thread-head { top: 0; background: var(--raised); }
}
.thread-back {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--tap); min-height: var(--tap);
  border-radius: var(--radius-md);
  color: var(--ink); flex-shrink: 0;
}
.thread-back:hover { background: var(--press); }
.thread-who { flex: 1; min-width: 0; }
.thread-who .nm { display: flex; align-items: center; gap: 7px; font-weight: 500; min-width: 0; }
/* Ник обрезается многоточием, а не переносом: шапка — ОДНА строка на
   любой ширине (макет владельца 07.09). Кнопки справа — иконки на тап-зону,
   их ширина известна, и ник получает всё остальное. */
.thread-nick { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Иконочные кнопки шапки (владение, карточка) — те же токены, что у
   `.btn-ghost`: рамка `--line`, скругление `--r-control`, светлый фон,
   нажатие `--press`. На десктопе без рамки они читались квадратами вне
   стилистики (замечание владельца 07.09). Ширина — тап-зона, без текста. */
.thread-card, .thread-head .own-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--tap); min-height: var(--tap); padding: 0; flex-shrink: 0;
  border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--raised); color: var(--ink); cursor: pointer;
}
.thread-card:hover, .thread-head .own-btn:hover { background: var(--press); }
/* Иконка владения = СОСТОЯНИЕ (владелец 07.09): у бота — робот, у вас —
   рука; какая видна, решает класс `is-mine`, а не подмена разметки. */
.thread-head .own-btn .own-hand { display: none; }
.thread-head .own-btn.is-mine .own-robot { display: none; }
.thread-head .own-btn.is-mine .own-hand { display: block; }
.thread-head .own-btn.is-mine { background: var(--accent-soft); border-color: var(--accent); }
/* Слово состояния — внутри кнопки владения (владелец 07.09): «бот» /
   «у вас». Отдельный чип на 380 не оставлял нику места. Без права записи
   кнопки нет — слово стоит само, тем же видом. */
.own-word { font-size: var(--text-micro); font-weight: 500; letter-spacing: .02em; white-space: nowrap; }
.own-word-solo {
  flex-shrink: 0; padding: 3px 9px; border-radius: var(--radius-md);
  background: var(--raised); border: 1px solid var(--line); color: var(--ink2);
}
.own-word-solo.is-mine { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }
/* Поля кнопки и зазор в нике — впритык по замеру 380 (07.09): при слове
   «у вас» нику нужно 120px, было 116; здесь +6, в `.nm` ещё +1. */
.thread-head .own-btn { gap: 5px; padding: 0 8px; }
.thread-head .thread-who .nm { gap: 6px; }
@media (max-width: 999px) {
  /* Пять элементов в одной строке на 380: зазоры 6px (замер 07.09: при
     «у вас» нику нужно 120px, с зазором 8 выходило ровно 120, запаса
     ноль). Правило стоит ПОСЛЕ базового: побеждает позднее. */
  .thread-head { gap: 6px; }
}

/* Примечание о будущем этапе — тихая строка `meta`, без рамки и без
   пунктира (книга: «пунктирных рамок и цветных подложек „для внимания“
   в системе нет»). Смысл прежний: это не исход операции и не действие,
   а пояснение, которое живёт постоянно. */
.stage-note {
  padding: 0;
  margin-bottom: var(--space-3);
  font-size: var(--text-meta); line-height: var(--line-meta); color: var(--ink2);
}
/* `.stage-note-block` снят 25.09.2026 вместе со своей единственной
   разметкой — строкой «подключение новых каналов…» на «Интеграциях»
   (п.4). Мёртвый класс не оставляем: он вернулся бы в разметку
   «по образцу» (урок `.btn-quiet` на «Текстах писем»). Сам
   `.stage-note` живёт дальше — его носит строка «демо-данные ·
   WhatsApp пока не подключён» в «Диалогах». */

.thread {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-2) 0 var(--space-4);
  flex: 1;
  overflow-y: auto;
}
.msg { max-width: 78%; padding: 10px 13px; border-radius: var(--radius-lg); font-size: var(--text-body);
       animation: rise 260ms cubic-bezier(.2,.7,.3,1) both; }
.msg.in  { align-self: flex-start; background: var(--raised); border: 1px solid var(--line);
           }
.msg.out { align-self: flex-end; background: var(--accent-soft); border: 1px solid var(--accent);
           }
.msg.out.by-admin { background: var(--accent-soft); border-color: var(--accent-soft); color: var(--ink); }
.msg-text { white-space: pre-wrap; word-break: break-word; }
.msg-meta { margin-top: 4px; font-size: var(--text-micro); opacity: .65; }

/* Вложение директа (№520). Ширина — по пузырю и НЕ шире: `.msg` уже
   ограничен 78 % ленты, поэтому здесь достаточно 100 % от него. Высота
   свободная — кадры приходят и вертикальные, и горизонтальные. */
.msg-media + .msg-media { margin-top: var(--space-2); }
.msg-media img, .msg-media video {
  display: block; width: 100%; max-width: 100%; height: auto;
  border-radius: var(--radius-lg); background: var(--accent-soft);
}
/* Запасной путь виден, только когда показать не удалось: подпись Meta
   временная (замер 08.09 — живёт минимум 39 часов, верх не измерен). */
.msg-media-link { display: none; word-break: break-all; }
.msg-media.is-broken img, .msg-media.is-broken video { display: none; }
.msg-media.is-broken .msg-media-link { display: inline-block; }
/* Документ Telegram (№544) без превью — ссылка ОБЯЗАНА быть видна сразу,
   не по `onerror`: у документа нет ни img, ни video, значит и события
   ошибки загрузки не будет никогда, а класс `is-broken` ставит только
   оно. До этой строки правило `.msg-media-link { display:none }` выше
   держало ссылку скрытой навсегда — смена видела пустой пузырёк с одним
   временем (разведка 18.09). `is-file` ставит сервер (`_media_of`), не
   JS: единственный `how`, у которого превью нет и не будет никогда. */
.msg-media.is-file .msg-media-link { display: inline-block; }

/* ------------------------------------------------ поле ответа */
.composer {
  position: sticky; bottom: 0;
  display: flex; align-items: flex-end; gap: var(--space-2);
  padding: var(--space-2) 0 calc(var(--bar-h) + var(--space-3) + env(safe-area-inset-bottom));
  background: linear-gradient(to bottom, rgba(var(--page-rgb),0), var(--page) 22%);
}
.composer textarea {
  flex: 1; resize: none; max-height: 130px;
  border-radius: var(--radius-md); padding: 11px var(--space-4);
  line-height: 1.4;
}
/* Круглая — только кнопка отправки, а не всякая кнопка внутри composer:
   `padding: 0` и `border-radius: 50%` поймали кнопки подсказки-отказа,
   и текст «Написать в WhatsApp» вылез за края овала. Тот же класс дефекта,
   что №30 в BACKLOG: правило под конкретный элемент, записанное на его
   контейнер. Селектор теперь называет саму кнопку. */
.composer > button[type="submit"].btn {
  min-width: 52px; border-radius: var(--radius-full); padding: 0; min-height: 48px;
}
.composer.disabled { display: block; }
.composer-note {
  padding: var(--space-3);
  background: var(--raised); border: var(--rule);
  border-radius: var(--radius-lg);
  font-size: var(--text-control); color: var(--ink2); text-align: center;
}

/* ------------------------------------------------ тумблер */
.switch {
  width: 58px; height: 32px;
  border-radius: var(--radius-md);
  border: 1px solid var(--accent);
  background: var(--sunken);
  padding: 3px;
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--ease), border-color var(--ease);
}
.switch .knob {
  display: block; width: 24px; height: 24px;
  border-radius: var(--radius-full);
  background: var(--accent);
  transition: transform var(--ease), background var(--ease);
}
.switch.on { background: var(--ink); border-color: var(--ink); }
.switch.on .knob { transform: translateX(26px); background: var(--raised); }

.check { display: flex; align-items: center; gap: 10px; font-weight: 500; }

/* ------------------------------------------------ витрина интеграций */
.cards-grid { display: grid; gap: var(--space-3); }
.integration.stage2 { opacity: .92; }
.live-dot {
  width: 7px; height: 7px; border-radius: var(--radius-full);
  background: var(--ok-ink);
  animation: live 2.4s ease-in-out infinite;
}
@keyframes live {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--ok-ink-rgb), .5); }
  50%      { box-shadow: 0 0 0 6px rgba(var(--ok-ink-rgb), 0); }
}

/* ------------------------------------------------ десктоп */
@media (min-width: 1000px) {
  /* Панель «Ещё» уходит вместе с нижней панелью — тем же порогом. */
  .more-sheet, .more-scrim, .more-tab { display: none; }


  /* «Диалоги» на десктопе — экран-приложение на всю высоту окна (№213).
     До 02.09 колонки держали `max-height: calc(100vh - 190px)` при
     `align-items: start`: 190 — магическое число, не равное каркасу
     (на ≥1000px нет ни шапки, ни нижней панели), список обрывался
     посреди карточки, а под правой колонкой оставалась пустота со строкой
     сборки. Теперь высоту задаёт сама страница: `main.wrap-dialogs` —
     колонка на 100dvh, сетка колонок занимает остаток над строкой
     сборки, каждая колонка прокручивается внутри себя. `dvh`, не `vh`:
     прецедент — `.login-page`; запрет №373 — про семейство 100vw
     (ширина при резерве полосы прокрутки), к высоте не относится.
     Правка держится в границах `.wrap-dialogs` намеренно: высота на общем
     `.wrap` сделала бы «Записи» не длиннее окна и сломала бы шов резерва
     полосы (`tests/browser/test_scrollbar_gutter.py`). */
  .wrap-dialogs {
    display: flex; flex-direction: column;
    height: 100dvh; box-sizing: border-box;
  }
  .dialogs {
    display: grid;
    /* Ширина панели списка ФИКСИРОВАНА (решение владельца 25.09.2026):
       было `minmax(280px, 360px)` — колонка дышала вместе с шириной окна,
       и вместе с ней ездили имена и время в строках. Книга требует, чтобы
       элемент одного вида стоял в одних координатах («Одно и то же —
       в одном месте»); длинное имя теперь режется многоточием, а не
       растягивает колонку. */
    grid-template-columns: 360px minmax(0, 1fr);
    gap: var(--space-4);
    align-items: stretch;
    flex: 1; min-height: 0;
  }
    /* Тот же запас, что у колонок «Внешнего вида»: список прокручивается
     сам, и обводка выбранной строки иначе срезается краем. */
.dlg-list {
    min-height: 0; overflow-y: auto;
    padding-inline: var(--ring-reach);
    margin-inline: calc(var(--ring-reach) * -1);
  }
  .dlg-thread {
    display: flex; flex-direction: column;
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 0 var(--space-4);
    min-height: 0;
  }
  .wrap-dialogs .build-mark { flex: none; }
  .thread-back { display: none; }
  /* Лента прокручивается САМА (`.thread`, `overflow-y: auto`), а боковые
     отступы до 29.09.2026 давала карточка `.dlg-thread`. Классическая
     полоса прокрутки (Windows, Linux — у владельца именно такая) занимает
     ширину ВНУТРИ `.thread`, у правого края: исходящий пузырь
     (`align-self: flex-end`) вставал вплотную к полосе, а входящий имел
     свои 16px до края карточки — отсюда «справа прижато сильнее».
     Замер 29.09 (1440px): прокручивается именно `.thread`, 3985px
     содержимого, собственных боковых отступов у неё не было вовсе.

     Лечение — не добавленный отступ справа, а перенос отступа ВНУТРЬ
     прокручиваемого: лента растягивается на всю ширину карточки
     (отрицательные поля), содержимое получает те же 16px с обеих сторон,
     полоса рисуется у края карточки и ширину у содержимого не отнимает.
     Тот же приём стоит выше у `.dlg-list`.
     ⚠️ Только десктоп: на 390 прокручивается страница, полосы у `.thread`
     нет вовсе, а отступы даёт `.wrap` — там стороны уже равны (замер:
     0 и 0 относительно карточки). */
  .thread {
    max-height: none;
    padding-inline: var(--space-4);
    margin-inline: calc(var(--space-4) * -1);
  }
  .composer { padding-bottom: var(--space-3); background: var(--raised); }
  .cards-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
}

@media (prefers-reduced-motion: reduce) {
  .msg, .more-sheet, .more-scrim, .switch .knob, .live-dot { animation: none !important; }
  .more-sheet { transition: none !important; }
}

/* ============================================================
   ВЛАДЕНИЕ ДИАЛОГОМ, НАСТРОЙКИ, МОДАЛКА ПРОДАЖИ, ЛОГОТИП
   ============================================================ */

/* ------------------------------------------------ логотип-картинка */
.logo {
  display: block;
  width: 46px; height: 46px;
  border-radius: var(--radius-full);
  background: none;
  object-fit: cover;
  flex-shrink: 0;
  /* крем на кремовом фоне сливался бы — держим тонкий контур */
  box-shadow: 0 0 0 1px rgba(var(--ink-rgb), .18), 0 2px 8px rgba(var(--ink-rgb), .10);
}
.login-card .logo {
  width: 88px; height: 88px;
  margin: 0 auto var(--space-4);
  box-shadow: 0 0 0 1.5px rgba(var(--ink-rgb), .22), 0 10px 26px rgba(var(--ink-rgb), .18);
}

/* ------------------------------------------------ два ряда навигации на десктопе */
.nav-row { display: flex; gap: var(--space-1); }

/* ------------------------------------------------ статусы диалога */
/* Срезов здесь ТРИ (28.09.2026): «Все», «Просят человека», «Без ответа».
   Сам ряд — сегмент (`.seg-three`, правило ниже по файлу); здесь остался
   только внешний отступ строки.
   ⚠️ `.dlg-filter .filter-chip` ниже — правило МЁРТВОЕ с перерисовки
   под сегмент: `.filter-chip` на «Диалогах» больше не рисуется. Оставлено
   до общей уборки чипов, а не потому, что работает. */
.dlg-filter { display: flex; gap: 6px; margin-bottom: var(--space-3); }
/* Подсказка о двух числах (№501): кружок на вкладке и чип «Неотвеченные» —
   про разное. Видимая, не title: на телефоне title не показывается. */
.dlg-hint { margin: calc(-1 * var(--space-2)) 0 var(--space-3); font-size: var(--text-meta); line-height: 1.35; }
.dlg-filter .filter-chip {
  flex: 1 1 0; min-width: 0; justify-content: center;
  padding: 0 var(--space-2); gap: 6px;
}/* ------------------------------------------------ чип-фильтр
   В системе фильтр — это СЕГМЕНТ. `.filter-chip` остался адресом
   разметки и швов на восьми экранах и потому приведён к тому же виду:
   подложки группы у него нет (чипы стоят россыпью), выбор показан
   подложкой `--accent-soft`, а не тёмной заливкой — выбор и действие
   не должны выглядеть одинаково. */
.filter-chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: var(--control-h); padding: 0 var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--line-strong);
  background: var(--raised);
  color: var(--ink2);
  font-size: var(--text-control); font-weight: 500;
  line-height: var(--line-control);
  white-space: nowrap; flex: none;
  text-decoration: none;
}
.filter-chip:hover { color: var(--ink); }
.filter-chip .chip-dot {
  width: var(--space-2); height: var(--space-2);
  border-radius: var(--radius-full); background: var(--accent);
}
/* Активность чипа — ОДИН класс `.is-on` (№769, заход 3). */
.filter-chip.is-on {
  background: var(--accent-soft); border-color: var(--accent); color: var(--ink);
}
.filter-chip.is-on .chip-dot { background: var(--accent); }
/* Счётчик — `--ink3` обычным весом, как у сегмента; ноль не рисуется,
   сам чип остаётся. */
.filter-chip .chip-count, .filter-chip .chip-n {
  color: var(--ink3); font-weight: 400; background: none; padding: 0; border: 0;
}

/* Живая навигация (№140): пока грузится ответ, старое содержимое
   остаётся на месте приглушённым — пустой экран хуже него. */
[data-live-swap].is-live-wait,
[data-live-page].is-live-wait { opacity: .55; transition: opacity .2s; }

.dlg-status {
  display: inline-block; margin-top: 5px;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: var(--text-micro); font-weight: 500; letter-spacing: .06em;
  border: 1px solid transparent;
}
.st-bot     { background: var(--raised); border-color: var(--line); color: var(--ink2); }
.st-admin   { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }
.st-waiting { background: var(--alert-bg); border-color: var(--alert-ink); color: var(--alert-ink); }

/* Окно ответа Meta в строке диалога (№492): тот же бейдж, что статус.
   Горящее — тревожные токены (время уходит), закрытое — приглушённое. */
.dlg-win {
  display: inline-block; margin-top: 5px; margin-left: 6px;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: var(--text-micro); font-weight: 500; letter-spacing: .06em;
  border: 1px solid transparent;
}
.dlg-win-hot    { background: var(--alert-bg); border-color: var(--alert-ink); color: var(--alert-ink); }
.dlg-win-closed { background: var(--raised); border-color: var(--line); color: var(--ink2); }


/* Системное событие — СТРОКА с точкой, а не плашка: в ленте таких строк
   подряд бывает пять (взял диалог, создана запись, списано с абонемента),
   и пять серых плашек читаются как сыпь. Точка держит цвет состояния. */
/* «Закрыть без ответа» (09.09) — полоса над полем ответа. Тише composer'а:
   это выход из тупика, а не основное действие, и спорить за внимание
   с полем ввода она не должна. */
.thread-close {
  display: flex; align-items: center; justify-content: flex-end;
  gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-2) var(--space-3) 0;
}
.thread-close-why { font-size: var(--text-meta); color: var(--ink2); }

.msg-system {
  align-self: center;
  max-width: 90%;
  padding: 2px 0;
  background: none;
  color: var(--ink2);
  font-size: var(--text-meta);
  text-align: center;
}

/* Разделитель дня в ленте (№395): переписка за несколько дней иначе
   читается как один вечер со скачущим временем. Плашка, не строка —
   должна ловить взгляд сильнее системной точки-события рядом. */
.thread-day {
  align-self: center;
  margin: var(--space-2) 0 2px;
  padding: 2px 10px;
  border-radius: var(--radius-lg);
  background: var(--accent-soft);
  color: var(--ink2);
  font-size: var(--text-micro); font-weight: 500;
  letter-spacing: .02em;
}

/* ------------------------------------------------ поля в две колонки */
.two-fields { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
/* Номер пары автоответчика стоит в слоте R1, то есть в круге `.line-av`,
   и круг сам центрует содержимое (`inline-flex` + `place`).
   ⚠️ СВОЕГО `display` ЗДЕСЬ БЫТЬ НЕ ДОЛЖНО (п.11, 25.09): `inline-block`
   перебивал `inline-flex` круга — правило `.pair-no` стоит НИЖЕ по файлу
   и побеждало каскадом, — и цифра садилась на базовую линию: в круге
   36×36 она оказывалась левее и выше центра. Дефект нашёлся глазами
   («цифры 1–4 в кружках не отцентрованы»), а не прогоном: геометрия
   оставалась чистой, просто цифра стояла не по центру. `min-width`
   ушёл по той же причине — ширину держит круг. */
.pair-no {
  color: var(--ink2); font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------ модалка */
body.modal-open { overflow: hidden; }
.modal {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: grid; place-items: center;
  padding: var(--space-4);
}

.modal-scrim {
  position: absolute; inset: 0;
  background: rgba(var(--ink-rgb), .45);
  animation: fade-in 200ms ease both;
}
.modal-card {
  position: relative;
  width: 100%; max-width: 460px;      /* как в эталоне: min(480px, 100%) с полями */
  max-height: 86vh; overflow-y: auto;
  padding: var(--space-4);
  background: var(--raised);
  border: 1px solid var(--ink);       /* окно держит линия, а не тень под ним */
  border-radius: var(--radius-lg);
}
/* Карточка с подвалом кнопок: скроллится ТЕЛО, а не карточка целиком.
   Иначе при высоком содержимом (смешанная оплата раскрывает «почему сумма
   другая» и комментарий) кнопки «Отмена» и «Принять» уезжают за нижний край
   и достаются только прокруткой внутри окна — человек читает это как
   «кнопок нет» и жмёт мимо. Раскладка та же, что у панели во весь экран:
   голова и подвал на месте, середина прокручивается. */
.modal-card.has-foot {
  display: flex; flex-direction: column;
  padding: 0;                 /* отступы у .panel-head / -body / -foot свои */
  overflow: hidden;           /* скролл забирает .panel-body */
}
.modal-card.has-foot .panel-body { flex: 1 1 auto; overflow-y: auto; }
.modal-card.has-foot .panel-foot { flex: 0 0 auto; background: var(--raised); }

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
/* ⚠️ Выезд окна (modal-in) убран в волне 3: он изображал появление «сверху»,
   то есть ту же глубину, от которой каркас отказался. Затемнение фона
   осталось — оно объясняет, что страница под окном сейчас недоступна. */

.modal-card > h2 { font-size: var(--text-section); margin-bottom: var(--space-2); }
/* ⚠️ Кнопки видны при ЛЮБОЙ высоте содержимого. У двух модалок для этого
   есть панельная раскладка `has-foot` (голова/тело/подвал), но у остальных
   шести её нет, а высота на 380px спокойно перебирает 86vh: правка остатка
   с двумя режимами, продажа с расчётом, передача с поиском. Липкий ряд
   кнопок решает то же самое, не трогая разметку — а разметку модалок
   сторожит смоук, и переверстка ради вида была бы худшим разменом.
   Дефект «не дотянуться до Принять» уже случался 10.08. */
.modal-card:not(.has-foot) > .btn-row {
  position: sticky; bottom: calc(-1 * var(--space-4));
  margin-top: var(--space-4);
  padding: var(--space-3) 0 0;
  background: var(--raised);
  border-top: var(--rule);
}

.summary { display: grid; grid-template-columns: auto 1fr; gap: 8px var(--space-3); margin: var(--space-4) 0 0; }
.summary dt { color: var(--ink2); font-size: var(--text-control); }
.summary dd { margin: 0; font-weight: 500; text-align: right; }

.just-sold { animation: just-sold 1600ms ease both; }
@keyframes just-sold {
  0%   { transform: scale(.98); box-shadow: 0 0 0 0 rgba(var(--accent-rgb), .8); }
  35%  { transform: scale(1);   box-shadow: 0 0 0 12px rgba(var(--accent-rgb), 0); }
  100% { transform: none;       }
}

@media (max-width: 899px) {
  .two-fields { grid-template-columns: 1fr; }
  /* Шапка диалога на узком НЕ переносится (07.09, отмена правила №498):
     текстовая кнопка ушла к полю ответа, справа остались две иконки
     на тап-зону — ник теряет 2×44px, а не 145, и умещается. */
  .modal-card { padding: var(--space-4); }
  .modal .btn { min-height: 50px; }
}

@media (prefers-reduced-motion: reduce) {
  .modal-card, .modal-scrim, .just-sold { animation: none !important; }
}

/* ------------------------------------------------ доступы */
.user-card.is-off { opacity: .6; }
.role-select {
  width: auto; min-width: 150px;
  min-height: 40px; padding: 6px var(--space-3);
  font-size: var(--text-control);
  flex-shrink: 0;
}
.session-card.is-revoked { opacity: .62; }
.session-card .badge { white-space: nowrap; }
/* п.5 (25.09): три строки с РАВНЫМИ промежутками и никакого пустого места.
   Слева — кто, роль с устройством, время входа; справа столбиком бейджи
   и кнопка, поэтому кнопка не занимает четвёртую строку. Промежуток между
   строками один (2px), своих `margin-top` у строк нет. */
.sess-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.sess-name { font-weight: 500; }
.sess-side {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--space-2); flex: none;
}
.session-card .row-acts { margin-top: 0; }
/* Два бейджа в углу карточки: «служебный» и статус. На узком экране они
   переносятся вниз строкой, а не выдавливают имя человека в столбик. */
.session-badges {
  display: flex; gap: 6px; align-items: center;
  flex-wrap: wrap; justify-content: flex-end;
}
/* Приглушённая метка: она поясняет происхождение входа, а не его состояние,
   и не должна спорить со статусным бейджем рядом. */
.badge-quiet { background: var(--sunken); color: var(--ink2); }
/* Отозванные внутри свёртки: отступ, чтобы карточки не липли к заголовку,
   и потолок по высоте — иначе через полгода блок раскрывается на экран
   с лишним. Скролл внутри, а не рост страницы. */
.old-sessions { margin-top: var(--space-3); }
.old-sessions-body {
  padding: 0 var(--space-3) var(--space-3);
  max-height: 60vh; overflow-y: auto;
}

.pass-box {
  margin-top: var(--space-4);
  padding: var(--space-4);
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius-lg);
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: var(--text-num); font-weight: 500;
  letter-spacing: .04em;
  text-align: center;
  word-break: break-all;
  user-select: all;
}

@media (max-width: 899px) {
  /* на телефоне селект роли уезжает под имя, иначе строка ломается */
  .user-card .card-row { flex-direction: column; align-items: stretch; gap: var(--space-2); }
  .role-select { width: 100%; min-height: var(--tap); }
  .pass-box { font-size: var(--text-section); }
  #pass-copy { min-height: 52px; }
}

/* ============================================================
   ЗАДАЧИ ↔ ДИАЛОГИ, ВОЗМОЖНОСТИ И ЦЕНЫ, СТАТИСТИКА
   ============================================================ */

/* ------------------------------------------------ действие в задаче */
.task-action { margin-top: 10px; }
.task-action .btn { width: 100%; justify-content: flex-start; gap: 10px; }
.mini-link { font-size: var(--text-meta); }

.task-strip {
  display: flex; align-items: center; gap: 8px;
  padding: 9px var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--alert-bg);
  border: 1px solid var(--alert-ink);
  border-radius: var(--radius-lg);
  color: var(--alert-ink);
  font-size: var(--text-meta); font-weight: 500;
  text-decoration: none;
}
.task-strip-go { margin-left: auto; opacity: .75; font-weight: 500; }

/* Этап по ссылке из переписки (№763): «получил / открыл / остановился
   на шаге / записался». Не тревога и не задача — справка, поэтому тон
   спокойный: та же рамка, что у карточек, без цветной заливки. Цвет
   даёт только дошедший до записи (`is-done`) — единственное состояние,
   при котором смене делать уже нечего. Hex здесь нет: палитра живёт
   в `[theme]` вертикали и приезжает токенами. */
.lead-stage {
  padding: 9px var(--space-3);
  margin-bottom: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  color: var(--ink2);
  font-size: var(--text-meta);
}
.lead-stage.is-done { color: var(--ok-ink); border-color: var(--ok-ink); }

/* ------------------------------------------------ возможности и цены */
/* Вводные абзацы `.lead` сняты целиком (решение владельца 05.09):
   подсказка первого дня, смена её не читает, а первый экран она съедала.
   Класс не оставлен «на всякий случай» — мёртвый стиль зовёт писать
   новые вводные. */

/* Стили подборщика возможностей (`.feat*`, `.total-*`) сняты 28.09.2026
   вместе с самим подборщиком — решение владельца. Классы не оставлены
   «на всякий случай» по тому же правилу, что снятые выше `.lead`:
   мёртвый стиль зовёт вернуть блок, которого решили не держать.
   ⚠️ `.pick*` НЕ снят: его держат «Записи» и «Абонементы». */

.plans { display: grid; gap: var(--space-3); }
/* Довод за год (правка 29.09.2026, решение владельца): подпись про
   подарочный месяц стоит В РЯДУ кнопки «назад», прижатая вправо,
   а переключатели — строкой НИЖЕ, тоже вправо (`.ctl-switches`). Так
   подпись стоит прямо над своим переключателем «Месяц / Год», а пустота
   под заголовком страницы занята. Прежняя полоса `.plan-bar` (довод
   слева, переключатели справа, отдельной строкой под заголовком) снята
   вместе с классом — второго вида этого ряда не держим.
   ⚠️ На телефоне ряд `.ctl` не переносится (`flex-wrap: nowrap`),
   поэтому подпись обязана УМЕТЬ СЖАТЬСЯ: `min-width: 0` разрешает ей
   лечь в две-три линии вместо того, чтобы растолкать ряд вбок. */
.plan-year-note {
  margin: 0;
  font-size: var(--text-body); line-height: var(--line-body);
  color: var(--ink); font-weight: 500;
}
.ctl > .plan-year-note { flex: 0 1 auto; min-width: 0; margin-left: auto; text-align: right; }
.plan { display: flex; flex-direction: column; }
.plan.accent {
  border: 2px solid var(--ink);
  background: var(--accent-soft);
}
.plan-head { display: flex; align-items: center; gap: var(--space-2); }
.plan-head h2 { font-size: var(--text-body); }
.plan-badge {
  padding: 3px 10px; border-radius: var(--radius-sm);
  background: var(--ink); color: var(--raised);
  font-size: var(--text-micro); font-weight: 500; letter-spacing: .04em;
}
.plan-price {
  font-family: var(--font);
  font-weight: 500; font-size: 34px;
  margin-top: var(--space-3);
  line-height: 1;
}
.plan-price-note { font-size: var(--text-meta); font-weight: 500; color: var(--ink2); }
.plan-recurring {
  font-family: var(--font);
  font-style: normal; font-weight: 500; font-size: var(--text-num); color: var(--ink2);
  margin-top: 4px;
}
.plan-points { margin: var(--space-4) 0 0; padding: 0; list-style: none; }
.plan-points li {
  position: relative; padding-left: 22px; margin-bottom: 8px; font-size: var(--text-control);
}
.plan-points li::before {
  content: ''; position: absolute; left: 2px; top: 7px;
  width: 7px; height: 7px; border-radius: var(--radius-full);
  background: var(--accent);
}
.plan.accent .plan-points li::before { background: var(--ink); }

.fineprint {
  margin: var(--space-6) 0 0;
  font-size: var(--text-meta); color: var(--ink2); text-align: center;
}

/* ------------------------------------------------ статистика */
.bar-row { margin-bottom: var(--space-3); }
.bar-row:last-child { margin-bottom: 0; }
/* `.bar`, `.bar i`, `.bar-head`, `.bar-name`, `.bar-num` СНЯТЫ 25.09.2026
   (п.13): разметки с ними в проекте нет ни одной (греп по шаблонам, JS
   и роутам), а заливка у полосы была градиентом «акцент → чернила» —
   то есть ровно тем, что владелец увидел как «фиксированный тёмный».
   Мёртвый стиль с таким цветом опаснее прочих: он возвращается в новый
   экран копией «как у соседа». Живые шкалы — `.fn-bar` (воронка),
   `.sub-progress` (остаток абонемента), `.day-col` (выручка по дням)
   и кольцо `.ring-fill` (загрузка месяца). */

/* Ряд метрик (книга, блок MetricCard, правка 24.09): подпись `meta`
   СВЕРХУ, число `numeral` снизу, до пяти в ряду в одну строку на
   1280px, на телефоне сетка 2 колонки. Классы прежние (`.stat-grid`,
   `.card.stat`, `.stat-num`, `.stat-label`) — это адреса разметки
   и швов; поменялись вид и порядок чтения. Порядок задаёт
   `column-reverse`, а не перестановка в двадцати шаблонах: разметка
   у метрики одна, и она уже правильная по смыслу («вот число, вот что
   это»), просто читается снизу вверх. */
.stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
             /* тот же токен, что у `.mets` (п.16) — см. комментарий там */
             gap: var(--space-3); margin-bottom: var(--space-6); }
/* Порядок внутри метрики — подпись, число, уточнение (книга, «Текст
   в тесноте»: «Числа в ряду метрик стоят на одной высоте. Подпись
   метрики — одна строка; уточнение („по 10 визитам“) — строка `meta`
   под числом, не вторая строка подписи»).
   ⚠️ Было `column-reverse`, и уточнение вставало НАД подписью: у соседей
   по ряду оно есть не у всех, высота шапки гуляла, и числа стояли
   ступеньками. Теперь порядок прямой, подпись одной строкой (`nowrap`
   ниже), уточнение — последним: число оказывается на одной высоте
   у всех карточек ряда по построению, а не подбором отступов. */
.stat { display: flex; flex-direction: column; justify-content: flex-start;
        text-align: left; padding: var(--space-3) var(--space-4);
        margin-bottom: 0; }
.stat-num {
  font-family: var(--font); font-weight: 500;
  font-size: var(--text-num); line-height: var(--line-num);
  font-variant-numeric: tabular-nums;
  margin-top: 2px;
  /* ⚠️ Число и единица не разрываются (№493): «1755 лари» на 390px ломалось
     на две строки — сначала «1755», под ним «лари», — и плитка читалась
     как две разные величины.
     ⚠️ Но одного `nowrap` мало, и это выяснилось замером: у шестизначной
     суммы неразрывная строка распирала страницу на 26px — горизонтальная
     прокрутка на телефоне. Поэтому вместе с запретом переноса кегль
     на узком ужимается по ширине плитки (`clamp` + `cqw` не нужен: хватает
     вьюпорта, плитки занимают половину экрана). */
  white-space: nowrap;
}
.stat-label { font-size: var(--text-meta); line-height: var(--line-meta);
              color: var(--ink2); white-space: nowrap;
              overflow: hidden; text-overflow: ellipsis; }

/* Прежней подгонки кегля под ширину плитки (`clamp`, №493) больше нет:
   число теперь `numeral` 22px, а единица уехала в заголовок ряда —
   шестизначная сумма помещается на 390px без ужимания. */
/* Уточнение под числом — `meta` `ink-2` (книга, «Текст в тесноте»).
   Было `body` 15px весом 500: строка спорила с самим числом за внимание,
   хотя объясняет его. Кегль и вес сняты, место — под числом. */
.stat-week {
  font-family: var(--font); font-style: normal; font-weight: 400;
  font-size: var(--text-meta); line-height: var(--line-meta);
  color: var(--ink2); margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.link-box {
  display: flex; align-items: center; gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-3); padding: var(--space-3);
  background: var(--accent-soft); border: 1px solid var(--accent);
  border-radius: var(--radius-lg);
}
.link-box code { flex: 1; min-width: 0; font-size: var(--text-meta); word-break: break-all; }
.link-preset { display: flex; align-items: center; gap: var(--space-3); }
.preset-url {
  display: block; font-size: var(--text-meta); color: var(--ink2);
  word-break: break-all; margin-top: 2px;
}

/* ---------------------------------------------- метки источников (09.09)
   Строка на метку: имя и ключ рядом, счёт справа, действия иконками.
   Своих цветов здесь нет — только токены, как во всём файле.

   ⚠️ Это НЕ `.line-row` (правило №433/№458), и разница смысловая: тот класс
   про строку-цель, которую нажимают целиком, — вместе с курсором, волной
   касания и подсветкой нажатия. Здесь целей несколько и все они внутри
   строки, сама строка не нажимается. Взяв `.line-row`, разметка обещала бы
   пальцу нажатие, которого нет. */
.label-row {
  display: flex; align-items: center; gap: var(--space-3);
  padding: 6px var(--space-3);
  border-bottom: var(--rule);
}
.label-row:last-child { border-bottom: none; }
.label-who {
  flex: 1; min-width: 0;
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--space-2);
}
.label-name { font-weight: 500; }
/* Ключ — то единственное, чем метки различаются; он стоит РЯДОМ с именем,
   а не мелким шрифтом под ним (решение владельца 09.09). */
.label-key {
  font-size: var(--text-meta); color: var(--ink2);
  word-break: break-all;
}
/* Вторая и следующие метки того же названия: стрелка связывает их с первой
   строкой группы. До этого четыре метки «Instagram» стояли одинаковыми
   карточками, и какая из них какая — было видно только по мелкому ключу. */
.label-row.is-tied .label-name { font-weight: 500; color: var(--ink2); }
.label-row.is-tied .label-name::before { content: '↳ '; }
.label-num {
  font-variant-numeric: tabular-nums; text-align: right;
  min-width: 5ch; flex-shrink: 0; font-size: var(--text-meta);
}
.label-acts { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* Место парной ссылки там, где её нет и быть не может (deep-link канала):
   столбец иконок не должен съезжать от строки к строке. */
.btn-ico.is-empty { border: none; background: none; pointer-events: none; }
/* Ответ кнопки-иконки на копирование — состояние, а не подпись:
   подмена текста снесла бы саму иконку (см. `copyWithFeedback`). */
.btn-ico.is-copied { border-color: var(--accent); background: var(--accent-soft); }
.btn-ico.is-copy-failed { border-color: var(--wait-ink); background: var(--wait-bg); }

/* ------------------------------ строка ведомости: зарплаты и партнёрка (09.09)
   Один вопрос на обеих страницах — «кому сколько отдать», — поэтому и строка
   одна: имя и состав слева, сумма справа, итог последней строкой той же
   ведомости.

   ⚠️ Здесь стоял общий `.task`, и его `align-items:center` работал, пока
   `.task` был СТРОКОЙ. В №482 он стал колонкой под карточку задачи, и то же
   свойство молча превратилось в горизонтальное центрирование: содержимое
   встало посередине, сумма упала под строку. Своя строка, а не возврат
   `.task`: у ведомости и у задачи разная форма, и следующая правка `.task`
   сломала бы это снова.

   ⚠️ Это НЕ `.line-row` — по той же причине, что у `.label-row` выше:
   строка ведомости не нажимается целиком, нажимается имя. */
/* 15.09 (№493, доделка): той же строкой стали визиты и документы карточки
   клиента и приведённые клиенты партнёра — у них был тот же `.task`
   с `align-items:center` и та же полоса слева (замер: текст с 116-го
   пикселя из 415, с 255-го из 1001). */
/* ⚠️ Выравнивание по ВЕРХУ, а не по центру: при раскрытой расшифровке
   строка становится в двадцать раз выше, и центрированная сумма уезжала
   на её середину — далеко от имени, к которому относится. */
/* Полоса выплаты под ведомостью (№113, 29.09.2026): что из начисленного уже
   выдано на руки, и кнопка отметки. СНАРУЖИ карточки ведомости — это не ещё
   одна строка ведомости, а действие над ней; цифры слева, действие справа,
   на узком экране — в столбик. */
.paid-bar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-2) var(--space-3); margin-top: var(--space-3);
}
.paid-nums {
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3);
  font-size: var(--text-meta); color: var(--ink2); min-width: 0;
}
/* Выданное — обычной подписью, невыданное — заметнее: именно оно требует
   действия. Цвет статуса, а не акцент: акцент держит главная кнопка. */
.paid-left { color: var(--wait-ink); }

.payline {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-3);
  padding: 10px var(--space-3);
  border-bottom: var(--rule);
}
.payline:last-child { border-bottom: none; }
/* Основа 10rem: широкий бейдж справа («пришёл, без оплаты», 158px) на 390
   сжимал текст визита до 119px — дата в две строки, состав в пять.
   Уже 10rem текст не сжимается: правая часть уходит строкой ниже. */
.payline-who { flex: 1 1 10rem; min-width: 0; }
.payline-name {
  font-weight: 500; color: var(--ink); text-decoration: none;
  border-bottom: 1px solid var(--line);
}
a.payline-name:hover { border-color: var(--ink); }
/* Итоговая строка не ссылка — подчёркивать её нечем и незачем. */
span.payline-name { border-bottom: none; }
.payline-meta {
  font-size: var(--text-meta); color: var(--ink2); margin-top: 2px;
}
/* Сумма читается как деньги, а не как чип: цифры моноширинно, кегль
   крупнее строки. Столбец сумм — то, ради чего страницу открывают. */
.payline-sum {
  font-variant-numeric: tabular-nums; font-weight: 500;
  font-size: var(--text-section); white-space: nowrap; flex-shrink: 0;
}
/* Ноль долга не требует действия и не должен спорить за внимание
   с непустыми суммами соседних строк. */
.payline-sum.is-quiet { color: var(--ink2); font-weight: 500; }
.payline.is-total {
  background: var(--sunken); border-top: var(--rule);
  border-bottom: none;
}
.payline.is-total .payline-name { font-weight: 500; }
.payline.is-total .payline-sum { font-weight: 500; }

/* Расшифровка начислений (№13, C3.3) — свёрнута, открывают при споре.
   Колонки выровнены: строка расшифровки читается как выписка, а не как
   склеенная фраза через точки. */
/* Раскрывашка — это САМА строка состава (`.payline-meta` на `<summary>`):
   отдельная строка «расшифровка» стоила по 28 px на мастера ни за что.
   Маркер браузера убран, его роль берёт слово «расшифровка» в конце строки. */
.payout-lines > summary { cursor: pointer; list-style: none; }
.payout-lines > summary::-webkit-details-marker { display: none; }
.payout-lines > summary:hover { color: var(--ink); }
.payout-lines[open] > summary { margin-bottom: 4px; }
/* ⚠️ Боковой отступ — ТОТ ЖЕ, что у строки спойлера (`.payline`, п.2
   пятого круга №783, 26.09). Причина, по которой его не стало: раскрывашка
   переехала (п.6 четвёртого круга) — `<details>` стоял ВНУТРИ
   `.payline-who`, то есть внутри уже отбитой строки, и отступ достался
   расшифровке даром; когда `<summary>` стала самой строкой, тело
   спойлера оказалось прямым ребёнком `<details>` и легло вплотную
   к рамке карточки. Отступ теперь свой и назван числом рядом с тем,
   которое он повторяет. */
.payout-line {
  display: flex; align-items: baseline; gap: 0 var(--space-3);
  font-size: var(--text-meta); color: var(--ink2);
  padding: 3px var(--space-3); border-top: var(--rule);
}
.payout-line > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.payout-line .pl-day { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
.payout-line .pl-who { flex: 1 1 30%; min-width: 0; color: var(--ink); }
/* Пояснение уступает место имени: у обычного начисления оно повторяет
   колонку минут, а имя человека — то, по чему строку ищут глазами. */
.payout-line .pl-note { flex: 1 1 20%; min-width: 0; }
.payout-line .pl-min,
.payout-line .pl-sum { flex: 0 0 auto; font-variant-numeric: tabular-nums; text-align: right; }
.payout-line .pl-min { min-width: 7ch; }
.payout-line .pl-sum { min-width: 9ch; color: var(--ink); }

/* ------------------------------------------------ меню строки (09.09)
   Редкие действия строки под «⋯». Механика — родной `<details>`: раскрытие,
   фокус и клавиатура браузерные, своего состояния нет. Закрытие по клику
   мимо и по Esc — одной точкой в `app.js`, не копией на каждом экране. */
.row-menu { position: relative; }
.row-menu > summary { list-style: none; }
.row-menu > summary::-webkit-details-marker { display: none; }
.row-menu-list {
  /* Сторона раскрытия живёт ЗДЕСЬ: `app.js` поднимает тело в `fixed`
     (книга: «выпадающее не обрезается никаким предком») и читает
     `--pop-align`. Вид остаётся в файле стилей, скрипт его не решает. */
  --pop-align: right;
  position: absolute; right: 0; top: calc(100% + 4px); z-index: var(--z-menu);
  min-width: 232px; padding: 5px;
  background: var(--raised);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  box-shadow: 0 0 0 1px rgba(var(--ink-rgb), .06),
              0 8px 24px rgba(var(--ink-rgb), .14);
}
.row-menu-item {
  display: flex; align-items: center; width: 100%;
  min-height: var(--tap); padding: 0 var(--space-3);
  background: none; border: none; border-radius: var(--radius-md);
  color: var(--ink); font: inherit; font-size: var(--text-control);
  text-align: left; cursor: pointer;
}
.row-menu-item:hover { background: var(--press); }
.row-menu-item.is-danger { color: var(--alert-ink); }
@media (max-width: 899px) {
  /* Вверх, а не вниз: снизу стоит фиксированный таббар, и раскрытое вниз
     меню пряталось бы под ним вместе с последним пунктом. Правило
     осталось запасным путём НА СЛУЧАЙ БЕЗ СКРИПТА: с `app.js` сторону
     выбирает замер места (таббар в нижнюю границу входит), и он
     перебивает эти строки своим `top`. */
  .row-menu-list { top: auto; bottom: calc(100% + 4px); }
}

@media (min-width: 900px) {
  /* ⚠️ Правило про три тарифа оставлено: `.plans` без модификатора
     читают другие экраны. Сетка подписок с 28.09.2026 — ЧЕТЫРЕ тарифа
     (`.plans-four`), и блока «Разовая» под ней больше нет.
     На 900px четыре карточки в ряд дали бы ~210px на карточку — цена,
     лимит и список в такую ширину не садятся, поэтому здесь 2×2,
     а в один ряд они встают с 1200px. */
  .plans { grid-template-columns: repeat(3, 1fr); }
  .plans-four { grid-template-columns: repeat(2, 1fr); }
  .stat-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  /* №806: ряд ИЗ ТРЁХ метрик («Партнёрка»). Модификатор нужен только
     от 900px и выше: ниже работает базовая сетка компонента — две
     колонки, как требует книга (S4: «на телефоне сетка 2 колонки»).
     Стоял инлайновым стилем в шаблоне и потому действовал на всех
     ширинах разом; `minmax(0, 1fr)` здесь не для красоты — без него
     дорожка не сжимается меньше содержимого, и ряд распирал страницу. */
  .stat-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Пять метрик в одну строку — на 1280px (книга, MetricCard, правка
   24.09): «Записи и посещения» несёт пять карточек, и на 900–1279px
   четвёртая с пятой ещё переносятся строкой ниже — это законный
   промежуточный шаг, а не нарушение. */
@media (min-width: 1280px) {
  .stat-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

@media (max-width: 899px) {
  .task-action .btn { min-height: 48px; }
  .link-preset { flex-direction: column; align-items: stretch; }
  .link-preset .btn { width: 100%; }
  .plan-price { font-size: var(--text-title); }
}

@media (prefers-reduced-motion: reduce) {
  .bar i { transition: none !important; }
}

/* ---------------------------------------- кнопка выбора («Записи», «Абонементы»)
   ⚠️ Заголовок раньше говорил «конфигуратор на /pricing» — подборщик снят
   28.09.2026, а `.pick` остался: им пользуются окно записи и продажа
   абонемента. Имя класса общее, и это единственный его дом. */
.pick {
  min-height: 40px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--raised);
  font-family: var(--font);
  font-size: var(--text-meta); font-weight: 500; letter-spacing: .02em;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  touch-action: manipulation;
  transition: background var(--ease), color var(--ease);
}
.pick:active { background: var(--ink); transition-duration: .1s; }
.pick .pick-off { display: none; }
.pick:not(.on) {
  background: transparent;
  color: var(--ink);
  border-color: var(--accent);
}
.pick:not(.on):active { background: var(--press); }
.pick:not(.on) .pick-on { display: none; }
.pick:not(.on) .pick-off { display: inline; }

/* «в подписке просто всё» — подпись, ради которой затевался контраст */
.plan-allin {
  margin-top: 8px;
  font-family: var(--font);
  font-style: normal; font-weight: 500; font-size: var(--text-section);
  color: var(--ink2);
}
.plan.accent .plan-allin { color: var(--ink); }

@media (max-width: 899px) {
  .pick { min-height: var(--tap); font-size: var(--text-meta); padding: 0 var(--space-4); }
  .feat-foot { flex-direction: column; align-items: stretch; gap: 10px; }
  .feat-foot .badge { align-self: flex-start; }
  .pick { width: 100%; }

  /* Итог виден всегда: на телефоне карточек одиннадцать, и без липкой
     строки после третьего клика непонятно, что вообще пересчиталось. */
  .total-line {
    position: sticky;
    bottom: calc(var(--bar-h) + env(safe-area-inset-bottom));
    z-index: 30;
    background: var(--raised);
    border-radius: var(--radius-lg);
    border: 1px solid var(--line);
    padding: var(--space-3) var(--space-4);
  }
}

/* ============================================================
   МОБИЛЬНАЯ ПЛОТНОСТЬ И КОМПАКТНЫЙ ТАБ-БАР
   Всё внутри @media (max-width: 899px) — десктоп не трогаем.
   ============================================================ */

.only-mobile { display: none; }

@media (max-width: 899px) {
  .only-desktop { display: none !important; }
  .only-mobile  { display: inline; }

  /* ---------- нижняя панель: пять пунктов с подписями ----------
     Подписи вернулись перерисовкой (№769, макет design/screens.html):
     иконка без слова опознаётся не всеми. Прежний довод («на 390px
     текст даёт 10px и обрезку») снят тем, что подпись одна и короткая,
     а многоточие у длинной честнее молчания. */
  :root { --bar-h: 62px; }

  .tabbar {
    padding-bottom: env(safe-area-inset-bottom);
    border-top: 1px solid rgba(var(--line-rgb), .8);
  }
  /* Панель телефона — ПЯТЬ пунктов С ПОДПИСЯМИ (книга, блок Sidebar;
     макет design/screens.html). Подписи вернулись вместе с перерисовкой:
     иконка без слова опознаётся не всеми, а места хватило — панель
     осталась той же высоты, потому что подпись занимает 11px под
     иконкой 20px. Пилюля под активной иконкой снята: активность
     показывает акцент на иконке и подписи. */
  .tab {
    min-height: var(--bar-h);
    gap: 3px;
    padding: 6px 2px;
    font-size: var(--text-micro);
    touch-action: manipulation;   /* без 300 мс задержки */
  }
  .tab svg { width: 22px; height: 22px; stroke-width: 1.5; }
  .tab .dot {
    position: absolute; top: 5px; left: 50%; margin-left: 5px;
    min-width: 17px; height: 17px; padding: 0 4px; font-size: var(--text-micro);
  }

  /* панель «Ещё» отсчитывается от нового бара автоматически: она
     привязана к var(--bar-h) */

  /* ---------- карточка задачи ---------- */
  .task { padding: 10px 0; gap: 10px; }
  .task-title { font-size: var(--text-control); line-height: 1.35; }

  /* описание — одна строка, раскрывается тапом по карточке */
  .task-body {
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: var(--text-meta);
    margin-top: 2px;
    cursor: pointer;
  }
  .task.is-open .task-body { -webkit-line-clamp: unset; }

  /* метаданные и кнопка — одной строкой */
  .task-line {
    display: flex; align-items: center; flex-wrap: wrap;
    gap: 6px;
    margin-top: 6px;
    font-size: var(--text-meta);
  }
  .task-line .badge { font-size: var(--text-micro); padding: 2px 7px; }
  .task-due { font-variant-numeric: tabular-nums; }
  .mini-link { font-size: var(--text-meta); }

  /* кнопка действия компактная, но тап-зона остаётся 44px */
  .task-go {
    margin-left: auto;
    min-height: 34px;             /* визуальная высота; десктопные 29px сюда не доходят */
    background: var(--accent-soft);     /* на телефоне контур читается хуже — оставляем заливку */
    border-color: var(--accent);
    padding: 0 12px;
    font-size: var(--text-meta);
    gap: 6px;
    border-radius: var(--radius-md);
    position: relative;
  }
  .task-go::after {               /* расширяем тап-зону до 44px, не раздувая вид */
    content: '';
    position: absolute; left: 0; right: 0;
    top: -5px; bottom: -5px;
  }
  .task-go svg { width: 15px; height: 15px; }

  .acc-body { padding: var(--space-4) var(--space-3) var(--space-2); }

  /* ---------- клиенты ---------- */
  .client-line { min-height: 52px; padding: 10px var(--space-3); gap: 10px; }
  .client-line .avatar { width: 34px; height: 34px; font-size: var(--text-meta); }
  .client-line .nm { font-size: var(--text-control); }
  .client-line .muted { font-size: var(--text-meta); }
  .client-grid { gap: 6px; }

  /* ---------- записи ---------- */
  .card { padding: var(--space-3); }
  .day-head { padding: 7px var(--space-3); font-size: var(--text-meta); margin-bottom: 10px; }
  .slot-time { font-size: var(--text-section); }
  .slot-master { font-size: var(--text-control); }
  .master-name { font-size: var(--text-body); margin-bottom: 6px; }
  .master-group { margin-bottom: var(--space-3); }

  /* ---------- абонементы ---------- */
  .card .muted { font-size: var(--text-meta); }

  /* ---------- диалоги: список ---------- */
  .dlg-item { min-height: 58px; padding: 10px var(--space-3); gap: 10px; }
  .dlg-item .avatar { width: 34px; height: 34px; font-size: var(--text-meta); }
  .dlg-name { font-size: var(--text-control); }
  .dlg-last { font-size: var(--text-meta); margin-top: 1px; }
  .dlg-time { font-size: var(--text-micro); }
  .dlg-status { font-size: 9px; padding: 1px 6px; margin-top: 3px; }

  /* ---------- общее: вторичный текст мельче ---------- */
  .muted { font-size: var(--text-meta); }
  .section { margin-bottom: var(--space-4); }
  h2 { font-size: var(--text-body); }
}

/* ============================================================
   ЗАДАЧИ: кромка по типу, три действия, панель фильтров
   ============================================================ */

/* ⚠️ Цветная кромка по виду задачи СНЯТА перерисовкой (№769).
   Причина не вкусовая: после сведения палитры к теме студии все шесть
   видов красились одним `--ink2` — то есть полоса перестала различать
   что-либо, оставшись украшением. Вид задачи теперь назван СЛОВОМ
   в первом факте второй строки («Касание · Нино · срок 23.09»), как
   требует книга: смысл несёт слово, а не цвет. */

/* Срок у правого края — не шире 96px и в две строки по смыслу
   («12 августа, / среда»): длинная дата иначе съедает текст задачи
   (книга: R4 не отбирает место у R2). */
.line-row.task .line-val { max-width: 110px; white-space: normal;
                           text-align: right; line-height: var(--line-meta); }
.due-short { display: none; }
@media (max-width: 899px) {
  .due-full { display: none; }
  .due-short { display: inline; }
  .line-row.task .line-val { max-width: 56px; white-space: nowrap; }
}

/* Только десктоп: мобильный вариант кнопки уже настроен выше, и правило
   без медиа-запроса перебило бы его — тап-зона упала бы с 34 до 29px. */
@media (min-width: 900px) {
  /* строка метаданных: слева бейдж, срок и имя, кнопка прижата вправо */
  .task-line { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
  .task-line > .task-go { margin-left: auto; }

  /* контурная кнопка в одну высоту с бейджами: тёмная заливка спорила
     с бейджами и притягивала взгляд сильнее самой задачи */
  .task-go {
    min-height: 29px;
    padding: 0 11px;
    border-radius: var(--radius-md);
    border: 1px solid var(--accent);
    background: transparent;
    color: var(--ink);
    font-family: var(--font);
    font-size: var(--text-meta); font-weight: 500;
    letter-spacing: .01em;
    gap: 6px;
    box-shadow: none;
    transition: background var(--ease), border-color var(--ease);
  }
  .task-go:hover { background: var(--press); border-color: var(--ink); transform: none; }
  .task-go svg { width: 14px; height: 14px; }
}
.attempts {
  color: var(--ink2);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* ---------- три действия ---------- */
.task-acts {
  display: flex; align-items: center; gap: 6px;
  flex-shrink: 0;
}
.act {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 38px; padding: 0 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--raised);
  color: var(--ink2);
  font-family: var(--font);
  font-size: var(--text-meta); font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  touch-action: manipulation;
  transition: background var(--ease), color var(--ease), border-color var(--ease);
}
.act:hover { background: var(--press); color: var(--ink); }
.act:active { background: var(--sunken); transition-duration: .1s; }
.act[disabled] { opacity: .5; }
.act-done { border-color: var(--accent); color: var(--ink); }
.act-done:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }

/* меню «Позже» */
.act-later { position: relative; }
/* Подпись группы внутри меню строки: сроки идут пунктами того же
   списка, и заголовок отделяет их от связи и недозвона. */
.row-menu-sep {
  padding: var(--space-2) var(--space-3) var(--space-1);
  font-size: var(--text-caps); line-height: var(--line-caps); font-weight: 500;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink2);
}

/* ---------- панель фильтров ---------- */
/* Панель «Задач» — обычный слот S3 (`.ctl`): срезы сегментами, справа
   за распоркой сортировка. Своей раскладки у неё больше нет. */
.chips { display: inline-flex; align-items: center; gap: 2px; min-width: 0; }
.chip-n {
  /* №344: тот же счётчик, те же названия групп, что у групп задач (ныне .fold-n) —
     починить один и оставить этот значило бы не закрыть жалобу. Разметка
     `<span class="chip-n">` на 30.08 встречался только в tasks.html;
     с 05.09 (№453) его носит ещё чип «Неотвеченные» на «Диалогах», и
     правка кегля/веса задевает обе страницы. */
  margin-left: var(--space-1);
  color: var(--ink3); font-weight: 400; font-variant-numeric: tabular-nums;
}

.sort-wrap { display: flex; align-items: center; gap: var(--space-2); margin: 0; }
/* Селект — тот же орган, что кнопка и сегмент: высота `--control-h`,
   не растягивается и не переносится (книга, «Текст в тесноте»).
   ⚠️ С 29.09.2026 родное поле накрыто своим виджетом и человеку
   не видно — но правило НЕ мёртвое и удалять его нельзя: оно рисует
   поле там, где скрипт не поднялся, а виджет — надстройка над родным
   полем, а не замена ему. Те же два числа виджет берёт себе
   (`.sel-control`), чтобы орган не менял размер от того, поднялся
   скрипт или нет. */
.sort-select { width: auto; min-width: 190px; min-height: var(--control-h);
               font-size: var(--text-control); }

/* ═══ СВОЙ ВЫПАДАЮЩИЙ СПИСОК (решение владельца 29.09.2026) ═══════════
   Родной `<select>` раскрывается окном ОПЕРАЦИОННОЙ СИСТЕМЫ: подсветку,
   шрифт и высоту строк рисует она, и CSS внутрь не достаёт — на боевом
   это читалось чужим элементом посреди экрана. Поэтому список свой.

   ⚠️ Родной `select` ОСТАЁТСЯ в разметке и остаётся источником правды:
   он несёт значение в форму, через него работают `requestSubmit()`
   вкладок и он же отвечает, если скрипт не поднялся. Виджет — надстройка
   над ним: прозрачный (`opacity: 0`) и непрокликиваемый, но с настоящей
   коробкой — иначе он выпал бы и из доступности, и из приёмки.

   ⚠️ Раскрытый список — `position: fixed` и живёт в `<body>`, а не рядом
   с кнопкой: кнопки стоят внутри окон и полос со своей прокруткой
   (`.sheet-body`, `.ctl`), и список, оставленный на месте, срезался бы
   их краем. Координаты считает скрипт при открытии и пересчитывает
   при прокрутке. */
.sel {
  position: relative; display: inline-flex;
  width: 100%; min-width: 0; max-width: 100%;   /* поле формы — на всю строку */
}
/* Орган строки управления (период, срез, сортировка) — по содержимому,
   но не уже 190px: те же числа, что были у `.sort-select`. */
.sel-control { width: auto; min-width: 190px; }
.sel-btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  width: 100%; min-height: var(--control-h);
  padding: 0 var(--space-3);
  background: var(--raised); border: var(--rule-strong);
  border-radius: var(--radius-md);
  font: inherit; font-size: var(--text-control); color: var(--ink);
  text-align: left; cursor: pointer;
}
.sel-btn:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.sel-btn[aria-expanded="true"] { border-color: var(--accent); }
.sel-btn:disabled { opacity: .75; cursor: not-allowed; }
.sel-val {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sel-arrow { flex: none; width: 14px; height: 14px; color: var(--ink2); }
.sel-native {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}
.sel-list {
  position: fixed; z-index: var(--z-select);
  margin: 0; padding: var(--space-1) 0; list-style: none;
  background: var(--raised); border: var(--rule-strong);
  border-radius: var(--radius-md); box-shadow: var(--shadow-panel);
  max-height: min(50vh, 320px); overflow-y: auto;
  overscroll-behavior: contain;
}
.sel-opt {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: var(--tap); padding: 0 var(--space-3);
  font-size: var(--text-control); color: var(--ink);
  cursor: pointer; white-space: nowrap;
}
/* Подсветка ведомого пункта — НАШ токен отклика, а не системный синий:
   ради этого список и переписан. */
.sel-opt.is-active { background: var(--press); }
.sel-opt[aria-selected="true"] { font-weight: 600; }
.sel-opt[aria-selected="true"]::after {
  content: "✓"; margin-left: auto; color: var(--accent);
}
.sel-opt[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
/* На телефоне список занимает ширину экрана за вычетом полей страницы:
   ширины кнопки ему мало, а длинные названия периодов не переносятся. */
@media (max-width: 899px) {
  .sel-opt { white-space: normal; }
}

/* Полоса периода отчётных экранов — «Платежи», «Зарплаты», «Статистика»
   (№769, заход 1). Тот же орган, что на «Событиях» №768: селект
   с «Свой период» внутри, даты формой под ним. Имя ОБЩЕЕ, не `ev-`:
   классы событий на чужих экранах были ровно той грязью, которую
   аудит и снимал. Телефонное правило — ниже, в @media 899. */
.period-bar { margin: 0 0 var(--space-3); }
/* Пояснение за знаком «?» (книга, блок Notice): раскрывается рядом
   с собой, а не плашкой в потоке страницы. */
.hint-q { position: relative; flex: none; }
.hint-q > summary { list-style: none; cursor: pointer; }
.hint-q > summary::-webkit-details-marker { display: none; }
.hint-q-body {
  --pop-align: right;
  position: absolute; right: 0; top: calc(var(--control-h) + var(--space-1));
  z-index: var(--z-menu); width: min(340px, 86vw);
  padding: var(--space-3); background: var(--raised);
  border: var(--rule); border-radius: var(--radius-lg);
  font-size: var(--text-meta); line-height: var(--line-meta); color: var(--ink2);
}
.hint-q-body p { margin: 0 0 var(--space-2); }
.hint-q-body p:last-child { margin-bottom: 0; }
/* Тот же «?» внутри блока, а не в строке управления: раскрывается
   под собой и ширину берёт по месту. */
.hint-q-block { display: block; margin-bottom: var(--space-3); }
.hint-q-block .hint-q-body { position: static; width: auto; margin-top: var(--space-2); }

/* Заголовок блока со знаком «?» рядом (книга: «между блоками текста нет» —
   пояснение уезжает за знак у заголовка своего блока). Капитель и знак
   стоят ОДНОЙ строкой: `.caps` — блочный элемент, и без этого `details`
   уходил под него отдельной строкой, растя отступ до рамки вдвое.
   Знак — тихая кнопка ростом со строку капители, не 36px органа: в потоке
   содержимого он подпись, а не орган управления. */
.caps-q { display: flex; align-items: center; gap: var(--space-2); }
.caps-q .hint-q > summary {
  min-height: 0; height: var(--line-caps); padding: 0 6px;
  font-size: var(--text-caps); line-height: var(--line-caps);
  color: var(--ink3); border-radius: var(--radius-sm);
}
.caps-q .hint-q > summary:hover { color: var(--ink); background: var(--sunken); }
/* Знак «?» рядом с ПОДПИСЬЮ ПОЛЯ и рядом с ЗАГОЛОВКОМ блока (п.9, 25.09):
   та же механика, что у капители (`.caps-q` ниже), но подпись поля и h3 —
   не капитель, поэтому свой ряд и свой рост знака. Знак остаётся тихой
   кнопкой ростом со строку, а не органом 36px: в потоке содержимого
   он подпись. */
.field-q, .head-q { display: flex; align-items: center; gap: var(--space-2); }
.field-q label { margin-bottom: 0; }
.field-q .hint-q > summary, .head-q .hint-q > summary {
  min-height: 0; height: var(--line-body); padding: 0 6px;
  font-size: var(--text-meta); line-height: var(--line-body); color: var(--ink3);
  border-radius: var(--radius-sm);
}
.field-q .hint-q > summary:hover, .head-q .hint-q > summary:hover {
  color: var(--ink); background: var(--sunken);
}
.field-q .hint-q-body, .head-q .hint-q-body {
  --pop-align: left;
  right: auto; left: 0; top: calc(var(--line-body) + var(--space-1));
}

.caps-q .hint-q-body {
  --pop-align: left;
  right: auto; left: 0; top: calc(var(--line-caps) + var(--space-1));
  text-transform: none; letter-spacing: 0; font-weight: 400;
}
.period-bar-label { display: inline-flex; align-items: center; gap: var(--space-2); }
.period-dates { margin: 0 0 var(--space-3); }

@media (max-width: 899px) {
  .task-acts { gap: 4px; }
  .act {
    min-height: 44px;          /* тап-зона по норме */
    padding: 0 10px;
    font-size: var(--text-micro);
    gap: 4px;
  }
  .act span { display: none; }  /* на телефоне только иконки */
  .act svg { width: 19px; height: 19px; }
  .act-done { background: var(--press); }

  /* №805: шапка раздела ПЕРЕНОСИТ действия на вторую строку. Была
     `display: flex` без переноса, и кнопка «+ Касание на дату» (171px)
     вместе с заголовком в 390 не влезала: карточка клиента ехала вбок
     ЦЕЛИКОМ — замер 28.09.2026 дал 404px документа при экране 390.
     Перенос, а не сжатие кнопки: текст в сжатой кнопке читается хуже,
     чем кнопка строкой ниже.
     ⚠️ Правило у КОМПОНЕНТА, а не у карточки, хотя дефект нашли
     на карточке: `.head-actions` есть ещё у «Мастеров», «Платежей»
     и «Зарплат», и сужение до одной страницы оставило бы дефект
     в компоненте — он вернулся бы на любом из них от одного длинного
     заголовка. Ничего им при этом не меняет: перенос срабатывает
     ТОЛЬКО когда не влезает, а их шапки на 390 влезают (замерены
     тем же зондом: 380px, то есть без горизонтальной прокрутки). */
  .section-head { flex-wrap: wrap; }

  /* №804: переход в переписку на телефоне стоит СЛЕВА, сразу за «‹».
     В разметке он последний, после распорки, — на десктопе так и надо
     (кнопка прижата вправо). Но ряд управления на телефоне шире экрана
     и прокручивается вбок, а слева от кнопки стоит строка контактов
     со словами «с нами с 28 сентября, понедельник»: кнопка уезжала
     за правый край и человеку была не видна, пока ряд не протащишь.
     Порядком, а не переносом в разметке: разметка одна на обе ширины. */
  .ctl .back-ico { order: -2; }
  .ctl .card-write { order: -1; }

  /* На телефоне ряд управления скроллится вбок целиком (`.ctl`),
     поэтому своих правил переноса у сегментов и селекта больше нет. */
  .sort-select { min-width: 150px; }
  /* Селект периода — ОРГАН, а не поле формы: он не растягивается
     на свободное место (книга, «Текст в тесноте»). Прежнее правило
     «на всю строку» снято перерисовкой: ряд управления скроллится
     вбок целиком, и растянутый селект отбирал место у соседей. */
  .period-bar .sort-select { min-width: 150px; }
}

/* ------------------------------------------------ шапка: только бренд */
.brand {
  font-family: var(--font);
  font-weight: 500;
  font-size: var(--text-num);
  letter-spacing: .14em;
  line-height: 1.1;
  color: var(--ink);
}
.top .title .sub { margin-top: 1px; }

/* Контекстная строка на страницах без собственного заголовка:
   несёт данные, а не повторяет название вкладки. */
.page-note {
  margin: 0 0 var(--space-3);
  font-family: var(--font);
  font-style: normal; font-weight: 500;
  font-size: var(--text-body);
  color: var(--ink2);
}

@media (max-width: 899px) {
  .brand { font-size: var(--text-section); letter-spacing: .12em; }
  .top .title .sub { font-size: var(--text-meta); }
  .page-note { font-size: var(--text-control); margin-bottom: var(--space-2); }
}

/* Шапка на телефоне ниже (решение владельца 05.09): логотип, имя и две
   иконки занимали 71px при нулевой пользе. Высота 44 (плюс вырез),
   логотип 28, имя 15. Прилипание не тронуто — липнет тот же `.top`.
   ⚠️ Слоган здесь НЕ трогается: его порог — отдельное решение №423
   (видим на 480–899, спрятан ниже 480, правило ниже по файлу), и первая
   редакция этой правки молча его отменяла — поймал браузерный шов.
   Строгие 44px получаются там, где слогана нет (<480px, телефон —
   ровно предмет решения); на планшете шапка выше на строку слогана. */
@media (max-width: 999px) {
  .top {
    min-height: 44px;
    padding-top: calc(4px + env(safe-area-inset-top));
    padding-bottom: 4px;
  }
  .top .logo { width: 28px; height: 28px; font-size: var(--text-meta); }
  .brand { font-size: var(--text-body); }
  .top .me, .top .exit { min-height: 36px; min-width: 36px; }
}

/* Шапка на очень узком (№423, решение владельца): слогану не хватает
   ширины рядом с логотипом и профилем — переносится по слову и ложится
   на первую строку содержимого страницы (замечено на «Карточке клиента»,
   380px; шапка общая — то же самое на любой другой странице). Логотип
   и название студии остаются, слоган ниже 480px просто не показываем —
   ужимать его в одну строку с усечением было бы читаемо хуже пустого
   места, а место под него всё равно есть только на один смысл: либо
   имя, либо девиз. */
@media (max-width: 479px) {
  .top .title .sub { display: none; }
}

/* ------------------------------------------------ две колонки на широких страницах */
.cols { display: block; }

@media (min-width: 900px) {
  /* п.21 (25.09): КОЛОНКИ РАВНЫЕ. Было `48fr 52fr` — «слева формы, справа
     списки, им нужнее ширина», — и замер показал 503 против 545 px на
     1440: два списка одного вида («Услуги» и «Кабинеты», «Пользователи»
     и «Входы») стояли рядом разной ширины, а книга держит одно и то же
     в одних координатах на всех экранах. Сорока двух пикселей ни одному
     списку не хватало по делу: строки в них усекаются многоточием, а не
     переносятся.
     align-items: start, иначе короткая колонка растянется под длинную. */
  .cols {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-6);
    align-items: start;
  }
  .col { min-width: 0; }
  .col .section:last-child { margin-bottom: 0; }
}

/* ------------------------------------------------ воронка по источникам */
.fn-row { padding: 10px 0; border-bottom: 1px solid var(--line); }
.fn-row:last-of-type { border-bottom: none; }
.fn-organic { opacity: .82; }

.fn-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
}
.fn-name { font-weight: 500; font-size: var(--text-control); }
.fn-nums { font-variant-numeric: tabular-nums; font-size: var(--text-control); white-space: nowrap; }
.fn-arrow { color: var(--accent); margin: 0 2px; }
.fn-pct { color: var(--ink2); font-size: var(--text-meta); }

/* Одна полоса из сегментов (книга, «Чистота экрана», правка 24.09):
   пришли → записались → дошли — это ОДНА величина на трёх ступенях,
   не три. Слева направо: дошли (тёмным, самый ценный шаг), затем
   разница «записались, но не дошли» (акцентом); остаток полосы —
   не записавшиеся вовсе — не закрашен и показывает подложку самой
   полосы. Раньше здесь стояли три отдельные полосы друг под другом
   («видно, как воронка сужается») — правило владельца требует одну
   полосу на одно сравнение. */
/* Подложка шкалы — ТОКЕН ПОВЕРХНОСТИ, а не чернила с прозрачностью:
   прозрачные чернила на тёмной теме дают грязно-серый, которого нет
   в палитре (п.13). */
.fn-bar {
  display: flex; height: 6px; border-radius: var(--radius-sm);
  overflow: hidden; margin-top: 7px;
  background: var(--sunken);
}
.fn-seg {
  display: block; height: 100%; flex: none;
  width: var(--fill, 0%);
  transition: width 700ms cubic-bezier(.2,.7,.3,1);
}
/* ⚠️ Оба сегмента — ОТ АКЦЕНТА ТЕМЫ (п.13, 25.09), а не чернилами:
   «дошли» сплошным `--bar-fill` (акцент, доведённый ядром до 3:1
   с подложкой), «записались» — тем же цветом вполсилы. До правки
   первый сегмент красился `var(--ink)`, то есть цветом ТЕКСТА: на любой
   теме он выглядел чужой чёрной заливкой и с палитрой не менялся. */
.fn-seg-reached { background: var(--bar-fill, var(--accent)); }
.fn-seg-booked  { background: color-mix(in srgb, var(--bar-fill, var(--accent)) 45%, var(--sunken)); }

.fn-total {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
  margin-top: var(--space-3); padding-top: var(--space-3);
  border-top: 2px solid var(--line);
  font-family: var(--font);
  font-weight: 500; font-size: var(--text-meta);
  text-transform: uppercase; letter-spacing: .05em;
}
.fn-total .fn-nums { text-transform: none; letter-spacing: 0; font-size: var(--text-control); }

@media (max-width: 899px) {
  .fn-name { font-size: var(--text-meta); }
  .fn-nums { font-size: var(--text-meta); }
  .fn-pct  { font-size: var(--text-meta); }
}

@media (prefers-reduced-motion: reduce) {
  .fn-seg { transition: none !important; }
}

/* ------------------------------------------------ компактные действия в строке карточки */
.row-acts {
  display: flex; justify-content: flex-end; gap: var(--space-2);
  margin-top: var(--space-3); flex-wrap: wrap;
}
.row-btn {
  min-height: 34px;
  padding: 0 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--accent);
  background: transparent;
  color: var(--ink);
  font-size: var(--text-meta);
  box-shadow: none;
}
.row-btn:hover { background: var(--press); border-color: var(--ink); transform: none; }
.row-btn[disabled] { opacity: .45; }

@media (max-width: 899px) {
  /* на телефоне кнопка во всю ширину удобнее пальцу */
  .row-acts { flex-direction: column; }
  .row-acts form { width: 100%; }
  .row-btn { width: 100%; min-height: var(--tap); font-size: var(--text-meta); }
}

/* ------------------------------------------------ подписка студии */
.bill-price { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.bill-old { color: var(--muted); font-size: var(--text-num); text-decoration-thickness: 2px; }
.bill-now { font-size: 34px; line-height: 1; letter-spacing: -.02em; }
.bill-note {
  margin: 8px 0 0; font-family: var(--font);
  font-style: normal; font-weight: 500; color: var(--accent); font-size: var(--text-body);
}
.bill-list {
  margin: var(--space-3) 0 0; padding-left: 18px;
  display: grid; gap: 6px; font-size: var(--text-control);
}
.bill-list li { color: var(--ink); }

/* ------------------------------------------------ галочки доступов */
.perm-grid {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: 1fr;
  gap: 2px;
}
.perm-grid.is-hidden { display: none; }
.perm-head {
  font-size: var(--text-meta); letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); margin-bottom: var(--space-2);
}
.perm-item {
  display: flex; align-items: center; gap: 10px;
  min-height: var(--tap);            /* палец, а не курсор: смена работает с телефона */
  padding: 0 4px; cursor: pointer; font-size: var(--text-control);
}
.perm-foot {
  display: flex; align-items: center; gap: var(--space-2);
  flex-wrap: wrap; margin-top: var(--space-2);
}
@media (min-width: 700px) {
  .perm-grid { grid-template-columns: 1fr 1fr; }
  .perm-head, .perm-foot { grid-column: 1 / -1; }
}

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

.plan-devices {
  display: flex; align-items: center; gap: var(--space-2);
  margin-top: var(--space-3); padding-top: var(--space-3);
  border-top: 1px solid var(--line);
  font-size: var(--text-control); font-weight: 500;
}
.plan.accent .plan-devices { border-top-color: var(--accent); }

.ico-dev { width: 20px; height: 20px; flex-shrink: 0; color: var(--ink2); }
.plan.accent .ico-dev { color: var(--ink); }

/* «всё из Базового, плюс:» — связка между соседними тарифами */
.plan-includes {
  margin-top: var(--space-4);
  font-family: var(--font);
  font-style: normal; font-weight: 500; font-size: var(--text-section); color: var(--ink2);
}
.plan-includes + .plan-points { margin-top: var(--space-2); }

/* разовая покупка: карточка во всю ширину под тремя тарифами.
   display:block вместо унаследованного от .plan флекса — обычный поток
   гарантированно обнимает содержимое; блок инфраструктуры выпадал наружу. */
.plan-single { display: block; margin-top: var(--space-4); }
.plan-single-cols {
  display: grid; gap: var(--space-4); margin-top: var(--space-4);
  align-items: start;   /* колонки по своей высоте, ряд — по самой высокой */
}
.plan-single .plan-points { margin: 0; }
.plan-single .infra { align-self: start; }

/* инфраструктура при разовой — не мелкий шрифт-ловушка, а честный блок */
.infra {
  padding: var(--space-3) var(--space-4);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.infra-title {
  font-family: var(--font); font-weight: 500;
 letter-spacing: .05em; font-size: var(--text-meta);
  color: var(--ink2);
  margin-bottom: var(--space-2);
}
.infra-list { margin: 0; padding: 0; list-style: none; }
.infra-list li {
  position: relative; padding-left: 16px; margin-bottom: 6px;
  font-size: var(--text-meta); line-height: 1.5;
}
.infra-list li:last-child { margin-bottom: 0; }
.infra-list li::before {
  content: ''; position: absolute; left: 0; top: 8px;
  width: 6px; height: 1.5px; background: var(--accent);
}

.plan-notes { margin-top: var(--space-4); }
.plan-note {
  margin: 0 0 var(--space-2);
  font-size: var(--text-meta); line-height: 1.5; color: var(--ink2);
  max-width: 78ch;
}
.plan-note:last-child { margin-bottom: 0; }

/* --- счётчик устройств в «Доступах» --- */
.devices-line {
  display: flex; align-items: flex-start; gap: var(--space-3);
  margin-bottom: var(--space-4); padding: var(--space-3) var(--space-4);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.devices-body { min-width: 0; }
.devices-count { font-size: var(--text-control); }
.devices-count b { font-variant-numeric: tabular-nums; }
.devices-sub { margin-top: 3px; font-size: var(--text-meta); color: var(--ink2); }

/* превышение лимита: терракота, но блокировки нет — это витрина */
.devices-line.is-over {
  background: rgba(var(--accent-rgb), .10);
  border-color: var(--ink2);
}
.devices-line.is-over .ico-dev,
.devices-line.is-over .devices-count b { color: var(--accent); }
.devices-warn {
  margin-top: 3px; font-size: var(--text-meta); line-height: 1.5;
  color: var(--accent); font-weight: 500;
}

/* Две колонки только с 1100px: на 900–1099 пунктам и инфраструктуре тесно,
   там инфраструктура встаёт под списком на всю ширину карточки. */
@media (min-width: 1100px) {
  .plan-single-cols { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 899px) {
  /* тарифы в столбик — .plans и так одна колонка, здесь только плотность */
  .plan-includes { font-size: var(--text-body); }
  .plan-note { font-size: var(--text-meta); }
  .devices-line { padding: var(--space-3); }
}

/* ------------------------------------------------ карточка в узкой колонке
   Список абонементов живёт в правой колонке и стал уже: бейдж не должен
   сжиматься, а мета-строка обязана переноситься по разделителям. */
.card-side { flex-shrink: 0; text-align: right; }

.sub-meta {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 4px 0;
  margin-top: 10px;
}
.sub-meta span { white-space: nowrap; }
.sub-meta span + span::before {
  content: '·';
  margin: 0 7px;
  color: var(--accent);
}

/* ------------------------------------------------ сообщество и код продажи */

/* Сообщество — спокойная поверхность без акцентной рамки:
   акцент на странице один, у тарифа «Стандарт». */

/* Код проданного абонемента: его диктуют вслух с экрана,
   поэтому крупно, моноширинно и с разрядкой. */
.code-big {
  margin: var(--space-4) 0;
  padding: var(--space-3) var(--space-4);
  background: var(--sunken);
  border: 1px solid var(--accent);
  border-radius: var(--radius-lg);
  font-family: var(--font);
  font-weight: 500; font-size: 34px;
  letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  text-align: center;
  word-break: break-all;
}

@media (max-width: 899px) {
  .code-big { font-size: var(--text-title); }
}

/* ============================================================
   ВКЛАДКА «МАСТЕРА»: сетка графиков, боковая панель, карточки.
   Мобильный вид — базовый: сетка разворачивается в список
   «мастер → его неделя строками», панель занимает весь экран.
   ============================================================ */

/* --- навигация по периоду над сеткой --- */
.sched-nav {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-4);
  /* Вычет запаса под обводку, как у `.ctl`: шаг до графика остаётся 12px. */
  margin-bottom: calc(var(--space-3) - var(--ring-reach));
}
/* Узкий экран (решение владельца 05.09): навигация графика ОДНОЙ строкой —
   ‹ период › слева, «Сегодня» справа. Стрелки месяца не показываются
   вовсе: недельного шага хватает, а три группы складывались в три
   строки (148px замером). Десктоп не тронут — всё внутри медиа. */
@media (max-width: 999px) {
  .sched-nav { flex-wrap: nowrap; }
  .sched-nav .sched-nav-month { display: none; }
  .sched-nav .sched-today { margin-left: auto; }
}
.sched-nav-group { display: flex; align-items: center; gap: var(--space-2); }
/* Стрелки листания недели и месяца — те же кнопки-иконки 40×36, что
   у «Записей» (книга, блок DayNav): один орган, один вид. Своих правил
   у графика больше нет. */
/* Ширина фиксирована, не минимальна (книга, DayNav, правка 24.09):
   `min-width` позволял полосе расти под текст и двигать «Сегодня»
   вправо при смене недели или месяца — ровно то, что правило
   запрещает («стрелки не двигаются никогда»). */
.sched-period {
  font-weight: 500; white-space: nowrap; flex: none;
  font-size: var(--text-control);
  width: 13em; text-align: center;
}
.sched-period { font-variant-numeric: tabular-nums; }
.sched-month { color: var(--ink2); width: 9.5em; text-align: center; }
/* «Сегодня» — контурная кнопка `.btn-ghost` (книга, DayNav; правка
   24.09). Нажатие на сегодняшнем дне безвредно — тот же день;
   отдельного вида для «вы уже здесь» не заводим — второй активный
   признак рядом с самой сеткой был бы лишним. */

/* Пока едет запрос следующей недели, сетка гаснет — но не исчезает:
   мигание пустотой читается как сбой. */
[data-sched-box].is-loading { opacity: .55; transition: opacity var(--ease); }

/* --- сетка мастера × дни --- */
.sched-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  /* Системную полосу прячем: под сеткой лежит своя, всегда видимая
     и в палитре вертикали. Две полосы разом читались бы как сбой. */
  scrollbar-width: none;
  /* Таблица подчиняется рамке (книга, «Чистота экрана», правка 24.09):
     линия и радиус те же, что у списка. `overflow-x: auto` уже клипует
     содержимое по обеим осям (не `visible`), поэтому скругление режет
     и прокручиваемое содержимое, не только видимый край. */
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--raised);
}
.sched-scroll::-webkit-scrollbar { height: 0; }

/* Свой ползунок под сеткой */
.sched-bar {
  height: 10px; margin-top: var(--space-2);
  background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  position: relative; touch-action: none;
}
.sched-bar-thumb {
  position: absolute; top: -1px; bottom: -1px; left: 0;
  min-width: 40px; border-radius: var(--radius-sm);
  background: var(--accent); border: 1px solid var(--accent);
  cursor: grab;
}
.sched-bar-thumb:hover { background: var(--ink2); border-color: var(--ink2); }
.sched-bar-thumb:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.sched-bar.is-drag .sched-bar-thumb { cursor: grabbing; background: var(--ink2); }

.sched { border-collapse: separate; border-spacing: 0; width: 100%; }

/* Прошлые дни видно, но они приглушены: посмотреть, кто работал на той
   неделе, нужно регулярно, а править задним числом — почти никогда. */
.sched-day.is-past { opacity: .55; }
.sched-cell.is-past .cell { opacity: .5; }
.sched-cell.is-past .cell:hover { opacity: .8; }

/* №477: сетка — таблица на ОБЕИХ ширинах. Прежний мобильный вид
   (карточка мастера → строки «день → время», 928px на человека) снят:
   обзора «кто когда» он не давал вовсе. На телефоне ячейка 44px
   (явная ширина в медиа-блоке ниже — почему, см. там же), таблица
   шире экрана и листается; время открывает тап (панель правки). */
.sched { table-layout: fixed; }
.sched tr.is-off-duty { opacity: .62; }

.sched-master {
  background: var(--accent-soft);
  text-align: left;
}
.sched-name {
  /* Тот же дефект и то же лечение, что у заголовка группы задач: было
     15px/700/.05em — засечная антиква в режиме мелкой подписи.

     Кегль ОДИН с группами (18px), а не пропорционально больше. Роль
     у них одна — структурная подпись своего экрана, и одинаковый уровень
     как раз и держит иерархию между экранами. Пропорция от прежних
     15/13 дала бы 21px: имя мастера стало бы вровень с заголовком
     раздела «График» (22px) — вот тогда бы иерархия и поехала.
     В липкую колонку (168px минус отступы) 18px влезает с запасом:
     самое длинное имя студии — «Валентина», это около 76px. */
  font-family: var(--font); font-weight: 500;
  font-size: var(--text-body); letter-spacing: .01em; line-height: 1.22;
  color: var(--ink); background: none; border: 0; padding: 0;
  cursor: pointer; min-height: 0;
}
.sched-badge {
  font-size: var(--text-micro); text-transform: uppercase; letter-spacing: .04em;
  color: var(--wait-ink); border: 1px solid currentColor;
  border-radius: var(--radius-sm); padding: 1px 8px;   /* острое, исключение: бейдж */
}

.sched-cell {
  padding: 0; height: 38px;
  background: var(--raised);
  border-bottom: 1px solid var(--line);
  border-right: 1px solid var(--line);
  vertical-align: top;
}
.sched-cell.is-today { background: var(--accent-soft); }
.sched-cell.is-past .cell { opacity: .5; }

/* №477 (плашки, 06.09): ячейка = день мастера языком сетки записей.
   Тап-зона — вся кнопка, содержимое поверх. */
.cell {
  position: relative; display: block;
  width: 100%; height: 100%;
  padding: 0; border: 0; background: none;
  font: inherit; cursor: pointer;
}
/* Выходной и «нет смены» — та же диагональная штриховка, что «не рабочее
   время» в сетке записей: токен --hatch, не копия. */
.cell-off, .cell-empty { background: var(--hatch); }
.cell:hover::after {
  content: ""; position: absolute; inset: 0;
  border: 1px solid var(--accent); pointer-events: none;
}

/* Рабочий день — плашка на всю ячейку: заливка, рамка и радиус плашки
   записи (.bk), отступ 3px как у неё в колонке дня. */
.plate {
  position: absolute; inset: 3px;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  background: var(--accent-soft); border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  font-size: var(--text-micro); font-weight: 500; line-height: 1.2;
  font-variant-numeric: tabular-nums; color: var(--ink);
  overflow: hidden; white-space: nowrap;
}
.t-full span { display: block; text-align: center; }
.t-short { display: none; }
/* Своё время на дату — та же плашка, поверх пунктирная рамка. */
.plate-own { border: 1.5px dashed var(--accent); }

/* Отпуск и любой ряд ≥2 закрытых дней — штриховка ОДНИМ элементом через
   ячейки: сегменты по дням (тап каждого дня открывает правку своего),
   отрицательные поля сшивают их через внутренние границы. */
.vac {
  position: absolute; top: 3px; bottom: 3px; left: -1px; right: -1px;
  background: var(--hatch);
}
.vac-first { left: 3px; border-radius: var(--radius-md) 0 0 var(--radius-md); }
.vac-last { right: 3px; border-top-right-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); }
.vac-first.vac-last { border-radius: var(--radius-md); }
.vac-label {
  position: absolute; z-index: 2; top: 50%; transform: translateY(-50%);
  left: 8px; max-width: calc(var(--run) * 100% - 16px);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--text-micro); font-weight: 500; color: var(--ink);
  pointer-events: none;
}

/* Легенда: три образца теми же элементами, что в сетке (06.09). */
.sched-legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  font-size: var(--text-meta); color: var(--ink2); margin-top: var(--space-3);
}
.lg { width: 22px; height: 14px; display: inline-block; }
.lg-plate {
  background: var(--accent-soft); border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
}
.lg-own   { border: 1.5px dashed var(--accent); }
.lg-hatch { background: var(--hatch); border: 1px solid var(--line); }
.sched-legend .lg:not(:first-child) { margin-left: var(--space-3); }

/* --- карточки мастеров --- */
/* Сетка адаптивная: сколько карточек влезает по ширине, столько и будет.
   `auto-fill` с минимумом 340px даёт одну колонку на телефоне (там рабочая
   область уже 360px), две на ноутбуке и три на широком мониторе — число
   не зашито, и новый монитор не требует правки CSS.

   `align-items: stretch` (по умолчанию у grid) плюс колонка внутри карточки
   и `margin-top: auto` у кнопок держат ряд ровным: у кого-то есть фото,
   у кого-то нет, услуг разное количество — без этого нижние края разъезжаются
   и кнопки прыгают между строками при разной длине имени. */
.master-grid {
  display: grid;
  /* `min(340px, 100%)`, а не голые 340px: на экране 360px рабочая область
     уже 328px, и колонка в 340 вылезла бы за край — страница поехала бы
     вбок целиком. Правило «горизонтальной прокрутки на телефоне нет»
     сильнее желания держать ровно 340. */
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  gap: var(--space-3);
}
/* `margin-bottom` у `.card` в сетке складывался бы с `gap` — между рядами
   получался бы двойной отступ, а последний ряд висел бы с хвостом. */
/* Карточка — колонка ЗОН ПОСТОЯННОЙ ВЫСОТЫ (№49). `padding: 0`, потому что
   отступы теперь у каждой зоны свои: тело со вкладками обязано доходить
   до краёв, иначе его прокрутка читается как вложенная коробка.
   `overflow: hidden` держит нижнюю зону в пределах скругления; раскрытый
   график и меню «Ещё» лежат внутри границ карточки и им не мешает. */
.master-card {
  display: flex; flex-direction: column; margin-bottom: 0; padding: 0;
  position: relative; overflow: hidden;
}
.master-card.is-off-duty { opacity: .68; }

/* Шапка: аватар, имя, опыт — по одной строке на каждое, ничего условного. */
.mc-top { display: flex; gap: var(--space-3); align-items: center; padding: var(--space-3) var(--space-3) var(--space-2); }
.mc-who { min-width: 0; flex: 1; }
.mc-who h2 {
  margin: 0; font-size: var(--text-body); line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mc-sub {
  margin: 0; font-size: var(--text-meta); color: var(--ink2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Оценки под именем (№100). Своя строка постоянной высоты: она есть у каждой
   карточки, даже когда оценок нет, — иначе соседи по ряду разъедутся. */
.mc-rate {
  margin: 2px 0 0; font-size: var(--text-meta); color: var(--ink2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-variant-numeric: tabular-nums;
}
.mc-rate.is-on { color: var(--accent); }

/* Отзывы словами (№294): свёрнутый список под оценками. Цвета — только
   токенами, по правилу палитры админки. */
/* `.mc-reviews` (спойлер отзывов) умер с переделкой карточки мастера
   в строку — правило снято №476; сами отзывы (`.mc-review`) живы. */
.mc-review { margin: 6px 0 0; color: var(--ink); overflow-wrap: anywhere; }

/* Переключатель «видят ли свои оценки»: настройка студии, а не право,
   поэтому живёт на самой странице, а не в «Доступах». */
.mc-ratings-toggle {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2);
  margin: 0 0 var(--space-3); font-size: var(--text-meta);
}
.mc-ratings-toggle label {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
}
/* Чекбокс компактный, как у остальных тумблеров (.check input): без этого
   его красит ГЛОБАЛЬНЫЙ стиль полей ввода — width:100% с рамкой и паддингом,
   и получалась пустая плашка на всю ширину с квадратиком посередине. */


/* Фото — круглое и одного размера у всех: снимки приходят разные, а список
   должен читаться как список, а не как коллаж. */
.master-ava {
  width: 44px; height: 44px; border-radius: var(--radius-full); object-fit: cover;
  background: var(--accent-soft); flex: none; display: block;
}

/* Три состояния словами, всегда все три. Строка не переносится и не растёт:
   от её постоянной высоты зависит, начнётся ли тело у соседей по ряду
   на одном пикселе. Отсутствующее — не пустота, а тихое состояние. */
.mc-states {
  display: flex; align-items: center; gap: 10px; height: 26px;
  padding: 0 var(--space-3) var(--space-2); font-size: var(--text-meta); color: var(--ink2);
  white-space: nowrap; overflow: hidden;
}
.mc-st { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
.mc-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; flex: none; }
.mc-st.is-ok { color: var(--ok-ink); }
.mc-st.is-warn { color: var(--wait-ink); }
.mc-st.is-mute { color: var(--ink2); }
.mc-st.is-mute .mc-dot { background: var(--accent); }
.mc-sep { color: var(--line); flex: none; }

/* График: одна строка с усечением, полный вид — раскрытием ПОВЕРХ карточки.
   Плоскость держит линия, а не тень: --shadow в этой теме обнулён. */
.mc-sched { padding: 0 var(--space-3) var(--space-2); }
.mc-sched > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center; gap: 6px;
  font-size: var(--text-meta); font-variant-numeric: tabular-nums; min-height: 24px;
}
.mc-sched > summary::-webkit-details-marker { display: none; }
.mc-clock, .mc-chev { color: var(--ink2); flex: none; }
.mc-sched-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mc-sched[open] .mc-chev { transform: rotate(180deg); }
.mc-pop {
  position: absolute; left: var(--space-3); right: var(--space-3); z-index: 5;
  background: var(--raised); border: 1px solid var(--ink);
  border-radius: var(--radius-lg); padding: var(--space-2) var(--space-3); font-size: var(--text-meta);
}
.mc-pop-row { padding: 3px 0; font-variant-numeric: tabular-nums; }
.mc-pop-row + .mc-pop-row { border-top: 1px solid var(--line); }

/* Тело: услуги и выходные одной зоной с переключателем на радиокнопках.
   Высота фиксирована, лишнее прокручивается внутри — десять услуг и пять
   выходных не растягивают карточку, а их отсутствие не оставляет дыру. */
.mc-body { border-top: 1px solid var(--line); }
.mc-radio { position: absolute; opacity: 0; pointer-events: none; }
.mc-tabs { display: flex; background: var(--sunken); }
.mc-tabs label {
  flex: 1; min-height: 40px; font-size: var(--text-meta); cursor: pointer; color: var(--ink2);
  border-bottom: 2px solid transparent; display: inline-flex; align-items: center;
  justify-content: center; gap: 5px;
}
.mc-n {
  font-variant-numeric: tabular-nums; background: var(--accent-soft);
  border-radius: var(--radius-full); padding: 1px 6px; font-size: var(--text-micro);
}
/* Выбор — по позиции инпута, а не по id: id в разметке динамический
   (`mc-tab-svc-7`), и селектор по нему в CSS написать нечем. */
.mc-body > .mc-radio:first-of-type:checked ~ .mc-tabs label:first-child,
.mc-body > .mc-radio:nth-of-type(2):checked ~ .mc-tabs label:nth-child(2) {
  color: var(--ink); border-bottom-color: var(--ink); background: var(--raised);
}
.mc-body > .mc-radio:nth-of-type(2):disabled ~ .mc-tabs label:nth-child(2) {
  opacity: .5; cursor: default;
}
.mc-pane { display: none; height: 104px; overflow-y: auto; padding: var(--space-2) var(--space-3); font-size: var(--text-meta); }
.mc-body > .mc-radio:first-of-type:checked ~ .mc-pane-svc { display: block; }
.mc-body > .mc-radio:nth-of-type(2):checked ~ .mc-pane-off { display: block; }
.mc-row {
  display: flex; justify-content: space-between; gap: var(--space-2);
  padding: 3px 0; border-bottom: 1px solid var(--line);
}
.mc-row:last-child { border-bottom: 0; }
.mc-row-name { min-width: 0; }
.mc-row-val { color: var(--ink2); white-space: nowrap; font-variant-numeric: tabular-nums; }
.mc-row-val i { font-style: normal; font-weight: 500; }
.mc-row.is-own .mc-row-val { color: var(--ink); }
.mc-row-val.is-off { color: var(--wait-ink); display: inline-flex; align-items: center; gap: 4px; }
.mc-row-val.is-off button {
  background: none; border: 0; color: inherit; cursor: pointer;
  font-size: var(--text-body); line-height: 1; padding: 0 2px; min-height: 28px;
}
.mc-empty { color: var(--ink2); text-align: center; padding-top: var(--space-3); font-size: var(--text-meta); }

/* Кнопки. Счётчик фиксированной ширины табличными цифрами: падеж больше
   не двигает соседей от карточки к карточке. */
.mc-foot {
  display: flex; gap: 6px; align-items: center;
  padding: var(--space-2) var(--space-3); border-top: 1px solid var(--line);
}
.mc-foot .btn { min-height: 40px; font-size: var(--text-meta); }
.mc-count { flex: none; min-width: 124px; font-variant-numeric: tabular-nums; }
.mc-edit { flex: 1; }
.mc-more { position: relative; flex: none; }
.mc-more > summary { list-style: none; padding: 0 12px; }
.mc-more > summary::-webkit-details-marker { display: none; }
.mc-menu {
  position: absolute; right: 0; bottom: calc(100% + 6px); z-index: 6;
  background: var(--raised); border: 1px solid var(--ink);
  border-radius: var(--radius-lg); min-width: 190px; overflow: hidden;
}
.mc-menu button {
  display: block; width: 100%; text-align: left; font: inherit; font-size: var(--text-meta);
  padding: 11px 12px; background: none; border: 0; cursor: pointer; color: var(--ink);
}
.mc-menu button:hover { background: var(--press); }
.mc-menu button.is-danger { color: var(--alert-ink); }
/* Загрузка фото в карточке */
.photo-row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.photo-preview {
  width: 84px; height: 84px; border-radius: var(--radius-lg); object-fit: cover;
  border: 1px solid var(--line);
}
.photo-blank {
  width: 84px; height: 84px; border-radius: var(--radius-lg); display: grid; place-items: center;
  border: var(--rule); color: var(--ink2); font-size: var(--text-meta);
  background: var(--sunken);
}
.photo-row .btn-row { flex: 1 1 160px; }
.fld-check { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-body); }
/* Держатели абонемента: до трёх человек на код */
.holder-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--radius-md); padding: 3px 10px; margin: 0 6px 6px 0; font-size: var(--text-control);
}
.holder-chip button {
  border: 0; background: none; color: var(--ink2); cursor: pointer;
  font-size: var(--text-body); line-height: 1; padding: 0 2px;
}
.master-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tag {
  font-size: var(--text-meta); padding: 3px 10px; border-radius: var(--radius-sm);
  background: var(--sunken); border: 1px solid var(--line);
}
/* Ряд кнопок карточки услуги (вкладка «Услуги»). У карточки исполнителя
   свой ряд — `.mc-foot`: там счётчик фиксированной ширины и меню «Ещё». */
.master-foot {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin-top: auto; padding-top: var(--space-4);
}
.master-foot .btn { flex: 1 1 auto; }

/* №432: услуга в списке — СТРОКА (название, цены, «В прайсе», справа
   мелкие стрелки порядка); остальное — в окне общего скелета по тапу.
   Позиционный контекст и overflow: hidden строка берёт из списка волны
   касания выше (вместе с .client-line). */
/* №461: пара автоответчика — строка «номер · вопрос · признак справа».
   Вопрос усекается многоточием и НИКОГДА не переносится: строка держит
   высоту списка, а полный текст стоит в окне (и в поле правки). Признак —
   текст того же размера, что счётчики соседних списков, не бейдж. */
.faq-row .faq-q {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.faq-row .faq-note { flex: none; font-size: var(--text-meta); }
/* Строка длинного текстового поля «Автоответчика» (№503): название и
   состояние, полный текст — окном. Вид — общий `.line-row`. */
.tx-row { cursor: pointer; }
.tx-row .tx-title { flex: 1 1 auto; min-width: 0; font-weight: 500; }
.tx-row .tx-state { flex: none; font-size: var(--text-meta); white-space: nowrap; }
/* На узком состояние («RU задан · EN по умолчанию», ~190px) отжимало
   название в столбик по слову (кадр 380, 07.09) — строка ложится в две
   линии: название целиком, состояние под ним. */
@media (max-width: 899px) {
  .tx-row { flex-wrap: wrap; row-gap: 2px; }
  .tx-row .tx-title { flex-basis: 100%; }
  .tx-row .tx-state { margin-left: 0; }
}
.tx-hint { margin: 0 0 var(--space-3); font-size: var(--text-meta); line-height: 1.4; }
[data-tx-panel] textarea { width: 100%; }
.faq-row .pair-no { flex: none; }

/* `svc-line-main/-name/-prices` сняты 25.09.2026: это была копия слотов
   ListRow (`line-tx`, `.n`, `em`) под другими именами — четвёртая на
   систему. Списки услуг, кабинетов, мастеров и абонементов переведены
   на общие слоты (книга, принцип 4: «различаются составом слотов
   R1–R5, не видом»). */
.svc-line .badge { flex: none; }
/* Стрелки порядка — мелко, только на строке (в окне их нет, решение
   владельца №432). Тап-зона держится высотой строки, не кнопки. */
.svc-side {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--space-1); flex: none;
}
@media (min-width: 900px) {
  .svc-side { flex-direction: row; align-items: center; gap: var(--space-3); }
}
.svc-order { display: flex; gap: var(--space-1); flex: none; }
.svc-order .btn {
  min-height: 34px; padding: 0 10px; font-size: var(--text-meta); line-height: 32px;
}
/* №432: высота окна услуги на узком ПОСТОЯННАЯ — приёмка владельца требует
   совпадения координат кнопок у ЛЮБОЙ услуги, а авто-высота нижнего листа
   двигала бы их вместе с начинкой полей (замерено тестом: у услуги с девятью
   мастерами лист вырастал на ~130px и уносил кнопки вверх). Длинное
   содержимое прокручивается внутри, признак-тень у кромки уже есть.
   На ≥900 высота остаётся авто: там верх окна прибит (`align-items: start`
   в медиа-блоке скелета), и координаты кнопок постоянны сами. */
@media (max-width: 899px) {
  [data-svc-panel] .sheet { height: min(420px, 88dvh); }
  /* №433: у окна абонемента начинка выше (держатели, история) — та же
     постоянная высота, свой размер. Причина та же, что у услуг выше. */
  [data-sub-panel] .sheet { height: min(560px, 88dvh); }
}
/* №433: подписи кнопок окна абонемента длинные («Корректировать остаток»),
   в ячейке из двух/четырёх колонок усекаются — а усекаться постоянной
   подписи нельзя (правило владельца с №451). Узко — столбик во всю
   ширину, широко — две колонки по 340px: замерено, влезает всё. */
[data-sub-panel] .sheet-grid { grid-template-columns: 1fr; }
@media (min-width: 900px) {
  [data-sub-panel] .sheet-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* №433: строка абонемента. Полоса остатка — ПОД строкой, на всю ширину:
   `.line-row` — flex, и полоса третьим элементом ужалась бы в столбец;
   перенос на новую строку даёт flex-wrap, ширину — flex-basis 100%. */
.sub-line { flex-wrap: wrap; }
.sub-line-bar { flex: 1 1 100%; min-width: 0; }
.sub-line-bar .sub-progress { margin: 2px 0 0; }
/* Подсветка только что проданного — прежняя (`just-sold` на строке). */

/* №445, второй заход (05.09, приёмка на телефоне): строка кабинета —
   ТОЛЬКО информация, класс `.rooms-row`, не `.room-line` — тот уже занят
   (кабинет в карточке записи, `«Кабинет: X»` на плашке, ниже по файлу);
   общая ошибка отсюда и была: первая редакция назвала строку `.room-line`
   и молча унаследовала чужие правила (font-size, margin) того класса —
   отсюда часть тесноты на скриншоте приёмки, не только три кнопки в ряд.
   Кнопок в строке больше нет вовсе — action ходят через окно. */
.rooms-row.is-off-duty { opacity: .62; }

/* Строка мастера (05.09): кружок фото 36px — тот же аватар, что в «Диалогах»
   и списке {{ клиентов }}, только меньше; без фото — монограмма в нём же.
   Кнопок в строке нет вовсе, всё в окне. Снятый с записи приглушён. */
.masters-row.is-off-duty { opacity: .62; }
.masters-ava { width: 36px; height: 36px; font-size: var(--text-control); object-fit: cover; }
/* Выходные и отзывы в окне — чипами и абзацами, а не таблицей: их число
   у мастеров разное, а строка полей обязана держать одну сетку. */
.mc-off-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; margin: 0 4px 4px 0;
  border: 1px solid var(--line); border-radius: var(--radius-md);
  font-size: var(--text-meta);
}
.mc-off-chip button {
  background: none; border: 0; padding: 0 2px; cursor: pointer;
  color: var(--ink2); font-size: var(--text-control); line-height: 1;
}
.mc-off-chip button:hover { color: var(--alert-ink); }

/* --- боковая панель и модалка --- */
.panel-scrim {
  position: fixed; inset: 0; z-index: var(--z-panel-scrim);   /* лестница — в :root */
  background: rgba(var(--ink-rgb), .45);

}
/* Выше нижнего бара: панель раскрывается во весь экран, и бар, нарисованный
   позже в разметке, накрывал её кнопки «Отмена» и «Сохранить». С №287 —
   выше и окна записи (`--z-sheet`): форма «Изменить» открывается ИЗ него
   и обязана лечь поверх. Ниже карточки события (`--z-alert`) — решение
   владельца, см. лестницу в :root. */
.panel, .master-modal {
  position: fixed; z-index: var(--z-panel);
  background: var(--raised);
  display: flex; flex-direction: column;
}
/* телефон: панель во весь экран — иначе форма не помещается */
.panel, .master-modal { inset: 0; border-radius: 0; }

.panel-head {
  display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-3) var(--space-4); border-bottom: var(--rule);
  background: transparent;      /* заливка шапки ушла: блок держит линия */
}
.panel-head h2 { margin: 0; font-size: var(--text-num); }
.panel-head .muted { margin: 2px 0 0; }
/* Крестик закрытия — один на все окна админки (шесть мест в трёх шаблонах).
   ⚠️ Зона клика равна ВИДИМОЙ кнопке. До 14.08 здесь стояли только
   `min-width/min-height: var(--tap)` без фона: значок 26px, а нажималось
   поле 44px — курсор ещё далеко от крестика, а он уже подсвечен, и это
   читается как поломка. Правило простое: если зону расширяем, её надо
   ПОКАЗАТЬ, иначе человек видит одно, а система слушает другое.
   На телефоне зона доходит до 44px (там нет наведения, и промах пальцем
   дороже лишних восьми пикселей). */
.panel-close {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; margin-left: var(--space-2);
  background: none; border: 0; border-radius: var(--radius-md);
  font-size: var(--text-title); line-height: 1; color: var(--ink2);
  cursor: pointer; transition: background-color .15s, color .15s;
}
.panel-close:hover { background: var(--press); color: var(--ink); }
.panel-close:active { background: var(--sunken); }
@media (prefers-reduced-motion: reduce) { .panel-close { transition: none; } }
@media (pointer: coarse) {
  .panel-close { width: var(--tap); height: var(--tap); }
}
.panel-body { flex: 1 1 auto; overflow-y: auto; padding: var(--space-4); }
.panel-foot {
  display: flex; gap: var(--space-2); padding: var(--space-4);
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  border-top: var(--rule); background: var(--raised);
}
.panel-foot .btn { flex: 1 1 auto; }

.panel-block { margin-top: var(--space-6); }
.panel-label {
  display: block; font-size: var(--text-meta); text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink2); margin-bottom: var(--space-2);
}
.panel-hint { margin: var(--space-2) 0 0; }
.panel-row { display: flex; gap: var(--space-2); }
.panel-mode {
  display: flex; flex-direction: column; gap: var(--space-2);
  margin-top: var(--space-4); padding: var(--space-3);
  background: var(--sunken); border-radius: var(--radius-md);
}
.panel-mode label { display: flex; align-items: center; gap: var(--space-2); min-height: var(--tap); }
/* Глобальное правило для input тянет поля на всю ширину — радиокнопке это
   превращает кружок в полосу. Возвращаем ей собственный размер. */


.iv-row { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.iv-row input { flex: 1 1 auto; }
.iv-drop {
  background: none; border: 1px solid var(--line); border-radius: var(--radius-lg);
  color: var(--ink2); cursor: pointer;
  min-width: var(--tap); min-height: var(--tap); font-size: var(--text-section);
}
.iv-drop:hover { color: var(--alert-ink); border-color: var(--alert-ink); }

.panel input[type="time"], .panel input[type="date"], .panel input[type="text"],
/* ⚠️ Типы перечислены НЕ ДЛЯ КРАСОТЫ. Раньше здесь стоял голый
   `.master-modal input`, и он красил рамкой, фоном и высотой 44px ещё
   и флажки: «Клиенты могут записаться сами» выглядел большим квадратом
   браузерного вида. Заплатку тогда поставили соседнему классу
   (`.check input { width: auto }`), а этому — забыли. Правило про поля
   ввода обязано называть поля ввода, иначе оно достаётся всем. */
.master-modal input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
  font: inherit; font-size: var(--text-body); color: var(--ink);
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius-md); padding: 10px var(--space-3);
  min-height: var(--tap); width: 100%;
}
.fld { display: flex; align-items: center; gap: var(--space-2); flex: 1 1 0; }
.fld > span { font-size: var(--text-meta); color: var(--ink2); }
.fld-col { display: block; margin-bottom: var(--space-3); }
/* Подпись поля — `body-strong` над полем (книга, блок Field). Прописные
   сняты перерисовкой: капитель в системе живёт только у разделителей
   групп (`.caps`), и подпись поля ею не является. */
.fld-col > span {
  display: block; font-size: var(--text-body); line-height: var(--line-body);
  font-weight: 500; color: var(--ink); margin-bottom: var(--space-1);
}
.checks { display: flex; flex-direction: column; gap: var(--space-1); }
.check { display: flex; align-items: center; gap: var(--space-2); min-height: var(--tap); }

/* Шапка «Записей»: заголовок-подсказка и кнопка новой записи в одной строке */
.bookings-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
}
.bookings-head .page-note { margin: 0; }

/* Сетка времени в форме записи. Занятое не исчезает, а гаснет: администратор
   видит не только «куда можно», но и «почему нельзя». */
/* Сетка времени в форме записи. `min-height` — не украшение: слоты
   приезжают отдельным запросом, и без зарезервированного места окно
   открывалось коротким, а через долю секунды вырастало (находка владельца
   14.08). Высота под три ряда: столько занимает обычный день студии,
   остаток дорастает уже от заполненного окна, а не с нуля. */
/* Позиции составной записи (№13, Т2): строка «услуга · длительность · ×»,
   под ними итог. Селекты делят ширину, крестик — минимальная тап-зона. */
.nb-items .nb-item { display: flex; gap: var(--space-2); align-items: center; margin-top: var(--space-2); }
.nb-items .nb-item select { flex: 1; min-width: 0; }
.nb-items .nb-item [data-nb-item-remove] { min-width: var(--tap); }
/* На телефоне два select в одну строку режут названия («Лицо Фор…»):
   услуга занимает строку целиком, длительность и «×» — под ней. */
@media (max-width: 480px) {
  .nb-items .nb-item { flex-wrap: wrap; }
  .nb-items .nb-item [data-item-service] { flex: 1 1 100%; }
}
.nb-items-bar { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: center; margin: var(--space-2) 0; }
.bk-item { display: inline; }
.nb-times {
  display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-2);
  min-height: calc(38px * 3 + var(--space-1) * 2);
  align-content: flex-start;
}
/* Пока слоты грузятся, место занимают их «тени» — человек видит, ЧТО
   появится, а не пустоту, которая потом толкнёт окно вниз. */
.nb-time.is-ghost {
  border-color: transparent; background: var(--accent-soft);
  color: transparent; pointer-events: none; opacity: .55;
  animation: ghost-pulse 1.1s ease-in-out infinite;
}
@keyframes ghost-pulse { 0%, 100% { opacity: .35; } 50% { opacity: .6; } }
@media (prefers-reduced-motion: reduce) {
  .nb-time.is-ghost { animation: none; }
}
.nb-time {
  min-width: 64px; min-height: 38px; padding: 4px 10px;
  border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--raised); color: var(--ink); cursor: pointer;
  font-variant-numeric: tabular-nums;
}
/* ⚠️ ВЫБРАННОЕ ВРЕМЯ — ЗАЛИВКОЙ, а не оттенком (жалоба смены,
   01.10.2026: «не всегда вижу, что выбрал»). Прежняя отметка стояла
   на `--accent-soft` — ТОМ ЖЕ цвете, которым залиты «тени» слотов
   (`.nb-time.is-ghost` выше), то есть выбранное выглядело как ещё
   не загруженное; держалась она на 1px рамке и начертании 500.
   Берём заливку читаемой пары `--accent-btn`/`--on-accent` — ровно
   так отмечает выбранное время страница записи клиента
   (`.pv-bk-times span.is-on` ниже), и смена видит на ресепшене то же,
   что человек в боте. */
.nb-time.is-on {
  background: var(--accent-btn, var(--accent));
  color: var(--on-accent);
  border-color: var(--accent-btn, var(--accent));
  font-weight: 500;
}
.nb-time.is-busy { opacity: .35; cursor: default; text-decoration: line-through; }
/* Свободно, но ближе лид-тайма: выбирается, с подтверждением. Пунктир,
   а не жёлтый хардкод — цвета в just.css только токенами темы. */
.nb-time.is-lead { border: 1px dashed var(--accent); background: var(--raised); }
/* ⚠️ Лид-тайм И выбрано одновременно: правило `.is-lead` стоит ПОЗЖЕ
   и своей заливкой `--raised` стёрло бы отметку выбора. Заливка —
   выбора, пунктир остаётся признаком лид-тайма, но рисуется по
   заливке, иначе его не видно. */
.nb-time.is-lead.is-on {
  background: var(--accent-btn, var(--accent));
  border: 1px dashed var(--on-accent);
}

/* Кабинет в карточке записи: назначен автоматически, меняется одной кнопкой */
.room-line {
  display: flex; align-items: baseline; gap: var(--space-2);
  margin-top: 6px; font-size: var(--text-meta); color: var(--ink2);
}
.room-move {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-size: var(--text-meta); color: var(--ink2); text-decoration: underline dotted;
}
.room-move:hover { color: var(--ink); }

/* Услуги мастера с переопределениями. Поля цены и длительности приглушены,
   пока услуга не отмечена: сначала «делает или нет», потом «почём». */
.svc-rows { display: flex; flex-direction: column; gap: var(--space-1); }
.svc-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-1) 0; border-bottom: 1px solid var(--line);
}
.svc-row:last-child { border-bottom: 0; }
/* ⚠️ `.svc-over` — РАСКЛАДКА, а не состояние. Приглушение живёт отдельным
   правилом и только там, где состояние есть: в списке услуг, пока услуга
   не отмечена. Раньше `opacity: .45` стоял на самом `.svc-over`, а блок
   «Ставка за визит» переиспользует тот же класс — и его поля были ВСЕГДА
   приглушены, хотя активны и заполняемы. Владелец прочитал ставки как
   выключенные, и был прав: они так и нарисованы (14.08). */
.svc-over { display: flex; align-items: center; gap: 5px; margin-left: auto; }
.svc-row[data-svc-row]:not(.is-on) .svc-over { opacity: .5; }
.svc-over input { width: 58px; min-height: 38px; text-align: right; padding: 4px 6px; }
.svc-unit { font-size: var(--text-meta); color: var(--ink2); }

/* Подключение уведомлений мастеру */
.notify-state {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.notify-link {
  margin-top: var(--space-2); padding: var(--space-3);
  background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
}
/* Ссылку показываем текстом: в студии её чаще пересылают с телефона,
   где копирование из невидимого поля невозможно. Перенос по любому символу —
   иначе длинный токен растягивает панель по горизонтали. */
.notify-link code {
  display: block; margin-bottom: var(--space-2);
  font-size: var(--text-meta); word-break: break-all; color: var(--ink);
}
/* Тот же блок в карточке учётки: там он идёт потоком, а не в панели формы,
   и без верхнего отступа слипается со строкой «создан …». */
.notify-state.user-tg { margin-top: var(--space-3); }

.notify-manual { margin-top: var(--space-2); }   /* вид — `.fold-line` (№476) */

/* Форма новой услуги внутри карточки мастера: утоплена, чтобы читалась
   как часть блока специализаций, а не как вторая карточка поверх первой. */
.svc-new {
  margin-top: var(--space-3); padding: var(--space-3);
  background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
}
.svc-new .fld-col { margin-bottom: var(--space-2); }

/* Ступени ставки в модалке правки: «60 мин» слева, поле справа.
   Список услуг самой карточки переехал во вкладку `.mc-pane-svc` (№49). */
.master-svc-name { min-width: 0; }

/* --- мини-календарь --- */
.cal { background: var(--raised); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--space-3); }
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-2); }
.cal-head span { font-weight: 500; text-transform: capitalize; }
.cal-head button {
  background: none; border: 0; font-size: var(--text-num); color: var(--ink);
  cursor: pointer; min-width: var(--tap); min-height: var(--tap);
}
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.cal-wd { font-size: var(--text-micro); color: var(--ink2); text-align: center; padding-bottom: 2px; }
.cal-day {
  aspect-ratio: 1; min-height: 36px;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  font: inherit; font-size: var(--text-control); color: var(--ink); cursor: pointer;
}
.cal-day:hover { background: var(--sunken); }
.cal-day.is-today { border-color: var(--accent); }
.cal-day.is-sel { background: var(--accent-soft); color: var(--ink); }

/* Шапка дней и липкая колонка имён — общие для обеих ширин (№477):
   таблица больше не разворачивается в список. Ширина колонки имён —
   токен `--sched-col`: 168px на десктопе, узкая на телефоне. */
.sched { --sched-col: 168px; }
/* Строка дней залита ТАК ЖЕ, как первый столбец (книга, «Чистота
   экрана», правка 24.09): было — прозрачно, сквозь неё шёл фон
   страницы, а колонка имён уже стояла на `--raised`; две разные
   заливки в одной таблице читались как два разных объекта. */
.sched-day {
  padding: var(--space-2) var(--space-3); text-align: center;
  font-size: var(--text-meta); color: var(--ink2); font-weight: 500;
  border-bottom: 1px solid var(--line);
  /* Заливка шапки — та же, что у шапки сетки записей (`.dv-head`):
     книга требует, чтобы строка заголовка таблицы была залита так же,
     как первый столбец («Таблицы и матрицы подчиняются рамке»), а
     `.sched-master` слева уже несёт `accent-soft`. */
  background: var(--accent-soft);
  white-space: nowrap;
}
.sched-day.is-weekend { color: var(--wait-ink); }
.sched-day.is-today { color: var(--ink); background: var(--accent-soft); border-radius: var(--radius-sm) var(--radius-sm) 0 0; }   /* острое, исключение: сетка */
/* День недели и число — ОДНОЙ строкой (решение владельца 25.09.2026),
   как в шапке сетки записей: та же таблица, та же высота шапки. */
.sched-wd { text-transform: uppercase; letter-spacing: .06em; }
.sched-num { margin-left: 4px; color: var(--ink); font-weight: 500; }

/* Липкая колонка с именами. Собственный фон обязателен: под ней проезжают
   ячейки, и прозрачная колонка показывала бы их сквозь имя. Левый отступ
   не ноль — прижатый к самому краю контейнера текст режется его границей
   (ровно это давало «АЛОМЕ» вместо «САЛОМЕ» при прокрутке).
   Правая граница отделяет колонку от уезжающей сетки. */
.sched-corner, .sched-master {
  position: sticky; left: 0; z-index: 3;
  background: var(--raised); text-align: left;
  padding: var(--space-1) var(--space-3); min-width: var(--sched-col); width: var(--sched-col);
  border-bottom: 1px solid var(--line);
  border-right: 1px solid var(--line);
}
/* Тень вправо появляется только когда под колонку что-то заехало —
   иначе на непрокрученной сетке она выглядит случайной полосой. */
.sched-scroll.is-scrolled .sched-corner::after,
.sched-scroll.is-scrolled .sched-master::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 100%;
  width: 10px; pointer-events: none;
  background: linear-gradient(to right, rgba(var(--ink-rgb), .10), transparent);
}
.sched-corner {
  font-size: var(--text-meta); text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink2); font-weight: 500;
  /* Угол шапки залит так же, как её дни и первый столбец — иначе в углу
     рамки светится дыра цвета страницы (книга: строка заголовка залита
     как первый столбец и скруглена по углам рамки). */
  background: var(--accent-soft);
}

/* Телефон: колонка дня 44px — в неё влезает подпись «пн 28» целиком;
   узкая колонка имён, время по тапу (панель правки, там же кнопка
   «Изменить»).

   ⚠️ Ширина колонки — ЯВНАЯ, и это несущая часть (28.09.2026): без неё
   `table-layout: fixed; width: 100%` делит ширину телефона на все
   10 дней — по ~32px, а однострочная подпись (день недели 9px + 4px
   + число 13px ≈ 34px) при `white-space: nowrap` шире колонки и
   наползает на соседнюю: «пн 28 29 30 1 пт 2…» читалось кашей, день
   недели был виден не у всех чисел. С явной шириной таблица становится
   шире экрана и листается — как и обещает горизонт сетки (masters.py:
   «неделя с запасом, шире экрана — скроллится»): на 390px видно ~7 дней,
   остальное докручивается, ползунок под сеткой появляется сам. */
@media (max-width: 899px) {
  .sched { --sched-col: 64px; }
  .sched-corner, .sched-master { padding: var(--space-2) var(--space-1) var(--space-2) var(--space-2); }
  .sched-name {
    font-size: var(--text-meta); max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: block;
  }
  .sched-master { padding: var(--space-1) var(--space-1) var(--space-1) var(--space-2); }
  .sched-badge { display: none; }
  .sched-day { padding: 3px 1px; font-size: 9px; width: 44px; }
  .sched-num { font-size: var(--text-meta); }
  .sched-cell { height: 30px; }
  .t-full { display: none; }
  /* «10–22» в ~26px: кегль 9, плотнее межбуквенный, отступ плашки 2px.
     Хвост длиннее ячейки режется СПРАВА (не с обеих сторон центровкой):
     подпись во всю ширину с text-align, а не центр flex-оси. */
  /* «10–22» строкой в ~20px не живёт (замер: нужно 28) — часы
     столбиком: «10» над «22»; два интервала — «2» над «окна». */
  .t-short {
    display: flex; flex-direction: column; align-items: center;
    width: 100%; overflow: hidden;
    font-size: 9px; line-height: 1.15; letter-spacing: -.02em;
  }
  .plate { inset: 2px; padding: 0; }
}

@media (min-width: 900px) {
  /* Панель — ящик справа, как в референсе; модалка мастера — по центру. */
  .panel {
    inset: 0 0 0 auto; width: min(420px, 92vw);
    border-left: 1px solid var(--line);
  }
  .master-modal {
    inset: auto; top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: min(560px, 92vw); max-height: 86vh;
    border-radius: var(--radius-lg);
  }
  .checks { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-1) var(--space-3); }
  .master-foot .btn { flex: 0 0 auto; }

  /* Карточка исполнителя плотнее на десктопе: там курсор, а не палец,
     и тап-зона в 40px съедает высоту, которую лучше отдать содержимому.
     Высота зон при этом остаётся одинаковой у всех — меняется общая. */
  .mc-foot .btn { min-height: 34px; }
  .mc-tabs label { min-height: 34px; }
}

/* Записи вне графика мастера и гашение закрытых дней.
   График могли поменять уже после записи — терять её нельзя, но и делать
   вид, что всё в порядке, тоже: плашка адресует администратора к клиенту. */
.astray {
  margin: var(--space-2) 0 0; font-size: var(--text-meta); color: var(--wait-ink);
  border-left: 2px solid var(--wait-ink); padding-left: var(--space-2);
}
.master-group.is-closed, td.is-closed { opacity: .55; }
td.is-closed { background: repeating-linear-gradient(135deg,
  transparent, transparent 6px, rgba(var(--accent-rgb), .22) 6px, rgba(var(--accent-rgb), .22) 12px); }
.off-mark {
  font-size: var(--text-micro); text-transform: uppercase; letter-spacing: .04em;
  color: var(--wait-ink); border: 1px solid currentColor;
  border-radius: var(--radius-sm); padding: 1px 8px; margin-left: var(--space-2);   /* острое, исключение: бейдж */
}

/* Пара селектов вместо <input type="time">: нативное поле показывает AM/PM
   при английском языке интерфейса браузера, а демо открывают с разных
   устройств. Шаг минут — 30, часы полные сутки. */
.tp {
  display: inline-flex; align-items: center; gap: 2px; flex: 1 1 auto;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius-md); padding: 0 var(--space-2);
}
.tp i { font-style: normal; color: var(--ink2); }
.tp select {
  border: 0; background: none; padding: 10px 2px; min-height: var(--tap);
  font: inherit; font-size: var(--text-body); color: var(--ink);
  width: auto; -webkit-appearance: none; appearance: none; text-align: center;
}
.tp select:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-md); }

/* Уведомление о событии — ТОСТ ВНИЗУ, поверх всего (вариант «Б»,
   решение владельца 10.09, №642).
   Отмена записи и недоставленное письмо — единственное, из-за чего
   администратор бросает дела, поэтому уведомление стойкое: гаснет
   только по «Понятно» (№349), а не само.

   ⚠️ **ПОЧЕМУ ПЕРЕЕХАЛО ВНИЗ.** Стояло сверху и двигало страницу:
   `body.has-alert { padding-top }` плюс сдвиг липкой `.top`. Владелец:
   «сдвигает страницу вниз — на десктопе и на телефоне», убрать.
   Здесь `position: fixed` без единой правки потока — страница
   не двигается ни при появлении, ни при исчезновении, ни когда
   уведомлений несколько подряд.

   ⚠️ **Низ, а не верх — ради №570.** Сверху лежит шапка с навигацией
   (`.top`, липкая) и первый ряд управления любой вкладки; тот пункт
   и заводился из-за перекрытия кнопки «Продать абонемент» на 390px.
   Внизу перекрывать нечего: таббар телефона обойдён отступом
   `--bar-h` — тем же, которым уже отодвинут обычный тост.

   ⚠️ **`id` остался `#alert-card`** — это АДРЕС для швов: на нём стоят
   пять файлов браузерных сценариев (№570, replay №349, обход в
   `test_rooms_fold`, парковка чужих карточек в conftest). Менять его
   ради имени значило бы править пять файлов тестов и получить
   тот же экран. Класс переименован — он про вид, и вид сменился. */
.alert-card {
  /* Место — в полосе `.popups` (№667); здесь вид и переход. */
  transform: translateY(20px);
  width: 100%;
  background: var(--raised); border: 1px solid var(--wait-ink);
  border-radius: var(--radius-lg); opacity: 0;
  transition: opacity var(--ease), transform var(--ease);
}
.alert-card.show { opacity: 1; transform: translateY(0); }
.alert-body { padding: var(--space-3) var(--space-4) var(--space-2); }
.alert-kind {
  display: inline-block; font-size: var(--text-micro); text-transform: uppercase;
  letter-spacing: .06em; color: var(--wait-ink);
  border: 1px solid currentColor; border-radius: var(--radius-sm); padding: 1px 8px;   /* острое, исключение: бейдж */
}
.alert-card h3 { margin: 4px 0 2px; font-size: var(--text-body); }
.alert-card p { margin: 0; color: var(--ink2); white-space: pre-line;
                font-size: var(--text-meta); }
.alert-day { margin-top: var(--space-2); }
.alert-foot {
  display: flex; gap: var(--space-2); padding: 0 var(--space-4) var(--space-3);
}
.alert-foot .btn { flex: 1 1 auto; min-height: 38px; }

/* ------------------------------------------------ страница открыта в старой версии (№646)

   Тот же механизм, что у карточки события выше, и по той же причине:
   `position: fixed` без единой правки потока — страница не двигается ни
   при появлении, ни при исчезновении. Низ, а не верх — ради №570: сверху
   лежит липкая шапка с навигацией и первый ряд управления любой вкладки.

   ⚠️ **Стоит НАД карточкой события, а не в той же точке.** Раньше это
   держалось на замере: `app.js` клал высоту карточки в `--alert-h`,
   и «обновление» вычитало её из своего `bottom`. Обход ОДНОГО соседа
   из двух и был дефектом №667 — тост так и остался в той же точке, что
   карточка, и накрыл её кнопку «Понятно». С 11.09 порядок держит полоса
   `.popups`, замер снят. */
.stale-card {
  /* Место — в полосе `.popups` (№667). Обход карточки события через
     `--alert-h` больше не нужен: полоса раскладывает соседей сама,
     и высоту мерить нечем не приходится. */
  transform: translateY(20px);
  width: 100%;
  background: var(--raised); border: 1px solid var(--accent);
  border-radius: var(--radius-lg); opacity: 0;
  transition: opacity var(--ease), transform var(--ease);
}
.stale-card.show { opacity: 1; transform: translateY(0); }
.stale-card h3 { margin: 0 0 2px; font-size: var(--text-body); }
.stale-card p { margin: 0; color: var(--ink2); font-size: var(--text-meta); }

/* Предварительная запись в «Записях» */
.pending-mark {
  display: inline-block; font-size: var(--text-micro); text-transform: uppercase;
  letter-spacing: .04em; color: var(--wait-ink);
  border: 1px solid currentColor; border-radius: var(--radius-sm); padding: 1px 8px;   /* острое, исключение: бейдж */
  margin-bottom: var(--space-2);
}


/* Монограмма вместо логотипа: у вертикали без своего файла (см. [studio].logo)
   шапка и вход не должны показывать битую картинку. */
.logo-mono {
  display: grid; place-items: center;
  width: 46px; height: 46px; border-radius: var(--radius-full);
  background: var(--ink); color: var(--raised);
  font-family: var(--font); font-weight: 500; font-size: var(--text-num);
  letter-spacing: .04em;
}
.logo-mono.lg { width: 88px; height: 88px; font-size: 38px; }


/* ============================================================
   КАРТОЧКА КЛИЕНТА: правка полей
   ============================================================ */

/* Форма правки живёт прямо в карточке — отдельная страница «редактировать»
   ради четырёх полей была бы лишним шагом. Поля тянутся на всю ширину:
   на телефоне это единственный работающий вариант. */
.client-form textarea {
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius-md); padding: 10px var(--space-3);
  width: 100%; font: inherit; color: var(--ink); resize: vertical;
}
.client-form .btn { width: 100%; margin-top: var(--space-1); }

/* Факты из истории — под формой и не полем ввода: их правка означала бы
   подделку истории (см. repo.update_client). */
.client-facts {
  margin-top: var(--space-4); padding-top: var(--space-3);
  border-top: 1px solid var(--line); color: var(--ink2); font-size: var(--text-control);
}
.client-facts strong { color: var(--ink); font-weight: 500; }

@media (min-width: 900px) {
  /* Десктоп: источник и язык встают в строку — оба поля короткие */
  .client-form .fld-row { display: flex; gap: var(--space-3); }
  .client-form .fld-row > .fld-col { flex: 1 1 0; }
  .client-form .btn { width: auto; min-width: 180px; }
}


/* ============================================================
   КОНТАКТЫ КЛИЕНТА: строка каналов
   ============================================================ */

/* Одна строка на все каналы: админ должен увидеть их взглядом, а не читать
   таблицу с прочерками. Пустые каналы в разметку не попадают вовсе
   (см. contact_links в core/contacts.py), поэтому строка всегда плотная. */
.contact-row {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin: 0 0 var(--space-3);
}
.contact-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px var(--space-3); min-height: 34px;
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--radius-md);
  font-size: var(--text-meta); color: var(--ink); text-decoration: none;
  max-width: 100%;
}
.contact-chip svg { width: 15px; height: 15px; flex: none; opacity: .75; }
.contact-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.contact-chip:not(.is-plain):hover { border-color: var(--accent); background: var(--press); }
/* Справочные значения (дата рождения, email без ссылки) — тише кликабельных */
.contact-chip.is-plain { color: var(--ink2); }

/* Тап-зона: чип визуально ниже 44px, добираем псевдоэлементом — как везде */
.contact-chip { position: relative; }
.contact-chip::after { content: ''; position: absolute; inset: -5px 0; }

/* Заметка о клиенте в шапке диалога: тот же текст, что в карточке. */
/* Окно карточки поверх переписки (07.09). Неактивная кнопка — видна,
   но не действует: смена видит, что позвонить нечем (решение владельца).
   ⚠️ Селектор — ДВУМЯ классами родителя (№142): правило с одним
   `.cc-actions .btn` стояло в файле РАНЬШЕ общего `.sheet-grid .btn`
   (display:block; line-height:46px), специфичность равная — побеждал
   поздний, и центрирование было мёртвым: значок сидел на базовой
   линии 46px-строки, на 4px выше центра (замер сцены
   test_contact_buttons_are_aligned_and_equal_width). line-height
   возвращается к нормальному: центрует flex, а не высота строки.
   Кнопок три РАВНОЙ ширины в одном ряду (решение владельца 30.09) —
   своя сетка на три колонки вместо общих двух; приём №698
   (третья на всю ширину) здесь не годится. */
.sheet-grid.cc-actions { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.sheet-grid.cc-actions .btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 8px; line-height: var(--line-body);
}
.cc-actions .btn svg { width: 18px; height: 18px; flex-shrink: 0; }
.cc-actions .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; pointer-events: none; }
.cc-sheet .sheet-zone:last-child .btn { width: 100%; }

.thread-note {
  margin: 0 0 var(--space-2); padding: var(--space-2) var(--space-3);
  background: var(--accent-soft); border-radius: var(--radius-lg);
  font-size: var(--text-meta); color: var(--ink);
}


/* ============================================================
   ПРОФИЛЬ: кнопка в шапке и блоки на странице
   ============================================================ */

/* Кнопка профиля заняла место прежней строки расхода ИИ. Цифра висела
   на каждой странице и каждый раз спрашивала «это про меня?»; смотрят её
   раз в неделю — теперь она внутри профиля. */
.me {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 6px var(--space-3); min-height: 34px;
  border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--sunken);
  font-size: var(--text-meta); color: var(--ink2); text-decoration: none;
  white-space: nowrap; max-width: 220px;
}
.me:hover { color: var(--ink); border-color: var(--accent); }
.me.active { background: var(--press); border-color: var(--accent); color: var(--ink); }
.me svg { width: 18px; height: 18px; flex: none; }
.me-name { overflow: hidden; text-overflow: ellipsis; }

/* На телефоне в шапке места нет: остаётся значок, имя уходит — иначе
   элемент раздувает шапку и ломает подзаголовок студии на три строки. */
@media (max-width: 899px) {
  .me { padding: 4px; min-height: 32px; border-radius: var(--radius-full); }
  .me-name { display: none; }
}

/* Расход ИИ на странице профиля */
.ai-figures {
  display: flex; flex-wrap: wrap; gap: var(--space-6);
  margin-bottom: var(--space-3);
}
.ai-big { font-family: var(--font); font-size: var(--text-title); font-weight: 500; color: var(--ink); }
.ai-rows { margin-top: var(--space-3); }
.ai-row {
  display: flex; align-items: baseline; gap: var(--space-2);
  padding: 6px 0; border-top: 1px solid var(--line);
}
.ai-row span:first-child { flex: 1 1 auto; }
.ai-row b { font-variant-numeric: tabular-nums; }
.ai-tokens { margin: var(--space-3) 0 0; font-size: var(--text-meta); }
.over-limit { color: var(--wait-ink); }

/* ============================================================
   БЕЙДЖИ ВМЕСТО ГОЛЫХ ЧИСЕЛ И ПОМЕТКИ АРХИВА
   ============================================================ */

/* Бейдж остатка по абонементу: иконка + число + слово. Голая цифра рядом
   с именем читалась как что угодно — визиты, скидка, номер. */
.badge-ico { width: 14px; height: 14px; flex: none; }
.badge b { font-weight: 500; }
.badge-word { opacity: .8; }
/* Один сеанс или ноль — повод предложить продление, и это должно быть видно */
.badge-sub.is-low { background: var(--wait-bg); color: var(--wait-ink); }

/* Архив: строка гаснет, но остаётся читаемой — её открывают, чтобы вернуть */
.client-line.is-archived { opacity: .6; }
.tag-archived {
  font-size: var(--text-micro); text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink2); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 1px 8px; margin-left: 6px;   /* острое, исключение: бейдж */
}
/* Действие внутри строки текста: «Бот: подключён · отвязать».
   Не `.btn`, потому что у того тап-зона 44px и рамка — он разорвал бы строку.
   Тап-зону добираем вертикальными отступами, а не высотой: строка остаётся
   строкой, а палец попадает. */
.btn-link {
  appearance: none; background: none; border: 0;
  padding: 6px 2px; margin-left: 4px;
  font: inherit; font-size: var(--text-control);
  color: var(--ink2); text-decoration: underline;
  text-underline-offset: 3px; cursor: pointer;
}
.btn-link:hover { color: var(--alert-ink); }

/* «TG не подключён» в списке. Метка предупреждающая, но не тревожная:
   это норма для большей части базы, а не поломка. Красным здесь кричать
   не о чем — цветом отделяем от обычного серого текста строки, и всё. */
/* Метки состояния в строке списка. `white-space: nowrap` обязателен: без него
   «TG НЕ ПОДКЛЮЧЁН» рвалось ПОСРЕДИ СЕБЯ, стоило рядом появиться бейджу
   остатка абонемента, — переносить строку можно между элементами, но не
   внутри плашки (№30). Правило держим и для новой метки: она короче, но
   узкие экраны никуда не делись. */
.tag-no-tg {
  font-size: var(--text-micro); text-transform: uppercase; letter-spacing: .04em;
  border-radius: var(--radius-sm); padding: 1px 8px;   /* острое, исключение: бейдж */
  white-space: nowrap;
  color: var(--wait-ink); border: 1px solid var(--wait-ink);
  background: var(--wait-bg);
}
/* №474: «в боте» в списке клиентов — точка перед именем, не чип.
   Подключён — редкое и хорошее состояние, точки достаточно: она
   не рвёт строку и не спорит с бейджем остатка. */
.tg-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: var(--radius-full);
  background: var(--ok-ink); margin-right: 5px; vertical-align: 1px;
  flex: none;
}

/* Чип «Открыть диалог» в строке контактов: тот же вид, что у остальных
   каналов, но с акцентной рамкой — это единственный из них, где ответ уже
   ждёт. Раньше был кнопкой под строкой и прижимался к подписи поля «Имя». */
.contact-chip.is-dialog {
  border-color: var(--accent); color: var(--ink);
  font-weight: 500;
}
.contact-chip.is-dialog:hover { background: var(--press); }

/* История карточки: события человека одной колонкой, свежее сверху.
   Разметка как у списка визитов — смена читает их одинаково. */
.ev-list { display: grid; gap: var(--space-3); list-style: none; padding: 0; margin: 0; }
.ev { padding-left: var(--space-3); border-left: 2px solid var(--line); }
.ev-text { line-height: 1.4; }
.ev-meta { font-size: var(--text-meta); margin-top: 2px; }
.ev-tg_linked { border-left-color: var(--accent); }
.ev-tg_unlinked, .ev-merged { border-left-color: var(--ink2); }
.ev-invite_sent { border-left-color: var(--ink2); }
/* Звонок (№89): цветом категории «Звонки» из «Задач» — одно действие студии
   должно выглядеть одинаково в обоих местах. */
.ev-call { border-left-color: var(--ink2); }
/* Неудачная привязка — предупреждением: ссылку открыли, а бота нет,
   и разбирает это человек за стойкой. */
.ev-link_miss { border-left-color: var(--wait-ink); }

/* Действие внутри отказа: «нельзя» без выхода — стена, в которую смена
   упиралась, глядя вниз экрана. Кнопки идут в строку под текстом. */
.composer-do {
  display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-2);
}
.composer.disabled .composer-note { display: grid; gap: 2px; }

/* Очередь приглашений: строка = человек, действие справа. Отправленные
   гаснут, но не исчезают — смена должна видеть, что уже сделано. */
.invite-list { display: grid; gap: var(--space-2); }
.invite-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3); border: 1px solid var(--line);
  border-radius: var(--radius-lg); background: var(--raised);
}
.invite-row.is-sent { background: var(--sunken); opacity: .75; }
/* Канал строки различает подпись, а не пунктир (книга: пунктира
   в системе нет ни у одного элемента). */
.invite-who { display: grid; gap: 2px; min-width: 0; }
.invite-who .nm { font-weight: 500; }
.invite-act { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* Экран сверки дублей. Карточки группы стоят рядом и читаются как выбор:
   рамка у выбранной, приглушение у пустой — глазами видно, что теряем. */
.merge-group { margin-bottom: var(--space-4); }
.merge-group.is-locked { opacity: .85; background: var(--wait-bg); }
.merge-head {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-3);
}
.merge-cards { display: grid; gap: var(--space-2); }
.merge-card {
  display: grid; gap: 4px; padding: var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--raised); cursor: pointer;
}
.merge-card:has(input:checked) {
  border-color: var(--ink); box-shadow: 0 0 0 2px rgba(var(--accent-rgb), .45);
}
.merge-card.is-empty { background: var(--sunken); }
.merge-pick { display: flex; align-items: center; gap: var(--space-2); }
/* Радиокнопку ловит общий стиль полей (ширина 100%), и она раздувалась
   на полкарточки, отбрасывая имя к правому краю. Фиксируем размер здесь —
   это выбор, а не поле ввода. */

.merge-pick .nm { font-weight: 500; }
.merge-facts { display: flex; gap: 6px; flex-wrap: wrap; font-size: var(--text-meta); }
.merge-trace { font-size: var(--text-meta); color: var(--ink); }
.merge-card.is-empty .merge-trace { color: var(--ink2); }
.chip {
  border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: 1px 8px; font-size: var(--text-meta); color: var(--ink2);
}
.merge-foot { margin-top: var(--space-3); padding-top: var(--space-3);
              border-top: 1px solid var(--line); }
.merge-preview { max-width: 70ch; display: grid; gap: 4px; font-size: var(--text-meta); }
.merge-preview p { margin: 0; }
/* Предупреждение внутри превью: две живые привязки к боту — это не
   деталь, а причина не сливать. Цветом, а не жирностью: рядом стоят
   ещё три строки, и жирная среди них не выделяется. */
.merge-warn { color: var(--accent); }
@media (min-width: 900px) {
  /* На десктопе карточки группы встают в ряд — сравнивать удобнее рядом,
     а не одну под другой. */
  .merge-cards { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
}

/* Строка фактов, которая только что изменилась сама (человек подключился
   к боту, пока карточка была открыта). Подсветка гаснет: это сообщение
   о событии, а не постоянное состояние. */
.client-facts.is-fresh {
  background: var(--ok-bg); border-radius: var(--radius-lg);
  box-shadow: 0 0 0 6px var(--ok-bg);
  transition: background var(--ease), box-shadow var(--ease);
}

/* Приглашение в бота в карточке. Утопленный блок: это не форма правки
   карточки, а отдельное действие рядом с фактом «бот не подключён». */
.invite-box {
  margin-top: var(--space-3); padding: var(--space-3);
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.invite-hint { display: inline-block; margin-left: 6px; }
.invite-out { margin-top: var(--space-3); }
.invite-text {
  width: 100%; resize: vertical; font: inherit; font-size: var(--text-control);
  color: var(--ink); background: var(--raised);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: var(--space-2);
}
.invite-actions {
  display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-2);
}
.invite-note { margin: var(--space-2) 0 0; font-size: var(--text-meta); color: var(--ink2); }

/* Охват Telegram. Главная строка — карточка, вторая — одна строка текстом:
   разница в весе и есть утверждение, что метрика одна, а не две. */
.reach { display: grid; gap: var(--space-2); }
.reach.is-alarm { border-color: var(--wait-ink); background: var(--wait-bg); }
.reach-why { margin: 0; color: var(--ink); font-size: var(--text-body); max-width: 60ch; }
.reach .btn { justify-self: start; }
.reach-second {
  margin-top: var(--space-3); color: var(--ink2); font-size: var(--text-body);
}
.reach-second b { color: var(--ink); }
.archived-note {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-4);
}
.list-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
}
.list-head .page-note { margin: 0; }

/* Поиск по списку: поле с иконкой, во всю ширину строки заголовка */
/* ⚠️ ВЫСОТА ПОЛЯ ПОИСКА — ОДНА на все экраны и объявлена ЗДЕСЬ, один раз
   (жалоба владельца 29.09.2026 по снимкам с боя). Было три высоты сразу:
   44 у «Клиентов», «Абонементов» и архива задач (этот `.search`), 36
   у «Событий» (голый `input.search-field` — ему доставался `--control-h`
   полей формы) и 46 у «Диалогов» (`.dlg-search` рисовал поле по-своему,
   и к `--tap` добавлялась рамка). Поиск — ГЛАВНЫЙ орган экрана со
   списком, а не поле формы, поэтому высота у него тапабельная (`--tap`),
   и берут её обе разметки: и обёртка `.search`, и голое поле. */
.search, .ctl .search-field { min-height: var(--tap); }
.search {
  display: flex; align-items: center; gap: var(--space-2);
  flex: 1 1 260px; min-width: 200px;
  padding: 0 var(--space-3);
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius-md);
}
.search svg { width: 16px; height: 16px; flex: none; color: var(--ink2); }
.search input {
  border: 0; background: none; padding: 0; min-height: auto;
  font: inherit; color: var(--ink); width: 100%;
}
.search input:focus { outline: none; }
.search:focus-within { border-color: var(--accent); }
.search-empty { margin-top: var(--space-4); }
/* Счётчик найденных (№121): у правого края поля, не толкает ввод.
   `flex: none` и цифры одной ширины — иначе строка дёргается на каждом
   символе, и читать её на ходу невозможно. */
.search-count {
  flex: none; font-size: var(--text-meta); color: var(--ink2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* «Нашли и показали» (№128): вспышка на элементе, к которому перешли
   по ссылке. Обводка токеном акцента, гаснет сама — класс снимает JS.
   Без анимации box-shadow: она дёргает композитинг на слабых планшетах. */
/* Подсветка найденной плашки (№413, 02.09): не гаснет по таймеру — снимается
   кликом мимо (см. `reveal()` в app.js). Заметнее прежней тонкой рамки:
   рамка акцентом + мягкое свечение вокруг + два импульса на входе, дальше
   ровное свечение, чтобы глаз нашёл плашку и она не «мигала» бесконечно.
   Только цвета-токены, ни одного hex (правило палитры). */
.is-revealed {
  outline: 4px solid var(--accent); outline-offset: 3px;
  box-shadow: 0 0 0 9px color-mix(in srgb, var(--accent) 45%, transparent),
              0 14px 34px color-mix(in srgb, var(--ink) 34%, transparent);
  position: relative; z-index: 4;
  animation: reveal-pulse 1.1s ease-in-out 2;
}
@keyframes reveal-pulse {
  0%, 100% { box-shadow: 0 0 0 9px color-mix(in srgb, var(--accent) 45%, transparent),
                         0 14px 34px color-mix(in srgb, var(--ink) 34%, transparent); }
  50%      { box-shadow: 0 0 0 18px color-mix(in srgb, var(--accent) 18%, transparent),
                         0 14px 34px color-mix(in srgb, var(--ink) 34%, transparent); }
}

/* Справочник источников (№130). Строка: название · сколько людей · действия.
   Значения вне справочника помечены слева — их видно списком, не вчитываясь. */
.src-list { list-style: none; margin: 0; padding: 0; }
.src {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-3); min-height: var(--tap);
  border-bottom: 1px solid var(--line); flex-wrap: wrap;
}
.src:last-child { border-bottom: 0; }
.src.is-unknown { border-left: 3px solid var(--wait-ink); }
.src-name { flex: 1 1 200px; min-width: 0; display: flex;
            align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.src-count { flex: none; font-variant-numeric: tabular-nums; }
.src-acts { flex: none; display: flex; gap: var(--space-1); flex-wrap: wrap; }

/* Поиск клиента: поле, под ним живой список (№2 от 13.08).
   Сам `select` остаётся в разметке — он хранит значение и уходит с формой, —
   но с экрана убран. `display:none` не годится: часть браузеров отказывается
   отправлять и валидировать такое поле, а нам нужно только спрятать. */
.picker-search { margin-bottom: var(--space-2); }
.picker-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip-path: inset(50%);
}

/* Список под полем. Высота ограничена и прокручивается внутри: восемьсот
   строк в поток класть нельзя, а двадцать — ровно столько, сколько человек
   успевает окинуть взглядом, не отрываясь от телефонного разговора. */
.picker { position: relative; margin: calc(-1 * var(--space-1)) 0 var(--space-2); }
/* Подпись пикера — только состояния ПОИСКА («Никого не нашли»,
   «Показаны первые N из M»); в пустом состоянии она пуста (№145,
   решение владельца 30.09.2026 — «N в базе» перегружала окно). */
.picker-note {
  margin: var(--space-1) 0 0; font-size: var(--text-meta); color: var(--ink2);
  font-variant-numeric: tabular-nums;
}
.picker-list {
  list-style: none; margin: 0; padding: var(--space-1) 0;
  max-height: 240px; overflow-y: auto;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.picker-item {
  padding: var(--space-2) var(--space-3); cursor: pointer;
  min-height: var(--tap); display: flex; align-items: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.picker-item:hover, .picker-item.is-cursor { background: var(--sunken); }
/* Различающее (телефон, ник, день визита) — второй частью строки, тускло:
   имя остаётся главным, но тёзки перестают быть одинаковыми (14.09). */
/* Переход по дню внутри пояснения карточки события (14.09): ссылка,
   а не кнопка — ряд кнопок остаётся «действие и закрыть». Подчёркивание
   оставляем: в приглушённом абзаце ссылка иначе не читается ссылкой. */
.alert-day-link { color: var(--accent); text-decoration: underline; white-space: nowrap; }
/* Подсказка «абонемента нет — вот что делать» в окне приёма оплаты (№697).
   Не отказ и не ошибка: это путь, поэтому тон обычный, а ссылка видна. */
.pay-no-sub { margin: 0 0 var(--space-3); }
.pay-no-sub-link { color: var(--accent); text-decoration: underline; white-space: nowrap; }
.picker-hint { color: var(--ink2); font-size: var(--text-meta);
               margin-left: var(--space-2); flex: none; }
.picker-item.is-on { color: var(--accent); }
.head-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
/* `.btn-quiet` — тихая кнопка; вид у класса наверху (№769). */

/* Опасная зона: удаление без возврата. Рамка предупреждает до нажатия. */
.danger-zone { border-color: var(--alert-ink); }
.danger-zone h2 { color: var(--alert-ink); }
/* Вид опасной кнопки — ОДНА точка, у класса наверху (контурная, текст
   тревоги). Прежняя красная заливка была четвёртым весом кнопки. */
.footprint { margin: 0 0 var(--space-3); padding-left: 1.1em; color: var(--ink2); }
.footprint b { color: var(--ink); }

/* Отменённая запись: видна, но не спорит за внимание с теми, кто придёт */
.card.is-cancelled, .mini.is-cancelled { opacity: .55; }
.card.is-cancelled .slot-time, .mini.is-cancelled .t { text-decoration: line-through; }

/* ============================================================
   ЗАПИСИ: компактная сетка в двух режимах
   Прежние карточки занимали по 140px на запись — на экран влезало
   полтора мастера. Здесь высота плашки равна длительности сеанса,
   а действия появляются по клику, а не занимают место всегда.
   ============================================================ */

/* Листалка периода (книга, блок DayNav) — одна на все экраны с датой:
   записи, график мастеров, отчёты за день. Стрелки 40×36 не сжимаются
   в тесной строке, дата между ними — постоянной ширины под самую
   длинную подпись ЛЮБОГО РЕЖИМА (день, неделя), а не только дня —
   правка 24.09: «Понедельник, 7 сентября» и «31 августа — 6 сентября»
   меряны браузером под раздвинутым на .08em текстом (приём теснноты),
   шире вышла неделя на стыке долгих месяцев («…августа — …сентября»).
   Число — с запасом, а не впритык: лишнее место безвредно, а нехватка
   двигает стрелки, что правило запрещает. Сама строка — слот S2/S3
   (`.ctl`), поэтому переносов и раскладки по `order` здесь больше нет:
   не помещается — ряд скроллится вбок. */
.bk-period {
  font-weight: 500; width: 16em; flex: none; text-align: center;
  font-size: var(--text-body);
}
/* Две формы подписи дня: полную («Воскресенье, 21 сентября») показывает
   десктоп, короткую («Вс, 21 сентября») — телефон; у недели короткой
   формы нет (`title_short` совпадает с `title`), и ширина мобильного
   поля посчитана под ЕЁ длину, а не под короткий день. */
.bk-period-short { display: none; }
.bk-chips { display: contents; }
@media (max-width: 899px) {
  .bk-period { width: 15.5em; font-size: var(--text-control); }
  .bk-period-full { display: none; }
  .bk-period-short { display: inline; }
}

/* переключатель режима */
/* ------------------------------------------------ сегменты
   ЕДИНСТВЕННЫЙ вид фильтра-переключателя (книга, блок Segmented):
   срезы списка, режим показа, вкладки внутри страницы. Группа лежит
   на подложке `--sunken`, выбранный поднят до `--raised`. Заливки
   акцентом здесь не бывает никогда: выбор не должен выглядеть
   как кнопка (принцип 2).

   ⚠️ Классов состояния осталось ДВА и это разметка, а не разнобой:
   `a.on` у ссылок, `button.is-on` у кнопок. Вид у них один. */
.seg {
  display: inline-flex; align-items: center; gap: 2px;
  min-height: var(--control-h); padding: 3px;
  background: var(--sunken); border-radius: var(--radius-md);
  flex: none; max-width: 100%;
}
.seg a, .seg button {
  display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: 30px; padding: 0 var(--space-3);
  border: 0; border-radius: var(--radius-sm); background: none;
  font-family: inherit; font-size: var(--text-control); font-weight: 500;
  line-height: var(--line-control); color: var(--ink2);
  text-decoration: none; cursor: pointer; white-space: nowrap;
  appearance: none; flex: none;
}
.seg a:hover, .seg button:hover { color: var(--ink); }
/* Активность сегмента — `a.on` у ссылок, `is-on` у кнопок И у ссылок
   (швы адресуются `is-on`, живая навигация — `on`): вид один. */
.seg a.on, .seg a.is-on, .seg button.is-on { background: var(--raised); color: var(--ink); }
/* Счётчик в сегменте — `--ink3`, обычным весом, после названия.
   При нуле счётчик не рисуется, сам сегмент остаётся: состав панели
   не должен скакать (№232, №768). */
.seg .chip-n, .seg .chip-count {
  font-weight: 400; color: var(--ink3); background: none;
  padding: 0; min-width: 0; border: 0;
}

.seg-wide { display: flex; width: 100%; margin-bottom: var(--space-3); }
.seg-wide a { flex: 1; justify-content: center; }

/* Ряд из ТРЁХ срезов («Диалоги», 28.09.2026). Отличий от `.seg-wide` два,
   и оба вынуждены шириной колонки списка — 360px на десктопе и ~348
   на телефоне 380:
   • `flex: 1 1 auto`, а не `flex: 1` (то есть `1 1 0`) — у `.seg-wide`
     пункты делят ширину РОВНО поровну, и здесь это обрезало бы самый
     длинный: «Просят человека» — 15 знаков против 3 у «Все». Основа
     `auto` значит «своя ширина по содержимому», а лишнее место делится
     поверх неё; ряд всё так же занимает строку целиком;
   • отступ пункта `--space-2` вместо `--space-3` — это и есть те 24px,
     на которые третий пункт не влезал прежними отступами. Название
     укорачивать не пришлось: разница срезов читается только словами.
   Замер на узком (380): 28 знаков подписей ≈ 207px, отступы и зазоры 58,
   два счётчика до 44 — около 310 из 348. Влезает; вернётся длинное
   название — не влезет, и это цена решения «различимые слова дороже».
   Ряд занимает ВСЮ ширину колонки намеренно — ровно ту же, что поле
   поиска под ним (`.dlg-search input` — `width: 100%`); два органа
   одного списка стоят в одних границах. */
.seg-three { display: flex; width: 100%; flex: 1 1 auto; }
.seg-three a { flex: 1 1 auto; justify-content: center; min-width: 0;
               padding: 0 var(--space-2); }

/* История абонемента: про людей, а не про деньги. Свёрнута по умолчанию —
   в обычный день администратору нужен остаток, а не биография кода. */
.sub-history { margin-top: var(--space-2); }   /* вид — `.fold-line` (№476) */
.sub-event {
  display: flex; flex-wrap: wrap; gap: 6px;
  font-size: var(--text-meta); padding: 2px 0 2px var(--space-3);
  border-left: 2px solid var(--line);
}

/* --- ДЕНЬ --- */
/* Минута шкалы в пикселях. Всё остальное считается из неё, поэтому
   масштаб меняется одной строкой и сервер об этом не знает. */
/* ⚠️ Явный `overflow-y: visible` здесь БЕСПОЛЕЗЕН и не пишется: браузер
   всё равно вычисляет его как `auto`, раз `overflow-x` не `visible`
   (правило CSS Overflow — у пары «x/y» либо оба «visible», либо ни одного).
   Значит вертикальный скролл у `.dayview` появляется от ЛЮБОГО реального
   переполнения по высоте, и лечить его нужно у источника переполнения,
   а не здесь. Ровно так вылез вертикальный ползунок сетки после №198
   (ушёл `overflow: hidden` у `.dv-body`) — источник был в `_bookings_grid.html`,
   получасовая метка последнего часа рисовалась ЗА нижним краем колонки;
   починка — там, не тут. */
.dayview { --minpx: .9; --axis: 54px; --colw: 1fr; overflow-x: auto; }
/* Одна и та же раскладка у шапки и у тела: считают они её от `--cols`,
   а не от собственной ширины, иначе при прокрутке колонки разъезжаются. */
.dv-head, .dv-body {
  display: grid;
  grid-template-columns: var(--axis) repeat(var(--cols, 3), var(--colw));
  min-width: max-content;
}
.dv-head {
  position: sticky; top: 0; z-index: 3;
  background: var(--accent-soft); border: 1px solid var(--line);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.dv-corner, .dv-master {
  padding: var(--space-2) var(--space-3); font-size: var(--text-meta); font-weight: 500;
  border-left: 1px solid var(--line);
}
.dv-corner {
  border-left: 0; color: var(--ink2); font-size: var(--text-micro);
  text-transform: uppercase; letter-spacing: .05em;
  /* Липкая колонка времени (№198, решение владельца изменено с «второй
     шкалы справа»): угол шапки закреплён горизонтально вместе со шкалой
     под ним — обе части одной колонки, иначе прокрутка их бы разъединила. */
  position: sticky; left: 0; z-index: 1; background: var(--accent-soft);
}
.dv-master.is-off { color: var(--ink2); }
.dv-master .off-mark {
  display: block; font-size: var(--text-micro); font-weight: 400; color: var(--wait-ink);
}
.dv-body {
  border: 1px solid var(--line); border-top: 0;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);   /* острое, исключение: сетка */
  background: var(--raised);
  /* ⚠️ `overflow: hidden` здесь БЫЛ — снят ради липкой колонки времени
     (№198). Любое значение overflow, кроме `visible`, на предке между
     `.dv-axis` и реальным скролл-контейнером (`.dayview`) делает ЕГО
     точкой отсчёта для `position: sticky` — колонка молча переставала
     прилипать, хотя computed position оставался «sticky». Проверено
     прокруткой в headless-браузере: без этой строки `.dv-axis` уезжал
     вместе с сеткой. Скруглению нижних углов эта строка была не нужна —
     содержимое и так не достаёт до края (вертикальный отступ `.dv-axis,
     .dv-col`), проверено скриншотом.
  */
}
/* Полчаса сверху и снизу — чтобы первая и последняя метки времени
   не упирались в шапку и в край. */
.dv-axis, .dv-col {
  position: relative;
  height: calc(var(--rows) * var(--minpx) * 1px);
  margin: calc(var(--minpx) * 15px) 0;
}
.dv-col { border-left: 1px solid var(--line); }
/* Липкая колонка времени (№198): вместо второй шкалы справа — первая
   остаётся видна при прокрутке вбок на любом мастере, не только на
   первом и последнем столбце. `z-index` выше плашки записи (`.bk`, 1)
   и её выбранного состояния (`.bk.is-sel`, 4) — иначе выбранная плашка
   у первого мастера легла бы поверх шкалы. */
.dv-axis {
  position: sticky; left: 0; z-index: 5; background: var(--raised);
}
.dv-hour {
  position: absolute; right: 6px; top: calc(var(--top) * var(--minpx) * 1px);
  transform: translateY(-50%);
  font-size: var(--text-micro); color: var(--ink2); font-variant-numeric: tabular-nums;
}
.dv-line {
  position: absolute; left: 0; right: 0; top: calc(var(--top) * var(--minpx) * 1px);
  border-top: 1px solid var(--line);
}
.dv-line.is-half { border-top-style: dotted; opacity: .55; }
/* Нерабочее время штрихуется, рабочее остаётся чистым. Штриховка заметнее
   заливки: «сюда записывать не следует» должно читаться боковым зрением,
   а не сравнением двух похожих оттенков. */
.dv-col { background: var(--hatch); }
.dv-work {
  position: absolute; left: 0; right: 0;
  top: calc(var(--top) * var(--minpx) * 1px);
  height: calc(var(--len) * var(--minpx) * 1px);
  background: var(--raised);
}

/* --- плашка записи --- */
.bk {
  position: relative; z-index: 1;
  background: var(--accent-soft); border: 1px solid var(--accent);
  border-radius: var(--radius-sm); padding: 3px 7px;   /* острое, исключение: плашка записи */
  font-size: var(--text-meta); cursor: pointer; overflow: hidden;
  touch-action: manipulation;        /* сетка плашек перетаскивания не знает */
  transition: background var(--ease);
}
/* Своё нажатие вместо системной вспышки браузера (решение владельца 05.09):
   лёгкое затемнение, без движения. На iOS `:active` у `<div>` без обработчика
   касания не срабатывает — пустой `touchstart` стоит на контейнере сетки
   (`initBookingsGrid` в app.js). */
.bk:active { background: var(--sunken); transition-duration: .1s; }
.dv-col .bk {
  position: absolute; left: 3px; right: 3px;
  top: calc(var(--top) * var(--minpx) * 1px);
  height: calc(var(--len) * var(--minpx) * 1px);
}
/* Выбранная плашка не растёт — действия уехали в панель под сеткой.
   Обводка снята (решение владельца 05.09): она давала ту же бирюзовую
   вспышку на кадр, что и системная подсветка касания, — «открыт» теперь
   виден по самому листу, а не по рамке на плашке под ним. */
.bk.is-sel { z-index: 4; }
.bk-head { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
.bk-time { font-weight: 500; font-variant-numeric: tabular-nums; }
.bk-who { overflow: hidden; text-overflow: ellipsis; }
.bk-ico { margin-left: auto; display: inline-flex; gap: 4px; flex: none; }
/* Размер — на самом классе, не через родителя `.bk-ico`: значок
   «абонемент» с 03.09 стоит и в легенде под сеткой (`.bk-legend`), которая
   не значок плашки, — тот же приём, что уже был у `.rem` ниже. Значка
   «кабинет» («.room») с 03.09 нет — снят тем же решением, что и с плашки. */
/* ⚠️ Имя `bk-sub`, а не `sub` (№692, 13.09). Голый `.sub` попадал ещё
   и в ПОДЗАГОЛОВОК СТУДИИ в шапке (`base.html`, `.top .title .sub`):
   строка «спокойствие в центре Тбилиси» получала размер значка 13×13,
   переносилась по одному слову и, поскольку `overflow` видимый, рисовалась
   ЗА пределами своего блока — поверх панели под шапкой. Два разных
   предмета жили под одним именем, и совпадение было молчаливым:
   геометрия оставалась чистой (прямоугольник 13×13 ни с чем
   не пересекается), а глаз видел кашу. */
.bk-sub-ico { width: 13px; height: 13px; }
/* ⚠️ Вторая строка плашки красится ЧЕРНИЛАМИ С ПРОЗРАЧНОСТЬЮ, а не
   `--ink2`. Замер 24.09.2026 (№769): `ink2` на статусных заливках даёт
   2.9–4.2:1 — ниже порога читаемости, и сильнее всего на «ожидается»
   (`accent-soft`). Чернила на .72 держат 4.5:1 на всех пяти заливках
   во всех пресетах каталога — это утверждает `tests/test_status_scale.py`
   числом, а не на глаз. */
.bk-svc {
  color: rgba(var(--ink-rgb), .72); font-size: var(--text-micro);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Вторая строка плашки: с 25.09 в ней только длительность (`.bk-dur`).
   ⚠️ ЗНАЧКОВ ЗДЕСЬ БОЛЬШЕ НЕТ — все они стоят в углу `.bk-head`
   (`.bk-ico`), одним порядком у короткой и длинной плашки: книга,
   «значки — у правого края плашки, в одном порядке на всех». Правило
   №202 («значкам своя строка, крупнее и не в углу») этим пересмотрено:
   оно и давало два места, из-за которых «NEW» стоял у соседних плашек
   по-разному. Имя класса оставлено: своя строка у длительности всё ещё
   есть, и переименование тронуло бы адреса швов ни за что. */
.bk-badges { display: flex; align-items: center; gap: 4px; margin-top: 2px; }
.bk-dur { color: rgba(var(--ink-rgb), .72); font-size: var(--text-micro); margin-left: auto; }
/* Свой размер, не через родителя (`.bk-ico`/`.bk-badges`/`.bk-legend`) —
   колокольчик встречается во ВСЕХ трёх местах, и дублировать
   width/height/opacity в трёх родительских правилах менее надёжно, чем
   задать их у самого класса (тот же приём — у `.room`/`.sub` выше). */
.rem { width: 13px; height: 13px; opacity: .65; flex: none; }
.bk-mark { font-size: var(--text-micro); color: var(--wait-ink); }
/* Тоном вторичного текста, не `--warn` у соседней «вне графика»:
   соседство важнее заметности — закричи оба наравне, и в конце дня
   половина сетки была бы оранжевой, а смена перестала бы читать обе.
   ⚠️ В плашке под этим правилом осталась ОДНА строка — «предоплата
   не получена» (факт). «Конец сеанса: отметьте приход» из плашки убрано
   25.09 (п.28) и живёт строкой «Исход:» в панели записи — правило ниже. */
.bk-mark.bk-ask { color: var(--ink2); }
/* Та же подсказка в панели записи: сетка «метка | значение», тот же тон. */
.sheet-fields .bk-ask { color: var(--ink2); }
/* Значение «Оплачено» в строке полей окна записи (№153). Цветом оплаты,
   а не `--warn` у соседнего `.bk-mark`: это не предупреждение, а факт.
   С №287 — обычная строка сетки «метка | значение» («Оплата: Оплачено»),
   своей ширины у неё больше нет. */
.sheet-fields .bk-paid { font-weight: 500; color: var(--ok-ink); }
.bk-new {
  font-style: normal; font-size: 9px; font-weight: 500; letter-spacing: .04em;
  padding: 1px 4px; border-radius: var(--radius-sm);
  background: var(--accent); color: var(--raised);
}
/* Значок «АБ» — действующий абонемент (№199+№200). СВОЙ токен, не `--accent`:
   тот цвет уже означает «новый клиент» (.bk-new), и переиспользование
   спутало бы два разных факта на одной плашке. `--paid`/`--paid-bg` — те же
   токены, которыми на этой странице уже красится факт денег/оплаты
   (`.bk.is-paid`, `.bk-act .bk-paid`). */
.bk-sub {
  font-style: normal; font-size: 9px; font-weight: 500; letter-spacing: .04em;
  padding: 1px 4px; border-radius: var(--radius-sm);
  background: var(--ok-bg); color: var(--ok-ink);
}

/* Статусная шкала: статус несёт ЗАЛИВКА, свои токены st-* из /theme.css
   (tenants/<slug>.toml). ⚠️ ПЕРЕСМОТРЕНО 24.08 (решение владельца): статусы
   больше не ступени яркости одного тона — каждый СВОЙ ЦВЕТ, приглушённый,
   тёплый. Матрица всех восьми различима на глаз (мин. попарное расстояние
   CIE76 ≥ 15) и держит контраст текста — tests/test_status_scale.py.
   Легенда внизу сетки обязана брать ТЕ ЖЕ токены (там же тестом).
   Пунктир у pending/prepay — третий канал «не окончательно», но цвет
   различает и без него: у обоих свой тон.

   ⚠️ «Ожидается» с 0.3.2 ядра красится СВОИМ токеном (`--expected-bg`),
   а не `--accent-soft`. Прежний цвет зависел от акцента студии и в темах,
   где акцент близок к цвету исхода, сливался с ним (замер: ΔE 7.4
   к «пришёл» на «Спа на опушке»). Новый стоит на фиксированной шкале
   рядом с четырьмя исходами и держит ΔE ≥ 15 ко всем четырём на любом
   сочетании фона и акцента каталога — перебором, в наборе ядра.
   `--accent-soft` остался тем, чем был по книге: подложкой ВЫБРАННОЙ
   строки. */
.bk.expected { background: var(--expected-bg); border-color: var(--expected-ink); }
.bk.confirmed { background: var(--ok-bg); border-color: var(--ok-ink); }
.bk.came { background: var(--ok-bg); border-color: var(--ok-ink); }
.bk.pending { background: var(--wait-bg); border-color: var(--wait-ink); }
/* Ждёт предоплаты: место придержано, но денег ещё нет. Пунктир говорит
   «не окончательно» — так же, как у закрытого дня в календаре. */
.bk.prepay { background: var(--wait-bg); border-color: var(--wait-ink); }
.bk.no_show { background: var(--alert-bg); border-color: var(--alert-ink); }
/* Отменённая — призрак (решение владельца 23.08): слот фактически свободен,
   и тянуть взгляд сильнее живых записей плашка не должна — гаснет целиком,
   вместе с текстом. Токен при этом подобран ПОД СМЕСЬ: фактический цвет
   после наложения .55 на --surface держит свою — самую дальнюю — ступень
   шкалы (закреплено тестом по смеси, не по токену). Меняя прозрачность,
   меняй и долю в color-mix легенды ниже — тест сверяет числа. */
.bk.cancelled {
  background: var(--done-bg); border-color: var(--done-ink);
  opacity: .55;
}
.bk.cancelled .bk-time { text-decoration: line-through; }
/* Оплачено — ПОЛОСОЙ у левого края, а не заливкой: тем же приёмом и тем же
   токеном, что в таблице занятости кабинетов (`.rb.is-paid` ниже, решение
   владельца 28.08 — «заливка занята статусом, оплата полосой»). Смена
   смотрит на сетку с полуметра, и вопрос «за кого ещё не взяли деньги»
   обязан читаться краем глаза: полоса `ok-ink` держит к каждой из пяти
   заливок ≥ 4,9:1 и ΔE ≥ 58 на всех шести пресетах каталога (замер
   28.09.2026; порог 3:1 утверждает `tests/test_paid_stripe_stands_out.py`
   установки). Подпись полосе — в легенде под сеткой (`.sw-paid`, там же,
   где она объясняет ту же полосу в кабинетах).

   ⚠️ Заливку оплата больше НЕ перекрашивает (28.09.2026, жалоба смены
   «плохо видно, где оплата проведена»). Прежнее правило красило фон
   в `ok-bg` — РОВНО ТОТ ЖЕ токен, что у «подтвердил» и «пришёл», — то есть
   у самых частых записей оплата не давала на плашке ничего, кроме цвета
   времени. Формулировка «оплата сильнее, чем пришёл» (23.08) писалась,
   когда статусы были ступенями яркости одного тона и у оплаты был СВОЙ
   цвет; после перехода на пять смыслов (24.09, №769) своего цвета у неё
   не осталось, и «сильнее» означало «вместо статуса»: оплаченная НЕЯВКА
   выглядела «пришёл» — красная `alert-bg` подменялась зелёной.

   ⚠️ Полоса — на `::before`, а не на `box-shadow`, хотя у `.rb` она именно
   тенью: у плашки `box-shadow` занят вспышкой автообновления
   (`.bk.is-fresh` ниже), и анимация гасила бы полосу на свои 1,2 с.
   Из контура полоса не выходит — у `.bk` стоит `overflow: hidden`,
   и левый край обрезается вместе со скруглением (книга, п.4).

   ⚠️⚠️ **ВОЛОСЯНАЯ ГРАНИЦА `--raised` СЛЕВА ОБЯЗАТЕЛЬНА, и она половина
   правила.** Первая редакция (28.09, релиз 20260928-093013) ставила голую
   полосу 3px вплотную к рамке — и на «пришёл»/«подтвердил» смена её
   не увидела вовсе: у этих статусов `border-color` — ТОТ ЖЕ `--ok-ink`,
   и 1px рамки плюс 3px полосы читались как чуть более толстый левый край
   того же цвета. Замер пикселей средней линии плашки: у оплаченной шли
   ЧЕТЫРЕ тёмных пикселя подряд, у неоплаченной — один; всё остальное
   (класс на плашке, `content`, ширина, цвет, отсутствие перекрытия)
   было верным. Волосяная граница цветом поверхности разрывает этот ряд:
   рамка · поверхность · полоса — три разных пикселя, и полоса читается
   отдельным предметом на ЛЮБОМ статусе, а не продолжением рамки.
   ⚠️ Меняя `width` или убирая тень, мерить надо ПИКСЕЛИ ДВУХ ПЛАШЕК
   ОДНОГО СТАТУСА (`tests/browser/test_paid_chip_wears_a_stripe.py`),
   а не вычисленный стиль полосы: вычисленный стиль у дефекта был
   безупречен. */
.bk.is-paid::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 6px;
  background: var(--ok-ink);
  box-shadow: inset 1px 0 0 var(--raised);
}
.bk.is-paid .bk-time { color: var(--ok-ink); }
.bk.is-astray { border-color: var(--wait-ink); }
/* №175, тикет 02: «шкала устарела» — точечная перерисовка отложена
   (панель действий открыта), позиция плашки могла разъехаться с новым
   составом дня. Тот же токен, что у `.bk.is-astray` (--warn —
   предупреждение о геометрии, не отказ действия — тому служит --danger).
   Outline, а не border-color: рамка уже занята статусом/оплатой записи,
   а этот признак — временный, поверх любого статуса. */
.bk.is-scale-stale { outline: 2px dashed var(--wait-ink); outline-offset: -2px; }
/* Зеркало парной записи в колонке второго ведущего (№143): та же запись,
   приглушённая и с пунктиром — видно, ЧЕМ занят мастер, но ясно, что
   работа с записью идёт через основную плашку (клик ведёт к ней). */
.bk.is-mirror { opacity: .62; border-style: dashed; }
/* Плашка, только что изменившаяся при автообновлении: короткая вспышка,
   чтобы администратор заметил чужое действие, а не искал разницу глазами. */
.bk.is-fresh { animation: bk-flash 1.2s ease-out; }
@keyframes bk-flash {
  0% { box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .55); }
  100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); }
}
/* ═══════════════ Окно действий над записью (№287, 03.09) ═══════════════
   Было панелью под сеткой (`.bk-panel`), стало модалкой. Одна разметка,
   два вида по ширине: узко — лист снизу, широко — окно по центру.
   Слой — `--z-sheet` из лестницы в :root: над нижним баром, ПОД всем, что
   открывается из этого окна (форма «Изменить», боковая панель, оплата,
   вопрос) и ПОД карточкой события — та обязана быть видна поверх любого
   окна (решение владельца). Следствие: непогашенная карточка ложится на
   первый ряд кнопок — сначала «Понятно», потом работа; шов паркует чужие
   карточки на время сцены (`tests/browser/conftest.py`). Две ошибочные
   редакции по пути (190 — накрывала форму «Изменить»; 52 — окно под
   карточкой, но тогда карточка была ниже формы) — обе поймал шов, не глаза. */
.sheet-modal {
  position: fixed; inset: 0; z-index: var(--z-sheet);
  display: grid; align-items: end;         /* узко: лист прижат к низу */
}
/* ⚠️ `place-self: stretch` обязателен: абсолютный элемент внутри grid
   с `place-items: center` (широкий вид) получает `justify-self: center`
   и с `inset: 0` схлопывается до ширины содержимого — до нуля. Фон под
   окном на широком экране не затемнялся, а тап по фону не срабатывал
   (поймано скриншотом приёмки 03.09, не замером). */
.sheet-modal .modal-scrim { position: absolute; inset: 0; place-self: stretch; }
/* Высота — ПО СОДЕРЖИМОМУ (второй заход 03.09: постоянная высота дала
   дыру в 450px под двумя кнопками у отменённой записи — «собрано
   на коленках», ради чего задача и заводилась). `min-height` — чтобы окно
   не вырождалось в полоску; максимум — 88dvh, но не выше полосы
   `--sheet-top-band`, лишнее прокручивается внутри `.sheet-body`.
   Узко: низ листа прибит к низу экрана (`.sheet-modal { align-items: end }`),
   лист растёт вверх — шапка с крестиком по вертикали плавает, основной
   путь закрытия там свайп (принято владельцем). Широко: верх окна прибит
   (см. @media ниже) — верх и первый ряд кнопок стоят в одной точке у любой
   записи, низ подстраивается.
   Прокрутка страницы под окном заперта своим классом `body.sheet-open`,
   а не общим `modal-open`: окна поверх (оплата, вопрос) снимают
   `modal-open` при своём закрытии безусловно, и лист под ними снова
   поехал бы вместе со страницей. */
body.sheet-open { overflow: hidden; }
.sheet {
  position: relative; display: flex; flex-direction: column;
  width: 100%; overflow: hidden;
  min-height: 300px;
  max-height: min(88dvh, calc(100dvh - var(--sheet-top-band)));
  background: var(--raised); border: 1px solid var(--ink); border-bottom: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  /* Свайп вниз: лист следует за пальцем (`--bk-drag` ставит app.js на
     touchmove), отпущенный ближе 80px — плавно возвращается. Пока тянут,
     переход снят — иначе лист отставал бы от пальца. */
  transform: translateY(var(--bk-drag, 0px));
  transition: transform var(--ease);
  /* Появление окна — 200 мс (решение владельца 05.09: было мгновенным
     появлением, а не выездом). Играет на КАЖДЫЙ показ: `[hidden]` на
     родительской `.sheet-modal` даёт этому узлу `display: none`, и браузер
     запускает CSS-анимацию заново при каждом возврате в рендер.
     ⚠️ Анимировать здесь `translate` РЯДОМ с уже занятым `transform`
     (свайп вниз, выше) нельзя — испробовано и отклонено: пока анимация
     держит `translate`, `.sheet` меряется на 15–16px выше своей
     итоговой высоты (`max-height: min(88dvh, …)` считается иначе на
     трансформированном элементе), и слой-тень `.sheet-fade` внутри
     уезжает за нижний край листа — поймано пиксельным тестом
     (`tests/browser/test_header_and_sheet_scroll_hint.py`), не догадкой.
     Анимация — только `opacity`: другое свойство, никакого взаимодействия
     с трансформацией листа вообще. */
  /* ⚠️ Кривая — ЛИТЕРАЛОМ, не var(--ease): токен несёт ПАРУ «220ms + кривая»,
     и в сокращении animation его второе время парсится как ЗАДЕРЖКА —
     лист открывался 420 мс, из них первые 220 стоял невидимым (дефект
     №457, найден замером getAnimations() при сборке №432). */
  animation: sheet-in 200ms cubic-bezier(.4, 0, .2, 1) both;
}
@keyframes sheet-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.sheet.is-dragging { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .sheet { animation: none; }
}
.sheet-head {
  flex: 0 0 auto; position: relative;
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-4) var(--space-4) var(--space-3);
  border-bottom: 1px solid var(--line);
}
.sheet-titles { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.sheet-titles > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-head .panel-close { flex: none; margin-left: auto; }
/* Гриф — цель для свайпа вниз и подсказка, что это лист; только узко. */
.sheet-grip {
  position: absolute; top: 6px; left: 50%; transform: translateX(-50%);
  width: 40px; height: 4px; border-radius: var(--radius-sm); background: var(--line);   /* острое, исключение: ручка листа */
}
/* `min-height: 0` — иначе flex-элемент не ужимается ниже содержимого,
   `max-height` листа не срабатывает и прокрутка внутри не появляется. */
.sheet-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--space-3) var(--space-4) var(--space-4); }

/* Признак прокрутки на нижней кромке (№425). Первая редакция пробовала
   чистый CSS (`background-attachment: local`/`scroll` на `.sheet-body`
   самой): не годится — кнопки `.sheet-grid` непрозрачны, и там, где кромка
   приходится на ряд кнопок (частый случай — их до шести), тень красилась
   ПОД кнопкой и была не видна вовсе. Поймано скриншотом при приёмке, не
   расчётом (тот же урок, что у `scrollbar-gutter`: посчитать — не значит
   увидеть). Слой ОТДЕЛЬНЫЙ, лежит НАД содержимым (`.sheet` — общий
   `position: relative`, этот слой — последний в DOM и красится поверх),
   `pointer-events: none` — клики сквозь него доходят до кнопок как есть.
   Включатель — класс `is-on`, одна строка JS на `scroll` (`app.js`,
   `_updateSheetFade`): виден, пока есть что докручивать, гаснет на дне. */
.sheet-fade {
  /* Скруглять нижние углы не нужно самому слою: `.sheet` несёт
     `overflow: hidden` и свой `border-radius` — обрежет ЛЮБОГО ребёнка
     по своей же форме, второй раз задавать её здесь незачем. */
  position: absolute; left: 0; right: 0; bottom: 0; height: 36px;
  background: linear-gradient(rgba(var(--ink-rgb), 0), rgba(var(--ink-rgb), .32));
  pointer-events: none; opacity: 0; transition: opacity var(--ease);
}
.sheet-fade.is-on { opacity: 1; }
.bk-act { display: block; }

/* Сетка кнопок: ячейки ОДНОЙ ширины (`minmax(0,1fr)` — не даёт содержимому
   растянуть колонку), узко 2 колонки, широко 4. Кнопка — блок во всю
   ячейку; переменный текст («Кабинет: …», «Мастер: …») усекается
   многоточием, а не растягивает ячейку. `display:block`, а не `inline-flex`
   базового `.btn`: у flex-контейнера текст лежит в анонимном элементе,
   и `text-overflow` до него не достаёт. */
.sheet-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}
/* №698: кнопка на всю ширину зоны — третья в зоне из двух («Клиент» под
   «Изменить»/«Отменить»), чтобы в сетке не оставалось пустой ячейки. */
.sheet-grid .sheet-wide { grid-column: 1 / -1; }
.sheet-grid .btn {
  display: block; width: 100%; min-width: 0;
  height: 48px; min-height: 0; line-height: 46px; padding: 0 var(--space-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-align: center;
  /* Без разрядки базового `.btn` (.03em): в ячейке 160px постоянные
     подписи «В ожидание» и «Придёт» (№478 укоротил обе) иначе не помещаются
     и усекаются наравне с переменными — а усекаться должен только
     переменный текст («Кабинет: …», «Мастер: …»). */
  font-size: var(--text-meta); letter-spacing: 0;
}
/* ⚠️ Пин `[data-cancel-booking] { grid-column: -2 }` СНЯТ (№439, 04.09).
   Он был нужен, пока состав кнопок скакал: «Отменить» приходилось
   притягивать к углу, чтобы она не оказалась случайно рядом с «Мастер».
   Теперь у каждой кнопки фиксированный слот и порядок в DOM постоянен
   при ЛЮБОЙ записи — соседи «Отменить» всегда одни и те же, и притягивать
   её нечем и незачем. Отметка владельца к №287 («необратимое действие
   не на расстоянии промаха») держится теперь конструкцией, а не правилом.

   Денежный ряд — своя сетка над остальными слотами: он единственный,
   кто появляется и исчезает целиком (решение владельца к №439), и в общей
   сетке его исчезновение сдвигало бы всё, что ниже. Отдельным блоком
   ряд уходит вместе со своей строкой, а слоты 5–15 остаются на местах. */

/* ТРИ СОСТОЯНИЯ КНОПОК ОКНА ЗАПИСИ (решение владельца 05.09 по скриншоту):
   белая — доступна; серая, текст светлее — недоступна; тёмная — ОДНО
   главное действие (`.btn-hero`: «Принять оплату», у предоплаты —
   «Предоплата получена», см. шаблон). Четвёртого состояния — бежевой
   подсветки «текущий статус» — нет: состояние читается в шапке окна
   и на плашке, кнопкам повторять его незачем. Правило стоит ЗДЕСЬ, по
   контейнеру, а не по классам кнопок: базовый `.btn` в проекте тёмный,
   и любая новая кнопка окна унаследовала бы чужой цвет молча — а так
   она белая, пока её не назвали главной. (Бежевая `.btn-quiet` удалена
   из системы 05.09 — из перечня ниже она ушла вместе с классом.) */
.sheet-grid .btn, .sheet-grid .btn-ghost {
  background: var(--raised); color: var(--ink); border-color: var(--line);
}
.sheet-grid .btn:hover { background: var(--press); }
/* Нажатие — своя пара: белые кнопки темнеют до `--tint` (то же, что hover —
   на тапе hover не срабатывает, и это единственный видимый отклик), у
   главной кнопки фон и так тёмный, темнеет дальше до `--ink-deep`. Обе
   строки нужны из-за равной специфичности с базовым `.btn:active`. */
.sheet-grid .btn:active { background: var(--press); transition-duration: .1s; }
.sheet-grid .btn-hero, .sheet-grid .btn-hero:hover {
  background: var(--ink); color: var(--raised); border-color: var(--ink);
}
.sheet-grid .btn-hero:active { background: var(--ink); transition-duration: .1s; }
/* Недоступная — БЕЗ заливки (решение владельца 05.09): бежевый фон
   читался как «выбрано». Тот же белый, что у доступной, рамка светлее
   обычной, текст серый — отличие в контрасте, а не в цвете. */
.sheet-grid .btn[disabled], .sheet-grid .btn[disabled]:hover {
  background: var(--raised); color: var(--ink2);
  border-color: rgba(var(--ink-rgb), .08); opacity: 1;
}
/* Зазор между зонами окна (деньги / статус / параметры / действия /
   справочное) — полтора обычных промежутка между кнопками (`gap: --s2`),
   решение владельца 05.09. Зона — своя `.sheet-grid.sheet-zone`; внутри
   одной сетки «шире между этими рядами» CSS не даёт. */
.sheet-zone + .sheet-zone { margin-top: calc(var(--space-2) * 1.5); }

/* Строки полей: сетка «метка | значение». Метка всегда в первой колонке
   на одном x; строка — пара соседних ячеек, либо есть целиком, либо нет.
   104px — по самой длинной метке («Второй мастер:») на 13px. */
/* ⚠️ Промежуток между строками — `--space-1` 4px, а не 6px (28.09, просьба
   владельца «сократить межстрочный интервал»): строк в окне до девяти, и
   6px сверх строки в 18px растягивали блок на треть экрана телефона. Число
   взято из ритма `--space-*`, а не набрано на глаз: 6px в шкале книги нет
   вовсе, и оно было единственным таким зазором в окне. Сам кегль и
   межстрочье строки не тронуты — это `meta` 13/18 из шкалы книги. */
.sheet-fields {
  display: grid; grid-template-columns: 104px minmax(0, 1fr);
  gap: var(--space-1) var(--space-2); align-items: baseline;
  margin-top: var(--space-4); font-size: var(--text-meta);
}
.sheet-k { color: var(--ink2); }
.sheet-v { min-width: 0; overflow-wrap: anywhere; }

@media (min-width: 900px) {
  /* Верх окна прибит: отступ сверху — `--sheet-top-band`, ниже неё
     верх и первый ряд кнопок стоят в одной точке у любой записи. Высота
     по содержимому, потолок — до низа экрана.
     ⚠️ Величина ПЕРЕЖИЛА карточку события (№642, 10.09): до переезда
     уведомления вниз константа звалась `--bk-alert-band` и объяснялась
     как «полоса карточки». Карточки сверху больше нет, а отступ остался —
     он про то, что верх окна стоит в одной точке у любой записи, и
     трогать его владелец не просил. Имя переименовано именно поэтому:
     прочитав «полоса карточки события» и не найдя карточки, следующая
     правка сочла бы константу мёртвой и снесла — окно прыгнуло бы вверх
     на 240px. */
  .sheet-modal {
    align-items: start; justify-items: center;
    padding: var(--sheet-top-band) var(--space-4) var(--space-4);
  }
  .sheet {
    width: 720px; max-width: 100%;
    max-height: calc(100vh - var(--sheet-top-band) - var(--space-4));
    border-bottom: 1px solid var(--ink); border-radius: var(--radius-lg);
  }
  .sheet-grip { display: none; }
  .sheet-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  /* №698: на четырёх колонках «Клиент» встаёт в ряд к «Изменить»/«Отменить»
     двумя ячейками, а не отдельной строкой с пустыми соседями. */
  .sheet-grid .sheet-wide { grid-column: span 2; }
  .sheet-grid .btn { height: 40px; line-height: 38px; font-size: var(--text-meta); }
}
/* Кнопка, ставшая вопросом. Тихое подтверждение — только обводка; громкое
   (неявка, отмена) — цветом: последствия там серьёзнее и должны выглядеть
   иначе, чем обычное «пришёл». */
.btn.is-asking { border-color: var(--ink); font-weight: 500; }
.btn.is-asking.is-loud {
  background: var(--alert-bg); border-color: var(--alert-ink); color: var(--alert-ink);
}
.btn[disabled] { opacity: .45; cursor: default; }
.bk-act .btn { font-size: var(--text-meta); }

/* --- НЕДЕЛЯ --- */
.weekview { overflow-x: auto; }
/* Фиксированная раскладка: без неё длинное имя клиента забирает половину
   таблицы, а соседний день сжимается в три строки по букве. */
.wv {
  border-collapse: separate; border-spacing: 0;
  width: 100%; min-width: 900px; table-layout: fixed;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.wv th, .wv td { border-bottom: 1px solid var(--line); vertical-align: top; padding: 5px 6px; }
.wv thead th {
  background: var(--accent-soft); font-size: var(--text-meta); font-weight: 500;
  text-align: center; position: sticky; top: 0; z-index: 2;
}
.wv thead th.is-today { background: var(--accent-soft); }
.wv-corner, .wv-master {
  position: sticky; left: 0; z-index: 3; background: var(--raised);
  text-align: left; width: 140px; border-right: 1px solid var(--line);
  font-family: var(--font); font-size: var(--text-meta); font-weight: 500;
  text-transform: uppercase; letter-spacing: .04em;
}
.wv td.is-closed { background: var(--sunken); }
.wv-empty { color: var(--ink2); font-size: var(--text-meta); }
/* В неделе запись — одна строка: время, имя, иконки. Услуга и длительность
   остаются в подсказке, иначе семь дней не помещаются на экран. */
.weekview .bk { margin-bottom: 3px; }
.weekview .bk-svc, .weekview .bk-mark { display: none; }
.weekview .bk-who { min-width: 0; }

.bk-legend {
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4);
  font-size: var(--text-meta); color: var(--ink2); margin-top: var(--space-3);
}
.bk-legend span { display: inline-flex; align-items: center; gap: 5px; }
/* Легенда-спойлер (задание владельца 28.09.2026). Разметка одна на обе
   ширины: на телефоне — обычный `.fold`, сервер отдаёт закрытым, state
   ставит app.js (`syncLegendFold`); на десктопе спойлера нет — рамка и
   summary снимаются, и остаются те же две строки `.bk-legend`, что и
   до правки («на десктопе как есть»). */
/* Отступ сверху — шаг остальных спойлеров (задание владельца 29.09.2026):
   до спойлера зазор давал `margin-top` первой строки `.bk-legend`, он
   ушёл ВНУТРЬ карточки, и спойлер встал вплотную к сетке — читался её
   частью. Шаг тот же, что между соседними `.fold` (их `margin-bottom`).
   На десктопе гасится сбросом `.fold.legend-fold { margin: 0 }` ниже. */
.legend-fold { margin-top: var(--space-4); }
.legend-body { padding: 0 var(--space-4) var(--space-3); }
/* Потолок высоты и своя прокрутка — СРАЗУ, а не «когда не влезет»
   (урок меню «Ещё», 0.4.22): раскрывшееся на маленьком экране без
   потолка либо режется жёстко, либо уезжает за экран. */
@media (max-width: 999px) {
  .legend-body { max-height: 45vh; overflow-y: auto; }
}
@media (min-width: 1000px) {
  /* `.fold.legend-fold` — не украшение: правила `.fold { border;
     background; margin }` и `.fold > summary { display: grid }` стоят
     НИЖЕ по файлу и при равной специфичности перебивали бы этот сброс —
     легенда на десктопе рисовалась бы белой карточкой со стрелкой
     (поймано снимком и браузерным тестом, 28.09.2026). */
  .fold.legend-fold {
    border: 0; border-radius: 0; background: none;
    overflow: visible; margin: 0;
  }
  .fold.legend-fold > summary { display: none; }
  .legend-body { padding: 0; }
}
/* Квадратики — ТЕ ЖЕ токены st-*, что у плашек выше (закреплено тестом):
   похожий-но-другой цвет здесь означал бы легенду, которая врёт.
   18×18 — ступени шкалы близки по яркости, и на 12px их не разглядеть. */
/* Значок легенды — ОБРАЗЕЦ ПЛАШКИ, поэтому радиус берётся у неё:
   `.bk` скруглена `--radius-sm` («острое, исключение: плашка записи»),
   и легенда обязана показывать ту же форму, иначе она объясняет не то,
   что нарисовано на сетке.
   ⚠️ До 28.09.2026 здесь стоял `--radius-lg`, и на боксе 18px это давало
   РОВНЫЙ КРУГ при «Мягких» углах (18px радиуса на 18px стороны) —
   значки читались кружками, а плашки на сетке остаются прямоугольными.
   ⚠️ Потолок 6px нужен по геометрии, а не из вкуса: на стороне 18px круг
   начинается с 9px, то есть ровно с `--radius-sm` при «Мягких». Без
   потолка правка не изменила бы ничего на самой частой теме — умолчание
   платформы «Латте с корицей» как раз «Мягкие». При «Строгих» (3px)
   и «Обычных» (6px) потолок не делает ничего.
   ⚠️ Полоса оплаты (`.sw-paid`) при этом узнаваема сама: она нарисована
   `box-shadow: inset 3px 0 0` — полосой у ЛЕВОГО края, а не половиной
   заливки. Кружком её делало скругление; на квадрате полоса читается
   так же, как на плашке. */
.sw {
  width: 18px; height: 18px; display: inline-block;
  border-radius: min(var(--radius-sm), 6px);
}
.sw-expected { background: var(--expected-bg); border: 1px solid var(--expected-ink); }
.sw-confirmed { background: var(--ok-bg); border: 1px solid var(--ok-ink); }
.sw-came { background: var(--ok-bg); border: 1px solid var(--ok-ink); }
.sw-pending { background: var(--wait-bg); border: 1px solid var(--wait-ink); }
.sw-prepay { background: var(--wait-bg); border: 1px solid var(--wait-ink); }
.sw-no_show { background: var(--alert-bg); border: 1px solid var(--alert-ink); }
/* Квадратик «отменена» — не голый токен, а ТА ЖЕ смесь, что видна на сетке:
   плашка-призрак лежит на рабочем окне (--surface) под opacity .55, и легенда
   обязана показывать результат наложения, иначе она врёт. Доля в color-mix
   равна прозрачности плашки — расхождение ловит тест. */
.sw-cancelled {
  background: color-mix(in srgb, var(--done-bg) 55%, var(--raised));
  border: 1px solid color-mix(in srgb, var(--done-ink) 55%, var(--raised));
}
/* «Вне графика» ортогонален статусу и остаётся РАМКОЙ — тем же --warn,
   что красит border у .bk.is-astray; заливка нейтральная, вне статусных. */
.sw-off { background: var(--sunken); border: 1px solid var(--wait-ink); }

/* ============================================================
   №467: СВЁРТКА ОТРАБОТАННОГО УТРА И ЛИНИЯ ВРЕМЕНИ — только узкий экран
   ============================================================
   Сервер считает свёртку и пишет на `.dayview` класс `is-folded`
   и `--fold` (минуты от начала шкалы). ВСЯ механика — здесь, внутри
   @media: на десктопе те же атрибуты не меняют ни пикселя (правило
   владельца), поэтому вне медиа `.dv-fold` и `.dv-now` просто не
   рисуются, а `--fold-on` не определён и вычитание даёт ноль.

   Схлопывание — ПРЕДСТАВЛЕНИЕМ: смещения (`--top`) всех детей колонки
   написаны от ПОЛНОЙ шкалы (`_scale_from`), и в свёрнутом состоянии
   из них вычитается `--fold-on`. Поэтому `bookings_cells`, вставляющий
   плашку со смещением от полной шкалы, попадает в то же место и в
   свёрнутой сетке — риск №175/№135 (два пути смотрят в разные шкалы)
   закрыт по конструкции, а не договорённостью.

   Всплывшее выше границы (часы, линии, отработанные плашки) получает
   отрицательный `top` и обрезается `overflow: hidden` на колонке и оси;
   ровно поэтому точечная перерисовка свёрнутой плашки безвредна — она
   ложится в невидимую зону. */
.dv-fold, .dv-now { display: none; }

@media (max-width: 999px) {
  .dayview.is-folded { --fold-on: var(--fold, 0); }
  .dayview.is-folded .dv-axis,
  .dayview.is-folded .dv-col {
    height: calc((var(--rows) - var(--fold-on)) * var(--minpx) * 1px);
    overflow: hidden;
  }
  /* Метка часа центрирована на своей линии (translateY(-50%)), и у метки
     ровно НА границе свёртки верхняя половина уходит за клип — «13:00»
     резалось пополам. Оси даём 8px воздуха сверху (полвысоты метки)
     через clip-path: в оси живут ТОЛЬКО метки часов, плашек там нет,
     и показать выше границы нечего, кроме этой половины. Колонкам
     оставляем строгий overflow: отработанная плашка, кончающаяся ровно
     на границе, иначе выглядывала бы нижним краем. */
  .dayview.is-folded .dv-axis {
    overflow: visible;
    clip-path: inset(-8px 0 0 0);
  }
  .dayview.is-folded :is(.dv-hour, .dv-line, .dv-work, .dv-now) {
    top: calc((var(--top) - var(--fold-on)) * var(--minpx) * 1px);
  }
  .dayview.is-folded .dv-col .bk {
    top: calc((var(--top) - var(--fold-on)) * var(--minpx) * 1px);
  }

  .dayview.is-folded .dv-fold { display: flex; }
  .dv-fold {
    align-items: center; justify-content: center; gap: 6px;
    width: 100%; min-height: 40px; margin-top: var(--space-2);
    padding: 8px var(--space-3);
    border: var(--rule);
    border-radius: var(--radius-lg);
    background: var(--raised);
    color: var(--ink2); font-size: var(--text-meta); font-family: inherit;
    cursor: pointer; appearance: none;
  }

  /* Линия «сейчас» — только сегодня (сервер не рисует её на другой дате)
     и только на узком. Токен акцента, литерального цвета не заводим. */
  .dv-now {
    display: block; position: absolute; left: 0; right: 0; z-index: 2;
    top: calc(var(--top) * var(--minpx) * 1px);
    border-top: 2px solid var(--accent);
    pointer-events: none;
  }
}

/* Телефон: колонок мастеров в ширину не хватает — день прокручивается вбок,
   а неделя и так со своей прокруткой. */
@media (max-width: 899px) {
  /* №468 (решение владельца 06.09): колонка — половина окна после шкалы
     времени, в окне ровно две колонки целиком, третья за краем; правая
     граница второй ложится на край экрана. Для этого сетка уходит
     в полную ширину экрана (минус-поля съедают отступ `.wrap`), а ширина
     колонки считается от самого контейнера (cqw), не от вьюпорта:
     семейство 100vw запрещено (№373 — vw врёт на ширину полосы
     прокрутки). Вычитаемые 2px — левый и правый border `.dv-head`/
     `.dv-body`: без них правая граница второй колонки висла бы в 2px
     за краем. До 06.09 тут стояло `--colw: 168px` («полторы колонки»,
     имя мастера резалось краем). */
  .dayview {
    container-type: inline-size;
    margin-inline: calc(-1 * var(--space-4));
    --colw: calc((100cqw - var(--axis) - 2px) / 2);
  }
  /* Имя мастера в шапке усекается многоточием, не режется краем. */
  .dv-master { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .bk-period { min-width: auto; }
  .bk-nav { gap: var(--space-2); }
}

/* ============================================================
   ПРИЁМ ОПЛАТЫ: абонемент, доплата, деньги
   ============================================================ */
.pay-modal { max-width: 520px; }
.pay-sum {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: var(--space-3); margin-bottom: var(--space-3);
  background: var(--sunken); border-radius: var(--radius-lg);
}
.pay-sum b { font-family: var(--font); font-size: var(--text-num); }
.pay-modes { display: flex; flex-direction: column; gap: var(--space-2); }
/* Способ оплаты — выбор из трёх, а не галочки: платят чем-то одним. */
.pay-mode {
  display: flex; align-items: flex-start; gap: var(--space-2);
  padding: var(--space-3); min-height: var(--tap);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  cursor: pointer;
}
.pay-mode:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.pay-mode b { display: block; font-weight: 500; }
.pay-mode i { font-style: normal; font-size: var(--text-meta); }
.pay-money { margin-top: var(--space-3); }

/* Комбинированная оплата в истории: две суммы рядом, чтобы не считать в уме */
.pay-line { display: inline-flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
.pay-line .from-sub { color: var(--ink2); }
.pay-line .extra { color: var(--ink); font-weight: 500; }
/* Отменённое списание: видно, но не считается — строка гаснет, сумма зачёркнута */
.task.is-refunded { opacity: .6; }
.task.is-refunded .badge { text-decoration: line-through; }

/* Пометка внутри контактной плашки: «имя, не ник». Тише самого значения —
   это уточнение, а не второй контакт. */
.chip-note {
  font-style: normal; font-size: var(--text-micro); color: var(--ink2);
  opacity: .8; margin-left: 4px;
}

/* Занятый кабинет в дневной сетке. Цвет отличается от статусов записей:
   это занятость комнаты, а не человека, и путать их нельзя — «аренда» рядом
   с «пришёл» читалась бы как ещё один статус клиента. */
.dv-blocks {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
}
.room-block {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  /* Был `var(--r2)` — такого токена в проекте нет вовсе, и скругление молча
     не применялось: неизвестная переменная не ошибка, а пустое значение. */
  padding: 6px var(--space-3); border-radius: var(--radius-lg);
  background: var(--sunken); border: var(--rule);
  font-size: var(--text-meta);
}
.room-block b { font-weight: 500; }
.room-block .rb-money { font-variant-numeric: tabular-nums; font-weight: 500; }
.room-block .btn { min-height: 32px; padding: 0 var(--space-3); font-size: var(--text-meta); }
.rb-аренда { border-color: var(--ok-ink); background: var(--ok-bg); }
.rb-ремонт { border-color: var(--wait-ink); background: var(--wait-bg); }

/* След блокировки: та же механика, что «История» у абонемента, — свёрнутый
   <details> прямо под своей строкой. Занимает всю ширину карточки аренды
   (она flex-контейнер), иначе встал бы четвёртым столбиком рядом с кнопкой
   и на 380px раздавил бы её в две буквы. */
.rb-log { flex: 1 1 100%; margin-top: 4px; }   /* вид — `.fold-line` (№476) */
.rb-log-row {
  display: flex; flex-wrap: wrap; gap: 6px;
  font-size: var(--text-meta); padding: 2px 0 2px var(--space-3);
  border-left: 2px solid var(--line);
}
/* Снятые за день — тот же вид, что живые аренды, но приглушённый: это
   история, а не занятость, и спорить за внимание со списком занятых
   кабинетов она не должна. */
.rb-dropped { opacity: .8; }

/* Свёрнутые блоки на «Записях»: календарь месяца и аренды кабинетов.
   Общая механика — нативный <details>: работает без JS, с клавиатуры и в
   поиске по странице. Маркер свой, чтобы стрелка была той же, что у шагов
   календаря, и одинаково смотрелась в обеих темах.

   ⚠️ `.sell-fold` (форма продажи на «Абонементах», №409 часть 2, 01.09)
   снят 29.09.2026: форма открывается ОКНОМ (книга, «Экран со списком:
   порядок органов», п.5) — развёрнутая на месте, она выталкивала список
   вниз, и смена теряла из виду то, ради чего пришла. */
/* №476: спойлер-полоса — ОДИН класс `.fold` на всю админку. Прежние
   имена (.month-fold, .rents, .rooms-fold, .acc; .sell-fold снят
   29.09.2026, .ev-fold снят №769 — разметки с ними не осталось)
   остались доменными метками в разметке — адреса для JS, швов и своих
   отступов; вида они больше не несут. Новый спойлер = `fold` + метка. */
.fold {
  margin-bottom: var(--space-4);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--raised);
  /* Без него фон заголовка (ниже) прямоугольником вылез бы за скруглённые
     верхние углы карточки — тем же приёмом, что у `.rooms-fold`. */
  overflow: hidden;
}
/* Раскрывающийся блок ОДИН на всю систему (книга, «Чистота экрана»):
   строка списка с шевроном СПРАВА (`ink-3`, вниз/вверх), раскрытие внутри
   той же рамки. Второго вида свёрнутой полосы нет.
   ⚠️ Было: заливка `accent-soft` на всю строку и шеврон «›» слева цветом
   акцента. Книга держит акцент за одним главным действием страницы
   (принцип 2), а свёрнутая группа — не действие; от заливки спойлер
   читался кнопкой. Теперь строка — обычная поверхность списка, отличает
   её только шеврон. */
/* ⚠️ СЕТКА, А НЕ ФЛЕКС (п.2 шестого круга №786, 26.09.2026; книга,
   правило переписано владельцем). Шеврон обязан стоять РОВНО ПОСЕРЕДИНЕ
   строки — на 50% её ширины, на одной вертикали во всех строках экрана,
   и не зависеть ни от длины заголовка слева, ни от подписи справа.
   Флексом это невыразимо: там положение середины считает содержимое.
   Три колонки `1fr | 20px | 1fr` при симметричных боковых отступах дают
   центр средней ровно на половине строки — по построению, а не подбором.
   Прежняя правка (п.2 четвёртого круга) ставила шеврон в КРАЙНЮЮ колонку
   у правого края; правило было неверное, книга исправлена.
   Число (или подпись) живёт в правой колонке и прижато к правому краю —
   так столбик чисел сохраняется, как и раньше. */
.fold > summary {
  display: grid; grid-template-columns: 1fr 20px 1fr;
  align-items: center; column-gap: var(--space-2);
  /* 44px тап-зона: заголовок сворачивания нажимают пальцем, а не курсором */
  min-height: 44px; padding: 0 var(--space-4);
  cursor: pointer; list-style: none;
  font-weight: 500; font-size: var(--text-body);
  background: var(--raised);
}
.fold > summary::-webkit-details-marker { display: none; }
/* Телефон (задание владельца 29.09.2026): подписи-подсказки правой
   колонки скрыты до 900px (правило `.mf-hint, .rents-peek, .rf-peek`
   ниже), и при сетке «1fr | 20px | 1fr» заголовок обрезался на
   полуслове («Занятость каби…») ровно на половине ПУСТОЙ строки.
   Здесь заголовок занимает свободную ширину, шеврон уходит к правому
   краю (за ним остаётся только колонка числа, если она есть). Центр
   шеврона (книга №786 п.2) остаётся правилом широких экранов, где
   правая колонка живёт, — держит его тот же страж
   `test_fold_row_columns.py` на 1440. */
@media (max-width: 899px) {
  .fold > summary { grid-template-columns: minmax(0, 1fr) 20px max-content; }
}
/* п.23: полоска просроченной группы (`.fold-overdue`) обязана повторять
   контур рамки спойлера, поэтому скругления живут у САМОЙ строки, а не
   только у `.fold`: закрытый спойлер — все четыре, раскрытый — верхние
   (низ закрывает тело). Раньше радиусы были только у рамки, а `overflow`
   резал полоску по кривой. */
.fold > summary { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.fold:not([open]) > summary { border-radius: var(--radius-lg); }
/* Место и кегль глифа у большой полосы — в общем блоке «шеврон
   раскрытия» ниже: там же он получает свою постоянную колонку. */
.fold > summary:hover { background: var(--sunken); }
.fold[open] > summary { border-bottom: 1px solid var(--line); }
/* Число справа — мелко, без слова (образец владельца 05.09): слово живёт
   в title/aria, глазу достаточно цифры. */
/* Число группы — СВОЯ КОЛОНКА У ПРАВОГО КРАЯ (п.1, 25.09): фиксированная
   ширина и выравнивание вправо, чтобы у всех строк экрана числа стояли
   в столбик, а не плясали за концом заголовка. `3ch` держит трёхзначное;
   цифры табличные, поэтому «111» и «999» одной ширины. */
.fold-n {
  /* Правую колонку и выравнивание даёт сетка строки (`justify-self: end`);
     `margin-left: auto` снят — во флексе он забирал свободное место,
     в сетке забирать нечего, а лишнее правило врало бы об устройстве. */
  min-width: 3ch; text-align: right;
  font-size: var(--text-meta); font-variant-numeric: tabular-nums;
  color: var(--ink2);
}
/* Срочность группы — СТАТУСОМ, а не заливкой всей строки (№476, правка
   25.09.2026): цвет статуса законен (книга, «Цвета»), но подложка во всю
   ширину делала из спойлера плашку. Теперь просроченные несут полоску
   `alert` у левого края — тем же приёмом, каким строка диалога говорит
   «ход за студией»; «Сегодня» не красится вовсе: она и так первая
   раскрытая. */
.fold-overdue > summary { box-shadow: inset 3px 0 0 var(--alert-ink); }
.fold-overdue > summary .fold-n { color: var(--alert-ink); }
/* Мелкий рядный спойлер ВНУТРИ карточки (история, журнал, отзывы,
   напоминание вручную): те же глиф и механика, но строкой 13px без
   рамки-карточки — полноразмерная полоса раздула бы карточку. Было
   четыре независимые копии этого вида. */
.fold-line > summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 6px;
  font-size: var(--text-meta); color: var(--ink2);
  min-height: var(--tap);
}
.fold-line > summary::-webkit-details-marker { display: none; }
.fold-line > summary:hover { color: var(--ink); }

/* ═══ ШЕВРОН РАСКРЫТИЯ — ОДИН ВИД НА ВСЮ СИСТЕМУ (п.4, 25.09) ════════════
   Книга: «раскрывающийся блок один на всю систему: строка списка
   с шевроном СПРАВА (`ink-3`, вниз/вверх)».
   Видов было четыре: большой `.fold` (верный), рядный `.fold-line`
   (глиф «›» ПЕРЕД текстом и цветом АКЦЕНТА), расшифровка начислений
   и «Цены и ставки» в панели мастера (нативный треугольник браузера
   слева), плюс шесть нативных `<details>` в Партнёрке — их владелец
   и увидел («в Партнёрке шеврон слева у текста»).
   Геометрию строки каждый спойлер держит СВОЮ — полоса 44px, строка 13px,
   вторая строка строки списка, — а вид глифа и сторона живут здесь, одним
   местом. Новый спойлер дописывает себя в этот список, а не заводит своё
   `::before`. */
.fold > summary::after,
.fold-line > summary::after,
.payout-lines > summary::after,
.mf-money > summary::after {
  content: "⌄";
  /* СВОЯ КОЛОНКА ПОСТОЯННОЙ ШИРИНЫ, глиф по её центру (п.2, 25.09):
     ширина глифа плавает со шрифтом темы, и без колонки правый край
     строки дышал бы от экрана к экрану. */
  flex: none; width: 20px; text-align: center;
  margin-left: auto;
  font-size: var(--text-body); line-height: 1; color: var(--ink3);
  transition: transform var(--ease); transform-origin: center 40%;
}
/* ⚠️ У БОЛЬШОЙ полосы справа стоит ЕЩЁ И ЧИСЛО (`.fold-n`), и свободное
   место обязан забирать ОДИН из двух. Здесь глиф его не берёт: его
   отступ фиксирован, а `margin-left: auto` остаётся числу.
   ПОЧЕМУ ЭТО ВАЖНО (дефект 25.09, п.1): когда `auto` стоял у обоих,
   flexbox делил свободное место ПОПОЛАМ — по 440,7 px каждому на 1440, —
   и число вставало в середине, своей для каждой строки: «Просроченные»
   на 882, «Позже» на 850. В столбик они не выстраивались никогда, и
   виноват был не шаблон, а два `auto` в одной строке. */
/* У большой строки шеврон стоит в СРЕДНЕЙ колонке сетки (п.2 шестого
   круга №786): `margin-left: auto` из общего правила выше здесь ни при
   чём — в сетке место задаёт колонка. Кегль крупнее, чем у рядного
   спойлера: полоса выше. */
.fold > summary::after {
  grid-column: 2; grid-row: 1; justify-self: center;
  margin-left: 0; font-size: var(--text-section);
}
.fold[open] > summary::after,
.fold-line[open] > summary::after,
.payout-lines[open] > summary::after,
.mf-money[open] > summary::after { transform: rotate(180deg); }
/* Строка-носитель обязана быть флексом, иначе глиф не уедет к правому
   краю (`margin-left: auto`). У `.fold`/`.fold-line` он уже стоит. */
.payout-lines > summary, .mf-money > summary {
  display: flex; align-items: center;
}
/* «Цены и ставки» в панели мастера: нативного маркера у неё не было
   скрыто вовсе — треугольник браузера стоял слева от подписи. */
.mf-money > summary { cursor: pointer; list-style: none; }
.mf-money > summary::-webkit-details-marker { display: none; }
/* ⚠️ `.fold-body` СНЯТ 30.09.2026: его единственным носителем была
   Партнёрка, и её формы уехали в окна (задание владельца) — разметки
   с этим классом не осталось ни одной. Мёртвый класс не оставляем
   «на всякий случай»: `.btn-quiet` уже выстрелил один раз.

   ⚠️ И заодно поправлено УТВЕРЖДЕНИЕ, которое здесь стояло: «до 25.09
   таких тел было три со своими отступами (`.acc-body`,
   `.old-sessions-body`, инлайновые `margin-top` в Партнёрке)» —
   читалось как «сведены в одно», а сведён был только третий.
   `.acc-body` («Задачи») и `.old-sessions-body` («Доступы») живут
   со своими отступами и сейчас, каждый со своей причиной.
   Правило книги «раскрывающийся блок дышит» (16px между строкой,
   которая раскрывает, и первым элементом внутри) держат ОНИ; общего
   тела у `.fold` в системе нет, и следующему спойлеру его придётся
   либо завести заново, либо взять один из этих двух. */

/* №473 снят целиком (№769, заход 1). `.ev-fold`/`.ev-fold-body` носили
   сами «События» (свёрнутые группы чипов — ушли в №768) и три денежных
   экрана (спойлер «Свой период…» — заменён селектом `.period-bar`);
   `.filter-chip.is-zero` был про нулевые чипы тех же групп. Разметки
   с этими именами больше нет ни одной — стили удалены, а не оставлены
   «на всякий случай»: мёртвый класс уже выстрелил один раз
   (`.btn-quiet` на «Текстах писем»). */

/* ⚠️ `.sell-fold` снят отсюда 29.09.2026 вместе со свёрткой продажи:
   форма «Продать абонемент» открывается окном (книга, «Экран со
   списком: порядок органов», п.5), разметки с этим именем больше нет
   ни одной. Мёртвый класс не оставляем «на всякий случай» — правило
   абзацем выше, и оно уже выстреливало (`.btn-quiet`). */
.month-fold > summary:focus-visible, .rents > summary:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
/* Подсказки трёх спойлеров внизу «Записей» («месяц целиком», сводка аренд,
   сводка занятости кабинетов) — одним правилом (решение владельца 01.09):
   на узком экране их не показываем вовсе, только название. Порознь они
   переносились на вторую строку по-разному (у одной по центру, у другой
   слева, у третьей вовсе пряталась) — блоки выходили разной высоты.
   Раскрытие всё равно показывает содержимое, подсказка в заголовке —
   только удобство «не раскрывая». 900px — тот же порог, на котором
   раньше держался только `.mf-hint`: тут действительно начинает
   переноситься самая длинную из трёх подсказок (сводка аренд с
   несколькими записями). */
.mf-hint, .rents-peek, .rf-peek { display: none; }
@media (min-width: 900px) {
  /* ⚠️ БЕЗ `margin-left: auto` (п.7, 25.09). Он был здесь ТРЕТЬИМ `auto`
     в одной строке — рядом с числом и шевроном, — и flexbox делил
     свободное место между ними: подпись уезжала вправо, а на узком
     переносилась второй строкой и утаскивала шеврон за собой. Книга:
     правый край строки — две фиксированные колонки (число и шеврон),
     подпись в них не входит и стоит сразу за заголовком. */
  /* ⚠️ Подписи — К ПРАВОМУ КРАЮ строки (п.3 шестого круга №786,
     26.09.2026). Было: сразу за заголовком, то есть в левой половине —
     три спойлера «Записей» читались как продолжение названия, а правый
     край строки пустовал. Колонку и прижим даёт сетка строки
     (`> :nth-child(2)`), здесь остаётся только видимость и усечение. */
  .mf-hint, .rents-peek, .rf-peek {
    display: inline; margin-left: 0;
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
}
/* Кто в какой колонке. Первый ребёнок — заголовок (левая половина),
   второй — число или подпись (правая, прижата к краю), шеврон —
   средняя. Голый текст без обёртки (`<summary>Выплачено</summary>`)
   становится анонимным элементом сетки и попадает в первую колонку сам.
   ⚠️ Правило «второй ребёнок — направо» намеренно общее, а не список
   классов: подписи трёх спойлеров «Записей» (п.3 шестого круга №786 —
   «Кресло 1 — 3 · Кресло 2 — 1…», «сегодня нет», «месяц целиком · 5%»)
   и число групп `.fold-n` стоят в разметке на одном месте, и второй
   перечень имён разошёлся бы с ней молча. */
/* ⚠️ Прижим — `text-align`, а НЕ `justify-self` (замер 26.09 на 390):
   `justify-self: start` даёт элементу его max-content ширину, и длинный
   заголовок вылезает за свою колонку ПОВЕРХ шеврона — «Календарь
   загруженности» перекрывал его на 56 px. При растяжении по умолчанию
   элемент равен колонке, и многоточие ниже наконец работает. */
.fold > summary > * { grid-row: 1; }
.fold > summary > :first-child { grid-column: 1; text-align: left; }
.fold > summary > :nth-child(2) { grid-column: 3; text-align: right; }
/* Строка спойлера не переносится: и заголовок, и подпись усекаются
   многоточием, число и шеврон остаются в своих колонках (книга,
   «Текст в тесноте» и «Раскрывающийся блок один на всю систему»). */
.rents-title, .fold > summary > * {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Подсказки в заголовке — обычным регистром: это данные, а не метка */
.mf-hint, .rents-peek {
  font-family: var(--font); font-weight: 400;
  text-transform: none; letter-spacing: 0; font-size: var(--text-meta);
}
/* Раскрывающийся блок дышит (книга, «Чистота экрана», правка 24.09):
   между строкой-заголовком и первым элементом внутри — space-4 16px,
   а не впритык. Раньше верхний паддинг был 0 — «Себе / В подарок»
   у продажи абонемента упирался прямо в заголовок спойлера. */
.month-fold > div, .rents > .dv-blocks { padding: var(--space-4) var(--space-3) var(--space-3); }
.month-fold .month { margin-bottom: 0; }


/* ═══ Экран «Расчёт зарплат» (№113, вторая половина, 29.09.2026) ═══════
   Строка сетки — длительность, сумма и «убрать»; на телефоне поля рядом,
   а не столбиком: пара «40 → 50» читается одной мыслью и ломать её
   переносом незачем. Подпись поля мелкая и над ним — форма та же, что
   у пар «вопрос-ответ» автоответчика. */
.rate-rows { display: flex; flex-direction: column; gap: var(--space-2); }
.rate-row {
  display: flex; align-items: flex-end; gap: var(--space-2);
  flex-wrap: wrap; min-width: 0;
}
.rate-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 0 1 8rem; }
.rate-lab { font-size: var(--text-micro); color: var(--ink2); text-transform: uppercase;
            letter-spacing: .05em; }
.rate-cell input { width: 100%; }
/* Ступень и «у всех» — текстом, не полем: в личных ставках правится
   только своя колонка, и поле там, где менять нечего, обещало бы правку. */
.rate-step { font-weight: 500; flex: 0 0 5rem; }
.rate-common { flex: 1 1 10rem; min-width: 0; font-size: var(--text-meta); }
.rate-drop { flex: none; min-width: var(--tap); }
.rate-foot {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  align-items: center; justify-content: space-between;
  margin-top: var(--space-3);
}
.mr-body { padding: var(--space-4) var(--space-3) var(--space-3); }
/* ⚠️ `.payroll-masters .fold`, `… > summary` и `… .mr-body` сняты
   30.09.2026 вместе с разворотом личных ставок на месте: элементы списка
   стали СТРОКАМИ с окном (`tx-row line-row`; книга, «Настройки списком»),
   и спойлеров на этом экране не осталось ни одного. Мёртвые правила
   не оставляем «на всякий случай» — тем же порядком сняты `.sell-fold`
   и `.ev-fold`.
   Префикс `.master-rates` у `.mr-body` ушёл там же: он повторял общее
   правило в той же строке, а начинка окна отступы берёт у `.sheet-body`.
   Сам класс `.master-rates` остался доменной меткой строки (как
   `.svc-line`, `.tx-row`) — адрес для швов, вида он не несёт. */

/* «Тексты писем»: объяснение — первая строка блока (книга, 29.09.2026),
   а карточки текстов внутри него — строки списка. Тот же приём, что
   у личных ставок выше: своя рамка и фон у вложенных спойлеров сняты,
   делит их линия. */
.lt-lead { margin: 0 0 var(--space-3); max-width: 70ch; }
.lt-list .lt-card {
  border: 0; border-radius: 0; background: none; margin: 0;
  border-top: 1px solid var(--line);
}
.lt-list .lt-card > summary { background: none; padding-inline: 0; }
/* Разделитель групп внутри списка остаётся на месте: он делит строки
   ВНУТРИ блока, а не стоит между заголовком страницы и блоком. */
.lt-list .settings-group { margin: var(--space-4) 0 var(--space-1); }

/* ============================================================ КАЛЕНДАРЬ МЕСЯЦА
   Кольцо показывает долю занятого времени от того, сколько студия может
   принять. Шкала ступенчатая, а не непрерывный градиент: четыре состояния
   различимы на глаз и не требуют вглядывания в оттенок, а между ними стоят
   границы, которые администратор может назвать словами. */
.month { margin-bottom: var(--space-4); }

.month-head {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-3); margin-bottom: var(--space-3);
}
.month-title {
  font-family: var(--font); font-weight: 500;
  letter-spacing: .04em; font-size: var(--text-body); min-width: 9em; text-align: center;
}

.month-grid {
  /* №423б (05.09): `minmax(0, 1fr)`, не `1fr`. Голый `1fr` значит
     `minmax(auto, 1fr)`, и min-content клетки (44px тап-зоны) не давал
     колонкам ужаться — на 380 семь колонок просили 332px при 303
     доступных, и воскресенье обрезалось. Клетка масштабируется от ширины
     контейнера: aspect-ratio держит круг, фиксированных px нет. */
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
}
.month-wd {
  text-align: center; font-size: var(--text-meta); font-weight: 500;
  color: var(--ink2); padding-bottom: 2px;
}

.month-cell {
  position: relative; aspect-ratio: 1;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 1px;
  border: 0; background: transparent; cursor: pointer;
  border-radius: var(--radius-full); padding: 0;
  font-family: inherit; color: var(--ink);
}
.month-cell.is-empty { cursor: default; }
.month-cell:disabled { cursor: default; }

/* Кольцо рисуется поверх клетки, а число — внутри него. Обводка идёт
   против часовой от двенадцати, как на всех индикаторах прогресса. */
.month-ring {
  position: absolute; inset: 0; width: 100%; height: 100%;
  transform: rotate(-90deg); pointer-events: none;
}
.month-ring circle {
  fill: none; stroke-width: 2.5; stroke-linecap: round;
}
.ring-bg { stroke: var(--line); }
/* 2πr при r=15 ≈ 94.25 — длина окружности, от неё считается заполнение */
.ring-fill {
  stroke: var(--accent);
  stroke-dasharray: calc(var(--p, 0) * 94.25) 94.25;
  transition: stroke-dasharray .3s ease-out;
}

.month-num { font-size: var(--text-control); font-weight: 500; line-height: 1; }
.month-count {
  font-size: var(--text-micro); line-height: 1; color: var(--ink2);
  font-variant-numeric: tabular-nums;
}

/* Ступени загрузки. Пустое кольцо не красим вовсе: свободный день —
   отсутствие сигнала, а не ещё один сигнал. */
.month-cell.is-free .ring-fill { stroke: var(--line); }
.month-cell.is-low  .ring-fill { stroke: var(--ok-ink); }
.month-cell.is-mid  .ring-fill { stroke: var(--wait-ink); }
.month-cell.is-high .ring-fill { stroke: var(--alert-ink); }
/* Записи есть, а графика на день нет: кольцо целое и пунктирное — это не
   «занято», а «смена не выставлена». Пунктир повторяет отметку закрытого дня,
   цвет говорит, что это требует внимания. */
.month-cell.is-astray .ring-fill {
  stroke: var(--alert-ink); stroke-dasharray: 4 4;
}

.month-cell.is-today .month-num { color: var(--on-accent, var(--ink)); font-weight: 500; }
.month-cell.is-today::after {
  content: ''; position: absolute; bottom: 4px; width: 4px; height: 4px;
  border-radius: var(--radius-full); background: var(--ink);
}
.month-cell.is-selected { background: var(--accent-soft); }
.month-cell.is-selected .month-num { font-weight: 500; }
/* Прошедшие дни приглушены так же, как в сетке графика: работа идёт
   вперёд, а прошлое остаётся читаемым, но не тянет взгляд. */
.month-cell.is-past { opacity: .45; }
.month-cell.is-closed .month-num { color: var(--ink2); }
.month-cell.is-closed .ring-bg { stroke-dasharray: 3 4; }

.month-cell:hover:not(.is-empty) { background: var(--sunken); }
.month-cell:focus-visible {
  outline: 2px solid var(--ink); outline-offset: 2px;
}

.month-legend {
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
  margin: var(--space-3) 0 0; font-size: var(--text-meta); color: var(--ink2);
}
.month-legend span { display: inline-flex; align-items: center; gap: 5px; }
.ld {
  width: 10px; height: 10px; border-radius: var(--radius-full);
  border: 2.5px solid var(--line);
}
.ld-free { border-color: var(--line); }
.ld-low  { border-color: var(--ok-ink); }
.ld-mid  { border-color: var(--wait-ink); }
.ld-high { border-color: var(--alert-ink); }
.ld-astray { border-color: var(--alert-ink); }

[data-month-box].is-loading { opacity: .5; pointer-events: none; }

/* На десктопе календарь не должен растягиваться во всю ширину экрана:
   клетка в 120px — это мишень, а не календарь. */
@media (min-width: 900px) {
  .month, [data-month-box] { max-width: 420px; }
}

/* ------------------------------------------------ поиск в списках */
/* ⚠️ Своей отрисовки у поиска «Диалогов» БОЛЬШЕ НЕТ (29.09.2026).
   Здесь лежал третий экземпляр одного и того же органа: `display: block`
   вместо общей флекс-обёртки, лупа абсолютом вместо соседа в строке,
   своя `min-height` и свой `padding-left: 34px`. Разметка у него всё
   это время была ТА ЖЕ, что у общего `.search` (`<label class="search
   dlg-search"><svg>…<input></label>`), — эти правила лишь перекрывали
   общие и давали высоту 46 вместо 44. Класс остался адресом (ширина
   `search-wide`, швы), вида он больше не несёт. */
.dlg-search input {
  /* 16px, иначе iOS зумит страницу при фокусе — на списке это выглядит
     как прыжок вёрстки при каждом касании поля. */
  font-size: var(--text-body);
}
[aria-busy="true"] { opacity: .6; transition: opacity 120ms ease; }

/* ------------------------------------------------ журнал платежей */
.pay-log { width: 100%; border-collapse: collapse; font-size: var(--text-control); }
.pay-log th {
  text-align: left; padding: 9px var(--space-3); font-size: var(--text-micro);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink2);
  border-bottom: var(--rule); background: var(--sunken); font-weight: 500;
}
.pay-log td { padding: 8px var(--space-3); border-bottom: var(--rule); vertical-align: middle; }
.pay-log .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* ⚠️ Ширина колонки действий НЕ зависит от подписи кнопки (09.09):
   `width: 1%` в таблице с автораскладкой означает «сожмись по содержимому»,
   а содержимое здесь — иконки известного размера. До правки в колонке
   стояли подписи, и самая длинная из них («Отменить продажу», одна строка
   из тринадцати) раздвигала колонку у ВСЕХ строк и уводила таблицу
   за правый край экрана. */
.pay-log .pay-acts { width: 1%; white-space: nowrap; text-align: right; }
.pay-log .pay-acts .btn + .btn { margin-left: 4px; }
/* Коробка истории списаний — карточка без внутреннего отступа, когда в ней
   таблица (как у журнала «Платежей»): иначе строки стояли бы с двойным
   отступом. Пустое состояние и шапка «Списания по коду» свой отступ держат. */
.card:has(> .pay-log.usage-log) { padding: 0; }
/* Код абонемента — одно слово: в узкой колонке карточки клиента (647px)
   «PFX-0000» переносился по дефису. */
.pay-log.usage-log .c-code { white-space: nowrap; }
.card:has(> .pay-log.usage-log) > .usages-scope { padding: var(--space-3) var(--space-3) 0; }
.card:has(> .pay-log.usage-log) > .usages-more { margin: var(--space-2) var(--space-3) var(--space-3); width: calc(100% - 2 * var(--space-3)) !important; }
/* Дата и время операции — одной строкой, цифры моноширинно: столбик дат
   читается сверху вниз, а перенос делал бы строку выше кнопки. */
.pay-log .c-when { white-space: nowrap; color: var(--ink2);
                   font-variant-numeric: tabular-nums; }
/* «оплачено 3 сен» — вторая строка ячейки и только при расхождении дней
   (№621). Тише самой даты: это уточнение, а не второй равноправный факт. */
.pay-log .when-paid {
  display: block; font-size: var(--text-micro); color: var(--wait-ink);
}
/* Списание с абонемента — не деньги студии, и число не должно читаться
   как касса: тот же кегль, тише цветом. Разводит их колонка «Способ». */
.pay-log .c-sum.is-sub { color: var(--ink2); }
/* Имя факту вместо прочерка (№622): «Бесплатно» и «Не оплачено».
   Это слово, а не сумма, поэтому кегль обычный, цифровые начертания
   не наследуются, а перенос запрещён — в узкой колонке «Не оплачено»
   разваливалось на две строки и делало строку выше кнопки.
   Долг выделен `--warn`: смена ищет его глазами при сверке кассы,
   чтобы добрать деньги. Бесплатный — тише: он закрыт, добирать нечего. */
.pay-log .sum-word {
  font-variant-numeric: normal; white-space: nowrap;
  font-size: var(--text-meta); font-weight: 400; color: var(--ink2);
}
.pay-log .sum-word.is-debt { color: var(--wait-ink); font-weight: 500; }
.pay-log .sum-extra {
  display: block; font-size: var(--text-micro); color: var(--ink2);
  font-variant-numeric: normal; white-space: nowrap;
}
/* Комментарий операции (№547): узкая колонка, длинный текст переносится
   словами, а не растягивает таблицу за экран. */
.pay-log .pay-note { max-width: 18em; white-space: normal; overflow-wrap: anywhere;
                     color: var(--ink2); font-size: var(--text-meta); }

/* Кнопка-иконка: подпись живёт в `title`/`aria-label`, на месте — значок.
   ⚠️ Размер — 40×36 (книга, DayNav и «Экран объекта»), один на все
   иконочные кнопки; вид у класса выше по файлу. Прежние `--tap`
   (44×44) снято: тап-зону на телефоне добивает внешний отступ ряда,
   а разная высота кнопок в одной строке управления ломала слот. */
.btn-ico {
  padding: 0; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.btn-ico svg { width: 19px; height: 19px; display: block; }
/* Общая модалка вопроса (`askConfirm` / `askForm`). Факты операции идут
   строкой над полями и могут быть многострочными — журнал записи приходит
   готовым списком, и переносы в нём значащие. */
/* Вопрос стоит ПОВЕРХ других модалок: его открывают из приёма оплаты
   и из формы записи. У всех `.modal` z-index 200, и порядок решался бы
   местом в разметке — то есть менялся бы от страницы к странице. */
#ask-modal { z-index: var(--z-ask); }

#ask-modal [data-ask-facts] {
  white-space: pre-line;
  margin: 0 0 var(--space-3);
}
#ask-modal [data-ask-error] { color: var(--alert-ink); }
#ask-modal .field:last-child { margin-bottom: 0; }

/* Карточка-метрика с плохой новостью: отменённые операции. Тот же приём,
   что у охвата бота, — рамка и фон предупреждения, без красных цифр. */
.stat.is-alarm { border-color: var(--wait-ink); background: var(--wait-bg); }

/* Свой период: две даты и кнопка. Отдельная форма, а не модалка, — отчёт
   за произвольный отрезок открывают, чтобы сверить с бухгалтерией, и поля
   должны быть видны вместе с результатом. */
.period-own { display: grid; gap: var(--space-3); }
.period-own .two-fields { margin: 0; }

/* Динамика по дням — столбики из тех же чисел, что в таблицах выше.
   Без библиотеки графиков: один ряд значений рисуется высотой блока,
   и в отличие от холста он переживает и пустые данные, и печать.
   Высота приезжает из `data-fill` (см. `paintFills` в app.js) — inline-style
   в разметке запрещён политикой безопасности страницы. */
.day-chart {
  display: flex; align-items: flex-end; gap: var(--space-2);
  overflow-x: auto; padding: var(--space-3);
}
.day-col {
  flex: 1 0 44px; display: flex; flex-direction: column;
  align-items: center; gap: 4px; min-width: 44px;
}
.day-col i {
  display: block; width: 100%; min-height: 2px; height: 0;
  background: var(--accent); border: var(--rule); border-bottom: 0;
  transition: height .2s ease;
}
.day-col .day-sum {
  font-size: var(--text-meta); font-variant-numeric: tabular-nums; color: var(--ink);
}
.day-col .day-name { font-size: var(--text-micro); color: var(--ink2); }
/* Столбики растут снизу: колонка выравнивает содержимое по низу, а сам
   столбик стоит первым в потоке — поэтому переворачиваем колонку. */
.day-col { flex-direction: column-reverse; justify-content: flex-start; }

/* Колонка, к которой пришли по ссылке «N записей впереди»: подсветка
   на пару секунд, чтобы глаз нашёл нужного мастера среди восьми. */
.dv-col.is-target { background: var(--accent-soft); transition: background .3s ease; }

/* Контакты клиента в окне записи: дозвониться из сетки в одно касание.
   С №287 живут в ячейке значения строки «Телефон:» / «Telegram:»
   (`.sheet-v.bk-contact`). Тап-зона 44px — планшет ресепшена стоит на стойке,
   и попадать по строке текста пальцем неудобно. */
.bk-contact { display: flex; flex-wrap: wrap; gap: 0 var(--space-3); align-items: center; }
.bk-contact a {
  min-height: var(--tap); display: inline-flex; align-items: center;
  color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--line);
}
.bk-contact a:hover { border-color: var(--ink); }
.bk-contact .bk-tel { font-variant-numeric: tabular-nums; font-weight: 500; }

/* «Бот не сможет написать» (№78) — предупреждение в ячейке «Откуда:»:
   от него зависит, набирать ли человека руками накануне. Цветом
   предупреждения, тем же, что «вне графика», без восклицательных знаков. */
.bk-nobot { display: block; color: var(--wait-ink); }

/* Отменённый платёж остаётся видимым, но перестаёт читаться как деньги. */
.pay-log tr.is-void td { color: var(--ink2); }
.pay-log tr.is-void .num { text-decoration: line-through; }
@media (max-width: 899px) {
  /* Аналитические таблицы (способы оплаты, по мастерам, по услугам) — три-четыре
     колонки чисел, их читают целиком и прокручивают вбок. */
  .pay-log { min-width: 560px; }
  .card:has(.pay-log) { overflow-x: auto; }

  /* ⚠️ Журнал операций и расходы (`.cards`) — НАОБОРОТ: строка
     разворачивается карточкой (решение владельца 09.09, «смена работает
     с телефона»). До правки они прокручивались вбок вместе с остальными,
     и на 390px за краем оставались сумма, способ и обе кнопки — то есть
     всё, ради чего на страницу заходят.

     Карточка собирается ПЕРЕСТАНОВКОЙ ячеек (flex + `order`), а не второй
     разметкой под телефон: два набора строк в одном документе разъехались
     бы на первой же правке, и половину из них никто бы не увидел. Шапка
     таблицы прячется — её роль берут на себя место и вес ячейки. */
  .pay-log.cards { min-width: 0; display: block; }
  .card:has(.pay-log.cards) { overflow-x: visible; }
  .pay-log.cards thead { display: none; }
  .pay-log.cards tbody { display: block; }
  .pay-log.cards tr {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: 2px var(--space-2);
    padding: var(--space-3) var(--space-3) 10px;
    border-bottom: var(--rule);
  }
  /* ⚠️ Длинное название переносится, а не распирает страницу (№493, 13.09):
     услуга «Очень Длинное Название…» в карточке операции уводила документ
     вбок на 30px — горизонтальная прокрутка на телефоне. Найдено замером
     ширины, а не чтением разметки: в короткой сцене дефект не виден. */
  .pay-log.cards td {
    display: block; padding: 0; border: none;
    min-width: 0; overflow-wrap: anywhere;
  }
  /* Пустая ячейка на телефоне не занимает строку прочерком: в таблице
     прочерк держит колонку, в карточке он держал бы только место. */
  .pay-log.cards td.is-blank { display: none; }
  .pay-log.cards .c-when { order: 1; font-size: var(--text-meta); }
  .pay-log.cards .c-sum {
    order: 2; margin-left: auto; text-align: right;
    font-size: var(--text-section); font-weight: 500;
  }
  .pay-log.cards .c-who { order: 3; flex-basis: 100%; font-weight: 500; }
  .pay-log.cards .c-what { order: 4; flex-basis: 100%; }
  .pay-log.cards .c-how { order: 5; font-size: var(--text-meta); color: var(--ink2); }
  .pay-log.cards .c-took { order: 6; font-size: var(--text-meta); color: var(--ink2); }
  /* Разделитель ставит РАЗМЕТКА карточки, а не текст ячейки: в таблице
     эти же значения стоят в своих колонках и точка между ними лишняя. */
  .pay-log.cards .c-how:not(.is-blank) + .c-took::before { content: '·'; margin-right: 5px; }
  /* ⚠️ `width: 1%` — приём ТАБЛИЦЫ («сожмись по содержимому»); у flex-элемента
     он читается как основа в 1 % ширины строки, и обе кнопки уезжали
     за правый край карточки. Снимаем его вместе со сменой раскладки. */
  .pay-log.cards .c-acts { order: 7; margin-left: auto; width: auto; }
  .pay-log.cards .c-note { order: 8; flex-basis: 100%; max-width: none; }
  /* История списаний (№493, доделка 15.09): код — рядом с датой, мастер —
     приглушённо рядом с тем, кто списал. Своих ячеек у «Платежей» нет,
     порядок остальных общий. */
  .pay-log.cards .c-code { order: 1; font-size: var(--text-meta); color: var(--ink2); }
  .pay-log.cards .c-master { order: 5; font-size: var(--text-meta); color: var(--ink2); }
  .pay-log.cards .pay-why td { flex-basis: 100%; }
}

/* ═══════════════ Отчётная таблица → карточки на телефоне (№493) ═══════════
   Три таблицы «Платежей» («Способы оплаты», «По мастерам», «По услугам»)
   на 390px оставались таблицами: колонки сжимались до «ДЕ», числа липли
   к краю, а последняя колонка уезжала за экран. Телефон — не уменьшенный
   десктоп (решение владельца 13.09).

   Приём тот же, что у журнала операций выше (`.pay-log.cards`), и это
   намеренно: карточка собирается ПЕРЕСТАНОВКОЙ ячеек, второй разметки
   под телефон нет — два набора строк разъехались бы на первой правке.
   Отличие одно: у отчётной таблицы нет действий, поэтому строка проще —
   имя слева, число справа, остальное подписью под ними. */
.report-table { width: 100%; border-collapse: collapse; font-size: var(--text-control); }
.report-table th {
  text-align: left; padding: 8px var(--space-3); font-size: var(--text-meta);
  font-weight: 500; color: var(--ink2); text-transform: uppercase;
  letter-spacing: .06em; border-bottom: var(--rule);
}
.report-table td {
  padding: 10px var(--space-3); border-bottom: var(--rule);
  overflow-wrap: anywhere;
}
.report-table tr:last-child td { border-bottom: none; }
/* Числовая колонка — табличные цифры и правый край: столбец сумм читают
   сверху вниз, и «1 755» под «195» должны совпасть разрядами. */
.report-table .num {
  text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* ОДНА СЕТКА КОЛОНОК НА ЭКРАН (п.5, 25.09; книга: «таблицы, стоящие друг
   под другом на одном экране, держат одну сетку колонок — одноимённые
   по роли колонки стоят на одной вертикали»). До правки ширину каждой
   таблице считал браузер ПО СОДЕРЖИМОМУ, и три таблицы «Платежей»
   («Способы оплаты», «По мастерам», «По услугам») расходились колонками
   на десятки пикселей: глаз сравнивал суммы по диагонали.
   `table-layout: fixed` + постоянная ширина числовых колонок ставит их
   на одну вертикаль во всех таблицах экрана; имя забирает остаток.
   ⚠️ Только от 561px: ниже таблицы разворачиваются карточками
   (`@media (max-width: 560px)` ниже), и колонок там нет вовсе. */
@media (min-width: 561px) {
  .report-table { table-layout: fixed; }
  /* 140, а не 132: самый длинный заголовок числовой колонки —
     «С АБОНЕМЕНТА» — в капители с разрядкой занимает 112,4 px, и
     с отступами ячейки (12+12) в 132 не помещался: текст заголовка
     вылезал за правый край своей колонки на 4,4 px и стоял правее
     собственного числа (замер 26.09, п.1 пятого круга №783). Ширина общая
     для всех таблиц экрана — это и есть «одна сетка колонок». */
  .report-table .num { width: 140px; }
  .report-table .pay-acts { width: 104px; }
  /* ⚠️ Числовая ячейка, которая на телефоне становится ПОДПИСЬЮ (`.c-side`),
     на широком остаётся колонкой под своим заголовком — и правый край
     у неё тот же (п.1 пятого круга №783, 26.09). Без этого правила «Операций»,
     «Посещения» и «С абонемента» стояли слева в столбце, чей заголовок
     прижат вправо: глаз искал число не там, где подпись, а сравнивать
     столбец сверху вниз было нечем — разряды не совпадали. Вправо их
     ставит не `.num` (у той на телефоне своя роль — крупное главное
     число), а это правило, действующее только на широком. */
  .report-table .c-side {
    text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  /* Имя услуги или мастера длиннее колонки — многоточием, а не переносом
     в три строки: высота строк таблицы должна быть одинаковой. */
  .report-table td:first-child, .report-table th:first-child {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
}
/* Слово-подпись внутри ячейки: на широком его роль играет ЗАГОЛОВОК
   колонки, и дублировать его в каждой строке значит удваивать текст;
   на узком заголовков нет вовсе, и без слова остаются голые числа. */
.report-table .u { display: none; }

@media (max-width: 560px) {
  .report-table, .report-table tbody, .report-table tr, .report-table td {
    display: block;
  }
  .report-table thead { display: none; }
  .report-table tr {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: 2px var(--space-2);
    padding: var(--space-3) var(--space-3) 10px; border-bottom: var(--rule);
  }
  .report-table td { padding: 0; border: none; }
  /* ⚠️ Длинное имя переносится, а не распирает страницу: в отчёт попадают
     названия услуг и мастеров любой длины, и одно слово без пробелов
     уводило страницу вбок — горизонтальная прокрутка на телефоне (№570
     держит это числом). */
  .report-table .c-name {
    flex-basis: 100%; font-weight: 500; min-width: 0; overflow-wrap: anywhere;
  }
  /* Главное число уходит вправо и растёт: цифры — то, ради чего страницу
     открывают (направление владельца 13.09). */
  .report-table .num {
    order: 2; margin-left: auto;
    font-size: var(--text-section); font-weight: 500;
  }
  .report-table .c-side {
    order: 3; font-size: var(--text-meta); color: var(--ink2);
  }
  .report-table .c-side + .c-side::before { content: '·'; margin-right: 5px; }
  .report-table .u { display: inline; }
  /* Доля на телефоне — подпись, а не второе крупное число рядом с суммой:
     два одинаково громких числа в строке спорят между собой. */
  .report-table .c-share {
    order: 3; margin-left: 0;
    font-size: var(--text-meta); font-weight: 400; color: var(--ink2);
  }
  .report-table .c-share + .c-side::before,
  .report-table .c-side + .c-share::before { content: '·'; margin-right: 5px; }
}

/* ═══════════════ Занятость кабинетов (№202) ═══════════════
   Свёрнутый блок в самом низу «Записей»: строки — кабинеты, столбцы — часы
   студии. Форма из постановки Т4 и утверждённого макета 28.08.
   Цветов здесь нет — только токены, как во всём файле. */
/* Базовый вид несёт `.fold` (№476); здесь — только свой отступ сверху. */
.rooms-fold { margin-top: 16px; }
/* Видимость и положение — общим правилом трёх спойлеров, у `.mf-hint`
   выше (скрыта на узком экране, справа от 900px). Здесь остаётся только
   то, что у этой подсказки своё: шрифт. */
.rf-peek { font-weight: 400; font-size: var(--text-meta); }
.rf-body { padding: 14px; border-top: 1px solid var(--line); overflow-x: auto; }

/* ⚠️ Ширина НЕ подгоняется под один экран (правка задания 28.08): это код
   платформы, у следующего клиента и экраны другие, и часов в настройке может
   быть не четырнадцать, и кабинетов не три.
   Поэтому ни одного зашитого числа про размер экрана:
   — колонок часов ровно `--h`, значение приходит из настройки студии;
   — кабинеты это СТРОКИ, их число задаётся данными и на ширину не влияет;
   — ячейка живёт между 46px (ниже подпись «10:00» не читается) и 96px
     (выше растёт пустота, а не польза) — отсюда таблица сама перестаёт
     растягиваться на широком экране;
   — `width: max-content` не даёт растянуться в пустоту, `max-width: 100%`
     не даёт вылезти, когда место есть;
   — когда не влезает даже минимум, прокрутка идёт ВНУТРИ `.rf-body`,
     страница при этом не едет и сетку записей не выталкивает. */
.rooms {
  display: grid;
  /* 46px — не вкус, а замер: ниже перестаёт помещаться подпись «10:00»
     (11px шрифт плюс отбивка). Это и есть граница читаемости; уже неё
     таблица не сжимается, а прокручивается. Верхние 96px — предел, после
     которого растёт пустота, а не польза. */
  grid-template-columns: minmax(76px, 104px) repeat(var(--h), minmax(46px, 96px));
  width: max-content; max-width: 100%;
  border: 1px solid var(--line); background: var(--raised);
}
/* ⚠️ НИ `overflow: hidden`, НИ скругления у самой таблицы — и это не вкус.
   С `overflow: hidden` содержимое, не влезающее в `max-width: 100%`, ОБРЕЗАЛОСЬ
   вместо прокрутки: на 390px таблица показывала 324px из нужных 708 и доскроллить
   их было нечем — часть кабинетов просто исчезала. Замер поймал это там, где
   глаз бы не заметил: страница вбок не ехала, и всё выглядело исправным.
   Прокрутку держит `.rf-body`; скругление даёт карточка `.rooms-fold` снаружи. */
.rt-corner, .rh {
  background: var(--accent-soft); border-bottom: 1px solid var(--line);
  font-size: var(--text-micro); padding: 6px 4px; color: var(--ink2); text-align: center;
}
.rt-corner { text-align: left; padding-left: 10px; font-weight: 500; }
.rt-name {
  border-bottom: 1px solid var(--line); border-right: 1px solid var(--line);
  padding: 0 10px; font-weight: 500; font-size: var(--text-meta);
  display: flex; align-items: center; background: var(--sunken);
  min-height: 54px;
}
/* Полоса кабинета занимает все часовые столбцы: плашка внутри позиционируется
   процентами от ширины полосы, а не привязывается к столбцу, — сеанс на 40
   минут не обязан начинаться на границе часа. */
.rt-lane {
  grid-column: 2 / -1; position: relative; min-height: 54px;
  border-bottom: 1px solid var(--line);
  display: grid; grid-template-columns: repeat(var(--h), 1fr);
}
.rt-cell { border-right: 1px solid var(--line); opacity: .5; }

.rb {
  position: absolute; top: 4px; bottom: 4px; left: var(--l); width: var(--w);
  border-width: 1px; border-style: solid; border-radius: var(--radius-lg);
  padding: 3px 6px; overflow: hidden; font-size: var(--text-micro); line-height: 1.25;
  display: flex; flex-direction: column; gap: 1px;
}
.rb-time { font-weight: 500; font-size: var(--text-micro); }
.rb-who { font-weight: 500; white-space: nowrap; overflow: hidden;
          text-overflow: ellipsis; }
.rb-m { color: var(--ink2); font-size: var(--text-micro); white-space: nowrap;
        overflow: hidden; text-overflow: ellipsis; }

/* ⚠️ Статусы — СОСТАВНЫМИ селекторами и ПОСЛЕ `.rb`: иначе шорткат
   `border-width/style` выше сбросил бы цвет рамки в currentColor, и заливка
   была бы статусной, а рамка — цвета текста. Ровно этот дефект поймала
   слепая приёмка в макете; здесь он закрыт по построению.
   Список статусов короче, чем у плашек: отменённая кабинет не занимает,
   а «вне графика» — свойство графика мастера, у кабинета его нет. */
.rb.expected { background: var(--expected-bg); border-color: var(--expected-ink); }
.rb.confirmed { background: var(--ok-bg); border-color: var(--ok-ink); }
.rb.came { background: var(--ok-bg); border-color: var(--ok-ink); }
.rb.pending { background: var(--wait-bg); border-color: var(--wait-ink); }
.rb.prepay { background: var(--wait-bg); border-color: var(--wait-ink); }
.rb.no_show { background: var(--alert-bg); border-color: var(--alert-ink); }
/* Оплата — полосой слева, а НЕ заливкой, как на плашках сетки: там заливка
   свободна, здесь она занята статусом (решение владельца 28.08). Токен тот же. */
.rb.is-paid { box-shadow: inset 3px 0 0 var(--ok-ink); }
.rb.is-pair { border-width: 2px; border-style: double; }

.rooms-legend {
  display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
  font-size: var(--text-meta); color: var(--ink2); margin: 10px 0 0;
}
.rooms-legend span { display: inline-flex; align-items: center; gap: 5px; }
.rooms-legend .sw { width: 14px; height: 14px; }
/* Квадратик оплаты — в ДВУХ легендах: здесь, под таблицей кабинетов,
   и под сеткой записей (с 28.09, там же полоса появилась у плашки). Один
   класс и одно слово «оплачено» на оба места — вторая формулировка одного
   смысла разошлась бы с первой. Сама полоса у плашки нарисована `::before`,
   а у строки кабинета тенью: вид один, причина разницы — в комментарии
   у `.bk.is-paid::before` (вспышка автообновления занимает `box-shadow`). */
.sw-paid { background: var(--raised); border: 1px solid var(--line);
           box-shadow: inset 3px 0 0 var(--ok-ink); }
.sw-pairmark { background: var(--raised); border: 2px double var(--ink2); }

@media (max-width: 900px) {
  /* ⚠️ Ячейку здесь НЕ трогаем. Раньше минимум прыгал 46→52px на границе
     900px, и получался уступ: на 768px таблица влезала, а на 1024 — уже нет,
     хотя экран шире. Замер поймал; лечится тем, что минимум один на все
     ширины, а брейкпоинт правит только подпись кабинета. */
  .rt-name { font-size: var(--text-meta); padding: 0 6px; }
}

/* Аренда и ремонт в полосе кабинета: занятость БЕЗ записи — ни клиента,
   ни мастера, поэтому свой вид, а не статус. Найдено adversarial-проходом:
   движок считает блокировки наравне с записями (`room_spans`), и таблица,
   рисовавшая только записи, показывала арендованный кабинет свободным. */
.rb-block {
  position: absolute; top: 4px; bottom: 4px; left: var(--l); width: var(--w);
  border: 1px solid var(--wait-ink); border-radius: var(--radius-lg);
  background: var(--wait-bg); color: var(--wait-ink);
  padding: 3px 6px; overflow: hidden; font-size: var(--text-micro); line-height: 1.25;
  display: flex; flex-direction: column; gap: 1px;
}
/* Хвост уборки: комната ещё занята, хотя сеанс кончился (Т5). Штриховка,
   а не заливка, — это не занятость человеком, а пауза студии. */
.rb-tail {
  position: absolute; top: 4px; bottom: 4px; left: var(--l); width: var(--w);
  border-radius: var(--radius-lg); opacity: .55;
  background: repeating-linear-gradient(45deg,
    var(--line) 0 4px, transparent 4px 8px);
}
.sw-block { background: var(--wait-bg); border: 1px solid var(--wait-ink); }
.sw-tail { background: repeating-linear-gradient(45deg,
    var(--line) 0 4px, transparent 4px 8px); border: 1px solid var(--line); }

/* Закрытый кабинет: движок не предлагает его никогда (`free_rooms` берёт
   `list_rooms()` без выключенных), поэтому полоса не должна выглядеть
   свободной. Штриховка на всю полосу плюс слово в подписи; живая запись,
   если она там есть, рисуется поверх и остаётся читаемой. */
.rt-lane.is-closed {
  background: repeating-linear-gradient(45deg,
    var(--line) 0 5px, transparent 5px 10px);
}
.rt-name.is-closed { color: var(--ink2); }
.rt-off {
  display: block; font-weight: 400; font-size: var(--text-micro); color: var(--wait-ink);
  text-transform: uppercase; letter-spacing: .04em;
}
.sw-closed { background: repeating-linear-gradient(45deg,
    var(--line) 0 5px, transparent 5px 10px); border: 1px solid var(--line); }


/* Подпись под сеткой времени в форме записи: место под ДВЕ строки.
   ⚠️ Не косметика. У выходного мастера текст длиннее («…не работает
   (отпуск) — записать можно только вне графика») и переносится на вторую
   строку, у работающего он в одну. На обычном ноутбуке разницы не видно —
   окно упирается в `max-height: 86vh`; на высоком экране потолок
   перестаёт работать, и окно прыгало на 21.7px при переключении мастера
   (замер слепой приёмки 28.08, 1440×1600). Резерв убирает прыжок
   у содержимого, а не прячет его потолком.
   3.4em — это ровно две строки при здешнем line-height (замер: строка
   21.7px, две — 43.4px). Не «на глаз»: меньше не хватает, больше отдаёт
   пустоту под однострочной подписью. */
[data-nb-times-note] { min-height: 3.4em; }

/* Список времени, пока идёт запрос: приглушён, но НА МЕСТЕ.
   Стирать его и рисовать тени значило проводить высоту блока через
   промежуточное состояние при каждом переключении мастера. */
.nb-times.is-waiting { opacity: .45; pointer-events: none; }

/* ------------------------------------------------ тексты писем (№716) */
/* Карточка текста — свёртка `.fold`; содержимое под заголовком получает
   те же поля по бокам, что и заголовок, иначе поля ввода легли бы
   вплотную к рамке.
   ⚠️ `.fold` + `.card` СКЛАДЫВАЮТ отступы, и от этого страдает не вид,
   а НАЖАТИЕ: у `.card` свой `padding: var(--space-4)`, поэтому `summary`
   оказывался уже рамки на 34 px с каждой стороны — по этой полосе клик
   не раскрывал ничего (замер 25.09, п.6: summary 1038 при рамке 1072).
   Книга: «кликабельна вся строка целиком, не её часть» — значит поля
   строки держит сам `.fold`, а карточке они тут не нужны. */
.fold.card { padding: 0; }
.lt-card > :not(summary) { margin-left: var(--space-3); margin-right: var(--space-3); }
.lt-card > form { margin-bottom: var(--space-3); }
.lt-state { font-weight: 400; }
/* Кнопки вставок — не во всю ширину, как обычный `.btn-row`: их бывает
   шесть, и растянутые они читались бы как главные действия формы. */
.lt-slots { margin-bottom: 6px; }
.lt-slots .btn { flex: 0 0 auto; min-height: 44px; }
/* Предпросмотр — готовое письмо, переносы строк как в Telegram. */
.lt-preview {
  white-space: pre-wrap; font: inherit;
  margin: var(--space-3) 0 0; padding: var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--accent-soft);
}

/* ═══ «Внешний вид» (№771): конструктор темы ════════════════════════════
   Перенос `design/theme-lab.html`. Две части и они не смешиваются:

   * ЭКРАН (`.ap-*`) — обычная админка, свои токены темы админки
     (`--ink`, `--surface`, `--line`): это инструмент, он выглядит как
     всё остальное и от выбираемой темы не зависит;
   * ПРЕДПРОСМОТР (`.pv-*`) — только токены `--p-*`, которые ставит
     скрипт из ответа сервера. Ни одного `var(--ink)` внутри `.pv-*`:
     иначе предпросмотр показывал бы нынешнюю тему вперемешку с
     выбираемой и врал бы ровно там, ради чего заведён.

   Телефон первым; две колонки — от 1000px, как борт. */

.ap { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
.ap-step { margin-bottom: var(--space-4); }
.ap-step h2 {
  font-size: var(--text-meta); font-weight: 500;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink2); margin: 0 0 var(--space-2);
}
.ap-step .field-hint { margin-top: var(--space-2); }

/* Ряды выбора: образцы цвета сеткой, названия — в две колонки, три
   варианта — в три. */
.ap-sw { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
.ap-names { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
.ap-one { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }

.ap-sw button {
  appearance: none; border: 1px solid var(--line); background: none;
  padding: 0; cursor: pointer; border-radius: var(--radius-md);
  overflow: hidden; height: 34px; position: relative;
}
.ap-sw button span { position: absolute; inset: 0; }
.ap-names button, .ap-one button {
  appearance: none; border: 1px solid var(--line); background: var(--raised);
  color: var(--ink); min-height: 34px; padding: 0 10px;
  border-radius: var(--radius-md); cursor: pointer; font: inherit;
  font-size: var(--text-meta); text-align: left;
  display: flex; align-items: center; gap: 8px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ap-names button i, .ap-one button i {
  width: 12px; height: 12px; border-radius: var(--radius-sm); flex: none;
  border: 1px solid rgba(var(--ink-rgb), .12);
}
/* Выбранное — той же меткой `is-on`, что чипы админки (№769, заход 3). */
.ap-sw button.is-on { outline: 2px solid var(--ink); outline-offset: 2px; }
.ap-names button.is-on, .ap-one button.is-on {
  border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink);
}
/* Недоступный акцент — не прячется, а гаснет: место в ряду остаётся,
   и видно, что вариант есть, но на этом фоне не читается. */
.ap-sw button[disabled] { opacity: .25; cursor: not-allowed; }

.ap-logo { display: flex; align-items: center; gap: var(--space-3); }
.ap-logo-box {
  width: 56px; height: 56px; flex: none; display: flex;
  align-items: center; justify-content: center; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--raised);
}
.ap-logo-box img { width: 100%; height: 100%; object-fit: contain; }
.ap-logo-box i {
  font-style: normal; font-size: var(--text-micro); color: var(--ink2);
  text-align: center; padding: 0 4px;
}
.ap-logo-acts { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.ap-logo-acts label { cursor: pointer; }

/* Полоса сохранения — липкая: выбор делают, глядя на предпросмотр. */
.ap-bar {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-2) 0; margin-bottom: var(--space-2);
  background: var(--page);
}
.ap-state { margin-right: auto; }
.ap-top { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-3); }
.ap-seg button { font: inherit; }

.ap-stage { min-width: 0; }
.ap-frame {
  margin: 0 auto; border: 1px solid var(--line);
  border-radius: var(--radius-lg); overflow: hidden;
  background: var(--p-page); max-width: 100%;
  transition: max-width var(--ease);
}
.ap-frame.is-phone { max-width: 390px; }

/* ⚠️ У КАЖДОЙ КОЛОНКИ СВОЯ ПРОКРУТКА, и это не украшение (п.8 пятого
   круга, 26.09.2026). Было: `position: sticky` на колонке выборов,
   которая ВЫШЕ экрана — 1359 px против 900 (замер 26.09). Прилипнув,
   такая колонка перестаёт ехать вместе со страницей, и её низ — шрифт,
   углы, логотип — недостижим вовсе: докрутить до них можно только
   прокрутив галерею справа, то есть случайно. Sticky законен лишь
   у колонки НИЖЕ экрана.
   Выбор из двух, названных владельцем, — второй: предпросмотр обязан
   оставаться на виду, пока человек перебирает шаги, ради этого экран
   и сделан. Сделано ТЕМ ЖЕ приёмом, что «Диалоги» (№213): страница —
   экран-приложение на 100dvh, сетка занимает остаток под заголовком,
   каждая колонка прокручивается внутри себя. Полоса сохранения остаётся
   липкой внутри правой.
   ⚠️ Промежуточная редакция (sticky + `max-height` у колонок) отвергнута
   замером: страница при этом сохраняет свою прокрутку на высоту
   заголовка, и низ колонки оказывается за нижним краем окна, пока
   человек не прокрутит ещё и саму страницу — то есть прокруток две,
   и первая незаметна (последний шаг: низ 972 при окне 900).
   ⚠️ Высота живёт на `.wrap-appearance`, а не на общем `.wrap`: общий
   сделал бы «Записи» не длиннее окна и сломал бы шов резерва полосы
   прокрутки — тот же довод, что у «Диалогов».
   ⚠️ Только от 1000px: ниже колонки идут в столбик, и страница
   прокручивается обычным потоком — проверено на 390. */
@media (min-width: 1000px) {
  .wrap-appearance {
    display: flex; flex-direction: column;
    height: 100dvh; box-sizing: border-box;
  }
  .ap {
    grid-template-columns: 300px minmax(0, 1fr); align-items: stretch;
    flex: 1; min-height: 0;
  }
  /* ⚠️ `overflow-y: auto` включает и горизонтальную обрезку (браузер
     не умеет «прокрутка по одной оси, видимое по другой»), поэтому
     запас под обводку нужен СЛЕВА И СПРАВА, а не только справа
     под полосу прокрутки. */
  .ap-side, .ap-main {
    min-height: 0; overflow-y: auto;
    padding-inline: var(--ring-reach);
    margin-inline: calc(var(--ring-reach) * -1);
  }
  .wrap-appearance .build-mark { flex: none; }
}

/* ─────────────── галерея обложек страницы записи (№780) ────────────────
   Сетка превью под предпросмотром. Плитка — пропорция 8:3, та же, что
   у самой обложки: превью, которое врёт о кадре, хуже отсутствия превью.
   Выбранная — ПОДЛОЖКОЙ `accent-soft` и рамкой акцентом (книга,
   принцип 2: «выбранный фильтр — подложка, никогда заливка»). */
.ap-covers { margin-top: var(--space-6); }
.cov-group { margin-top: var(--space-4); }
/* Ширина плитки: пять в ряд на 1280 (п.6 пятого круга №783, 26.09.2026).
   Колонка галереи там 661 px, зазор 8 — значит порог не больше
   (661 + 8) / 5 − 8 = 125,8 px; берём 120, чтобы пятый столбец не
   срывался от округления полосы прокрутки. Было 150 — четыре в ряд
   и плитка крупнее нужного: галерея из шестидесяти обложек листалась
   вдвое дольше, чем разглядывалась. На 390 в ряду по-прежнему две. */
.cov-grid {
  display: grid; gap: var(--space-2);
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  margin-bottom: var(--space-2);
}
.cov-tile {
  appearance: none; border: 1px solid var(--line); background: var(--raised);
  border-radius: var(--radius-md); padding: 0; cursor: pointer;
  color: var(--ink); font: inherit; text-align: left;
  display: flex; flex-direction: column; overflow: hidden;
  touch-action: manipulation;
}
.cov-tile:hover { background: var(--sunken); }
.cov-tile.is-on { border-color: var(--accent); background: var(--accent-soft); }
/* Плитка ПОРТРЕТНАЯ — той же пропорции, что слот обложки на странице
   записи (`.book-entry-photo`, высокий на компьютере). Превью, которое
   врёт о кадре, хуже отсутствия превью: на телефоне тот же слот
   становится полосой, и середина кадра обязана держать оба вида. */
.cov-shot {
  display: block; aspect-ratio: 2 / 3; background: var(--sunken);
  position: relative;
}
.cov-shot img, .cov-shot svg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.cov-name {
  display: block; padding: 6px var(--space-2);
  font-size: var(--text-meta); color: var(--ink2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cov-tile.is-on .cov-name { color: var(--ink); }
.cov-own-line { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
/* Тихая кнопка-ссылка: «загрузить своё» — редкий случай рядом с готовым
   каталогом, и обычная кнопка спорила бы с выбором из галереи. */
.linkish {
  color: var(--accent); cursor: pointer;
  border-bottom: 1px solid var(--line-strong);
}
.linkish:hover { color: var(--ink); }

/* ─── предпросмотр: ТОЛЬКО токены `--p-*` ──────────────────────────── */
.pv-app, .pv-book {
  --pv-sm: calc(var(--p-radius) * .75);
  --pv-md: var(--p-radius);
  --pv-lg: calc(var(--p-radius) * 1.5);
  font-family: var(--p-font), system-ui, sans-serif;
  color: var(--p-ink); background: var(--p-page);
  font-size: 15px; line-height: 22px;
}
.pv-app { display: grid; grid-template-columns: 232px minmax(0, 1fr); min-height: 560px; }
.is-phone .pv-app { grid-template-columns: minmax(0, 1fr); min-height: 640px; }

.pv-nav {
  background: var(--p-sidebar); color: var(--p-sidebar-muted);
  padding: 18px 12px; display: flex; flex-direction: column; gap: 2px;
}
.is-phone .pv-nav { display: none; }
.pv-logo {
  display: flex; align-items: center; gap: 10px;
  color: var(--p-sidebar-ink); font-weight: 500; padding: 4px 8px 16px;
}
.pv-logo i, .pv-bk-logo {
  width: 28px; height: 28px; border-radius: var(--pv-sm);
  background: var(--p-accent); display: inline-block; flex: none;
  background-size: cover; background-position: center;
}
.pv-g {
  font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
  padding: 0 8px; margin: 8px 0 4px; color: var(--p-sidebar-muted); opacity: .8;
}
.pv-nav a {
  display: flex; justify-content: space-between; padding: 7px 8px;
  border-radius: var(--pv-md); color: inherit; text-decoration: none;
}
.pv-nav a.on { background: var(--p-sidebar-active); color: var(--p-sidebar-ink); }
.pv-nav a b { font-weight: 500; color: var(--p-sidebar-ink); font-size: 13px; }
.pv-me {
  margin-top: auto; padding: 8px; font-size: 13px;
  border-top: 1px solid var(--p-sidebar-active);
}

.pv-main { padding: 24px 28px; min-width: 0; }
.is-phone .pv-main { padding: 16px; }
.pv-ph { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
.is-phone .pv-ph { flex-direction: column; align-items: stretch; gap: 10px; }
.pv-t { font-size: 28px; line-height: 34px; font-weight: 500; letter-spacing: -.01em; margin: 0; }
.pv-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; height: 36px; padding: 0 14px; border-radius: var(--pv-md);
  font-size: 14px; font-weight: 500; border: 1px solid transparent;
}
.pv-btn-p { background: var(--p-accent); color: var(--p-on-accent); }
.is-phone .pv-btn-p { width: 100%; }

/* Ряд управления «Внешнего вида» — не `.ctl`, но той же породы:
   прокручивается и держит фокусируемые органы (поиск, кнопки). Запас
   под обводку тот же; нижний отступ вычитает его, чтобы шаг не вырос. */
.pv-ctl {
  display: flex; align-items: center; gap: 10px;
  padding: var(--ring-reach); margin: calc(-1 * var(--ring-reach));
  margin-bottom: calc(16px - var(--ring-reach));
  overflow-x: auto; scrollbar-width: none;
}
.pv-ctl::-webkit-scrollbar { display: none; }
.pv-search {
  display: flex; align-items: center; height: 36px; padding: 0 12px;
  width: 230px; flex: none; border: 1px solid var(--p-line-strong);
  border-radius: var(--pv-md); background: var(--p-raised);
  color: var(--p-ink3); font-size: 14px;
}
.is-phone .pv-search { width: 100%; min-width: 160px; }
.pv-seg { display: inline-flex; background: var(--p-sunken); border-radius: var(--pv-md); padding: 3px; gap: 2px; flex: none; }
.pv-seg span { padding: 4px 12px; border-radius: var(--pv-sm); font-size: 14px; font-weight: 500; color: var(--p-ink2); white-space: nowrap; }
.pv-seg span.on { background: var(--p-raised); color: var(--p-ink); }
.pv-seg span i { font-style: normal; font-weight: 400; color: var(--p-ink3); margin-left: 5px; }

.pv-mets { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
           gap: var(--space-3); margin-bottom: var(--space-6); }
.is-phone .pv-mets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pv-met { background: var(--p-raised); border: 1px solid var(--p-line); border-radius: var(--pv-lg); padding: 12px 14px; min-width: 0; }
.pv-met small { display: block; font-size: 13px; color: var(--p-ink2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pv-met b { display: block; font-size: 22px; line-height: 28px; font-weight: 500; font-variant-numeric: tabular-nums; margin-top: 2px; }
.pv-caps { font-size: 12px; line-height: 16px; font-weight: 500; letter-spacing: .06em; text-transform: uppercase; color: var(--p-ink2); margin: 0 0 6px; }
.pv-list { background: var(--p-raised); border: 1px solid var(--p-line); border-radius: var(--pv-lg); overflow: hidden; margin-bottom: 20px; }
.pv-row { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-top: 1px solid var(--p-line); }
.pv-row:first-child { border-top: 0; }
.pv-av {
  width: 32px; height: 32px; border-radius: var(--radius-full); background: var(--p-sunken);
  color: var(--p-ink2); display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 500; flex: none;
}
.pv-tx { flex: 1; min-width: 0; }
.pv-tx em { font-style: normal; display: block; font-size: 13px; color: var(--p-ink2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pv-chip { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: var(--pv-sm); font-size: 13px; font-weight: 500; white-space: nowrap; }
.pv-chip.ok { background: var(--p-ok-bg); color: var(--p-ok-ink); }
.pv-chip.wait { background: var(--p-wait-bg); color: var(--p-wait-ink); }
.pv-chip.alert { background: var(--p-alert-bg); color: var(--p-alert-ink); }
.pv-chip.done { background: var(--p-done-bg); color: var(--p-done-ink); }
.is-phone .pv-chip { display: none; }
.pv-tm { font-size: 13px; color: var(--p-ink3); font-variant-numeric: tabular-nums; flex: none; }

/* ⚠️ ПРЕДПРОСМОТР ПОКАЗЫВАЕТ ПАРУ (№788, 26.09.2026): заголовок
   страницы, имя студии, заголовок шага записи и большие числа —
   заголовочным семейством и ЕГО весом, ровно как на живых экранах.
   Без этого владелец выбирал бы пару по названию, а видел один
   текстовый шрифт.
   ⚠️ Правило стоит ПОСЛЕ остальных правил превью намеренно: у `.pv-t`,
   `.pv-met b` и `.pv-logo` свой `font-weight: 500` объявлен выше,
   и при обратном порядке вес пары затёрся бы (у «Славянской вязи»
   это означало бы поддельный полужирный). */
.pv-t, .pv-logo, .pv-met b, .pv-bk-h, .pv-bk-name {
  font-family: var(--p-font-head), system-ui, sans-serif;
  font-weight: var(--p-font-head-weight);
}

.pv-tabbar { display: none; }
.is-phone .pv-tabbar {
  display: grid; grid-template-columns: repeat(5, 1fr);
  border-top: 1px solid var(--p-line); background: var(--p-raised);
  padding: 8px 0 10px; font-size: 11px; text-align: center; color: var(--p-ink2);
}
.is-phone .pv-tabbar span.on { color: var(--p-accent); }
.pv-tabbar i { display: block; width: 20px; height: 20px; border-radius: var(--radius-sm); border: 1.5px solid currentColor; margin: 0 auto 3px; }

/* ─── предпросмотр страницы записи ─────────────────────────────────── */
.pv-book { min-height: 560px; }
.is-phone .pv-book { min-height: 640px; }
/* Обложка в предпросмотре `/book` — та же пропорция, что на странице. */
.pv-bk-cover {
  position: relative; aspect-ratio: 16 / 9;
  border-radius: var(--p-radius, 8px); overflow: hidden;
  margin-bottom: 10px; background: var(--p-sunken);
}
.pv-bk-cover img, .pv-bk-cover svg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.pv-bk-top {
  display: flex; align-items: center; gap: 12px;
  padding: 16px 20px; border-bottom: 1px solid var(--p-line);
  background: var(--p-raised);
}
.pv-bk-name { font-weight: 500; }
.pv-bk-name em { font-style: normal; display: block; font-size: 13px; color: var(--p-ink2); }
.pv-bk-body { padding: 20px; max-width: 640px; margin: 0 auto; }
.pv-bk-step { font-size: 13px; color: var(--p-ink3); margin-bottom: 4px; }
.pv-bk-h { font-size: 17px; font-weight: 500; margin: 18px 0 8px; }
.pv-bk-h:first-of-type { margin-top: 8px; }
.pv-bk-cards { display: grid; gap: 8px; }
.pv-bk-card {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 16px; border: 1px solid var(--p-line);
  border-radius: var(--pv-lg); background: var(--p-raised);
}
.pv-bk-card span { font-size: 13px; color: var(--p-ink2); }
.pv-bk-card.is-on { border-color: var(--p-accent); box-shadow: inset 0 0 0 1px var(--p-accent); }
.pv-bk-masters { display: flex; gap: 10px; flex-wrap: wrap; }
.pv-bk-master { display: flex; align-items: center; gap: 8px; padding: 8px 12px 8px 8px; border: 1px solid var(--p-line); border-radius: var(--radius-full); background: var(--p-raised); font-size: 14px; }
.pv-bk-master i {
  font-style: normal; width: 28px; height: 28px; border-radius: var(--radius-full);
  background: var(--p-sunken); color: var(--p-ink2);
  display: flex; align-items: center; justify-content: center; font-size: 13px;
}
.pv-bk-master.is-on { border-color: var(--p-accent); box-shadow: inset 0 0 0 1px var(--p-accent); }
.pv-bk-times { display: flex; flex-wrap: wrap; gap: 8px; }
.pv-bk-times span {
  padding: 8px 14px; border: 1px solid var(--p-line-strong);
  border-radius: var(--pv-md); background: var(--p-raised); font-size: 14px;
}
.pv-bk-times span.is-on { background: var(--p-accent); color: var(--p-on-accent); border-color: var(--p-accent); }
.pv-bk-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; margin-top: 22px; padding-top: 16px;
  border-top: 1px solid var(--p-line);
}
.pv-bk-total { font-size: 13px; color: var(--p-ink2); }
.pv-bk-total b { display: block; font-size: 22px; line-height: 28px; font-weight: 500; color: var(--p-ink); }
.is-phone .pv-bk-go { width: 100%; }

/* Четыре тарифа в один ряд — только когда каждой карточке хватает
   ширины (правка 28.09.2026, см. пояснение у `.plans-four` выше). */
@media (min-width: 1200px) {
  .plans-four { grid-template-columns: repeat(4, 1fr); }
}
