/* ============================================================
   Админка студии. 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);
  /* Затемнение под окнами и боковой панелью (книга: `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;
  /* Верх окна записи на широком экране — одна константа: первый ряд
     кнопок стоит в одной точке у любой записи. */
  --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 ради явности — конфликта нет,
   свойство аддитивное. */
html { scroll-behavior: smooth; scrollbar-gutter: stable; 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;
}
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 {
    font-family: var(--font);
    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); }
  /* «Имя · роль» — две строки одной ссылки: имя читается, роль отвечает
     на «под кем я сижу». Ниже — «Выйти», и больше внизу ничего (книга). */
  .side-foot a { justify-content: flex-start; }
  .side-me { display: flex; flex-direction: column; align-items: flex-start;
             gap: 0; min-height: 40px; }
  .side-me .label { font-weight: 500; color: var(--sidebar-ink); }
  .side-role { font-size: var(--text-meta); color: var(--sidebar-muted); }

  /* №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; }

.ctl {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: var(--control-h);
  margin-bottom: var(--space-3);
  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; }
.ctl + .caps, .ctl + .list, .ctl + .mets, .ctl + .section,
.ph + .caps, .ph + .list, .ph + .mets { margin-top: var(--space-4); }

/* Заголовок страницы: одна строка, многоточие, кегль `title`. */
.page-title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-title); line-height: var(--line-title); font-weight: 500;
  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; }
}

/* Разделитель группы — единственное место прописных в системе. */
.caps {
  margin: 0 0 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);
}
.caps + .list { margin-top: 0; }
/* Заголовок раздела внутри страницы — `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) ════════════════════════════
   Подпись `meta` сверху, число `numeral` снизу; не больше четырёх
   в ряду, на телефоне 2×2. Единица — один раз в заголовке ряда,
   а не в каждой карточке. */
.mets {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2); margin: 0 0 var(--space-6);
}
@media (min-width: 1000px) { .mets { grid-template-columns: repeat(4, 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;
  font-size: var(--text-num); line-height: var(--line-num); font-weight: 500;
  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; }
/* ВОЗВРАТ НА РОДИТЕЛЬСКИЙ ЭКРАН — ОДИН КЛАСС `.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;
  background: 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);
}
/* Наведение и нажатие: тот же акцент, подмешанный к чернилам. Движения
   нет (решение владельца 05.09) — меняется только тон. */
.btn:hover  { background: color-mix(in srgb, var(--accent) 88%, var(--ink)); }
.btn:active { background: color-mix(in srgb, var(--accent) 80%, var(--ink));
              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`. Теперь у него есть роль,
   названная книгой, и заливки нет вовсе. */
.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; }

/* Опасное действие — контурная кнопка с текстом тревоги (книга):
   подтверждение всё равно спрашивается отдельно, и красная заливка
   спорила бы с главным действием страницы. */
.btn-danger {
  background: var(--raised); color: var(--alert-ink); border-color: var(--line-strong);
}
.btn-danger:hover { background: var(--press); }

/* Кнопка из одной иконки — только поиск, закрыть, «ещё» и стрелки
   листания периода (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); }

/* Несохранённое — у СВОЕЙ кнопки (№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: 0 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. */

/* Полоса управления: поиск отдельной строкой выше, здесь — период,
   «Показать» и служебные. */
.ev-search { margin-bottom: var(--space-2); }
.ev-search input[type="search"] { width: 100%; }
.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); }
/* Служебные — тот же чип, что снятые фильтры: это переключатель, а не
   действие, и кнопкой он читался бы как «сделать что-то». */
.ev-service { margin-left: auto; }
.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); }
.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; }
  /* ⚠️ Прежняя сетка 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); }
/* Архив задач: форма фильтра в строку, пагинация по краям. */
.archive-panel { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3); }
.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;
}
.rows > .line-row:first-child { border-top: 0; }

.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;
  transition: background 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;
}

.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;
  transition: background var(--ease), box-shadow var(--ease);
}
/* Активный стиль — одна точка, ниже по файлу («кнопки: мягкий подъём»),
   рядом со своим фоновым токеном (`--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;
}
.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 {
  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;
}
.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: rgba(var(--ink-rgb), .13);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.sub-progress i {
  display: block; height: 100%;
  width: var(--fill, 100%);
  border-radius: var(--radius-sm);
  /* Полоса остатка — чернилами, не акцентом: акцент идёт на одну
     заливную кнопку, фокус и активный пункт борта, а полосы
     прогресса им не красятся (книга, «Цвета»). */
  background: var(--ink);
  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 и в глаза не бросалось,
     поэтому баг выглядел «зависящим от роли». Он и зависел — но от ЧИСЛА
     видимых разделов, а не от прав: права на нижние вкладки у обеих ролей
     полные. */
  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); }
/* Низ листа — тот же, что низ борта: «Имя · роль» и «Выйти», больше
   ничего (книга, блок Sidebar). На телефоне этот лист и есть борт. */
.more-foot {
  margin-top: var(--space-2); padding-top: var(--space-2);
  border-top: 1px solid rgba(var(--sidebar-ink-rgb), .14);
}
.more-foot .side-role { margin-left: auto; }
/* «Возможности и цены» — под черту, мелким (зеркало `.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);
}
.dlg-item.active { background: var(--accent-soft); }
.dlg-body { flex: 1; min-width: 0; }
.dlg-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.dlg-name { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; }
.dlg-time { font-size: var(--text-meta); color: var(--ink2); flex-shrink: 0;
            font-variant-numeric: tabular-nums; }
.dlg-last {
  display: block; margin-top: 2px;
  font-size: var(--text-control); color: var(--ink2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Пометка «бот:» в превью диалога (№726). Заметнее «Вы:», потому что
   говорит о другом: не «кто-то из нас», а «человек к этому не притрагивался».
   Цветом акцента, без фона и рамки — строка превью однострочная и ужимается
   многоточием, любая плашка съела бы её ширину. */
.dlg-bot { color: var(--accent); font-weight: 500; }

.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 { margin: var(--space-6) 0 var(--space-4); }

.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;
    grid-template-columns: minmax(280px, 360px) 1fr;
    gap: var(--space-4);
    align-items: stretch;
    flex: 1; min-height: 0;
  }
  .dlg-list { min-height: 0; overflow-y: auto; padding-right: 4px; }
  .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 { max-height: none; }
  .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); }

/* ------------------------------------------------ статусы диалога */
/* Чипов здесь два (№453), и с 05.09 они стоят В ОДНУ строку двумя
   половинами (решение владельца): подписи укоротились, и переносить
   больше нечего. `flex: 1 1 0` — равная ширина независимо от длины
   подписи и от числа на счётчике: ряд не должен перестраиваться, когда
   счётчик становится двузначным. `min-width: 0` обязателен рядом
   с `flex: 1`, иначе длинная подпись расталкивает половины. */
.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); }
.pair-no {
  display: inline-block; min-width: 22px;
  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; }
/* Два бейджа в углу карточки: «служебный» и статус. На узком экране они
   переносятся вниз строкой, а не выдавливают имя человека в столбик. */
.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-grid { display: grid; gap: var(--space-3); }
.feat { display: flex; flex-direction: column; gap: 8px; }
.feat.stage2 { opacity: .93; }
.feat-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.feat-head h2 { font-size: var(--text-body); text-transform: none; letter-spacing: .01em; }
.feat-price {
  font-family: var(--font);
  font-weight: 500; font-size: var(--text-num);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.feat-note { margin: 0; font-size: var(--text-control); }
.feat .badge { align-self: flex-start; }

.total-line {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-6);
}
.total-label {
  font-family: var(--font); font-weight: 500;
  text-transform: uppercase; letter-spacing: .06em; font-size: var(--text-meta);
}
.total-sum {
  font-family: var(--font); font-weight: 500; font-size: var(--text-title);
  color: var(--ink2);
  text-decoration: line-through;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
}

.plans { display: grid; gap: var(--space-3); }
.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-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); }
.bar-name { font-weight: 500; font-size: var(--text-control); }
.bar-num { font-variant-numeric: tabular-nums; font-size: var(--text-control); }
.bar {
  height: 10px; margin-top: 6px;
  background: rgba(var(--ink-rgb), .12);
  border-radius: var(--radius-sm); overflow: hidden;   /* острое, исключение: полоса */
}
.bar i {
  display: block; height: 100%; width: var(--fill, 0%);
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--accent), var(--ink));
  transition: width 800ms cubic-bezier(.2, .7, .3, 1);
}

/* Ряд метрик (книга, блок MetricCard): подпись `meta` СВЕРХУ, число
   `numeral` снизу, не больше четырёх в ряду, на телефоне 2×2. Классы
   прежние (`.stat-grid`, `.card.stat`, `.stat-num`, `.stat-label`) —
   это адреса разметки и швов; поменялись вид и порядок чтения.
   Порядок задаёт `column-reverse`, а не перестановка в двадцати
   шаблонах: разметка у метрики одна, и она уже правильная по смыслу
   («вот число, вот что это»), просто читается снизу вверх. */
.stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
             gap: var(--space-2); margin-bottom: var(--space-6); }
.stat { display: flex; flex-direction: column-reverse; justify-content: flex-end;
        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 без ужимания. */
.stat-week {
  font-family: var(--font); font-style: normal; font-weight: 500;
  font-size: var(--text-body); color: var(--ink2); margin-top: 4px;
}

.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). */
/* ⚠️ Выравнивание по ВЕРХУ, а не по центру: при раскрытой расшифровке
   строка становится в двадцать раз выше, и центрированная сумма уезжала
   на её середину — далеко от имени, к которому относится. */
.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; }
.payout-line {
  display: flex; align-items: baseline; gap: 0 var(--space-3);
  font-size: var(--text-meta); color: var(--ink2);
  padding: 3px 0; 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 {
  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) {
  /* Вверх, а не вниз: снизу стоит фиксированный таббар, и раскрытое вниз
     меню пряталось бы под ним вместе с последним пунктом. */
  .row-menu-list { top: auto; bottom: calc(100% + 4px); }
}

@media (min-width: 900px) {
  .feat-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
  /* три тарифа подписки в ряд; разовая живёт отдельной карточкой ниже */
  .plans { grid-template-columns: repeat(3, 1fr); }
  .stat-grid { grid-template-columns: repeat(4, 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 */
.feat-benefit {
  font-family: var(--font);
  font-style: normal; font-weight: 500;
  font-size: var(--text-section);
  line-height: 1.4;
  /* латте здесь дал бы контраст 1.93 — берём приглушённый шоколад:
     тише основного текста, но читается (6.7) */
  color: var(--ink2);
  margin: 0;
}

.feat-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-2);
}

.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; }

/* Исключённая карточка гаснет, но остаётся читаемой */
.feat.is-off { opacity: .45; }
.feat.is-off .feat-price { text-decoration: line-through; }

.total-hint {
  margin: -12px 0 var(--space-6);
  font-size: var(--text-meta); color: var(--ink2); text-align: center;
}
.total-line { transition: border-color var(--ease); }
.total-sum #total-sum { font-variant-numeric: tabular-nums; }

/* «в подписке просто всё» — подпись, ради которой затевался контраст */
.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: 0 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`,
   не растягивается и не переносится (книга, «Текст в тесноте»). */
.sort-select { width: auto; min-width: 190px; min-height: var(--control-h);
               font-size: var(--text-control); }

/* Полоса периода отчётных экранов — «Платежи», «Зарплаты», «Статистика»
   (№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 {
  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); }
.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); }

  /* На телефоне ряд управления скроллится вбок целиком (`.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) {
  /* Левая чуть уже правой: слева формы, справа списки — им нужнее ширина.
     align-items: start, иначе короткая колонка растянется под длинную. */
  .cols {
    display: grid;
    grid-template-columns: 48fr 52fr;
    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); }

/* Три полосы одна под другой: видно, как воронка сужается */
.fn-bars { display: flex; flex-direction: column; gap: 3px; margin-top: 7px; }
.fn-bars i {
  display: block; height: 6px; border-radius: var(--radius-sm);
  transition: width 700ms cubic-bezier(.2,.7,.3,1);
}
.fn-came    { width: 100%; background: rgba(var(--ink-rgb), .14); }
.fn-booked  { width: var(--fill, 0%); background: var(--accent); }
.fn-reached { width: var(--fill, 0%); background: var(--ink); }

.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-bars i { 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);
}

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

/* Сообщество — спокойная поверхность без акцентной рамки:
   акцент на странице один, у тарифа «Стандарт». */
.community { margin-top: var(--space-4); }
.community h2 { font-size: var(--text-section); margin-bottom: var(--space-3); }
.community-line { margin: 0 0 var(--space-2); font-size: var(--text-control); line-height: 1.6; max-width: 78ch; }
.community-line:last-of-type { margin-bottom: var(--space-4); }
.community-btn { display: inline-flex; text-decoration: none; }

/* Код проданного абонемента: его диктуют вслух с экрана,
   поэтому крупно, моноширинно и с разрядкой. */
.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); }
  .community-btn { width: 100%; }
}

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

/* --- навигация по периоду над сеткой --- */
.sched-nav {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-4);
  margin-bottom: var(--space-3);
}
/* Узкий экран (решение владельца 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): один орган, один вид. Своих правил
   у графика больше нет. */
.sched-period {
  font-weight: 500; white-space: nowrap; flex: none;
  font-size: var(--text-control);
  min-width: 11em; text-align: center;
}
.sched-period { font-variant-numeric: tabular-nums; }
.sched-month { color: var(--ink2); min-width: 8.5em; text-align: center; }
/* «Сегодня» — чип (05.09): на сегодняшнем дне АКТИВЕН (тёмный, как
   активный фильтр), а не погашен — прежняя полупрозрачность снята,
   состояние «вы уже здесь» показывается тем же языком, что у всех
   чипов админки. Нажатие на активном безвредно — тот же день. */

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

/* --- сетка мастера × дни --- */
.sched-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  /* Системную полосу прячем: под сеткой лежит своя, всегда видимая
     и в палитре вертикали. Две полосы разом читались бы как сбой. */
  scrollbar-width: none;
}
.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 на человека) снят:
   обзора «кто когда» он не давал вовсе. На 380 ячейка ~30px — только
   полосы, время открывает тап (панель правки). */
.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 { flex: 1 1 auto; min-width: 0; }
.svc-line-name, .svc-line-prices {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.svc-line-prices { font-size: var(--text-meta); }
.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;
}
.nb-time.is-on { background: var(--accent-soft); border-color: 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); }

/* Кабинет в карточке записи: назначен автоматически, меняется одной кнопкой */
.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; }
.sched-day {
  padding: var(--space-2); text-align: center;
  font-size: var(--text-meta); color: var(--ink2); font-weight: 500;
  border-bottom: 1px solid var(--line);
}
.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; }   /* острое, исключение: сетка */
.sched-wd { display: block; text-transform: uppercase; letter-spacing: .06em; }
.sched-num { display: block; font-size: var(--text-body); color: var(--ink); }

/* Липкая колонка с именами. Собственный фон обязателен: под ней проезжают
   ячейки, и прозрачная колонка показывала бы их сквозь имя. Левый отступ
   не ноль — прижатый к самому краю контейнера текст режется его границей
   (ровно это давало «АЛОМЕ» вместо «САЛОМЕ» при прокрутке).
   Правая граница отделяет колонку от уезжающей сетки. */
.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;
}

/* Телефон: ячейка ~30px — только полосы, цифры снимаются; узкая колонка
   имён, время по тапу (панель правки, там же кнопка «Изменить»). */
@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; }
  .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). Неактивная кнопка — видна,
   но не действует: смена видит, что позвонить нечем (решение владельца). */
.cc-actions .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.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; }

/* Поиск по списку: поле с иконкой, во всю ширину строки заголовка */
.search {
  display: flex; align-items: center; gap: var(--space-2);
  flex: 1 1 260px; min-width: 200px;
  padding: 0 var(--space-3); min-height: var(--tap);
  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); }
.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); }
/* Подпись под списком: сколько нашлось и сколько скрыто. Своя строка,
   а не хвост последнего пункта — иначе её принимают за ещё одного человека. */
.picker-note {
  margin: var(--space-1) 0 0; font-size: var(--text-meta); color: var(--ink2);
  font-variant-numeric: tabular-nums;
}
.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 не сжимаются
   в тесной строке, дата между ними — постоянной ширины под самую
   длинную подпись, чтобы смена дня не двигала стрелки и соседей.
   Сама строка — слот S2/S3 (`.ctl`), поэтому переносов и раскладки
   по `order` здесь больше нет: не помещается — ряд скроллится вбок. */
.bk-period {
  font-weight: 500; width: 11em; flex: none; text-align: center;
  font-size: var(--text-body);
}
/* Две формы подписи дня: полную («Воскресенье, 21 сентября») показывает
   десктоп, короткую («Вс, 21 сентября») — телефон. */
.bk-period-short { display: none; }
.bk-chips { display: contents; }
@media (max-width: 899px) {
  .bk-period { width: 9.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; }

/* История абонемента: про людей, а не про деньги. Свёрнута по умолчанию —
   в обычный день администратору нужен остаток, а не биография кода. */
.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;
}
/* Строка значков вместо названия услуги (Т3, №202, 01.09) — своя строка,
   а не угол `.bk-head`: там значки теснились с именем клиента. Не `.bk-svc`
   сознательно: у той есть `.weekview .bk-svc { display:none }` (ниже),
   а значки нужны и в «Неделе» — были видны там и до этой правки.
   ⚠️ У короткого сеанса (40 мин) этой строки нет вовсе — не помещается
   вторая строка (та же граница, что раньше прятала `.bk-svc`), значки
   остаются в `.bk-ico`; макрос `_prop_badges` один на оба места, чтобы
   набор значков не разошёлся между «короткой» и «длинной» плашкой. */
.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); }
/* «Конец сеанса: отметьте приход» (№215) — подсказка, а не тревога: цвет
   вторичного текста, не --warn у соседней «вне графика». Соседство важнее
   заметности: закричи она наравне с «вне графика», и в конце дня половина
   сетки была бы оранжевой, а смена перестала бы читать обе. */
.bk-mark.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; }
/* Оплачено — своим цветом, и ПОСЛЕ статусов: за оплаченную запись студия
   уже рассчиталась, и это сильнее, чем «пришёл». Смена смотрит на сетку
   с полуметра, и вопрос «за кого ещё не взяли деньги» должен читаться
   цветом, а не открыванием плашек по одной. */
.bk.is-paid { background: var(--ok-bg); border-color: var(--ok-ink); }
.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. */
.sheet-fields {
  display: grid; grid-template-columns: 104px minmax(0, 1fr);
  gap: 6px 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; }
/* Квадратики — ТЕ ЖЕ токены st-*, что у плашек выше (закреплено тестом):
   похожий-но-другой цвет здесь означал бы легенду, которая врёт.
   18×18 — ступени шкалы близки по яркости, и на 12px их не разглядеть. */
.sw { width: 18px; height: 18px; border-radius: var(--radius-lg); display: inline-block; }
.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) —
   тот же приём: форма нужна изредка и не должна стоять первым экраном,
   когда смена открыла вкладку посмотреть список или историю. */
/* №476: спойлер-полоса — ОДИН класс `.fold` на всю админку. Прежние
   имена (.month-fold, .rents, .sell-fold, .rooms-fold, .acc;
   .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;
}
.fold > summary {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  /* 44px тап-зона: заголовок сворачивания нажимают пальцем, а не курсором */
  min-height: 44px; padding: 0 var(--space-3);
  cursor: pointer; list-style: none;
  /* Тот же шрифт И фон заголовка, что у «Занятость кабинетов» (`.rooms-fold
     > summary`, решение владельца 01.09) — три спойлера внизу «Записей»
     читаются одной рукой письма, а не разного цвета. */
  font-weight: 500; font-size: var(--text-control);
  background: var(--accent-soft);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::before {
  content: "›"; font-size: var(--text-section); line-height: 1;
  transition: transform var(--ease); color: var(--accent);
}
.fold[open] > summary::before {
  transform: rotate(90deg);
}
.fold > summary:hover { background: var(--accent-soft); }
/* Число справа — мелко, без слова (образец владельца 05.09): слово живёт
   в title/aria, глазу достаточно цифры. */
.fold-n {
  margin-left: auto;
  font-size: var(--text-meta); font-variant-numeric: tabular-nums;
  color: var(--ink2);
}
/* Цвет подложки срочных групп — модификатор ТОГО ЖЕ класса (№476). */
.fold-overdue > summary { background: var(--alert-bg); color: var(--alert-ink); }
.fold-overdue > summary .fold-n { color: inherit; }
.fold-today > summary { background: var(--accent-soft); }
/* Мелкий рядный спойлер ВНУТРИ карточки (история, журнал, отзывы,
   напоминание вручную): те же глиф и механика, но строкой 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::before {
  content: "›"; font-size: var(--text-body); line-height: 1;
  transition: transform var(--ease); color: var(--accent);
}
.fold-line[open] > summary::before { transform: rotate(90deg); }
.fold-line > summary:hover { color: var(--ink); }

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

.month-fold > summary:focus-visible, .rents > summary:focus-visible,
.sell-fold > 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) {
  .mf-hint, .rents-peek, .rf-peek { display: inline; margin-left: auto; }
}

/* Подсказки в заголовке — обычным регистром: это данные, а не метка */
.mf-hint, .rents-peek {
  font-family: var(--font); font-weight: 400;
  text-transform: none; letter-spacing: 0; font-size: var(--text-meta);
}
.month-fold > div, .rents > .dv-blocks, .sell-fold > div { padding: 0 var(--space-3) var(--space-3); }
.month-fold .month { margin-bottom: 0; }

/* ============================================================ КАЛЕНДАРЬ МЕСЯЦА
   Кольцо показывает долю занятого времени от того, сколько студия может
   принять. Шкала ступенчатая, а не непрерывный градиент: четыре состояния
   различимы на глаз и не требуют вглядывания в оттенок, а между ними стоят
   границы, которые администратор может назвать словами. */
.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; }
}

/* ------------------------------------------------ поиск в списках */
.dlg-search {
  position: relative; display: block; margin-bottom: var(--space-3);
}
.dlg-search svg {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--ink2); pointer-events: none;
}
.dlg-search input {
  width: 100%; min-height: var(--tap);
  padding-left: 34px;
  /* 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;
}
/* Слово-подпись внутри ячейки: на широком его роль играет ЗАГОЛОВОК
   колонки, и дублировать его в каждой строке значит удваивать текст;
   на узком заголовков нет вовсе, и без слова остаются голые числа. */
.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; }
.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`; содержимое под заголовком получает
   те же поля по бокам, что и заголовок, иначе поля ввода легли бы
   вплотную к рамке. */
.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-sub { margin: 0 0 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; }

@media (min-width: 1000px) {
  .ap { grid-template-columns: 300px minmax(0, 1fr); align-items: start; }
  .ap-side { position: sticky; top: var(--space-3); }
}

/* ─── предпросмотр: ТОЛЬКО токены `--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%; }

.pv-ctl { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; 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: 10px; margin-bottom: 22px; }
.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; }

.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; }
.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%; }
