/* Lidz Dashboard — Apple Liquid Glass (iOS 26 / macOS Tahoe visual language).
   Две темы: светлая (Apple light) и тёмная (Apple dark) — тёмная НЕ инверсия
   светлой, у неё свои материалы, свои шаги палитры и свои токены текста.

   Правила проекта, которые здесь закодированы:
   - яркие токены (--color-lead, --color-success, --color-warning) — только
     графики/заливки/крупные KPI; мелкий текст — *-text варианты (≥4.5:1);
   - --color-dim — декор, не текст;
   - backdrop-filter только на слоях, которые реально «стекло»: сайдбар,
     топбар и карточки. На таблицах/строках блюра нет — он дорогой.

   Категориальная палитра графиков живёт в dashboard.js (COLORS.palette*) и
   провалидирована scripts/validate_palette.js (dataviz) ОТДЕЛЬНО для светлой
   и тёмной подложки: 6/6 проверок в обеих темах. */

/* ============================================================
   1. Токены — светлая тема (значения по умолчанию)
   ============================================================ */

/* Типизированное свойство моторики. Без @property браузер считает --press
   произвольными строками: подставить их в transform можно, а вот ПЕРЕХОД по ним
   не интерполируется, и обновление матрицы происходит непредсказуемо (проверено
   в Chrome: с нетипизированной переменной computed transform оставался
   единичным). Объявление типа делает их полноценными анимируемыми величинами.
   --press — сжатие кнопки при нажатии (ставит CSS в :active), и с 07.08 это
   ЕДИНСТВЕННАЯ моторика иконочной кнопки: магнитный сдвиг к курсору (жил в
   отдельном свойстве translate) снят по прямой просьбе владельца. */
@property --press { syntax: "<number>"; inherits: false; initial-value: 1; }

:root {
  /* --- Поверхности --- */
  --color-bg:             #F1F1F6;   /* systemGroupedBackground */
  /* ЦВЕТ ЗА СКРУГЛЁННЫМ УГЛОМ ОКНА (см. `.app-corners`). Это «стол», на
     котором лежит дашборд: полутон ТЕМНЕЕ страницы, иначе срез угла не виден
     вовсе и правка не состоится. Откат скругления — одна строка в
     `.app-corners`, а не поиск цвета по файлу. */
  --frame-canvas:         #DCDCE4;
  --color-surface:        rgba(255, 255, 255, 0.60);
  --color-surface-strong: rgba(255, 255, 255, 0.78);
  --color-surface-solid:  #FFFFFF;
  --color-rule:           rgba(60, 60, 67, 0.13);   /* separator */
  --color-rule-strong:    rgba(60, 60, 67, 0.20);   /* opaqueSeparator */

  /* Fill-уровни Apple (подложки под контролами, не текст) */
  --fill-1: rgba(120, 120, 128, 0.10);
  --fill-2: rgba(120, 120, 128, 0.15);
  --fill-3: rgba(120, 120, 128, 0.06);
  /* Нейтральная заливка бара «запас без подписки»: обязана читаться ОТДЕЛЬНО
     от своего трека, поэтому это не --color-dim (тот сливался с треком). */
  --bar-idle: #7B7B88;

  /* --- Стекло --- */
  --glass-bg:          rgba(255, 255, 255, 0.52);
  --glass-bg-strong:   rgba(255, 255, 255, 0.68);
  --glass-bg-chrome:   rgba(248, 248, 252, 0.72);  /* сайдбар/топбар */
  --glass-border:      rgba(60, 60, 67, 0.12);     /* хром: сайдбар/топбар */
  --glass-border-hi:   rgba(60, 60, 67, 0.18);
  --glass-specular:    rgba(255, 255, 255, 0.90);  /* светлая кромка сверху */
  --glass-blur:        34px;
  --glass-blur-chrome: 30px;
  --glass-sat:         190%;
  /* Кромка панели — светлая, а не тёмная: на цветной подложке тёмная линия
     читается как обводка, светлая — как торец стекла (референс Images/1.png). */
  --glass-edge:        rgba(255, 255, 255, 0.60);
  --glass-edge-hi:     rgba(255, 255, 255, 0.72);
  --glass-shadow:      0 2px 6px rgba(0, 0, 0, .05), 0 26px 60px -22px rgba(20, 24, 48, .32);
  --glass-sheen:       linear-gradient(180deg, rgba(255,255,255,.20) 0%, rgba(255,255,255,0) 44%);
  /* Материал строк-пилюль внутри панели (модификатор table.lh.rows-pill) */
  --glass-row:      rgba(255, 255, 255, 0.44);
  --glass-row-hi:   rgba(255, 255, 255, 0.80);
  --glass-row-edge: rgba(255, 255, 255, 0.62);

  /* --- Mesh-подсветка фона (Apple: blue / indigo / teal) --- */
  --color-mesh-lavender: #5E5CE6;   /* systemIndigo */
  --color-mesh-pink:     #30B0C7;   /* systemTeal   */
  --color-mesh-purple:   #007AFF;   /* systemBlue   */
  /* Подложка .app::before: плотнее прежнего mesh — иначе за 34px блюра стеклу
     нечего размывать и панель выглядит просто белым прямоугольником. Верхний
     потолок плотности задан читаемостью текста ВНЕ панелей (фильтры, футер):
     на этих значениях --color-text-soft держит ≥4.5:1 над самым ярким блобом. */
  --mesh-a: rgba(0, 122, 255, 0.44);
  --mesh-b: rgba(48, 176, 199, 0.34);
  --mesh-c: rgba(94, 92, 230, 0.40);
  --mesh-d: rgba(255, 149, 0, 0.16);

  /* --- Текст (семантические уровни Apple) --- */
  --color-text:      #16161A;   /* label */
  --color-text-soft: #4A4A55;   /* secondaryLabel — 8.3:1 */
  --color-muted:     #6B6B78;   /* tertiaryLabel  — 5.1:1, мелкие подписи */
  --color-dim:       #BCBCC6;   /* ТОЛЬКО декор: рамки, нейтральный сегмент */

  /* --- Акцент (systemBlue) --- */
  --color-lead:       #007AFF;   /* заливки, графики, крупные KPI */
  --color-lead-hi:    #0A84FF;
  --color-lead-deep:  #006AE3;
  --color-lead-text:  #0B5FCC;   /* мелкий текст — 6.0:1 */
  /* ⚠️ АКЦЕНТ ТЕКСТА — ОТДЕЛЬНЫЙ ТОКЕН (владелец, 21.08: «давай выделение
     попробуем цветом как у буквы Z в слове Lidz, если не понравится вернём»).
     Раньше акцент и предупреждение делили `--color-warning-text`, и поменять
     одно, не задев другое, было нельзя. Теперь их два:
       `--color-accent-text`  — «посмотрите сюда»: выделения в панелях, активный
                                пункт меню, подсветка строки под курсором;
       `--color-warning-text` — «что-то не так»: просрочка, перерасход, лимит.
     ⚠️ ЦВЕТ АКЦЕНТА — ОРАНЖЕВЫЙ, И ЭТО ПРОВЕРЕНО ГЛАЗАМИ. 21.08 пробовали
     синий — цвет «z» из логотипа (`--color-lead-text`): владелец посмотрел на
     живом дашборде и вернул оранжевый. Пробу не повторять без спроса.
     ⚠️ ПЕРЕКРАСКА — ЭТА ОДНА СТРОКА. Токен и заведён ради этого: цвет акцента
     меняется в одном месте, а смысловые предупреждения ниже остаются на своём
     `--color-warning-text` и не едут за ним.
     ⚠️ С 23.08 АКЦЕНТ БОЛЬШЕ НЕ ОРАНЖЕВЫЙ — он равен `--hi` (см. ниже), то есть
     выделения дашборда красятся тем же предельным нейтралем, что и меню
     (владелец: «все выделения в дашборде должны быть как в меню — белые»).
     ⚠️ ОТКАТ К ОРАНЖЕВЫМ ВЫДЕЛЕНИЯМ — ЭТА ОДНА СТРОКА:
     `--color-accent-text: var(--color-warning-text)`. Меню при этом останется
     белым (у него свой `--nav-hi`), то есть вернётся ровно состояние 23.08. */
  --color-accent-text: var(--hi);
  /* ⚠️ ПОДСВЕТКА — ОДИН ТОКЕН НА ВЕСЬ ДАШБОРД (владелец, 23.08: сначала
     «давай заменим цвет выделения на белый» про левое меню, следом — «все
     выделения в дашборде должны быть как в меню — белые»).
     `--hi` — это ВЫДЕЛЕНИЕ: «вы здесь», «выбрано», «вот эта строка», выделенное
     слово в панели. Смысловые сигналы («что-то не так»: просрочка, перерасход,
     лимит, спам, ошибка) остаются на `--color-warning-text` / `--color-hunter-hi`
     и за `--hi` НЕ едут — иначе страница потеряет способность кричать о беде.
     ⚠️ В СВЕТЛОЙ ТЕМЕ ЭТО НЕ #FFF, И ЭТО НЕ ОТСЕБЯТИНА. Материал меню и панелей
     светлый (`--glass-bg-chrome` = rgba(248,248,252,.72) поверх mesh); белая
     подпись на нём даёт ~1.05:1 — выделение исчезает вовсе. Смысл просьбы —
     «выделяем предельным нейтральным, а не цветом»; предельный нейтраль светлой
     темы — чернильный `--color-text` (16.9:1 против 5.9:1 у покоя
     `--color-text-soft`), то есть тот же приём, что белый в тёмной.
     ⚠️ ОТКАТ ВСЕГО (включая меню) — ЭТА ОДНА СТРОКА:
     `--hi: var(--color-warning-text)`. */
  --hi: var(--color-text);
  /* Меню читает выделение через свой псевдоним: его перекрасили первым, и если
     когда-нибудь захотят развести меню и остальной дашборд — разводится здесь,
     не трогая ни одного правила. */
  --nav-hi: var(--hi);
  --color-lead-press: #094BA3;
  --tint-accent:      rgba(0, 122, 255, 0.10);
  --tint-accent-hi:   rgba(0, 122, 255, 0.16);
  --focus-ring:       rgba(0, 122, 255, 0.35);
  /* Готовый рецепт кольца фокуса. До 02.08 этот box-shadow был
     переписан руками 28 раз (5 в каноне, остальное по шаблонам), и одной
     правки ширины хватало, чтобы фокус стал разным на разных страницах.
     Токен цвета (--focus-ring) остаётся: он живёт в обеих темах отдельно, а
     рецепт формы у них общий.
     Внутренний вариант — для контролов, которым нельзя выходить за свои
     границы (строка списка внутри прокрутки, зона внутри пилюли). */
  --ring-focus:       0 0 0 3px var(--focus-ring);
  --ring-focus-inset: inset 0 0 0 2px var(--focus-ring);
  /* ЦВЕТ САМОЙ КРОМКИ ВЫДЕЛЕННОГО КОНТРОЛА — второй половина рецепта фокуса
     (владелец, 07.08: «если выделяем какой-либо текстбокс, он у нас выделяется
     синей рамкой — давай выделять так же и иконки»).
     До этой правки «синяя рамка» жила ТРЕМЯ независимыми копиями и уже
     разъехалась: поля правки подписки и строка поиска брали `--color-lead`,
     общий образец `.lh-form` — нейтральный `--color-rule-strong`, а иконочная
     кнопка не красила кромку вовсе (только гало). Теперь у всех троих один
     токен: правится в одном месте, разъехаться нечему.
     ⚠️⚠️ СИНЕВЫ ЗДЕСЬ БОЛЬШЕ НЕТ (владелец, 08.08). Дословно: «выделенный
     текстбокс подсвечивается синим, а должен серым или белым ярким», и
     отдельно — «да, поправь синюю подсветку». Механизм оставлен как был (ОДИН
     токен на поля и иконки), поменялось только его содержимое.
     ⚠️⚠️ ПРИГЛУШЕНО В ТОТ ЖЕ ДЕНЬ (владелец, 08.08: «подсветка теперь сильно
     яркая»). Первая редакция брала крайние точки шкалы — `--color-muted`
     (#6B6B78) в светлой и чистый белый в тёмной, то есть 5.03:1 и 15.99:1.
     Обе опущены примерно на середину между спокойной рамкой и прежним
     состоянием, и обе — тем же нейтралом, что и сами разделители
     (`--color-rule` = rgba(60,60,67,α) в светлой, rgba(255,255,255,α) в
     тёмной): выделение стало ПЛОТНЕЕ ТОЙ ЖЕ ЛИНИИ, а не другим цветом.
       · светлая тема — rgba(60,60,67,0.62): **3.62:1** к белому дну поля
         (было 5.03). Порог для нетекстовой графики 3:1, запас есть;
       · тёмная тема — rgba(255,255,255,0.62): **7.21:1** к дну поля
         (было 15.99). Серый в тёмной читался бы «выключено», поэтому
         приглушается не колер, а плотность того же белого.
     Ниже 0.62 по альфе не опускать: светлая уходит под 3:1 (0.55 даёт 3.03).
     ⚠️⚠️ ГАЛО У ПОЛЕЙ И ИКОНОК УБРАНО. Расходящейся тени вокруг выделенного
     контрола быть не должно — вместо неё кромка УПЛОТНЯЕТСЯ до двух пикселей
     внутренней тенью (`--ring-focus-edge`): рисунок остаётся чётким, габарит не
     меняется (тень внутренняя), заметность не падает. Прежний
     `box-shadow: var(--ring-focus)` давал 3px размытого синего вокруг.
     ⚠️ `--ring-focus` НЕ УДАЛЁН и остаётся у контролов, где кольцо —
     ЕДИНСТВЕННЫЙ признак фокуса (строка списка, чип, заголовок-раскрывашка,
     ссылка): там нет собственной рамки, которую можно уплотнить, и снять гало
     значит оставить их вовсе без видимого фокуса. */
  --focus-edge:       rgba(60, 60, 67, 0.62);
  --ring-focus-edge:  inset 0 0 0 1px var(--focus-edge);

  /* --- ЧЕРНИЛА ДАННЫХ: диаграмма, полоса, крупное число (владелец, 23.08) ---
     Дословно: «на вкладке "Обзор" убрать все синие цвета и оранжевые, надо
     сделать красиво — и во всём дашборде так».

     Цвет на дашборде разложен на ТРИ РОЛИ, и правка касается ровно одной:
       · ДЕКОРАТИВНЫЙ — столбик диаграммы, полоса заполнения, крупное число
         плитки. Он ничего не сообщал: синим было и «Лидов», и «Упущенная
         выручка», оранжевым — «Спам отсеяно», то есть НОРМАЛЬНАЯ работа
         системы. Этот цвет снят и заменён нейтральной шкалой (здесь);
       · СИГНАЛЬНЫЙ — просрочка, ошибка, лимит, встала выдача. Остался на
         своих `--color-warning-text` / `--color-hunter-hi` / `.wk-dot` и за
         этой шкалой НЕ едет: обесцветить его значит ослепить страницу
         (прокси умирали четверо суток при живом `systemctl is-active`);
       · РАЗЛИЧИТЕЛЬНЫЙ — серии графика. Собран из ЭТОЙ ЖЕ шкалы разной
         светлоты, а не из радуги: у соседних ступеней контраст ≥1.9, линии
         различимы и в светлой, и в тёмной теме.

     ⚠️ ЭТО НЕЙТРАЛЬ, А НЕ СЕРАЯ КАША. Иерархию после снятия цвета держат
     кегль, вес и разрядка (`.kpi-value` — 2xl/bold/tabular, подпись — s/soft):
     они и раньше несли её, цвет был третьим слоем поверх.

     ⚠️ ОТКАТ — ЭТИ ЧЕТЫРЕ СТРОКИ (плюс их пара в тёмной теме) и `--kpi-tint`
     ниже. У владельца привычка попробовать и вернуть: синий акцент пробовали
     21.08 и отменили, оранжевые выделения — 23.08 утром. */
  /* ⚠️ ШКАЛА ПЕРЕСОБРАНА 23.08 ВЕЧЕРОМ (владелец: «с белыми было лучше, как
     было до этого — просто проработать белые цвета»). Прежние четыре значения
     подбирались на глаз и разъезжались по двум признакам сразу: шаг светлоты
     гулял (ΔL 0.179 / 0.165 / 0.133 в светлой теме), а подтон плавал от
     ступени к ступени (хрома 0.021 → 0.020 → … против 0.023 у соседней), из-за
     чего одна ступень отдавала сиреневым, другая читалась чистым серым.
     Теперь шкала строится в OKLCH: тон 285.6° — ТОТ ЖЕ, что у `--color-text`,
     `--color-text-soft`, `--color-muted` и дна карточки, шаг светлоты ровный
     (ΔL 0.15-0.16), хрома постоянная (0.019), и только у крайней ступени она
     ниже (0.010) — как у самого текста, иначе «почти белое» число отдаёт в
     сирень. Контрасты к дну (#FFFFFF): 11.8 / 6.0 / 3.2 / 1.8.
     ⚠️ ТРЕТЬЯ СТУПЕНЬ ПОДНЯТА НАД ПОРОГОМ 3:1 — она рисует линию на графике,
     а прежние 2.71:1 не проходили порог для нетекстовой графики. */
  --data-ink:   #37373D;   /* основная серия, полоса заполнения — 11.82:1 */
  --data-ink-2: #62626E;   /* вторая серия                       —  6.01:1 */
  --data-ink-3: #8E8E9B;   /* третья                             —  3.23:1 */
  --data-ink-4: #BFBFCD;   /* нейтральный слот «уже не в счёт»   —  1.82:1 */
  /* ЦВЕТ КРУПНОГО ЧИСЛА ПЛИТКИ. Классы `.accent` / `.warn` / `.ok` в разметке
     ОСТАВЛЕНЫ: они называют РОЛЬ числа, а не краску, и вернуть цвет стоит
     одной строки на роль. Сейчас все три ведут в предельный нейтраль. */
  --kpi-tint:   var(--color-text);

  /* --- ПАЛИТРА ДАННЫХ «ОБЗОРА»: три смысловые группы (владелец, 23.08) -------
     Дословно: «на вкладке "Обзор" не нравятся мне цвета, все такие — давай
     сделаем цветом, просто более гармонично, чем было».

     ⚠️ ЭТО ТРЕТЬЯ ИТЕРАЦИЯ ЗА ДЕНЬ, И ДВЕ ПЕРВЫЕ ПРОМАХНУЛИСЬ. Утром на плитках
     был синий вперемешку с оранжевым — владелец: «убрать синий и оранжевый».
     Днём цвет сняли вовсе (нейтральная шкала `--data-ink*` выше) — владелец
     посмотрел и сказал, что цвет нужен, но ГАРМОНИЧНЫЙ. То есть не нравилась не
     краска, а её СЛУЧАЙНОСТЬ: каждый показатель красили во что придётся.

     ЧТО ЗДЕСЬ СИСТЕМА, А НЕ НАБОР КРАСОК:
       · тон закреплён за СМЫСЛОВОЙ ГРУППОЙ, а не за плиткой. Групп на «Обзоре»
         ровно три — ПОТОК (сколько система произвела: сообщения, лиды, спам,
         приток, обе серии «Динамики»), ЗАПАС (что можно продать сейчас: свежие,
         утилизация, запас по нишам, денежные под-ниши) и ДЕНЬГИ (упущенная
         выручка, конверсия в сделки). Соседние по смыслу показатели — соседние
         по тону, разные группы — разные;
       · тоны СОСЕДНИЕ по кругу и взяты из mesh-подложки бренда: бирюза (H≈200),
         синий (H≈258), пурпур (H≈312). Это аналоговая гамма — то, что глаз
         читает как «согласовано», в отличие от красного с зелёным;
       · у каждой группы ДВЕ ступени одной краски: `--data-<группа>` — метка на
         графике (≥3:1 к дну), `--data-<группа>-deep` — крупное число плитки
         (≥4.5:1, кегль там текстовый). Внутри группы различает СВЕТЛОТА, между
         группами — ТОН. Ровно то же правило, по которому собраны две серии
         «Динамики по дням»: это одна группа, значит один тон и две ступени.

     ⚠️ ТЁПЛОЙ ПОЛОВИНЫ КРУГА ЗДЕСЬ НЕТ, И ЭТО ГЛАВНОЕ ОГРАНИЧЕНИЕ. Оранжевый
     (H≈53/67) — «что-то не так», красный (H≈29) — беда, зелёный (H≈148) — живое
     состояние. Данные, покрашенные сигнальным тоном, ломают сигнал: показатель
     в норме кричал бы тем же цветом, что просрочка. Поэтому вся палитра данных
     живёт в холодном секторе 195–320° и в сигнальную зону не заходит.

     ⚠️ ТРИ ТОНА НИКОГДА НЕ ВСТРЕЧАЮТСЯ В ОДНОЙ ДИАГРАММЕ — они разведены по
     плиткам и карточкам, каждая со своей подписью. Поэтому проверка
     «различимы ли соседние серии» (validate_palette.js из скилла dataviz)
     применена там, где она уместна: к ПАРЕ СТУПЕНЕЙ внутри группы (ординальная
     шкала, ΔL≥0.06 — PASS в обеих темах) и к паре «запас/протухло» в донате.
     Тройку тонов она мерит как категориальную палитру одного графика и на
     аналоговой гамме честно ругается — этот сценарий на странице не возникает.

     ⚠️ ПЕРЕКЛЮЧЕНИЕ ВАРИАНТА — ОДИН АТРИБУТ `data-palette` НА <html>
     (`base.html`). Без атрибута работает вариант «Гамма» (этот блок):
       · `data-palette="strong"` — те же три тона в полную силу (насыщенность
         на уровне mesh-подложки);
       · `data-palette="mono"`   — одна краска (синий бренда), группы различает
         только глубина.
     ⚠️ ОТКАТ К НЕЙТРАЛИ 23.08 — `data-palette="ink"`: тот же нейтральный
     `--data-ink*`, что стоял до этой правки. У владельца привычка попробовать и
     вернуть (синий акцент 21.08, оранжевые выделения и нейтральный декор 23.08),
     поэтому прежнее состояние оставлено достижимым одним словом, а не откатом. */
  /* ⚠️ ОСНОВНОЙ ВИД — НЕЙТРАЛЬНЫЙ (владелец, 23.08 вечером: «всё-таки с белыми
     было лучше, как было до этого — просто проработать белые цвета»). Группы
     ведут в ту же шкалу `--data-ink*` выше: различает СВЕТЛОТА, тона нет.
     Крупные числа плиток одинаковы намеренно — «Лидов», «Спам отсеяно» и
     «Сделок закрыто» сообщают о нормальной работе, и три разных краски рядом
     читались как три разные тревоги (тот же довод, что у `.accent`/`.warn`).
     ⚠️ ЦВЕТНАЯ ГАММА НЕ ВЫБРОШЕНА — она ниже, под `data-palette="hue"`, вместе
     со `strong` и `mono`. Четвёртой правки за день не будет: у владельца
     привычка попробовать и вернуть (синий акцент 21.08, оранжевые выделения
     утром 23.08, цветные группы днём), поэтому все варианты достижимы одним
     словом в атрибуте, а не откатом коммита. */
  --data-flow:        var(--data-ink-3);   /* поток — метка на графике */
  --data-flow-deep:   var(--color-text);   /* поток — крупное число */
  --data-stock:       var(--data-ink);     /* запас — метка */
  --data-stock-deep:  var(--color-text);   /* запас — крупное число */
  --data-money:       var(--data-ink-2);   /* деньги — метка */
  --data-money-deep:  var(--color-text);   /* деньги — крупное число */
  /* «Уже не в счёт»: протухший сегмент доната, пустая часть полосы. Намеренно
     рецессивен (1.82:1) — последняя ступень шкалы; число рядом с кольцом
     подписано словами, цветом его читать не нужно. */
  --data-off:         var(--data-ink-4);
  /* ⚠️ РАЗРЫВ СВЕТЛОТЫ У ГРУППЫ ПОТОКА САМЫЙ ШИРОКИЙ, и это не произвол:
     только у неё обе ступени попадают в ОДНУ диаграмму — «Динамика по дням»
     рисует «Сообщений» базовой (`--data-ink-3`), «Лидов» глубокой (текстовой).
     Замер на цветной гамме показал, что пара с ΔL≈0.11 сливается на кадре,
     хотя ординальный порог скилла dataviz (0.06) она проходила; здесь разрыв
     втрое шире (ΔL≈0.46 в светлой теме), то есть запас есть с избытком. */

  /* --- Статусы --- */
  --color-success: #248A3D;
  --color-warning: #B25000;
  --color-danger:  #D3000A;
  --color-hunter:  #C7261D;
  --color-hunter-hi:    #A3241C;   /* мелкий текст — 5.9:1 */
  --color-success-text: #1B6E33;   /* 5.8:1 */
  --color-warning-text: #8A4200;   /* 6.3:1 */

  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", "Inter", system-ui, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, "JetBrains Mono", monospace;

  --tracking-label: 0.34em;
  --tracking-snug:  -0.015em;
  --tracking-tight: -0.035em;

  /* --- Шкала размера и начертания -----------------------------------------
     До 02.08 токенов размера и веса в системе НЕ БЫЛО: замер дал 35 различных
     `font-size` и 13 весов (400, 500, 510, 530, 550, 560, 580, 590, 600, 620,
     640, 650, 660, 680). Причина структурная — автор новой страницы не мог
     собрать компонент «по документу», только скопировать соседний и подправить
     на пару сотых, поэтому ступени размножались сами.
     Шкала выведена из ролей, которые реально несут смысл (DESIGN.md §1.5), а
     значения взяты СУЩЕСТВУЮЩИЕ: соседние литералы снесены к ближайшей
     ступени, и ни один сдвиг не превышает половины пикселя при корне 16px.
     Крупные одноразовые кегли «Пути к 1M» (1.1…2.6rem) осознанно оставлены
     литералами: страница целиком приводится к каноническим `.kpi` отдельно. */
  --text-3xs:  0.7rem;    /* пилюля, счётчик, микроподпись */
  --text-2xs:  0.735rem;  /* заголовок колонки таблицы, метаданные под текстом */
  --text-xs:   0.78rem;   /* мелкая кнопка, подпись в строке */
  --text-s:    0.82rem;   /* сегмент тумблера, подпись секции */
  --text-sm:   0.86rem;   /* тело таблицы, кнопка-призрак */
  --text-base: 0.9rem;    /* поле ввода, основная кнопка */
  --text-md:   0.98rem;   /* заголовок карточки */
  --text-lg:   1.06rem;   /* подзаголовок блока */
  --text-xl:   1.6rem;    /* крупное число второго плана */
  --text-2xl:  2.1rem;    /* число KPI */

  /* Веса: шесть ступеней вместо тринадцати. У системного SF начертание
     переменное, поэтому разница в 10–20 единиц не читается глазом — но плодит
     ступени, между которыми потом никто не может выбрать. */
  --fw-regular:  400;
  --fw-medium:   530;
  --fw-semibold: 560;
  --fw-strong:   590;
  --fw-bold:     640;
  --fw-heavy:    700;

  /* Скругления в духе Apple continuous corners — эмулируем крупным радиусом. */
  --radius-sm:  10px;
  --radius-md:  16px;
  --radius-lg:  22px;
  --radius-xl:  28px;
  /* РАДИУС ПЛИТКИ — 26px, и это ОДИН токен на два места (владелец, 23.08:
     «углы дашборда сделать круглыми как у плитки»). До этого число жило
     литералом внутри `.glass` и повторить его во внешнем контуре можно было
     только на глаз. Читают токен ровно двое: стекло карточек и скруглённая
     рамка окна дашборда (`.app-corners`). Правишь форму карточки — внешний
     угол едет за ней сам, и разнобоя не будет. */
  --radius-tile: 26px;
  /* СКРУГЛЕНИЕ ТЕКСТБОКСА — отдельная ступень, и она не совпадает ни с одной из
     четырёх выше намеренно (владелец, 08.08: «у нас все текстбоксы круглые, а в
     Клиентах…»). Половина высоты поля (40px) = капсула; у полей ниже сорока
     браузер сам ужимает радиус пропорционально, и капсула остаётся капсулой.
     ⚠️ Ставить радиус поля числом НЕЛЬЗЯ — ровно так дашборд и разъехался: 20px
     у формы клиента и композера переписки, 10px у строки поиска и базового
     `.form-row`, 9px у полей правки подписки и долива пробы. Один экран
     «Клиентов» показывал сразу три разных текстбокса. */
  --radius-field: 20px;

  /* --- Шкала отступов ------------------------------------------------------
     Ступени были объявлены (4/8/12/16/22/32/44) и НЕ ИСПОЛЬЗОВАЛИСЬ ни разу:
     `var(--sp-` встречался в файле 0 раз. Хуже всего было именно это —
     документ обещал шкалу, которой в коде нет, поэтому каждая новая страница
     ставила отступ на глаз.
     02.08 ступени переписаны по ФАКТИЧЕСКОМУ ритму дашборда (замер по всем 12
     роутам), а канонические правила ниже переведены на них. Оптические
     исключения остаются литералами и это нормально: 11px в ячейке таблицы
     (плотность списка) и 80px внизу `.shell` (место под футер) шкалой не
     описываются. */
  --sp-1:  4px;   /* микрозазор: подпись к иконке */
  --sp-2:  8px;   /* мелкий зазор внутри строки */
  --sp-3: 10px;   /* ячейка таблицы, зазор в ряду контролов */
  --sp-4: 14px;   /* заголовок → контент, ряд KPI, форма */
  --sp-5: 20px;   /* сетка колонок */
  --sp-6: 22px;   /* внутренний отступ панели, ритм секций */
  --sp-7: 26px;   /* полоса контента .shell */

  /* Тени: мягкие и нейтральные — стекло «висит» над фоном, а не обведено рамкой */
  --shadow-sm:  0 1px 2px rgba(0,0,0,.05);
  --shadow-md:  0 1px 2px rgba(0,0,0,.04), 0 6px 20px rgba(0,0,0,.06);
  --shadow-lg:  0 2px 6px rgba(0,0,0,.05), 0 18px 44px rgba(0,0,0,.10);
  --shadow-pop: 0 2px 6px rgba(0,0,0,.06), 0 14px 32px rgba(0,0,0,.11);

  /* Фирменный «сигнал» — акцентная полоска на KPI и у заголовков секций.
     Теперь в системной палитре: blue → indigo → teal. */
  --grad-signal:   linear-gradient(90deg, rgba(0,122,255,.92) 0%, rgba(94,92,230,.88) 52%, rgba(48,176,199,.92) 100%);
  --grad-signal-v: linear-gradient(180deg, rgba(0,122,255,.94) 0%, rgba(94,92,230,.88) 52%, rgba(48,176,199,.94) 100%);

  /* Геометрия сайдбара */
  --side-w:      68px;
  --side-w-open: 192px;   /* 06.09: отступ справа от самой длинной подписи = отступу слева до иконок (24px), замер в Chromium */
  --topbar-h:    58px;

  /* --- Моторика (кривые и длительности) -----------------------------------
     Одна семья кривых на весь дашборд. Основа — та, что уже стояла у сайдбара
     (cubic-bezier(.32,.72,0,1)): резкий старт, длинный тихий хвост. Дефолтный
     `ease` из кода убран — он симметричный и делает движение «резиновым».

     Где что:
       --ease-out-quart  — короткие ховеры и смена цвета (≤180 мс);
       --ease-out-soft   — легаси-геометрия; каркас переведён на пружины,
                           см. --ease-spring-fast ниже;
       --ease-out-expo   — осталась только на смене ЦВЕТА (там пружина
                           бессмысленна: у цвета нет инерции). Все геометрические
                           раскрытия с неё сняты — это была самая чужая кривая
                           в системе (отклонение до 22.8% от настоящей пружины);
       --ease-in-out     — симметричные смены (там, где элемент уезжает и
                           приезжает обратно);
       --ease-pop        — лёгкий доводчик за 100%: ТОЛЬКО мелкие пилюли/чипы,
                           на крупных блоках читается как желе.
     Исключения, где кривую НЕ ставим: вращение спиннера (linear физически
     верен) и «дыхание» точек статуса (ease-in-out у @keyframes). */
  --ease-out-quart: cubic-bezier(.25, 1, .5, 1);
  --ease-out-soft:  cubic-bezier(.32, .72, 0, 1);
  --ease-out-expo:  cubic-bezier(.16, 1, .3, 1);
  --ease-in-out:    cubic-bezier(.65, 0, .35, 1);
  --ease-pop:       cubic-bezier(.34, 1.26, .64, 1);
  /* Капсула сегментного тумблера: резкий старт как у expo + мелкий перелёт
     (~3%) в конце — «пружина» вместо «доехало». Снята с референса, см. .pt-ind. */
  --ease-seg:       cubic-bezier(.26, 1.08, .32, 1);

  --dur-1: 110ms;   /* цвет/фон под курсором */
  --dur-2: 170ms;   /* смена состояния контрола */
  --dur-3: 260ms;   /* геометрия каркаса — ЛЕГАСИ, см. --dur-spring-fast */
  --dur-4: 320ms;   /* появление секции */
  --stagger: 28ms;  /* шаг каскада между соседями */

  /* --- ПРУЖИНЫ (моторика Apple) -------------------------------------------
     Считано из настоящей SwiftUI `Spring` на macOS 26, 24 контрольные точки,
     ошибка кусочно-линейной аппроксимации 0.011–0.017. Вывод — tasks/apple-design-system.md.

     ⚠️ ГЛАВНОЕ ПРАВИЛО: длительность перехода ОБЯЗАНА равняться времени
     успокоения пружины (`--dur-spring-*` рядом с каждой кривой). Поставишь
     другую — кривая сожмётся или растянется, и физика пропадёт. Кривая и
     длительность здесь ходят ПАРОЙ, поодиночке их не берут.

     Perceptual duration (то, чем пружина задана у Apple) — НЕ время анимации:
       spring-fast  = smooth(0.20), settling 300 мс — геометрия каркаса и раскрытия;
       spring-frame = smooth(0.30), settling 500 мс — крупные редкие проезды (полосы);
       spring-snappy= snappy(0.25, bounce .15), settling 470 мс — сегментная капсула.

     Почему каркас взял 0.20, а не дефолтные Apple 0.5 (=800 мс CSS): дашборд —
     плотный курсорный интерфейс с десятками действий подряд, для него в HIG
     эталон `interactiveSpring` (0.15–0.30 перцептивных), а не `.smooth`.

     ЗАМЕР до/после (время до доли пути), раскрытия и сайдбар:
       было  cubic-bezier(.32,.72,0,1) 260 мс: 50% за 42 мс, 86% за 81, 99% за 187;
       стало spring 0.20 / 300 мс:            50% за 53 мс, 86% за 110, 99% за 211.
     То есть старт стал НЕ резче, а ровнее (это и читалось как рывок), общий
     темп прибавил 40 мс — на глаз столько же, физика другая.

     `--ease-spring-snappy` намеренно НЕ подключён ни к чему: капсула тумблера
     живёт на `--ease-seg`, чья форма и так совпадает со `snappy` (RMS 0.035),
     а трогать её отдельно от остальной моторики запрещено (дефект 02.08).
     Токен стоит здесь как готовое место для неё, если решим переводить.

     Значения ниже — bezier-ФОЛБЭК для браузеров без `linear()`; настоящие
     пружины подставляются в блоке @supports сразу под этим :root. */
  --ease-spring-fast:  cubic-bezier(.28, .594, .081, 1.059);
  --ease-spring-frame: cubic-bezier(.28, .594, .081, 1.059);
  --ease-spring-snappy: cubic-bezier(.26, 1.08, .32, 1);
  /* Ужатая пружина без перелёта — сюда съезжают все кривые при
     prefers-reduced-motion (HIG: «tightening springs», а не «отключить»). */
  --ease-spring-tight: cubic-bezier(.32, .72, 0, 1);

  --dur-spring-fast:  300ms;
  --dur-spring-frame: 500ms;
  --dur-spring-snappy: 470ms;

  /* --- РАСКРЫТИЕ ИЗ ИКОНКИ (08.08, владелец: «хочу как в ТГ») -------------
     ⚠️ ЭТО ВТОРОЙ РОД ДВИЖЕНИЯ, И ОН НЕ ОТМЕНЯЕТ ПЕРВЫЙ. Пружина выше — это
     ПРОЕЗД ПО ВЫСОТЕ (сайдбар, тело карточки клиента, свёрнутые подблоки), и
     требование владельца 31.07 «раскрытие карточек как панель меню» про неё.
     Здесь — панели и формы, вызванные ИКОНКОЙ: они не раздвигают соседей, а
     выезжают из точки нажатия, как всплывающее меню Telegram.
     Едут только `transform: scale()` и `opacity`; `transform-origin` ставит
     скрипт по факту — от центра самой иконки (`popOrigin` в dashboard.js).
     Кривая ease-out БЕЗ перелёта: пружина сайдбара тут «доводится» хвостом, а
     всплывашка обязана останавливаться.
     Закрытие быстрее открытия и уходит в ту же точку — единственная асимметрия
     системы (у проезда по высоте `COLLAPSE_RATIO = 1`, там сходство с
     сайдбаром важнее).
     ⚠️ ЗЕРКАЛО В JS — `POP_MS` / `POP_OUT_MS` / `POP_SCALE` / `POP_EASE*`
     в `dashboard.js`. Правишь одну сторону — правь вторую. */
  --dur-pop-in:  200ms;
  --dur-pop-out: 140ms;
  /* ⚠️ quart, а не expo: у expo при 200 мс половина пути проходится за 20 мс —
     движение кончается раньше, чем глаз его поймал, и «выезд из иконки»
     читается как «панель просто появилась». Замер обеих — в dashboard.js. */
  --ease-pop-in:  var(--ease-out-quart);
  --ease-pop-out: cubic-bezier(.4, 0, .9, .4);
  --pop-scale: 0.92;   /* не 0, а «чуть меньше»: с нуля жест читается зумом */
}

/* Настоящие пружины. Отдельным блоком, а не прямо в :root, потому что при
   отсутствии поддержки `linear()` браузер выбросит объявление и значение
   переменной станет невалидным — трансформируется в `linear`, что хуже
   бесплатного ease-out. Так же безопаснее для шорткада `transition:`, где
   двойное объявление timing-function не сделаешь. */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    /* smooth, perceptual 0.20 с; CSS duration 300ms; err 0.011 */
    --ease-spring-fast: linear(0.000, 0.055, 0.192, 0.329, 0.460, 0.588, 0.682, 0.757,
      0.823, 0.868, 0.902, 0.930, 0.949, 0.962, 0.974, 0.981, 0.986, 0.990, 0.993, 0.995,
      0.997, 0.998, 0.998, 0.999, 0.999);
    /* smooth, perceptual 0.30 с; CSS duration 500ms; err 0.013 */
    --ease-spring-frame: linear(0.000, 0.067, 0.224, 0.376, 0.515, 0.645, 0.736, 0.806,
      0.865, 0.903, 0.931, 0.953, 0.967, 0.977, 0.984, 0.989, 0.992, 0.995, 0.997, 0.998,
      0.998, 0.999, 0.999, 1.000, 1.000);
    /* snappy, perceptual 0.25 с, bounce 0.15; CSS duration 470ms; err 0.017 */
    --ease-spring-snappy: linear(0.000, 0.085, 0.284, 0.473, 0.636, 0.778, 0.867, 0.926,
      0.968, 0.989, 1.000, 1.005, 1.006, 1.006, 1.005, 1.003, 1.002, 1.002, 1.001, 1.001,
      1.000, 1.000, 1.000, 1.000, 1.000);
  }
}

/* ============================================================
   2. Токены — тёмная тема (собственный рецепт, не инверсия)
   ============================================================ */

:root[data-theme="dark"] {
  --color-bg:             #0E0E12;
  /* В тёмной теме «стол» — чистый чёрный: он темнее любой подложки страницы,
     и срез угла читается, не заводя пятого серого. */
  --frame-canvas:         #000000;
  --color-surface:        rgba(72, 72, 82, 0.42);
  --color-surface-strong: rgba(78, 78, 90, 0.52);
  --color-surface-solid:  #1C1C21;
  --color-rule:           rgba(255, 255, 255, 0.11);
  --color-rule-strong:    rgba(255, 255, 255, 0.18);

  --fill-1: rgba(120, 120, 128, 0.24);
  --fill-2: rgba(120, 120, 128, 0.32);
  --fill-3: rgba(120, 120, 128, 0.16);
  --bar-idle: #9EA0AE;

  --glass-bg:          rgba(58, 58, 70, 0.42);
  --glass-bg-strong:   rgba(80, 80, 94, 0.50);
  --glass-bg-chrome:   rgba(28, 28, 34, 0.70);
  --glass-border:      rgba(255, 255, 255, 0.11);
  --glass-border-hi:   rgba(255, 255, 255, 0.17);
  --glass-specular:    rgba(255, 255, 255, 0.24);
  --glass-blur:        40px;
  --glass-blur-chrome: 32px;
  --glass-sat:         170%;
  --glass-edge:        rgba(255, 255, 255, 0.14);
  --glass-edge-hi:     rgba(255, 255, 255, 0.20);
  --glass-shadow:      0 2px 8px rgba(0, 0, 0, .28), 0 32px 72px -22px rgba(0, 0, 0, .68);
  --glass-sheen:       linear-gradient(180deg, rgba(255,255,255,.09) 0%, rgba(255,255,255,0) 44%);
  --glass-row:      rgba(255, 255, 255, 0.055);
  --glass-row-hi:   rgba(255, 255, 255, 0.115);
  --glass-row-edge: rgba(255, 255, 255, 0.075);

  --color-mesh-lavender: #5E5CE6;
  --color-mesh-pink:     #40C8E0;
  --color-mesh-purple:   #0A84FF;
  --mesh-a: rgba(10, 132, 255, 0.44);
  --mesh-b: rgba(64, 200, 224, 0.24);
  --mesh-c: rgba(120, 92, 255, 0.40);
  --mesh-d: rgba(255, 105, 97, 0.17);

  --color-text:      #F2F2F7;
  --color-text-soft: #B9B9C6;   /* 9.2:1 на фоне */
  --color-muted:     #9797A6;   /* 6.1:1 */
  --color-dim:       #4C4C58;   /* декор */

  --color-lead:       #0A84FF;
  --color-lead-hi:    #4DA6FF;
  --color-lead-deep:  #4DA6FF;
  --color-lead-text:  #6FB8FF;   /* 7.4:1 */
  /* ⚠️ ПАРЫ У `--color-accent-text` И `--nav-hi` ЗДЕСЬ НЕТ, И ЭТО НАМЕРЕННО.
     Оба выражены через `--hi`, а он тему держит сам — второе значение было бы
     вторым источником правды и разъехалось бы при первой же правке.
     Подсветка тёмной темы — БЕЛЫЙ, дословно по просьбе. Не `--color-text`
     (#F2F2F7): у него та же роль, что у обычного текста, и выделенное не
     отличалось бы от подписи рядом. Чистый #FFF даёт 21:1 против 9.2:1 у
     покоя — разрыв виден. */
  --hi: #FFFFFF;
  --color-lead-press: #A6D4FF;
  --tint-accent:      rgba(10, 132, 255, 0.16);
  --tint-accent-hi:   rgba(10, 132, 255, 0.26);
  --focus-ring:       rgba(77, 166, 255, 0.50);
  /* Тот же белый, что и разделители (`--color-rule`), только плотнее: 7.21:1 к
     дну поля `--color-surface-solid` (#1C1C21). Чистый #FFFFFF стоял здесь
     несколько часов 08.08 и дал 15.99:1 — владелец назвал это «сильно ярко».
     Приглушаем ПЛОТНОСТЬЮ, а не колером: серый в тёмной читается «выключено». */
  --focus-edge:       rgba(255, 255, 255, 0.62);

  /* Чернила данных тёмной темы — та же шкала, отражённая: самая плотная
     ступень светлая. Пересобрана 23.08 вечером вместе со светлой (разбор — там
     же, в §1): тон 285.6° как у `--color-text` и дна, шаг светлоты ровный
     (ΔL 0.15-0.17), хрома 0.019 и 0.010 у крайней ступени.
     Контрасты к дну `--color-surface-solid` (#1C1C21): 13.8 / 8.5 / 4.9 / 2.5 —
     соседние ступени различимы, последняя читается как «выключено». */
  --data-ink:   #E7E7EE;   /* 13.79:1 */
  --data-ink-2: #B6B6C3;   /*  8.46:1 */
  --data-ink-3: #898996;   /*  4.92:1 */
  --data-ink-4: #595965;   /*  2.46:1 */

  /* Группы тёмной темы — нейтраль, зеркало светлой (разбор в §1). Ступень
     `-deep` здесь СВЕТЛЕЕ базовой: в тёмной вперёд выходит то, что светлее, и
     «крупное число» обязано быть первым — поэтому глубокая ведёт в текстовый
     `--color-text` (#F2F2F7), а базовая в среднюю ступень шкалы. */
  --data-flow:        var(--data-ink-3);
  --data-flow-deep:   var(--color-text);
  --data-stock:       var(--data-ink);
  --data-stock-deep:  var(--color-text);
  --data-money:       var(--data-ink-2);
  --data-money-deep:  var(--color-text);
  --data-off:         var(--data-ink-4);   /* 2.46:1 — «уже не в счёт» */

  --color-success: #30D158;
  --color-warning: #FF9F0A;
  --color-danger:  #FF453A;
  --color-hunter:  #FF6961;
  --color-hunter-hi:    #FF8F86;   /* 7.0:1 */
  --color-success-text: #5EE07F;   /* 9.6:1 */
  --color-warning-text: #FFB454;   /* 9.3:1 */

  --shadow-sm:  0 1px 2px rgba(0,0,0,.40);
  --shadow-md:  0 1px 2px rgba(0,0,0,.34), 0 8px 24px rgba(0,0,0,.42);
  --shadow-lg:  0 2px 8px rgba(0,0,0,.38), 0 22px 52px rgba(0,0,0,.52);
  --shadow-pop: 0 2px 8px rgba(0,0,0,.40), 0 16px 38px rgba(0,0,0,.50);

  --grad-signal:   linear-gradient(90deg, rgba(10,132,255,.95) 0%, rgba(94,92,230,.90) 52%, rgba(64,200,224,.95) 100%);
  --grad-signal-v: linear-gradient(180deg, rgba(10,132,255,.95) 0%, rgba(94,92,230,.90) 52%, rgba(64,200,224,.95) 100%);

  color-scheme: dark;
}
:root { color-scheme: light; }

/* --- ВАРИАНТЫ ПАЛИТРЫ ДАННЫХ (владелец выбирает глазами, 23.08) -------------
   Атрибут `data-palette` на <html>. Порядок правил в файле важен: светлые
   варианты (0,2,0) стоят ПОСЛЕ тёмного блока (0,2,0) и переиграли бы его, —
   поэтому у каждого варианта есть пара для тёмной темы со специфичностью
   (0,3,0). Правишь один вариант — правь обе его половины.

   ⚠️ БЕЗ АТРИБУТА РАБОТАЕТ НЕЙТРАЛЬ (§1) — так решил владелец вечером 23.08,
   посмотрев все четыре варианта живьём. Прежде без атрибута работала цветная
   «Гамма», а нейтраль пряталась под `ink`; теперь наоборот, и вариант `ink`
   удалён за ненадобностью — это и есть основной вид. Цвет никуда не делся:
   три группы ниже, слово в атрибуте возвращает их целиком. */

/* «Гамма»: тон закреплён за смысловой группой — поток бирюзовый (H≈200), запас
   синий (H≈258), деньги пурпурные (H≈312). Тона соседние по кругу и взяты из
   mesh-подложки бренда; тёплой половины круга здесь нет намеренно (оранжевый и
   красный — сигнал, см. §1). Стоял основным видом полдня 23.08. */
:root[data-palette="hue"] {
  --data-flow:        #069FA6;   /* поток — метка на графике,  3.22:1 */
  --data-flow-deep:   #066166;   /* поток — крупное число,     7.22:1 */
  --data-stock:       #3F76C8;   /* запас — метка,             4.53:1 */
  --data-stock-deep:  #1659B5;   /* запас — крупное число,     6.71:1 */
  --data-money:       #8247A3;   /* деньги — метка,            6.21:1 */
  --data-money-deep:  #6F2F91;   /* деньги — крупное число,    8.43:1 */
  --data-off:         #BFC8D6;   /* 1.69:1 — «уже не в счёт» */
}
:root[data-theme="dark"][data-palette="hue"] {
  --data-flow:        #0A999F;   /* 4.91:1 */
  --data-flow-deep:   #4ECCD3;   /* 8.81:1 */
  --data-stock:       #4B88E2;   /* 4.78:1 */
  --data-stock-deep:  #6BA5FB;   /* 6.79:1 */
  --data-money:       #A05FC4;   /* 3.98:1 */
  --data-money-deep:  #BA7EDE;   /* 5.76:1 */
  --data-off:         #444E5D;   /* 2.02:1 */
}

/* «В полную силу»: те же три тона, насыщенность на уровне mesh-подложки. */
:root[data-palette="strong"] {
  --data-flow:        #0B9BA8;   /* 3.35:1 */
  --data-flow-deep:   #036169;   /* 7.20:1 */
  --data-stock:       #1E72E4;   /* 4.58:1 */
  --data-stock-deep:  #0258BE;   /* 6.68:1 */
  --data-money:       #9431B9;   /* 6.18:1 */
  --data-money-deep:  #7E12A2;   /* 8.42:1 */
}
:root[data-theme="dark"][data-palette="strong"] {
  --data-flow:        #0DA5B2;   /* 5.69:1 */
  --data-flow-deep:   #07D6E7;   /* 9.52:1 */
  --data-stock:       #3789FD;   /* 4.98:1 */
  --data-stock-deep:  #6EA8FF;   /* 7.04:1 */
  --data-money:       #B64FDE;   /* 4.17:1 */
  --data-money-deep:  #CC70F2;   /* 5.79:1 */
}

/* «Одна краска»: синий бренда, группы различает только глубина —
   поток светлее, запас средний, деньги самые тёмные (в тёмной теме порядок
   тот же по НАСЫЩЕННОСТИ, светлота везде поднята). */
:root[data-palette="mono"] {
  --data-flow:        #5D94DA;   /* 3.13:1 */
  --data-flow-deep:   #2877D3;   /* 4.50:1 */
  --data-stock:       #1570D1;   /* 4.92:1 */
  --data-stock-deep:  #005BB3;   /* 6.67:1 */
  --data-money:       #0053A4;   /* 7.57:1 */
  --data-money-deep:  #02468B;   /* 9.33:1 */
}
:root[data-theme="dark"][data-palette="mono"] {
  --data-flow:        #7AAEEF;   /* 7.38:1 */
  --data-flow-deep:   #91C1FF;   /* 9.11:1 */
  --data-stock:       #4390EE;   /* 5.22:1 */
  --data-stock-deep:  #60A7FF;   /* 6.84:1 */
  --data-money:       #0475E1;   /* 3.75:1 */
  --data-money-deep:  #308CF6;   /* 5.02:1 */
}

/* ⚠️ ВАРИАНТА `ink` БОЛЬШЕ НЕТ — нейтраль стала основным видом (§1), и держать
   для неё отдельное имя значило бы завести вторую копию тех же семи строк.
   `data-palette="ink"` в адресной строке просто ничего не переключит: это и
   есть вид без атрибута. */

* { box-sizing: border-box; }

/* Шрифт в элементы форм НЕ НАСЛЕДУЕТСЯ: браузер даёт им свой (на macOS это
   Arial). Замер 02.08 показал 126 подписей кнопок на «Клиентах», 6 на
   «Конкурентах» и 2 на «Операциях», набранных Arial вместо SF, — каждая
   кнопка, забывшая объявить `font-family`, уезжала молча. Одно правило
   закрывает класс ошибки целиком; отдельные объявления в компонентах
   оставлены (они безвредны и переживут перенос правила).
   ⚠️ Наследуем ТОЛЬКО семейство: `font: inherit` утащил бы ещё и размер, а
   кнопки без явного `font-size` рассчитаны на дефолтные 13.3px и выросли бы
   до 16px вместе со всей раскладкой. */
button, input, select, textarea, optgroup { font-family: inherit; }

html {
  font-family: var(--font-sans);
  color: var(--color-text);
  background: var(--color-bg);
  letter-spacing: -0.011em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body {
  margin: 0;
  line-height: 1.5;
  min-height: 100vh;
  background-color: var(--color-bg);
}

/* Подложка «стекла» — единственный источник цвета за панелями (прежний
   background-image на body убран, две системы фона давали двойное затемнение).
   Fixed: не скроллится, значит не перерисовывается и не удорожает скролл.

   z-index именно 0, а НЕ -1: у body непрозрачный background-color, а
   отрицательный слой красится РАНЬШЕ фонов блочных потомков — подложку бы
   просто закрасило. При z-index:0 она позиционированный слой, лежит над фоном
   body; весь контент поднят над ней через .shell{z-index:1}, топбар (40) и
   сайдбар (60) и так выше. */
.app::before {
  content: "";
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(ellipse 46% 40% at 12% 8%,   var(--mesh-a) 0%, transparent 62%),
    radial-gradient(ellipse 40% 44% at 88% 18%,  var(--mesh-c) 0%, transparent 64%),
    radial-gradient(ellipse 52% 38% at 70% 88%,  var(--mesh-b) 0%, transparent 62%),
    radial-gradient(ellipse 34% 30% at 26% 74%,  var(--mesh-d) 0%, transparent 66%);
}

/* --- ОБОИ: дудл-паттерн поверх mesh-подложки (02.08, выбор владельца) --------
   Второй слой того же механизма, что и `.app::before`: fixed, z-index 0, ниже
   `.shell{z-index:1}`, кликов не ловит. Второй фон на body НЕ заводим — ровно
   от этого в своё время и отказались.

   ⚠️ ГЛАВНОЕ РЕШЕНИЕ: слой красит ТОЛЬКО ЛИНИИ, а плоский фон картинки не
   красит НИЧЕГО. Исходник (Images/wall_3.jpeg) — тёмно-серый холст со светлыми
   дудлами; положи его как есть, и он перекрасил бы всю страницу в серое, а на
   светлой теме дал бы грязь. Поэтому картинка нормализована при подготовке
   (фон уведён ровно в 0, медиана файла = 0), и дальше:
     • тёмная тема — `screen`: чёрный фон картинки нейтрален (screen с 0 не
       меняет ничего), светлые линии добавляют света;
     • светлая тема — `invert(1)` + `multiply`: после инверсии фон становится
       белым (нейтраль для multiply), а линии — тёмными и вычитают свет.
   Следствие, которое и нужно для контраста: СРЕДНЯЯ яркость страницы не
   меняется вовсе, меняются только пиксели под самими штрихами. Именно поэтому
   тут не понадобилось поднимать альфу панелей — то, чем 28.07 угробили 68
   проверок контраста в тёмной теме.

   Плитка, а не кадр 3840×2160: у исходника найден точный горизонтальный период
   1699px (остаточная ошибка 2.6 против 16.8 у случайного сдвига, шов 12.7 при
   внутренней разнице соседних столбцов 9.0 — то есть шва нет). Плитка ужата до
   1200px и лежит одним webp на 181 КБ вместо мегабайтов.

   `background-size` ФИКСИРОВАННЫЙ, не `cover` и не `auto 100%`: масштаб дудлов
   не должен зависеть от высоты окна, иначе на телефоне тот же паттерн стал бы
   вдвое мельче и зарябил бы именно там, где текст самый мелкий. */
.app::after {
  /* Обои включены обратно 02.08 по решению владельца. Отключал их я во время
     разбора зависания дашборда — они оказались НЕ ВИНОВАТЫ: петля была в
     наблюдателе капсулы в dashboard.js. Замеры влияния на контраст (худшее
     изменение −0.28 из 24 проб, ни одна не сменила класс WCAG) остаются в силе. */
  content: "";
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-image: url("/static/wall-pets.webp");
  background-repeat: repeat;
  background-size: 1160px auto;
  background-position: center top;
  /* Плотность паттерна — ЕДИНСТВЕННАЯ ручка приглушения (замер: см. отчёт).
     Линия в файле доходит до 255; при этой альфе её вклад в яркость экрана
     ≤ 0.10·255 ≈ 26 уровней в пике штриха и ~2 уровня в среднем по площади. */
  opacity: 0.10;
  mix-blend-mode: multiply;
  filter: invert(1);
}
:root[data-theme="dark"] .app::after {
  /* В тёмной теме инверсия не нужна: линии и так светлее фона. Альфа чуть выше
     — на тёмном подложка съедает штрих сильнее, чем на светлом. */
  filter: none;
  mix-blend-mode: screen;
  opacity: 0.13;
}
/* --- ВНЕШНИЙ КОНТУР ДАШБОРДА СКРУГЛЁН (владелец, 23.08) ---------------------
   «Углы дашборда сделать круглыми как у плитки». Радиус берётся из
   `--radius-tile` — того же токена, что носит `.glass`; второго числа в
   проекте нет.

   ⚠️ РАСКЛАДКА НЕ ТРОНУТА НИ НА ПИКСЕЛЬ, и это главное в решении. Скруглить
   по-настоящему пришлось бы либо клипом на предке (а сайдбар `fixed`, топбар
   `sticky` — оба из клипа выпадают или ломаются), либо отступом всего каркаса
   (тогда едут `margin-left` у `.main`, `top` у липкого топбара и ритм `.shell`).
   Вместо этого — НАКЛАДКА поверх всего: `fixed`, `inset: 0`, кликов не ловит,
   а срез угла рисует ВЫНОСНАЯ тень без размытия. Внешняя тень красит область
   ВНЕ скруглённого прямоугольника — то есть ровно четыре уголка между дугой и
   прямым углом окна, и ничего больше. Ни один узел страницы про неё не знает.

   z-index 70 — выше сайдбара (60), скрима (55) и топбара (40) и ниже ничего:
   больших слоёв в дашборде нет, панели живут внутри `.shell` (z-index 1) и в
   его стек-контексте заперты. Тень 40px хватает с запасом: срез при радиусе
   26px не глубже 8px.

   ⚠️ ОТКАТ — УДАЛИТЬ ЭТОТ БЛОК (узел в base.html можно оставить, он пустой и
   без стилей ничего не рисует). Владелец судит по живому виду и возвращает
   пробы — цена отката обязана быть одной правкой. */
.app-corners {
  position: fixed; inset: 0; z-index: 70;
  pointer-events: none;
  border-radius: var(--radius-tile);
  box-shadow: 0 0 0 40px var(--frame-canvas);
}

/* Блюр отключён системно → панели становятся непрозрачными, и обои видны только
   по краям страницы. Мешать они там не могут, но плотность всё равно снижаем:
   в этом режиме пользователь просил спокойный интерфейс, а не декор. */
@media (prefers-reduced-transparency: reduce) {
  .app::after { opacity: 0.06; }
  :root[data-theme="dark"] .app::after { opacity: 0.08; }
}

::selection { background: var(--color-lead); color: #fff; }

a { color: var(--color-lead-text); text-decoration: none; }
a:hover { color: var(--color-lead-press); text-decoration: underline; }

/* ============================================================
   3. Каркас: сайдбар + топбар + контент
   ============================================================ */

.side {
  position: fixed;
  z-index: 60;
  top: 0; left: 0; bottom: 0;
  width: var(--side-w);
  display: flex;
  flex-direction: column;
  padding: 12px 10px 14px;
  background: var(--glass-bg-chrome);
  backdrop-filter: blur(var(--glass-blur-chrome)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur-chrome)) saturate(var(--glass-sat));
  /* ⚠️ ЦВЕТА У РАМКИ НЕТ — вертикальную линейку рисует `.side::after` (ниже).
     Рамка оставлена ради ГАБАРИТА: её пиксель входит в ширину рельса, и
     `border: 0` увёл бы раскладку на 1px. Тот же приём, что при гашении
     линейки под курсором. */
  border-right: 1px solid transparent;
  box-shadow: inset 0 1px 0 var(--glass-specular);
  overflow: visible;
  transition: width var(--dur-spring-fast) var(--ease-spring-fast), box-shadow var(--dur-spring-fast) var(--ease-spring-fast);
}
/* ВЕРТИКАЛЬНАЯ ЛИНЕЙКА РЕЛЬСА — ОТДЕЛЬНЫМ УЗЛОМ, ЧТОБЫ НАЧИНАТЬСЯ НИЖЕ ШАПКИ.
   ⚠️ Владелец 23.08: «убрать вертикальную полосу, которая разделяет логотип и
   панель». Рамка `border-right` шла от самого верха, то есть на высоте топбара
   она разрезала надвое одну и ту же поверхность: слева знак-котик, справа
   шапка, материал у обоих ОДИН (`--glass-bg-chrome`). Ниже шапки та же линия
   осмысленна — там она граница рельса и рабочей области, поэтому убираем не
   всю, а её верхний отрезок.
   Отсюда `top: var(--topbar-h)` и `overflow: visible` у `.side` (при `hidden`
   псевдоэлемент обрезался бы по padding-боксу). */
.side::after {
  content: "";
  position: absolute;
  top: var(--topbar-h);
  right: -1px;              /* -1px = ровно место прозрачной рамки */
  bottom: 0;
  width: 1px;
  background: var(--glass-border);
  pointer-events: none;
}
/* Свёрнутый сайдбар выезжает НАД контентом (страница не перекладывается).
   ⚠️ В РАСКРЫТОМ ПО НАВЕДЕНИЮ СОСТОЯНИИ ПРАВОЙ ЛИНЕЙКИ НЕТ (владелец, 23.08:
   «когда я навожу мышкой на левое меню — убери полосу, которая разделяет и
   верхнюю панель»). `border-right` — одна вертикальная линия, но режет она
   две вещи сразу: отделяет меню от контента И перечёркивает топбар, который
   лежит под сайдбаром (z-index 60 против 40). Материал у меню и у топбара
   ОДИН (`--glass-bg-chrome`), поэтому без линии раскрытое меню и шапка
   читаются одной поверхностью — ровно этого и просили.
   Границу гасим ЦВЕТОМ, а не `border: 0`: ширина рамки держит габарит, иначе
   раскрытие поехало бы на 1px. Тень остаётся — она мягкая, полосой не
   читается и нужна, чтобы панель поверх контента не висела в воздухе.
   В покое (68px, страница не перекрыта) линейка на месте: там она граница
   рельса, а не шов. */
.side:hover,
.side:focus-within {
  width: var(--side-w-open);
  /* ⚠️ БЕЗ ТЕНИ (владелец 06.09: «когда меню выезжает, оно тоже должно быть
     цельное с панелью»). Тень отрывала раскрытое меню от шапки и показывала
     его отдельной плашкой; материал у них один, край гасится цветом, линия
     шапки продолжается через рельс (`.side::before`). */
  box-shadow: inset 0 1px 0 var(--glass-specular);
}
/* ⚠️ Гасим линейку ТОЛЬКО у НЕзакреплённого меню. Закреплённое раздвигает
   контент, его правый край — настоящая граница двух областей, а не шов поверх
   топбара; сняли бы и там — линия мигала бы под курсором на каждом заходе в
   меню. Скобка `.app:not(.pinned)` и есть вся разница между двумя режимами. */
/* ⚠️ Владелец 23.08: «вертикальная полоса так же не должна быть видна и при
   раскрытом меню» — то есть в ЛЮБОМ раскрытом состоянии, включая закреплённое.
   Прежняя редакция гасила линейку только у незакреплённого (там она шов поверх
   топбара), а у закреплённого считала настоящей границей двух областей.
   Решение владельца это отменяет: раскрытое меню и рабочая область разделяются
   материалом и тенью, линейка остаётся только у свёрнутого рельса. */
.side:hover::after,
.side:focus-within::after,
.app.pinned .side::after { background: transparent; }
/* Закреплённый — раздвигает контент (см. .app.pinned ниже), ховер ничего не меняет. */
.app.pinned .side { width: var(--side-w-open); }

.side-brand {
  display: flex; align-items: center; gap: 11px;
  min-height: 44px;
  padding: 0 8px;
  margin-bottom: 14px;
  border-radius: var(--radius-sm);
  color: var(--color-text);
  text-decoration: none;
  white-space: nowrap;
}
.side-brand:hover { text-decoration: none; color: var(--color-text); }
/* Плашка знака. Раньше здесь была буква «L» на mesh-градиенте; теперь кроп
   головы кота из фирменного логотипа. Фон тёмно-синий ВШИТ В КАРТИНКУ, поэтому
   плашка держит тот же цвет и в светлой теме: знак нарисован белой линией, на
   светлом фоне он исчез бы. Цвет взят пипеткой из исходника — rgb(11,5,36). */
/* Знак кладётся БЕЗ ПЛАШКИ — прозрачным PNG прямо на фон сайдбара, как обычная
   иконка. Тёмный квадрат-бейдж (он же вшитый в старый кроп фон) убран: на
   светлой теме он читался как пятно.
   Картинка — фоном, а не <img>: только так знак переключается по теме чистым
   каскадом. `data-theme` ставит инлайн-скрипт в <head> ДО первой отрисовки,
   поэтому вспышки не того знака нет, а тумблер меняет атрибут — и знак едет
   вместе с темой, без перезагрузки и без JS в этом месте.
   Знак не квадратный (180×159), поэтому `contain`, а не `cover`: `cover`
   срезал бы уши. */
.side-logo {
  flex: 0 0 auto;
  width: 30px; height: 30px;
  display: inline-block;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  /* Светлая тема — знак тёмным штрихом. Это же дефолт на случай, если атрибут
     темы не проставился вовсе (JS отключён): подложка тогда светлая. */
  background-image: -webkit-image-set(url("/static/logo-mark-light.png") 1x, url("/static/logo-mark-light@2x.png") 2x);
  background-image: image-set(url("/static/logo-mark-light.png") 1x, url("/static/logo-mark-light@2x.png") 2x);
}
:root[data-theme="dark"] .side-logo {
  background-image: -webkit-image-set(url("/static/logo-mark-dark.png") 1x, url("/static/logo-mark-dark@2x.png") 2x);
  background-image: image-set(url("/static/logo-mark-dark.png") 1x, url("/static/logo-mark-dark@2x.png") 2x);
}
/* Слово «Lidz» живёт только в развёрнутом состоянии — в свёрнутой полосе
   68px оно обрезалось по букве и читалось как артефакт. */
.side-word {
  font-weight: var(--fw-bold); font-size: var(--text-lg);
  letter-spacing: var(--tracking-snug);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--dur-2) var(--ease-out-quart), transform var(--dur-spring-fast) var(--ease-spring-fast);
}
.side:hover .side-word,
.side:focus-within .side-word,
.app.pinned .side-word { opacity: 1; transform: none; }
.side-word .lead   { color: var(--color-text); }
.side-word .hunter { color: var(--color-lead-text); }

.side-nav { display: flex; flex-direction: column; gap: 2px; min-height: 0; overflow-y: auto; overflow-x: hidden; }
.side-link {
  display: flex; align-items: center; gap: 12px;
  min-height: 44px;
  padding: 0 10px;
  border-radius: var(--radius-sm);
  color: var(--color-text-soft);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  text-decoration: none;
  border: 1px solid transparent;   /* резерв под рамку .active — иначе прыжок на 1px */
  transition: background var(--dur-1) var(--ease-out-quart), color var(--dur-1) var(--ease-out-quart);
}
/* ⚠️ МЕНЮ ОТВЕЧАЕТ ЦВЕТОМ, А НЕ СЕРОЙ ПЛАШКОЙ (владелец, 21.08: «когда вкладка
   активна — должна гореть иконка оранжевым, при наведении подсвечивается иконка
   и текст, выделение серым убираем»). Серая заливка под курсором и синяя
   плашка у активной спорили друг с другом: две разные подсветки на один
   элемент, и ни одна не читалась «вы здесь». Остался один сигнал — оранжевый,
   тот же акцент, что у выделений в тексте панелей. Второго тона не заводим.
   ⚠️ У АКТИВНОЙ ОСТАЁТСЯ И ВЕС (`--fw-strong`), а не только цвет: цветом одним
   информацию передавать нельзя — правило того же дашборда, из-за которого
   кружки статусов заменили фигурами. Плюс `aria-current="page"` в разметке. */
.side-link:hover {
  background: transparent; text-decoration: none;
  color: var(--nav-hi);
}
.side-link:hover .ic { color: var(--nav-hi); }
.side-link .ic {
  flex: 0 0 auto;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  color: inherit;
}
.side-link .ic svg { width: 19px; height: 19px; display: block; }
.side-link .lb {
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis;
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--dur-2) var(--ease-out-quart), transform var(--dur-spring-fast) var(--ease-spring-fast);
}
.side:hover .side-link .lb,
.side:focus-within .side-link .lb,
.app.pinned .side-link .lb { opacity: 1; transform: none; }
.side-link.active {
  background: transparent;
  border-color: transparent;
  color: var(--nav-hi);
  font-weight: var(--fw-strong);
}
.side-link.active .ic { color: var(--nav-hi); }

/* ⚠️ РАЗДЕЛИТЕЛЬ ЕСТЬ, НО ЕГО НЕ ВИДНО (владелец 06.09, уточнение: «разделитель
   должен быть, но его не должно быть видно»). Первая правка убирала узел
   целиком и вместе с ним отступ между группами — группы слипались. Габарит
   остаётся (1px + поля), цвет прозрачный. */
.side-sep {
  height: 1px;
  margin: 7px 10px;
  background: transparent;
  border: 0;
  flex: 0 0 auto;
}

.side-pin {
  margin-top: auto;
  display: flex; align-items: center; gap: 12px;
  min-height: 44px;
  padding: 0 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font: inherit; font-size: var(--text-s);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease-out-quart), color var(--dur-1) var(--ease-out-quart);
}
/* Кнопка закрепления живёт в том же меню — и отвечать обязана так же, иначе
   серая плашка осталась бы ровно в одном месте столбца. */
.side-pin:hover { background: transparent; color: var(--nav-hi); }
.side-pin:hover .ic { color: var(--nav-hi); }
.side-pin .ic { flex: 0 0 auto; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; }
.side-pin .ic svg { width: 18px; height: 18px; transition: transform var(--dur-spring-fast) var(--ease-spring-fast); }
.app.pinned .side-pin .ic svg { transform: rotate(180deg); }
.side-pin .lb { opacity: 0; transition: opacity var(--dur-2) var(--ease-out-quart); }
.side:hover .side-pin .lb,
.side:focus-within .side-pin .lb,
.app.pinned .side-pin .lb { opacity: 1; }

.side-scrim {
  position: fixed; inset: 0; z-index: 55;
  background: rgba(0, 0, 0, 0.38);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-spring-fast) var(--ease-out-quart);
}

.main {
  margin-left: var(--side-w);
  min-width: 0;
  transition: margin-left var(--dur-spring-fast) var(--ease-spring-fast);
}
.app.pinned .main { margin-left: var(--side-w-open); }

.topbar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 12px;
  min-height: var(--topbar-h);
  padding: 8px 22px;
  background: var(--glass-bg-chrome);
  backdrop-filter: blur(var(--glass-blur-chrome)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur-chrome)) saturate(var(--glass-sat));
  border-bottom: 1px solid var(--glass-border);
  box-shadow: inset 0 1px 0 var(--glass-specular);
}
.brand-tag {
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text);
  white-space: nowrap;
}
/* КАНАЛЫ СБОРА (три силуэта Telegram / ВКонтакте / Max) УБРАНЫ ИЗ ТОПБАРА
   23.08 по прямой просьбе владельца («убери иконки tg vk max»). Вместе с
   разметкой в base.html снят и весь класс `.chan*` — второго носителя у него
   не было (проверено grep'ом по dashboard/). Вернуть = три `<span>` и
   десять строк стилей; смысл индикатора описан в docs/dashboard.md. */

.topbar .health { margin-left: auto; }

/* Слот «последнее событие» (наполняет страница; сейчас — прогресс цепочки
   «Завести аккаунт» на «Чатах»). Раньше лог дописывался внутрь выпадающей
   панели и растягивал её — владелец забраковал 01.08.
   ⚠️ position:absolute внутри sticky-топбара выбран НЕ ради центровки, а ради
   гарантии: слот вне flex-потока и физически не может отобрать место у
   бургера, бейджа здоровья и тумблера темы. Пустой слот = [hidden] =
   display:none, то есть на страницах, которые его не наполняют, в топбаре не
   появляется вообще ничего.
   Строка ровно одна и не переносится: длинный текст обрезается многоточием,
   полный уходит в title (его ставит скрипт). Горизонтальный скролл невозможен
   по построению — ширина ограничена, overflow скрыт.
   Резерв 620px = удвоенная более широкая половина топбара (справа бейдж +
   тумблер + поля): слот центрирован, поэтому упирается в бóльшую из сторон. */
.topbar-live {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  max-width: min(46ch, calc(100% - 620px));
  padding: 5px 12px;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
  background: var(--fill-3);
  font-size: var(--text-xs); line-height: 1.3;
  color: var(--color-text-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;   /* декоративный статус: клики по топбару не ловит */
}
.topbar-live[hidden] { display: none !important; }
/* Финал и провал обязаны читаться по-разному — те же токены, что были у
   строки прогресса внутри панели. Новых цветов не заводим. */
.topbar-live.done { color: var(--color-success-text); }
.topbar-live.err  { color: var(--color-warning-text); }

.burger {
  display: none;
  align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-sm);
  background: var(--fill-3);
  color: var(--color-text);
  cursor: pointer;
}
.burger svg { width: 19px; height: 19px; }

/* Переключатель темы — сегмент из двух иконок (как Apple-контрол).
   ⚠️ ВЫСОТА РОВНО КАК У ПИЛЮЛИ АПТАЙМА (владелец, 23.08: «пилюлю с
   переключением режима светлой темы сделай по размеру с пилюлей аптайм»).
   Эталон — `.health`, и меняли ТОЛЬКО тумблер: было 38px против 30.72px у
   соседа, две пилюли в одном ряду разной высоты.
   Высота НЕ вбита числом, а СОБРАНА ИЗ ТЕХ ЖЕ СЛАГАЕМЫХ, что у `.health`:
   вертикальный отступ 5px + строка `1.5em` при `--text-xs` + рамка 1px.
   12.48·1.5 = 18.72 → 18.72 + 10 + 2 = 30.72px — совпадение до сотых, и оно
   переживёт правку типоразмера: сдвинется `--text-xs` — сдвинутся обе пилюли
   вместе. Числом ставить нельзя ровно по той же причине, по которой числом не
   ставится `--radius-field`.
   Глиф 14px — размер иконки в `.topbar-alarm`, третьей пилюле того же ряда;
   16px в кнопке высотой 18.72 просто не помещался. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  padding: 5px;
  gap: 2px;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
  background: var(--fill-3);
  font-size: var(--text-xs);   /* база для 1.5em ниже — типоразмер `.health` */
  line-height: 1.5;
}
.theme-toggle button {
  display: inline-flex; align-items: center; justify-content: center;
  /* ⚠️ `font-size` ОБЯЗАТЕЛЕН, хотя текста в кнопке нет: `1.5em` ниже считается
     от СВОЕГО кегля, а `button` наследует от родителя только семейство (см.
     правило `button, input, select` в §2) и сидит на дефолтных 13.33px. Без
     этой строки высота выходила 32px вместо 30.72 — на 1.28px выше эталона. */
  font-size: var(--text-xs);
  width: 30px; height: 1.5em;
  border: 0; border-radius: 999px;
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease-out-quart), color var(--dur-2) var(--ease-out-quart), box-shadow var(--dur-2) var(--ease-out-quart), transform var(--dur-2) var(--ease-pop);
}
.theme-toggle button svg { width: 14px; height: 14px; }
.theme-toggle button:hover { color: var(--color-text); }
.theme-toggle button[aria-pressed="true"] {
  background: var(--color-surface-solid);
  color: var(--color-lead-text);
  box-shadow: var(--shadow-sm);
}

.shell {
  max-width: 1320px;
  margin: 0 auto;
  padding: var(--sp-7) var(--sp-7) 80px;
  /* Поднимаем ВЕСЬ контент страницы над подложкой .app::before (z-index 0).
     Без этого непозиционированные куски (фильтры, футер, заголовки вне .glass)
     красились бы раньше подложки и оказались бы под цветной плёнкой. */
  position: relative;
  z-index: 1;
}

@media (max-width: 900px) {
  /* Мобила: сайдбар — оверлей, контент во всю ширину, страница не едет вбок. */
  .side {
    width: var(--side-w-open);
    transform: translateX(-100%);
    transition: transform var(--dur-spring-fast) var(--ease-spring-fast);
    box-shadow: var(--shadow-lg);
  }
  .side:hover, .side:focus-within, .app.pinned .side { width: var(--side-w-open); }
  .app.nav-open .side { transform: none; }
  .app.nav-open .side-scrim { opacity: 1; pointer-events: auto; }
  .side-link .lb, .side-pin .lb, .side-word { opacity: 1; transform: none; }
  .side-pin { display: none; }   /* закрепление бессмысленно у оверлея */
  .main, .app.pinned .main { margin-left: 0; }
  .burger { display: inline-flex; }
  /* Высота топбара на мобиле зафиксирована, а не «сколько получится»: тумблер
     темы с тач-таргетом 46px делал его выше токена (63 против 58), и его уход
     на время цепочки дёргал бы шапку на 5px. Теперь высота одна в обоих
     состояниях. */
  .topbar { padding: 8px 14px; min-height: 62px; }
  /* Мобила: центрировать абсолютом нечего — свободного места в топбаре нет,
     строка легла бы поверх бургера и бейджа. Поэтому слот возвращается в
     поток и забирает то, что освобождают двое соседей.
     Что уступает место (и почему именно оно): «Панель управления» — статичная
     подпись, на ≤420px её и так уже прячут ради статуса; текст бейджа
     здоровья — дублирует цвет точки, а точка и её подсказка остаются на
     месте. Прогресс цепочки — единственная НОВАЯ и временная информация на
     экране, ради неё эти двое и ужимаются, причём только пока цепочка идёт
     (класс снимается вместе с ней). Бургер и тумблер темы не трогаем: это
     управление, а не подписи. */
  .topbar-live {
    position: static; transform: none;
    flex: 1 1 auto; min-width: 0; max-width: none;
    text-align: center;
  }
  /* ⚠️ Соседям — flex: 0 0 auto, иначе растущий слот их ужимает. Замер поймал:
     бургер сплющивался 40 → 33px, то есть тач-таргет уходил ниже нормы. */
  .topbar .burger, .topbar .health, .topbar .theme-toggle { flex: 0 0 auto; }
  .topbar.has-live .brand-tag { display: none; }
  .topbar.has-live .health #health-text { display: none; }
  /* Тумблер темы уходит на время цепочки. Он единственный элемент топбара, без
     которого можно прожить две минуты ожидания: бургер — единственный путь к
     навигации, точка здоровья — единственный индикатор живости сбора. Без
     этих 94px слот на 375px оставался бы шириной в двадцать символов. */
  .topbar.has-live .theme-toggle { display: none; }
  .shell { padding: 20px 16px 56px; }
}
@media (max-width: 420px) {
  /* На самом узком экране подпись «Панель управления» уступает место статусу */
  .brand-tag { display: none; }
}

/* ============================================================
   4. Фильтры
   ============================================================ */

.filter-row {
  display: flex; align-items: center; gap: 12px;
  flex-wrap: wrap; row-gap: 10px;
  margin-bottom: 22px;
}
.section-label {
  font-size: var(--text-s);
  font-weight: var(--fw-strong);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text-soft);
}
/* Сегментный тумблер = ОДНА капсула-контейнер (эталон — пара иконок в тулбаре
   Claude): скруглённый бордюр вокруг всей группы, сегменты сидят ВНУТРИ. Гамма
   строго НЕЙТРАЛЬНАЯ, без синевы. Неактивный сегмент — прозрачный, текст
   --color-text-soft; активный — лёгкая светлая заливка --fill-2 + текст
   --color-text + жир (раздельно от трека даже в тёмной: fill-2 = более светлый
   слой на карточке). Hover неактивного — --fill-1 (фейнтее активного) + текст
   --color-text. Три различимых уровня заливки (transparent < fill-1 < fill-2),
   плюс жирность и цвет текста — активный отличим в обеих темах.
   Контраст (на стеклянной карточке): неактивный text-soft ≥8:1, активный
   --color-text над fill-2-слоем ≥12:1.
   ⚠️ На «Обзоре» капсула лежит ВНЕ стекла, на mesh-блобах — там прозрачный трек
   пропускает блоб под полупрозрачные сегменты и роняет контраст; ниже капсула
   получает ОПАКОВУЮ подложку (.filter-row .period-toggle). Сегменты переносятся:
   5-7 в ряд не влезают на мобильном (на «Лидах» капсула вместо переноса
   скроллится внутри себя — см. .leads-toolbar). */
.period-toggle {
  position: relative;               /* система координат скользящей капсулы */
  display: inline-flex;
  flex-wrap: wrap;
  max-width: 100%;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
}
/* Скользящая капсула активного сегмента. Узел вставляет dashboard.js
   (initToggles) и он же двигает её через inline transform/width — CSS отвечает
   только за материал и за переход. Пока JS не отработал, класс .has-ind не
   стоит и активный сегмент красится по-старому: без скрипта вид не ломается.
   Ширина/высота меняются переходом вместе с transform: элемент вне потока,
   собственный layout из одного узла — дешевле, чем scaleX, который на
   капсуле r=999px превратил бы скругление в эллипс. */
.pt-ind {
  position: absolute;
  left: 0; top: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  border-radius: 999px;
  background: var(--fill-2);
  /* ⚠️ ДЛИТЕЛЬНОСТЬ И КРИВАЯ СНЯТЫ С РЕФЕРЕНСА (видео переключателя папок
     Telegram, 02.08), а не подобраны на глаз. Ролик HEVC, Chromium его не
     декодирует — перегнан системным avconvert в H.264 и разложен на 329 кадров
     при 60 к/с; по кадрам отслежен центр капсулы. Переход «Личные» → «Новые»
     занимает ≈0.33–0.35 с, больше половины пути проходится за первую четверть
     времени, в конце — долгая доводка с едва заметным перелётом. Это профиль
     пружины, а не ease-out.
     Взята СВОЯ кривая, а не общая --ease-out-expo от раскрытий: у той перелёта
     нет вовсе, и движение читалось «механически доехало». Отличие ровно одно —
     контрольная точка чуть выше единицы. Перелёт намеренно МЕЛКИЙ (≈3%): на
     капсуле шириной в кнопку больший читается как дребезг, а не как пружина.
     РАСТЯЖЕНИЕ на ходу сделано РАЗНЫМИ длительностями transform и width:
     позиция приезжает за 330 мс, ширина догоняет за 380 мс, поэтому в полёте
     капсула деформируется и собирается уже на месте. Это дешевле и честнее,
     чем scaleX, который на r=999px превратил бы скругление в эллипс.
     prefers-reduced-motion гасится в JS (placeIndicator ставит transition:none),
     а не тут: переход должен исчезать вместе с решением «не анимируем». */
  transition: transform 330ms var(--ease-seg),
              width 380ms var(--ease-out-expo),
              height var(--dur-2) var(--ease-out-quart),
              opacity var(--dur-1) linear;
}
.period-toggle.has-ind button.active,
.period-toggle.has-ind button.active:hover { background: transparent; }
.period-toggle button {
  position: relative;               /* подпись над капсулой */
  z-index: 1;
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  min-height: 34px;
  padding: 6px 14px;
  font: inherit;
  font-size: var(--text-s);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-soft);
  border-radius: 999px;
  transition: background var(--dur-1) var(--ease-out-quart), color var(--dur-1) var(--ease-out-quart), border-color var(--dur-1) var(--ease-out-quart), box-shadow var(--dur-1) var(--ease-out-quart), transform var(--dur-2) var(--ease-pop);
}
.period-toggle button:hover {
  background: var(--fill-1);
  color: var(--color-text);
}
.period-toggle button.active,
.period-toggle button.active:hover {
  background: var(--fill-2);
  color: var(--color-text);
  font-weight: var(--fw-semibold);
}
@media (pointer: coarse) {
  .period-toggle button { min-height: 40px; }
}

/* «Обзор»: капсула лежит ВНЕ стекла, прямо на mesh-блобах. Прозрачный трек
   пропустил бы блоб под полупрозрачные сегменты (fill-1/fill-2) и уронил бы
   контраст подписи (< 4.5:1, зависит от блоба). Осознанный компромисс: здесь
   ОПАКОВА сама КАПСУЛА (--color-surface-solid) — блоб не просвечивает, а сегменты
   рендерятся над детерминированным опаковым фоном, не над блобом. Токены сегментов
   те же (fill-1/fill-2), поэтому вид совпадает с остальными тумблерами.
   Замер (композит блоба не нужен — трек опаков): неактивный --color-text-soft
   над surface-solid = 9.0:1 (светлая) / 9.1:1 (тёмная); активный --color-text
   над fill-2+surface = 14:1 (светлая) / 11:1 (тёмная) — все ≥4.5:1, активный
   сегмент (fill-2) над белым/тёмным треком чётко различим.
   Селектор .filter-row .period-toggle встречается только в _filters.html
   («Обзор»); «Лиды» используют .lf-row, «Клиенты» — #add-toggle/.add-head-ctl. */
.filter-row .period-toggle {
  background: var(--color-surface-solid);
  border-color: var(--color-rule-strong);
}

.filters { display: flex; flex-direction: column; gap: 12px; margin-bottom: 22px; }
.filters .filter-row { margin-bottom: 0; }
.filter-key { flex-shrink: 0; min-width: 78px; }
@media (max-width: 560px) {
  .filters .filter-row { align-items: flex-start; flex-direction: column; gap: 6px; }
}

/* ============================================================
   5. Стекло (карточки)
   ============================================================ */

/* Многослойный материал: заливка + blur+saturate + светлый sheen сверху
   (блик на скруглении) + светлая кромка + широкая мягкая тень.

   Sheen — фоновым слоем, а НЕ ::before: исторически у .kpi (это .glass-strong)
   псевдоэлемент был занят акцентной полоской. Полоску убрали 29.07, но sheen
   намеренно оставлен фоном — так он не конкурирует ни с одним ::before у
   потомков .glass* и не требует переписывания при добавлении новых.

   backdrop-filter здесь и только здесь — на строках таблиц, плитках и вложенных
   карточках блюра нет никогда: десятки слоёв стоили бы кадров, тем более под
   SSE-перерисовкой списков на /business и /leads. */
.glass,
.glass-strong {
  position: relative;
  border-radius: var(--radius-tile);
  padding: var(--sp-6);
  background-image: var(--glass-sheen);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}
.glass {
  background-color: var(--glass-bg);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-specular);
}
.glass-strong {
  background-color: var(--glass-bg-strong);
  border: 1px solid var(--glass-edge-hi);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-specular);
}

/* --- СТЕКЛО БЕЗ БЛЮРА, ПОД ЖИВОЕ СОДЕРЖИМОЕ -------------------------------
   ⚠️ СЕКЦИЯ, ВНУТРИ КОТОРОЙ ЧТО-ТО ДВИЖЕТСЯ НЕПРЕРЫВНО, СТЕКЛОМ БЫТЬ НЕ
   МОЖЕТ. Причина та же, из-за которой у `.leads-main` снят блюр (§ниже), но
   упор другой — не в потолок текстуры, а в бюджет кадра.

   `backdrop-filter` пересчитывается при КАЖДОЙ перерисовке элемента. Пока
   внутри статика, это разовая цена. Как только внутри едет точка по схеме,
   блюр считается 60 раз в секунду — и цена умножается на площадь подложки.

   Замер 30.08 (Chromium 1440x900, кадры rAF за 3 с, страница /how с её
   анимированной схемой пайплайна): секции стеклом — 25 fps, те же секции без
   `backdrop-filter` — 59 fps. Больше вдвое, при неотличимом на глаз виде:
   под секцией лежат mesh-блобы, размывать там нечего, кроме плавных пятен.

   Материал — рецепт `.leads-main`: сплошное дно `--color-surface-solid`, слой
   колера `--glass-bg` сверху, sheen закреплён в пикселях (иначе на высокой
   секции блик растягивается на треть её высоты). Кромка, тень и радиус
   остаются от `.glass` — снаружи по-прежнему цветная подложка.

   ⚠️ НЕ ВЕШАТЬ НА ВСЁ ПОДРЯД. Обычная секция обязана оставаться стеклом: это
   айдентика Lidz. Модификатор — только там, где внутри непрерывное движение
   (анимированные схемы) или очень длинный переcобираемый список. */
.glass.glass-live {
  background-color: var(--color-surface-solid);
  background-image: var(--glass-sheen),
                    linear-gradient(var(--glass-bg), var(--glass-bg));
  background-repeat: no-repeat;
  background-size: 100% 160px, auto;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Строки-пилюли — ОПЦИЯ, а не поведение всех таблиц: включается классом
   table.lh.rows-pill. На коротких таблицах (6–8 строк) это «контролы внутри
   панели» с референса; на «Лидах»/«Чатах» с десятками строк 6px зазора между
   каждой парой строк превращают список в рябь, поэтому там обычные линейки.
   border-collapse: separate обязателен — при collapse ячейки не берут радиус. */
table.lh.rows-pill { border-collapse: separate; border-spacing: 0 6px; }
table.lh.rows-pill thead th { padding-bottom: 8px; }
table.lh.rows-pill tbody td {
  border-bottom: 0;
  padding: 12px 13px;
  background: var(--glass-row);
  box-shadow: inset 0 1px 0 var(--glass-row-edge);
  transition: background var(--dur-1) var(--ease-out-quart);
}
table.lh.rows-pill tbody td:first-child {
  border-radius: 13px 0 0 13px;
  box-shadow: inset 0 1px 0 var(--glass-row-edge), inset 1px 0 0 var(--glass-row-edge);
}
table.lh.rows-pill tbody td:last-child {
  border-radius: 0 13px 13px 0;
  box-shadow: inset 0 1px 0 var(--glass-row-edge), inset -1px 0 0 var(--glass-row-edge);
}
/* ⚠️ ПИЛЮЛЯ ПОД КУРСОРОМ СВЕТЛЕЕТ (23.08). До этого дно ячейки не менялось
   вовсе: метку несла ОРАНЖЕВАЯ окраска текста, общая для всех таблиц. С 23.08
   выделение — предельный нейтраль (`--hi`), а текст пилюльной строки в покое
   уже `--color-text`, то есть цветом сказать нечего: на «Операциях» строка
   осталась бы без единого признака наведения (замер до правки: дно строки
   `rgba(0,0,0,0)` и в покое, и под курсором — заливки у `rows-pill` нет по
   правилу выше).
   Носитель взят СВОЙ ЖЕ материал на ступень ярче — `--glass-row-hi`
   (белый .44 → .80 в светлой, .055 → .115 в тёмной). Это не то «серое
   выделение», от которого владелец отказался 03.08: серый лист поверх строки
   там давал `--fill-*`, а здесь БЕЛЕЕТ САМА ПИЛЮЛЯ — ровно тот же смысл, что у
   просьбы «выделения белые».
   Красится `td`, а не `tr`: у пилюли дно живёт на ячейках (скругления и кромки
   тоже), фон на строке под ними не виден. */
table.lh.rows-pill tbody tr:hover { background: transparent; }
table.lh.rows-pill tbody tr:hover > td { background: var(--glass-row-hi); }
/* Строка-заглушка «загрузка…»/«ничего нет» пилюлей не притворяется — и под
   курсором тоже: у правила выше специфичность (0,2,4) против (0,2,3) здесь,
   поэтому сброс для неё выписан отдельной строкой той же силы. */
table.lh.rows-pill tbody td[colspan] { background: transparent; box-shadow: none; }
table.lh.rows-pill tbody tr:hover > td[colspan] { background: transparent; }

/* Кому системно мешает прозрачность — отдаём плотные поверхности и гасим
   цветную подложку. Работает на всех страницах: правила глобальные. */
@media (prefers-reduced-transparency: reduce) {
  .app::before { display: none; }
  .glass, .glass-strong, .side, .topbar {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background-image: none;
    background-color: var(--color-surface-solid);
  }
  .glass, .glass-strong {
    border-color: var(--color-rule);
    box-shadow: var(--shadow-md);
  }
  table.lh.rows-pill tbody td { background: var(--color-surface-strong); }
  /* Фолбэк без прозрачности не возвращает ни синеву, ни серую подсветку строки:
     выделение и здесь несёт только цвет текста. */
  table.lh.rows-pill tbody tr:hover td { background: var(--color-surface-strong); }
}

.grid-kpi {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--sp-4);
  margin-bottom: var(--sp-6);
}
.kpi {
  position: relative;
  padding: 19px 20px 17px;
  overflow: hidden;
  transition: box-shadow var(--dur-3) var(--ease-out-quart), transform var(--dur-3) var(--ease-out-quart);
}
/* Акцент-полоски сверху плитки нет (убрана 29.07, решение Дениса): цветной
   тик по верхней кромке читался как лишний сигнал и спорил с цветом самой
   цифры. Псевдоэлемент ::before у .kpi СВОБОДЕН — цвет метрики кодируют
   классы .kpi-value.accent/.warn/.danger/.ok, они остаются. */
.kpi:hover { box-shadow: var(--shadow-pop), inset 0 1px 0 var(--glass-specular); transform: translateY(-2px); }
.kpi-label {
  display: block;
  font-size: var(--text-s);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text-soft);
  margin-bottom: 7px;
}
.kpi-value {
  display: block;
  font-size: var(--text-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
.kpi-sub {
  margin-top: 7px;
  font-size: var(--text-xs);
  color: var(--color-text-soft);
}
/* ⚠️ `.accent` / `.warn` / `.ok` ВЕДУТ В ОДИН НЕЙТРАЛЬ (владелец, 23.08).
   Это были синий, оранжевый и зелёный на числах, которые НИЧЕГО не сообщают:
   «Лидов», «Спам отсеяно», «Сделок закрыто» — нормальная работа системы, а
   три разных цвета рядом читались как три разные тревоги. Классы оставлены в
   разметке намеренно: они называют роль числа, и возврат цвета — одна строка
   на роль (`--kpi-tint` объявлен в §1).
   ⚠️ `.danger` НЕ ТРОГАЕМ: единственное место — «дыры покрытия» на карте, там
   красный означает «этих заявок никто не получит». Это сигнал, а не украшение. */
.kpi-value.accent { color: var(--kpi-tint); }
.kpi-value.warn   { color: var(--kpi-tint); }
.kpi-value.danger { color: var(--color-hunter); }
.kpi-value.ok     { color: var(--kpi-tint); }
.kpi-value.mute   { color: var(--color-text-soft); }
/* ⚠️ ГРУППЫ «ОБЗОРА» — ОТДЕЛЬНЫЕ КЛАССЫ, А НЕ ПЕРЕКРАШЕННЫЕ `.accent`/`.warn`
   (23.08). `.accent` и `.warn` живут ещё на шести страницах («Как работает»,
   «Цель», «Цены», «Конкуренты», «Карта», `/iam`), а палитра пока не выкачена
   дальше «Обзора» — вернуть им цвет здесь значило бы раскрасить всё сразу.
   Имя класса называет СМЫСЛОВУЮ ГРУППУ («поток» / «запас» / «деньги»), а не
   краску: перекрашивается группа в §1 одной строкой, разметка не трогается. */
.kpi-value.g-flow  { color: var(--data-flow-deep); }
.kpi-value.g-stock { color: var(--data-stock-deep); }
.kpi-value.g-money { color: var(--data-money-deep); }

.grid-2 {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: var(--sp-5);
  margin-bottom: var(--sp-6);
}
/* У grid-элемента min-width по умолчанию = auto, поэтому canvas Chart.js
   и широкие таблицы не давали колонке сжиматься и растягивали страницу. */
.grid-2 > *, .grid-3 > *, .grid-kpi > * { min-width: 0; }
.table-wrap { min-width: 0; }
@media (max-width: 900px) {
  .grid-2 { grid-template-columns: 1fr; }
}
.grid-3 {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--sp-5);
}
@media (max-width: 1100px) {
  .grid-3 { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 700px) {
  .grid-3 { grid-template-columns: 1fr; }
}

.card-title {
  position: relative;
  font-size: var(--text-md);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  margin: 0 0 var(--sp-4);
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--sp-1) 12px;
}
/* Заголовок секции: контекстная иконка + текст, обёрнутые в левый .ct-h —
   так justify-content:space-between оставляет card-hint/кнопки-пилюли справа.
   Иконка приглушённая (currentColor от --color-text-soft), читаема в обеих темах. */
.ct-h {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.ct-ic {
  flex: none;
  width: 17px;
  height: 17px;
  color: var(--color-text-soft);
}
.card-hint {
  font-size: var(--text-2xs);
  color: var(--color-muted);
  font-weight: var(--fw-regular);
  letter-spacing: 0;
}

/* ============================================================
   6. Таблицы  (без backdrop-filter — блюр на сотнях строк дорог)
   ============================================================ */

.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

table.lh {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
table.lh th,
table.lh td {
  text-align: left;
  padding: 10px 11px;
  border-bottom: 1px solid var(--color-rule);
}
table.lh thead th {
  border-bottom: 1px solid var(--color-rule-strong);
}
/* ТАБЛИЦА БЕЗ ЛИНЕЕК (`table.lh.bare`) — модификатор, а не правка базы.
   Владелец, 08.08, двумя просьбами подряд: «убери горизонтальные полосы во
   вкладке Аккаунты, которые отделяют название столбцов от аккаунтов» и «вкладка
   Чаты — убрать горизонтальные полосы в двух верхних плитках. На плитке „Чаты“
   оставь».

   ⚠️ КОГДА ВЕШАТЬ КЛАСС — ЭТО ПРАВИЛО, А НЕ СПИСОК ИМЁН БЛОКОВ. Линейки нужны
   там, где строк МНОГО и глаз ведёт их по горизонтали: «Чаты» (десятки строк с
   числами в семи колонках), «Заявки», «Исходы». Там линия — рабочий инструмент,
   а не украшение, и снимать её нельзя. Линейки лишние там, где строк ЕДИНИЦЫ и
   каждая — высокий блок с аватаром и подстроками: «Аккаунты» (3–5 строк по
   64px), агенты поиска, города. Такие строки разделяет воздух, а полоса между
   двумя блоками читается рамкой.
   Порог словами: **до десятка строк, и строка выше одной текстовой** — вешаем
   `bare`; **список, который листают** — не вешаем. Следующая правка, которая
   захочет «унифицировать», пусть сначала посчитает строки.
   ⚠️⚠️ ПРОПИСНЫХ ЗДЕСЬ НЕТ И БЫТЬ НЕ ДОЛЖНО (владелец, 08.08, через час после
   правки про линию). Дословно: «АККАУНТ СТРАНА ПРОКСИ В РАБОТЕ С… — такие
   названия столбцов некрасивые. Сделай вида „Аккаунт“, „Страна“». Капс жил
   ровно в этом правиле (в разметке подписи всегда были нормальные) и был
   заведён как компенсация снятой линии — компенсация оказалась хуже болезни.
   Капса нет НИ У ОДНОЙ шапки дашборда: базовое `table.lh th` его никогда не
   имело, и заводить его отдельным правилом на странице тоже нельзя.

   ⚠️ ЧЕМ ТОГДА ДЕРЖИТСЯ «ШАПОЧНОСТЬ», когда сняты И линия, И прописные.
   Осталось четыре признака, и они работают вместе:
     · КЕГЛЬ — `--text-2xs` (11.2px) против `--text-sm` данных: шапка почти
       вдвое мельче строки, это видно раньше, чем прочитано слово;
     · ВЕС — `--fw-strong` при обычном весе данных;
     · КОЛЕР и ВЕС — `--color-text-soft` + `--fw-bold`. ⚠️ С 08.08 это живёт
       в БАЗОВОМ `table.lh th` (см. выше), а не здесь: владелец сказал «в чатах
       в таблице всё-таки не хватает выделения» про таблицу, у которой линия
       ОСТАЛАСЬ, — значит слабой была шапка вообще, а не только у `bare`.
       Держать усиление в модификаторе значило бы завести две шапки на одном
       дашборде. Замер: светлая #4A4A55 = 8.3:1, тёмная #B9B9C6 = 9.2:1;
     · ВОЗДУХ ВНИЗ — группировка близостью: шапка отбита от первой строки не
       меньше, чем строки друг от друга. На «Аккаунтах» это 14px шапки + 13px
       строки = 27 против 26 между строками (у строк там свой ритм по id
       секции).
   Разрядка оставлена минимальной (0.02em): на строчных буквах она работает
   как «подпись», а не как капитель, и слов не растягивает.
   ⚠️ ОТБИВКУ ДАЁТ ШАПКА, А НЕ ПЕРВАЯ СТРОКА. Правило вида
   `table.lh.bare tbody tr:first-child > td { padding-top }` (0,3,4) молча
   проигрывает страничному `#sec-accounts .wk-tbl tbody td` (1,1,3) — то есть
   было бы мёртвым ровно на той странице, ради которой пишется. Проверено
   замером: `padding-top` первой строки так и остаётся 13px.
   ⚠️ ВЕРТИКАЛЬНЫЙ РИТМ СТРОК (13px вместо 10) — часть этого же модификатора, а
   не свойство «Аккаунтов». Без линейки взгляд ищет границу по пустоте, и
   прежний зазор читается теснее, чем есть. Числа пришли с «Аккаунтов», где
   были подобраны замером 03.08; страничная копия оттуда убрана, чтобы два
   источника одного вида не разъехались. */
table.lh.bare thead th {
  border-bottom: 0;
  padding-bottom: 14px;
  letter-spacing: 0.02em;
}
table.lh.bare tbody td {
  border-bottom: 0;
  padding-top: 13px;
  padding-bottom: 13px;
}
/* ШАПКА — ОДНО ОПРЕДЕЛЕНИЕ НА ВСЕ ТАБЛИЦЫ ДАШБОРДА.
   ⚠️ УСИЛЕНА 08.08 (владелец, после снятия линии на «Аккаунтах», капса и
   оранжевого маркера: «в чатах в таблице всё-таки не хватает выделения»).
   Было `--color-muted` + `--fw-strong`, стало `--color-text-soft` +
   `--fw-bold` — то есть подпись столбца отличается от данных не только
   кеглем, но и весом с плотностью колера. Замер по токенам: светлая #4A4A55
   = 8.3:1 к подложке (было 5.1:1 у muted), тёмная #B9B9C6 = 9.2:1 (было 6.1).
   ⚠️ Приглушать шапку обратно НЕЛЬЗЯ: серый, который «достаточно виден» на
   светлом фоне, в тёмной теме уходит первым, а линии под шапкой на половине
   таблиц больше нет — держать её стало нечем, кроме типографики.
   ⚠️ Прописных здесь нет и не будет — см. `table.lh.bare` ниже. */
table.lh th {
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text-soft);
  font-weight: var(--fw-bold);
  white-space: nowrap;
}
table.lh tbody tr {
  transition: background var(--dur-1) var(--ease-out-quart);
}
/* Цвет теперь несёт выделение — значит и переход по цвету, иначе строка
   вспыхивает рывком, а соседние (у которых курсор уже ушёл) гаснут плавно. */
table.lh tbody td {
  transition: color var(--dur-1) var(--ease-out-quart);
}
table.lh tbody tr:last-child td { border-bottom: 0; }
/* ВЫДЕЛЕНИЕ СТРОКИ ПОД КУРСОРОМ — ТЁПЛЫЙ ТЕКСТ ПЛЮС ЛЁГКАЯ ЗАЛИВКА.
   История места в три хода, и каждый — слово владельца:
     31.07 — снята синева `--tint-accent` (монохром);
     03.08 — «убрать серое выделение, при наведении сделать оранжевый»: заливка
             снята ЦЕЛИКОМ, остался тёплый текст плюс оранжевый маркер левого
             края (2px внутри первой ячейки);
     08.08 — маркер убран по прямой просьбе («убери оранжевую вертикальную
             полосу слева»), и в тот же час: «в чатах в таблице всё-таки не
             хватает выделения».
   ⚠️ ЗАЛИВКА ВЕРНУЛАСЬ ОСОЗНАННО И НЕ ТА, ЧТО ЗАБРАКОВАЛИ. Тогда строка
   заливалась `--fill-1` (0.10 / 0.24) и это читалось «серым выделением».
   Здесь `--fill-3` — САМАЯ ЛЁГКАЯ ступень шкалы (0.06 светлая / 0.16 тёмная),
   вдвое слабее забракованной, и она не одна: главным носителем остаётся
   тёплый текст. Без заливки строка стала немой — по строкам «Чатов» и
   «Аккаунтов» кликают, и человек обязан видеть, на какой он стоит.
   ⚠️ Оранжевую вертикальную полосу сюда НЕ возвращать: её убрали отдельной
   просьбой того же дня. Отклик даёт горизонтальная подсветка всей строки.
   ⚠️ С 23.08 СТРОКА ПОД КУРСОРОМ НЕ ТЁПЛАЯ, А ПРЕДЕЛЬНО НЕЙТРАЛЬНАЯ (владелец:
   «все выделения в дашборде должны быть как в меню — белые»). Токен тот же —
   `--color-accent-text`, он теперь равен `--hi`. Цена решения записана честно:
   ячейка в покое уже `--color-text`, поэтому в СВЕТЛОЙ теме цвет обычной
   ячейки под курсором не меняется вовсе, и вес отклика перешёл на заливку
   `--fill-3` плюс подтяжку приглушённых ячеек (`.mono`, `small`, `--color-muted`)
   до чернильного. В тёмной теме ячейка ещё и белеет (#F2F2F7 → #FFF).
   Понадобится вернуть тёплую строку — это одна строка в §1
   (`--color-accent-text: var(--color-warning-text)`), а не правка здесь.
   ⚠️ Специфичность у правила текста намеренно завышена (`:root` + `> td`):
   иначе утилитарные классы ячеек (`td.mono`, `td.num` и прочие с собственным
   `color`) остались бы приглушёнными, и строка красилась бы наполовину.
   Пилюли и ссылки внутри ячейки свой цвет сохраняют — у них он смысловой. */
table.lh tbody tr:hover {
  background: var(--fill-3);
}
:root table.lh tbody tr:hover > td {
  color: var(--color-accent-text);
}
/* Строка-заглушка («ничего нет», «загрузка…», пустое состояние во всю ширину)
   строкой данных не является: выделять в ней нечего, и подсветка на ней
   читалась как «нашлась запись». Признак — `colspan`. Гасим ОБА носителя:
   до 08.08 гасился только цвет, потому что заливки не было вовсе. */
:root table.lh tbody tr:hover > td[colspan] { color: inherit; }
:root table.lh tbody tr:has(> td[colspan]):hover { background: transparent; }
/* Заголовок числовой колонки выравнивается так же, как её значения. */
table.lh th.num,
table.lh td.num {
  text-align: right;
}
table.lh td.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
table.lh td.nw { white-space: nowrap; }
table.lh td.mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-soft);
}

/* --- СОРТИРУЕМАЯ ШАПКА СТОЛБЦА (владелец, 26.08: «во вкладке Чаты мы должны
   сортировать каждый столбец — город, участники и так далее — по убыванию или
   возрастанию») ---------------------------------------------------------
   Компонент ОБЩИЙ, а не страничный: сортируемая шапка — свойство таблицы, и
   вторая её копия на следующей странице разъехалась бы с этой (ровно так в
   дашборде появились девять ширин всплывающей панели, см. docs/dashboard.md).
   Носит его `#tbl-chats`; кто повесит `th.sortable` дальше — получит тот же вид.

   ⚠️ ПОЛЕ ЯЧЕЙКИ ПЕРЕЕЗЖАЕТ НА КНОПКУ, а не дублируется. Иначе мишень равна
   длине слова («Город» — 38px при высоте строки 11px), то есть промахнуться
   легче, чем попасть. С переносом кликается вся ячейка шапки целиком.
   ⚠️ ПОДПИСЬ ОСТАЁТСЯ ПОДПИСЬЮ: кнопка наследует кегль, вес, колер и разрядку
   `table.lh th` (`font: inherit; color: inherit`) — шапка не должна начать
   выглядеть рядом кнопок. Признак «сюда можно нажать» даёт курсор и стрелка,
   проявляющаяся под курсором.
   ⚠️ ВЫДЕЛЕНИЕ АКТИВНОГО СТОЛБЦА — НЕЙТРАЛЬНОЕ (`--color-accent-text`, он же
   `--hi`), как все выделения дашборда с 23.08. Цвет тут не смысл, а «где я
   сейчас», поэтому красить его семантическим токеном нельзя. */
table.lh th.sortable { padding: 0; }
table.lh th.sortable > .th-s {
  display: flex;
  align-items: center;
  gap: 5px;
  width: 100%;
  padding: 10px 11px;
  margin: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out-quart);
}
/* Числовая шапка прижата вправо к своим значениям — стрелка идёт за подписью
   и уходит к самому краю, чтобы столбик цифр под ней не съезжал. */
table.lh th.num.sortable > .th-s { justify-content: flex-end; }
table.lh.bare th.sortable > .th-s { padding-bottom: 14px; }
table.lh th.sortable > .th-s:hover { color: var(--color-text); }
table.lh th.sortable > .th-s:focus-visible {
  outline: none;
  border-radius: var(--radius-sm);
  box-shadow: var(--ring-focus-edge);
}
/* Стрелка. В покое её нет вовсе — девять серых стрелок в шапке читаются как
   украшение; под курсором она проявляется вполсилы («этот столбец можно
   сортировать»), у активного столбца горит целиком. */
.th-arr {
  flex: none;
  width: 11px; height: 11px;
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease-out-quart),
              transform var(--dur-spring-fast) var(--ease-spring-fast);
}
table.lh th.sortable > .th-s:hover .th-arr { opacity: .45; }
table.lh th[aria-sort="ascending"] > .th-s,
table.lh th[aria-sort="descending"] > .th-s { color: var(--color-accent-text); }
table.lh th[aria-sort="ascending"] .th-arr,
table.lh th[aria-sort="descending"] .th-arr { opacity: 1; }
/* Шеврон нарисован «вниз» = по убыванию (большие сверху); по возрастанию он
   разворачивается — то же движение, что у складывающихся заголовков. */
table.lh th[aria-sort="ascending"] .th-arr { transform: rotate(180deg); }
/* Палец крупнее курсора: на тач-экране шапка тянется до тач-минимума. */
@media (pointer: coarse) {
  table.lh th.sortable > .th-s { min-height: 44px; }
}

/* --- ТАБЛИЦА НА УЗКОЙ ПОДЛОЖКЕ: СТРОКА СТАНОВИТСЯ КАРТОЧКОЙ ----------------
   Модификатор `table.lh.stack` + класс `.tbl-host` на ближайшем предке, по
   ШИРИНЕ КОТОРОГО принимается решение.
   ⚠️ ПОРОГ СЧИТАЕТ КОНТЕЙНЕР, А НЕ ОКНО, и это не украшательство. У `.leads`
   такая же карточная раскладка сидит на `@media (max-width: 900px)` — там
   таблица занимает всю страницу, и ширина окна про неё говорит правду. Здесь
   таблица лежит ВНУТРИ карточки внутри колонки: на окне 1440 подложка 1136px,
   на 768 — уже 628, на 375 — 235. Медиа-запрос про это не знает вовсе, и
   единственный честный источник ширины — сам контейнер.
   ⚠️ Подписи столбцов берутся из `data-th` у `<td>`, а не дублируются в
   разметке второй раз: шапка в этом режиме уводится от глаза, но остаётся для
   скринридера (clip-path, а не `display: none`), и две копии подписи назавтра
   разъехались бы. Ячейка БЕЗ `data-th` (длинный текст заявки) занимает всю
   ширину карточки без подписи — ей подпись и не нужна, она главная.
   Горизонтальной прокрутки в этом режиме нет по построению: строк-ячеек в ряд
   не остаётся вовсе. */
.tbl-host { container-type: inline-size; }
@container (max-width: 900px) {
  table.lh.stack,
  table.lh.stack tbody,
  table.lh.stack tr { display: block; }
  table.lh.stack thead {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  table.lh.stack tbody tr {
    padding: 11px 12px;
    margin-bottom: 9px;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--fill-3);
  }
  table.lh.stack tbody tr:last-child { margin-bottom: 0; }
  table.lh.stack tbody tr:hover { background: var(--fill-3); }
  table.lh.stack td {
    display: grid;
    grid-template-columns: minmax(0, 84px) minmax(0, 1fr);
    gap: 2px 10px;
    align-items: baseline;
    width: auto; min-width: 0; max-width: none;
    padding: 3px 0;
    border-bottom: 0;
    text-align: left;
    white-space: normal;
  }
  /* Подпись ячейки — тем же набором признаков, что и шапка широкой таблицы:
     мельче, приглушённее, без прописных (владелец, 08.08: «названия столбцов
     некрасивые, сделай вида „Аккаунт“, „Страна“» — правило одно на обе
     раскладки, иначе одна и та же подпись выглядела бы по-разному до и после
     порога). Значения `data-th` пишутся в разметке уже с заглавной. */
  table.lh.stack td::before {
    content: attr(data-th);
    font-size: var(--text-3xs);
    color: var(--color-muted);
    letter-spacing: 0.02em;
  }
  /* ⚠️ ВСЕ ДЕТИ ЯЧЕЙКИ — ВО ВТОРУЮ КОЛОНКУ, И ЭТО НЕ КОСМЕТИКА. В ячейке
     бывает не один узел (пилюля исхода + причина отказа под ней), а
     автоматическая раскладка грида разложила бы второй узел ПОД ПОДПИСЬЮ, в
     первую колонку — подпись «исход» оказалась бы над причиной отказа.
     Явная колонка ставит их столбиком справа, друг под другом. */
  table.lh.stack td > * { grid-column: 2; }
  /* Главная ячейка строки — без подписи и во всю ширину карточки. */
  table.lh.stack td:not([data-th]) { display: block; padding-top: 7px; }
  table.lh.stack td:not([data-th])::before { content: none; }
}
/* Совсем узко (карточка клиента на 375px даёт подложку 235px): подпись встаёт
   НАД значением — на две колонки места нет, значение ужималось до столбика. */
@container (max-width: 430px) {
  table.lh.stack td { grid-template-columns: minmax(0, 1fr); }
  /* Колонка снова одна — иначе `grid-column: 2` выше завёл бы вторую,
     НЕЯВНУЮ колонку, и значения уехали бы за подпись. */
  table.lh.stack td > * { grid-column: 1; }
}
.pill {
  display: inline-block;
  font-size: var(--text-3xs);
  font-weight: var(--fw-semibold);
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--tint-accent-hi);
  color: var(--color-lead-text);
  letter-spacing: 0.005em;
  text-transform: lowercase;
  box-shadow: inset 0 0 0 1px var(--tint-accent-hi);
  white-space: nowrap;
}
.pill.warm   { background: rgba(199, 38, 29, 0.13); color: var(--color-hunter-hi);    box-shadow: inset 0 0 0 1px rgba(199, 38, 29, 0.20); }
.pill.mute   { background: var(--fill-2);           color: var(--color-text-soft);    box-shadow: inset 0 0 0 1px var(--color-rule); }
.pill.ok     { background: rgba(36, 138, 61, 0.15); color: var(--color-success-text); box-shadow: inset 0 0 0 1px rgba(36, 138, 61, 0.22); }
.pill.warn   { background: rgba(178, 80, 0, 0.15);  color: var(--color-warning-text); box-shadow: inset 0 0 0 1px rgba(178, 80, 0, 0.22); }
.pill.crit   { background: rgba(211, 0, 10, 0.14);  color: var(--color-danger);       box-shadow: inset 0 0 0 1px rgba(211, 0, 10, 0.22); }
:root[data-theme="dark"] .pill.warm { background: rgba(255, 105, 97, 0.16); box-shadow: inset 0 0 0 1px rgba(255, 105, 97, 0.24); }
:root[data-theme="dark"] .pill.ok   { background: rgba(48, 209, 88, 0.16);  box-shadow: inset 0 0 0 1px rgba(48, 209, 88, 0.24); }
:root[data-theme="dark"] .pill.warn { background: rgba(255, 159, 10, 0.16); box-shadow: inset 0 0 0 1px rgba(255, 159, 10, 0.24); }
:root[data-theme="dark"] .pill.crit { background: rgba(255, 69, 58, 0.16);  color: #FF8A80; box-shadow: inset 0 0 0 1px rgba(255, 69, 58, 0.26); }

/* мини прогресс-бар (квота клиента) */
.bar-mini {
  position: relative;
  height: 6px;
  width: 100%;
  min-width: 90px;
  background: var(--fill-2);
  border-radius: 999px;
  overflow: hidden;
}
.bar-mini > i {
  position: absolute;
  inset: 0 auto 0 0;
  background: var(--grad-signal);
  border-radius: 999px;
}
.bar-mini > i.free { background: linear-gradient(90deg, var(--color-mesh-lavender), var(--color-mesh-purple)); }

/* ============================================================
   7. Формы
   ============================================================ */
.form-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  align-items: flex-end;
}
.form-row label {
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-size: var(--text-3xs);
  color: var(--color-muted);
  font-weight: var(--fw-medium);
}
.form-row input,
.form-row select {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  color: var(--color-text);
  background: var(--color-surface-solid);
  border: 1px solid var(--color-rule-strong);
  /* Радиус — из токена, а не числом (см. `--radius-field`): здесь стояло 10px,
     то есть базовые поля дашборда были заметно «квадратнее» эталонной формы
     клиента с её капсулой. */
  border-radius: var(--radius-field);
  padding: 9px 11px;
  min-height: 40px;
  outline: none;
  transition: border-color var(--dur-2) var(--ease-out-quart), box-shadow var(--dur-2) var(--ease-out-quart);
}
/* ⚠️ ПЯТАЯ КОПИЯ «ВЫДЕЛЕНО», И ОНА БЫЛА САМОЙ СИНЕЙ: здесь стоял голый
   `--color-lead` + гало. Переведена на общий рецепт полей 08.08. */
.form-row input:focus,
.form-row select:focus {
  border-color: var(--focus-edge);
  box-shadow: var(--ring-focus-edge);
}
.form-row input[type="number"] { width: 96px; }
.form-row input#f-username { width: 160px; }
.form-row > .btn-primary { margin-left: auto; }
@media (max-width: 480px) {
  .form-row > .btn-primary { margin-left: 0; }
}
/* ============================================================
   Кнопки-пилюли — единая ФОРМА для всех ТЕКСТОВЫХ кнопок.
   Эталон — .acc-new-btn (chats.html): иконка + подпись, скруглённая
   «таблетка», прозрачный фон, 1px рамка. Кнопка НЕЙТРАЛЬНА в покое,
   цвет/подсветка появляются ТОЛЬКО при взаимодействии — решение
   владельца «с иконкой и без цвета, пока не выделишь». Отдельной
   ACCENT-заливки нет: главное действие тоже нейтрально в покое.
   Единственный цветной модификатор — .danger, и тот КРАСНЕЕТ только
   на hover/focus (деструктив читается при наведении, но не давит).
   Икоки-действия (.row-*, .pw-eye, .add-open) сюда НЕ входят — у них
   своя круглая форма.
   ============================================================ */
.btn-primary,
.btn-ghost,
.btn-mini,
.acc-new-btn,
.flow-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font-family: var(--font-sans); font-weight: var(--fw-semibold); line-height: 1;
  color: var(--color-text-soft);
  background: transparent;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  /* Волна от точки клика живёт отдельным узлом внутри кнопки — её надо обрезать
     по форме пилюли. box-shadow (фокус-кольцо) overflow не режет: он рисуется
     вне бокса, а не потомком. */
  position: relative;
  overflow: hidden;
  /* Нажатие: сжатие вниз и УПРУГИЙ возврат (--ease-pop за 100%). Отдельная
     длительность у transform — цвет доводится быстрее, чем геометрия. */
  transform: scale(var(--press, 1));
  transition: background var(--dur-2) var(--ease-out-quart), border-color var(--dur-2) var(--ease-out-quart), color var(--dur-2) var(--ease-out-quart), box-shadow var(--dur-2) var(--ease-out-quart), transform var(--dur-2) var(--ease-pop);
}
/* Волна: круг, отмасштабированный из точки нажатия. Только прозрачность и
   transform, никакого цвета — монохром кнопок (31.07) не нарушается. */
.lh-rip {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  background: currentColor;
  opacity: 0;
}
/* Иконка слева от подписи — 15px, как у эталонной .acc-new-btn. Правило
   размера общее, чтобы переиспользуемые SVG-константы (TRASH/GIFT/CHEV без
   собственных width/height) в текстовых кнопках были ровно 15px. */
.btn-primary svg, .btn-ghost svg, .btn-mini svg, .acc-new-btn svg, .flow-btn svg {
  flex: none; width: 15px; height: 15px;
}
.btn-primary:hover,
.btn-ghost:hover,
.btn-mini:hover,
.acc-new-btn:hover,
.flow-btn:hover { background: var(--fill-1); border-color: var(--color-rule-strong); color: var(--color-text); }
.btn-primary:active,
.btn-ghost:active,
.btn-mini:active,
.acc-new-btn:active,
.flow-btn:active { --press: 0.955; }
.btn-primary:focus-visible,
.btn-ghost:focus-visible,
.btn-mini:focus-visible,
.acc-new-btn:focus-visible,
.flow-btn:focus-visible { outline: none; box-shadow: var(--ring-focus); }
.btn-primary:disabled,
.btn-ghost:disabled,
.btn-mini:disabled,
.flow-btn:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-ghost[hidden] { display: none; }

/* Размерные тиры: форма одна, плотность разная. */
.btn-primary { font-size: var(--text-base);  padding: 10px 22px; min-height: 44px; }
.btn-ghost   { font-size: var(--text-sm); padding: 9px 20px;  min-height: 44px; }
.btn-mini    { font-size: var(--text-xs); padding: 7px 16px;  min-height: 38px; }
.acc-new-btn { font-size: var(--text-s); padding: 7px 15px;  min-height: 38px; }
.flow-btn    { font-size: var(--text-sm); padding: 8px 16px;  min-height: 40px; }
@media (pointer: coarse) { .btn-mini, .acc-new-btn, .flow-btn { min-height: 44px; } }

/* КЛИКАБЕЛЬНЫЙ ЗАГОЛОВОК секции-витрины (правило владельца 02.08: длинная
   таблица-витрина показывает три строки, остальное раскрывается; правка 02.08:
   отдельной иконочной кнопки больше нет — раскрывает сам заголовок).
   Это НАСТОЯЩИЙ <button> внутри <h3>/<h4>, обёрнутый вокруг иконки, текста,
   счётчика и шеврона: клавиатура, роль и Enter/Space достаются даром, а
   role="button"+tabindex не нужны. Ставится вместе с `.ct-h`, поэтому
   space-between в .card-title по-прежнему уводит пилюли/подсказки вправо.
   Отрицательные margin компенсируют паддинг — базовая линия заголовка не
   съезжает относительно соседних секций.
   Шеврон доворачивается от data-open, а не от aria-expanded, потому что тот же
   атрибут ставится и из пути отрисовки (кнопка самовосстанавливается после
   SSE) — стрелка и подсказка не могут разъехаться. Всё это ставит LH.foldHead. */
.fold-h {
  appearance: none; -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 5px 9px;
  margin: -5px -9px;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-align: left;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease-out-expo),
              color var(--dur-1) var(--ease-out-expo);
}
.fold-h:hover { background: var(--fill-1); color: var(--color-text); }
.fold-h:hover .ct-ic,
.fold-h:hover .fold-chev { color: var(--color-text); }
.fold-h:focus-visible { outline: none; box-shadow: var(--ring-focus); }
/* Складывать нечего (строк меньше лимита) — заголовок остаётся заголовком:
   не в фокусе, без курсора, без шеврона и счётчика. */
.fold-h[disabled] { cursor: default; background: none; opacity: 1; }
.fold-h[disabled] .fold-chev,
.fold-h[disabled] .fold-count { display: none; }
.fold-count { flex: none; }
.fold-chev {
  flex: none; width: 15px; height: 15px;
  color: var(--color-text-soft);
  transition: transform var(--dur-spring-fast) var(--ease-spring-fast);
}
.fold-h[data-open="1"] .fold-chev { transform: rotate(180deg); }
@media (pointer: coarse) { .fold-h { min-height: 44px; } }
@media (max-width: 480px) { .btn-primary { width: 100%; } }

/* Модификатор DANGER — деструктив. В ПОКОЕ нейтрален (как все прочие), но на
   hover/focus КРАСНЕЕТ: красная рамка + text-safe красный текст на лёгкой
   красной подложке. Так «без цвета, пока не тронешь» соблюдено, а «Удалить»
   при наведении не безлико и не путается с нейтральной «Отменой». Перебивает
   базовый сине-акцентный hover (`.btn-mini:hover`) большей специфичностью. */
.btn-mini.danger:hover,
.btn-mini.danger:focus-visible {
  color: var(--color-hunter-hi);
  background: rgba(199, 38, 29, 0.12);
  border-color: rgba(199, 38, 29, 0.55);
}
:root[data-theme="dark"] .btn-mini.danger:hover,
:root[data-theme="dark"] .btn-mini.danger:focus-visible {
  color: var(--color-hunter-hi);
  background: rgba(255, 105, 97, 0.16);
  border-color: rgba(255, 105, 97, 0.60);
}
.chats-area {
  width: 100%;
  box-sizing: border-box;
  min-height: 110px;
  resize: vertical;
  /* ⚠️ МОНОШИРИННЫЙ ЗДЕСЬ ФУНКЦИОНАЛЕН, А НЕ ДЕКОРАТИВЕН, и остаётся
     единственным таким полем на дашборде: сюда ВСТАВЛЯЮТ машинный текст —
     слаги вида `t.me/zhk_salarevo_1`, где `l`, `1` и `I` надо различать
     глазом. Кегль при этом приведён к общему полю ввода (`--text-sm`, как у
     `.tg-ta` и `.bc-text`): разный шрифт у поля — это смысл, разный кегль —
     это разнобой. */
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-text);
  background: var(--color-surface-solid);
  /* Рамка, радиус и фокус — образца формы клиента (§19): поле лежит в `.tg-field`
     рядом с `.ibtn-fill`, то есть в том же компоненте, что композер переписки, и
     обязано быть из того же материала. Стояли `--color-rule-strong`, радиус 12px
     и СИНИЙ фокус `--color-lead` (синеву из органов управления убрали 31.07). */
  border: 1px solid var(--color-rule);
  /* ⚠️ ТОКЕН, А НЕ 20px ЧИСЛОМ. Правило 08.08 «радиус поля числом больше не
     ставить» это поле пропустило: значение совпадало, но следующая правка
     токена его бы не догнала. */
  border-radius: var(--radius-field);
  padding: 12px 14px;
  outline: none;
  transition: border-color var(--dur-2) var(--ease-out-quart), box-shadow var(--dur-2) var(--ease-out-quart);
}
/* Выделение — общий рецепт полей (`--focus-edge` + `--ring-focus-edge`, §1).
   Своей копии тут стояло `--color-rule-strong` + синее гало — четвёртый вариант
   «выделено» на том же дашборде. */
.chats-area:focus {
  border-color: var(--focus-edge);
  box-shadow: var(--ring-focus-edge);
}
/* iOS Safari зумит страницу при фокусе в поле со шрифтом <16px и не отъезжает. */
@media (max-width: 480px) {
  .chats-area,
  .form-row input,
  .form-row select { font-size: 16px; }
}
.form-msg { margin-top: 12px; font-size: var(--text-sm); min-height: 1.2em; }
.form-msg.ok  { color: var(--color-success-text); }
.form-msg.err { color: var(--color-hunter-hi); }

/* ⚠️ ГЕОМЕТРИИ У `.row-*` ЗДЕСЬ БОЛЬШЕ НЕТ (07.08, владелец: «пусть приведёт
   всё к единому стилю, который мы сейчас соблюдаем»). Стоял ВТОРОЙ полный
   набор: `width/height: 30px`, `border: 1px solid var(--color-rule)`,
   `border-radius: 9px` (скруглённый квадрат вместо круга), `color:
   var(--color-muted)`, свой `transform`/`transition` и `svg { width: 15px }`.
   Работал он вхолостую — `.ibtn` идёт ниже по файлу и всё это перебивал, — но
   именно так и заводится расхождение: правишь `--ib-size`, а на «Клиентах»
   ничего не меняется, потому что оттуда кнопку держит забытая копия.
   ⚠️ ПРОВЕРЕНО ПЕРЕД УДАЛЕНИЕМ: в шаблонах нет ни одного элемента с классом
   `.row-del/.row-trial/.row-edit/.row-out/.row-pause` БЕЗ `.ibtn` рядом
   (`grep 'class="[^"]*row-…"' | grep -v ibtn` → пусто). Сами классы остаются
   якорями для делегированных слушателей страницы, вида у них нет и быть не
   должно. */
/* ⚠️ ЦВЕТА ПОД КУРСОРОМ У ЭТИХ ПЯТИ КНОПОК УБРАНЫ (07.08). Здесь стояли синяя
   таблетка с синей рамкой (`.row-edit/.row-trial/.row-out`), янтарная
   (`.row-pause`), зелёная (`.row-pause.on`) и красная (`.row-del`) — каждая со
   своим `background` + `border-color`, плюс две темовые копии. Это был
   ДОИКОНОЧНЫЙ вид (до 03.08 кнопки были в кольцах), и он пережил переезд на
   `.ibtn`: общее правило `:root .ibtn:hover` задавало только цвет глифа, а дно
   и рамку не обнуляло — они и проступали. Замер 07.08, «Клиенты», светлая тема:
     .row-edit  покой bg rgba(0,0,0,0) → ховер bg rgba(0,122,255,0.10), рамка 1px rgb(0,122,255)
     .row-pause покой bg rgba(0,0,0,0) → ховер bg rgba(178,80,0,0.10),  рамка 1px rgba(178,80,0,0.45)
   Теперь вид этих кнопок целиком в `.ibtn` (см. «ТЕЛЕГРАМ-МОДЕЛЬ» ниже по
   файлу): ховер — только глиф ярче, нажатие — серая зона. Смысл действия несут
   рисунок и позиция, а красный остался у деструктива через `.ibtn-danger`.
   Классы `.row-*` с разметки НЕ сняты: на них висят делегированные слушатели
   страницы. (Второй держатель, `MAGNET_SEL` в `dashboard.js`, снят 07.08 вместе
   с магнитом — иконка под курсором больше не едет.) Отсюда правило: страничного
   цвета у иконки быть не должно вовсе, иначе он снова обойдёт общий класс.
   Состояние `.on` (пауза включена, исходы раскрыты) рисует `:root .ibtn.on`. */
/* Недоступное действие (статус done ставит бот) — видно, но не кликается.
   Гашение цветом ведёт `.ibtn[disabled]`; здесь остался только курсор. */
.row-del[disabled], .row-trial[disabled], .row-edit[disabled], .row-out[disabled], .row-pause[disabled] {
  cursor: not-allowed;
}

/* На тач-устройствах управляющие элементы 44×44 (Apple HIG / WCAG 2.5.5) */
@media (pointer: coarse) {
  .row-del, .row-trial, .row-edit, .row-out, .row-pause { width: 44px; height: 44px; }
  .row-del svg, .row-trial svg, .row-edit svg, .row-out svg, .row-pause svg { width: 18px; height: 18px; }
  .period-toggle button { min-height: 44px; padding: 6px 14px; }
  .side-link, .side-pin { min-height: 46px; }
  /* Высоту НЕ переопределяем: тумблер обязан совпадать с пилюлей аптайма и на
     узком экране (правило владельца 23.08), а `.health` на мобиле не растёт.
     Тач-запас добираем ШИРИНОЙ — 42px против 30 на десктопе. */
  .theme-toggle button { width: 42px; }
}

/* ============================================================
   8. Доступность
   ============================================================ */
:where(a, button, input, select, textarea):focus-visible {
  outline: 2px solid var(--color-lead-text);
  outline-offset: 2px;
  border-radius: 8px;
}
/* prefers-reduced-motion: ПРУЖИНУ УЖИМАЕМ, ПЕРЕХОД НЕ ГАСИМ.
   Так предписывает HIG (Accessibility): «tightening animation springs to reduce
   bounce effects» — то есть убрать перелёт и укоротить, а не выключить движение.
   Мгновенный скачок состояния читается резче, чем короткий переход, и хуже
   объясняет, что изменилось.

   Что делаем:
     1. все пружинные кривые и оба «доводчика за 100%» (--ease-pop, --ease-seg)
        съезжают на --ease-spring-tight — bounce = 0, перелёта нет;
     2. длительности ужимаются до 140 мс потолком.
   Что по-прежнему гасим полностью: @keyframes-анимации (пульс точек статуса,
   блик скелетона, каскад появления) — это декор, повторяющийся сам по себе,
   и HIG требует убирать именно его. Задержки каскада сбрасываются отдельным
   блоком в конце файла. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --ease-spring-fast:   var(--ease-spring-tight);
    --ease-spring-frame:  var(--ease-spring-tight);
    --ease-spring-snappy: var(--ease-spring-tight);
    --ease-pop:           var(--ease-spring-tight);
    --ease-seg:           var(--ease-spring-tight);
    --dur-spring-fast:  140ms;
    --dur-spring-frame: 140ms;
    --dur-spring-snappy: 140ms;
  }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 140ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================
   9. Графики
   ============================================================ */

.chart-wrap {
  position: relative;
  height: 280px;
}
.chart-wrap.tall { height: 360px; }

/* Заглушка поверх канваса: при нулевых данных Chart.js рисует пустоту,
   которую не отличить от несработавшего графика. */
.chart-empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  text-align: center;
  padding: 0 16px;
  color: var(--color-text-soft);
  font-size: var(--text-sm);
}
.chart-empty[hidden] { display: none; }
.chart-empty .hint { font-size: var(--text-xs); color: var(--color-muted); }

/* ============================================================
   10. Лента
   ============================================================ */

.feed-item {
  padding: 14px 0;
  border-bottom: 1px dashed var(--color-rule);
}
.feed-item:last-child { border-bottom: 0; }
.feed-meta {
  font-size: var(--text-2xs);
  color: var(--color-muted);
  margin-bottom: 4px;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.feed-meta .who { color: var(--color-lead-text); font-weight: var(--fw-medium); }
.feed-text {
  color: var(--color-text);
  line-height: 1.5;
  font-size: var(--text-base);
  overflow-wrap: anywhere;
}
/* break-word, а не anywhere: anywhere обнуляет min-content ширину ячейки,
   колонка схлопывается и текст встаёт в столбик по одной букве. */
table.lh td.wrap {
  overflow-wrap: break-word;
  white-space: normal;
  min-width: 240px;
  max-width: 420px;
}
.clamp {
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ============================================================
   11. Health-бейдж и футер
   ============================================================ */

.health {
  display: inline-flex; align-items: center; gap: 8px;
  min-width: 0;
  font-size: var(--text-xs); color: var(--color-text-soft);
  padding: 5px 12px 5px 10px;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
  background: var(--fill-3);
  white-space: nowrap;
}
.dot {
  width: 8px; height: 8px; border-radius: 50%;
  flex: 0 0 auto;
  background: var(--color-success);
  box-shadow: 0 0 0 3px rgba(36, 138, 61, 0.20);
}
/* ⚠️ ВЕЧНОГО ПУЛЬСА У ТОЧКИ БОЛЬШЕ НЕТ, И ВЕРНУТЬ ЕГО НЕЛЬЗЯ (30.08).
   Здесь стояло `animation: pulse 2s ease-in-out infinite`, и эти 8 пикселей
   стоили дашборду 85% бюджета кадра.

   Механизм. `backdrop-filter` Chrome считает так: снимает подложку элемента в
   текстуру и размывает её ЗАНОВО при любой перерисовке. Точка меняет opacity
   60 раз в секунду — и 60 раз в секунду пересчитываются ВСЕ стеклянные слои
   страницы: топбар, рельс и каждая `.glass`-секция. Цена зависит от того, что
   лежит под стеклом, поэтому дефект виден не везде.

   Замер 30.08 (Chromium 1440x900, headless, кадры считаны rAF за 3 с):
     /how   (документ 9489px, 31 SVG, 5 стеклянных секций)   7-9 fps
     /niches (короткая страница, то же стекло, та же точка)   59 fps
   Снять `animation` с одной этой точки — и /how становится 60 fps.
   Проверено и отвергнуто, НЕ пробовать заново: `will-change: opacity` (9 fps),
   `will-change`+`translateZ(0)` (8), `transform: scale` вместо opacity (8),
   `contain: strict` на бейдже (8), перенос бейджа из топбара в body (8),
   `position: fixed` (9). Ребёнка `backdrop-filter`-предка Chrome в свой слой
   не выносит — спасает не композитинг, а отсутствие вечной перерисовки.
   Снятие блюра со всех слоёв даёт лишь 34 fps: блюр — усилитель, а не причина.

   ИТОГ ПО /how (обе правки — эта и `.glass-live` выше). Замер A/B в одной
   вкладке, четыре круга: сначала как сейчас, потом тем же кадром возвращён
   вечный пульс и блюр секций — то есть машина и её загрузка одинаковые.
       БЫЛО  9, 9, 10, 9      fps  (медиана 9)
       СТАЛО 41, 43, 58, 61   fps  (медиана 50.5)
   ⚠️ РАЗБРОС «стало» — ЭТО НОРМА, А НЕ НЕДОДЕЛКА: снизу лежит сама машина
   (headless Chromium делит процессор с флотом, load average 2.2 в момент
   замера), а не наша перерисовка. Отсюда и разница с первым замером на
   спокойном сервере, где выходило ровное 60. «Было» же держится на девяти при
   любой загрузке — там потолок ставит принудительная перерисовка, и машине
   уже нечего добавить. Если однажды увидите здесь снова 9 при спокойной
   машине — вернулась вечная анимация, ищите её, а не оптимизируйте схему.

   Взамен пульс стал СОБЫТИЕМ: класс `.beat` вешает `refreshHealth` в
   `base.html` на каждый удачный ответ `/api/health`. Одно биение на реальные
   данные вместо ста двадцати на таймер — сигнал стал честнее (он про приход
   ответа, а не про то, что страница открыта), а перерисовок стало 1 на цикл
   обновления вместо 60 в секунду. */
.dot.beat { animation: pulse 620ms ease-out 1; }
.dot.stale { background: var(--color-warning); box-shadow: 0 0 0 3px rgba(178, 80, 0, 0.20); }
.dot.dead  { background: var(--color-danger);  box-shadow: 0 0 0 3px rgba(211, 0, 10, 0.22); animation: none; }
@keyframes pulse {
  0%   { opacity: 1;    transform: scale(1); }
  40%  { opacity: 0.45; transform: scale(0.68); }
  100% { opacity: 1;    transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .dot.beat { animation: none; }
}
@media (max-width: 560px) {
  #health-text { display: none; }   /* остаётся точка + title — бейдж не давит топбар */
  .health { padding: 5px 9px; }
}

/* ЖАЛОБЫ НА ПЕРВОЕ КАСАНИЕ («просил не писать») за сутки — бейдж в топбаре.
   Формой повторяет .health (пилюля, та же высота и типоразмер), чтобы соседи
   читались одним рядом, но это ссылка на карточку «Первое касание».
   Три состояния:
     • нет жалоб  → [hidden], в топбаре ни пикселя (ставит скрипт);
     • 1–2        → монохром, как весь дашборд;
     • ≥3 (data-hot="1") → предупреждение. Красный здесь ФУНКЦИОНАЛЬНЫЙ сигнал
       «тормозить рассылку», тот же класс исключения из монохрома, что у
       .btn-mini.danger. Мигания нет намеренно: порог и так виден цифрой. */
.alarm-slot { display: contents; }
.topbar-alarm {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--text-xs); font-weight: var(--fw-semibold);
  color: var(--color-text-soft);
  text-decoration: none;
  padding: 5px 12px 5px 10px;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
  background: var(--fill-3);
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease-out-expo),
              border-color var(--dur-1) var(--ease-out-expo),
              color var(--dur-1) var(--ease-out-expo);
}
.topbar-alarm[hidden] { display: none; }
.topbar-alarm .ic { width: 14px; height: 14px; flex: none; }
.topbar-alarm .n { font-variant-numeric: tabular-nums; color: var(--color-text); }
.topbar-alarm:hover { background: var(--fill-1); border-color: var(--color-rule-strong); color: var(--color-text); }
.topbar-alarm:focus-visible { outline: none; box-shadow: var(--ring-focus); }
.topbar-alarm[data-hot="1"] {
  color: var(--color-hunter-hi);
  border-color: rgba(199, 38, 29, 0.55);
  background: rgba(199, 38, 29, 0.12);
}
.topbar-alarm[data-hot="1"] .n { color: var(--color-hunter-hi); }
:root[data-theme="dark"] .topbar-alarm[data-hot="1"] {
  border-color: rgba(255, 105, 97, 0.45);
  background: rgba(255, 105, 97, 0.16);
}
/* Узко — остаются иконка и цифра, слово уходит в title (тот же приём, что у
   #health-text). Тач-таргет добираем высотой, а не подписью. */
@media (max-width: 1100px) {
  .topbar-alarm .lb { display: none; }
  .topbar-alarm { padding: 5px 10px; }
}
/* 38px, а не 44: это тир ТОПБАРА (тумблер темы там тоже 42×38) — топбар высотой
   58px, и 44 его распирают. Ссылка при этом дублирующая: та же цифра открыта на
   /chats через сайдбар, тач-таргет которого 46px. */
@media (pointer: coarse) { .topbar-alarm { min-height: 38px; } }

.footer-mini {
  margin-top: 36px;
  font-size: var(--text-3xs);
  /* Футер лежит ВНЕ стекла, прямо на цветной подложке: с --color-muted замер
     дал 3.96:1 над нижним блобом (провал AA). --color-text-soft там 6.3:1. */
  color: var(--color-text-soft);
  text-align: center;
  letter-spacing: 0.04em;
  display: flex; align-items: center; justify-content: center;
  /* Кусков стало четыре (появились версия и дата сборки) — на 375px строка
     обязана переноситься, а не выпирать за экран. */
  flex-wrap: wrap; gap: 4px 10px;
}
/* Разделитель рисует CSS, а не отдельный <span>·</span>: свой «·» при переносе
   уезжал на новую строку и вставал сиротой в её начале. */
.footer-mini .fm-i { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.footer-mini .fm-i + .fm-i::before { content: "·"; opacity: 0.5; }
/* ⚠️ ВЕРСИЯ ИЗ ПОДВАЛА УБРАНА 22.08 (решение владельца: «версию убрать вообще,
   оставить только сборку»). Вместе с ней ушло правило `.fm-ver` и — что важнее
   — прежнее скрытие даты сборки на узком экране: оно стояло ровно потому, что
   ЧЕТЫРЕ куска не помещались в 343px, и убирало как раз тот кусок, который
   владелец просил оставить.
   Замер трёх оставшихся кусков на 375px: доступно 343, нужно 354.3 — не хватает
   11px, и «обновлено» переносилось на вторую строку с «·»-сиротой в начале.
   Поэтому на узком экране слегка поджимаются разрядка и зазор: информации не
   теряется ни куска, а строка снова одна (замер после правки — 338.7 из 343).
   Скрывать что-либо здесь больше НЕ НАДО: следующий кусок в подвале сначала
   меряем, потом добавляем. */
@media (max-width: 480px) {
  .footer-mini { letter-spacing: 0.02em; gap: 4px 8px; }
}
.refresh-spinner {
  width: 10px; height: 10px;
  border-radius: 50%;
  border: 1.5px solid var(--color-dim);
  border-top-color: var(--color-lead-deep);
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease-out-quart);
}
.refresh-spinner.active {
  opacity: 1;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ============================================================
   12. «Как это работает»: блок-схема пайплайна
   ============================================================ */

.flow-toolbar {
  display: flex; align-items: center; gap: 14px;
  flex-wrap: wrap; row-gap: 10px;
  margin-bottom: 16px;
}
/* Кнопка стоит вплотную к капсуле `.period-toggle`, а выглядела иначе: стеклянная
   заливка, прямоугольные углы и СИНИЙ бордюр на hover. Приведена к общему
   монохрому кнопок (эталон 31.07) — прозрачная пилюля, нейтральная подсветка.
   Ширина 92px оставлена: подпись переключается «Пауза»/«Пуск», без неё кнопка
   прыгала бы и утаскивала за собой тумблер скорости. */
/* `.flow-btn` — НЕ шестая реализация кнопки, а модификатор канонической формы
   (правило выше объявляет её вместе с .btn-primary/.btn-ghost/.btn-mini/
   .acc-new-btn). До 02.08 здесь лежал дословный повтор рецепта со своим
   min-height и БЕЗ правила `pointer: coarse` — то есть канон сам себе
   противоречил на соседних тридцати строках. Осталось ровно то, чем эта
   кнопка отличается: ширина, чтобы соседние кнопки схемы были одинаковыми. */
.flow-btn { min-width: 92px; }
.flow-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-left: auto; }
.flow-legend .lg {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--text-xs); color: var(--color-text-soft);
}
.dot-flow {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--color-text-soft); flex: none;
}
.dot-flow.hero { width: 13px; height: 13px; background: var(--color-lead-deep); }
.dot-flow.good { background: var(--color-success); }
.dot-flow.ghost {
  background: transparent; box-shadow: inset 0 0 0 1.5px var(--color-muted);
}

/* Схема шире мобильного экрана — скроллим её саму, а не страницу. */
.flow-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }
#flow-svg { width: 100%; min-width: 1080px; height: auto; display: block; }

.flow-link { stroke: var(--color-rule-strong); stroke-width: 1.5; }
.flow-link.drop { stroke-dasharray: 4 5; }

.flow-node rect {
  fill: var(--color-surface-strong);
  stroke: var(--color-rule-strong); stroke-width: 1;
  transition: stroke var(--dur-3) var(--ease-out-quart), fill var(--dur-3) var(--ease-out-quart);
}
.flow-node .n-title {
  font-size: 14px; font-weight: var(--fw-strong); fill: var(--color-text);
  text-anchor: middle; letter-spacing: -0.01em;
}
.flow-node .n-sub { font-size: 11px; fill: var(--color-muted); text-anchor: middle; }
.flow-node.idle rect { stroke-dasharray: 5 4; }
.flow-node.idle .n-title { fill: var(--color-muted); }
.flow-node.active rect { stroke: var(--color-lead-deep); stroke-width: 2; fill: var(--color-surface-solid); }
.flow-node.hit rect { stroke: var(--color-success); stroke-width: 2; }

.flow-bin rect { fill: var(--fill-1); stroke: var(--color-rule); stroke-width: 1; }
.flow-bin .bin-label { font-size: 11px; fill: var(--color-muted); text-anchor: middle; }
.flow-bin .bin-count {
  font-size: 13px; font-weight: var(--fw-strong); fill: var(--color-text-soft); text-anchor: middle;
  font-variant-numeric: tabular-nums;
}
.flow-bin.hit rect { fill: rgba(199, 38, 29, 0.12); stroke: var(--color-hunter); }

.flow-node.gate rect { fill: var(--fill-3); }
.flow-ghost .ghost-lane {
  stroke: var(--color-rule-strong); stroke-width: 1.5; stroke-dasharray: 3 6;
}
.flow-ghost .ghost-gate {
  stroke: var(--color-muted); stroke-width: 3; stroke-linecap: round;
}
.flow-ghost .ghost-label { font-size: 12.5px; fill: var(--color-muted); }

#flow-svg .dot { fill: var(--color-text-soft); opacity: 0.55; }
#flow-svg .dot.ghost { fill: none; stroke: var(--color-muted); stroke-width: 1.5; opacity: 0.55; }
#flow-svg .dot.good { fill: var(--color-success); opacity: 0.9; }
#flow-svg .dot.hero {
  fill: var(--color-lead-deep); opacity: 1;
  stroke: var(--color-surface-solid); stroke-width: 2.5;
}

.flow-inspect {
  margin-top: 18px; padding: 16px 18px;
  background: var(--glass-bg-strong);
  border: 1px solid var(--glass-border-hi);
  border-radius: var(--radius-md);
}
.fi-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.fi-chat { font-size: var(--text-xs); color: var(--color-muted); }
/* Текст заявки тоже фиксирован по высоте: сообщения из Telegram бывают в одну
   строку и в десять, и без потолка карточка прыгала при каждой смене «героя» —
   та же болезнь, что у выросшего списка шагов. Три строки, дальше многоточие. */
.fi-text {
  margin: 10px 0 12px; font-size: var(--text-base); line-height: 1.55;
  color: var(--color-text); overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden; min-height: calc(3 * 1.55em);
}
/* Пояснение к текущему шагу. Высоту задаёт JS (`--fi-step-h`) по самой длинной
   из всех возможных подписей — при смене шага коробка не меняет размер, и
   страница под курсором не едет. Запасное значение на случай, если замер ещё
   не отработал (первый кадр, отключённый JS). */
.fi-step {
  display: flex; align-items: flex-start; gap: 10px;
  min-height: var(--fi-step-h, 2.4em);
}
.fi-step-n {
  flex: none; min-width: 5.5ch; padding-top: 0.1em;
  font-size: var(--text-2xs); font-variant-numeric: tabular-nums; color: var(--color-muted);
}
/* flex: 1 1 auto + min-width: 0 обязательны. Без них пустой span имеет ширину
   0, и первый (единственный) замер высоты в sizeStepBox мерил бы текст в
   колонке нулевой ширины — то есть не мерил бы ничего. */
.fi-step-t {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--text-s); line-height: 1.5; color: var(--color-text-soft);
}
.fi-step.ok  .fi-step-t { color: var(--color-success-text); font-weight: var(--fw-medium); }
.fi-step.bad .fi-step-t { color: var(--color-hunter-hi); }
.flow-note { margin: 14px 0 0; font-size: var(--text-s); color: var(--color-muted); }

.flow-swipe { display: none; }
@media (max-width: 1120px) {
  .flow-swipe {
    display: block; margin: 8px 0 0;
    font-size: var(--text-xs); color: var(--color-muted);
  }
}

.flow-caveats {
  margin: 12px 0 0; padding: 0; list-style: none;
  display: grid; gap: 8px; max-width: 88ch;
}
.flow-caveats li {
  position: relative; padding-left: 14px;
  font-size: var(--text-xs); line-height: 1.55; color: var(--color-muted);
}
.flow-caveats li::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--color-rule-strong);
}

/* ---------- Ферма: что стоит за потоком (/how) ----------------------------
   Раздел статический — ни одной анимации: страница и так самая тяжёлая на
   дашборде по CPU, а движение здесь ничего не объясняет. Карточки повторяют
   язык `.mod` (плоский fill, та же рамка и радиус, без blur — их тут шесть). */
.farm-lead {
  margin: 0 0 18px; font-size: var(--text-sm); line-height: 1.6;
  color: var(--color-text-soft); max-width: 82ch;
}
.farm-chain {
  margin: 0; padding: 0; list-style: none;
  display: grid; gap: 14px; grid-template-columns: 1fr;
  counter-reset: farm;
}
@media (min-width: 700px)  { .farm-chain { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1080px) { .farm-chain { grid-template-columns: repeat(3, 1fr); } }
.farm-step {
  counter-increment: farm;
  position: relative; min-width: 0; padding: 16px 18px 14px;
  background: var(--fill-3);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  display: flex; flex-direction: column; gap: 8px;
}
/* Номер шага — он же связка «за чем следует что». Стрелок между карточками нет
   намеренно: на сетке в 1/2/3 колонки они врали бы на каждом переносе. */
.farm-step::after {
  content: counter(farm); position: absolute; top: 12px; right: 14px;
  font-size: var(--text-3xs); font-variant-numeric: tabular-nums; font-weight: var(--fw-strong);
  color: var(--color-rule-strong);
}
.farm-ic {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: none;
  border-radius: 9px; background: var(--fill-1);
  color: var(--color-text-soft);
}
.farm-ic svg { width: 16px; height: 16px; }
.farm-step.is-end .farm-ic { color: var(--color-success-text); }
.farm-step h4 {
  margin: 0; font-size: var(--text-base); font-weight: var(--fw-strong);
  letter-spacing: var(--tracking-snug); padding-right: 18px;
}
.farm-step p {
  margin: 0; font-size: var(--text-s); line-height: 1.55; color: var(--color-text-soft);
  flex: 1 1 auto;
}
/* Цифры прижаты к нижнему краю карточки: в ряду их базовые линии совпадают,
   даже когда описания разной длины. */
.farm-nums {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px;
  padding-top: 10px; border-top: 1px solid var(--color-rule);
  font-size: var(--text-xs); color: var(--color-muted);
}
.farm-nums b {
  font-size: var(--text-md); font-weight: var(--fw-bold); color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.farm-nums i { font-style: normal; color: var(--color-rule-strong); }
.farm-sub {
  margin: 26px 0 12px; font-size: var(--text-sm); font-weight: var(--fw-strong);
  letter-spacing: 0.02em; text-transform: uppercase; color: var(--color-text);
}
.farm-rules {
  margin: 0; padding: 0; list-style: none;
  display: grid; gap: 10px; grid-template-columns: 1fr;
}
@media (min-width: 760px)  { .farm-rules { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1180px) { .farm-rules { grid-template-columns: repeat(3, 1fr); } }
.farm-rules li {
  min-width: 0; display: flex; align-items: baseline; gap: 10px;
  font-size: var(--text-xs); line-height: 1.55; color: var(--color-muted);
}
.farm-rules li > b {
  flex: none; min-width: 4.5ch;
  font-size: var(--text-sm); font-weight: var(--fw-bold); color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.farm-rules li span b { font-weight: var(--fw-strong); color: var(--color-text-soft); }
/* Правило без числа: отбиваем его на ширину числовой колонки, чтобы текст
   встал в ту же вертикаль, что у соседей. */
.farm-rules li.is-note { padding-left: calc(4.5ch + 10px); }

/* Ритм раздела «Модули системы» — лесенка от карточки к группе (правка 02.08).
   Было: группа↔группа 26, шапка группы↔сетка 14, зазор карточек 16 — то есть
   внутренняя отбивка группы была МЕНЬШЕ, чем расстояние между её карточками, и
   ступени не читались. Стало: 16 внутри группы (ровно зазор сетки) и 32 между
   группами (двойной зазор). Числа не новые — это тот же шаг 16, на котором
   стоит сетка карточек. */
.mod-group + .mod-group { margin-top: 32px; }
.mod-group-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding-bottom: 8px; margin-bottom: 16px;
  border-bottom: 1px solid var(--color-rule);
}
.mod-group-head h4 {
  margin: 0; font-size: var(--text-sm); font-weight: var(--fw-strong);
  letter-spacing: 0.02em; text-transform: uppercase;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.mod-group-hint { font-size: var(--text-xs); color: var(--color-muted); }
.mod-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 8px; margin-bottom: 6px;
}
.mod-head h4 { margin: 0; }
.mod-grid {
  display: grid; gap: 16px;
  grid-template-columns: 1fr;
}
@media (min-width: 760px)  { .mod-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1180px) { .mod-grid { grid-template-columns: repeat(4, 1fr); } }
/* Модульные карточки — плоский fill без blur: их на странице 20+, каждая
   со своим backdrop-filter стоила бы кадров на скролле. */
.mod {
  min-width: 0; padding: 16px 18px;
  background: var(--fill-3);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
}
.mod h4 { margin: 0 0 6px; font-size: var(--text-base); font-weight: var(--fw-strong); letter-spacing: var(--tracking-snug); }
.mod code {
  font-family: var(--font-mono); font-size: var(--text-3xs);
  color: var(--color-muted); overflow-wrap: anywhere;
}
.mod p { margin: 10px 0 0; font-size: var(--text-s); line-height: 1.55; color: var(--color-text-soft); }
.mod-why { color: var(--color-lead-text) !important; font-weight: var(--fw-medium); }

@media (prefers-reduced-motion: reduce) {
  #flow-svg .dot { display: none; }
}
.flow-disclaimer {
  margin: -4px 0 14px; font-size: var(--text-xs); line-height: 1.5;
  color: var(--color-muted); max-width: 78ch;
}

/* ============================================================
   13. Аккаунты (userbot'ы)
   ============================================================ */
.wk-cell { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
.wk-ava {
  width: 30px; height: 30px; border-radius: 50%; flex: 0 0 auto;
  object-fit: cover; background: var(--fill-2);
  box-shadow: 0 0 0 1px var(--color-rule);
}
.wk-ava-ph {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--text-s); font-weight: var(--fw-strong); color: #fff;
  background: var(--color-lead-deep);
}
:root[data-theme="dark"] .wk-ava-ph { color: #08131F; }
.wk-name { min-width: 0; overflow-wrap: break-word; }
.wk-text { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.wk-sub {
  font-size: var(--text-2xs);
  line-height: 1.35;
  color: var(--color-muted);
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
}

.wk-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  background: var(--color-dim);
}
.wk-dot.on {
  background: var(--color-success);
  animation: wk-pulse 1.6s ease-in-out infinite;
}
.wk-dot.off { background: var(--color-warning); }
.wk-dot.unk { background: var(--color-dim); }
/* Намеренно остановленный аккаунт (join_config → halted). Красный, а не жёлтый:
   жёлтый занят коротким «сейчас вступает в чат», и глаз его не читает. */
.wk-dot.dead { background: var(--color-danger); }
/* ⚠️ ДВЕ ДОБАВЛЕННЫЕ СТУПЕНИ (владелец, 23.08: «да, заведи им разные цвета»).
   На «Клиентах» выдача вставала по ДВУМ разным причинам, а точка была одна и
   та же красная — различить их можно было только наведением:
     · кончилась квота — лечится НАШИМ действием (долить пробу, поднять
       платные). Осталось красным: это остановка, за которой стоят деньги;
     · клиент держит потолок открытых заявок — лечится действием КЛИЕНТА и
       рассасывается само. Красным этому быть нечего: система здорова, мастер
       занят работой. Отсюда `busy` — янтарный, ступень «внимание, но не беда».
   ⚠️ `hold` — ПАУЗА, ЯНТАРНАЯ И НЕ ЗАЛИТАЯ (владелец, 23.08: «верни оранжевый
   в статусы» — уточнение к «убрать синий и оранжевый», которое касалось
   ДЕКОРА). Оттенок тот же, что у `busy`, а форма другая: кольцо значит
   «выключено нами и включается обратно», диск — «состояние наступило само».
   Одна семья тонов на две ситуации одного рода («выдача не идёт, но это не
   поломка») и красный отдельно на ту, где нужны деньги, — так три причины
   остановки читаются взглядом, а не наведением.
   ⚠️ ОБЕ СТУПЕНИ ДОБАВЛЕНЫ, А НЕ ПЕРЕНАЗНАЧЕНЫ: `.off` и `.unk` на
   «Аккаунтах» значат другое (сервис не жив / состояние неизвестно), и
   переклеивать их смысл ради соседней страницы нельзя.
   ⚠️ ЦВЕТ НЕ ЕДИНСТВЕННЫЙ НОСИТЕЛЬ: у каждой точки свой `title`/`aria-label`
   («в работе 5 из 5, новые не идут, пока он не закроет хотя бы одну»), и он
   объясняет, что делать, — цвет только ускоряет поиск строки глазами. */
.wk-dot.busy { background: var(--color-warning); }
.wk-dot.hold {
  background: transparent;
  box-shadow: inset 0 0 0 2.5px var(--color-warning);
}

/* Бейдж «остановлен» у аккаунта. Материал — тот же, что у `.pill.crit`:
   тонированное дно + цветной текст, а НЕ белый текст на сплошном красном.
   Первая редакция (11.08) заливалась цветом целиком и давала в тёмной теме
   3.41:1 при пороге 4.5 для мелкого текста — единственная такая точка на всём
   дашборде. В тёмной теме красный ещё и осветляется до #FF8A80, ровно как у
   `.pill.crit`: `--color-danger` (#FF453A) на тёмной подложке до порога не
   дотягивает. */
.wk-halt {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 11px;
  line-height: 1.5;
  text-transform: lowercase;
  white-space: nowrap;
  /* Цвета подобраны ЗАМЕРОМ на живой подложке, а не расчётом на плоском фоне:
     под бейджем стекло с blur поверх градиента, и расчётная модель врёт на
     0.3-1.9 пункта. Текст темнее/светлее токена ровно настолько, чтобы пройти
     4.5:1 на своём тонированном дне. */
  color: #B00008;
  background: rgba(211, 0, 10, 0.14);
  box-shadow: inset 0 0 0 1px rgba(211, 0, 10, 0.22);
}
:root[data-theme="dark"] .wk-halt {
  color: #FFB0A8;
  background: rgba(255, 69, 58, 0.16);
  box-shadow: inset 0 0 0 1px rgba(255, 69, 58, 0.26);
}

/* Звезда Telegram Premium у имени аккаунта (владелец, 30.08).
   ⚠️ ЦВЕТ ЗДЕСЬ НАМЕРЕННЫЙ И ПРАВИЛУ 23.08 НЕ ПРОТИВОРЕЧИТ: нейтральными
   стали ВЫДЕЛЕНИЯ, а цвет остался у СМЫСЛА. Здесь смысл — «подписка платная,
   потолки у аккаунта другие», и рядом стоят цветные значки состояния, среди
   которых серая звезда потерялась бы. Синий взят у самого Telegram, чтобы не
   заводить второй язык для того же факта.
   Тон разный по темам: тёмно-синий на светлой подложке не читается, светлый
   на белой даёт контраст ниже 3:1. */
.wk-prem {
  margin-left: 5px;
  font-size: 12px;
  line-height: 1;
  vertical-align: 1px;
  color: #1D82D6;
  cursor: default;
}
:root[data-theme="dark"] .wk-prem { color: #62B8F6; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .wk-prem { color: #62B8F6; }
}

@keyframes wk-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(36,138,61,.55); }
  70%  { box-shadow: 0 0 0 6px rgba(36,138,61,0); }
  100% { box-shadow: 0 0 0 0 rgba(36,138,61,0); }
}
@media (prefers-reduced-motion: reduce) {
  .wk-dot.on { animation: none; }
}

/* ============================================================
   14. Регулярные платежи (Ops)
   ============================================================ */
.pw-row { display: flex; align-items: center; gap: 6px; margin: 2px 0; flex-wrap: wrap; }
.pw-k { color: var(--color-muted); font-size: var(--text-s); }
.pw-val { font-family: var(--font-mono, monospace); font-size: var(--text-s); }
.pw-eye {
  font-size: var(--text-3xs); padding: 3px 9px; border-radius: 7px; cursor: pointer;
  border: 1px solid var(--color-rule); background: transparent; color: var(--color-lead-text);
  transform: scale(var(--press, 1));
  transition: background var(--dur-1) var(--ease-out-quart), transform var(--dur-2) var(--ease-pop), translate var(--dur-2) var(--ease-out-quart);
}
.pw-eye:hover { background: var(--tint-accent); }
.pw-eye:active { --press: 0.92; }
.pay-note { font-size: var(--text-2xs); color: var(--color-warning-text); margin-top: 2px; }

/* ============================================================
   15. Продажный пульс (Обзор)
   ============================================================ */
.kpi-fresh {
  display: flex;
  align-items: center;
  gap: 12px;
}
.kpi-fresh-main { flex: 1 1 auto; min-width: 0; }
.kpi-fresh-main .kpi-sub b {
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-bold);
}
.kpi-fresh-donut {
  position: relative;
  flex: 0 0 auto;
  width: 62px;
  height: 62px;
}
.kpi-fresh-donut .chart-empty { font-size: var(--text-3xs); }

.meter {
  margin-top: 11px;
  height: 7px;
  border-radius: 999px;
  background: var(--fill-2);
  overflow: hidden;
}
.meter-fill {
  height: 100%;
  border-radius: 999px;
  /* Полоса заполнения живёт на «Утилизации лидов» — это ЗАПАС, значит тон
     группы запаса (23.08, вторая редакция: сначала полосу увели в нейтраль,
     потом владелец попросил вернуть цвет — но уже осмысленный). Больше
     `.meter-fill` нигде на дашборде нет, страница одна. */
  background: var(--data-stock);
  min-width: 3px;
  transition: width var(--dur-spring-frame) var(--ease-spring-frame);
}

.delta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: var(--text-s);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}
/* ⚠️ ДЕЛЬТА БОЛЬШЕ НЕ КРАСИТСЯ (23.08). Она стоит на «Притоке целевых за
   сутки», где падение на четверть — это обычное колебание буднего дня, а не
   происшествие; оранжевая стрелка там кричала каждый день и тем обесценивала
   оранжевый везде, где он значит беду. Направление несёт САМА СТРЕЛКА
   (▲ / ▼ / =) — она была и раньше, цвет её только дублировал. */
.delta.up   { color: var(--color-text-soft); }
.delta.down { color: var(--color-text-soft); }
.delta.flat { color: var(--color-muted); }

.searcher-note { margin: 0; font-size: var(--text-xs); color: var(--color-muted); }

/* ============================================================
   16. «Лиды»: запас доступных по под-нишам
   ============================================================ */
.supply-list { display: flex; flex-direction: column; }
.supply-row {
  display: grid;
  grid-template-columns: 200px minmax(60px, 1fr) 208px;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 9px 6px;
  min-height: 44px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease-out-quart), transform var(--dur-2) var(--ease-out-quart);
}
.supply-row + .supply-row { border-top: 1px solid var(--color-rule); }
/* ⚠️ ЭТО БЫЛА ПОСЛЕДНЯЯ СИНЯЯ ЗАЛИВКА СПИСКА НА ДАШБОРДЕ (`--tint-accent`), и
   docs/dashboard.md держали её в списке «не переведено на правило, осознанно,
   вне задачи 03.08». Задача про «Заявки» — та самая, в границы которой строка
   входит: строка кликабельна (фильтрует список), значит она обычная строка
   списка и обязана выделяться так же, как строки всех таблиц дашборда —
   тёплым текстом плюс самой лёгкой ступенью заливки `--fill-3`.
   Выбранная (aria-pressed) — на ступень плотнее, чтобы отличаться от строки
   под курсором. */
.supply-row:hover { background: var(--fill-3); color: var(--color-accent-text); }
.supply-row:hover .supply-name { color: inherit; }
.supply-row[aria-pressed="true"] { background: var(--fill-2); }
.supply-name {
  min-width: 0;
  font-size: var(--text-sm);
  color: var(--color-text);
  overflow-wrap: break-word;
}
.supply-name small {
  display: block;
  font-size: var(--text-3xs);
  color: var(--color-muted);
}
.supply-bar {
  display: block;
  max-width: 420px;
  height: 8px;
  border-radius: 999px;
  background: var(--fill-2);
  overflow: hidden;
}
.supply-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--color-lead);
  min-width: 4px;
  transition: width var(--dur-spring-frame) var(--ease-spring-frame);
}
/* нет подписки на эту под-нишу — нейтральный бар: запас есть, отдать некому */
.supply-fill.idle { background: var(--bar-idle); }
.supply-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: flex-end;
  white-space: nowrap;
}
.supply-n {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  font-weight: var(--fw-bold);
  font-size: var(--text-md);
  color: var(--color-text);
}
/* Легенда «синий бар = есть подписка / серый = нет» переехала в панель под
   иконкой «i» строками `.tip-item` (13.08): три служебные фразы над списком
   отнимали высоту у первого экрана, а панель ровно для таких пояснений и
   заведена (прецедент — `#cs-tip` и `#sr-tip` на «Чатах»). */
@media (max-width: 1000px) {
  .supply-row { grid-template-columns: 165px minmax(50px, 1fr) 200px; gap: 10px; }
}
@media (max-width: 560px) {
  .supply-row { grid-template-columns: 1fr auto; row-gap: 8px; }
  .supply-bar { grid-column: 1 / -1; }
}

/* ============================================================
   17. «Заявки»: сайдбар фильтров + список карточек (13.08)
   ============================================================
   ⚠️ ЧТО ЗДЕСЬ ИЗМЕНИЛОСЬ И ПОЧЕМУ. До 13.08 фильтры были ТРЕМЯ рядами
   горизонтальных капсул в шапке таблицы, а сами заявки — таблицей из четырёх
   колонок. Владелец: «несколько огромных горизонтальных рядов фильтров,
   дублирование фильтров, перегруженная строка „Показаны только“, слишком
   плотное расположение, визуально тяжёлые таблицы». Стало: слева
   фиксированный сайдбар фильтров (280px), справа широкая колонка карточек.

   ⚠️ `.leads-toolbar` / `.lf-row` / `.lf-key` ВЫШЕ НЕ УДАЛЕНЫ И УДАЛЯТЬ ИХ
   НЕЛЬЗЯ: с 03.08 на них живут ряды «Тематика» и «Сортировка» на «Чатах»
   (там прямо написано, что механизм взят с «Лидов»). «Заявки» ими больше не
   пользуются — это единственное, что поменялось.
   ============================================================ */

/* Две колонки: сайдбар фиксированной ширины + всё остальное. `align-items:
   start` обязателен — иначе колонка-сайдбар растянется на высоту списка и
   `position: sticky` внутри неё будет некуда ехать. */
.leads-split {
  display: grid;
  grid-template-columns: 284px minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}
/* Порог 1024, а не 900: на планшете 768–1024 сайдбар 284px оставлял карточке
   меньше 420px, и мета-строка ломалась на четыре ряда. Ниже порога сайдбар
   встаёт над списком обычным блоком. */
@media (max-width: 1024px) {
  .leads-split { grid-template-columns: minmax(0, 1fr); }
}

/* --- ПАНЕЛЬ СПИСКА: ОПАКОВА, БЕЗ backdrop-filter --------------------------
   ⚠️ ЕДИНСТВЕННОЕ СТЕКЛО ДАШБОРДА, КОТОРОМУ СТЕКЛОМ БЫТЬ НЕЛЬЗЯ. Владелец
   13.08: «в Заявках плитка с заявками иногда становится прозрачной — этого не
   должно быть». Ключевое слово «иногда», и вот его механизм.

   Chrome реализует `backdrop-filter` так: снимает подложку элемента в ОДНУ
   текстуру размером с его границы в device-пикселях. Потолок текстуры у GPU —
   16384px. `.leads-main` со 200 карточками (`limit: 200` в `load()`) имеет
   высоту 31 920 CSS px; на ретине это 63 840 device px — вчетверо выше
   потолка. Снимок обрезается на 16384, и ниже этой отметки размывать нечего:
   фильтр молча выключается, а на экране остаётся ГОЛАЯ заливка `--glass-bg`
   (0.52 в светлой теме, 0.42 в тёмной) поверх mesh-блобов и обоев — обои
   `.app::after` начинают читаться сквозь текст заявок.

   Замер 13.08 (Chromium, 1440×900, «Все» = 200 карточек): при dpr 2 блюр жив
   до scrollY 8459 и мёртв с 8502 — ровно там, где видимая полоса панели
   пересекает 16384 device px от её верхней кромки. При dpr 1 дефекта нет на
   всей длине, на короткой выборке («Доступные», 9 карточек, панель 1537px)
   нет вовсе. Ретина + длинная выборка + прокрутка примерно на четверть — это
   и есть «иногда».

   ЛЕЧЕНИЕ — НЕ ПОДПОРКА БЛЮРА, А СНЯТИЕ ЗАВИСИМОСТИ ОТ НЕГО. Уложить список в
   потолок нечем: 16384 device px это ~52 карточки, то есть пришлось бы вводить
   виртуализацию или пагинацию ради декоративного слоя. Вместо этого у панели
   непрозрачное дно — просвечивать нечему ни при какой длине списка, плотности
   пикселей, теме и глубине прокрутки. Тот же ход, которым 31.07 сделали
   опаковой капсулу `.period-toggle` на «Обзоре»: рабочая поверхность обязана
   иметь ДЕТЕРМИНИРОВАННЫЙ контраст, а не тот, что достался от блоба под ней.
   Побочно ушёл расход: блюра на 30 000px списка, который пересобирается по
   SSE, больше нет.

   Материал повторяет рецепт `.tip-panel` и вложенного стекла (§19): сплошное
   дно `--color-surface-solid` + слой колера `--glass-bg` + прежний sheen.
   В светлой теме это чистый #FFFFFF (тот же материал, что у полей и панелей
   подсказок), в тёмной — rgb(40,40,48), на ступень светлее дна страницы.
   Кромка, тень и радиус остаются от `.glass`: снаружи панели по-прежнему
   цветная подложка, и светлая кромка читается торцом, а не обводкой.
   Замер 13.08 (dpr 2, шесть точек прокрутки от 0 до 29 995, обе темы): фон
   панели ОДИН И ТОТ ЖЕ во всех точках; контраст к нему `--color-text`
   18.04:1 / 13.10:1, `--color-text-soft` 8.74:1 / 7.53:1, `--color-muted`
   5.25:1 / 5.08:1 — весь текст выше 4.5:1 в обеих темах.

   ⚠️ ПРАВКА АДРЕСНАЯ, НЕ ПОВТОРЯЙ ЕЁ НА ОСТАЛЬНЫХ СТЁКЛАХ. Сайдбар рядом
   остаётся стеклом осознанно: он sticky и не выше экрана (`max-height:
   100vh`), его снимок в потолок не упирается. Обход десяти страниц 13.08
   (dpr 2) не нашёл ни одного другого `.glass`/`.side`/`.topbar` выше
   8192 CSS px — этот дефект есть только здесь. */
.leads-main {
  background-color: var(--color-surface-solid);
  background-image: var(--glass-sheen),
                    linear-gradient(var(--glass-bg), var(--glass-bg));
  /* ⚠️ SHEEN ЗАКРЕПЛЁН В ПИКСЕЛЯХ, А НЕ В ПРОЦЕНТАХ ВЫСОТЫ. Токен
     `--glass-sheen` гаснет к 44% высоты элемента — на карточке в 300px это
     132px блика на верхней фаске, а на панели в 31 920px это 14 045px
     подсветки, то есть треть списка светлее остального без всякой причины.
     Пока панель была стеклом, разнобой прятала мешанина блобов; на ровном
     дне он стал виден замером (тёмная тема: rgb(54,54,61) вверху против
     rgb(41,41,49) ниже 15 787px). 160px — та же фаска, что у обычной
     карточки. Второй слой (колер) остаётся тянущимся. */
  background-repeat: no-repeat;
  background-size: 100% 160px, auto;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* --- САЙДБАР ФИЛЬТРОВ ----------------------------------------------------- */
/* Липнет под топбаром (он sticky, высота --topbar-h) и никогда не выше экрана:
   у ниши с восемью под-нишами список длиннее вьюпорта, и без своего потолка
   «Сбросить фильтры» внизу уезжал бы за нижний край. */
/* ⚠️ ПОЛЕ — ОБЩЕЕ `--sp-6`, А НЕ 18px «на глаз». Здесь стояло своё число, и
   заголовок «Фильтры» вставал на 19px от верха карточки против 23px у соседей
   («Доступные по под-нишам» и шапка списка) — четыре пикселя разницы на трёх
   стоящих рядом стёклах видно как перекос, а не как приём. Ритм секций задаёт
   один токен, у `.glass` он и так `--sp-6`; строка остаётся только ради того,
   что у сайдбара своя прокрутка и липкость. */
.leads-side {
  position: sticky;
  top: calc(var(--topbar-h) + 12px);
  max-height: calc(100vh - var(--topbar-h) - 24px);
  overflow-y: auto;
  overscroll-behavior-y: contain;
  padding: var(--sp-6);
}
/* ⚠️ НИЖЕ ПОРОГА САЙДБАР РАСКЛАДЫВАЕТСЯ В РЯД ГРУПП, а не вытягивается в
   колонку на 600px. Замер на 375: колонкой это семь статусов + пять ниш +
   дата + сброс = список заявок начинался ниже двух экранов. Двумя колонками
   он вдвое короче, и никакого механизма сворачивания заводить не нужно —
   `auto-fit` сам решает, сколько колонок влезло. */
@media (max-width: 1024px) {
  .leads-side {
    position: static; max-height: none; overflow: visible;
    display: grid;
    /* ⚠️ 128px, А НЕ 138: поле сайдбара стало общим `--sp-6` (22px вместо
       своих 18px, см. выше), и внутренняя ширина на 375 упала с 305 до 297 —
       ровно под порогом, за которым `auto-fit` укладывал ДВЕ колонки. Замер
       13.08: 138px давали одну колонку, сайдбар 834px и первую заявку на 1648;
       128px возвращают две — 518px и 1332, то есть ту же раскладку, ради
       которой ряд групп и заводили. Фактическая ширина колонки при этом 137px:
       минимум только решает, сколько их влезло. */
    grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
    gap: 16px 22px;
    align-items: start;
  }
  .leads-side > .card-title, .leads-side > .ls-foot { grid-column: 1 / -1; }
  /* ⚠️ ГОРОД ЗДЕСЬ ЗАНИМАЕТ ВЕСЬ РЯД, А НЕ ПОЛОВИНУ. Он стал первой группой
     (владелец, 13.08), а высоту ряда в гриде задаёт самый высокий его житель:
     в паре с «Статусом» (семь вариантов, ~250px) выпадающий список в 60px
     отдавал бы соседу 190px пустоты. Замер 13.08 на 375: пара «Город+Статус»
     давала сайдбар 691px и первую заявку на 1505, своим рядом — 599px и 1413.
     ⚠️ Это всё равно на 81px больше, чем было, когда город стоял ПОСЛЕДНИМ и
     ложился в один ряд со второй такой же 60-пиксельной «Датой заявки»
     (518px и 1332). Восемьдесят пикселей — цена требования «город первым»;
     дешевле её не сделать, не разлучив «Дату» со «Статусом» и «Нишей». */
  .leads-side > #grp-city,
  .leads-side > #grp-country { grid-column: 1 / -1; }
  /* ⚠️ Специфичность (0,2,0), а не (0,2,0)-в-порядке-файла: базовое правило
     `.ls-group + .ls-group { margin-top: 18px }` объявлено НИЖЕ этого блока и
     при равном весе побеждало бы — верхние края соседних групп разъезжались
     на 18px (видно на замере 768: «Статус» на 555, «Ниша» на 573). */
  .leads-side .ls-group + .ls-group { margin-top: 0; }
}
.ls-group + .ls-group { margin-top: 18px; }
/* ⚠️ ГОРОД СТОИТ ПЕРВЫМ И ПОЧТИ ВСЕГДА СПРЯТАН — а `+` смотрит на разметку, а
   не на видимость. Без этой строки «Статус» получал бы 18px сверху от
   спрятанного соседа и отъезжал от заголовка «Фильтры» на пустом месте (замер
   13.08: 18px там, где у карточки-соседа 0). Правило ИМЕННОЕ, а не общее
   `.ls-group[hidden] + .ls-group`: у спрятанной «Под-ниши» отступ следующей
   группы как раз и держит зазор между «Нишей» и «Датой заявки», и общее
   правило схлопнуло бы его. */
#grp-city[hidden] + .ls-group { margin-top: 0; }
/* То же для «Страны» — она стоит первой на «Чатах» (26.08) и прячется по тому
   же правилу «вариант один — фильтровать нечем». Оба id страничные и живут на
   разных страницах, поэтому правил два, а не одно общее (см. выше, почему
   `.ls-group[hidden] + .ls-group` заводить нельзя). */
#grp-country[hidden] + .ls-group { margin-top: 0; }
/* Подпись группы. Кегль и вес те же, что у шапки таблицы (`table.lh th`):
   на дашборде это один и тот же смысл — «название столбца/группы значений». */
.ls-t {
  margin: 0 0 7px;
  font-size: var(--text-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text-soft);
}
.ls-opts { display: flex; flex-direction: column; gap: 1px; }
/* Вариант фильтра. Это КНОПКА с `aria-pressed`, а не `role="radio"`: тот
   требует навигации стрелками внутри группы, а весь дашборд переключает
   фильтры кнопками с `aria-pressed` (сегменты `.period-toggle`) — второй
   клавиатурной модели заводить нельзя. Радио-вид даёт кружок слева. */
.ls-opt {
  display: grid;
  grid-template-columns: 15px minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: 32px;
  padding: 5px 9px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--color-text-soft);
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease-out-quart),
              color var(--dur-1) var(--ease-out-quart);
}
.ls-opt:hover { background: var(--fill-3); color: var(--color-text); }
.ls-opt[aria-pressed="true"] {
  background: var(--fill-2);
  color: var(--color-text);
  font-weight: var(--fw-semibold);
}
.ls-opt:focus-visible { outline: none; border-color: var(--focus-edge); box-shadow: var(--ring-focus-edge); }
/* Кружок выбора. Носитель состояния НЕ ОДИН (есть ещё заливка, цвет и вес) —
   правило «цвет не единственный признак» соблюдено. */
.ls-mark {
  width: 13px; height: 13px;
  border-radius: 999px;
  border: 1.5px solid var(--color-rule-strong);
  box-sizing: border-box;
  transition: border-color var(--dur-1) var(--ease-out-quart),
              box-shadow var(--dur-1) var(--ease-out-quart);
}
.ls-opt[aria-pressed="true"] .ls-mark {
  border-color: var(--color-text);
  box-shadow: inset 0 0 0 2.5px var(--color-surface-solid), inset 0 0 0 13px var(--color-text);
}
.ls-lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Счётчик — вторичная величина и ЧИСЛО: tabular, чтобы столбец не дрожал при
   обновлении от SSE. */
.ls-n {
  font-size: var(--text-2xs);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
.ls-opt[aria-pressed="true"] .ls-n { color: inherit; font-weight: var(--fw-regular); opacity: .88; }
/* Стрелка направления в слоте счётчика (сортировка на «Чатах», 26.08). У
   варианта сортировки числа нет, а направление показать надо — и ТЕМ ЖЕ
   знаком, что в шапке столбца (`.th-arr`): один смысл — один рисунок. */
.ls-arr { display: block; width: 11px; height: 11px; }
.ls-n.asc .ls-arr { transform: rotate(180deg); }
/* «Показать все» / «Свернуть» — текстовая кнопка группы, не пилюля: она про
   ПОКАЗ списка, а не про выбор значения, и пилюля читалась бы вариантом. */
.ls-more {
  appearance: none; border: 0; background: none; font: inherit;
  margin: 4px 0 0 33px;
  padding: 3px 4px;
  font-size: var(--text-2xs);
  color: var(--color-muted);
  cursor: pointer;
  border-radius: 4px;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--color-dim);
  transition: color var(--dur-1) var(--ease-out-quart);
}
.ls-more:hover { color: var(--color-text); text-decoration-color: currentColor; }
.ls-more:focus-visible { outline: 2px solid var(--focus-edge); outline-offset: 2px; }
.ls-more[hidden] { display: none; }
.ls-sel { display: block; }
.ls-sel select { width: 100%; }
/* «Сбросить фильтры» внизу сайдбара — отделено линейкой, чтобы не читалось
   продолжением последней группы. */
.ls-foot { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--color-rule); }
.ls-foot .lf-reset { width: 100%; }
@media (pointer: coarse) {
  .ls-opt { min-height: 44px; }
}

/* --- ШАПКА СПИСКА: сколько нашли, что отфильтровано, как показать ---------- */
.lm-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.lm-count {
  margin: 0;
  font-size: var(--text-md);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
.lm-sub { display: block; margin-top: 3px; font-size: var(--text-2xs); font-weight: var(--fw-regular); color: var(--color-muted); }
/* Инструменты справа. Высоты выровнены числом: выпадающий список общего
   образца — ровно 40px, значит капсула тоже обязана быть 40 (34 у сегмента
   давали 42 и ряд «плавал» на 2px). */
.lm-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.lm-tools .period-toggle button { min-height: 32px; padding: 5px 12px; font-size: var(--text-xs); }
@media (pointer: coarse) { .lm-tools .period-toggle button { min-height: 40px; } }
/* ⚠️ СОРТИРОВКА — ОРГАН УПРАВЛЕНИЯ СПИСКОМ, А НЕ ПОЛЕ ФОРМЫ, и выглядит она
   как сосед по ряду (`.period-toggle` «Карточки / Список»), а не как поле
   ввода. Материал полей (`--color-surface-solid`, сплошная белая заливка) в
   шапке списка давал единственное яркое пятно: белая таблетка рядом с
   прозрачной капсулой тумблера, при том что синеву и акцентные заливки из
   органов управления владелец убрал ещё 31.07. Здесь берётся лесенка
   `.period-toggle`: прозрачный покой → `--fill-1` под курсором. Рамка и радиус
   не трогаются — они и так общие (`--color-rule`; `--radius-field` 20px при
   высоте 40px это та же капсула, что 999px у тумблера).
   Правило «поля и списки — как `.tg-ta`» не нарушено: оно про ФОРМЫ, а формы
   на этой странице нет — есть ряд контролов над списком. */
.lm-tools .f-sel > select {
  font-size: var(--text-xs); padding-top: 0; padding-bottom: 0;
  background: transparent;
  color: var(--color-text-soft);
}
.lm-tools .f-sel > select:hover {
  background: var(--fill-1);
  border-color: var(--color-rule-strong);
  color: var(--color-text);
}
.lm-seg-ic { width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px; }

/* Активные фильтры — снимаемые чипы. Прежняя строка «Показаны только: …»
   называла фильтры, но снять их можно было только общим сбросом. */
.lf-active {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin: 0 0 14px;
  font-size: var(--text-xs);
  color: var(--color-muted);
}
.lf-active[hidden] { display: none; }
/* ⚠️ ЧИП СТАЛ НЕЙТРАЛЬНЫМ. Был синий (`--tint-accent` + `--color-lead-text`) —
   а с крестиком он стал органом управления, и синева в органах управления
   убрана с 31.07. Цвет тут ничего не кодирует: чип и так подписан словом. */
.lf-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px 4px 3px 11px;
  border-radius: 999px;
  background: var(--fill-2);
  color: var(--color-text);
  font-weight: var(--fw-medium);
  font-size: var(--text-2xs);
}
.lf-chip button {
  appearance: none; border: 0; background: none; font: inherit;
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px;
  margin: 0; padding: 0;
  border-radius: 999px;
  color: var(--color-muted);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out-quart);
}
.lf-chip button:hover { color: var(--color-text); }
.lf-chip button:focus-visible { outline: 2px solid var(--focus-edge); outline-offset: 1px; }
.lf-chip button svg { width: 11px; height: 11px; }
/* Тач: зона нажатия дорастает до 44px невидимым ::after — тем же приёмом, что
   у `.ibtn`, и так же не меняя видимый размер и высоту чипа. */
@media (pointer: coarse) {
  .lf-chip button { position: relative; }
  .lf-chip button::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }
}

/* --- КАРТОЧКА ЗАЯВКИ ------------------------------------------------------
   Не вложенное стекло: карточка внутри карточки даёт «рамку в рамке», от
   которой в системе избавлялись отдельной правкой. Разделение — волосок и
   воздух, как у строк таблицы.
   Слева содержание (метки, суть, описание, метаданные), справа — действие. */
.lead-list { display: flex; flex-direction: column; }
/* ⚠️ ДВЕ КОЛОНКИ, А НЕ ОДИН GRID С ПРОБРОСОМ СТРОК. Первая редакция ставила
   `.lc-side` в `grid-row: 1 / -1`, и grid раздавал его высоту по строкам —
   первая строка карточки вырастала с 20.8 до 56.6px, пилюля статуса уезжала
   вниз на 18px, а id справа оставался наверху (замер 13.08, 1440: pill.top
   629.3 против id.top 611.4). Вложенная колонка `.lc-main` этой раздачи не
   допускает по построению. */
.lc {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 168px;
  gap: 18px;
  align-items: start;
  padding: 15px 10px 16px;
  border-radius: var(--radius-sm);
  transition: background var(--dur-1) var(--ease-out-quart);
}
.lc-main { min-width: 0; }
.lc + .lc { border-top: 1px solid var(--color-rule); }
/* ⚠️ ТЁПЛОГО ТЕКСТА ПОД КУРСОРОМ ЗДЕСЬ НЕТ НАМЕРЕННО. Общее правило проекта
   («строка под курсором: тёплый текст + `--fill-3`») написано для строк,
   ПО КОТОРЫМ КЛИКАЮТ. Карточка заявки целиком не кликабельна — у неё три
   отдельных действия внутри, и подкрашенный текст обещал бы клик, которого
   нет. Остаётся самая лёгкая ступень заливки как подсказка глазу. */
.lc:hover { background: var(--fill-3); }
.lc-top {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 7px;
}
/* Возраст — моно и tabular: столбец чисел не должен дрожать при тике SSE. */
.lc-age {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-soft);
  white-space: nowrap;
}
.lc-burn { font-size: var(--text-2xs); color: var(--color-warning-text); white-space: nowrap; }
/* Суть заявки. Это НЕ отдельное поле в базе — первая фраза самого сообщения
   (см. `leadTitle` в шаблоне): своего заголовка у заявки не существует, и
   выдумывать его нельзя. Полный текст — в `title` и в описании ниже. */
.lc-title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--fw-semibold);
  line-height: 1.42;
  letter-spacing: var(--tracking-snug);
  color: var(--color-text);
  overflow-wrap: break-word;
  text-wrap: pretty;
  max-width: 68ch;
}
.lc-desc {
  margin: 4px 0 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-text-soft);
  overflow-wrap: break-word;
  max-width: 72ch;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.lc .l-meta { margin-top: 9px; }
/* ⚠️ ДВА РЯДА МЕТАДАННЫХ, А НЕ ОДИН ПЕРЕНОСЯЩИЙСЯ. Одной строкой сюда влезали
   ниша, под-ниша, медиана, автор, чат, город и два действия — на 1440 это
   переносилось само, и на второй ряд регулярно уезжала ОДНА корзина: иконка
   удаления, висящая в пустоте, читается как чужая. Ряды разведены по смыслу и
   ровно так, как просил владелец: «ниша · под-ниша · деньги», под ними
   «автор · где». Перенос стал детерминированным, а не случайным.
   `width: 100%` у ряда — это flex-basis внутри `.l-meta`: своей раскладки ряд
   не заводит, разделители и зазоры остаются общими. */
.l-mrow { display: flex; align-items: center; flex-wrap: wrap; gap: 5px 7px; width: 100%; min-width: 0; }
/* В компактном виде ряды снова текут в одну строку: там плотность дороже. */
.lead-list.is-compact .l-mrow { width: auto; }
/* Название чата пишет посторонний человек, и в карточке оно соревнуется за
   строку с нишей, ценой, автором, городом и двумя действиями. 22ch вместо
   общих 28 — ровно чтобы корзина («не эта ниша») не уезжала одна на второй
   ряд: висящая в пустоте иконка удаления читается как чужая. Обрезка
   ОПТИЧЕСКАЯ (ellipsis), полное имя — в `title`. */
.lc .l-chat { max-width: 22ch; }
/* Правая колонка — действие и то, что к нему относится. Прижата к верху:
   выравнивание по низу гуляло бы вместе с длиной текста заявки. */
.lc-side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  text-align: right;
  min-width: 0;
}
/* id заявки — по нему владелец ссылается на лид («проверь лид 3580993»),
   поэтому он на видном месте и выделяется мышью. */
.lc-id {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--color-muted);
  user-select: all;
  /* ⚠️ ПОДНЯТ НАД НЕВИДИМОЙ ЗОНОЙ СОСЕДНЕЙ КНОПКИ (13.08). Под id стоит `.ibtn`
     «написать», а её тач-зона (`::after`, 44px) вылезает на 7px вверх за
     видимый кружок: в компактном виде зазор всего 5px, и нижние 2px строки с
     id накрывались прозрачным прямоугольником кнопки. id владелец выделяет
     мышью («проверь лид 3854176»), и выделение начиналось бы промахом по
     кнопке. Ни высоты, ни рисунка это не меняет — только порядок наложения. */
  position: relative;
  z-index: 1;
}
/* ⚠️ `.lc-side .btn-mini { width: 100% }` УДАЛЁН 13.08 вместе с текстовой
   кнопкой «Написать»: главное действие карточки стало иконкой `.ibtn`, и
   растягивать её на всю колонку нечем и незачем. Класса `.btn-mini` в правой
   колонке заявки не осталось ни на одном элементе — правило работало бы
   вхолостую и ждало бы следующей кнопки, чтобы неожиданно её растянуть. */
.lc-when, .lc-who {
  font-size: var(--text-3xs);
  line-height: 1.4;
  color: var(--color-muted);
  max-width: 100%;
  overflow-wrap: break-word;
}
.lc-who .who { font-family: var(--font-mono); color: var(--color-text-soft); }
/* Медиана под-ниши. ⚠️ ЭТО НЕ БЮДЖЕТ ЗАЯВКИ, и подпись обязана это говорить:
   бюджета заказчика в данных нет вовсе. */
.lc-price { font-variant-numeric: tabular-nums; }

/* --- КОМПАКТНЫЙ ВИД («Список») -------------------------------------------
   Тот же DOM, другая плотность: описание скрыто, суть в одну строку,
   вертикальные поля вдвое меньше. Переключатель — в шапке списка. */
.lead-list.is-compact .lc { padding: 9px 10px; gap: 2px 14px; }
.lead-list.is-compact .lc-desc { display: none; }
.lead-list.is-compact .lc-title { -webkit-line-clamp: 1; display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; font-size: var(--text-sm); }
.lead-list.is-compact .lc-top { margin-bottom: 4px; }
.lead-list.is-compact .lc .l-meta { margin-top: 5px; }
.lead-list.is-compact .lc-side { gap: 5px; }

/* Узкий экран: правая колонка уходит под текст и перестаёт быть колонкой. */
@media (max-width: 700px) {
  .lc { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .lc-side {
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap;
    text-align: left;
    gap: 8px 12px;
  }
  /* Кнопка «Написать» — иконка `.ibtn` (13.08), и на узком экране она встаёт
     в тот же ряд, что id и подписи выдачи, а не занимает строку целиком.
     44px на палец у неё свои: `.ibtn::after` растит невидимую зону наружу, а на
     `pointer: coarse` сам кружок становится 44px. Ряд от этого не растёт —
     подписи `.lc-who`/`.lc-when` в нём и так выше 30px. */
}

.leads-toolbar {
  margin: 0 -22px 0;
  padding: 0 22px 14px;
  border-bottom: 1px solid var(--color-rule-strong);
  display: flex;
  flex-direction: column;
  gap: 9px;
}
/* Ряд фильтра — СЕТКА, а не flex-wrap. При flex-wrap группа сегментов, которая
   не влезала рядом с подписью, переносилась строкой ниже и вставала от левого
   края карточки — то есть теряла вертикаль, на которой стоят соседние ряды
   (боевой случай: 8 под-ниш недвижки). Grid переносить нечему по построению:
   подпись всегда в первой колонке, группа всегда во второй. */
.lf-row {
  display: grid;
  grid-template-columns: 76px minmax(0, 1fr);
  align-items: center;
  gap: 10px;
}
/* Группа шире колонки — штатно (под-ниш у недвижки восемь, будет больше):
   прокручивается ВНУТРИ себя, страницу по горизонтали не тянет. Одинаково на
   всех трёх рядах — механизм один, заплаток на «Под-нишу» нет.
   justify-self: start сохраняет шринк-врап коробки, когда сегменты влезают. */
.leads-toolbar .period-toggle {
  justify-self: start;
  max-width: 100%;
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}
/* ⚠️ ПОЛОСА ПРОКРУТКИ СКРЫТА, САМА ПРОКРУТКА — НЕТ (владелец, 08.08: «убери
   видимость полосы прокрутки, где выбор тематики»). `overflow-x: auto` остаётся
   на месте: колесо, тачпад, свайп, Shift+колесо и клавиши работают как раньше,
   прячется только полоска. Приём ТОТ ЖЕ, что у ленты заявок в кабинете
   (`.dk-feed`, 07.08) и у капсулы на «Заявках» ниже 900px, — третьего в проекте
   не заводим.
   Два свойства обязательны, потому что движки разные: `scrollbar-width` —
   стандарт (Firefox), псевдоэлемент — WebKit/Blink. Одного мало.
   ⚠️ Здесь стояло `scrollbar-width: thin`, а гашение жило только в
   `@media (max-width: 900px)` — то есть полоса была видна ровно на десктопе,
   где её и увидел владелец. Мобильная копия убрана: она стала дубликатом.
   ПРИЗНАК ПРОДОЛЖЕНИЯ ОСТАЁТСЯ И ОН НЕ ЗАТУХАНИЕ: сегменты капсулы —
   пилюли с зазором, и при переполнении крайняя обрезается кромкой капсулы
   ровно посередине. Мягкая маска у края тут была бы хуже: она гасит текст
   пилюли всегда, в том числе когда прокручивать нечего (определить это в CSS
   нельзя), и «выключенная» на вид пилюля читается как недоступный фильтр. */
.leads-toolbar .period-toggle { scrollbar-width: none; }
.leads-toolbar .period-toggle::-webkit-scrollbar { width: 0; height: 0; }
.leads-toolbar .period-toggle button { flex: 0 0 auto; }
.lf-key {
  min-width: 76px;
  font-size: var(--text-2xs);
  font-weight: var(--fw-strong);
  letter-spacing: var(--tracking-snug);
  color: var(--color-muted);
}
/* Счётчик на сегменте — вторичная величина, но это ТЕКСТ: приглушаем его
   text-safe токеном, а не opacity (currentColor × .55 уводил контраст под
   4.5:1). На выбранном сегменте берём его же цвет и снимаем только жирность. */
.period-toggle button .n {
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
  margin-left: 1px;
}
.period-toggle button.active .n { color: inherit; font-weight: var(--fw-regular); opacity: 0.88; }

/* ⚠️ `.lf-active` / `.lf-chip` ПЕРЕЕХАЛИ ВЫШЕ, В §17 (13.08): чип стал
   снимаемым (крестик) и нейтральным по цвету. Двух определений одного класса
   в файле быть не должно — тот, что ниже, молча побеждал бы новый. */
.lf-reset {
  font: inherit;
  font-size: var(--text-2xs);
  font-weight: var(--fw-medium);
  color: var(--color-text-soft);
  background: transparent;
  border: 1px solid var(--color-rule);
  border-radius: 999px;
  padding: 4px 12px;
  min-height: 30px;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease-out-quart), border-color var(--dur-1) var(--ease-out-quart), color var(--dur-1) var(--ease-out-quart);
}
.lf-reset:hover { background: var(--fill-1); border-color: var(--color-rule-strong); color: var(--color-text); }
@media (pointer: coarse) {
  .lf-reset { min-height: 44px; padding: 4px 16px; }
}

/* --- мета-строка заявки: ниша, автор, чат, действия ------------------------
   ⚠️ ТАБЛИЦЫ ЗАЯВОК БОЛЬШЕ НЕТ (13.08) — вместе с ней сняты `table.lh.leads`
   (fixed-раскладка и доли колонок), `.l-text`, `.l-age-v`, `.l-age-sub`,
   `.l-status-sub`, `.l-status-when`, `table.lh.leads.is-empty thead` и весь
   мобильный блок, превращавший строки в карточки ниже 900px: заявка теперь
   карточка на любой ширине (`.lc` в §17 выше).
   Мета-строка ПЕРЕЖИЛА замену целиком и правится здесь же — в ней те же
   разделители-точки, те же тихие ссылки на автора/чат/пост и те же зоны
   нажатия у «написать» и корзины. */
.l-meta {
  margin: 6px 0 0;
  display: flex;
  align-items: center;
  gap: 5px 7px;
  flex-wrap: wrap;
  font-size: var(--text-2xs);
  color: var(--color-muted);
  min-width: 0;
}
.l-niche {
  font-weight: var(--fw-medium);
  color: var(--color-text-soft);
}
.l-who {
  font-family: var(--font-mono);
  font-size: var(--text-3xs);
  color: var(--color-text-soft);
}
/* Название чата пишет посторонний человек и оно бывает в 90 символов —
   режем ОПТИЧЕСКИ (ellipsis), а не slice() по символам: срез в JS рвал слово
   и терял конец без всякого признака, что текст обрезан. */
.l-chat {
  min-width: 0;
  max-width: 28ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Разделитель — часть самого элемента (и nbsp внутри), поэтому при переносе
   строки метаданных точка уезжает вместе со своим словом, а не повисает
   в конце предыдущей строки. Чисто декоративная пунктуация. */
.l-meta .l-chat::before {
  content: "·\00a0";
  display: inline-block;   /* подчёркивание ссылки не должно тянуться на точку */
  color: var(--color-dim);
}

/* --- ссылки в мета-строке лида ---------------------------------------------
   Автор / чат / исходный пост ведут в Telegram. Глобальный `a` синий, но здесь
   три ссылки подряд под текстом заявки: покрась их лидовым синим — и подпись
   начнёт перетягивать взгляд с самой заявки. Поэтому в покое ссылка выглядит
   как обычная мета, а «кликабельно» сообщают курсор, подчёркивание по наведению
   и кольцо фокуса. Цвет не единственный носитель состояния. */
.l-meta a.l-link {
  color: var(--color-text-soft);
  text-decoration: none;
  border-radius: 4px;
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out-quart);
}
.l-meta a.l-link:hover {
  color: var(--color-lead-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.l-meta a.l-link:focus-visible {
  color: var(--color-lead-text);
  outline: 2px solid var(--color-lead-text);
  outline-offset: 2px;
}
/* --- ИКОНКА В МЕТА-СТРОКЕ (`.l-mico`, 13.08) -----------------------------
   ⚠️ ЭТО НЕ `.ibtn` И НЕ ЕГО ОБЛЕГЧЁННАЯ ВЕРСИЯ. `.ibtn` — 30px габарита, и в
   мета-строке заявки он вырастил бы КАЖДУЮ карточку списка (замер 12.08 на
   таблице: строка 72.62px с иконкой и без). Здесь другая задача: глиф ростом со
   строку метаданных (13px), а честные 44px на палец добираются ОТРИЦАТЕЛЬНЫМ
   margin — зона растёт наружу, высота ряда не меняется ни на пиксель.
   Семья маленькая и закрытая: корзина «не эта ниша» (`.l-del`, красная на
   наведении) и «открыть исходное сообщение» (`.l-src`). Заводя третью иконку в
   мета-строке — вешать этот класс, а не копировать числа: 07.08 дашборд уже
   разъезжался ровно на копиях вида иконки.
   Разделитель «·» — псевдоэлемент самого элемента, чтобы при переносе точка
   уезжала вместе со своим значком, а не повисала в конце прежнего ряда. */
.l-mico {
  appearance: none; border: 0; background: none; font: inherit;
  display: inline-flex; align-items: center; cursor: pointer;
  padding: 3px 4px; margin: -3px -4px; border-radius: 4px;
  color: var(--color-muted);
  text-decoration: none;
  transition: color var(--dur-1) var(--ease-out-quart);
}
.l-mico::before { content: "·\00a0"; color: var(--color-dim); }
.l-mico > svg { width: 13px; height: 13px; display: block; flex: none; }
.l-mico:hover { color: var(--color-text); }
.l-mico:focus-visible {
  outline: 2px solid var(--focus-edge); outline-offset: 2px;
  color: var(--color-text);
}
@media (pointer: coarse) { .l-mico { padding: 8px 7px; margin: -8px -7px; } }
/* ⚠️ ПЕРЕБИВАЕМ `.l-meta a.l-link` (0,2,1) — у ссылки-иконки не должно быть ни
   синего наведения, ни подчёркивания: подчёркивать нечего, подписи нет. */
.l-meta a.l-mico { color: var(--color-muted); text-decoration: none; }
.l-meta a.l-mico:hover,
.l-meta a.l-mico:focus-visible { color: var(--color-text); text-decoration: none; }
/* t.me/c/… откроется ТОЛЬКО у участника чата, и сказать это надо не только в
   `title`. Раньше условность несло пунктирное подчёркивание подписи; у иконки
   подписи нет, поэтому пунктир рисуется фоном — он не занимает места и не
   двигает ряд ни на пиксель (border снизу дал бы +1px высоты каждой карточке). */
.l-meta a.l-src.priv {
  background-image: repeating-linear-gradient(to right,
    currentColor 0 1px, transparent 1px 3px);
  background-size: calc(100% - 8px) 1px;
  /* Прижимаем к самому глифу, а не к низу коробки: коробка на 5px выше рисунка
     (её высоту держит строчный разделитель «·»), и пунктир у нижнего края
     читался бы отдельной чёрточкой под иконкой, а не её подчёркиванием. */
  background-position: 4px calc(100% - 3px);
  background-repeat: no-repeat;
}

/* --- пустой результат: объясняем ПОЧЕМУ пусто и что нажать --- */
/* КНОПКА «+» (раскрывает форму добавления).
   ⚠️ СВОЕЙ ГЕОМЕТРИИ У НЕЁ БОЛЬШЕ НЕТ (07.08, «привести всё к единому стилю»).
   Здесь стоял третий по счёту набор габаритов — `min-height/min-width: 44px`,
   `padding: 0 7px`, `border: 1px solid var(--color-rule)`, свой `transform`,
   `transition` и `--press: 0.93`, — который `.ibtn` ниже по файлу и так
   перебивал целиком. Оставлять мёртвую копию нельзя ровно по той же причине,
   что у `.row-*`: правишь `--ib-size`, а кнопка не двигается, потому что её
   держит забытый блок. Все `.add-open` в шаблонах несут `.ibtn` (проверено
   grep'ом). Своим у неё осталось одно — доворот «+» в «×». */
/* [hidden] у UA-стиля слабее авторского display — гасим руками. */
.add-open[hidden] { display: none; }
/* ⚠️ ЗАЛИВКИ И РАМКИ ПОД КУРСОРОМ ЗДЕСЬ БОЛЬШЕ НЕТ (07.08). Кнопка «+» несёт и
   `.ibtn`, и весь её вид теперь оттуда: ховер — только глиф ярче, нажатие —
   серая зона. Своя `outline: 2px solid` на фокусе тоже убрана: кольцо в
   системе одно (`--ring-focus`), а вторая рамка рисовалась поверх него.
   Осталось только гашение цвета глифа — оно совпадает с общим. */
.add-open:hover { color: var(--ib-ink-hi); }
.add-open-ic {
  flex: 0 0 auto; width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  color: currentColor;
}
.add-open-ic svg { width: 17px; height: 17px; transition: transform var(--dur-spring-fast) var(--ease-spring-fast); }
/* Развёрнуто (форма открыта) = «выделено»: лёгкая подсветка + «+» доворачивается
   в «×». Это активное состояние, поэтому цвет тут допустим (как у пилюли фильтра). */
.add-open[aria-expanded="true"] .add-open-ic svg { transform: rotate(45deg); }
/* Раскрытая форма помечена ПОВОРОТОМ «+» в «×» — этого довольно, и это видно
   без курсора. Дна и рамки здесь нет: форма раскрывается наведением, и серая
   зона зажигалась бы ровно под курсором (07.08). Своей копии правила «на тач
   дно возвращается» тут тоже больше нет — этим заведует общий блок «СТОЯЩЕЕ
   СОСТОЯНИЕ» у `.ibtn`, а `data-hover-panel` кнопке ставит сам механизм
   раскрытия. */

/* ПОИСК ПО СПИСКУ — один компонент на все страницы.
   Был написан дважды (в `business.html` и `competitors.html`) построчной
   копией, и копия уже отстала от оригинала: не было `transition` у крестика и
   не было ни одного правила `pointer: coarse`, то есть на телефоне поле и
   крестик оказывались меньше 44px. Перенесён 02.08 дословно из «Клиентов» —
   вида не менял ни на одной странице. */
.lh-find { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0 0 14px; }
.lh-find-box { position: relative; flex: 1 1 300px; min-width: 0; max-width: 460px; }
.lh-find-box .lh-find-ic {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--color-muted); pointer-events: none;
}
.lh-find-box input {
  width: 100%; box-sizing: border-box;
  font-family: var(--font-sans); font-size: var(--text-sm); color: var(--color-text);
  background: var(--color-surface-solid);
  border: 1px solid var(--color-rule-strong); border-radius: var(--radius-field);
  padding: 9px 40px 9px 34px; min-height: 40px; outline: none;
  transition: border-color var(--dur-2) var(--ease-out-quart), box-shadow var(--dur-2) var(--ease-out-quart);
}
.lh-find-box input::placeholder { color: var(--color-muted); }
/* Родной крестик WebKit убираем — свой крестик стоит на его месте и виден в
   обеих темах (нативный в тёмной почти чёрный). */
.lh-find-box input::-webkit-search-cancel-button,
.lh-find-box input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
.lh-find-box input:focus { border-color: var(--focus-edge); box-shadow: var(--ring-focus-edge); }
/* Крестик появляется только когда есть что чистить — пустая кнопка сбивает. */
.lh-find-clear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  display: none; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; border-radius: 8px;
  box-sizing: border-box;
  border: 1px solid transparent; background: transparent; color: var(--color-muted); cursor: pointer;
  transition: color var(--dur-1) var(--ease-out-quart), background var(--dur-1) var(--ease-out-quart);
}
.lh-find-clear svg { width: 15px; height: 15px; }
/* Общая модель иконки: под курсором меняется ТОЛЬКО крестик, серая зона
   приезжает на нажатии (07.08). Класс `.ibtn` крестику не вешаем — у него своя
   геометрия (он сидит внутри поля и позиционируется абсолютом), но поведение
   берётся из тех же токенов, чтобы не разъехалось. */
.lh-find-clear:hover { color: var(--ib-ink-hi); }
.lh-find-clear:active { background: var(--ib-press-bg); color: var(--ib-ink-hi); }
/* Выделение — тот же рецепт, что у текстбокса рядом и у `.ibtn`: кромка
   `--focus-edge` + её уплотнение `--ring-focus-edge` (гало снято 08.08). Рамка
   заведена прозрачной специально, чтобы крестик не менял габарит, когда кромка
   проявляется. */
.lh-find-clear:focus-visible {
  outline: none;
  border: 1px solid var(--focus-edge);
  box-shadow: var(--ring-focus-edge);
}
.lh-find.has-q .lh-find-clear { display: inline-flex; }
.lh-find-n {
  font-size: var(--text-xs); color: var(--color-text-soft);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum";
}
.lh-find-n b { color: var(--color-text); font-weight: var(--fw-bold); }
/* «Свернуть/развернуть все» уезжает вправо: это действие над всем списком. */
.lh-find .lh-find-all { margin-left: auto; }
/* Подпись убрана (решение владельца 02.08) — тот же ход, что у кнопки «+»
   рядом с тумблером: остаётся квадратная пилюля с шевроном, а смысл целиком
   в aria-label/title, которые переставляет setFindAllLabel вместе с
   состоянием. Кнопка обязана ЧИТАТЬСЯ кнопкой, поэтому рамка и hover от
   .btn-mini остаются — меняются только пропорции. */
.lh-find-all { padding: 0; width: 38px; min-width: 38px; justify-content: center; }
/* Шеврон доворачивается: вниз = «развернуть», вверх = «свернуть». Крутим от
   data-next (следующее действие) — его ставит тот же код, что и подпись,
   поэтому стрелка и подсказка не могут разъехаться. */
.lh-find-all svg { transition: transform var(--dur-spring-fast) var(--ease-spring-fast); }
.lh-find-all[data-next="collapse"] svg { transform: rotate(180deg); }
@media (pointer: coarse) { .lh-find-all { width: 44px; min-width: 44px; } }
@media (pointer: coarse) {
  .lh-find-box input { min-height: 44px; font-size: 16px; }
  .lh-find-clear { width: 38px; height: 38px; }
}
@media (max-width: 520px) {
  .lh-find-box { flex: 1 1 100%; max-width: none; }
  .lh-find .lh-find-all { margin-left: 0; }
  .lh-find-box input { font-size: 16px; }  /* iOS Safari не зумит поле от 16px */
}
/* Пустой результат поиска — канонический `.empty-line.boxed` (dashboard.css).
   Свой рецепт жил здесь и уже разошёлся с таким же на «Конкурентах». */


/* ПУСТОЕ СОСТОЯНИЕ — ДВА КАНОНИЧЕСКИХ ВИДА, а не шесть страничных.
   `.empty` — крупное центрированное: занимает место списка и объясняет, ПОЧЕМУ
   пусто и что нажать. `.empty-line` — строка на месте блока данных, когда
   объяснять нечего («ещё не запускался», «нет платежей»).
   Имя `.leads-empty` оставлено синонимом: разметка «Лидов» его использует, а
   переименование ради переименования — лишний риск на живой странице. */
.empty-line {
  margin: 0;
  padding: 12px 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-muted);
}
/* Пустой результат ПОИСКА — это ответ на запрос, а не «данных нет»: рамка и
   подложка показывают, что блок отработал. Использовалось на «Клиентах»
   (`.q-empty`) и «Конкурентах» — теперь один рецепт. */
.empty-line.boxed {
  padding: 16px;
  border-radius: var(--radius-md);
  border: 1px dashed var(--color-rule-strong);
  background: var(--fill-3);
  color: var(--color-text-soft);
}
.empty-line.boxed b { color: var(--color-text); overflow-wrap: anywhere; }
.empty-line.boxed .btn-mini { margin-top: 10px; }

.leads-empty, .empty {
  max-width: 46ch;
  margin: 0 auto;
  padding: 34px 8px 36px;
  text-align: center;
}
.leads-empty .ic, .empty .ic {
  display: block;
  margin: 0 auto 12px;
  width: 26px; height: 26px;
  color: var(--color-dim);
}
.leads-empty h4, .empty h4 {
  margin: 0 0 7px;
  font-size: var(--text-md);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text);
}
.leads-empty p, .empty p {
  margin: 0 0 15px;
  font-size: var(--text-s);
  line-height: 1.6;
  color: var(--color-text-soft);
  text-wrap: pretty;
}
.leads-empty p:last-child, .empty p:last-child { margin-bottom: 0; }

/* ⚠️ БЛОК «≤900px: таблица превращается в карточки» УДАЛЁН 13.08 вместе с
   самой таблицей. Заявка теперь карточка (`.lc`) на ЛЮБОЙ ширине, и второй
   раскладки для узкого экрана ей не нужно — правится только число колонок
   внутри карточки (см. `@media (max-width: 700px)` в §17). */

/* Пальцем в подпись 12px не попасть. Растим зону нажатия только у ссылок
   (нишу и под-нишу никто не жмёт) и заодно разводим строки мета по вертикали,
   чтобы соседние зоны не наезжали друг на друга при переносе.
   Отрицательных отступов здесь нет намеренно: при flex-wrap они схлопывали бы
   расстояние между строками до перекрытия зон. */
@media (pointer: coarse) {
  .l-meta { row-gap: 2px; }
  .l-meta a.l-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

/* Полоса фильтров на узком экране: подпись над сегментами, а не слева —
   иначе на 375px «Под-ниша» съедает половину строки. */
@media (max-width: 560px) {
  .lf-row { grid-template-columns: minmax(0, 1fr); align-items: start; gap: 6px; }
  .lf-key { min-width: 0; }
}

/* ============================================================
   18. Моторика: появление, скелетоны, микро-состояния
   ============================================================

   ГЛАВНОЕ ОГРАНИЧЕНИЕ ПРОЕКТА: дашборд живой. SSE дёргает refresh почти на
   каждое входящее сообщение в чатах, и страницы перерисовывают СОДЕРЖИМОЕ
   списков (innerHTML у tbody / #cl-list / #supply-list). Любая анимация
   «на появление», привязанная к пути отрисовки, переигрывалась бы каждые
   несколько секунд.

   Отсюда два структурных правила, а не договорённости:
   1. Каскад появления навешен ТОЛЬКО на серверные секции `.shell > section`
      и их прямых детей — эти узлы не пересоздаёт ни один рендер, списки
      живут ВНУТРИ них. Переиграть анимацию физически нечем.
   2. Гейт — класс `lh-intro` на <html>. Его ставит инлайн-скрипт в <head>
      (до первой отрисовки, иначе моргнёт готовым макетом) и снимает
      dashboard.js через ~1 с. После снятия правило перестаёт существовать
      вовсе: даже если секцию когда-нибудь начнут пересоздавать, анимация
      второй раз не сыграет.
   Анимации по ДЕЙСТВИЮ пользователя (раскрытие карточки клиента) остаются
   императивными в business.html — см. комментарий «Движение» там. */

@keyframes lh-rise {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: none; }
}

/* --- Переход между страницами (View Transitions для MPA) -------------------
   Дашборд многостраничный: каждый переход по сайдбару — полная перезагрузка с
   белой вспышкой между документами. `navigation: auto` даёт браузеру снять
   кадр старой страницы и перекрестить его с новой. Где API нет — не меняется
   НИЧЕГО (обычная навигация), поэтому фолбэк тут бесплатный.

   Два условия, без которых это была бы не «плавность», а мельтешение:
   1. Сайдбар и топбар получают собственные view-transition-name и потому НЕ
      участвуют в кроссфейде: хром остаётся неподвижным, меняется только
      содержимое. Заодно это самый дешёвый способ не размывать 30px blur.
   2. Каскад появления секций на такой навигации ГАСИТСЯ (dashboard.js слушает
      pagereveal): фейд документа и каскад секций поверх него — двойное
      движение на каждый клик по меню.
   160 мс — сознательно меньше --dur-4: переход не должен ощущаться как
   ожидание, он должен просто убрать вспышку. */
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 160ms; animation-timing-function: var(--ease-out-quart); }
.side   { view-transition-name: lh-side; }
.topbar { view-transition-name: lh-topbar; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* Секции въезжают каскадом; шаг между соседями --stagger (28 мс),
   длительность --dur-4 (320 мс). Худший случай: индекс 5 + 3 = 8 шагов,
   224 + 320 = 544 мс на весь экран. */
:root.lh-intro .shell > section,
:root.lh-intro .shell > .filters,
:root.lh-intro .shell > .filter-row {
  animation: lh-rise var(--dur-spring-fast) var(--ease-spring-fast) both;
  animation-delay: calc(var(--lh-i, 0) * var(--stagger));
}
/* Grid-секции въезжают не одним куском, а плитками: ряд KPI, собирающийся
   слева направо, — единственное место, где каскад читается как замысел,
   а не как лаг. Сама секция при этом не анимируется (иначе двойной fade). */
:root.lh-intro .shell > section.grid-kpi,
:root.lh-intro .shell > section.grid-2,
:root.lh-intro .shell > section.grid-3 { animation: none; }
:root.lh-intro .shell > section.grid-kpi > *,
:root.lh-intro .shell > section.grid-2 > *,
:root.lh-intro .shell > section.grid-3 > * {
  animation: lh-rise var(--dur-spring-fast) var(--ease-spring-fast) both;
  animation-delay: calc((var(--lh-i, 0) + var(--lh-j, 0)) * var(--stagger));
}
/* Индекс секции (наследуется детьми через custom property) и индекс плитки.
   Оба с потолком: на «Клиентах» секций семь, без потолка низ экрана поехал бы
   через секунду после верха. */
:root.lh-intro .shell > section:nth-of-type(1)   { --lh-i: 1; }
:root.lh-intro .shell > section:nth-of-type(2)   { --lh-i: 2; }
:root.lh-intro .shell > section:nth-of-type(3)   { --lh-i: 3; }
:root.lh-intro .shell > section:nth-of-type(4)   { --lh-i: 4; }
:root.lh-intro .shell > section:nth-of-type(n+5) { --lh-i: 5; }
:root.lh-intro .shell > section > *:nth-child(2)   { --lh-j: 1; }
:root.lh-intro .shell > section > *:nth-child(3)   { --lh-j: 2; }
:root.lh-intro .shell > section > *:nth-child(n+4) { --lh-j: 3; }

/* --- Скелетоны ------------------------------------------------------------
   Пустая карточка со словом «загрузка…» читается как поломка. Полоса-скелет
   показывает форму будущего контента. Блик едет transform'ом (не
   background-position) — композитится на GPU и не вызывает перерисовку. */
.skel {
  position: relative;
  display: block;
  overflow: hidden;
  height: 11px;
  border-radius: 999px;
  background: var(--fill-2);
}
.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translate3d(-100%, 0, 0);
  background: linear-gradient(90deg, transparent 0%, var(--glass-row-hi) 50%, transparent 100%);
  animation: lh-shimmer 1.35s var(--ease-in-out) infinite;
}
@keyframes lh-shimmer { to { transform: translate3d(100%, 0, 0); } }
.skel-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 11px 0;
}
.skel-row + .skel-row { border-top: 1px solid var(--color-rule); }
.skel.w-xs { width: 46px; }
.skel.w-sm { width: 88px; }
.skel.w-md { width: 150px; }
.skel.w-lg { flex: 1 1 auto; min-width: 60px; }
.skel.tall  { height: 15px; }

/* --- Строки таблиц --------------------------------------------------------
   Появления строки нет вовсе — есть только реакция на курсор, и это ТОЛЬКО
   тёплый цвет текста строки (`table.lh tbody tr:hover > td`, правило владельца
   03.08: «заливки нет, красится текст»).

   ⚠️ МАРКЕРА ЛЕВОГО КРАЯ БОЛЬШЕ НЕТ (владелец, 08.08: «при наведении на
   аккаунт убери оранжевую вертикальную полосу слева»). Здесь стояло
   `table.lh:not(.rows-pill) tbody tr:hover > td:first-child { box-shadow:
   inset 2px 0 0 var(--color-warning-text) }` под `@media (min-width: 901px)`,
   плюс `transition: box-shadow` на всех ячейках ради него и сброс у
   строк-заглушек с `colspan`.
   Что важно знать, если полосу когда-нибудь захотят вернуть:
     · она была ОДНА НА ВЕСЬ ДАШБОРД — «Аккаунты», «Чаты», «Заявки», таблица
       менеджеров, «Исходы» и все прочие `table.lh`. Поэтому и убрана одним
       правилом: чинить только «Аккаунты» значило бы завести разнобой;
     · она была признаком НАВЕДЕНИЯ и только его. Состояний ею никто не
       помечал: строка на паузе, выбранная и раскрытая строки несут свои
       признаки (пилюля, `aria-expanded`, класс), и они не тронуты. Маркер
       выбранной карточки в кабинете менеджера (`.dk-row`) — своя разметка в
       `desk.html`, к этому правилу отношения не имеет;
     · раскладку она не двигала и не двигает: `box-shadow: inset` места не
       занимает. Замер до и после снятия — левый край первой ячейки в покое и
       под курсором совпадает до сотых.
   Реакция на курсор при этом ОСТАЛАСЬ (строка кликабельна — по ней
   раскрывается личка аккаунта): весь текст строки уходит в
   `--color-warning-text`, замер на боевой подложке — тёмная 8.49:1, светлая
   6.77:1. */

/* Нажатие: сжатие с упругим возвратом. Кривая --ease-pop заходит за 100% на
   ОБРАТНОМ ходу (палец отпустили) — отклик «пружинит», гамма не меняется
   (монохром кнопок от 31.07 цел). Величина сжатия обратна размеру элемента:
   мелкой иконке нужно больше процентов, чтобы движение было заметно.
   ⚠️ У ИКОНОЧНОЙ КНОПКИ ЭТО ЖИВЁТ В `.ibtn` И БОЛЬШЕ НИГДЕ. Здесь стояла
   строка `.row-del:active, … { --press: 0.9 }` — четвёртая по счёту копия,
   мёртвая (общий `.ibtn:active { --press: 0.92 }` идёт ниже по файлу и
   выигрывал), но именно такие копии и разводят «одинаковые» кнопки. Снята
   07.08 вместе с магнитом и геометрией `.row-*`. */
.period-toggle button:active { --press: 0.955; transform: scale(var(--press, 1)); }
.theme-toggle button:active { transform: scale(0.9); }
/* ⚠️ ЗДЕСЬ СТОЯЛА СТРАХОВКА ОТ МАГНИТА на тач-устройстве
   (`@media (pointer: coarse) { .row-*, .pw-eye { translate: none } }`). Магнит
   снят целиком 07.08 (владелец: «иконки не должны двигаться, когда я навожу»),
   страховать больше нечего — сдвиг `translate` иконке никто не ставит. */

/* --- Уважение к prefers-reduced-motion -----------------------------------
   Глобальный блок в §8 сводит длительности к 0.001 мс, но `animation-delay`
   он не трогает: без сброса задержки секции с --lh-i=5 просто появились бы
   через 140 мс пустым скачком. Плюс блик скелетона и Chart.js — их гасят
   явно (Chart.js — в dashboard.js, он мимо CSS). */
@media (prefers-reduced-motion: reduce) {
  :root.lh-intro .shell > section,
  :root.lh-intro .shell > .filters,
  :root.lh-intro .shell > .filter-row,
  :root.lh-intro .shell > section > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .skel::after { animation: none; display: none; }
  .period-toggle button:active,
  .theme-toggle button:active,
  .btn-primary:active, .btn-ghost:active, .btn-mini:active, .acc-new-btn:active,
  .pw-eye:active { transform: none; --press: 1; }
  /* `.row-*` отсюда убраны: сжатие им ведёт `.ibtn`, и он же гасит его в своём
     блоке `prefers-reduced-motion` (см. конец секции `.ibtn`). */
  /* Волна гасится ещё и в JS (prefersReducedMotion). Сброс `translate` у
     иконочных кнопок отсюда убран вместе с магнитом (07.08): двигать их больше
     нечему. */
  .lh-rip { display: none; }
  /* Капсула тумблера двигается inline-стилем из JS, но её ПЕРЕХОД — здесь.
     Глобальный сброс длительностей в §8 его и так сводит к 0.001 мс; правило
     оставлено явным, чтобы это не зависело от порядка объявлений. */
  .pt-ind { transition: none !important; }
}

/* ============================================================
   §9. ВХОД И УЧЁТНАЯ ЗАПИСЬ (02.08)
   Страница входа заменила системный попап basic-auth: попап нельзя ни
   оформить, ни забрендировать, а вход — первое, что человек видит.

   Шаблон login.html НЕ наследует base.html (там сайдбар и топбар, которые
   невошедшему показывать нечего), поэтому здесь описан весь каркас страницы.
   Фон при этом достаётся даром: `<body class="app">` тянет .app::before
   (mesh) и .app::after (обои), а карточка — обычный .glass. Никакой второй
   палитры для входа не заведено намеренно.
   ============================================================ */
.login-body {
  min-height: 100vh;
  min-height: 100dvh;
}
.login-wrap {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px 18px 40px;
  /* Тот же подъём над подложкой, что у .shell: без него карточка красится
     раньше .app::before и уходит под цветную плёнку. */
  position: relative;
  z-index: 1;
}
.login-card {
  width: 100%;
  max-width: 372px;
  padding: 30px 26px 24px;
}

/* Знак — фоном, а не <img srcset>, и переключается СЕЛЕКТОРОМ `data-theme`,
   как .side-logo. Через `media="(prefers-color-scheme: dark)"` знак слушал бы
   системную тему и врал бы всем, кто выбрал тему руками: гейт в <head>
   ставит атрибут из localStorage, а <picture> о нём не знает. */
.login-logo {
  display: block;
  width: 52px; height: 52px;
  margin: 0 auto 14px;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  background-image: -webkit-image-set(url("/static/logo-mark-light.png") 1x, url("/static/logo-mark-light@2x.png") 2x);
  background-image: image-set(url("/static/logo-mark-light.png") 1x, url("/static/logo-mark-light@2x.png") 2x);
}
:root[data-theme="dark"] .login-logo {
  background-image: -webkit-image-set(url("/static/logo-mark-dark.png") 1x, url("/static/logo-mark-dark@2x.png") 2x);
  background-image: image-set(url("/static/logo-mark-dark.png") 1x, url("/static/logo-mark-dark@2x.png") 2x);
}
/* Начертание слова — то же, что в сайдбаре (.side-word): «Lid» цветом текста,
   «z» акцентом. Здесь оно крупнее, потому что несёт роль заголовка. */
.login-title {
  margin: 0;
  text-align: center;
  font-size: var(--text-xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text);
}
.login-title .z { color: var(--color-lead-text); }
.login-hi {
  margin: 6px 0 0;
  text-align: center;
  font-size: var(--text-base);
  color: var(--color-text-soft);
}

/* ⚠️ СЛОТ ОШИБКИ ЗАРЕЗЕРВИРОВАН ВСЕГДА. Страница «неверный пароль» — это тот
   же шаблон со статусом 401, то есть человек видит её на месте предыдущей.
   Появись сообщение из ниоткуда, карточка (она отцентрована) поехала бы вверх
   на свою высоту, и поле ввода ушло бы из-под курсора. Держим место пустым:
   при 1440 это 18px разницы в положении карточки, при пустом слоте — 0. */
.login-slot {
  min-height: 42px;
  display: flex;
  align-items: center;
  margin-top: 16px;
}
.login-error {
  margin: 0;
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--color-hunter-hi);
  border-radius: var(--radius-sm);
  font-size: var(--text-s);
  line-height: 1.4;
  color: var(--color-hunter-hi);
}

.login-form { display: grid; gap: 4px; }
.login-label {
  font-size: var(--text-3xs);
  font-weight: var(--fw-medium);
  color: var(--color-muted);
  margin-top: 8px;
}
/* ⚠️ МАТЕРИАЛ ПОЛЯ ЗДЕСЬ БОЛЬШЕ НЕ ОПИСАН (04.08). Вход, регистрация и
   учётная запись подключают ОБЩИЙ ОБРАЗЕЦ ФОРМЫ КЛИЕНТА — класс `lh-form` на
   самой <form> (§19: подложка `--color-surface-solid`, рамка `--color-rule`,
   радиус 20px, высота ровно 40px числом, фокус `--color-rule-strong` +
   `--ring-focus`). До этой правки §9 держала СВОЮ копию материала (рамка
   `--color-rule-strong`, радиус `--radius-sm`, `min-height: 44px`, фокус в
   `--color-text-soft`), то есть у дашборда было два вида полей: один на
   вкладках, другой на страницах вне base.html. Правило владельца «все формы по
   образцу формы клиента» на эти страницы распространяется тоже.
   За `.login-input` остаётся ровно то, чего нет у эталона: ширина во всю
   карточку и защита от автозаполнения Chrome. Материал сюда не возвращать —
   вторая копия ровно так и разъехалась. */
.login-input { width: 100%; }
/* ⚠️ АВТОЗАПОЛНЕНИЕ. Chrome красит подставленное поле своим фоном
   (#E8F0FE) и своим цветом текста, игнорируя background — перекрыть можно
   только толстой внутренней тенью. Без этого в тёмной теме поле становится
   светло-голубым, а текст в нём — чёрным на чёрном при первом же клике.
   `transition` на 1e5s — общепринятый способ не дать Chrome нарисовать свой
   фон вовсе: анимация «до» просто не доходит. */
.login-input:-webkit-autofill,
.login-input:-webkit-autofill:hover,
.login-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--color-text);
  caret-color: var(--color-text);
  -webkit-box-shadow: 0 0 0 1000px var(--color-surface-solid) inset;
  box-shadow: 0 0 0 1000px var(--color-surface-solid) inset;
  transition: background-color 100000s ease-in-out 0s;
}
/* ⚠️ ПОСЛЕДНЕЕ СИНЕЕ ГАЛО СРЕДИ ПОЛЕЙ, найдено замером 08.08: здесь стоял
   `var(--ring-focus)`, и подставленное менеджером паролей поле при фокусе
   вспыхивало синим ободком — ровно тем, который со всех остальных полей сняли
   утром того же дня. Виден он был только при автозаполнении, поэтому и пережил
   правку. Теперь тот же рецепт, что у всех: уплотнение кромки.
   ⚠️ ПОРЯДОК ТЕНЕЙ ЗНАЧИМ: кромка обязана стоять ПЕРВОЙ. Список рисуется
   сверху вниз, и заливка в 1000px, поставленная раньше, закрасила бы её. */
.login-input:-webkit-autofill:focus {
  -webkit-box-shadow: var(--ring-focus-edge), 0 0 0 1000px var(--color-surface-solid) inset;
  box-shadow: var(--ring-focus-edge), 0 0 0 1000px var(--color-surface-solid) inset;
}

/* Кнопка входа. Форма — общая пилюля (.btn-primary), но это ЕДИНСТВЕННОЕ
   действие на экране, поэтому она во всю ширину и с заливкой: монохром
   31.07 запрещает цвет, а не вес. Та же пара --fill-1/--fill-2, что у
   кнопки отправки в переписках. */
.login-submit {
  width: 100%;
  min-height: 44px;
  margin-top: 18px;
  font-size: var(--text-md);
  background: var(--fill-1);
  border-color: var(--color-rule-strong);
  color: var(--color-text);
}
.login-submit:hover { background: var(--fill-2); border-color: var(--color-text-soft); }

/* `.login-foot` удалено 02.08: сноску со страницы входа убрали, правило
   осталось висеть мёртвым. Разметки с этим классом в шаблонах нет ни одной. */

/* --- учётная запись (/account) -------------------------------------------
   Страница делает ровно два дела: сменить свой пароль и выйти. Поэтому она
   собрана как три блока одинакового устройства — «кто вошёл», «Смена пароля»,
   «Выход» — разделённые ОДНОЙ и той же волосяной линией. Единый ритм тут не
   украшение: два действия рядом, одно из них необратимо для чужих сессий, и
   человек должен видеть границу между ними, не вчитываясь.

   Внутри блока порядок всегда один: заголовок → одна строка «что произойдёт»
   → место под ответ → поля → кнопка. Раньше объяснение стояло ПОСЛЕ кнопки,
   то есть читалось уже после нажатия.

   Поля — те же `.login-input` и `.login-label`, что на входе, и тот же общий
   образец формы клиента (`lh-form` на <form>): обе страницы про одну учётку, и
   разная типографика полей читалась бы как разные продукты.

   ⚠️ ПЛИТКИ СТОЯТ РЯДОМ, А НЕ СТОЛБИКОМ (04.08, решение владельца). Раньше
   страница была колонкой шириной 560px: на 1440 это 660px пустоты справа, и
   «Выход» уезжал ниже сгиба. Теперь «Учётная запись» и «Смена пароля» — пара в
   общем `.grid-2` (порог схлопывания 900px тот же, что у пары «Поиск чатов» /
   «Города» на «Чатах»; второго механизма не заводим), а «Заявки на
   регистрацию» лежат отдельной строкой во всю ширину — у таблицы пять колонок,
   в половине ряда кнопки решения уезжают в горизонтальный скролл.
   Отсюда снят `max-width: 560px`: ширину плитки задаёт колонка сетки, а
   собственный потолок оставлял бы её уже своей колонки и ломал бы ровно то
   выравнивание, ради которого пару и собрали. Меру текста держат `.acc-lead`
   (46ch) и `.acc-form` (340px) — они и без потолка карточки не расползаются. */

/* Шапка: инициал, логин, роль. Аватарки у сотрудников нет и заводить её ради
   этой страницы незачем — кружок с буквой, как у клиента без фото. */
.acc-head {
  display: flex; align-items: center; gap: 12px;
  margin: 0 0 18px;
}
.acc-ava {
  flex: none;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--fill-2);
  box-shadow: inset 0 0 0 1px var(--color-rule);
  font-size: var(--text-md);
  font-weight: var(--fw-strong);
  color: var(--color-text-soft);
  line-height: 1;
}
.acc-id { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.acc-name {
  font-size: var(--text-md);
  font-weight: var(--fw-strong);
  color: var(--color-text);
  overflow-wrap: break-word;
}

.acc-block + .acc-block { margin-top: 0; }
.acc-sub {
  margin: 0 0 4px;
  font-size: var(--text-md);
  font-weight: var(--fw-strong);
  color: var(--color-text);
}
/* Строка «что произойдёт». Мера ограничена — длинная строка в узкой карточке
   читается хуже, чем перенос. */
.acc-lead {
  margin: 0 0 10px;
  max-width: 46ch;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-text-soft);
}
.acc-form { display: grid; gap: 4px; max-width: 340px; }
.acc-hint { margin-top: 6px; font-size: var(--text-3xs); color: var(--color-muted); }
/* Кнопка по ширине содержимого, а не во всю форму: на входе кнопка одна и
   тянется намеренно, здесь их две на странице, и растянутая читалась бы как
   главное действие вкладки. */
.acc-submit { justify-self: start; margin-top: 14px; }
.acc-note {
  margin: 18px 0 0;
  max-width: 60ch;
  font-size: var(--text-xs); line-height: 1.55; color: var(--color-muted);
}
.acc-note code {
  font-size: 0.95em;
  padding: 1px 4px;
  border-radius: 5px;
  background: var(--fill-1);
  color: var(--color-text-soft);
}
.acc-hr { margin: 22px 0; border: 0; border-top: 1px solid var(--color-rule); }
/* Строка ответа держит высоту ВСЕГДА — иначе поля и кнопка съезжают вниз ровно
   в момент, когда человек смотрит на кнопку. Тот же приём, что у `.login-slot`. */
.acc-msg {
  min-height: 20px; margin: 0 0 8px;
  font-size: var(--text-s); line-height: 1.45;
  color: var(--color-muted);
}
.acc-msg.ok  { color: var(--color-success-text); }
.acc-msg.err { color: var(--color-hunter-hi); }

@media (max-width: 420px) {
  .login-card { padding: 26px 20px 22px; }
  /* На узком экране форма во всю карточку: 340px посреди 375px оставляли бы
     справа полосу пустоты шириной с палец. */
  .acc-form { max-width: none; }
  .acc-submit { justify-self: stretch; }
}

/* ============================================================
   ВСПЛЫВАЮЩИЕ ИНФОРМАЦИОННЫЕ ПАНЕЛИ — ОДИН МАТЕРИАЛ НА ВСЕ (03.08)
   Правило владельца: «все информационные панели, раскрывающиеся по иконке
   «i», — в одном стиле». До этого их было три вида в двух файлах, и они уже
   разъехались: `.acc-peek` на «Чатах» несла прозрачное стекло, `.ot-tip`/
   `.qs-tip` — непрозрачную подложку с тем же колером, `.hd-tip` на «Клиентах»
   — свою копию второго. Здесь лежит ОБЩАЯ ОБОЛОЧКА, страницы задают только
   геометрию (ширину, поля, привязку) и содержимое.

   ⚠️ Подложка НЕПРОЗРАЧНАЯ, и это замер, а не вкус. Панель висит внутри
   `section.glass`, у которой есть `backdrop-filter`, то есть секция —
   backdrop-root: Chromium НЕ отдаёт вложенному стеклу поточное содержимое
   секции как подложку (A/B 01.08: blur 0 → 120px даёт побитово одинаковый
   кадр). Значит читаемость над таблицей нельзя добрать ни блюром, ни
   яркостью — только дном. Колер `chrome` кладётся ПОВЕРХ дна: материал тот
   же, что у сайдбара и топбара, а текст не спорит с чужими числами под ним.

   Раскрывает их `initTipPanel` (наведение + фокус + тап, Esc и клик мимо
   закрывают); метка `.tip-panel` — то, по чему секция понимает, осталась ли
   открытой хоть одна панель, и держит свой временный z-index.
   ============================================================ */
/* ШКАЛА ШИРИН — ЧЕТЫРЕ СТУПЕНИ НА ВЕСЬ ДАШБОРД (08.08).
   Повод — задача владельца «все формы, которые появляются при наведении,
   привести к единому виду». Замер ДО правки, 1440, светлая тема: панелей
   пятнадцать, а РАЗНЫХ ширин девять — 300 / 320 / 340 / 380 / 420 / 430 /
   460 / 560 плюс `max-content`. Разница в 20–40px на глаз не читается как замысел,
   она читается как небрежность, а вместе с разными потолками высоты
   (560 / 620 / 640) и разным отступом от кнопки (−4 / +4 / +6) панели одного
   дашборда выглядели собранными разными руками.
   Ступеней ровно четыре, и у каждой своя работа:
     S  — числа и короткие строки («подпись — значение»);
     M  — справка фразами и виджет-сводка; ДЕФОЛТ, его получает панель,
          которая ничего про ширину не сказала;
     L  — форма: два поля в ряд по образцу формы клиента;
     XL — список, который листают (журнал работы);
     2XL — ЛЕНТА ПЕРЕПИСКИ В ОДНУ КОЛОНКУ (владелец 07.09: «форму диалогов
          сделать шире»). У ленты пузырей своя мера: на 560px чужая реплика
          ломалась по три-четыре слова, и разговор читался столбиком. 680
          дают пузырю ~55 знаков в строке при мере читаемости 45–75.
     3XL — ДВЕ КОЛОНКИ «как в чате» (владелец 07.09: «слева выбор диалога,
          справа сам диалог»). Складывается из частей, а не подобрано на глаз:
          264 колонка выбора + 14 зазор + ~470 лента + 32 поля панели = 780.
          Дальше растить нельзя: панель висит НАД карточкой клиента, и на 1280
          (карточка 1114) она начала бы закрывать её целиком.
   ⚠️ Ширину панели числом больше не ставить. Ровно так дашборд и разъехался:
   каждая следующая панель копировала соседнюю и подправляла на 20px. */
:root {
  --tip-w-s:  320px;
  --tip-w-m:  380px;
  --tip-w-l:  440px;
  --tip-w-xl: 560px;
  --tip-w-2xl: 680px;
  --tip-w-3xl: 780px;
}
.tip-panel {
  position: absolute; z-index: 30;
  top: calc(100% + 4px); right: 0;
  /* Ступень по умолчанию: новая панель без единой строки в стилях страницы
     сразу правильной ширины. Страница переопределяет ТОКЕНОМ, не числом. */
  width: var(--tip-w-m);
  /* ⚠️ Потолок ширины — от ЯКОРЯ (`100%`), а не от вьюпорта, и это не мелочь.
     Панели привязаны к заголовку карточки, который ровно совпадает с её
     содержимым: `right: 0` + `max-width: 100%` дают панель, которая по
     построению не может выйти за края карточки ни на одной ширине. Замер на
     375px с потолком от вьюпорта: левый край панели уезжал на −4…−7px.
     Второе слагаемое — страховка для панелей с узким якорем.
     У панели с УЗКИМ якорем (кнопка, а не заголовок карточки) этот потолок
     наоборот душит — см. `.tip-narrow-anchor` ниже. */
  max-width: min(100%, calc(100vw - 32px));
  max-height: min(620px, 76vh);
  overflow-y: auto; overscroll-behavior: contain;
  padding: 14px 16px;
  border: 1px solid var(--glass-edge); border-radius: var(--radius-md);
  background-color: var(--color-surface-solid);
  background-image: var(--glass-sheen),
                    linear-gradient(var(--glass-bg-chrome), var(--glass-bg-chrome));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-specular);
  text-align: left; font-weight: var(--fw-regular); letter-spacing: normal;
}
.tip-panel[hidden] { display: none !important; }
/* ⚠️ ЯКОРЬ ШИРИНОЙ С КНОПКУ — СНИМАЕТ ПОТОЛОК «ОТ ЯКОРЯ». Общий потолок
   `min(100%, …)` считает `100%` от позиционированного предка: когда предок —
   заголовок карточки, это ровно то, что нужно, а когда обёртка кнопки (полоса
   кабинета, шапка плитки) — панель схлопывается до ширины кнопки. Замер ДО
   правки: `#dk-stats-tip` объявлен на 320px, фактически 202.3px и 620px в
   высоту с внутренней прокруткой, то есть справка читалась в колодце.
   Панель при этом всё равно не выходит за экран: `right: 0` держит её правый
   край у якоря, а якорь стоит внутри карточки. */
.tip-panel.tip-narrow-anchor { max-width: calc(100vw - 32px); }
/* Узкий экран — поля панели меньше. Правило ЖИЛО В `_tip_panel.html`, то есть
   доставалось только страницам, подключившим партиал; «Клиенты» его не
   подключают, и там панель на 375px оставалась с полями десктопа. Перенесено
   сюда 08.08 вместе с переводом `.hd-tip` на общую оболочку. */
@media (max-width: 560px) {
  .tip-panel { padding: 12px 13px; }
}
/* Фокус на панель ставится ПРОГРАММНО (предохранитель от случайного Enter),
   управляющим элементом она не является — кольцо во всю панель читалось бы
   как ошибка ввода. Кольца кнопок внутри целы. */
.tip-panel:focus, .tip-panel:focus-visible { outline: none; }
/* Блюр отключён системой — панель становится плотной, а не полупрозрачной без
   размытия. Дословно то же, что этот файл делает с `.glass`. */
@media (prefers-reduced-transparency: reduce) {
  .tip-panel {
    backdrop-filter: none; -webkit-backdrop-filter: none;
    background-image: none;
    background-color: var(--color-surface-solid);
    border-color: var(--color-rule);
    box-shadow: var(--shadow-md);
  }
}
/* Заголовок панели — один на все: капитель, разрядка, приглушённый цвет.
   ⚠️ ЗАГОЛОВОК ЕСТЬ У КАЖДОЙ ПАНЕЛИ. Панель вызывает БЕЗЫМЯННАЯ иконка, и без
   строки-названия человек читает содержимое, ещё не зная, чтó открылось: до
   08.08 так вели себя пять панелей из пятнадцати («добавить чаты», рассылка,
   очередь на вступление, как идут касания, балансы). Исключение ровно одно и
   оно решение владельца: `.acc-peek` — «панель без слов».
   ⚠️ ВТОРОЙ ЗАГОЛОВОК В ПАНЕЛИ — отбивка сверху правилом, а не `style=""` в
   разметке: инлайновый `margin-top: 18px` у второй группы в «настройке
   работы» кабинета был единственной такой строкой на дашборде. */
.tip-h {
  display: block; margin: 0 0 10px;
  font-size: var(--text-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-muted);
}
.tip-h:not(:first-child) { margin-top: 18px; }
/* Строка «иконка + текст» — форма пояснений внутри панели. Абзацем такие
   вещи не пишут: их читают взглядом сверху вниз, а иконка держит колонку. */
.tip-list { display: flex; flex-direction: column; gap: 10px; margin: 0; }
.tip-item { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 10px; align-items: start; }
/* ⚠️ РАЗМЕР И ТОЛЩИНУ ШТРИХА ЗАДАЁТ CSS, А НЕ АТРИБУТ В РАЗМЕТКЕ (08.08).
   Замер ДО правки: у «Чатов» и «Заявок» в панелях стоял `stroke-width="1.8"`,
   у кабинета — `1.9`; на 16px это видимая разница в плотности, а рисунок
   рядом с текстом обязан читаться как один набор. Правило проекта — 1.9 при
   фигуре ≈20 единиц из 24, то же число, что у глифа `.ibtn`. CSS перебивает
   презентационный атрибут, поэтому одной строкой чинится вся разметка и
   новая панель приезжает правильной сама.
   `.tip-ic` — историческое имя того же глифа в кабинете менеджера; класс не
   переименовывали (он стоит в разметке), но правило у них общее. */
.tip-item > svg, .tip-item > .tip-ic {
  width: 16px; height: 16px; flex: none; margin-top: 2px;
  stroke-width: 1.9; color: var(--color-muted);
}
.tip-item-t { font-size: var(--text-s); line-height: 1.45; color: var(--color-text-soft); }
/* ВЫДЕЛЕНИЕ В ТЕКСТЕ — ОДНИМ ТОКЕНОМ НА ВЕСЬ ДАШБОРД (правило владельца 03.08
   «выделяем одним цветом», цвет с 23.08 — `--hi`, предельный нейтраль темы).
   Здесь перекраска работает в полную силу: вокруг `--color-text-soft`, а
   выделенное слово уходит в чернильный (светлая) или белый (тёмная), то есть
   разрыв ровно такой же, как у активного пункта меню. */
.tip-item-t b, .tip-em { color: var(--color-accent-text); font-weight: var(--fw-strong); }
/* ⚠️ ЗАГОЛОВОК СТРОКИ — ОТДЕЛЬНОЙ СТРОКОЙ (владелец, 21.08: «все панели с «i»
   должны быть с иконками, заголовки выдели оранжевым — это дизайн-система
   информационных иконок»). Раньше выделение жило внутри фразы (`<b>` в начале
   предложения); в панели на семь пунктов такой заголовок не отделялся от
   текста, и список читался сплошняком. Цвет тот же, что у всех выделений:
   правило «акцент только оранжевым» одно на дашборд, второго тона не заводим.
   Класс, а не `b:first-child`: в `chats.html` выделения стоят и посреди фразы,
   и разделять их по позиции значило бы угадывать смысл по вёрстке. */
.tip-item-h {
  display: block; margin-bottom: 2px;
  color: var(--color-accent-text); font-weight: var(--fw-strong);
}
/* Пара «подпись — число» внутри панели: числа сравниваются столбиком. */
.tip-rows { display: flex; flex-direction: column; }
.tip-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  /* ⚠️ Зазор РАЗНЫЙ по осям: 12px между подписью и числом, 2px до пояснения
     третьей строкой. С общим `gap: 12px` пояснение отрывалось от своей пары
     ровно настолько, чтобы читаться подписью СЛЕДУЮЩЕЙ строки — поймано на
     кадре сводки «Клиентов» после перевода её на этот кирпич. */
  align-items: baseline; gap: 2px 12px;
  padding: 8px 0; border-bottom: 1px solid var(--color-rule);
  font-size: var(--text-s);
}
.tip-row:last-child { border-bottom: 0; padding-bottom: 0; }
.tip-row:first-child { padding-top: 0; }
.tip-row-k { color: var(--color-text-soft); }
.tip-row-v {
  justify-self: end; font-variant-numeric: tabular-nums; font-feature-settings: "tnum";
  font-weight: var(--fw-heavy); color: var(--color-text);
}
/* ⚠️ ЧИСЛО В ПАНЕЛИ — НЕЙТРАЛЬНОЕ. Оранжевый здесь закреплён за смыслом «вот
   ради чего строку читают» (`.tip-em`), а не за «тут цифра»: покрасив все
   значения, тревогу становится нечем отличить от нормы. Единственный
   модификатор — `.alarm`, тем же text-safe токеном. */
.tip-row-v.alarm { color: var(--color-warning-text); }
/* ТРЕТЬЯ СТРОКА ПАРЫ — пояснение под «подписью и значением». Заведена 08.08,
   когда три разные копии этой строки («Сводка» на «Клиентах», «Запас» на
   «Заявках», подписи под числами) сводились в один кирпич. Занимает обе
   колонки: у пояснения нет своего значения справа. */
.tip-row-s {
  grid-column: 1 / -1;
  font-size: var(--text-2xs); line-height: 1.4; color: var(--color-muted);
}
.tip-note { margin: 10px 0 0; font-size: var(--text-2xs); line-height: 1.45; color: var(--color-muted); }

/* ============================================================
   ИКОНОЧНАЯ КНОПКА — ОДИН КЛАСС НА ВЕСЬ ДАШБОРД (`.ibtn`, 03.08)
   Правило владельца: «опять кнопки и иконки разные — делай все иконками».
   До этого управляющих элементов без подписи было ПЯТЬ разных видов, и они
   стояли в одной строке друг с другом (замер 03.08, «Клиенты» + «Чаты»):
     `.add-open`   44×44, круг,      глиф 15.5px
     `.hd-btn`     30×30, круг,      глиф 15.5px
     `.qs-btn`     39×30, пилюля,    глиф 15.5px
     `.row-*`      30×30, r=9px,     глиф 12.4px
     `.cl-chev`    28×28, r=8px,     глиф 8.8px
   То есть три формы, четыре габарита и три размера рисунка. Теперь форма,
   размер, обводка и поведение живут ЗДЕСЬ, а страницы только вешают класс.
   Второй такой базы заводить нельзя — ровно из-за копий всё и разъехалось
   (та же история, что у `.tip-panel` выше).

   ФОРМА — КРУГ, и это не вкус. (1) Круглыми уже были обе шапки и все кнопки
   «Чатов», скруглённый квадрат оставался только у `.row-*`/`.cl-chev` на одной
   странице. (2) В строке таблицы квадрат со скруглением 9px попадает в зазор
   между радиусом самой строки (`--radius-sm`) и радиусом карточки — три почти
   одинаковых скругления вложены друг в друга, и кнопка читается маленькой
   панелью, а не кнопкой. Круг из этого ряда выходит и встаёт в пару к аватару.

   РАЗМЕР ОДИН. Второй («компактный для строк») сознательно НЕ заводится: все
   эти кнопки — равноправные действия над одной сущностью, иерархии между ними
   нет, а любая вторая величина назавтра становится третьей. Разницу между
   «действием» и «справкой» несёт не габарит, а рисунок и позиция.

   ⚠️ КЛАССЫ-СТАРОЖИЛЫ (`.row-edit`, `.row-del`, `.add-open`, `.qs-btn`, …) с
   элементов НЕ сняты: на них висят делегированные слушатели страниц. `.ibtn`
   добавляется рядом и перебивает их геометрию порядком в файле — отсюда префикс
   `:root` у части правил, он нужен, чтобы обойти `.row-pause.on:hover` и
   темовые варианты выше. (Второй их держатель — `MAGNET_SEL` в `dashboard.js` —
   снят 07.08 вместе с магнитом: движения иконки под курсором больше нет нигде.)
   ============================================================ */
:root {
  --ib-size: 30px;   /* посадочное место (видимый кружок) */
  --ib-glyph: 17px;  /* сторона тега <svg> */
  /* ⚠️ ТЕЛЕГРАМ-МОДЕЛЬ ИКОНКИ (владелец, 07.08). Дословно: «При наведении на
     иконку „создать аккаунт“ у нас сразу подсвечивается серая зона за иконкой.
     Просто эту серую зону надо убрать, а то есть — только при нажатии она
     выделяется вот этим вот серым».
     Отсюда ТРИ чернила и ОДНА подложка. Подложка служит ДВУМ разным вещам, и
     это не экономия, а смысл: «нажато сейчас» и «включено и держится» обязаны
     выглядеть одинаково, иначе включающий клик мигал бы сменой материала
     (владелец, 07.08: «при нажатии, если кнопка активна, фон должен
     оставаться»). Полная матрица — у блока «СТОЯЩЕЕ СОСТОЯНИЕ» ниже.
       --ib-ink      покой      — приглушённый глиф;
       --ib-ink-hi   наведение  — глиф ЯРЧЕ, и это ЕДИНСТВЕННОЕ, что меняется:
                                  ни дна, ни рамки, ни кольца под курсором;
       --ib-ink-off  выключено  — гасим ЦВЕТОМ, не `opacity` (замер: 0.4 уводит
                                  глиф на 2.4:1 при пороге 3:1);
       --ib-press-bg нажатие И  — та самая серая зона. Она осталась, но переехала
                     включено     с ховера на `:active`, а у ПЕРЕКЛЮЧАТЕЛЯ
                                  остаётся и после отпускания — пока включено.
     Значения править ЗДЕСЬ. Страница, которая красит иконку своим правилом,
     ломает модель — ровно так до 07.08 жили `.row-*` (синие и янтарные заливки
     с рамками под курсором) и `.acc-bal-topup`. */
  --ib-ink:      var(--color-text-soft);
  --ib-ink-hi:   var(--color-text);
  --ib-ink-off:  var(--color-muted);
  --ib-press-bg: var(--fill-2);
  /* ЗАЛИТАЯ КНОПКА (`.ibtn-fill`) — своя лесенка дна из трёх ступеней.
     ⚠️ Общая модель «наведение красит ТОЛЬКО глиф» на неё не переносится:
     у голой иконки под курсором меняется единственное, что у неё есть, —
     колер глифа; у залитой глиф уже стоит на `--ib-ink-hi` В ПОКОЕ (иначе он
     не читался бы на дне), и «стать ярче» ему просто некуда. До 08.08 из-за
     этого `:hover` был ПУСТЫМ: и дно (`--fill-1`), и колер повторяли покой,
     то есть кнопка отправки в поле переписки на наведение не отвечала ничем —
     ровно то, на что владелец и указал («при наведении она должна тоже
     подсвечиваться»). Отвечает ДНО: покой → наведение → нажатие, три разные
     ступени одного материала. Нажатие обязано отличаться и от наведения,
     поэтому у него своя, четвёртая плотность `--fill-2` тут уже занята. */
  --ib-fill-bg:    var(--fill-1);
  --ib-fill-hi:    var(--fill-2);
  --ib-fill-press: rgba(120, 120, 128, 0.24);
}
/* Четвёртая ступень — единственный `--ib-*`, у которого своё значение по темам:
   у общей шкалы `--fill-*` четвёртой плотности нет, а в тёмной теме вся шкала
   вдвое плотнее (0.16/0.24/0.32 против 0.06/0.10/0.15). Ставится рядом с самой
   шкалой, а не в палитре темы за три тысячи строк отсюда: искать его будут
   здесь. */
:root[data-theme="dark"] { --ib-fill-press: rgba(120, 120, 128, 0.46); }
.ibtn {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; position: relative;
  box-sizing: border-box;
  width: var(--ib-size); height: var(--ib-size); min-width: var(--ib-size);
  min-height: var(--ib-size); padding: 0;
  /* ⚠️ КОЛЬЦА НЕТ — решение владельца 03.08 («уберём кружки, оставим только
     иконки»), сначала на «Аккаунтах», в тот же день на всём дашборде.
     `border` ОСТАЁТСЯ прозрачным, а не удаляется: он держит габарит (у нас
     `box-sizing: border-box`, но состояния ниже возвращают видимую рамку, и без
     этой строки они меняли бы размер кнопки). `border-radius` тоже остаётся —
     его использует дно у стоящего состояния и у залитой кнопки. */
  border: 1px solid transparent; border-radius: 999px;
  background: transparent; color: var(--ib-ink);
  font: inherit; cursor: pointer;
  /* Магнит к курсору ставит dashboard.js ОТДЕЛЬНЫМ свойством `translate`,
     сжатие при нажатии — через `--press` в `transform`; браузер их складывает.
     Не сводить в одно свойство: обработчик перетирал бы чужое значение. */
  transform: scale(var(--press, 1));
  transition: color var(--dur-2) var(--ease-out-quart),
              border-color var(--dur-2) var(--ease-out-quart),
              background var(--dur-2) var(--ease-out-quart),
              transform var(--dur-2) var(--ease-pop),
              translate var(--dur-2) var(--ease-out-quart);
}
.ibtn:active { --press: 0.92; }
/* [hidden] у UA-стиля слабее авторского display — гасим руками. */
.ibtn[hidden] { display: none !important; }
/* ⚠️ РАЗМЕР ГЛИФА ЗАДАЁТСЯ ЗДЕСЬ И БОЛЬШЕ НИГДЕ. У lucide фигуры внутри общего
   `viewBox 0 0 24 24` нарисованы РАЗНОЙ величины (люди — 20 единиц, корзина и
   карандаш — 18, мишень — 17, пауза — 16, шеврон — 12), поэтому одинаковый
   `width` у <svg> давал разный видимый размер. Правило проекта: бóльшая сторона
   фигуры = 20 единиц при обводке 1.9. Приводится оно НЕ пересчётом координат
   (там дуги и радиусы, легко ошибиться), а поджатием `viewBox` вокруг центра:
   для фигуры со стороной S ставим `viewBox` со стороной 1.2·S, смещением
   12 − 0.6·S и `stroke-width` 0.095·S. Тогда рисунок занимает ровно 20/24 поля,
   а штрих — ровно 1.9/24, как у эталонных иконок шапки. Проверять надо
   `getBBox()`, а не атрибуты.
   `overflow: visible` обязателен: `viewBox` поджат по фигуре БЕЗ обводки, и
   половина штриха выходит за край. Он же спасает кончик хвоста у самолётика,
   которому оптический сдвиг вбит в координаты. */
.ibtn > svg, .ibtn > span > svg {
  width: var(--ib-glyph); height: var(--ib-glyph);
  flex: none; overflow: visible;
}
/* Кликабельная зона — 44px (Apple HIG / WCAG 2.5.5), видимый кружок остаётся
   30px: иконка величиной с палец читается кнопкой действия, а не справкой.
   Зона невидимая, поэтому соседние кнопки в ряду её перекрывают — расстояние
   между ними в строке 6px, и верхняя по документу забирает наплыв. На
   `pointer: coarse` псевдоэлемент снимается (см. ниже): там кружок сам 44px, и
   двойное расширение уводило бы попадание к соседу. */
.ibtn::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: max(44px, 100%); height: max(44px, 100%);
  transform: translate(-50%, -50%);
}
/* НАВЕДЕНИЕ — МОНОХРОМ (откат 03.08 по замечанию владельца). Оранжевый тут
   стоял один день и был ошибкой прочтения: тёплым в Lidz помечается ВЫДЕЛЕННАЯ
   СТРОКА списка или таблицы, а кнопка — не строка. Поведение возвращено к
   дизайн-системе 31.07 («монохром, синевы нет»), общей с текстовыми кнопками
   `.btn-mini`/`.btn-ghost`: в покое приглушённый глиф, под курсором обычный
   цвет текста плюс лёгкая нейтральная заливка и рамка.
   Заливка `--fill-1` под курсором и `--fill-2` у СТОЯЩЕГО состояния (ниже) —
   две разные ступени одного материала: наведение отпускает вместе с курсором,
   состояние держится без него и подкреплено `aria-expanded`. */
:root .ibtn:hover {
  /* Без кольца наведение показывает ТОЛЬКО цвет глифа: заливка без рамки
     читается грязным пятном, а рамка вернула бы кружок, который и убирали.
     ⚠️ ТРИ ОБНУЛЕНИЯ НИЖЕ — НЕ ИЗБЫТОЧНОСТЬ. Они гасят чужие правила, которые
     красили эту же кнопку под курсором: `.row-edit/.row-trial/.row-out` давали
     синюю таблетку с рамкой, `.row-pause` — янтарную, `.add-open` — серую.
     Все эти классы висят на элементах ВМЕСТЕ с `.ibtn` (сняты они быть не
     могут — на них делегированные слушатели), поэтому единственный способ
     держать вид в одном месте — обнулять их отсюда явно. */
  color: var(--ib-ink-hi);
  background: transparent;
  border-color: transparent;
}
/* НАЖАТИЕ — вот здесь и ТОЛЬКО здесь появляется серая зона (владелец, 07.08:
   «только при нажатии она выделяется вот этим вот серым»). Это та же заливка,
   что до 07.08 зажигалась под курсором, — она не удалена, а переехала на жест,
   который её оправдывает.
   ⚠️ ПРАВИЛО ОБЯЗАНО СТОЯТЬ ПОСЛЕ `:hover`. Нажатая кнопка одновременно и
   `:hover`, и `:active`, специфичность у обоих (0,3,0) — побеждает то, что
   ниже по файлу. Поставленное выше, оно молча не работало: замер показывал
   `matches(":active") === true`, `--press: 0.92`, а фон транспарентный. */
:root .ibtn:active { background: var(--ib-press-bg); color: var(--ib-ink-hi); }
/* Кольцо фокуса живёт НИЖЕ ВСЕХ состояний — см. отдельный блок в конце секции.
   ============================================================
   СТОЯЩЕЕ СОСТОЯНИЕ — СЕРЫЙ КРУГ ДЕРЖИТСЯ, ПОКА СОСТОЯНИЕ ВКЛЮЧЕНО
   (владелец, 07.08, дословно, глядя на вкладку «Клиенты»): «При нажатии, если
   кнопка активна, фон — этот серый круг — должен оставаться, тогда понятно, что
   кнопка нажата. Если ещё раз нажать, то серый фон пропадает — то есть она
   неактивна».
   Отсюда МАТРИЦА СОСТОЯНИЙ ИКОНКИ, и других состояний у неё нет:

     жест / состояние │ глиф          │ дно
     ─────────────────┼───────────────┼──────────────────────
     покой            │ --ib-ink      │ нет
     наведение        │ --ib-ink-hi   │ нет  ← и это неизменно (07.08)
     нажатие          │ --ib-ink-hi   │ --ib-press-bg
     ВКЛЮЧЕНО         │ --ib-ink-hi   │ --ib-press-bg, держится без курсора
     фокус клавиатуры │ без изменений │ + кромка --focus-edge и её уплотнение
                      │               │   --ring-focus-edge (ровно как у поля;
                      │               │   гало снято 08.08)
     выключено        │ --ib-ink-off  │ нет

   ⚠️ У ЗАЛИТОЙ (`.ibtn-fill`) МАТРИЦА ДРУГАЯ — она отвечает ДНОМ, а не глифом:
   покой --ib-fill-bg → наведение --ib-fill-hi → нажатие --ib-fill-press.
   Причина у блока этих токенов выше; переносить сюда «наведение дна не
   трогает» нельзя, у залитой глиф ярок уже в покое.

   Дно «нажато» и дно «включено» — ОДИН И ТОТ ЖЕ материал намеренно
   (`--ib-press-bg`): включающий клик тогда не мигает — палец отпустил, а круг
   остался; выключающий клик, наоборот, круг гасит. Ровно это владелец и описал.

   ⚠️ РАЗЛИЧАТЬ ДВА РОДА ИКОНОК:
   • ДЕЙСТВИЕ (удалить, отправить, добавить) состояния не имеет — дно живёт
     только момент нажатия. Признака «включено» у неё нет, и заводить его
     нельзя: круг после клика читался бы как «действие всё ещё идёт».
   • ПЕРЕКЛЮЧАТЕЛЬ (раскрытие карточки, панель, режим) состояние имеет, и оно
     обязано быть видно И в ARIA: `.on` для режима, `aria-pressed` для тумблера,
     `aria-expanded` для раскрытия. Цвет — не единственный носитель смысла.

   ТРИ ПРИЗНАКА ВКЛЮЧЁННОСТИ, и все три рисуются ОДИНАКОВО:
   • `.on` — РЕЖИМ, включённый кликом (выдача на паузе, раскрытые исходы).
     Класс, а не атрибут, потому что подпись кнопки при этом меняется целиком
     («Поставить на паузу» → «Возобновить выдачу»), и `aria-pressed` рядом с
     такой подписью противоречил бы ей.
   • `aria-pressed="true"` — тумблер с НЕИЗМЕННОЙ подписью.
   • `aria-expanded="true"` — раскрытая панель/карточка/форма.

   ⚠️ ЕДИНСТВЕННОЕ ИСКЛЮЧЕНИЕ — ПАНЕЛЬ, КОТОРУЮ РАСКРЫВАЕТ НАВЕДЕНИЕ
   (`LH_tipPanel`, `initPop` на «Клиентах», `.acc-add` на «Аккаунтах»). У них
   `aria-expanded` становится `true` в тот же миг, что и `:hover`, — и дно
   читалось бы как серая зона, зажигающаяся под курсором, то есть ровно то, что
   владелец просил убрать 07.08 («просто эту серую зону надо убрать»). Замер
   07.08 на `#acc-add-shard`: покой `rgba(0,0,0,0)` → ховер
   `rgba(120,120,128,0.15)` при уже выставленном `aria-expanded`.
   Такие кнопки помечает САМ МЕХАНИЗМ РАСКРЫТИЯ атрибутом `data-hover-panel` —
   не страница и не рука: заведут новую панель тем же механизмом, и она получит
   пометку сама. Пока курсор рядом, что открыто, видно по самой панели.
   На тач-устройстве ховера нет вовсе: там панель открывают ТАПОМ, она держится
   без пальца — дно остаётся, иначе состояние показать нечем, поэтому гашение
   заперто в `@media (hover: hover) and (pointer: fine)`.

   ⚠️ БЛОК ОБЯЗАН СТОЯТЬ ПОСЛЕ `:hover` И `:active` — специфичность у всех троих
   (0,3,0), побеждает то, что ниже по файлу. Он же обязан стоять ДО
   `.ibtn-danger:hover` и `[disabled]`: тем и красный под курсором, и отказ
   нужны поверх состояния, а вес у них тот же.
   ============================================================ */
:root .ibtn.on,
:root .ibtn[aria-pressed="true"],
:root .ibtn[aria-expanded="true"] {
  background: var(--ib-press-bg); border-color: transparent;
  color: var(--ib-ink-hi);
}
/* ⚠️ ГАШЕНИЕ ДНА У HOVER-ПАНЕЛЕЙ СНЯТО 06.09 (владелец: «когда кнопка нажата,
   под ней должен быть круг, как у кнопки исходов в карточках клиентов»).
   Панели больше не открываются наведением, значит `aria-expanded="true"` —
   это всегда нажатие, и круг `--ib-press-bg` показывается у всех одинаково. */
/* ДЕСТРУКТИВ. Форма та же — различают четыре вещи, и только одна из них цвет:
   (1) рисунок (корзина против карандаша/облачка), (2) позиция — всегда
   последняя в ряду, (3) обязательное inline-подтверждение перед действием,
   (4) КРАСНЫЙ под курсором вместо общего оранжевого. Красный в Lidz закреплён
   за удалением, и это единственное исключение из монохрома (правило 31.07). */
:root .ibtn-danger:hover { color: var(--color-hunter-hi); }
/* ЗАЛИТАЯ — утвердительное действие ВНУТРИ поля или подтверждения (отправить,
   купить). Геометрия и глиф общие, отличается только материал: у неё есть дно,
   потому что она главная в своём блоке и обязана читаться первой. */
.ibtn-fill {
  background: var(--ib-fill-bg); border-color: transparent;
  color: var(--ib-ink-hi);
}
/* Дно у залитой ПОСТОЯННОЕ, поэтому «убрать серую зону под курсором» её не
   касается — убирать нечего, зона тут не появляется, а есть всегда.
   ⚠️ ОТВЕЧАЕТ НА НАВЕДЕНИЕ ОНА ДНОМ, А НЕ ГЛИФОМ (владелец, 08.08: «в
   диалогах в текстбоксе есть кнопка „отправить“ — при наведении она должна
   тоже подсвечиваться»). До этой правки оба правила повторяли покой
   (`--fill-1` + `--ib-ink-hi`), то есть наведение не меняло НИЧЕГО: у голой
   иконки под курсором ярче становится глиф, а тут он и в покое уже яркий.
   Лесенка из трёх ступеней живёт в токенах `--ib-fill-*` (см. §их блок):
   покой → наведение → нажатие. */
:root .ibtn-fill:hover { background: var(--ib-fill-hi); color: var(--ib-ink-hi); }
:root .ibtn-fill:active { background: var(--ib-fill-press); color: var(--ib-ink-hi); }
/* Недоступное действие видно, но не кликается. Гасим ЦВЕТ ГЛИФА, а не всю
   кнопку `opacity`: кольцо обязано остаться в полную силу — «выключено»
   отличается от «отсутствует» (замер: opacity 0.4 уводил глиф на 2.4:1 при
   пороге 3:1 для органов управления). */
:root .ibtn[disabled],
:root .ibtn[aria-disabled="true"] {
  color: var(--ib-ink-off); background: transparent;
  border-color: transparent; cursor: not-allowed;
}
:root .ibtn-fill[disabled] { background: transparent; }
/* Погашенная не оживает ни под курсором, ни под пальцем: ни цвета, ни серой
   зоны, ни сжатия. Специфичность (0,4,0) — она обязана бить и общий
   `:root .ibtn:active` (0,3,0), иначе отказ выглядел бы как срабатывание. */
:root .ibtn[disabled]:hover,
:root .ibtn[aria-disabled="true"]:hover,
:root .ibtn[disabled]:active,
:root .ibtn[aria-disabled="true"]:active {
  color: var(--ib-ink-off); background: transparent; --press: 1;
}
/* ⚠️ ФОКУС С КЛАВИАТУРЫ — ПОСЛЕДНИМ, И ЭТО НЕ ПОРЯДОК «ПО ВКУСУ». Правило
   обязано перебить ВСЕ состояния выше: у `.on`, `[aria-expanded]`, `[disabled]`
   и общего `:hover` в декларации стоит `border-color: transparent`,
   специфичность у всех (0,3,0), и стоя раньше — кромка фокуса молча гасла бы
   ровно на раскрытой или погашенной кнопке. `aria-disabled` при этом остаётся
   в табе намеренно (причину отказа надо где-то прочитать), значит выделять её
   тоже нужно.
   ⚠️ КОЛЬЦО ФОКУСА НЕПРИКОСНОВЕННО — это доступность, а не украшение. Убираем
   шум мыши, а не индикацию фокуса. `:root` — чтобы правило стояло выше
   страничных `outline: 2px solid …` (они рисуют вторую, чужую рамку поверх).
   ⚠️ ВЫДЕЛЕННАЯ ИКОНКА ВЫГЛЯДИТ КАК ВЫДЕЛЕННЫЙ ТЕКСТБОКС (владелец, 07.08:
   «если выделяем какой-либо текстбокс, он у нас выделяется синей рамкой —
   давай выделять так же и иконки»). Рецепт у поля и у иконки один и ровно из
   двух частей: кромка `--focus-edge` + её уплотнение `--ring-focus-edge`.
   Прозрачная рамка у `.ibtn` заведена в том числе ради этого — габарит не
   меняется, когда кромка становится видимой.
   ⚠️ СИНЕВЫ И ГАЛО ЗДЕСЬ БОЛЬШЕ НЕТ (владелец, 08.08). Стояло
   `box-shadow: var(--ring-focus)` — 3px размытого синего вокруг кнопки; теперь
   кромка просто становится двухпиксельной внутренней тенью того же цвета.
   Цвет живёт в `--focus-edge` (серый в светлой теме, белый в тёмной) — правится
   там и только там, у иконки своего значения нет.
   Показывается по `:focus-visible`, а не по `:focus`: клик мышью ставит фокус
   тоже, и обведённый круг после каждого нажатия был бы шумом, а не выделением.

   ⚠️ КРОМКА ПРОЯВЛЯЕТСЯ ЗА `--dur-2` (170 мс) — она в списке `transition` у
   `.ibtn`, а тень нет. При замере это ловушка: `getComputedStyle` сразу после
   Tab возвращает `border-color: rgba(0,0,0,0)` при уже выставленном
   `:focus-visible`, и это выглядит как «правило не сработало». Мерить надо
   через ~200 мс после фокуса. Проверено 08.08 в обеих темах: светлая
   `rgb(107,107,120)`, тёмная `rgb(255,255,255)` — совпадает с тенью. */
:root .ibtn:focus-visible {
  outline: none;
  border-color: var(--focus-edge);
  box-shadow: var(--ring-focus-edge);
}
/* Палец: кружок сам становится тач-таргетом, псевдозона больше не нужна. */
@media (pointer: coarse) {
  :root { --ib-size: 44px; --ib-glyph: 20px; }
  .ibtn::after { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ibtn { transition: none; translate: none !important; }
  /* `:root` обязателен: общий `:root .ibtn:active` (0,3,0) иначе перебьёт этот
     сброс и сжатие останется там, где движение просили не показывать.
     Серая зона нажатия при этом ОСТАЁТСЯ — она не движение, а обратная связь. */
  :root .ibtn:active { --press: 1; }
}

/* ============================================================
   19. ФОРМЫ ПО ОБРАЗЦУ ФОРМЫ КЛИЕНТА (03.08)
   ------------------------------------------------------------
   Владелец 03.08: «форму клиента по такому подобию должны». Эталон —
   панель под «+» на «Клиентах» (`business.html`, `.af-tip`). До этой
   правки его рецепт жил ИНЛАЙНОМ в `business.html`, то есть на других
   страницах его физически не было: «Конкуренты» несли свою копию
   образца ДО 03.08 (цветные цифры шагов, поля радиусом 10px, синий
   фокус), правки внутри строк «Клиентов» — третью.
   Здесь ОДНО определение на всех. Страница подключает его классом
   `lh-form` на контейнере формы и больше про материал полей ничего не
   знает. Второй базы не заводить — ровно из-за копий всё и разъехалось.

   ⚠️ Область — `.lh-form`, а не голый `.form-row`: `.form-row` живёт
   и там, где форм нет (фильтры, поиск), а базовое правило §7 остаётся
   как было, чтобы правка не задела ничего вслепую.
   ============================================================ */

/* --- ПОЛЯ: материал текстбокса из переписки (`.tg-ta`) ------------------
   Сплошная подложка `--color-surface-solid`, тонкая рамка `--color-rule`,
   скругление в половину высоты, кольцо `--ring-focus` на фокусе. Берётся
   ТОЛЬКО внешний вид: авторост, вырез под кнопку и черновики композера
   форме чужие.
   Два отличия от базового `.form-row input`, оба осознанные:
     · рамка `--color-rule` вместо `--color-rule-strong` — как у композера;
     · фокус уводит рамку в `--color-rule-strong`, а НЕ в синий
       `--color-lead`: синеву из органов управления убрали 31.07, базовое
       правило §7 её пережило.
   ВЫСОТА ЗАДАНА ЧИСЛОМ и полю, и списку: `min-height` у input и select
   считается от разного содержимого, и в одном ряду они разъезжаются на
   1–2px. 8 + 22 + 8 + 2 рамки = ровно 40. */
.lh-form input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
.lh-form select,
.lh-form textarea {
  box-sizing: border-box;
  font-family: var(--font-sans);
  /* `--text-base` — «поле ввода» по шкале (§1). Именно с ним живёт эталон:
     его инлайн высоту и рамки задавал, а размер шрифта брал у базового
     `.form-row input`. Ставим явно, чтобы он не зависел от порядка правил. */
  font-size: var(--text-base);
  color: var(--color-text);
  background: var(--color-surface-solid);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-field);
  outline: none;
  transition: border-color var(--dur-2) var(--ease-out-quart), box-shadow var(--dur-2) var(--ease-out-quart);
}
.lh-form input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
.lh-form select {
  height: 40px; min-height: 40px;
  padding: 8px 12px; line-height: 22px;
}
.lh-form textarea {
  line-height: 1.5;
  padding: 10px 14px;
  resize: vertical;
}
/* Кромка выделенного поля — общий токен `--focus-edge` (07.08). Здесь стоял
   нейтральный `--color-rule-strong`, а поля правки подписки и строка поиска
   красили кромку синим: одно и то же «выделено» выглядело на одной странице
   двумя разными способами. Теперь рецепт один и он же у иконочной кнопки.
   ⚠️ 08.08 из рецепта убраны СИНЕВА и ГАЛО (владелец: «должен серым или белым
   ярким», «расходящейся тени быть не должно»). Заметность добрана толщиной:
   `--ring-focus-edge` — внутренняя тень 1px тем же цветом, то есть кромка
   выделенного поля вдвое плотнее спокойной, а габарит не меняется. */
.lh-form input:focus,
.lh-form select:focus,
.lh-form textarea:focus,
.lh-form textarea:focus-visible {
  border-color: var(--focus-edge);
  box-shadow: var(--ring-focus-edge);
}
/* iOS Safari зумит страницу при фокусе в поле со шрифтом <16px и не отъезжает. */
@media (max-width: 480px) {
  .lh-form input,
  .lh-form select,
  .lh-form textarea { font-size: 16px; }
}

/* --- ПОДПИСЬ ПОЛЯ: иконка + слово ---------------------------------------
   Глиф не сжимается флексом и не спорит с подписью по весу: цвет тот же,
   что у самой подписи, то есть иконка — часть подписи, а не второй акцент.
   Размер рисунка держится общим правилом дашборда (фигура ~20 единиц из 24
   при обводке 1.9), иначе в одном столбце окажутся глифы разного веса. */
.f-lbl { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.f-ic  { width: 14px; height: 14px; flex: 0 0 auto; opacity: .85; }

/* --- ВЫПАДАЮЩИЙ СПИСОК ---------------------------------------------------
   Системное оформление снято: у нативного select своя стрелка, свой радиус
   и своя высота в каждой ОС, то есть рядом с полем ввода он остаётся другим
   материалом, сколько рамок ему ни задай.
   Стрелка — псевдоэлемент ОБЁРТКИ `.f-sel`, а не фон самого select: у
   `background-image` цвет пришлось бы дублировать data-URI'ем на каждую
   тему, а маска красится токеном и переключается вместе с темой сама.
   Обёртка нужна потому, что у select не бывает псевдоэлементов.
   Клавиатура не задета: `appearance: none` меняет только рисование —
   список остаётся в Tab-порядке и открывается пробелом/стрелками. */
.f-sel {
  position: relative; display: block; min-width: 0;
  --f-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 9 7 7 7-7'/%3E%3C/svg%3E");
}
.lh-form .f-sel > select { -webkit-appearance: none; appearance: none; padding-right: 34px; }
.f-sel::after {
  content: ""; position: absolute; right: 13px; top: 50%;
  width: 12px; height: 12px; transform: translateY(-50%);
  pointer-events: none; background: var(--color-text-soft);
  -webkit-mask: var(--f-chev) center / 12px 12px no-repeat;
          mask: var(--f-chev) center / 12px 12px no-repeat;
}

/* --- СПРАВКА ФОРМЫ: «i» В ПРАВОМ ВЕРХНЕМ УГЛУ ПАНЕЛИ (владелец, 23.08) ---
   «На всех выпадающих формах подсказки (как работает) убрать в иконки
   информации в правый верхний угол». Поведение — `LH_formHelp` в
   `_tip_panel.html`; здесь вид, и НОВОГО МАТЕРИАЛА он не заводит: кнопка —
   общий `.ibtn`, накрытие — общий `.tip-panel`, у которого переопределены
   только место и габарит.

   ⚠️ КНОПКА ЛЕЖИТ В САМОЙ ПАНЕЛИ, А НЕ В ЕЁ ЗАГОЛОВКЕ. Заголовки у форм
   разные (`.tip-h`, `.ed-head`, `.fset-head`, а у формы клиента их два), и
   вешать иконку внутрь каждого значило бы править четыре разных ряда. Угол
   панели один у всех, поэтому и правило одно. Заголовкам добавлен отступ
   справа, чтобы длинное название не уезжало под глиф. */
/* ⚠️ ОБЁРТКА БЕЗ СВОЕГО МЕСТА — АБСОЛЮТНА САМА КНОПКА. Это тот же приём, что у
   `.tip-anchor`: обёртка нужна ради СЛУШАТЕЛЕЙ (они висят на ней, и переход
   курсора «иконка → справка» уходом не считается), а место панель считает от
   ближайшего позиционированного предка. Сделаешь обёртку `absolute` — предком
   станет она, и накрытие схлопнется до ширины кнопки: замер до правки — 34px
   при панели 440px. */
/* ⚠️ ОБЁРТКА СПРАВКИ НЕ ДОЛЖНА БЫТЬ СТРОКОЙ ФОРМЫ (владелец, 23.08: «кнопка
   сохранения висит в пустоте»). Панели-формы разложены колонкой с зазором
   (`.cl-act` — flex, gap 12px), и обёртка НУЛЕВОЙ высоты всё равно получала
   свой зазор: под утвердительной кнопкой оставалось 27px вместо 15px поля
   панели, то есть подвал читался обрывком. Замер: `panel.bottom − foot.bottom`
   27.0 у панелей со справкой против 15.0 у той, где справки нет (форма
   долива) — разница ровно в зазор.
   `display: contents` убирает короб обёртки из раскладки целиком, а оба её
   потомка и так вне потока (`position: absolute` у кнопки и у накрытия),
   поэтому флекс-элементами они не станут. Слушатели живут на самой обёртке и
   этим не задеты: раскрытие наведением проверено кадром, элемент без короба
   по-прежнему получает `mouseenter` от своих потомков.
   ⚠️ АБСОЛЮТНОЙ ОБЁРТКУ ПО-ПРЕЖНЕМУ ДЕЛАТЬ НЕЛЬЗЯ (см. абзац выше): тогда
   содержащим блоком станет она, и накрытие схлопнется до ширины кнопки. */
.form-help { display: contents; }
.form-help-btn { position: absolute; top: 11px; right: 13px; z-index: 3; }
@media (max-width: 560px) { .form-help-btn { top: 9px; right: 10px; } }
/* Панель со справкой резервирует место под иконку в первой строке. 26px — это
   габарит `.ibtn` (`--ib-size`) минус его собственные поля; больше не нужно,
   меньше даёт наползание на длинный заголовок. */
.tip-panel:has(.form-help) > .tip-h,
.tip-panel:has(.form-help) > .ed-head { padding-right: 26px; }
/* ⚠️ НАКРЫТИЕ, А НЕ ВТОРАЯ ВСПЛЫВАЮЩАЯ ПАНЕЛЬ. Почему именно так — разбор в
   `LH_formHelp`: у родителя `backdrop-filter` (он же содержащий блок и область
   обрезки) и закрытие по уводу курсора. Дно СВОЁ и непрозрачное: под ним
   лежит форма, и текст, читаемый сквозь чужие поля, — это ровно та ошибка,
   из-за которой полосу подтверждения рассылки положили в два слоя. */
/* ⚠️ ВЫСОТА — ПО ТЕКСТУ, А НЕ ПО ФОРМЕ (владелец, 23.08: «почему такая большая
   форма с информацией»). До этого тут стоял `min-height: 100%`, и справка на
   два абзаца растягивалась на всю форму — размер не говорил ни о чём, потому
   что не зависел от содержимого. Ширина осталась родительской: она и не даёт
   накрытию вылезти за край экрана, считать место заново не нужно.
   Раз накрытие теперь ЧАСТИЧНОЕ, у него появился нижний край — его видно на
   фоне формы, и он обязан читаться как слой поверх, а не как обрыв: хватает
   кромки и тени (`--shadow-md`, тот же, что у прочих поверхностей второго
   слоя). Скруглением справка ПОЛЬЗУЕТСЯ РОДИТЕЛЬСКИМ — при длинном тексте она
   доходит до низа панели, и квадратные углы там торчали бы из её радиуса. */
.tip-panel .form-help-tip {
  position: absolute; inset: 0 0 auto 0;
  width: auto; max-width: none; max-height: none;
  margin: 0; border: 0; border-bottom: 1px solid var(--glass-edge);
  border-radius: inherit; box-shadow: var(--shadow-md);
  backdrop-filter: none; -webkit-backdrop-filter: none;
  background-image: none;
  background-color: var(--color-surface-solid);
  padding: 14px 40px 14px 16px;   /* справа — место под саму иконку */
  z-index: 2;
}
@media (max-width: 560px) { .tip-panel .form-help-tip { padding: 12px 36px 12px 13px; } }
.form-help-body { font-size: var(--text-s); line-height: 1.5; color: var(--color-text-soft); }
/* Абзацы внутри справки: свои отступы у перенесённых узлов остаются, но между
   ними нужен ритм — иначе два `.ed-note` подряд слипаются в стену.
   ⚠️ `display: block` И ОБЩИЙ КЕГЛЬ — НЕ КОСМЕТИКА (23.08). Под «i» уезжают и
   СТРОЧНЫЕ узлы: подпись под полем — это `<span class="ed-hint">`, и в справке
   она вставала в один абзац с предыдущим («…в цифрах выше не учтены.Пусто = не
   менять») да ещё и другим кеглем (`--text-3xs` против `--text-2xs` у
   `.ed-note`). Под полем разница в кегле означала «это мелкая подпись», в
   справке означать ей нечего: там всё — связный текст одного веса.
   `min-height` снимается по той же причине — он резервировал место под
   подпись «изменено», которой в справке не бывает. */
.form-help-body > * {
  display: block; margin: 0 0 10px;
  font-size: inherit; line-height: inherit; color: inherit; min-height: 0;
}
.form-help-body > *:last-child { margin-bottom: 0; }
.form-help-body b { color: var(--color-text); font-weight: var(--fw-strong); }

/* --- ПОЛЕ ГОРОДА: ВВОД С ПОДСКАЗКОЙ С ПЕРВОЙ БУКВЫ (владелец, 23.08) -----
   Дословно: «город должны вбивать в поиск и выпадает город, поиск городов
   начинает сразу когда я ввожу первую букву — такой поиск должен быть везде
   в дашборде, где встречаются города». Поведение — `LH.cityInput` в
   `dashboard.js`; здесь только вид, и он НЕ заводит нового материала: список
   повторяет рецепт `.tip-panel` (сплошное дно + кромка + тень), потому что это
   тоже всплывающая поверхность, а строка — рецепт `.ls-opt` фильтров.

   ⚠️ ОБЁРТКА `position: relative`, СПИСОК — `absolute`, И ЭТО НЕ ЛЕНЬ. Поле
   города живёт внутри `.tip-panel`, у которой `backdrop-filter`: она является
   содержащим блоком для `fixed`-потомков И областью обрезки, то есть `fixed`
   отсюда всё равно не выберется (разбор — `LH_popFixed` в `_tip_panel.html`).
   Абсолютный список внутри панели наращивает её прокрутку, а `scrollIntoView`
   на открытии доматывает его в кадр.

   ⚠️ ПАНЕЛЬ ПОД СПИСКОМ НЕ ЗАКРЫВАЕТСЯ: список лежит внутри обёртки поля, то
   есть внутри самой панели, — курсор её не покидает, а предикат `keep` видит
   изменившееся поле как «грязную» форму. */
.lh-city { position: relative; display: block; min-width: 0; }
.lh-city-list {
  position: absolute; z-index: 40;
  top: calc(100% + 4px); left: 0; right: 0;
  max-height: 216px; overflow-y: auto; overscroll-behavior: contain;
  padding: 5px;
  border: 1px solid var(--glass-edge); border-radius: var(--radius-md);
  background-color: var(--color-surface-solid);
  background-image: var(--glass-sheen),
                    linear-gradient(var(--glass-bg-chrome), var(--glass-bg-chrome));
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-specular);
}
.lh-city-list[hidden] { display: none !important; }
.lh-city-opt {
  display: flex; align-items: baseline; gap: 10px;
  padding: 7px 9px; border-radius: var(--radius-sm);
  font-size: var(--text-sm); color: var(--color-text);
  cursor: pointer;
}
/* Наведение и «выделено с клавиатуры» — ОДИН вид: иначе мышь и стрелки
   рисовали бы на одном списке два разных «вот эта строка». */
.lh-city-opt:hover, .lh-city-opt.on { background: var(--fill-2); }
.lh-city-n { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* Правая колонка — «ведём» у рабочего города, страна у остальных. Она отвечает
   на вопрос, который иначе задают в чате: «а Казань у нас уже работает?» */
.lh-city-c { flex: 0 0 auto; font-size: var(--text-3xs); color: var(--color-muted); }
.lh-city-none {
  padding: 7px 9px; font-size: var(--text-xs); color: var(--color-muted);
}
/* ⚠️ ЧЕСТНАЯ ПОДПИСЬ ВМЕСТО ЗАПРЕТА. Незнакомый город система умеет заводить
   (`world_cities.ensure`), и поле обязано сказать, что будет дальше, а не
   мигать красным. Колер приглушённый: это не ошибка. */
.lh-city-note {
  display: block; margin-top: 4px;
  font-size: var(--text-3xs); line-height: 1.35; color: var(--color-muted);
}
.lh-city-note[hidden] { display: none; }
/* Строка «похоже на …» — предложение, а не отказ: она НАЖИМАЕТСЯ и подставляет
   каноническое написание. Значит и выглядеть обязана нажимаемой; цвет тот же
   (это не ошибка), подчёркивание появляется под курсором. */
.lh-city-note.lh-city-fix { cursor: pointer; color: var(--color-text-soft); }
.lh-city-note.lh-city-fix:hover { color: var(--color-text); text-decoration: underline; }

/* --- ГРУППА ПОЛЕЙ: осмысленный шаг вместо ряда полей ---------------------
   ЗНАЧОК ШАГА — иконка, а не цифра (владелец 03.08): цифра обещает
   нумерованный путь, которого нет. Цвет ушёл вместе с цифрой НАМЕРЕННО: в
   Lidz тёплым помечается выделенная строка списка, а не заголовок, и
   градиентные кружки были единственным цветным пятном формы. Кружка вокруг
   глифа тоже нет — то же решение, что у `.ibtn` 03.08.
   `overflow: visible` — половина штриха выходит за край viewBox. */
.fset {
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--fill-3);
  padding: 14px 16px 16px;
  margin: 0 0 12px;
}
.fset-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; margin-bottom: 12px; }
.fset-n {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; color: var(--color-text-soft);
}
.fset-n > svg { width: 18px; height: 18px; flex: none; overflow: visible; }
.fset-t { font-size: var(--text-sm); font-weight: var(--fw-bold); letter-spacing: var(--tracking-snug); }
.fset-h { font-size: var(--text-2xs); color: var(--color-text-soft); }

/* Двойной класс .form-row.fgrid перебивает базовые flex/фикс-ширины .form-row. */
.form-row.fgrid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 14px 12px;
  align-items: start;   /* подписи в ряд: у поля с подсказкой высота больше */
}
.form-row.fgrid > label { grid-column: span 4; min-width: 0; }
.form-row.fgrid > label.wide   { grid-column: span 6; }
.form-row.fgrid > label.narrow { grid-column: span 3; }
.form-row.fgrid > label.full   { grid-column: 1 / -1; }
.form-row.fgrid label select,
.form-row.fgrid label input,
.form-row.fgrid label textarea,
.form-row.fgrid input#f-username {   /* перебиваем базовый width:160px (спец. с id) */
  width: 100%;
  box-sizing: border-box;
}
/* Пояснение живёт ПОД полем, а не в подписи: длинная подпись в узкой панели
   встаёт в две строки и опускает своё поле относительно соседнего в ряду. */
.fhint {
  font-size: var(--text-3xs); color: var(--color-text-soft);
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  min-height: 1.1em;
}
.fhint b { color: var(--color-text-soft); font-variant-numeric: tabular-nums; font-weight: var(--fw-strong); }

/* --- ПОДВАЛ ФОРМЫ: кнопка отправки — ИКОНКА, прижата вправо -------------
   Подписи на экране нет, действие называют `aria-label`/`title` — они
   обязательны: кнопка создаёт живую сущность. Класс `.ibtn-fill`, а не
   голый `.ibtn`: это утвердительное действие блока, и дно у залитой кнопки
   сохранено даже после снятия колец 03.08.
   ⚠️ Сам плюс НЕ крутим: на 45° он превращается в косой крест и читается
   как «отмена» — ровно наоборот к тому, что кнопка делает. */
.form-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 4px; }
.form-foot .btn-primary { margin-left: auto; }
.form-foot .ibtn { margin-left: auto; }
.fa-ic, .fa-sp { display: inline-flex; }
.ibtn .fa-sp,
.ibtn[data-busy="1"] .fa-ic { display: none; }
.ibtn[data-busy="1"] .fa-sp { display: inline-flex; animation: fa-spin .9s linear infinite; }
/* Пока запрос в полёте, дно у кнопки ОСТАЁТСЯ. Общий `.ibtn-fill[disabled]`
   его снимает, а колец в системе больше нет — вместе это оставляло от кнопки
   одну бледную дугу, то есть «отправляю» читалось как «кнопка пропала». */
:root .ibtn-fill[data-busy="1"][disabled] { background: var(--ib-fill-bg); }
@keyframes fa-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ibtn[data-busy="1"] .fa-sp { animation: none; }
}

/* Эхо ответа бэкенда — заметный баннер, а не серая строчка: на «Клиентах»
   он различает «🆕 новая подписка» и «♻️ ОБНОВЛЕНО», то есть работает
   предохранителем от тихой перезаписи. */
.form-echo {
  display: none; margin-top: 14px;
  padding: 11px 14px; border-radius: var(--radius-sm);
  font-size: var(--text-sm); line-height: 1.45;
  border: 1px solid var(--color-rule-strong);
  background: var(--fill-3); color: var(--color-text);
}
.form-echo.show { display: block; }
.form-echo.ok   { background: rgba(36,138,61,.12);  border-color: rgba(36,138,61,.28); }
.form-echo.upd  { background: rgba(178,80,0,.12);   border-color: rgba(178,80,0,.28); }
.form-echo.err  { background: rgba(199,38,29,.10);  border-color: rgba(199,38,29,.26); }
:root[data-theme="dark"] .form-echo.ok  { background: rgba(48,209,88,.13);  border-color: rgba(48,209,88,.26); }
:root[data-theme="dark"] .form-echo.upd { background: rgba(255,159,10,.13); border-color: rgba(255,159,10,.26); }
:root[data-theme="dark"] .form-echo.err { background: rgba(255,105,97,.13); border-color: rgba(255,105,97,.26); }
.form-echo .eh { display: block; font-weight: var(--fw-bold); margin-bottom: 2px; }

@media (max-width: 760px) {
  .form-row.fgrid { grid-template-columns: repeat(6, 1fr); }
  .form-row.fgrid > label.wide { grid-column: span 6; }
}
@media (max-width: 460px) {
  .form-row.fgrid { grid-template-columns: repeat(2, 1fr); }
  .form-row.fgrid > label,
  .form-row.fgrid > label.wide,
  .form-row.fgrid > label.narrow { grid-column: span 2; }
  .form-foot .btn-primary { margin-left: 0; width: 100%; }
}

/* ============================================================
   20. ВЫБОР СО СТРАНИЦЫ «ДИЗАЙН-ПРОЕКТ» (23.08.2026)
   ------------------------------------------------------------
   Владелец расставил отметки на `/design`; выбор лежит в
   `design_choices.json`, а на страницу приезжает АТРИБУТАМИ
   `data-ds-<элемент>="vN"` на теге `<html>` (`design_choices.attr_pairs`
   → `base.html`). Здесь — сами правила вида под эти атрибуты.

   ⚠️ ПРАВИЛО ПИШЕТСЯ ПОД АТРИБУТ, БАЗОВЫЙ ВИД НЕ ТРОГАЕТСЯ. Вариант `v1`
   у каждого элемента — это нынешний дашборд, и атрибута он не получает
   вовсе. Значит откат любого варианта = снятие галочки на витрине, без
   единой правки кода. Тем же приёмом живут темы.

   ⚠️ ОДИН ИСТОЧНИК НА БОЙ И НА ВИТРИНУ. Образцы на `/design` показывают
   ЭТИ ЖЕ правила: у каждого блока в списке селекторов стоит второй вход
   `.ds-on-<элемент>-<вариант>` — его вешает витрина на свою сцену, потому
   что атрибут на `<html>` может иметь только одно значение, а показать
   надо все три. Копий правил в `design.html` для применённых элементов
   нет и заводить их нельзя: разъедутся в первый же день.

   Выбрано 23.08: help v2 · table v3 · confirm v2 · search v2 · empty v2 ·
   progress v3. Остальные девять оставлены как есть (`v1`).
   ============================================================ */

/* Маски глифов для правил ниже. Рисунок — Lucide, сетка 24×24, обводка 1.9
   (правило проекта, docs/design-system.md §8). Маска, а не фон: цвет тогда
   берётся токеном и переключается вместе с темой сам, как у стрелки `.f-sel`. */
:root {
  --ds-ic-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
  --ds-ic-empty: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 12h-6l-2 3h-4l-2-3H2'/%3E%3Cpath d='M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'/%3E%3C/svg%3E");
  --ds-ic-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");
}

/* --- 0. ПАНЕЛЬ БЕЗ ОБОЛОЧКИ: ФОРМА ИЗ ОДНОГО ПОЛЯ ------------------------
   Правило дизайн-системы §9.6 (владелец, 23.08, три просьбы подряд про три
   разные формы): оболочка панели — с дном, кромкой, тенью и полями — нужна
   форме с НЕСКОЛЬКИМИ полями, она их группирует. Форме из одного значения
   группировать нечего: остаётся текстбокс с иконкой действия внутри.

   ⚠️ ЭТО ТРЕТЬЯ КОПИЯ ОДНОГО РЕЦЕПТА, СВЕДЁННАЯ В ОДНУ. К вечеру 23.08 он
   лежал отдельно у рассылки (`.bc-bare`), у очереди чатов (`.ca-bare`) и у
   долива пробы — три места, десять одинаковых объявлений. Разъехаться им
   оставалось до первой правки.
   ⚠️ МЕХАНИЗМ НЕ ТРОГАЕМ: класс `tip-panel` остаётся, на нём висят раскрытие
   наведением, Esc, клик мимо, закрепление и расчёт места от своей иконки.
   Снимается ВИД, а не поведение.
   ⚠️ ШИРИНУ ЗАДАЁТ СТРАНИЦА — ступенью шкалы `--tip-w-*` или числом, если
   поле у неё одно и узкое (долив пробы). Здесь её нет намеренно. */
.tip-panel.tip-bare {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  background-image: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
  max-height: none;
  overflow: visible;
}

/* --- 1. СПРАВКА ПОЛЕЙ: «ИКОНКА В КОЛОНКЕ» (help v2) ----------------------
   Пятая, и единственная перевёрнутая, реализация пары «заголовок + пояснение»
   (docs/design-system.md §1.6): подпись поля 11.2px весом 530 приглушённым
   колером СТОЯЛА НАД своим текстом 13.1px — заголовок был и мельче, и тусклее
   тела, то есть читался хвостом предыдущего абзаца.
   ⚠️ БАЗА ПЕРЕЕХАЛА СЮДА ИЗ `chats.html`. Она жила в инлайновом `<style>`
   страницы со специфичностью `.ss-tip .fh-item-t` (0,2,0) — то есть правило
   под атрибут её бы не перебило по порядку файлов, а не по весу. Одна копия,
   одно место.
   Вариант v2 — канон `.tip-item`: тот же грид 18px под глиф, заголовок в
   кегль текста, вес 590, предельный нейтраль `--hi`. Два канала различия из
   четырёх (вес + контраст), оба выше порога (§4.4). */
.fh-item + .fh-item { margin-top: 10px; }
.fh-item-t {
  display: block; margin-bottom: 2px;
  font-size: var(--text-3xs); font-weight: var(--fw-medium);
  color: var(--color-muted);
}
.fh-item-b { display: block; }

[data-ds-help="v2"] .fh-item,
.ds-on-help-v2 .fh-item {
  display: grid; grid-template-columns: 18px minmax(0, 1fr);
  gap: 10px; align-items: start;
}
/* Глиф — псевдоэлементом: список собирает скрипт из `meta.fields[]`, своей
   иконки у поля нет и выдумывать её на каждое поле нечем. Колонку держит один
   и тот же знак — работа у него ровно эта (§1.6 «иконка держит колонку»). */
[data-ds-help="v2"] .fh-item::before,
.ds-on-help-v2 .fh-item::before {
  content: ""; grid-row: 1; grid-column: 1;
  width: 16px; height: 16px; margin-top: 2px;
  background: var(--color-muted);
  -webkit-mask: var(--ds-ic-info) center / 16px 16px no-repeat;
          mask: var(--ds-ic-info) center / 16px 16px no-repeat;
}
[data-ds-help="v2"] .fh-item-t,
.ds-on-help-v2 .fh-item-t {
  grid-column: 2; font-size: inherit;
  font-weight: var(--fw-strong); color: var(--hi);
}
[data-ds-help="v2"] .fh-item-b,
.ds-on-help-v2 .fh-item-b { grid-column: 2; }

/* --- 2. ТАБЛИЦА: «ЗЕБРА» (table v3) --------------------------------------
   Линейка остаётся только под шапкой, чётные строки подкрашены самой лёгкой
   ступенью шкалы (`--fill-3`, 0.06 светлая / 0.16 тёмная).
   ⚠️ ХОВЕР ОБЯЗАН ОСТАТЬСЯ ВИДНЫМ. Базовая подсветка строки — тот же
   `--fill-3`, то есть на чётных строках она исчезла бы вовсе. Под зеброй
   курсор берёт ступень выше (`--fill-2`); носитель «предельно нейтральный
   текст» (`:root table.lh tbody tr:hover > td`) не тронут, он общий.
   ⚠️ ТРИ ИСКЛЮЧЕНИЯ, И ВСЕ ТРИ — НЕ ВКУСОВЩИНА:
     · `.rows-pill` («Операции») — у строк СВОЙ материал, заливки нет по
       правилу 03.08; зебра дралась бы с ним за одно и то же место;
     · `.bare` («Аккаунты», агенты поиска, города) — модификатор для таблиц,
       где строк единицы и каждая выше одной текстовой; там разделяет ВОЗДУХ,
       а зебра — инструмент длинного списка, который листают. Плюс у этих
       таблиц есть парные строки-панели (`.thr-row`, `.mg-act-row`), и
       `nth-child` считал бы их наравне с данными;
     · `.no-zebra` — явный отказ для таблиц с ДОПОЛНИТЕЛЬНОЙ строкой под
       строкой данных (`#mg-t`: под менеджером идёт `tr.dk-mgr-why` с
       причинами потерь). Чётность там означает не «вторая строка», а «строка,
       у которой сосед выше оказался с хвостом». */
[data-ds-table="v3"] table.lh:not(.rows-pill):not(.bare):not(.no-zebra) tbody td,
.ds-on-table-v3 table.lh tbody td { border-bottom: 0; }
[data-ds-table="v3"] table.lh:not(.rows-pill):not(.bare):not(.no-zebra) tbody tr:nth-child(even) > td,
.ds-on-table-v3 table.lh tbody tr:nth-child(even) > td { background: var(--fill-3); }
[data-ds-table="v3"] table.lh:not(.rows-pill):not(.bare):not(.no-zebra) tbody tr:hover > td,
.ds-on-table-v3 table.lh tbody tr:hover > td { background: var(--fill-2); }
/* Строка-заглушка («ничего нет», «загрузка…») строкой данных не является —
   гасим у неё и зебру, и подсветку. Продолжение общего правила §6. */
[data-ds-table="v3"] table.lh tbody tr:has(> td[colspan]) > td,
.ds-on-table-v3 table.lh tbody tr:has(> td[colspan]) > td { background: transparent; }
/* Узкая подложка: строка становится карточкой (`table.lh.stack` + `.tbl-host`),
   у карточки своё дно — зебра поверх него читалась бы грязью. */
@container (max-width: 900px) {
  [data-ds-table="v3"] table.lh.stack tbody tr:nth-child(even) > td,
  [data-ds-table="v3"] table.lh.stack tbody tr:hover > td { background: transparent; }
}

/* --- 3. ПОДТВЕРЖДЕНИЕ «НА МЕСТЕ ДЕЙСТВИЯ» (confirm v2) -------------------
   ⚠️ ШАГ ПОДТВЕРЖДЕНИЯ НЕ УБИРАЕТСЯ И НЕ УСКОРЯЕТСЯ. Это прямое следствие
   08.08: агент проверял ховер кнопки «Отправить», нажал её, и платящему
   клиенту в 02:10 дважды ушло слово «проверка». Здесь меняется ТОЛЬКО вид —
   ни один обработчик, ни один предохранитель (фокус на панель, а не на «Да»,
   сброс подтверждения при правке текста, запрет Enter) не тронут.
   Что меняет вариант: вопрос и ответы встают В ОДНУ СТРОКУ и полоса
   прижимается к своему действию, а не висит отдельным блоком под формой.
   Раньше `.grow { flex: 1 1 100% }` уводил вопрос на собственную строку —
   получался блок в две строки, то есть «отдельный слой» (v1).
   ⚠️ ТОН КАЖДОЙ ПОЛОСЫ ОСТАВЛЕН КАК БЫЛ. Красный у рассылки и у покупки
   агента — это сигнал «необратимо и стоит денег», а не украшение; вариант
   выбирался по ФОРМЕ («отдельным слоем» против «на месте действия»), и
   снимать заодно предупреждающий колер с самых дорогих кнопок дашборда
   значило бы поменять уровень защиты под видом смены вида. */
[data-ds-confirm="v2"] :is(.bc-confirm, .ca-confirm, .sb-ask, .sl-confirm, .cmp-confirm),
.ds-on-confirm-v2 .cf {
  flex-wrap: wrap;
  gap: 6px 10px;
  margin-top: 6px;
  padding: 7px 8px 7px 12px;
}
/* ⚠️ ДВЕ КНОПКИ ОТВЕТА — ОДНА ЕДИНИЦА ПЕРЕНОСА (`.ds-acts`), И БЕЗ ЭТОГО
   ВАРИАНТ НЕ РАБОТАЕТ. Соседние флекс-элементы переносятся поодиночке: замер
   на панели рассылки (полоса 358px, вопрос 212 + «Да, отправить» 136 +
   «Отмена» 88 + зазоры 20 = 456) — «Да» оставалось в первой строке, «Отмена»
   уезжала во вторую одна, и полоса читалась сломанной. Ни один подбор базиса
   этого не чинит: полосы живут в панелях 320 / 380 / 440 и в карточке во всю
   ширину, и «влезет / не влезет» у них разное.
   ⚠️ В `v1` ОБЁРТКА НЕ СУЩЕСТВУЕТ ДЛЯ РАСКЛАДКИ: `display: contents` убирает
   её короб целиком, кнопки остаются прямыми флекс-элементами полосы — то есть
   нынешний вид сохраняется до пикселя, а откат по-прежнему стоит снятия
   галочки. Обработчики к обёртке безразличны: они висят по id (`#bc-yes`,
   `#ca-yes`, `#sb-yes`) либо делегированы по классу самой кнопки. */
.ds-acts { display: contents; }

/* ⚠️ `.sg-confirm` («Заявки на регистрацию») В СПИСКАХ НЕТ НАМЕРЕННО: он уже
   устроен как `v2` — стоит В КОЛОНКЕ ДЕЙСТВИЙ строки таблицы вместо кнопок
   (`tr.confirming .row-actions { display: none }`), одной строкой, без дна и
   полей. Дать ему ещё и отступы полосы значило бы распереть строку таблицы по
   высоте — ровно то, от чего там стоит `flex-wrap: nowrap`. */
[data-ds-confirm="v2"] :is(.bc-confirm, .ca-confirm, .sb-ask, .sl-confirm, .cmp-confirm) > .grow,
.ds-on-confirm-v2 .cf > .grow {
  /* Вопрос делит строку с ответами и уходит на свою строку целиком лишь тогда,
     когда паре кнопок рядом с ним не хватает места. */
  flex: 1 1 200px;
  min-width: 0;
}
[data-ds-confirm="v2"] .ds-acts,
.ds-on-confirm-v2 .ds-acts {
  display: flex; align-items: center; gap: 8px;
  margin-left: auto; flex: 0 0 auto;
}

/* --- 4. ПОИСК: ЛУПА В ПОЛЕ И ПОДСВЕЧЕННОЕ СОВПАДЕНИЕ (search v2) ---------
   Компонент один на все восемь полей города и страны (`LH.cityInput`), поэтому
   и правило одно.
   ⚠️ ВЫДЕЛЕНИЕ СОДЕРЖИМОГО НА ФОКУСЕ НЕ ТРОНУТО. Подсветка живёт в СПИСКЕ
   (`<b class="lh-city-hit">` внутри строки варианта), а `select()` — в поле;
   пересечься им негде. Гонку «отложенный select() съедает набранную букву»
   чинили 23.08 сравнением значения — этот код не менялся.
   ⚠️ САМА ОБЁРТКА `<b>` СТОИТ В РАЗМЕТКЕ ВСЕГДА, а красит её только вариант.
   Иначе пришлось бы спрашивать выбор из скрипта — то есть завести второй
   источник правды о виде. В `v1` метка нейтральна и невидима. */
.lh-city-hit { font-weight: inherit; color: inherit; }

/* ⚠️ ЛУПА КЛАДЁТСЯ В ТУ ЖЕ ЯЧЕЙКУ ГРИДА, ЧТО И ПОЛЕ, — НЕ `top: 50%` И НЕ
   ЧИСЛОМ. В обёртке `.lh-city` кроме поля лежат выпадающий список и подпись
   «такого города у нас пока нет»: `top: 50%` считается от них всех и уезжает
   на строку варианта. Числом тоже нельзя — высота поля НЕ одна: замер 23.08 по
   живым страницам дал 40px у форм («Клиенты»), 34px на «Карте» и 32px в
   фильтре «Чатов», то есть любое `top` промахивалось бы на 3–4px минимум где-то.
   Одна ячейка грида и `align-self: center` центрируют глиф по ПОЛЮ, какой бы
   высоты оно ни было. Список остаётся `absolute` и в раскладку не входит. */
[data-ds-search="v2"] .lh-city,
.ds-on-search-v2 .lh-city { display: grid; grid-template-columns: minmax(0, 1fr); }
[data-ds-search="v2"] .lh-city > input,
.ds-on-search-v2 .lh-city > input { grid-row: 1; grid-column: 1; }
[data-ds-search="v2"] .lh-city::before,
.ds-on-search-v2 .lh-city::before {
  content: ""; grid-row: 1; grid-column: 1;
  align-self: center; justify-self: start;
  margin-left: 12px; z-index: 1;
  width: 16px; height: 16px; pointer-events: none;
  background: var(--color-muted);
  -webkit-mask: var(--ds-ic-search) center / 16px 16px no-repeat;
          mask: var(--ds-ic-search) center / 16px 16px no-repeat;
}
/* Место под лупу. Не `padding` вообще, а именно левое: правое у некоторых
   полей занято своим (`.f-sel` со стрелкой города на «Карте»).
   ⚠️ ЦЕПОЧКА `:not()` ПОВТОРЯЕТ БАЗОВОЕ ПРАВИЛО НЕ ДЛЯ КРАСОТЫ. Материал поля
   задан селектором `.lh-form input:not([type=checkbox]):not([type=radio]):not([type=range])`
   — это вес (0,4,1), и короткое `[data-ds-search] .lh-city > input` (0,2,1) ему
   проигрывает: поймано кадром, лупа легла ПОД первую букву. Специфичность
   добирается тем же способом, каким она нажита. */
[data-ds-search="v2"] .lh-city > input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
.ds-on-search-v2 .lh-city > input:not([type="checkbox"]):not([type="radio"]):not([type="range"]) {
  padding-left: 36px;
}
/* ⚠️ Подпись под полем и строка «ничего не нашлось» лупе не подчиняются: они
   не поле, у них своя раскладка. Отступ им не нужен — они начинаются с края. */
[data-ds-search="v2"] .lh-city-hit,
.ds-on-search-v2 .lh-city-hit {
  color: var(--hi); font-weight: var(--fw-strong);
}

/* --- 5. ПУСТОЕ СОСТОЯНИЕ: «ИКОНКА И ОБЪЯСНЕНИЕ» (empty v2) --------------
   Канонических видов в дашборде два (§«ПУСТОЕ СОСТОЯНИЕ»): `.empty` —
   крупное центрированное со значком, заголовком и строкой «что дальше»
   (это и есть v2), и `.empty-line` — одна приглушённая фраза на месте блока
   данных (это v1). Вариант поднимает вторую до первой.
   ⚠️ ТЕКСТ НЕ ВЫДУМЫВАЕТСЯ. Где во фразе уже есть разделитель («Проверенных
   лидов пока нет — аккаунт ещё не сделал ни одного прохода»), она разложена
   на `<b>` и `<span>` — заголовок и «что дальше» БЕЗ единого нового слова.
   Где объяснять нечем, строка остаётся строкой и получает только значок.
   ⚠️ `.boxed` не трогаем: это ответ ПОИСКА («по запросу ничего не нашлось»),
   у него своя рамка и свой смысл — блок отработал, а не пуст. */
[data-ds-empty="v2"] .empty-line:not(.boxed),
[data-ds-empty="v2"] .tg-empty,
.ds-on-empty-v2 .empty-line:not(.boxed) {
  text-align: center;
  padding: 16px 8px 18px;
  color: var(--color-muted);
  text-wrap: pretty;
}
[data-ds-empty="v2"] .empty-line:not(.boxed)::before,
[data-ds-empty="v2"] .tg-empty::before,
.ds-on-empty-v2 .empty-line:not(.boxed)::before {
  content: ""; display: block; margin: 0 auto 8px;
  width: 26px; height: 26px;
  background: var(--color-dim);
  -webkit-mask: var(--ds-ic-empty) center / 26px 26px no-repeat;
          mask: var(--ds-ic-empty) center / 26px 26px no-repeat;
}
[data-ds-empty="v2"] .empty-line > b,
.ds-on-empty-v2 .empty-line > b {
  display: block; margin-bottom: 3px;
  font-size: var(--text-s); font-weight: var(--fw-strong);
  color: var(--color-text);
}
[data-ds-empty="v2"] .empty-line > span,
.ds-on-empty-v2 .empty-line > span {
  display: block;
  font-size: var(--text-2xs); line-height: 1.45; color: var(--color-muted);
}

/* --- 6. ПОЛОСА ПРОГРЕССА: «СЕГМЕНТЫ» (progress v3) ----------------------
   Десять насечек по 10 % вместо сплошной полосы: видно шаг, а не только длину.
   ⚠️ ДЕЛАЕТСЯ МАСКОЙ, А НЕ ДЕСЯТЬЮ УЗЛАМИ. Полосы рисуют три разных места
   («Клиенты» — проба в строке подписки, «Аккаунты» — охват очереди, «Обзор» —
   утилизация), и раскладку ни одной из них трогать нельзя: колонку полос на
   «Клиентах» специально выравнивали сеткой (замер 23.08: было 226–367px,
   стало 309 у всех). Маска не занимает места и не меняет ни ширины, ни
   высоты — она режет уже нарисованное вместе с заливкой.
   Период считается как `(100% + зазор) / 10`, поэтому десятая насечка
   кончается ровно на 100 %, а не обрывается зазором у правого края.
   ⚠️ `.sp-bar` (ход прогона поиска) в список НЕ входит: она НЕОПРЕДЕЛЁННАЯ —
   бегунок ездит туда-сюда, доли у неё нет вовсе, и делить на десятые там
   нечего. Сегменты обещали бы шаг, которого система не знает. */
[data-ds-progress="v3"] :is(.bar-mini, .qsum-bar, .meter),
.ds-on-progress-v3 .bar-mini {
  --ds-seg-n: 10;
  --ds-seg-gap: 3px;
  --ds-seg-p: calc((100% + var(--ds-seg-gap)) / var(--ds-seg-n));
  border-radius: 2px;
  -webkit-mask: repeating-linear-gradient(90deg, #000 0 calc(var(--ds-seg-p) - var(--ds-seg-gap)),
                                          transparent calc(var(--ds-seg-p) - var(--ds-seg-gap)) var(--ds-seg-p));
          mask: repeating-linear-gradient(90deg, #000 0 calc(var(--ds-seg-p) - var(--ds-seg-gap)),
                                          transparent calc(var(--ds-seg-p) - var(--ds-seg-gap)) var(--ds-seg-p));
}
/* Скругление заливки внутри насечки читалось бы как обрыв полосы: у сегмента
   свои края, и они прямые. */
[data-ds-progress="v3"] :is(.bar-mini, .qsum-bar, .meter) > i,
[data-ds-progress="v3"] .meter-fill,
.ds-on-progress-v3 .bar-mini > i { border-radius: 0; }

/* ============================================================
   21. МНОГОСТРОЧНОЕ ПОЛЕ: ПОТОЛОК РОСТА (23.08)
   ------------------------------------------------------------
   Поведение — `LH.autoGrow` в `dashboard.js` (там же разбор кадра владельца
   «там видно косяк»: в поле «Что искать» третья строка фразы была срезана
   кромкой). Здесь ВЫСОТА, потому что высота — это вид.

   ⚠️ ПОТОЛОК НУЖЕН КАЖДОМУ ТАКОМУ ПОЛЮ. Все они живут во всплывающих
   панелях с собственной прокруткой: поле без потолка выдавливает за экран
   подвал с кнопкой, то есть отнимает у формы отправку. Сюда пишут фразу или
   список ссылок, а не абзац, — дорос до потолка, дальше прокрутка.
   ⚠️ ЗНАЧЕНИЯ СЧИТАНЫ ОТ СОДЕРЖИМОГО, а не подобраны: 180px — шесть строк
   фразы кеглем `--text-md` (15.7 × 1.45 = 22.8) плюс поля; 300px у списка
   ссылок — двенадцать строк, столько и вставляют пачкой.
   ============================================================ */
textarea[data-grow] { max-height: 300px; }
.ss-tip textarea.sp-text[data-grow] { max-height: 180px; }
.ss-tip textarea.sp-excl[data-grow] { max-height: 140px; }
/* Рост и ручка изменения размера — два способа задать одну высоту, и второй
   проигрывает первому: набранная строка сбрасывает то, что растянули рукой.
   У полей с кнопкой ВНУТРИ ручки и так нет (`resize: none` — она сидела бы
   под кнопкой), здесь снимаем её у остальных. */
textarea[data-grow] { resize: none; }


/* ============================================================
   ШАПКА ВО ВСЮ ШИРИНУ, МЕНЮ ПОД НЕЙ (владелец 06.09, вечер). После дня
   правок «чтобы меню и шапка были одним целым» владелец предложил простое:
   кот с названием живут в шапке, меню начинается ниже неё. Стекло меню и
   стекло шапки больше не накладываются, цвета хрома — утренние
   (`--glass-bg-chrome` 0.72/0.70, блик сверху, тень у раскрытого меню).
   ============================================================ */
.topbar {
  margin-left: calc(-1 * var(--side-w));
  padding-left: 14px;
  z-index: 70;
}
.app.pinned .topbar { margin-left: calc(-1 * var(--side-w-open)); }
.side { top: var(--topbar-h); }
.side::after { top: 0; }
.topbar .top-brand {
  margin: 0 10px 0 0; min-height: 0; padding: 0 4px;
  display: inline-flex; align-items: center; gap: 10px;
}
.topbar .top-brand .side-word { opacity: 1; transform: none; }
/* Разделители в меню остаются отступом, но невидимы (уточнение владельца). */
.side-sep { background: transparent; }

/* На узком экране меню — оверлей, `.main` без отступа: шапке отрицательный
   сдвиг не нужен, иначе кот уезжает за левый край (замер 06.09, 390px). */
@media (max-width: 900px) {
  .topbar, .app.pinned .topbar { margin-left: 0; padding-left: 14px; }
  .side { top: 0; }
}

/* ⚠️ БЕЗ ПОЛОСЫ НА СТЫКЕ ШАПКИ И МЕНЮ (кадр владельца 06.09: «полосу над
   Обзором, которая разделяет панель и меню, можно убрать?»). Полосу давали два
   штриха: блик по верхней кромке меню (`inset 0 1px 0 --glass-specular`) и
   нижняя рамка шапки. Оба сняты; у раскрытого меню остаётся только тень. */
.side { box-shadow: none; }
.side:hover, .side:focus-within { box-shadow: var(--shadow-lg); }
.topbar { border-bottom-color: transparent; }


/* ============================================================
   ГОЛОСОВОЕ В ЛЕНТЕ ПЕРЕПИСКИ (владелец 07.09: «в дашборде тоже возможность
   прослушивать голосовые в диалогах ВЕЗДЕ»)
   ------------------------------------------------------------
   Плеер РОДНОЙ БРАУЗЕРНЫЙ (`<audio controls>`), своего не рисуем: у системного
   есть перемотка, скорость и клавиатура, и он же стоит в мини-апке. Разметку
   собирает одно место — `LH.dmMediaHtml` в `dashboard.js`.

   ⚠️ ПРАВИЛО ЛЕЖИТ ЗДЕСЬ, А НЕ В ИНЛАЙНЕ СТРАНИЦЫ: голосовое рисуют личка
   мониторов («Аккаунты») и обе панели переписок («Клиенты»). Копия в каждом
   шаблоне — ровно тот случай, из-за которого в проекте разъехались `.dm-shot`
   и шесть иконок отправки.
   ⚠️ ШИРИНА ОТ СОДЕРЖИМОГО, А НЕ 100%: у Chrome минимальная ширина плеера с
   полным набором кнопок ≈ 200px, ниже он схлопывает перемотку. Потолок 260px
   держит его в пузыре и на узкой панели.
   ============================================================ */
.dm-voice {
  display: block;
  width: 100%; max-width: 260px; min-width: 0;
  height: 34px;
  margin: 2px 0 4px;
}
/* Пузырь без текста (голосовое без расшифровки) не должен нести нижний зазор
   под несуществующей строкой. Тот же приём, что у `.tg-msg.media-only .dm-media`. */
.tg-msg.media-only .dm-voice { margin-bottom: 0; }

/* ⚠️ КАРТИНКА ИЗ ЛИЧКИ — ТОЖЕ ОДНО ПРАВИЛО НА ДАШБОРД (07.09). Разметку
   `.dm-shot` собирает та же `LH.dmMediaHtml`, а вид у неё был только на
   «Аккаунтах»: на «Клиентах» панель переписки задавала половину рецепта
   (`.ch-tip .dm-shot img` без потолка высоты), а панель переписок от имени
   клиента — ничего вовсе. Замер 07.09 на живом снимке 589×1280: в переписке с
   мастером он выходил 280×608 при высоте ленты 300px, то есть закрывал её
   целиком и «прокрутите вверх» было единственным способом увидеть слова.
   Здесь тот же рецепт, что на «Аккаунтах» (потолок 260px, `contain` —
   скриншот не обрезаем, у него важны края). Страница по-прежнему может
   переопределить своё: правило базовое и слабое (0,1,0). */
.dm-shot {
  display: block; max-width: 260px; margin-bottom: 5px;
  border-radius: var(--radius-sm); overflow: hidden;
  border: 1px solid var(--color-rule);
}
.dm-shot img {
  display: block; width: 100%; height: auto; max-height: 260px;
  object-fit: contain; background: var(--fill-1);
}
.dm-shot:focus-visible { outline: none; box-shadow: var(--ring-focus); }
