/* =========================================================================
   LITTEN — design system «Litten Graphite»
   One source of tokens, themed via [data-theme]. Components below consume
   only CSS variables, so restyling means re-mapping tokens, not markup.
   ========================================================================= */

/* ---- Fonts (vendored, no CDN) — Onest, cyrillic+latin -------------------
   Геометрический гротеск, рисованный под кириллицу: круглые «о/с/е», короткие
   выносные, широкие цифры. Заменил Golos Text (03.09.2026, заказчик: «шрифт
   не тот») — у Golos гуманистический рисунок и узкие цифры, из-за чего
   крупные значения на плитках выглядели канцелярски, а не по-продуктовому.

   Variable font (wght 400..900) в четырёх сабсетах: браузер тянет только те
   диапазоны, которые реально встретились на странице — кириллица + латиница
   это ~88 КБ. Вариативность здесь не роскошь: дизайн-система использует
   промежуточные веса (550, 650, 750, 820, 850), которые на статике
   округлялись до ближайшей сотни. Лицензия SIL OFL 1.1, текст лежит рядом
   в OFL-Onest.txt.

   Файлы Golos НЕ удалены (golos-text-v7-*.woff2): откат — вернуть эти
   четыре @font-face и font-family на body.
   ------------------------------------------------------------------------ */
@font-face {
  font-family: "Onest";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/onest-v11-cyrillic.d1abf183873c.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "Onest";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/onest-v11-cyrillic-ext.598aa19ac1bf.woff2") format("woff2");
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: "Onest";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/onest-v11-latin.fd7dbbcb3633.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Onest";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/onest-v11-latin-ext.960412f3c7e3.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---- Design tokens : LIGHT (default) -----------------------------------
   «Litten Graphite»: neutral graphite canvas, vivid Ozon-blue primary,
   magenta accent (still exposed as --color-coral for the 600+ legacy
   consumers), calm sand tertiary. Every legacy variable name is preserved
   so every component class keeps resolving.
   ------------------------------------------------------------------------ */
:root,
html[data-theme="light"] {
  /* Полотно намеренно глубже белого на ~14% яркости: при прежних #F4F5F7
     шаг bg→surface был 1.09 и карточки «растворялись» в фоне. */
  --color-bg: #EEF0F4;
  --color-bg-deep: #E3E6EC;
  --color-surface: #FFFFFF;
  --color-surface-soft: #EFF1F5;
  --color-surface-alt: #F7F8FA;
  --color-border: #E0E3E9;
  --color-border-strong: #C9CDD7;
  --color-text: #16181D;
  --color-heading: #0C0E12;
  /* muted/faint доведены до AA (4.5:1) на белой карточке: прежние #667085 и
     #98A1B2 давали 4.97 и 2.60 — подписи таблиц и хинты не читались. */
  --color-muted: #5D6675;
  --color-faint: #6F7788;

  --color-primary: #005BFF;
  --color-primary-dark: #0047CC;
  --color-primary-strong: #0051E6;
  --color-primary-soft: #EBF1FF;
  --color-on-primary: #FFFFFF;

  /* Переписка: полотно ленты и два пузыря. Отдельные токены, а не общие
     поверхности (заказчик, 22.08.2026: «облачка плохо видно в обеих темах»).
     Пузырь без рамки виден ровно настолько, насколько отличается от полотна
     под ним, — а --color-surface на --color-surface-soft это разница в 3%,
     то есть никакой. Здесь шаг задан явно: полотно темнее обычного фона,
     чужой пузырь — чистый белый, свой — заметно голубой.

     26.08.2026, «облачко должно быть заметнее, как в телеграме»: полотно
     оставляли прежним (пробовали углублять — заказчик вернул), работали самим
     облачком. Свой пузырь дошёл до #D2E3FF — 1.06:1 к полотну, и видимость
     ему давала не заливка, а тень.

     03.09.2026 заказчик принёс макет мессенджера («не нравится дизайн чата,
     сделай так»): полотно БЕЛОЕ, чужой пузырь светло-голубой, свой —
     СПЛОШНОЙ синий с белым текстом. Это переворот прежней схемы, и он же
     ответ на прежнюю жалобу: свой пузырь теперь 5.33:1 к полотну вместо
     1.06:1. Чужой отдал ступень (1.16:1 против 1.30:1) — её не жалко:
     стороны разведены больше не светлотой, а цветом, и путать их нечем.
     Свой пузырь — сам бренд-синий #005BFF, подбирать ничего не пришлось:
     белый текст на нём даёт 5.33 ✓ AA. */
  --chat-canvas: #FFFFFF;
  --chat-bubble: #E9EEF8;
  --chat-bubble-own: #005BFF;
  /* Тень пузыря снята вместе с переворотом (макет плоский): она держала форму
     БЕЛОГО пузыря на светлом полотне, а голубому и синему форму держит
     собственная заливка. Токен оставлен — его читает .chat-msg__bubble, и
     вернуть тень можно, не трогая правило. */
  --chat-bubble-shadow: none;
  /* Вторичные тона ВНУТРИ пузыря. На чужом (#E9EEF8) общий muted даёт 4.98, а
     faint — 3.86, ниже AA: ему своя ступень. На своём синем всё вторичное
     обязано быть почти белым — #E8F0FF это 4.65, на полтона темнее уже 4.4;
     акцент (ссылка, чип «своя реакция», кромка цитаты) там чисто белый. */
  --chat-muted: var(--color-muted);
  --chat-faint: #626B7C;
  --chat-accent: var(--color-primary);
  --chat-muted-own: #E8F0FF;
  --chat-faint-own: #E8F0FF;
  --chat-accent-own: #FFFFFF;
  /* Подпись со временем с 04.09.2026 живёт ПОД пузырём, на полотне, и тона
     пузыря ей не годятся: на своём они почти белые. Значения записаны
     литералами, а не var(--color-faint): алиас раскрывался бы уже внутри
     пузыря, где эти токены перекрашены. Контраст к белому полотну — 4.50
     и 5.33. */
  --chat-stamp: #6F7788;
  --chat-stamp-read: #005BFF;
  /* Значок часов у времени в списке чатов (маска, а не фон: берёт currentColor
     и потому живёт в обеих темах). data: в CSP разрешён — img-src 'self' data: blob:. */
  --icon-clock: 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' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E");

  /* Accent (legacy name --color-coral): Ozon-magenta, use sparingly. */
  --color-coral: #DD0F5F;
  --color-coral-soft: #FDECF2;

  --color-beige: #C9AE85;
  --color-beige-soft: #F5EFE5;

  --color-violet: #6E4EF2;
  --color-violet-soft: #EFEBFE;

  /* Отметки оплаты в графике (заказчик, 08.2026): бирюза — «оплата сразу»,
     пурпур — «оплата сразу после смены». Взяты цвета его собственной таблицы,
     приведённые к контрасту системы; фиолетовый занят «неоформленным», и
     смешивать эти две оси нельзя — они отвечают на разные вопросы. */
  --color-cyan: #0B7285;
  --color-cyan-soft: #DFF3F7;
  --color-magenta: #A81E8F;
  --color-magenta-soft: #FBE7F6;

  /* Тона ТОЛЬКО для графиков: токены статусов (warning/beige) в светлой теме
     слишком приглушённые для заливок — сегменты выглядят блёкло. */
  --chart-amber: #F59E0B;
  --chart-sand: #C89B57;

  /* Статусные тона затемнены до AA как текст (было 3.3–4.35 на белом). Как
     заливка графиков читаются по-прежнему — --chart-* выше держат сегменты
     светлее там, где заливке нужна яркость. */
  --color-success: #08834B;
  --color-success-soft: #E4F6EC;
  --color-warning: #A56408;
  --color-warning-soft: #FBF0DB;
  --color-danger: #D02B39;
  --color-danger-soft: #FCE9EA;
  --color-info: #005BFF;
  --color-info-soft: #EBF1FF;

  /* Шапка (25.08.2026): своя подложка, а не полотно. Раньше .topbar брала
     --color-bg и в светлой теме сливалась с холстом в одно пятно — ряд
     кнопок висел в воздухе, границу держала одна волосяная линия. Теперь
     шапка — поверхность (как карточка), полотно под ней темнее на ступень,
     и мягкая тень отделяет её на скролле. В тёмной теме всё как было:
     подложка = полотно, тени нет. */
  --topbar-tint: #FFFFFF;
  --topbar-bg: color-mix(in srgb, var(--topbar-tint) 88%, transparent);
  --topbar-shadow: 0 10px 26px -24px rgba(12, 14, 18, 0.55);

  /* Sidebar is graphite in BOTH themes — the visual signature. */
  --sidebar-bg: #14161C;
  --sidebar-bg-deep: #0E1014;
  --sidebar-border: #24272F;
  --sidebar-text: #B4BAC7;
  --sidebar-heading: #F2F3F6;
  --sidebar-active: #3D7BFF;

  --shadow-xs: 0 1px 2px rgba(12, 14, 18, 0.06);
  --shadow-sm: 0 1px 3px rgba(12, 14, 18, 0.09), 0 1px 2px rgba(12, 14, 18, 0.05);
  --shadow-soft: 0 10px 30px rgba(12, 14, 18, 0.10);
  /* Два слоя вместо одного: ближняя тень даёт кромку, дальняя — отрыв от
     полотна. Одного 1px-слоя на светлом фоне не хватало, карточки читались
     плоско даже с бордером. */
  /* Кольцо spread 0.5px здесь ОСТАЁТСЯ, хотя в тёмной теме снято: ступень
     #EEF0F4 → #FFFFFF это всего 1,141:1, и без кольца белая карточка на
     светлом полотне растворяется — ошибка, уже пройденная выше (см. комментарий
     к --color-bg). В тёмной теме ступень разведена заливкой, и кольцо там
     работало подменой рамки, которой референс не знает. */
  --shadow-card:
    0 1px 2px rgba(12, 14, 18, 0.04),
    0 6px 16px -8px rgba(12, 14, 18, 0.09),
    0 0 0 0.5px rgba(12, 14, 18, 0.03);
  --shadow-pop: 0 16px 44px rgba(12, 14, 18, 0.18), 0 4px 12px rgba(12, 14, 18, 0.09);
  /* --shadow-md / --shadow-lg ИСПОЛЬЗОВАЛИСЬ, но никогда не были объявлены:
     подсказка графика (.dash-linechart__tip), ховер экономической плитки и
     модалка заметок входа месяцами жили вовсе без тени — несуществующая
     переменная убивает всё объявление целиком, молча и без ошибки в консоли.
     Объявлены здесь (03.09.2026) до правки теней ниже: иначе тот же дефект
     воспроизвёлся бы на каждой опечатке в новых именах. */
  --shadow-md: 0 8px 24px rgba(12, 14, 18, 0.12);
  --shadow-lg: 0 18px 48px rgba(12, 14, 18, 0.16);
  /* Тень главной кнопки НЕЙТРАЛЬНАЯ, а не синяя (03.09.2026, заказчик:
     «слишком дешёвое выделение»). Цветное свечение под кнопкой даёт синий ореол
     на полотне вокруг каждого главного действия — приём, который читается
     дешёвой отделкой. Кнопка и так синяя, подсвечивать её тем же цветом нечем. */
  --shadow-primary: 0 4px 12px rgba(12, 14, 18, 0.14);

  --ring: 0 0 0 3px rgba(0, 91, 255, 0.30);
  --ring-soft: 0 0 0 4px rgba(0, 91, 255, 0.12);

  --gradient-brand: linear-gradient(135deg, #005BFF, #4D8DFF);
  --gradient-primary: linear-gradient(180deg, #1A6BFF, #0051E6);
  --gradient-surface: linear-gradient(180deg, #FFFFFF, #F7F8FA);

  /* Лестница радиусов пересобрана 03.09.2026 под референс заказчика.
     ВАЖНО ПРО ЧИСЛА: у продукта html { zoom: .85 } (--ui-scale ниже), поэтому
     каждое написанное здесь px-значение на экране на 15% меньше. Референсные
     «карточка 20-24, вложенный элемент 14-16» это 24-28 и 17-19 в CSS.
       2xl 26 → 22,1 экранных — карточка, плитка метрики, шапка раздела;
       xl  16 — НЕ ТРОНУТ: на нём висят выпадашка колокольчика и командная
                строка с обязательным overflow: hidden, 24px срезали бы там
                первую строку списка и поле поиска;
       lg  18 → 15,3 экранных — вложенный элемент: обёртка таблицы, поле ввода;
       md  12 → 10,2 экранных — мелкий контрол; 138 потребителей, среди них
                квадратики 19-26px (.checkbox__box, .dlib__add), где больше 12
                даёт «мыло»;
       sm   9 — пропорциональный сдвиг низа лестницы. */
  --radius-2xl: 26px;
  --radius-xl: 16px;
  --radius-lg: 18px;
  --radius-md: 12px;
  --radius-sm: 9px;
  --radius-pill: 999px;

  /* Разделитель — ОТДЕЛЬНЫЙ токен, а не --color-border. Референс просит очень
     слабую линию (8-10% контраста), а бордер сейчас даёт 23% в светлой теме и
     12% в тёмной. Ослабить сам --color-border нельзя: в 28 местах он не рамка,
     а заливка (.divider, разделители меню сообщения и actions-menu, сетка трёх
     графиков, точка важности в колокольчике, трек прогресса тура) — там линия
     просто исчезла бы. Одна запись через color-mix работает в обеих темах.
     ОБЯЗАТЕЛЬНО продублирован в @media print (в самом низу файла): фон браузер
     не печатает, и без печатного значения границы таблиц пропадут на бумаге. */
  --color-divider: color-mix(in srgb, var(--color-text) 9%, transparent);

  /* Жёлоб графиков — тоже отдельно от --color-surface-soft. Тот же токен носят
     чипы, поля и ховер строк (236 мест), и подъём заливки ради чипа молча
     осветлял бы пустую часть колец и баров. */
  --chart-track: color-mix(in srgb, var(--color-text) 10%, transparent);

  /* Единый промежуток между карточками. Их было четыре разных: .metrics 14px,
     .dsec__grid 14px, .dcanvas__grid 16px, .grid 18px — на экране 11,9/13,6/15,3. */
  --gap-cards: 20px;

  /* Единая высота однострочного контрола. Нативные date/time рисуются выше
     select и text (40 / 43 / 45px) — без общего значения поля в одной строке
     формы съезжают друг относительно друга. --control-h-sm — та же роль для
     компактных панелей-тулбаров, где кнопки идут в btn-sm. */
  --control-h: 44px;
  --control-h-sm: 34px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;

  --sidebar-w: 260px;
  --topbar-h: 64px;

  --ease: cubic-bezier(0.22, 0.7, 0.25, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  color-scheme: light;
}

/* ---- Design tokens : DARK ----------------------------------------------
   «Litten Orbital» (08.2026, заменил графитово-индиговую тёмную тему).
   Нейтральный почти-чёрный холст БЕЗ цветного подмеса (прежний #12141C уходил
   в синеву, и на нём любой акцент грязнился) и один живой акцент — синий
   #4187FF. Ступени высоты крупнее прежних: bg #101012 → surface #191A1D →
   alt → soft, поэтому карточка читается на полотне без бордера.

   08.2026 акцент переведён с индиго (#7B7DF5) на синий: индиго читался как
   фиолетовый и расходился со светлой темой, где бренд — Ozon-синий #005BFF.
   Взят ТОТ ЖЕ тон 218°, что и в светлой теме, только поднятый по светлоте
   под чёрное полотно, — обе темы теперь один цвет, а не два родственных.
   Ступени подобраны по РАВНОЙ ЯРКОСТИ со снятым индиго (Δ контраста < 0.05
   на каждой), поэтому ни одно из ~700 мест, где акцент лежит под белым
   текстом или на карточке, не потеряло контраст — менялся тон, не светлота.

   Как устроен акцент (три значения, у каждого своя работа):
     --color-primary   #4187FF  текст, иконки, границы, ссылки  (5.1:1 ✓ AA)
     --gradient-primary #196DFF→#0059F2  заливка кнопок под белым (4.5–5.7 ✓)
     --color-primary-dark #80AFFF  ховер по ссылкам (в тёмной теме «dark» =
                                   светлее, а не темнее)
   Белый текст на самом --color-primary даёт 3.4:1 — это заливки-чипы, крупный
   жирный шрифт; для кнопок берём градиент, он для того и разведён.

   Контраст на карточке #191A1D: text 12.9:1, muted 6.2:1, faint 4.7:1.
   --color-violet (#BE8CFF) НЕ трогаем: он метит «неоформленных», это смысл,
   а не украшение. С синим primary он расходится лучше, чем с индиговым.
   ------------------------------------------------------------------------ */
html[data-theme="dark"] {
  /* Глубина пересобрана 03.09.2026 под референс заказчика. Раньше ступень
     полотно→карточка была 1,092:1, а рамка над карточкой — 1,183:1: край
     карточки рисовала ЛИНИЯ, а не заливка, и это и есть главное расхождение с
     референсом. Стало: ступень 1,128:1, рамка 1,111:1 — заливка вышла вперёд.
     Контрасты на новой карточке #1A1B1E проверены: text 13,95, heading 16,51,
     muted 6,37, faint 4,88, primary 5,03 — AA (4,5) держится везде. */
  --color-bg: #0D0D0E;
  --color-bg-deep: #060607;
  --color-surface: #1A1B1E;
  /* alt и soft подняты вместе с карточкой: они отсчитаны ОТ неё, и без сдвига
     ступень «вложенный блок» схлопнулась бы с 1,071 до 1,02 — блок внутри
     карточки слился бы с ней. */
  --color-surface-soft: #2A2B30;
  --color-surface-alt: #202125;
  /* Рамка поднята обратно до #2C2D31 (03.09.2026, заказчик: «нет чёткого
     деления блоков»). Я опускал её до #232428, когда край карточки рисовала
     ступень заливки; теперь рамка снова несёт границу, и ей нужна заметность:
     на карточке #1A1B1E это 1,29:1 против прежних 1,11:1. */
  --color-border: #2C2D31;
  --color-border-strong: #383A40;
  --color-text: #E7E7EA;
  --color-heading: #FAFAFB;
  --color-muted: #9C9DA4;
  /* Было #6F7789 — 3.74:1 на карточке, ниже AA. */
  --color-faint: #87888F;

  --color-primary: #4187FF;
  --color-primary-dark: #80AFFF;
  --color-primary-strong: #196DFF;
  --color-primary-soft: rgba(65, 135, 255, 0.16);
  --color-on-primary: #FFFFFF;

  --color-coral: #FF6E9E;
  --color-coral-soft: rgba(255, 110, 158, 0.15);

  --color-beige: #D9C29B;
  --color-beige-soft: rgba(217, 194, 155, 0.13);

  --color-violet: #BE8CFF;
  --color-violet-soft: rgba(190, 140, 255, 0.15);

  --color-cyan: #4ECDE0;
  --color-cyan-soft: rgba(78, 205, 224, 0.16);
  --color-magenta: #F08CDC;
  --color-magenta-soft: rgba(240, 140, 220, 0.16);

  --chart-amber: #F5B04C;
  --chart-sand: #D9C29B;

  --color-success: #3DD68C;
  --color-success-soft: rgba(61, 214, 140, 0.14);
  --color-warning: #F5B04C;
  --color-warning-soft: rgba(245, 176, 76, 0.14);
  --color-danger: #F57A83;
  --color-danger-soft: rgba(245, 122, 131, 0.14);
  --color-info: #4187FF;
  --color-info-soft: rgba(65, 135, 255, 0.16);

  /* Шапка в тёмной теме остаётся полотном: графит на графите не сливается —
     карточки и так светлее фона (см. светлую тему выше). */
  --topbar-tint: var(--color-bg);
  --topbar-bg: color-mix(in srgb, var(--topbar-tint) 72%, transparent);
  --topbar-shadow: none;

  /* Рельс намеренно ГЛУБЖЕ полотна — почти чёрный, как в референсе: при
     равной светлоте сайдбар сливается с фоном в единое пятно и теряет роль
     фирменного рельса, которую он играет в светлой теме. */
  /* Рельс обязан остаться ГЛУБЖЕ полотна (см. комментарий выше). После
     опускания --color-bg до #0D0D0E прежний #0A0A0C сравнялся бы с холстом. */
  --sidebar-bg: #060607;
  --sidebar-bg-deep: #030304;
  --sidebar-border: #1C1D21;
  --sidebar-text: #9C9DA4;
  --sidebar-heading: #F5F5F7;
  --sidebar-active: #196DFF;

  --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.45);
  /* Тени приглушены: референс держит «мягкие и почти невидимые», глубину даёт
     заливка. Прежняя --shadow-soft на почти чёрном полотне давала ореол вокруг
     плитки при наведении. */
  --shadow-soft: 0 10px 28px rgba(0, 0, 0, 0.38);
  /* Белое кольцо spread 0.5px снято: это была вторая подмена рамки — ступени
     теперь хватает своей. В СВЕТЛОЙ теме кольцо оставлено, там ступень
     полотно→белое всего 1,141:1 и без него карточка растворяется. */
  --shadow-card: 0 2px 6px rgba(0, 0, 0, 0.30);
  --shadow-md: 0 10px 28px rgba(0, 0, 0, 0.55);
  --shadow-lg: 0 20px 52px rgba(0, 0, 0, 0.62);
  --shadow-pop: 0 22px 60px rgba(0, 0, 0, 0.7), 0 6px 18px rgba(0, 0, 0, 0.5);

  /* Переписка (см. светлую тему): полотно глубже карточки, пузырь — светлее.
     Раньше чужой пузырь и карточка панели были одним и тем же #191A1D и
     сливались в сплошное полотно, стоило убрать рамку.

     26.08.2026, «облачко должно быть заметнее»: полотно оставлено прежним,
     двинуты сами пузыри — чужой светлее (#272A31 → #33373F), свой заметно
     синее (#1D3556 → #1F4A85). Было 1.29 / 1.50 / 1.16, стало
     1.56 / 2.10 / 1.35 (чужой к полотну, свой к полотну, свой к чужому).
     Слабым местом была именно последняя пара: свой и чужой отличались на
     16%, и на длинной ленте стороны сливались. ВАЖНО: светлее пузырь — ниже
     контраст всего, что на нём написано, потому ниже идут собственные
     вторичные тона переписки.

     03.09.2026, под тот же макет мессенджера, что и светлая тема: полотно
     ленты СРАВНЯЛОСЬ с карточкой (#121316 → #1A1B1E), а свой пузырь стал тем
     же сплошным бренд-синим, что и в светлой (#1F4A85 → #005BFF). Обе темы
     стали одним дизайном, а не двумя родственными: одна заливка своего
     пузыря, одна пастельная палитра имён на ней, одно доказательство
     контраста на обе (белый текст на #005BFF — 5.33 ✓ AA).
     Чужой пузырь оставлен прежним: на новом полотне он даёт 1.44:1 против
     прежних 1.56:1 — ступень, за которую заказчик держался 26.08.2026, почти
     не тронута, а любая более тёмная заливка роняла её до 1.17-1.33. */
  --chat-canvas: #1A1B1E;
  --chat-bubble: #33373F;
  /* 04.09.2026, заказчик: «цвет слишком яркий». Бренд-синий #005BFF на почти
     чёрном полотне светился; взят тот же тон ступенью глубже — #1E56B8.
     Белый текст 6.82, к полотну 2.52 (было 2.10 у прежнего #1F4A85), к чужому
     пузырю 1.75 (было 1.35 — та самая пара, на которую жаловались 26.08).
     В светлой теме заливка осталась бренд-синей: на белом она не слепит. */
  --chat-bubble-own: #1E56B8;
  /* Тень снята в обеих темах (макет плоский): на почти чёрном полотне она и
     так не читалась. */
  --chat-bubble-shadow: none;
  /* Вторичные тона ВНУТРИ пузыря (подпись со временем, имя, ссылки, галочки).
     Общие --color-muted/faint/primary считаны от карточки и на чужом пузыре
     падают ниже AA (faint 3.38, primary 3.49) — здесь та же роль ступенью
     светлее. На своём синем тона те же, что в светлой теме: заливка одна. */
  --chat-muted: #A8A9B0;
  --chat-faint: #A5A6AD;
  --chat-accent: #6FA5FF;
  --chat-muted-own: #E8F0FF;
  --chat-faint-own: #E8F0FF;
  --chat-accent-own: #FFFFFF;
  /* Подпись под пузырём — тона полотна (см. светлую тему): 4.88 и 5.03. */
  --chat-stamp: #87888F;
  --chat-stamp-read: #4187FF;
  --shadow-primary: 0 6px 16px rgba(0, 0, 0, 0.42);

  --ring: 0 0 0 3px rgba(65, 135, 255, 0.40);
  --ring-soft: 0 0 0 4px rgba(65, 135, 255, 0.16);

  --gradient-brand: linear-gradient(135deg, #196DFF, #4F90FF);
  --gradient-primary: linear-gradient(180deg, #196DFF, #0059F2);
  --gradient-surface: linear-gradient(180deg, #191A1D00, #26272B33);
  color-scheme: dark;
}

/* =========================================================================
   UI scale — плотность интерфейса
   Дизайн-система свёрстана в px под 100% зума, но на десктопе интерфейс
   разглядывали при 80% браузерного зума — эта плотность и была принята за
   норму. Вместо пересчёта ~3900 px по всему файлу один раз ужимаем корень.
   Один рычаг: поменять число здесь — поменяется вся плотность.

   С 22.08.2026 норма чуть крупнее — 0.85 вместо 0.8 (просьба заказчика
   «по умолчанию немного увеличить»): весь интерфейс на десктопе стал
   примерно на 6% больше, планшет вслед за ним 0.95 вместо 0.9. На телефоне
   масштаба нет и не было (там 1:1), поэтому увеличивать нечего.
   Брейкпоинты остались прежними: они меряют РЕАЛЬНЫЕ пиксели окна и от
   масштаба не зависят — сдвигать их вслед за нормой значило бы трогать
   раскладку там, где её никто не просил.

   Три следствия, из-за которых правил не одно, а три:
   1) zoom ужимает и вьюпортные единицы (100vh внутри масштаба = 80% экрана),
      поэтому каждая vh/vw в файле записана как calc(Nvh * var(--vu)),
      где --vu — обратный множитель (строго 1 / --ui-scale).
   2) media-запросы меряют РЕАЛЬНЫЙ вьюпорт, а не масштабированный: все
      десктопные брейкпоинты пересчитаны в реальные px (1025 -> 820,
      1280 -> 1024 и т.д.). Мобильные (< 820px) остались как были — там
      масштаб не действует.
   3) На телефоне масштаб не включаем: кабинет сотрудника — mobile-first,
      11px текста на телефоне нечитаемы. А вот планшет (тач, но экран уже
      не телефонный) без масштаба получал интерфейс на четверть крупнее
      задуманного: на iPad сайдбар съедал четверть ширины, подписи в
      карточках обрезались, а ячейки календаря переполнялись. Там масштаб
      мягче десктопного — 0.9: плотность выправляется, но тач-цели
      остаются крупными (кнопка 42px -> 40px, а не 36px).
   JS, который кладёт экранные координаты в inline-стили (меню сообщений,
   поповер периода, автоскролл матрицы, курсор на логине), делит их на
   этот же масштаб — см. uiScale() в app.js.
   ========================================================================= */
:root {
  --ui-scale: 1;
  --vu: 1;
}
/* @supports — страховка: если движок не знает zoom, масштаб не включается
   вовсе и --vu остаётся 1 (иначе вьюпортные единицы уехали бы в 1.25 без
   компенсирующего сжатия). */
@supports (zoom: 0.8) {
  @media (min-width: 820px) and (pointer: fine) {
    :root {
      --ui-scale: 0.85;
      --vu: 1.1765;   /* = 1 / --ui-scale, держать в паре */
    }
  }
  /* Планшет и тач-ноутбук: та же плотность, но помягче — палец крупнее курсора. */
  @media (min-width: 820px) and (pointer: coarse) {
    :root {
      --ui-scale: 0.95;
      --vu: 1.0526;  /* = 1 / --ui-scale, держать в паре */
    }
  }
  /* 4K без системного масштабирования (вьюпорт от 3000 реальных px): сжимать
     нечего — там и без того 11px подписи уходят в полтора миллиметра. Порог
     выше любого 2560-монитора, чтобы привычная плотность на 27" не менялась.
     Медиа меряет реальный вьюпорт, поэтому включение масштаба не сдвигает
     границу и правило не «моргает». */
  @media (min-width: 3000px) {
    :root {
      --ui-scale: 1;
      --vu: 1;
    }
  }

  /* ---- Размер шрифта: личная настройка (22.08.2026) ---------------------
     Рычаг тот же, что у плотности, — масштаб корня. Отдельного «крупнее
     только текст» в системе нет намеренно: вся вёрстка в px, и один
     увеличенный шрифт при прежних отступах вылезал бы из кнопок, ячеек
     смен и подписей графиков. Поэтому размер текста едет вместе с
     обвязкой, как зум браузера, — но помнится за человеком, а не за
     браузером: значение лежит в User.text_size, а атрибут data-text-size
     на <html> ставит СЕРВЕР (base.html). Поэтому размер приезжает и на
     телефон, и на компьютер точки, а страница не успевает моргнуть
     обычным шрифтом и прыгнуть под выбранный.

     Пары «--ui-scale / --vu» выписаны руками для каждого брейкпоинта:
     --vu строго 1 / --ui-scale (см. выше), а вычислить его в calc() нельзя —
     uiScale() в app.js читает переменную как ЧИСЛО, а невычисленный calc()
     в custom property возвращается строкой и даёт NaN.
     Множители к норме брейкпоинта: s = 0.92, l = 1.12, xl = 1.25.
     Правила живут внутри @supports (zoom): без zoom масштаб не включается
     вовсе, и трогать --vu там нельзя.
     Порядок важен: сначала база (телефон), потом брейкпоинты — селекторы
     равной специфичности, побеждает последний.
     ---------------------------------------------------------------------- */
  html[data-text-size="s"]  { --ui-scale: 0.92; --vu: 1.087; }
  html[data-text-size="l"]  { --ui-scale: 1.12; --vu: 0.8929; }
  html[data-text-size="xl"] { --ui-scale: 1.25; --vu: 0.8; }

  @media (min-width: 820px) and (pointer: fine) {
    html[data-text-size="s"]  { --ui-scale: 0.782;  --vu: 1.2788; }
    html[data-text-size="l"]  { --ui-scale: 0.952;  --vu: 1.0504; }
    html[data-text-size="xl"] { --ui-scale: 1.0625; --vu: 0.9412; }
  }
  @media (min-width: 820px) and (pointer: coarse) {
    html[data-text-size="s"]  { --ui-scale: 0.874;  --vu: 1.1442; }
    html[data-text-size="l"]  { --ui-scale: 1.064;  --vu: 0.9398; }
    html[data-text-size="xl"] { --ui-scale: 1.1875; --vu: 0.8421; }
  }
  @media (min-width: 3000px) {
    html[data-text-size="s"]  { --ui-scale: 0.92; --vu: 1.087; }
    html[data-text-size="l"]  { --ui-scale: 1.12; --vu: 0.8929; }
    html[data-text-size="xl"] { --ui-scale: 1.25; --vu: 0.8; }
  }
}

/* Планшет в портрете: 260px меню — это почти треть экрана, и на содержимое
   остаётся меньше, чем на телефоне в ландшафте. Сужаем рельс, подписи пунктов
   в него по-прежнему помещаются («Журнал действий» — самый длинный). */
/* Селектор с [data-theme] обязателен: базовые токены объявлены в
   «:root, html[data-theme="light"]», и голый :root проигрывает им по
   специфичности, сколько бы ниже по файлу ни стоял. */
@media (min-width: 820px) and (max-width: 1024px) and (pointer: coarse) {
  :root, html[data-theme] { --sidebar-w: 216px; }
}

/* ---- Reset / base ------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  zoom: var(--ui-scale);
  -webkit-text-size-adjust: 100%; background: var(--color-bg);
  /* Резервируем место под вертикальный скроллбар всегда: иначе переход с
     короткой страницы (без скролла) на длинную (со скроллом) сужает вьюпорт
     на ширину полосы и весь layout, включая sticky-сайдбар, «прыгает» вбок.
     На macOS с overlay-полосами не даёт видимого зазора. */
  scrollbar-gutter: stable;
  /* Резиновый отскок (rubber-band) macOS/iOS выключен. Иначе рывок колесом
     вверх на самом верху и упор вниз в конце страницы тянут весь документ
     целиком — вместе со sticky-шапкой и рельсом сайдбара, — и у кромки окна
     открывается голый холст: панель на глазах «отходит» от края.
     Только по вертикали: overscroll-behavior-x сломал бы жест «назад» двумя
     пальцами. Дублируем на body — движки берут значение с того элемента,
     что определяет вьюпорт (у body здесь свой overflow-x). */
  overscroll-behavior-y: none;
}
body {
  margin: 0;
  font-family: "Onest", "Segoe UI", system-ui, -apple-system, Roboto, Arial, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  font-size: 14px;
  line-height: 1.5;
  /* Onest уже плотнее Golos на тех же кеглях, поэтому общий отрицательный
     трекинг снят: он складывался с рисунком шрифта и на 11–12 px подписях
     буквы начинали слипаться. Трекинг остался там, где он осмыслен — на
     заголовках и крупных значениях (см. h1-h4 и --metric-value-size). */
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  overscroll-behavior-y: none;
  font-variant-numeric: tabular-nums;
}
/* Светлое полотно — тоже не плоская плита: тот же приём, что и в тёмной теме,
   только альфы втрое тише (1.5–3%). Даёт полотну лёгкий объём, при этом
   карточки остаются чисто белыми. */
html[data-theme="light"] body {
  background:
    radial-gradient(1100px 520px at 84% -8%, rgba(0, 91, 255, 0.030), transparent 62%),
    radial-gradient(900px 480px at -8% 104%, rgba(201, 174, 133, 0.045), transparent 60%),
    radial-gradient(760px 420px at 104% 92%, rgba(241, 37, 107, 0.016), transparent 58%),
    var(--color-bg);
  background-attachment: fixed;
}
/* Полотно почти плоское — как в референсе. Свечения оставлены на 3–4% альфы
   только чтобы у чёрного была глубина: синий сверху, голубой снизу.
   fixed — свечение не «едет» при скролле. Логин (.signin) рисует свой фон сам. */
html[data-theme="dark"] body {
  background:
    radial-gradient(1100px 520px at 82% -6%, rgba(25, 109, 255, 0.05), transparent 62%),
    radial-gradient(900px 480px at -8% 106%, rgba(79, 144, 255, 0.035), transparent 60%),
    var(--color-bg);
  background-attachment: fixed;
}
h1, h2, h3, h4, p { margin: 0; }
h1, h2, h3, h4 { color: var(--color-heading); letter-spacing: -0.02em; }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
button:disabled,
[aria-disabled="true"] { cursor: not-allowed; opacity: .55; }
img { max-width: 100%; }
svg { display: block; }
::selection { background: color-mix(in srgb, var(--color-primary) 26%, transparent); color: var(--color-text); }

/* Refined scrollbars — quiet until hovered. */
* { scrollbar-width: thin; scrollbar-color: var(--color-border-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--color-muted) 34%, transparent);
  border-radius: 999px; border: 2px solid transparent; background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-muted) 55%, transparent); background-clip: padding-box; }

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 0;
  box-shadow: var(--ring);
}

/* Плавную смену темы держит секция «Смена темы» ниже: волна на View
   Transitions (html.theme-vt), а где переходов нет — класс html.theme-shift,
   который живёт ровно полсекунды самой смены.

   Здесь же до 25.08.2026 висел ВТОРОЙ, постоянный набор переходов
   (html.theme-anim): по 0.4 s на цвет, фон, рамку и тень для .btn, .input,
   .card, .badge, пунктов меню, шапки и боковой панели. Он остался от версии
   до волны и с тех пор красил не тему, а каждое наведение мыши: селектор
   специфичнее собственного правила .btn, поэтому подсветка кнопки наливалась
   0.4 s вместо своих 0.18 s — «навожу на плюс, а выделяется он, только когда
   курсор уже в середине» (заказчик, 25.08.2026). Заодно он выбрасывал у .btn
   переходы transform и filter: transition перечисляет свойства целиком.

   Убрано. Подсветку состояний каждый компонент задаёт сам, а смена темы
   ничего не потеряла — обе её дороги живут ниже. */

/* =========================================================================
   Brand
   ========================================================================= */
.brand { display: inline-flex; align-items: center; gap: 12px; }
.brand__logo { height: 48px; width: auto; max-width: 172px; object-fit: contain; }
.brand--lg .brand__logo { height: 78px; max-width: 310px; }

/* Единый бренд-блок панелей (вход, пароли, страницы ошибок): настоящий
   логотип без плашки и рамки — картинка кладётся прямо на фон карточки.
   Текстовое написание — запасной вариант, если картинка не загрузилась. */
.brand-logo { display: inline-flex; align-items: center; justify-content: center; }
.brand-logo__img { height: 48px; width: auto; max-width: 100%; object-fit: contain; }
/* .auth-card — колоночный flex: без этого блок растянулся бы на всю ширину. */
.auth-card > .brand-logo { align-self: flex-start; }
.brand-logo--sm .brand-logo__img { height: 42px; }
.brand-logo--lg .brand-logo__img { height: 92px; }
.brand-logo__word {
  display: none; font-weight: 800; font-size: 22px;
  letter-spacing: .04em; color: var(--color-primary-dark);
}
.brand-logo--lg .brand-logo__word { font-size: 32px; }
.brand-logo--fallback .brand-logo__word { display: inline-flex; }

/* =========================================================================
   Buttons
   ========================================================================= */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  /* Кнопка — таблетка, как в референсе. line-height остаётся 1: у Onest нижний
     выносной глубже, чем у Golos, поэтому padding поднят 10 → 11px, иначе
     хвосты «у», «д», «щ» садились на нижнюю кромку. */
  padding: 11px 18px; border-radius: var(--radius-pill); border: 1px solid transparent;
  font-weight: 650; font-size: 13.5px; line-height: 1; white-space: nowrap;
  letter-spacing: -0.01em;
  transition: transform .16s var(--ease), box-shadow .18s var(--ease),
              background-color .18s var(--ease), border-color .18s var(--ease), filter .18s var(--ease);
}
.btn svg { width: 17px; height: 17px; }
.btn__caret { width: 15px !important; height: 15px !important; opacity: .7; }
.btn:active { transform: translateY(1px); }
/* Кнопка во всю ширину стоит под таким же полем — и высотой ей под стать. */
.btn-block { width: 100%; min-height: var(--control-h); }
.btn-sm { padding: 8px 14px; font-size: 13px; min-height: var(--control-h-sm); }
.btn-lg { padding: 14px 26px; font-size: 15px; border-radius: var(--radius-pill); }
.btn:disabled,
.btn.is-disabled { pointer-events: none; filter: grayscale(.08); }
.btn.is-loading { position: relative; color: transparent !important; pointer-events: none; }
.btn.is-loading::after {
  content: ""; position: absolute; width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent; color: #fff;
  animation: spin .8s linear infinite;
}

.btn-primary {
  background: var(--gradient-primary);
  color: var(--color-on-primary); box-shadow: var(--shadow-primary);
}
.btn-primary:hover { filter: brightness(1.06); box-shadow: var(--shadow-primary); transform: translateY(-1px); }

.btn-secondary {
  background: var(--color-surface); color: var(--color-text);
  border-color: var(--color-border-strong); box-shadow: var(--shadow-xs);
}
.btn-secondary:hover { border-color: color-mix(in srgb, var(--color-primary) 55%, var(--color-border)); color: var(--color-primary-dark); background: var(--color-surface-alt); transform: translateY(-1px); }

.btn-danger {
  background: var(--color-danger); color: #fff;
  box-shadow: 0 8px 20px color-mix(in srgb, var(--color-danger) 28%, transparent);
}
.btn-danger:hover { filter: brightness(1.05); transform: translateY(-1px); }

.btn-success {
  background: var(--color-success); color: #fff;
  box-shadow: 0 8px 20px color-mix(in srgb, var(--color-success) 26%, transparent);
}
.btn-success:hover { filter: brightness(1.05); transform: translateY(-1px); }

.btn-warning {
  background: var(--color-warning); color: #fff;
  box-shadow: 0 8px 20px color-mix(in srgb, var(--color-warning) 28%, transparent);
}
.btn-warning:hover { filter: brightness(1.04); transform: translateY(-1px); }

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

.btn-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: var(--radius-md);
  background: var(--color-surface); border: 1px solid var(--color-border);
  color: var(--color-muted); position: relative;
  transition: transform .16s var(--ease), color .16s var(--ease), border-color .16s var(--ease), background-color .16s var(--ease);
}
/* flex: 0 0 auto — иконка не обязана уступать место соседям. Соседей у
   неё быть не должно вовсе (счётчик лежит absolute), но когда в кнопку
   прилетало лишнее (htmx-поллинг после конца сессии), svg сжимался в ноль,
   и колокольчик исчезал. Ограничитель ущерба к HtmxAuthRedirectMiddleware. */
.btn-icon svg { width: 19px; height: 19px; flex: 0 0 auto; }
.btn-icon:hover { color: var(--color-primary-dark); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); background: var(--color-surface-alt); transform: translateY(-1px); }

/* =========================================================================
   Inputs / fields
   ========================================================================= */
.field { display: flex; flex-direction: column; gap: 7px; }
/* Фиксированная строка подписи: поля в сетке выравниваются по верху, поэтому
   подпись в одну строку обязана быть одной высоты у всех — иначе контролы
   встают на разном уровне. */
.field__label { font-size: 13px; font-weight: 600; color: var(--color-muted); line-height: 20px; }
.form-label { font-size: 13px; font-weight: 700; color: var(--color-muted); }
.input,
.form-control {
  width: 100%; padding: 12px 16px; font-size: 14px; line-height: 1.25;
  /* Шрифт формы НЕ наследуется от body сам: браузер даёт полям собственные
     дефолты, и <textarea class="input"> до этого рисовалась моноширинной —
     во всех формах проекта. Ни один стиль моноширинный текст в полях не
     запрашивал, так что это была потеря шрифта, а не решение. */
  font-family: inherit;
  min-height: var(--control-h);
  background: var(--color-surface-alt); color: var(--color-text);
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-lg);
  outline: none;
  transition: border-color .18s var(--ease), box-shadow .22s var(--ease), background-color .18s var(--ease);
}
/* Нативные виджеты даты/времени игнорируют padding и тянутся выше остальных
   контролов — высоту им задаём жёстко, иначе строка формы «лесенкой». */
input[type="date"].input,
input[type="time"].input,
input[type="datetime-local"].input,
input[type="month"].input,
input[type="date"].form-control,
input[type="time"].form-control,
input[type="datetime-local"].form-control,
input[type="month"].form-control { height: var(--control-h); }
/* Поле рядом с .btn-sm (действия в строке таблицы). Класс .input-sm разметка
   просила в трёх местах, а правил у него не было — поле оставалось полного
   роста и торчало над кнопками. Дате высоту переопределяем отдельно: правило
   выше жёстко задаёт ей --control-h. */
.input-sm { padding: 7px 11px; font-size: 13px; min-height: var(--control-h-sm); }
input[type="date"].input-sm,
input[type="time"].input-sm,
input[type="datetime-local"].input-sm,
input[type="month"].input-sm { height: var(--control-h-sm); }
.input::placeholder,
.form-control::placeholder { color: var(--color-faint); opacity: 1; }
.input:hover,
.form-control:hover { border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-border-strong)); }
.input:focus,
.form-control:focus { border-color: var(--color-primary); background: var(--color-surface); box-shadow: var(--ring-soft); }
textarea.input,
textarea.form-control { min-height: 118px; resize: vertical; }
select.input,
select.form-control { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--color-muted) 50%), linear-gradient(135deg, var(--color-muted) 50%, transparent 50%); background-position: calc(100% - 22px) 52%, calc(100% - 16px) 52%; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; padding-right: 40px; }

.checkbox,
.checkbox-row { display: inline-flex; align-items: center; gap: 9px; font-size: 13px; color: var(--color-muted); cursor: pointer; user-select: none; }
.checkbox input { position: absolute; opacity: 0; width: 0; height: 0; }
.checkbox__box {
  width: 19px; height: 19px; border-radius: 6px; border: 1.5px solid var(--color-border);
  background: var(--color-surface); position: relative; transition: all .18s var(--ease);
}
.checkbox__box::after {
  content: ""; position: absolute; inset: 0; margin: auto; width: 5px; height: 9px;
  border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0);
  transition: transform .18s var(--ease); top: -1px;
}
.checkbox input:checked + .checkbox__box { background: var(--color-primary); border-color: var(--color-primary); }
.checkbox input:checked + .checkbox__box::after { transform: rotate(45deg) scale(1); }
.checkbox input:focus-visible + .checkbox__box { box-shadow: var(--ring); }
/* Выключенная галочка (например, поштучный выбор под «все найденные»)
   не должна выглядеть активной — нативный disabled на скрытом input не виден. */
.checkbox input:disabled + .checkbox__box { opacity: .5; }
.checkbox:has(input:disabled) { opacity: .7; cursor: default; }

.link { color: var(--color-primary-dark); font-weight: 600; transition: color .15s var(--ease); }
.link:hover { color: var(--color-primary); text-decoration: underline; }

.alert { padding: 11px 14px; border-radius: var(--radius-md); font-size: 13px; font-weight: 550; border: 1px solid transparent; }
.alert-info,
.alert--info { background: var(--color-primary-soft); color: var(--color-primary-dark); border-color: color-mix(in srgb, var(--color-primary) 30%, transparent); }
.alert-success { background: var(--color-success-soft); color: var(--color-success); border-color: color-mix(in srgb, var(--color-success) 30%, transparent); }
.alert-warning { background: var(--color-warning-soft); color: var(--color-warning); border-color: color-mix(in srgb, var(--color-warning) 34%, transparent); }
.alert-danger,
.alert-error { background: var(--color-danger-soft); color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 34%, transparent); }
.input--error { border-color: var(--color-danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger) 16%, transparent); }

/* Плашка с кнопкой «Отменить» (02.09.2026, оживлена 04.09.2026).
   Флексом становится ТОЛЬКО она: внутри обычных плашек бывает своя вёрстка
   (.alert-rows на экране контроля), и делать флексовым сам .alert нельзя.
   position/overflow — ради полосы времени: она лежит по нижнему краю плашки
   и обязана обрезаться её же скруглением. */
.alert--action {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  position: relative; overflow: hidden;
  animation: undo-alert-in .34s var(--ease) both;
}
.alert--action .alert__text { flex: 1 1 200px; min-width: 0; }
.alert__action { flex: 0 0 auto; margin: 0; }
/* Кнопка живёт на цветной подложке плашки, поэтому берёт её цвет, а не
   приглушённый цвет .btn-ghost — на зелёном фоне успеха тот почти не виден. */
.alert__undo {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 12px; color: inherit;
  border-color: color-mix(in srgb, currentColor 32%, transparent);
  background: color-mix(in srgb, currentColor 8%, transparent);
}
.alert__undo:hover { color: inherit; background: color-mix(in srgb, currentColor 16%, transparent); }
/* Знак — 15px, а не общий размер иконки кнопки: подпись здесь мелкая (13px),
   и знак вровень с ней читается как часть слова, а не как отдельная кнопка. */
.alert__undo-icon { width: 15px; height: 15px; flex: 0 0 auto; transition: transform .3s var(--ease); }
/* Оборот знака на наведении — обещание того, что нажатие вернёт как было. */
.alert__undo:hover .alert__undo-icon { transform: rotate(-150deg); }
/* Остаток времени: моноширинные цифры, иначе «14:59» → «9:07» дёргает подпись
   на каждой секунде. Пустую клетку не показываем — без JS её вовсе нет. */
.alert__undo-clock {
  font-variant-numeric: tabular-nums; font-size: 12px; font-weight: 600;
  opacity: .72; letter-spacing: .01em;
}
/* Точка-разделитель — псевдоэлементом, а не текстом: клетка пуста до первого
   тика JS, и в разметке разделитель висел бы одиноким знаком препинания. */
.alert__undo-clock::before { content: "·"; margin-right: 5px; opacity: .6; }
.alert__undo-clock:empty { display: none; }
/* Полоса времени билета. Ширину ведёт JS через --undo-left (доля от 0 до 1):
   пятнадцать минут — это не тот срок, который можно отдать CSS-анимации,
   потому что страница может простоять открытой дольше и полоса «отыграла бы»
   раньше или позже настоящего конца. Переход в 1s linear делает шаг раз в
   секунду плавным. */
.alert__timer {
  position: absolute; left: 0; bottom: 0; height: 2px;
  width: 100%; transform-origin: left center;
  transform: scaleX(var(--undo-left, 1));
  background: currentColor; opacity: .32;
  transition: transform 1s linear;
}
/* Время вышло: кнопка гаснет и перестаёт быть кликабельной (сервер откажет всё
   равно), полоса убирается — плашка становится обычным сообщением. */
.alert--action.is-undo-expired .alert__undo { opacity: .45; pointer-events: none; }
.alert--action.is-undo-expired .alert__timer { opacity: 0; }
@keyframes undo-alert-in {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}
/* Первые секунды кнопка мягко «дышит»: плашка появляется после действия,
   взгляд в этот момент на таблице, и без подсказки кнопку не замечают.
   Пульс подложкой, а не расходящимся кольцом (box-shadow): у плашки
   overflow: hidden ради полосы времени, и кольцо она срезала бы по краям.
   Заливка ничего за границы не выносит, поэтому видна целиком. */
.alert__undo { animation: undo-btn-attention 1.15s var(--ease) 2; }
@keyframes undo-btn-attention {
  0%, 100% { background: color-mix(in srgb, currentColor 8%, transparent); }
  50% { background: color-mix(in srgb, currentColor 24%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .alert--action, .alert__undo { animation: none; }
  .alert__timer { transition: none; }
  .alert__undo:hover .alert__undo-icon { transform: none; }
}

/* Blast-radius list on the hard-delete confirmation screens. */
.impact-list { list-style: none; margin: 6px 0 14px; padding: 0; display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
.impact-list li { display: flex; justify-content: space-between; gap: 12px; padding: 7px 11px; border-radius: var(--radius-sm); background: var(--color-surface-2, color-mix(in srgb, var(--color-muted) 8%, transparent)); }
.impact-list--danger li { background: var(--color-danger-soft); color: var(--color-danger); }
.form-error { color: var(--color-danger); font-size: 12px; font-weight: 650; }
.field__hint,
.form-help { color: var(--color-muted); font-size: 12px; }
/* Подсказку, которую пишет JS (сколько человек осталось после сужения),
   в пустом виде показывать нечего — иначе колонка поля держит лишний отступ. */
.field__hint:empty { display: none; }
/* Подпись, которая предупреждает, а не поясняет: обещание формы («сотрудник
   это увидит») не выполнится из-за настройки вне этой формы. */
.field__hint--warn { color: var(--color-warning); font-weight: 600; }
.req { color: var(--color-coral); font-weight: 800; }

.password-field { position: relative; display: block; }
.password-field__input { padding-right: 48px; }
.password-field__toggle {
  position: absolute; top: 50%; right: 7px; transform: translateY(-50%);
  width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--color-muted);
  transition: background-color .16s var(--ease), color .16s var(--ease), transform .16s var(--ease);
}
.password-field__toggle:hover,
.password-field__toggle:focus-visible { color: var(--color-primary-dark); background: var(--color-primary-soft); }
.password-field__toggle:focus-visible { outline: none; box-shadow: var(--ring-soft); }
.password-field__toggle:active { transform: translateY(-50%) scale(.97); }
.password-field__icon {
  width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
.password-field__icon--hide,
.password-field__toggle.is-visible .password-field__icon--show { display: none; }
.password-field__toggle.is-visible .password-field__icon--hide { display: block; }

/* =========================================================================
   Badges
   ========================================================================= */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 11px; border-radius: var(--radius-pill);
  font-size: 12px; font-weight: 700; line-height: 1.35; white-space: nowrap;
}
.badge-info, .badge-primary { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge-success { background: var(--color-success-soft); color: var(--color-success); }
.badge-warning { background: var(--color-warning-soft); color: var(--color-warning); }
.badge-danger  { background: var(--color-danger-soft); color: var(--color-danger); }
/* Внутреннее кольцо снято со всех бейджей (03.09.2026): референс требует чистую
   заливку, а --color-*-soft в тёмной теме и так даёт ровно те «~14% тона».
   Возвращено ниже только .badge--nodata, где кольцо отличает «данных нет» от
   обычного серого статуса — там оно значащее, а не декоративное. */
.badge-secondary { background: var(--color-surface-soft); color: var(--color-muted); }

/* =========================================================================
   App shell : sidebar + main
   ========================================================================= */
.app-shell { display: flex; min-height: calc(100vh * var(--vu)); }

/* ---- Overscroll (rubber-band) continuation ------------------------------
   Страховка на случай движка без overscroll-behavior (Safari до 16): там
   «резиновый» отскок остаётся, и macOS тянет всю страницу целиком (включая
   sticky/fixed), обнажая голый canvas выше/ниже вьюпорта. Эти fixed-полосы
   продолжают интерфейс в зазор: графитовый рельс сайдбара слева, фон
   страницы справа — отскок выглядит намеренным, а не белой дырой.
   Там, где отскок выключен (html/body: overscroll-behavior-y: none), полосы
   не показываются вовсе: в обычном состоянии они целиком за пределами
   экрана. */
.app-shell::before,
.app-shell::after {
  content: ""; position: fixed; left: 0; right: 0; height: calc(60vh * var(--vu)); z-index: 45;
  pointer-events: none; background: var(--color-bg);
}
.app-shell::before { top: calc(-60vh * var(--vu)); }
.app-shell::after { bottom: calc(-60vh * var(--vu)); }

@media (min-width: 820px) {
  .app-shell::before,
  .app-shell::after { --overscroll-rail-w: var(--sidebar-w); }
  html.sidebar-collapsed .app-shell::before,
  html.sidebar-collapsed .app-shell::after { --overscroll-rail-w: 74px; }
  /* Сверху сайдбар начинается с --sidebar-bg, снизу заканчивается
     --sidebar-bg-deep (вертикальный градиент рельса), плюс 1px бордера. */
  .app-shell::before {
    background: linear-gradient(90deg,
      var(--sidebar-bg) 0 calc(var(--overscroll-rail-w) - 1px),
      var(--sidebar-border) calc(var(--overscroll-rail-w) - 1px) var(--overscroll-rail-w),
      var(--color-bg) var(--overscroll-rail-w));
  }
  .app-shell::after {
    background: linear-gradient(90deg,
      var(--sidebar-bg-deep) 0 calc(var(--overscroll-rail-w) - 1px),
      var(--sidebar-border) calc(var(--overscroll-rail-w) - 1px) var(--overscroll-rail-w),
      var(--color-bg) var(--overscroll-rail-w));
  }
}

.sidebar {
  position: sticky; top: 0; align-self: flex-start;
  width: var(--sidebar-w); height: calc(100vh * var(--vu)); flex: 0 0 var(--sidebar-w);
  background: linear-gradient(180deg, var(--sidebar-bg), var(--sidebar-bg-deep));
  border-right: 1px solid var(--sidebar-border);
  /* Рельс отбрасывает мягкую тень на полотно: слои шелла читаются глубиной,
     а не только 1px-бордером. Смещение только вправо — влево тени некуда. */
  box-shadow: 14px 0 34px -22px rgba(6, 8, 14, 0.55);
  display: flex; flex-direction: column; padding: 18px 14px; gap: 6px; z-index: 40;

  /* The sidebar is graphite in both themes, so every token-driven child
     (badges, role chips, profile card) resolves dark-surface values here. */
  --color-surface: #1D212B;
  --color-surface-soft: #232836;
  --color-surface-alt: #20242F;
  --color-border: #2A2F3C;
  --color-border-strong: #3B4252;
  --color-text: #D9DCE3;
  --color-heading: var(--sidebar-heading);
  --color-muted: var(--sidebar-text);
  --color-faint: #7C8497;
  --color-primary: var(--sidebar-active);
  --color-primary-dark: #8FAFFF;
  --color-primary-soft: rgba(91, 140, 255, 0.15);
  --color-coral-soft: rgba(255, 92, 147, 0.15);
  --color-beige-soft: rgba(213, 188, 147, 0.14);
  --color-success-soft: rgba(52, 201, 127, 0.15);
  --color-warning-soft: rgba(242, 169, 59, 0.15);
  --color-danger-soft: rgba(242, 109, 118, 0.16);
  --color-success: #34C97F;
  --color-warning: #F2A93B;
  --color-danger: #F26D76;
  color-scheme: dark;
}
/* В тёмной теме рельс уходит в нейтральный чёрный вместе с полотном — иначе
   графитовая плита из светлой темы отсвечивала бы синим. Значения на ступень
   светлее --sidebar-bg, чтобы вложенные карточки (профиль, бейджи)
   отделялись от самого рельса. */
html[data-theme="dark"] .sidebar {
  --color-surface: #141417;
  --color-surface-soft: #1D1E22;
  --color-surface-alt: #18191C;
  --color-border: #1F2024;
  --color-border-strong: #303237;
  --color-text: #D6D7DB;
  --color-faint: #7E7F87;
  --color-primary-dark: #80AFFF;
  --color-primary-soft: rgba(65, 135, 255, 0.16);
}
/* Активный пункт — сплошная синяя таблетка (подпись референса), а не
   полупрозрачная подсветка светлой темы. Метка-риска слева при сплошной
   заливке становится лишней. */
html[data-theme="dark"] .sidebar-item.is-active {
  background: var(--sidebar-active);
  box-shadow: 0 6px 18px -8px var(--sidebar-active);
}
html[data-theme="dark"] .sidebar-item.is-active::before { opacity: 0; }
html[data-theme="dark"] .sidebar-item.is-active .sidebar-item__icon { color: #FFFFFF; }
/* Верхний отступ больше нижнего: рельс начинается у самой кромки окна
   (padding-top самого .sidebar съедает правило безопасных зон), и без воздуха
   логотип упирался в край. */
.sidebar__brand { padding: 18px 8px 14px; }
.sidebar__nav { display: flex; flex-direction: column; gap: 2px; overflow-y: auto; flex: 1; }
.sidebar__footer { padding-top: 12px; border-top: 1px solid var(--sidebar-border); margin-top: 6px; }
/* Подпись разработчика внизу рельса: тихая, не перетягивает внимание с меню. */
.sidebar__copy {
  display: flex; flex-direction: column; gap: 1px;
  padding: 10px 12px 2px; font-size: 11px; line-height: 1.35;
  color: var(--color-faint); letter-spacing: .01em;
}
.sidebar__copy-line { white-space: nowrap; }

.sidebar-item {
  position: relative; display: flex; align-items: center; gap: 11px;
  /* 7px по вертикали вместо 10 (заказчик 03.09.2026: «чтобы там всё помещалось
     или практически всё»). Пункт становится 34px вместо 40 — при иконке 19px и
     подписи 13,5px это ещё комфортная цель, а на девятнадцати пунктах экономит
     114 CSS-px, ровно ту величину, которой не хватало. */
  padding: 7px 12px; border-radius: 12px;
  color: var(--sidebar-text); font-weight: 550; font-size: 13.5px;
  transition: background-color .18s var(--ease), color .18s var(--ease), transform .18s var(--ease);
}
.sidebar-item__icon { width: 19px; height: 19px; flex: 0 0 19px; opacity: .8; }
.sidebar-item__label { flex: 1; }
.sidebar-item__badge { margin-left: auto; }
.sidebar-item::before {
  content: ""; position: absolute; left: -14px; top: 50%; transform: translateY(-50%) scaleY(0);
  width: 3px; height: 20px; border-radius: 0 3px 3px 0; background: var(--sidebar-active);
  transition: transform .25s var(--ease);
}
.sidebar-item:hover { background: rgba(255, 255, 255, 0.05); color: var(--sidebar-heading); }
.sidebar-item:hover .sidebar-item__icon { opacity: 1; }
/* Подсветка активного пункта считается от --sidebar-active, а не зашита
   синим: рельс графитовый в обеих темах, но акцент в них разный. */
.sidebar-item.is-active {
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--sidebar-active) 18%, transparent),
    color-mix(in srgb, var(--sidebar-active) 8%, transparent));
  color: #FFFFFF; font-weight: 600;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sidebar-active) 22%, transparent);
}
.sidebar-item.is-active::before { transform: translateY(-50%) scaleY(1); }
.sidebar-item.is-active .sidebar-item__icon { color: var(--sidebar-active); opacity: 1; }

/* ---- Collapsed sidebar (только desktop; на мобильном остаётся drawer) --- */
@media (min-width: 820px) {
  html.sidebar-collapsed .sidebar {
    width: 74px; flex: 0 0 74px; padding: 18px 12px;
  }
  html.sidebar-collapsed .sidebar__brand { display: none; }
  html.sidebar-collapsed .sidebar__section,
  html.sidebar-collapsed .sidebar-item__label,
  html.sidebar-collapsed .sidebar-item__badge,
  html.sidebar-collapsed .sidebar-item__soon { display: none; }
  html.sidebar-collapsed .sidebar-item { justify-content: center; padding: 11px 0; gap: 0; }
  /* В свёрнутом рельсе (74px) название компании не помещается — остаётся год. */
  html.sidebar-collapsed .sidebar__copy { align-items: center; padding: 10px 0 2px; }
  html.sidebar-collapsed .sidebar__copy-line + .sidebar__copy-line { display: none; }
  html.sidebar-collapsed .sidebar-item::before { left: -12px; }
}

/* ---- Подпись иконки свёрнутого рельса (25.08.2026) ----------------------
   В рельсе шириной 74px от пункта остаётся одна иконка, и что она значит,
   объяснял родной title браузера. Он для этого не годится: ждёт около
   секунды НЕПОДВИЖНОГО курсора, на том же пункте второй раз не появляется
   и сам гаснет через несколько секунд — отсюда «приходится раздёргивать
   курсор, чтобы подпись показалась». Подпись рисуем сами (initRailTips в
   app.js): встаёт справа от иконки почти сразу, а при переходе на соседний
   пункт — мгновенно, без новой задержки.
   Узел лежит в <body> и позиционируется fixed, а не ::after у пункта:
   .sidebar__nav прокручивается (overflow-y: auto) и обрезает всё, что
   выходит за ширину рельса. Координаты ставит JS — они экранные, поэтому
   делятся на uiScale() (см. html { zoom }).
   Видимостью правит только .is-open: display не трогаем, иначе замер высоты
   (offsetHeight) перед позиционированием давал бы ноль.
   Слой 90 — выше рельса (40) и шапки (34).
   ------------------------------------------------------------------------ */
.railtip {
  position: fixed; left: 0; top: 0; z-index: 90; pointer-events: none;
  padding: 6px 10px; border-radius: 8px; white-space: nowrap;
  background: var(--color-heading); color: var(--color-bg);
  font-size: 12px; font-weight: 600; line-height: 1.25;
  box-shadow: var(--shadow-sm);
  opacity: 0; visibility: hidden; transform: translateX(-5px);
  transition: opacity .14s var(--ease), transform .14s var(--ease),
              visibility 0s linear .14s;
}
.railtip.is-open {
  opacity: 1; visibility: visible; transform: none; transition-delay: 0s;
}

.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* ---- Topbar ------------------------------------------------------------- */
/* Шапка — свой stacking context (sticky + z-index + backdrop-filter), поэтому
   z-index её выпадашек (.dropdown-menu, 60) работает только ВНУТРИ неё, а
   наружу шапка со всем содержимым идёт одним слоем. Слой 34 (25.08.2026): выше
   липких панелей контента (≤30) — на 30 панель режима сборки накрывала меню
   аккаунта — и ниже шторки телефона (скрим 35, рельс 40).
   Липкая панель внутри страницы должна брать z-index < 34. */
.topbar {
  position: sticky; top: 0; z-index: 34;
  height: var(--topbar-h); display: flex; align-items: center; gap: 14px;
  padding: 0 22px; background: var(--topbar-bg);
  backdrop-filter: saturate(150%) blur(14px);
  border-bottom: 1px solid var(--color-border);
  box-shadow: var(--topbar-shadow);
}
/* В тёмной теме кромка снята: на полотне #0D0D0E линия #232428 читается
   отчётливой чертой, а глубину шапке и так даёт backdrop-filter. В светлой
   ОСТАВЛЕНА — там почти белая шапка на светлом полотне, и это её единственная
   граница. Сезонный хайрлайн .topbar::after (bottom: -1px) рассчитан на то,
   что под ним есть 1px рамки — в тёмной теме он теперь ложится на полотно,
   что верно: он и должен читаться как тонкая цветная нить, а не как вторая линия. */
/* Кромку шапки вернули вместе с рамками карточек (заказчик: «нет чёткого
   деления блоков») — она отделяет шапку от полотна в обеих темах. */
/* Круглые «призрачные» кнопки — из референса, и ТОЛЬКО в шапке: базовый
   .btn-icon носят десятки квадратных кнопок по всему продукту, и подъём
   радиуса в нём превратил бы в круги вообще все. */
.topbar .btn-icon {
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: transparent; border-color: transparent;
}
.topbar .btn-icon:hover { background: var(--color-surface-soft); transform: none; }
/* Поиск на светлой шапке: она сама почти белая, и surface-alt в ней
   растворялся — колодец даём цветом полотна. */
html[data-theme="light"] .topbar__search-input { background: var(--color-bg); }
.topbar__menu { display: inline-flex; }
/* «Назад» — вторая кнопка слева, сразу за меню (см. includes/back_button.html).
   flex: 0 0 auto, иначе на телефоне её сжимает логотип с margin-right: auto. */
.topbar__back { display: inline-flex; flex: 0 0 auto; }
/* Бренд в шапке появляется только на телефоне (см. mobile.css): на десктопе
   логотип стоит в сайдбаре, и второй был бы дублем. */
.topbar__brand { display: none; }
.topbar__search { position: relative; flex: 1; max-width: 540px; }
.topbar__search-icon { position: absolute; left: 17px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--color-faint); pointer-events: none; }
/* height И min-height парой: одного height мало — min-height из .input (44px)
   всегда перебивает его, и поле было на 2px выше написанного здесь. */
.topbar__search-input { padding-left: 46px; height: 50px; min-height: 50px; border-radius: var(--radius-pill); background: var(--color-surface-alt); border-color: transparent; font-size: 14px; }
.topbar__actions { display: flex; align-items: center; gap: 12px; margin-left: auto; }

.topbar__bell { position: relative; }
.topbar__bell-count {
  /* Смещение рассчитано на КРУГЛУЮ кнопку 44px (см. .topbar .btn-icon ниже):
     на прежнем квадрате 40px счётчик сидел в -5px, на круге того же -5px он
     ушёл бы от кромки — у круга угол «съеден» дугой. */
  position: absolute; top: -2px; right: -2px; min-width: 19px; height: 19px;
  padding: 0 5px; border-radius: var(--radius-pill); background: var(--color-coral);
  color: #fff; font-size: 11px; font-weight: 800; display: inline-flex;
  /* Кольцо-просвет вокруг счётчика — цветом ШАПКИ, а не полотна. Тот же тон
     обязан попасть и в анимацию pulse-soft (она перебивает box-shadow), —
     отсюда --pulse-ring. */
  --pulse-ring: var(--topbar-tint);
  align-items: center; justify-content: center; box-shadow: 0 0 0 3px var(--topbar-tint);
}

.topbar__user { position: relative; }
.topbar__user-button {
  display: flex; align-items: center; gap: 10px; padding: 4px 6px 4px 10px;
  border: 0; border-radius: var(--radius-lg); background: transparent; color: var(--color-text);
  transition: background-color .18s var(--ease), transform .18s var(--ease);
}
.topbar__user-button:hover { background: var(--color-surface-soft); transform: translateY(-1px); }
.topbar__user-meta { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.25; }
.topbar__user-name { font-weight: 700; font-size: 13.5px; }
.topbar__user-role { font-size: 12px; color: var(--color-muted); }
.topbar__user-avatar {
  width: 38px; height: 38px; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center; font-weight: 800; font-size: 13px; color: #fff;
  background: var(--gradient-brand);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14), var(--shadow-xs);
}
.topbar__user-avatar--img { object-fit: cover; background: var(--color-surface-soft); }

/* ---- Avatar page / profile identity ------------------------------------- */
/* .card.avatar-card: базовый .card объявлен ниже по файлу и при равной
   специфичности перебил бы flex-direction. */
.card.avatar-card {
  max-width: 760px; padding: 28px;
  flex-direction: row; flex-wrap: wrap; align-items: center; gap: 32px;
}
.avatar-card__preview {
  flex: 0 0 auto; min-width: 170px;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
}
/* Градиентное кольцо вокруг фото — как «сторис», просвет даёт бордер цвета карточки. */
.avatar-card__ring {
  display: inline-flex; padding: 4px; border-radius: 50%;
  background: var(--gradient-brand);
  box-shadow: 0 12px 30px color-mix(in srgb, var(--color-primary) 28%, transparent);
}
.avatar-card__img,
.avatar-card__placeholder {
  width: 126px; height: 126px; border-radius: 50%; display: block;
  border: 3px solid var(--color-surface);
}
.avatar-card__img { object-fit: cover; background: var(--color-surface-soft); }
.avatar-card__placeholder {
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 36px; color: #fff; background: var(--gradient-brand);
}
.avatar-card__caption { font-size: 12.5px; font-weight: 600; color: var(--color-muted); text-align: center; }
.avatar-card__form { flex: 1 1 320px; min-width: 260px; display: flex; flex-direction: column; gap: 16px; }

/* Дропзона: невидимый file-input растянут на всю область, клик и drop нативные. */
.avatar-drop {
  position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 5px; padding: 28px 20px; text-align: center; cursor: pointer;
  border: 1.5px dashed color-mix(in srgb, var(--color-primary) 36%, var(--color-border-strong));
  border-radius: var(--radius-lg); background: var(--color-surface-alt);
  transition: border-color .18s var(--ease), background-color .18s var(--ease), box-shadow .22s var(--ease);
}
.avatar-drop:hover,
.avatar-drop:focus-within,
.avatar-drop.is-dragover {
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 6%, var(--color-surface-alt));
  box-shadow: var(--ring-soft);
}
.avatar-drop__input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.avatar-drop__icon {
  width: 46px; height: 46px; margin-bottom: 5px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--color-primary); background: var(--color-primary-soft);
}
.avatar-drop__icon svg { width: 21px; height: 21px; }
.avatar-drop__title { font-size: 14px; font-weight: 700; color: var(--color-heading); }
.avatar-drop__or { font-weight: 500; color: var(--color-muted); }
.avatar-drop__hint { font-size: 12px; color: var(--color-muted); max-width: 340px; line-height: 1.5; }
.avatar-drop__file {
  display: inline-flex; margin-top: 7px; padding: 6px 13px; max-width: 100%;
  border-radius: var(--radius-pill); font-size: 12.5px; font-weight: 650;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.avatar-drop__file[hidden] { display: none; }
.avatar-card__actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
@media (max-width: 640px) {
  .card.avatar-card { flex-direction: column; align-items: stretch; gap: 20px; padding: 20px; }
  .avatar-card__preview { min-width: 0; }
  .avatar-card__actions .btn { flex: 1 1 auto; }
}

/* ---- Кадрирование аватара (модалка) -------------------------------------- */
.avatar-crop-modal[hidden] { display: none; }
/* z-index 120: как у camera-modal — выше sidebar (40) и дропдаунов topbar (60). */
.avatar-crop-modal { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: 24px; }
.avatar-crop-modal__backdrop { position: absolute; inset: 0; background: rgba(7,17,31,.72); backdrop-filter: blur(9px); }
.avatar-crop-modal__dialog {
  position: relative; z-index: 1; width: min(440px, 100%);
  display: flex; flex-direction: column; gap: 14px; padding: 18px;
  border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
  border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-pop);
}
.avatar-crop-modal__title { font-size: 18px; font-weight: 850; }
.avatar-crop-modal__hint { margin-top: 4px; color: var(--color-muted); font-size: 12.5px; line-height: 1.5; }
.avatar-crop__stage {
  position: relative; width: min(100%, 360px); aspect-ratio: 1; margin: 0 auto;
  border-radius: var(--radius-md); overflow: hidden; background: var(--color-bg-deep);
  border: 1px solid rgba(255,255,255,.08);
}
.avatar-crop__stage canvas { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
.avatar-crop__stage canvas:active { cursor: grabbing; }
/* Круглая маска: затемняем всё, что не попадёт в круглый аватар. */
.avatar-crop__mask {
  position: absolute; inset: 0; pointer-events: none; border-radius: 50%;
  box-shadow: 0 0 0 9999px rgba(7,17,31,.55), inset 0 0 0 2px rgba(255,255,255,.75);
}
.avatar-crop__zoom { display: flex; align-items: center; gap: 12px; font-size: 12.5px; color: var(--color-muted); }
.avatar-crop__zoom input[type="range"] { flex: 1; accent-color: var(--color-primary); }
.avatar-crop-modal__actions { display: flex; justify-content: flex-end; gap: 8px; }
@media (max-width: 560px) {
  .avatar-crop-modal { padding: 10px; }
  .avatar-crop-modal__dialog { padding: 14px; }
}

/* Свежее (только что кадрированное) изображение — короткий «вдох» кольца,
   чтобы результат кадрирования было видно. Класс ставит avatar_crop.js. */
@keyframes avatar-fresh-in {
  0%   { transform: scale(.82); filter: saturate(.6); }
  60%  { transform: scale(1.05); }
  100% { transform: none; filter: none; }
}
.avatar-card__ring.is-fresh { animation: avatar-fresh-in .42s var(--ease) both; }

/* ---- Картинка группового чата (форма настроек группы) -------------------- */
/* Тот же кроп-инструмент, что у аватара, но компактнее: превью слева,
   дропзона и «убрать» справа. На узком экране — в колонку. */
/* --- Форма группового чата (26.08.2026) -----------------------------------
   Было: карточка 640px у левого края широкого полотна, «Отмена» в
   противоположном углу экрана, большая пунктирная дропзона под картинку и
   столбик из девяти разноцветных бейджей ролей — заказчик: «выглядит
   некрасиво и перегружено». Стало: одна колонка по центру; картинка и
   название — общей шапкой, где дропзоной служит сам кружок; участники —
   тихий список-пикер в две колонки, роль строкой, а не цветной пилюлей.
   Страница-колонка: заголовок, кнопки и карточка стоят по одной оси. */
.page--form > * { width: 100%; max-width: 760px; margin-inline: auto; }

.group-form { gap: 18px; padding: 22px; }
.group-form__body { display: flex; flex-direction: column; gap: 20px; }
/* Шапка: кружок картинки + название рядом — так группа и выглядит в списке. */
.group-form__ident { display: flex; align-items: flex-start; gap: 16px; }
.group-form__ident-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }

/* Кружок картинки И ЕСТЬ дропзона: класс .avatar-drop оставлен ради подсветки
   перетаскивания и кроп-модалки, а его прямоугольное оформление снимаем. */
.group-pic {
  position: relative; flex: 0 0 auto;
  width: 84px; height: 84px; padding: 0; gap: 0; border-radius: 50%;
  border: 1px solid var(--color-border); background: var(--color-surface-alt);
  overflow: hidden;
}
/* Оба имени: своё (разметка) и общее avatar-card__* — с ним кроп-модалка
   подменяет превью после кадрирования, и созданный ею <img> наших классов не
   несёт. Селектор с .group-pic перебивает размеры страницы аватара. */
.group-pic .avatar-card__img,
.group-pic__img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  border: 0; border-radius: 0;
}
.group-pic .avatar-card__placeholder,
.group-pic__letter {
  display: grid; place-items: center; width: 100%; height: 100%;
  border: 0; border-radius: 0;
  background: var(--color-primary); color: var(--color-on-primary);
  font-size: 30px; font-weight: 750;
}
/* Значок «сменить» — как в мессенджерах: полупрозрачная полоса по низу
   кружка, которая наливается под курсором. */
.group-pic__edit {
  position: absolute; inset: auto 0 0 0; height: 26px;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-bg-deep) 55%, transparent);
  color: #fff; opacity: .85; transition: opacity .18s var(--ease), height .18s var(--ease);
}
.group-pic__edit svg { width: 15px; height: 15px; }
.group-pic:hover .group-pic__edit,
.group-pic:focus-within .group-pic__edit { opacity: 1; height: 30px; }
.group-pic.is-dragover { border-color: var(--color-primary); box-shadow: var(--ring-soft); }
.group-pic__note { line-height: 1.5; }
/* Имя выбранного файла (data-avatar-drop-file): приписка к подсказке. */
.group-pic__file { display: inline; margin-left: 4px; font-weight: 650; color: var(--color-primary-dark); }
.group-pic__file[hidden] { display: none; }
.checkbox--quiet { font-size: 12.5px; align-self: flex-start; }
/* display: inline-flex у .checkbox перебивает UA-правило [hidden] — гасим явно.
   Прячет строку «Убрать картинку» живой JS: с ним удаление живёт в просмотре
   самой картинки, а без JS галочка обязана остаться видимой. */
.checkbox[hidden] { display: none; }
/* «Картинка будет убрана» — предупреждение до сохранения формы. */
.group-pic__gone {
  align-self: flex-start; font-size: 12.5px; font-weight: 600;
  color: var(--color-warning);
}
.group-pic__gone[hidden] { display: none; }

.group-form__members { display: flex; flex-direction: column; gap: 10px; }
.group-form__section { display: flex; flex-direction: column; gap: 3px; }

/* Список кандидатов: строки-пикеры, а не столбик галочек с бейджами. Две
   колонки на широкой карточке — девять человек умещаются без прокрутки. */
.pick-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); gap: 2px;
  padding: 6px; border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); background: var(--color-surface-alt);
}
.pick-row {
  display: flex; align-items: center; gap: 10px; min-width: 0;
  padding: 7px 9px; border-radius: var(--radius-md);
  font-size: 13px; color: var(--color-text); cursor: pointer;
  transition: background-color .15s var(--ease);
}
.pick-row:hover { background: var(--color-surface); }
.pick-row:has(input:checked) { background: var(--color-primary-soft); }
.pick-row .checkbox__box { flex: 0 0 auto; }
.pick-row__ava { width: 30px; height: 30px; font-size: 12px; }
.pick-row__main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pick-row__name {
  font-weight: 650; color: var(--color-heading);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pick-row__role {
  font-size: 11.5px; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

@media (max-width: 560px) {
  .group-form { padding: 16px; }
  .group-form__ident { gap: 12px; }
  .group-pic { width: 68px; height: 68px; }
  .group-pic__letter { font-size: 24px; }
}

.profile-id { display: flex; align-items: center; gap: 14px; }
.profile-id__avatar {
  width: 48px; height: 48px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
  background: var(--color-surface-soft);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14), var(--shadow-xs);
}
.profile-id__avatar--initials {
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 16px; color: #fff; background: var(--gradient-brand);
}
.profile-id__change { font-size: 12.5px; color: var(--color-primary); }
.profile-id__change:hover { text-decoration: underline; }
.topbar__user-caret { width: 16px; height: 16px; color: var(--color-muted); }

.dropdown { position: relative; }
.dropdown-menu {
  position: absolute; right: 0; top: calc(100% + 10px); width: 240px; z-index: 60;
  padding: 6px; border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-pop);
  transform-origin: top right; animation: dropdown-in .18s var(--ease-out) both;
}
.dropdown-menu[hidden] { display: none; }
.dropdown-menu__head {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 11px 12px; margin-bottom: 4px;
  border-bottom: 1px solid var(--color-border);
}
.dropdown-menu__head span { color: var(--color-muted); font-size: 12px; }
.dropdown-menu a,
.dropdown-menu button {
  width: 100%; display: flex; align-items: center; min-height: 38px;
  padding: 9px 11px; border: 0; border-radius: var(--radius-md);
  background: transparent; color: var(--color-text); font: inherit; text-align: left;
}
.dropdown-menu a:hover,
.dropdown-menu button:hover { background: var(--color-surface-soft); color: var(--color-primary-dark); }
/* Счётчик в пункте меню прижат к правому краю: вплотную к тексту он читается
   как часть названия («Что нового 3»), а не как отдельная метка. */
.dropdown-menu a > .badge { margin-left: auto; }

/* ---- Page --------------------------------------------------------------- */
.page { padding: 28px; display: flex; flex-direction: column; gap: var(--gap-cards); }
.page__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.page__title { font-size: 26px; font-weight: 800; letter-spacing: -0.01em; }
.page__subtitle { color: var(--color-muted); margin-top: 4px; font-size: 14px; }
.page__actions { display: flex; gap: 10px; flex-wrap: wrap; }
/* Заголовок со стрелкой «назад» слева (как в чатах, но для обычных страниц).
   Обёртка нужна: сам .page__head раскидывает заголовок и кнопки по краям. */
.page__head-lead { display: flex; align-items: flex-start; gap: 12px; min-width: 0; }
/* min-width на внутреннем блоке обязателен: без него длинный подзаголовок
   раздувает flex-элемент до max-content и сбрасывает .page__actions на строку ниже. */
.page__head-lead > div { min-width: 0; }
.page-back { width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0; margin-top: 1px; }
.page-back svg { width: 18px; height: 18px; }

/* =========================================================================
   Cards
   ========================================================================= */
.card {
  position: relative;
  /* Рамка ВЕРНУЛАСЬ 03.09.2026. Я снял её, сделав край ступенью заливки, как в
     референсе, — заказчик посмотрел и сказал: «нет чёткого деления блоков».
     И он прав для этого продукта: в референсе на экране пять крупных карточек,
     а здесь их бывает пятнадцать, и ступень в 1,13:1 на таком количестве
     перестаёт читаться границей. Ступень заливки при этом ОСТАЁТСЯ — рамка
     теперь не подменяет её, а подпирает. */
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-2xl); box-shadow: var(--shadow-card);
  padding: 24px; display: flex; flex-direction: column; gap: 16px;
}
/* Верхний «глянец» и hairline-подсветка кромки убраны 03.09.2026: обе рисовали
   карточке край светлой линией — тем же приёмом, что и снятый бордер. После
   подъёма ступени полотно→карточка до 1,128:1 карточка отделяется заливкой,
   и ровный фон читается спокойнее градиента. */
/* display: flex у карточки перебивает UA-правило [hidden]{display:none} — та же
   ловушка, что в .pattern-form и .wizard. Без этого скрытая карточка (например,
   «Точки управляющего» в кадровой форме у не-управляющего) остаётся на экране
   во всю высоту, хотя JS её «спрятал». */
.card[hidden] { display: none; }
.card__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.card__title { font-size: 18px; font-weight: 700; color: var(--color-heading); letter-spacing: -0.015em; }
.card__subtitle { margin: 4px 0 0; color: var(--color-muted); font-size: 14px; }
.card__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 16px; margin-top: auto; border-top: 1px solid var(--color-divider); font-size: 13px; }

/* =========================================================================
   Stage 2 auth / users
   ========================================================================= */
.messages { display: flex; flex-direction: column; gap: 10px; }
.form-card,
.card--form { max-width: 860px; }
.card--narrow { max-width: 640px; margin-inline: auto; width: 100%; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.form-field { display: flex; flex-direction: column; gap: 7px; }
.form-field--full { grid-column: 1 / -1; }
.form-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.auth-shell { min-height: calc(100vh * var(--vu)); display: grid; place-items: center; padding: 28px; position: relative; }
.auth-shell__toggle { position: absolute; top: 22px; right: 24px; }
.auth-card {
  width: min(100%, 460px); background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-xl);
  box-shadow: var(--shadow-pop); padding: 28px; display: flex; flex-direction: column; gap: 18px;
}
.auth-card__brand { display: inline-flex; align-items: center; gap: 11px; }
.auth-card__title { font-size: 26px; font-weight: 800; }
.auth-card__subtitle { color: var(--color-muted); }
/* Смена пароля: одноколоночная форма + верхняя строка с «Назад» и брендом. */
.auth-form { display: flex; flex-direction: column; gap: 16px; }
.auth-card__topline { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.auth-card__head { display: flex; flex-direction: column; gap: 6px; }
.auth-card__back {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--color-muted); font-weight: 600; font-size: 13px;
  padding: 6px 10px 6px 6px; margin: -6px 0; border-radius: var(--radius-md);
  transition: color .16s var(--ease), background .16s var(--ease);
}
.auth-card__back:hover { color: var(--color-text); background: var(--color-surface-soft); }
.auth-card__back svg { width: 17px; height: 17px; }
.auth-card__logout { padding-top: 4px; text-align: center; }
.auth-card__logout .link { background: transparent; border: 0; padding: 0; }
.password-rules {
  padding: 14px; border-radius: var(--radius-lg); border: 1px solid var(--color-border);
  background: var(--color-surface-soft);
}
.password-rules__title { font-size: 13px; font-weight: 800; margin-bottom: 8px; }
.password-rules ul { margin: 0; padding-left: 18px; color: var(--color-muted); font-size: 12.5px; }
.password-rules li + li { margin-top: 5px; }

.filters-bar {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px 14px; align-items: end; padding: 16px 18px;
}
/* The submit/reset cluster always drops to its own full-width row, right-aligned,
   so filter fields above stay on a clean auto-fit grid regardless of count. */
.filters-bar .form-actions { grid-column: 1 / -1; justify-content: flex-end; }
/* Кнопка, стоящая прямо в сетке рядом с полями (без обёртки .form-actions),
   должна быть высотой поля — иначе строка фильтров идёт «лесенкой». */
.filters-bar > .btn { min-height: var(--control-h); }
/* Галочка в строке фильтров: без выравнивания по высоте поля она «висит»
   на базовой линии сетки, а не рядом с селектами. */
.filters-bar__check {
  display: flex; align-items: center; gap: 8px; min-height: var(--control-h);
  color: var(--color-muted); font-size: 13px;
}
.filters-bar__check input { flex: none; }
.filters-bar__search { position: relative; }
.filters-bar__search svg { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--color-faint); }
.filters-bar__search .input { padding-left: 46px; border-radius: var(--radius-pill); }

.role-badge,
.status-badge {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  border-radius: var(--radius-pill); font-size: 12px; font-weight: 800; white-space: nowrap;
}
.role-badge--owner { background: linear-gradient(135deg, var(--color-primary-soft), var(--color-coral-soft)); color: var(--color-primary-dark); }
.role-badge--tech_admin { background: color-mix(in srgb, var(--color-primary-soft) 62%, var(--color-coral-soft)); color: color-mix(in srgb, var(--color-primary-dark) 68%, var(--color-coral)); }
.role-badge--manager { background: var(--color-beige-soft); color: var(--color-primary-dark); }
.role-badge--accountant { background: var(--color-success-soft); color: var(--color-success); }
/* Роли фазы 2: у каждой свой тон, чтобы в списке пользователей все девять
   ролей читались бейджами, а не голым текстом. Территориальный — бирюза
   (сеть по территориям), помощник УА — янтарь рядом с бежевым управляющего,
   претензионист — пурпур, объекты — фиолет. */
.role-badge--area_admin { background: var(--color-cyan-soft); color: var(--color-cyan); }
.role-badge--ops_assistant { background: var(--color-warning-soft); color: var(--color-warning); }
.role-badge--claims_specialist { background: var(--color-magenta-soft); color: var(--color-magenta); }
.role-badge--facility_manager { background: var(--color-violet-soft); color: var(--color-violet); }
.role-badge--employee { background: var(--color-surface-soft); color: var(--color-muted); }
.status-badge--active { background: var(--color-success-soft); color: var(--color-success); }
.status-badge--locked { background: var(--color-warning-soft); color: var(--color-warning); }
.status-badge--inactive { background: var(--color-danger-soft); color: var(--color-danger); }

.grid--detail { grid-template-columns: minmax(0, 1.05fr) minmax(320px, .95fr); }
/* Равные половины (карта + отзывы на карточке ПВЗ): одинаковая ширина и высота. */
.grid--half { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; }
.grid--half .map-embed,
.grid--half .map-embed__iframe,
.grid--half .yandex-reviews-widget,
.grid--half .yandex-reviews-widget__iframe { height: 480px; min-height: 480px; }
.grid--half .yandex-map-link-card,
.grid--half .yandex-empty { min-height: 480px; }
@media (max-width: 880px) {
  .grid--half { grid-template-columns: 1fr; }
}
/* container-type — ради запросов ниже: ширину строки задаёт карточка (половина
   grid--detail), а не ширина окна, поэтому медиа-запрос тут промахивается. */
.detail-list { display: grid; gap: 0; margin: 0; container-type: inline-size; }
.detail-list > div { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: 14px; padding: 11px 0; border-bottom: 1px solid var(--color-border); }
.detail-list > div:last-child { border-bottom: 0; }
.detail-list dt { color: var(--color-muted); font-weight: 650; }
.detail-list dd { margin: 0; min-width: 0; }
/* Окно в половину экрана — не телефон, но карточке от этого не легче: в
   двухколоночной grid--detail «Профиль» ужимается до ~350px, на значение
   остаётся ~165px, и бейдж роли (white-space: nowrap — сжиматься не умеет)
   вылезал за рамку карточки поверх соседней колонки. Два рубежа: сначала
   худеет подпись, потом строка встаёт в один столбец, как на телефоне —
   значение важнее подписи, оно и получает всю ширину. Числа считаны от самой
   длинной роли: «Помощник управляющего администратора» — 281px, значит строке
   нужно 281 + подпись + зазор.
   ВАЖНО: @container меряет МАСШТАБИРОВАННЫЕ px (внутри html { zoom: .8 }), в
   отличие от media-запросов, которым брейкпоинты пересчитывали в реальные (см.
   --ui-scale). Пороги здесь — в тех же единицах, что и вся остальная вёрстка,
   пересчитывать их не надо. */
@container (max-width: 520px) {
  .detail-list > div { grid-template-columns: 132px minmax(0, 1fr); gap: 10px; }
}
@container (max-width: 430px) {
  .detail-list > div { grid-template-columns: minmax(0, 1fr); gap: 4px; }
}
/* Последний рубеж — карточка уже уже самого бейджа (мини-карточки, узкие
   колонки отчётов). Тут бейджу разрешено переноситься: вертикали в карточке
   не жалко, а обрезка многоточием прячет как раз то, ради чего в карточку и
   смотрят — какая это роль и какой статус. */
.detail-list dd .role-badge,
.detail-list dd .status-badge,
.detail-list dd .badge { max-width: 100%; white-space: normal; }
/* Пояснение под значением — отдельной строкой, как в таблицах: инлайновым оно
   слипалось с самим значением («Все пункты — ПсковПодмена: выход в любую…»). */
.detail-list dd > .cell-sub { display: block; margin-top: 2px; }
.danger-zone { margin-top: 12px; padding: 14px; border: 1px solid color-mix(in srgb, var(--color-danger) 28%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-danger-soft); }
.danger-zone__title { font-size: 14px; font-weight: 800; margin-bottom: 12px; color: var(--color-danger); }
.action-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.copy-secret-box { display: flex; flex-direction: column; gap: 10px; padding: 14px; border-radius: var(--radius-lg); background: var(--color-surface-soft); border: 1px solid var(--color-border); }
.copy-secret-box__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.copy-secret-box__label { display: block; color: var(--color-muted); font-size: 12px; font-weight: 700; margin-bottom: 3px; }
.copy-secret-box__value { font-weight: 800; font-size: 16px; }
.copy-secret-box__value--mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.success-mark { width: 54px; height: 54px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: var(--color-success); background: var(--color-success-soft); }
.success-mark svg { width: 30px; height: 30px; }

.empty-state { display: grid; place-items: center; text-align: center; gap: 11px; padding: 44px 20px; }
.empty-state--sm { padding: 24px 12px; }
.empty-state__icon { width: 58px; height: 58px; display: inline-flex; align-items: center; justify-content: center; border-radius: 18px; background: var(--color-primary-soft); color: var(--color-primary-dark); }
.empty-state__icon svg { width: 31px; height: 31px; }
.empty-state__title { font-size: 17px; font-weight: 800; }
.empty-state__text { color: var(--color-muted); max-width: 360px; }

.timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.timeline__item { display: flex; gap: 11px; align-items: flex-start; }
.timeline__dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 5px; flex: 0 0 auto; background: var(--color-primary); box-shadow: 0 0 0 4px var(--color-primary-soft); }
.timeline__body { min-width: 0; }
.timeline__action { font-weight: 800; }
.timeline__meta { color: var(--color-muted); font-size: 12px; margin-top: 2px; }
.pager { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Левая половина подвала таблицы: счётчик + выбор размера страницы. */
.foot-left { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.page-size { display: flex; align-items: center; gap: 8px; }
.page-size__label { color: var(--color-muted); font-size: 13px; white-space: nowrap; }
/* select.input задаёт width/padding-right — селектор с тем же весом, ниже по файлу. */
select.page-size__select { width: auto; min-width: 74px; padding: 6px 30px 6px 11px; font-size: 13px; }

/* ---- Metric cards ------------------------------------------------------- */
.metrics,
.metric-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--gap-cards); }
/* Семь колонок держат общий ритм там, где плиток много (девять у претензий).
   Но у отчёта их две-три, и та же сетка резала карточку до 200px: «2 451 000,00 ₽»
   в неё не влезало и обрезалось краем — пробелы в сумме неразрывные, перенести
   число нельзя. Здесь колонка подстраивается под содержимое: сколько плиток,
   столько и места. */
/* Все минимумы производных сеток подняты на +12px (03.09.2026): они были
   рассчитаны под поле карточки 16px, а стало 22px — то есть содержимого в
   плитке той же ширины на 12px меньше. Без этого денежное значение ужималось
   вдвое сильнее соседних счётчиков и ряд выглядел рваным. */
.metric-grid--fit { grid-template-columns: repeat(auto-fill, minmax(252px, 1fr)); }
.metric-card {
  position: relative; overflow: hidden;
  /* Заливка плитки идёт через СВОЙ токен, а не через --color-surface напрямую:
     «ведущая» плитка ниже должна быть светлее соседей, и красить её сменой
     --color-surface значило бы задеть 278 потребителей этого токена. */
  --tile-bg: var(--color-surface);
  background: var(--tile-bg); border: 1px solid var(--color-border);
  border-radius: var(--radius-2xl); box-shadow: var(--shadow-card);
  /* Низ меньше верха: значение прижато к низу (margin-top: auto ниже), и при
     равных полях цифра визуально проваливалась. */
  padding: 22px 22px 20px; display: flex; flex-direction: column; gap: 10px;
  transition: transform .2s var(--ease), box-shadow .2s var(--ease), background-color .2s var(--ease);
  /* Карточка — контейнер для запросов ниже: её ширину задаёт сетка раздела и
     ширина сайдбара, а не вьюпорт, поэтому медиазапрос тут не помогает. */
  container-type: inline-size;
}
/* Ховер больше не может подсвечивать рамку — её нет. Отвечает заливка:
   ступень вверх от --tile-bg, тот же приём, что и у «ведущей» плитки. */
.metric-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-soft); background: color-mix(in srgb, var(--color-text) 5%, var(--tile-bg)); }
/* Цветная ШАПКА плитки-фильтра — из референса: подпись стоит НЕ под полосой, а
   внутри неё. Сначала это была полоса в 6px, но у заказчика в макете цветное
   поле само несёт заголовок («New orders», «Delivered orders»), и без подписи
   внутри полоса читается как украшение, а не как заголовок группы.
   Шапка бледнее ленты и делается не отдельным элементом, а самой подписью:
   она уже стоит первой в карточке, ей достаточно отрицательных полей, чтобы
   разойтись до кромок. Разметку шаблонов это не трогает.

   Тон приезжает от МОДИФИКАТОРА ЗНАЧЕНИЯ через :has() — плитки без модификатора
   остаются синими. Краска подписи тёмная в ОБЕИХ темах: на насыщенном тоне она
   даёт 5,67-10,36 (тёмная тема), белая же дала бы 1,88-3,42 — ниже AA.
   В светлой теме сам тон слишком тёмен под чёрную подпись (3,65 у синего),
   поэтому там шапка разбавлена белым до 60%: контраст с подписью 7,1-13,7,
   а с белым полотном 1,6-2,7 — то есть шапка остаётся отчётливо цветной. */
.metric-card--link {
  --tile-band: color-mix(in srgb, var(--tile-tone, var(--color-primary)) 60%, #FFFFFF);
  --tile-band-ink: #0D0D0E;
}
html[data-theme="dark"] .metric-card--link {
  --tile-band: var(--tile-tone, var(--color-primary));
}
.metric-card--link .metric-card__label {
  /* Отрицательные поля = внутренним полям карточки (22px), низ не трогаем. */
  margin: -22px -22px 0; padding: 11px 22px; min-height: 42px;
  display: flex; align-items: center;
  background: var(--tile-band); color: var(--tile-band-ink);
  font-size: 13px; font-weight: 700; line-height: 1.25;
  border-radius: var(--radius-2xl) var(--radius-2xl) 0 0;
  transition: background-color .2s var(--ease);
}
.metric-card--link:has(.metric-card__value--success) { --tile-tone: var(--color-success); }
.metric-card--link:has(.metric-card__value--warning) { --tile-tone: var(--chart-amber); }
.metric-card--link:has(.metric-card__value--danger)  { --tile-tone: var(--color-danger); }
/* Optional full-card click target (e.g. dashboard "Активные ПВЗ" → /pvz/). */
.metric-card__overlay { position: absolute; inset: 0; border-radius: inherit; z-index: 1; }
/* Круглая кнопка «↗» в углу — из референса, БЕЗ правки шаблонов: рисуется двумя
   псевдоэлементами уже существующей ссылки-подложки (.metric-card__overlay —
   пустой <a> во всю карточку, у него оба псевдоэлемента свободны). ::before —
   кружок, ::after — сама стрелка маской, чтобы её цвет шёл от токена и менялся
   вместе с темой; SVG внутри url() цвет не наследует. data: в масках разрешён
   политикой CSP (img-src 'self' data: blob:).

   На телефоне кнопка тоже есть, но НЕ у всех: mobile.css уводит .metric-card__icon
   в absolute top:10px/right:10px — то есть ровно под кнопку. Поэтому там она
   прячется у плиток С ИКОНКОЙ (это плитки сводной) и остаётся у плиток-фильтров,
   где иконки нет вовсе. На десктопе иконка стоит в потоке и не мешает.

   Кнопка появляется лишь у плиток с подложкой-ссылкой; там, где ссылки нет,
   угол остаётся пустым — это и есть разница «плитка ведёт куда-то» / «просто
   число», которую референс показывает тем же способом. */
.metric-card__overlay::before,
.metric-card__overlay::after {
  content: ""; position: absolute; top: 16px; right: 16px;
  width: 40px; height: 40px; pointer-events: none;
  transition: background-color .18s var(--ease), opacity .18s var(--ease);
}
.metric-card__overlay::before {
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
}
.metric-card__overlay::after {
  background: var(--color-text); opacity: .78;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M7%2017%2017%207M8.5%207H17v8.5'/%3E%3C/svg%3E") center / 17px 17px no-repeat;
  mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M7%2017%2017%207M8.5%207H17v8.5'/%3E%3C/svg%3E") center / 17px 17px no-repeat;
}
.metric-card:hover .metric-card__overlay::before {
  background: color-mix(in srgb, var(--color-text) 14%, transparent);
}
.metric-card:hover .metric-card__overlay::after { opacity: 1; }
/* У «ведущей» плитки кнопка сплошная синяя — тот же приём, что в референсе. */
.metric-card--link.is-active .metric-card__overlay::before { background: var(--color-primary); }
.metric-card--link.is-active .metric-card__overlay::after { background: var(--color-on-primary); opacity: 1; }
/* Подпись не должна заезжать под кнопку: 40px кружка + 16px отступ минус
   22px поля карточки = 34px, берём 40 с запасом на длинные заголовки. */
.metric-card:has(> .metric-card__overlay) .metric-card__label { padding-right: 40px; }

/* На плитке-фильтре кнопка живёт ВНУТРИ цветной шапки, а не над ней: шапка
   занимает весь верх карточки, и кружок на общем месте лёг бы поверх подписи.
   Здесь он меньше (28px) и красится краской шапки, а не текста страницы. */
.metric-card--link .metric-card__overlay::before,
.metric-card--link .metric-card__overlay::after {
  top: 7px; right: 12px; width: 28px; height: 28px;
}
.metric-card--link .metric-card__overlay::before {
  background: color-mix(in srgb, var(--tile-band-ink) 13%, transparent);
}
.metric-card--link .metric-card__overlay::after {
  background: var(--tile-band-ink); opacity: .72;
  -webkit-mask-size: 14px 14px; mask-size: 14px 14px;
}
.metric-card--link:hover .metric-card__overlay::before {
  background: color-mix(in srgb, var(--tile-band-ink) 22%, transparent);
}
.metric-card--link.is-active .metric-card__overlay::before { background: var(--tile-band-ink); }
.metric-card--link.is-active .metric-card__overlay::after { background: var(--tile-band); opacity: 1; }
.metric-card--link .metric-card__label { padding-right: 46px; }

/* На плотных сетках (девять плиток в ряд у претензий) карточка сужается до
   ~105px содержимого. Кнопку НЕ убираем — заказчик просил, чтобы она
   показывала кликабельность, — но ужимаем до 24px, иначе она съедает место
   у подписи в шапке. */
@container (max-width: 150px) {
  .metric-card--link .metric-card__overlay::before,
  .metric-card--link .metric-card__overlay::after {
    top: 9px; right: 9px; width: 24px; height: 24px;
  }
  .metric-card--link .metric-card__overlay::after {
    -webkit-mask-size: 12px 12px; mask-size: 12px 12px;
  }
  .metric-card--link .metric-card__label { padding-right: 38px; font-size: 12px; }
}
/* Плитка-фильтр (справочник ПВЗ): курсор говорит, что число кликабельно, а
   is-active показывает, какой отбор сейчас показан в таблице. */
.metric-card--link { cursor: pointer; }
.metric-card--link:focus-within { box-shadow: var(--shadow-soft), inset 0 0 0 2px var(--color-primary); }
.metric-card--link .metric-card__overlay:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; border-radius: inherit; }
/* «Ведущая» плитка из референса: СВЕТЛЕЕ соседей, а не темнее. Раньше активный
   отбор красился в --color-primary-soft (полупрозрачный синий), то есть был
   темнее ряда — на почти чёрном полотне это читалось как «выключена».
   Подпись внутри поднята с --color-faint: на заливке #24252A общий faint даёт
   4,33:1, ниже AA; #989AA1 возвращает 5,44:1. */
.metric-card--link.is-active { --tile-bg: #F7F8FA; box-shadow: var(--shadow-soft); }
html[data-theme="light"] .metric-card--link.is-active { --tile-bg: #FFFFFF; }
html[data-theme="dark"] .metric-card--link.is-active { --tile-bg: #24252A; }
html[data-theme="dark"] .metric-card--link.is-active .metric-card__delta { color: #989AA1; }
/* Пастельной подложки нет (09.2026) — тот же разбор, что у карточек отчётов и
   строк колокольчика: приём «сквиркл + тонкий глиф» повторён в системе 22 раза
   по одному рецепту, а шесть *-soft токенов лежат в коридоре светимости
   0,878-0,885 (контраст к белой карточке 1,12:1), то есть плитка ничего не
   группировала. Знак залит и держится сам.
   ЭЛЕМЕНТ при этом НЕ удалён и удалять его нельзя: на телефоне вся раскладка
   плитки переключается через :has(> .metric-card__icon) (mobile.css) — там и
   поле справа, и отключение кнопки «↗». Снимаем только заливку. */
.metric-card__icon { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; }
.metric-card__icon svg { width: 28px; height: 28px; }
.metric-card__icon--primary { color: var(--color-primary); }
.metric-card__icon--success { color: var(--color-success); }
.metric-card__icon--warning { color: var(--color-warning); }
.metric-card__icon--coral   { color: var(--color-coral); }
.metric-card__body { display: flex; flex-direction: column; gap: 3px; }
.metric-card__label { font-size: 14px; color: var(--color-muted); font-weight: 550; line-height: 1.3; }
/* Размер цифры — ПЕРЕМЕННАЯ, а фактический font-size считается один раз здесь.
   Так было не всегда: контекстные правила (.dsec, .metric-grid--money,
   mobile.css) задавали свой font-size и по специфичности (0,2,0 против 0,1,0 у
   правила внутри @container — контейнерный запрос специфичности не добавляет)
   молча выключали ужатие ниже. На сводной цифра всегда оставалась 34px, и
   «01.08.2026» в плитке «Последнее изменение» резалось краем карточки
   (.metric-card { overflow: hidden }) — заказчик, 01.09.2026.

   Через min() потолок перебить нельзя: контекст задаёт только ЖЕЛАЕМЫЙ размер,
   фактический не больше того, что помещается в карточку. 100cqi — ширина
   КОНТЕНТА карточки (container-type объявлен на .metric-card), вычитать padding
   не надо. Единицы масштабируются вместе с текстом, поэтому при «Крупном»
   размере шрифта цифра ужимается сама.

   0,62em на знак — замер Onest 800 + tabular-nums в живой странице (03.09.2026):
   дата «01.08.2026» даёт 0,5865, деньги 0,5494, «1 234 567» 0,5675; запас ~6%.
   Было 0,58 под Golos Text. Внимание, неочевидное: у Golos фичи tnum НЕТ, и
   font-variant-numeric на body всё это время не работал — цифры шли узкими
   пропорциональными. У Onest tnum есть и включилась, все цифры стали 0,672em,
   то есть числа СТАЛИ ШИРЕ на 5-17%, хотя сам шрифт уже. Первый замер (0,4985)
   был сделан без учёта этого и занижал множитель. Исключение — короткий процент
   «83%» (0,734 на знак: «%» шире цифры), но три знака упираются в потолок
   только на карточке уже 65px, а туда раньше приходят ступени @container ниже.
   Нижний ограничитель поднят 15 → 17px: при базе 40px разрыв между желаемым и
   фактическим на узкой плитке вдвое больше, и соседние карточки в одном ряду
   начинали скакать по кеглю. Длина приезжает из шаблона
   (--metric-value-len); max(1, …) обязателен — при пустой переменной деление на
   ноль сделало бы всё объявление невалидным. */
.metric-card__value {
  --metric-value-size: 36px;
  font-size: min(
    var(--metric-value-size),
    max(17px, calc(100cqi / (max(1, var(--metric-value-len, 6)) * 0.62)))
  );
  font-weight: 800; letter-spacing: -0.03em; line-height: 1.05;
  font-variant-numeric: tabular-nums;
  /* Страховка, а не лечение. Длину значения теперь проставляет initMetricValueLen
     в app.js — он обходит все плитки и кладёт --metric-value-len на каждую (её
     отдавал ровно ОДИН шаблон из 35, и денежные значения вылезали за карточку).
     Многоточие остаётся на случай, когда JS не отработал: значение оборвётся
     честно, а не по-живому посреди знака. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
/* Цифра ужимается вместе с карточкой: в плотных сетках (девять карточек в ряд
   у претензий) «724 тыс. ₽» в 27px не помещалось и обрезалось краем блока.
   Шаги — по ширине контента карточки, поэтому в одном ряду размер одинаковый. */
@container (max-width: 138px) { .metric-card__value { --metric-value-size: 28px; } }
@container (max-width: 100px) { .metric-card__value { --metric-value-size: 24px; } }
@container (max-width: 80px) { .metric-card__value { --metric-value-size: 20px; } }
/* Денежный итог длиннее счётчика: «2 451 000,00 ₽» просит ~175px даже там, где
   «330» обходится третью этого. Отдельные шаги для плиток отчёта — иначе на
   узком окне сумма снова упёрлась бы в край. */
@container (max-width: 203px) { .metric-grid--fit .metric-card__value { --metric-value-size: 28px; } }
@container (max-width: 163px) { .metric-grid--fit .metric-card__value { --metric-value-size: 24px; } }
@container (max-width: 128px) { .metric-grid--fit .metric-card__value { --metric-value-size: 20px; } }
/* Значение прижато к низу карточки: подпись в 1 или 2 строки больше не
   сдвигает цифры по вертикали относительно соседних карточек. */
.metric-card > .metric-card__value { margin-top: auto; }
/* Дельта — пилюля, как в референсе. Заливки берём готовые --color-*-soft: в
   тёмной теме это rgba(...,0.14-0.16) — ровно референсные «~14% тона», а текст
   полного тона даёт на них 7:1. Пилюля вводится ЗДЕСЬ, на базовом правиле; на
   сводной панели (.dsec) она ниже намеренно гасится — там решение заказчика от
   10.08.2026: цвет достаётся только вниманию и проблеме. */
.metric-card__delta {
  display: inline-flex; align-self: flex-start;
  padding: 4px 10px; border-radius: var(--radius-pill);
  font-size: 13px; font-weight: 700; line-height: 1.4;
  color: var(--color-muted); font-variant-numeric: tabular-nums;
}
.metric-card__delta--primary { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.metric-card__delta--success { background: var(--color-success-soft); color: var(--color-success); }
.metric-card__delta--warning { background: var(--color-warning-soft); color: var(--color-warning); }
.metric-card__delta--coral   { background: var(--color-coral-soft);   color: var(--color-coral); }

/* =========================================================================
   Layout grids
   ========================================================================= */
.grid { display: grid; gap: 18px; }
.grid--charts { grid-template-columns: 1.55fr 1fr 1fr; }
.grid--bottom { grid-template-columns: 1.7fr 1fr 1fr; }

/* ---- Chart card / line chart ------------------------------------------- */
.chart-card { min-height: 320px; }
.chart-legend { display: flex; gap: 18px; flex-wrap: wrap; font-size: 12.5px; color: var(--color-muted); }
.chart-legend__item { display: inline-flex; align-items: center; gap: 7px; }
.dot { width: 12px; height: 4px; border-radius: 3px; display: inline-block; }
.dot--primary { background: var(--color-primary); }
.dot--coral { background: var(--color-coral); }
.dot--dashed { background: repeating-linear-gradient(90deg, var(--color-beige) 0 4px, transparent 4px 7px); }

.linechart,
.line-chart { display: flex; flex-direction: column; gap: 8px; }
.linechart svg,
.line-chart svg { width: 100%; height: 210px; overflow: visible; }
.linechart__grid { stroke: var(--color-border); stroke-width: 1; opacity: .55; }
.linechart__plan { stroke: var(--color-beige); stroke-width: 2; stroke-dasharray: 6 6; }
.linechart__line { fill: none; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.linechart__line--primary { stroke: var(--color-primary); }
.linechart__line--coral { stroke: var(--color-coral); }
.linechart__axis { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--color-muted); }

/* ---- Donut -------------------------------------------------------------- */
.donut-block { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.donut,
.donut-chart,
.kpi-ring { position: relative; width: 168px; height: 168px; flex: 0 0 auto; }
.donut--sm { width: 150px; height: 150px; }
.donut svg,
.donut-chart svg,
.kpi-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.donut__track { fill: none; stroke: var(--color-surface-soft); stroke-width: 20; }
.donut__seg { fill: none; stroke-width: 20; stroke-linecap: round; }
.donut__center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
.donut__center strong { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; }
.donut__center span { font-size: 12px; color: var(--color-muted); }

.donut-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; flex: 1; min-width: 130px; }
.donut-legend__item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.donut-legend .dot { width: 11px; height: 11px; border-radius: 50%; }
.donut-legend__value { font-weight: 800; }
.donut-legend__pct { color: var(--color-muted); font-size: 12px; }
.donut-legend__label { color: var(--color-muted); margin-left: auto; }

/* ---- KPI bars ----------------------------------------------------------- */
.kpi-block { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
.kpi-bars { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 13px; flex: 1; min-width: 180px; }
.kpi-bars__row { display: grid; grid-template-columns: 1fr 90px auto; align-items: center; gap: 10px; font-size: 13px; }
.kpi-bars__label { color: var(--color-muted); }
.kpi-bars__track { height: 8px; border-radius: var(--radius-pill); background: var(--color-surface-soft); overflow: hidden; }
.kpi-bars__fill { display: block; width: var(--w, 0); height: 100%; border-radius: inherit; }
.kpi-bars__fill--primary { background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark)); }
.kpi-bars__fill--coral { background: linear-gradient(90deg, var(--color-coral), color-mix(in srgb, var(--color-coral) 60%, #fff)); }
.kpi-bars__value { font-weight: 800; min-width: 38px; text-align: right; }

/* ---- Chips -------------------------------------------------------------- */
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
/* Прозрачная рамка обязательна: без неё <button class="chip"> получает
   браузерную border: outset — на тёмной теме это белая объёмная кайма.
   Тот же приём, что у .btn: место под рамку занято всегда, состояния
   только красят её и не сдвигают соседей. */
/* Чип и кнопка обе стали таблетками — силуэты разводим высотой: чип 31px против
   кнопки 37,5px. line-height: 1 задан явно (наследовалось 1.5 от body). */
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border: 1px solid transparent; border-radius: var(--radius-pill); background: var(--color-surface-soft); color: var(--color-muted); font-size: 13px; font-weight: 650; line-height: 1; transition: all .18s var(--ease); }
.chip b { color: var(--color-text); }
/* Выбранный чип — графитовый, а не синий: сплошная заливка фирменным цветом на
   мелком элементе выглядит дешёвой меткой, а не выбором. Цвет остаётся у
   действия (кнопка), состояние держится тоном и весом. */
.chip.is-active { background: var(--color-heading); color: var(--color-surface); }
.chip.is-active b { color: var(--color-surface); }

/* ---- Живой список ------------------------------------------------------
   Карточка, содержимое которой htmx подменяет вместо перезагрузки страницы.
   Приглушение включается с задержкой: быстрый ответ (а он почти всегда
   быстрый) не должен моргать полупрозрачностью, медленный — обязан показать,
   что запрос идёт. */
.list-live { transition: opacity .12s var(--ease); }
.list-live.htmx-request { opacity: .5; transition: opacity .2s var(--ease) .25s; }
.list-live.htmx-request .table-wrap { pointer-events: none; }
/* Живой список, у которого вместе с таблицей обновляются плитки над ней
   (начисления, KPI, претензии): подменяется вся стопка целиком, иначе числа
   в плитках остались бы от прошлого фильтра. Обёртка повторяет промежуток
   колонки .page — без неё плитки и карточка слиплись бы. */
.list-live--stack { display: flex; flex-direction: column; gap: 20px; }
/* То же состояние у карточки со списком: сортировка по клику на заголовок
   меняет только её содержимое, страница не перезагружается (initLiveSortLinks).
   Приглушение с той же задержкой — быстрый ответ не моргает. */
.card.htmx-request,
.table-wrap.htmx-request { opacity: .5; transition: opacity .2s var(--ease) .25s; }
.card.htmx-request .table-wrap { pointer-events: none; }

/* ---- Table -------------------------------------------------------------- */
/* Горизонтальная прокрутка теперь ВИДНА. На реестре ПВЗ таблица 1169px в окне
   695px — сорок процентов колонок за краем, и об этом ничто не сообщало:
   выглядело как «данные обрезали», а не «есть ещё, прокрути».
   Приём известный: два «кроющих» слоя цвета карточки прикреплены к содержимому
   (background-attachment: local) и едут вместе с ним, гася тени под собой,
   пока таблица прижата к своему краю. Порядок слоёв важен — фоны рисуются
   сверху вниз по списку, поэтому кроющие обязаны идти ПЕРВЫМИ. */
.table-wrap {
  overflow-x: auto; border-radius: var(--radius-lg);
  background-image:
    linear-gradient(to right, var(--color-surface), transparent),
    linear-gradient(to left, var(--color-surface), transparent),
    linear-gradient(to right, rgba(0, 0, 0, 0.30), transparent),
    linear-gradient(to left, rgba(0, 0, 0, 0.30), transparent);
  background-position: left center, right center, left center, right center;
  background-size: 26px 100%, 26px 100%, 13px 100%, 13px 100%;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll;
}
html[data-theme="light"] .table-wrap {
  background-image:
    linear-gradient(to right, var(--color-surface), transparent),
    linear-gradient(to left, var(--color-surface), transparent),
    linear-gradient(to right, rgba(12, 14, 18, 0.16), transparent),
    linear-gradient(to left, rgba(12, 14, 18, 0.16), transparent);
}
.table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.table thead th {
  /* Микро-шапка референса: заливки НЕТ, работает только кегль и разрядка.
     Разрядка снижена .08 → .06em: у Onest прописные шире, чем у Golos, и на
     прежней «СТАВКА ЗА СМЕНУ» уже не помещалась в колонку. */
  /* 11.5px, а не 12.5. На 12.5 прописная кириллица (она шире латиницы примерно
     на 12%) рвала подписи на две строки там, где столбцов семь и они узкие:
     «ОСНОВНОЙ / ПВЗ», «РОЛЬ В / СИСТЕМЕ», «ТИП / ЗАНЯТОСТИ». Разрядка снижена
     по той же причине. */
  text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-faint); font-weight: 700; padding: 12px 15px;
  background: transparent; border-bottom: 1px solid var(--color-divider);
  white-space: nowrap;
}
/* Скругления крайних ячеек шапки удалены вместе с её заливкой: скруглять нечего.
   Липкой шапке заливка, наоборот, ОБЯЗАТЕЛЬНА — без неё строки при прокрутке
   просвечивают сквозь неё. Дефект не виден на коротком списке, поэтому правило
   стоит здесь, рядом со снятием фона, а не в разделе .table--sticky ниже. */
.table--sticky thead th { background: var(--color-surface); }
/* Строка выросла с 41 до 64px (54,6 экранных при zoom .85) — нижняя граница
   референсного коридора 56-64. Разделитель уведён с --color-border на
   --color-divider: полный бордер давал 23% контраста в светлой теме против
   референсных 8-10%. */
/* 18px, а не 22. На 22 однострочная строка выходила 64 CSS ровно по цели, но
   ячейка адреса переносится на ТРИ строки, и реальная строка реестра ПВЗ была
   106 CSS — таблица стала разреженной, на экран влезало шесть записей, и ряды
   вышли разной высоты. Теперь: однострочная 56 CSS, с подписью 66, с адресом в
   две строки (обрезка ниже) 76. */
.table tbody td { padding: 18px 15px; border-top: 1px solid var(--color-divider); vertical-align: middle; }
/* Двухстрочная ячейка и так выше — ей поля гасим, иначе строка уходит за 80px. */
.table tbody td:has(> .cell-sub) { padding-top: 13px; padding-bottom: 13px; }
/* Ячейка действий тоже не должна растить строку: .btn-sm внутри держит
   min-height 34px, и строка вырастала с 64 до 78px — на 21 реестре сразу, то
   есть минус две записи на каждом экране. 15 + 34 + 15 = ровно 64. */
.table tbody td:has(> .btn),
.table tbody td:has(> .row-actions),
.table tbody td:has(> form .btn) { padding-top: 11px; padding-bottom: 11px; }
/* Промежуток между кнопками в колонке действий. Раньше он делался четырьмя
   разными способами, а в восемнадцати шаблонах кнопки разделял просто пробел
   шрифта — 3,4px вместо 6, и кнопки почти слипались. */
.table tbody td:last-child.ta-right { white-space: nowrap; }
/* Кнопка «Открыть» в строке, которая И БЕЗ НЕЁ кликабельна (data-row-href его и
   ставит), — чистое дублирование: два десятка обведённых кнопок в колонке были
   главным визуальным шумом реестра, а вели туда же, куда клик по строке.
   Делаем их тихой ссылкой: разметку не трогаем, доступное имя и переход по Tab
   остаются на месте, а колонка перестаёт спорить с данными.
   Убрать колонку совсем — уже правка шаблонов (21 реестр), и она вернёт ~110px
   ширины; это отдельная задача. */
.table tbody tr[data-row-href] .row-actions .btn-secondary {
  background: transparent; border-color: transparent; box-shadow: none;
  color: var(--color-primary-dark); font-weight: 650;
}
.table tbody tr[data-row-href]:hover .row-actions .btn-secondary {
  background: var(--color-primary-soft); border-color: transparent;
  color: var(--color-primary-dark); transform: none;
}
.table tbody td:last-child.ta-right > .btn + .btn,
.table tbody td:last-child.ta-right > .btn + form,
.table tbody td:last-child.ta-right > form + .btn,
.table tbody td:last-child.ta-right > form + form { margin-left: 6px; }
/* Первая колонка — значение весом, подпись под ним тише: ровно как в референсе.
   Раньше значение шло весом 400 и почти не отличалось от собственной подписи. */
.table tbody td:first-child { font-weight: 650; }
/* Подпись в ячейке («дата + время», «код ПВЗ + название») — второй строкой
   под значением, а не вплотную к нему; вне таблиц .cell-sub остаётся инлайновым. */
.table tbody td > .cell-sub { display: block; margin-top: 1px; }
.table tbody tr:first-child td { border-top: 0; }
.table tbody tr { transition: background-color .14s var(--ease); }
/* Ховер строки — ОДНО правило на весь продукт, и висит оно на ЯЧЕЙКЕ, а не на
   <tr>: пять отдельных правил ниже по файлу красили td непрозрачным
   --color-surface-soft и перебивали его. Итог был перевёрнут — строка, которую
   МОЖНО открыть (скрипт вешает на неё .row-link), отвечала нейтральным серым, а
   некликабельный справочник фирменным синим.
   Цвет обязан быть ПРОЗРАЧНЫМ: непрозрачная заливка стирала смысловые подсветки
   строк (.claim-row--danger, --warning) — дорогая неразобранная претензия
   переставала быть красной ровно в тот момент, когда на неё наводят.
   9% → 10%: без подпорки в виде второго слоя нужен чуть более уверенный шаг. */
.table tbody tr:hover > td { background-color: color-mix(in srgb, var(--color-primary) 10%, transparent); }
/* Sticky header for long tables: add .table--sticky and keep the table inside a scroll box. */
.table--sticky thead th { position: sticky; top: 0; z-index: 2; }
.ta-right { text-align: right; }
/* .table thead th задаёт left с большей специфичностью — числовым колонкам
   заголовок выравниваем отдельно, чтобы «Сумма» стояла над своими значениями. */
.table thead th.ta-right { text-align: right; }
/* Сортировка по клику на заголовок ({% sort_th %}). Заголовок остаётся
   заголовком: ссылка наследует размер и регистр, добавляется только стрелка. */
.table thead th.th-sort { padding: 0; }
/* Один и тот же вид у двух механизмов: серверная сортировка рисует <a> с
   адресом, клиентская (initTableSort, любая таблица) — <button>. Поэтому
   сбрасываем и фон с рамкой: у кнопки они свои. */
.th-sort__link {
  display: inline-flex; align-items: center; gap: 5px; width: 100%;
  padding: 12px 15px; color: inherit; font: inherit; text-decoration: none;
  letter-spacing: inherit; text-transform: inherit; cursor: pointer;
  border: 0; background: transparent; text-align: inherit;
}
.table thead th.th-sort.ta-right .th-sort__link { justify-content: flex-end; }
.th-sort__link:hover { color: var(--color-text); background: color-mix(in srgb, var(--color-text) 5%, transparent); }
.th-sort__icon { width: 13px; height: 13px; flex: none; opacity: 0; transition: opacity .12s var(--ease); }
.th-sort__link:hover .th-sort__icon { opacity: .45; }
.table thead th.th-sort.is-sorted { color: var(--color-text); }
.table thead th.th-sort.is-sorted .th-sort__icon { opacity: 1; color: var(--color-primary); }
/* На тач-экране состояния hover нет — стрелка-подсказка видна сразу. */
@media (hover: none) {
  .th-sort__icon { opacity: .35; }
}
/* Длинный заголовок можно пустить в 2 строки — иначе он раздувает колонку
   и таблица уезжает в горизонтальный скролл. */
.table thead th.th-wrap { white-space: normal; min-width: 110px; }
/* Длинное текстовое значение в ячейке (адрес ПВЗ в отчётах): режем многоточием.
   Без этого один адрес растягивает строку на пять и уводит соседние колонки за
   край. Полный текст доступен в Excel-выгрузке. */
.table td.cell-clip {
  max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nb { white-space: nowrap; }
.cell-strong { font-weight: 700; }
.cell-sub { font-size: 12px; color: var(--color-faint); font-weight: 500; line-height: 1.35; }
.cell-muted { color: var(--color-muted); }
.cell-overdue { font-weight: 700; color: var(--color-coral); }
.cell-icon { display: inline-flex; vertical-align: middle; margin-right: 7px; }
.cell-icon svg { width: 16px; height: 16px; }
.cell-icon--warning { color: var(--color-warning); }
.cell-icon--danger { color: var(--color-danger); }
.cell-icon--info { color: var(--color-primary-dark); }

/* ---- Rank list ---------------------------------------------------------- */
.rank-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.rank-list__item { display: grid; grid-template-columns: 26px 1fr 84px auto; align-items: center; gap: 12px; }
.rank-list__num { width: 26px; height: 26px; border-radius: 8px; background: var(--color-surface-soft); color: var(--color-muted); font-weight: 800; font-size: 12px; display: inline-flex; align-items: center; justify-content: center; }
.rank-list__item--low .rank-list__num { background: var(--color-coral-soft); color: var(--color-coral); }
.rank-list__track { height: 7px; border-radius: var(--radius-pill); background: var(--color-surface-soft); overflow: hidden; }
.rank-list__fill { display: block; width: var(--w, 0); height: 100%; background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark)); border-radius: inherit; }
.rank-list__fill--coral { background: linear-gradient(90deg, var(--color-coral), color-mix(in srgb, var(--color-coral) 60%, #fff)); }
.rank-list__value { font-weight: 800; min-width: 40px; text-align: right; }
.rank-list__divider { height: 1px; background: repeating-linear-gradient(90deg, var(--color-border) 0 6px, transparent 6px 12px); margin: 2px 0; }

/* ---- Stat list ---------------------------------------------------------- */
.stat-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.stat-list__item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--color-border); font-size: 13.5px; }
.stat-list__item:last-child { border-bottom: 0; }
.stat-list__label { color: var(--color-muted); }
.stat-list__value { font-weight: 800; font-size: 16px; }
.stat-list__value--warning { color: var(--color-warning); }
.stat-list__value--coral { color: var(--color-coral); }
.stat-list__value--primary { color: var(--color-primary-dark); }
.stat-list__value--muted { color: var(--color-muted); }

/* =========================================================================
   Auth / login
   ========================================================================= */
/* =========================================================================
   Sign-in — «Litten Cosmos» premium scene
   A theme-aware sky around a glass card: slow blurred nebula clouds (CSS)
   + a living starfield of 4-point glints with twinkle, parallax and
   occasional shooting stars (one isolated <canvas>), plus a soft cursor
   halo. Motion halts under prefers-reduced-motion and when the tab hides.
   Palette exposed via scoped --sg-* tokens; --sg-star-alpha / --sg-neb-scale
   dim the particles and clouds per theme so the light sky reads as glints
   in a morning sky, not specks on a bright field.
   ========================================================================= */
.signin {
  position: relative;
  min-height: calc(100vh * var(--vu));
  min-height: calc(100dvh * var(--vu));
  display: grid;
  place-items: center;
  padding: 28px;
  overflow: hidden;
  isolation: isolate;
  /* Scene palette — canvas reads the hex vars via getComputedStyle. The whole
     scene is theme-aware: light = a soft dawn sky, dark = deep night cosmos. */
  --sg-blue: var(--color-primary);
  --sg-pink: var(--color-coral);
  /* Light-theme starfield: one narrow indigo family — mixed hues (violet/teal)
     on a bright field read as a stained microbe slide, so keep every glint blue. */
  --sg-star: #5563b5;
  --sg-star-blue: #3f6fd6;
  --sg-star-violet: #6f78d8;
  --sg-star-teal: #4a7fc9;
  --sg-star-alpha: .78;
  /* Dawn clouds: cool blue/lavender high up, warm peach/rose near the horizon. */
  --sg-neb-blue: #9ab4ff;
  --sg-neb-violet: #c9b4ff;
  --sg-neb-teal: #ffd9b8;
  --sg-neb-pink: #ffc2cf;
  --sg-neb-scale: .55;
  --sg-dot-color: #4E6BFF;
  /* Dawn sky — pale blue high up, warming to near-white at the horizon. */
  background:
    radial-gradient(120% 92% at 50% 4%, rgba(140, 172, 255, .5), transparent 58%),
    radial-gradient(150% 120% at 50% -16%, #d7e1ff 0%, #e8eefc 46%, #f4f7fd 100%),
    #f4f7fd;
}
html[data-theme="dark"] .signin {
  color-scheme: dark;
  /* Сцена входа живёт своей палитрой и НЕ следует за акцентом темы: --sg-blue
     здесь красит свечение карточки, и оранжевый посреди ночного неба выглядел
     бы пожаром. Отвязываем от --color-primary явно. */
  --sg-blue: #5B8CFF;
  --sg-pink: #FF6E9E;
  /* Night cosmos: bright, near-white stars with cool tints. */
  --sg-star: #eef2ff;
  --sg-star-blue: #9db4ff;
  --sg-star-violet: #c3a6ff;
  --sg-star-teal: #8ff0ec;
  --sg-star-alpha: 1;
  --sg-neb-scale: 1;
  --sg-neb-blue: #3b57ff;
  --sg-neb-violet: #7C5CFF;
  --sg-neb-teal: #17C3C0;
  --sg-neb-pink: #ff5c8a;
  --sg-dot-color: #86AAFF;
  /* Deep-space gradient, cool near the top where the nebulae glow. */
  background:
    radial-gradient(120% 90% at 50% 8%, rgba(23, 32, 63, .55), transparent 60%),
    radial-gradient(140% 120% at 50% -20%, #17203f 0%, #0a0f24 44%, #05070f 78%, #03040a 100%),
    #03040a;
}

/* ---- Background layers --------------------------------------------------- */
.signin__bg { position: absolute; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }

/* Nebula clouds — four blurred colour fields drifting slowly out of phase. */
.signin__aurora {
  position: absolute;
  width: calc(46vw * var(--vu)); height: calc(46vw * var(--vu));
  min-width: 380px; min-height: 380px;
  border-radius: 50%;
  filter: blur(96px);
  opacity: calc(.4 * var(--sg-neb-scale, 1));
  will-change: transform;
}
.signin__aurora--1 {
  top: -18%; left: -12%;
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--sg-neb-blue) 70%, transparent), transparent 66%);
  animation: sg-drift-1 24s ease-in-out infinite alternate;
}
.signin__aurora--2 {
  bottom: -22%; right: -14%;
  opacity: calc(.34 * var(--sg-neb-scale, 1));
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--sg-neb-pink) 60%, transparent), transparent 66%);
  animation: sg-drift-2 30s ease-in-out infinite alternate;
}
.signin__aurora--3 {
  top: 24%; left: 46%;
  width: calc(34vw * var(--vu)); height: calc(34vw * var(--vu)); min-width: 300px; min-height: 300px;
  opacity: calc(.32 * var(--sg-neb-scale, 1));
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--sg-neb-violet) 60%, transparent), transparent 68%);
  animation: sg-drift-3 34s ease-in-out infinite alternate;
}
.signin__aurora--4 {
  bottom: 4%; left: 0%;
  width: calc(30vw * var(--vu)); height: calc(30vw * var(--vu)); min-width: 260px; min-height: 260px;
  opacity: calc(.26 * var(--sg-neb-scale, 1));
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--sg-neb-teal) 55%, transparent), transparent 68%);
  animation: sg-drift-4 28s ease-in-out infinite alternate;
}

/* Twinkling stars + shooting stars all live in one GPU-friendly canvas. */
.signin__canvas { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 1; }

/* Soft halo that follows the cursor (desktop only; positioned by JS). */
.signin__cursor {
  position: absolute; top: 0; left: 0;
  width: 320px; height: 320px; margin: -160px 0 0 -160px;
  border-radius: 50%;
  opacity: 0; transition: opacity .5s var(--ease);
  background: radial-gradient(circle, color-mix(in srgb, var(--sg-dot-color) 14%, transparent), transparent 54%);
  will-change: transform;
}
.signin.is-pointer .signin__cursor { opacity: .6; }

.signin__toggle { position: absolute; top: 22px; right: 24px; z-index: 5; }

/* ---- Card ---------------------------------------------------------------- */
.signin__card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 508px;
  padding: 52px 52px 48px;
  border-radius: 30px;
  /* Поля входа — свои токены: на карточке они должны читаться утопленными,
     а не сливаться с её подложкой. */
  --signin-field: color-mix(in srgb, var(--color-surface-soft) 92%, transparent);
  --signin-field-edge: color-mix(in srgb, var(--color-border-strong) 78%, transparent);
  --signin-field-well: rgba(12, 14, 18, .06);
  --signin-rule: var(--color-border-strong);
  background: color-mix(in srgb, var(--color-surface) 84%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
  box-shadow:
    0 40px 90px -30px rgba(12, 14, 18, 0.42),
    0 14px 36px -20px rgba(12, 14, 18, 0.26),
    0 0 90px -34px color-mix(in srgb, var(--sg-blue) 50%, transparent),
    inset 0 1px 0 color-mix(in srgb, #fff 62%, transparent);
  -webkit-backdrop-filter: blur(26px) saturate(1.4);
  backdrop-filter: blur(26px) saturate(1.4);
  animation: signin-card-in .8s var(--ease-out) both;
}
/* Gradient hairline ring — the premium edge, drawn without an extra element. */
.signin__card::before {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--sg-blue) 62%, transparent),
    transparent 34%, transparent 66%,
    color-mix(in srgb, var(--sg-pink) 46%, transparent));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  opacity: .9;
  pointer-events: none;
}
html[data-theme="dark"] .signin__card {
  --signin-field: rgba(9, 12, 22, .55);
  --signin-field-edge: rgba(255, 255, 255, .11);
  --signin-field-well: rgba(0, 0, 0, .28);
  --signin-rule: rgba(255, 255, 255, .16);
  background: color-mix(in srgb, var(--color-surface) 78%, transparent);
  border-color: color-mix(in srgb, var(--color-border-strong) 62%, transparent);
  box-shadow:
    0 50px 100px -34px rgba(0, 0, 0, 0.74),
    0 18px 46px -24px rgba(0, 0, 0, 0.62),
    0 0 110px -34px color-mix(in srgb, var(--sg-blue) 62%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

.signin__brand { display: inline-flex; align-items: center; margin-bottom: 12px; }
.signin__logo { height: 66px; width: auto; max-width: 290px; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(12, 14, 18, .18)); }
/* Позиционирование продукта одной строкой: это не CRM, а операционная система
   сети ПВЗ. Стоит между логотипом и «Добро пожаловать», чтобы человек прочитал
   его раньше формы, но не спорил с ней за внимание. */
.signin__tagline { color: var(--color-muted); font-size: 12.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; margin-bottom: 24px; }
.signin__title { font-size: 31px; font-weight: 800; letter-spacing: -0.02em; }
.signin__subtitle { color: var(--color-muted); margin-top: 9px; font-size: 16px; }
.signin__form { display: flex; flex-direction: column; gap: 19px; margin-top: 32px; }
/* Taller, airier fields for the hero screen (global .input stays untouched).
   Это ежедневный ввод логина и пароля — контролы заметно крупнее общих. */
.signin__form .field__label { font-size: 14px; line-height: 22px; font-weight: 650; color: var(--color-text); }
.signin__form .input {
  padding: 15px 17px;
  font-size: 16px;
  border-radius: 14px;
  background: var(--signin-field);
  border-color: var(--signin-field-edge);
  box-shadow: inset 0 1px 2px var(--signin-field-well);
}
.signin__form .input:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--signin-field-edge)); }
.signin__form .input:focus {
  border-color: var(--color-primary);
  background: var(--signin-field);
  box-shadow:
    0 0 0 4px color-mix(in srgb, var(--color-primary) 17%, transparent),
    inset 0 1px 2px var(--signin-field-well);
}
.signin__form .password-field__input { padding-right: 52px; }
.signin__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 14.5px; }
.signin__row .checkbox { font-size: 14.5px; }

/* Leading icon inside login/password fields — a tangible upgrade to the daily
   entry window. Scoped to .signin__form so the shared .field/.input/.password-field
   components on other pages stay untouched. */
.field-affix { position: relative; display: block; }
.signin__form .field-affix__icon {
  position: absolute;
  left: 15px; top: 50%;
  width: 20px; height: 20px;
  transform: translateY(-50%);
  fill: none; stroke: var(--color-muted);
  stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
  pointer-events: none;
  transition: stroke .2s var(--ease);
  z-index: 1;
}
.signin__form .input--affixed { padding-left: 46px; }
.signin__form .field-affix:focus-within .field-affix__icon { stroke: var(--color-primary); }

.signin__submit { position: relative; margin-top: 8px; height: 54px; font-size: 16.5px; border-radius: 15px; box-shadow: var(--shadow-primary); overflow: hidden; }
.signin__submit { display: inline-flex; align-items: center; justify-content: center; gap: 9px; }
.signin__submit-arrow {
  width: 19px; height: 19px; flex: none;
  fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transform: translateX(-2px);
  opacity: .85;
  transition: transform .28s var(--ease-out), opacity .28s var(--ease-out);
}
.signin__submit:hover .signin__submit-arrow { transform: translateX(3px); opacity: 1; }
.signin__submit.is-loading .signin__submit-label,
.signin__submit.is-loading .signin__submit-arrow { visibility: hidden; }

/* Card entrance: the whole card rises + fades once, then its rows stagger in. */
.signin__card.signin--enter .signin__brand,
.signin__card.signin--enter .signin__tagline,
.signin__card.signin--enter .signin__title,
.signin__card.signin--enter .signin__subtitle,
.signin__card.signin--enter .signin__form > * { animation: sg-row-in .5s var(--ease-out) both; }
.signin__card.signin--enter .signin__tagline { animation-delay: .04s; }
.signin__card.signin--enter .signin__title { animation-delay: .08s; }
.signin__card.signin--enter .signin__subtitle { animation-delay: .12s; }
.signin__card.signin--enter .signin__form > *:nth-child(1) { animation-delay: .16s; }
.signin__card.signin--enter .signin__form > *:nth-child(2) { animation-delay: .22s; }
.signin__card.signin--enter .signin__form > *:nth-child(3) { animation-delay: .28s; }
.signin__card.signin--enter .signin__form > *:nth-child(4) { animation-delay: .34s; }
.signin__card.signin--enter .signin__form > *:nth-child(5) { animation-delay: .40s; }

/* Very slow, barely-there sheen crossing the primary button. Uses ::before so
   it never clashes with the shared .btn.is-loading::after spinner. */
.signin__submit::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 40%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .28), transparent);
  transform: skewX(-18deg);
  pointer-events: none;
  animation: sg-sheen 7s ease-in-out 2s infinite;
}
.signin__submit.is-loading::before { display: none; }

/* Caps Lock — самая частая причина «пароль не подходит». Предупреждаем до
   отправки формы, а не после отказа входа. */
.signin__caps {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 9px;
  font-size: 13px;
  font-weight: 650;
  color: var(--color-warning);
}
.signin__caps[hidden] { display: none; }
.signin__caps svg {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}

/* Кто выдаёт доступ — частый вопрос новых сотрудников, отвечаем прямо на входе. */
.signin__hint {
  position: relative;
  margin: 26px 0 0;
  padding-top: 22px;
  text-align: center;
  font-size: 14px;
  color: var(--color-muted);
}
.signin__hint::before {
  content: "";
  position: absolute;
  left: 50%; top: 0;
  transform: translateX(-50%);
  width: 78%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--signin-rule), transparent);
}
.signin__card.signin--enter .signin__hint { animation: sg-row-in .5s var(--ease-out) .46s both; }
.signin__alert { margin-top: 22px; }

/* ---- Two-zone layout ------------------------------------------------------
   Слева — форма-герой (принятая карточка без изменений), справа — «созвездие
   сети»: точки ПВЗ как звёзды на том же небе. На узких экранах правая зона
   исчезает и остаётся прежний центрированный вход. */
.signin__layout {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 1210px;
  display: grid;
  grid-template-columns: minmax(0, 508px) minmax(0, 548px);
  justify-content: center;
  align-items: center;
  column-gap: clamp(48px, calc(7vw * var(--vu)), 116px);
}

/* Витрина выключена (LOGIN_SHOWCASE_ENABLED=False) — одна колонка по центру.
   Без этого правила вторая колонка сетки остаётся пустой и форма съезжает
   влево, а на широком экране это выглядит как поломка вёрстки. */
.signin__layout--solo { grid-template-columns: minmax(0, 508px); column-gap: 0; }

.signin__aside { position: relative; z-index: 1; min-width: 0; --stage-h: 348px; --main-w: 318px; --side-w: 244px; }
.signin__aside > * { animation: sg-row-in .55s var(--ease-out) both; }
.signin__aside .signin__stage { animation-delay: .3s; }
.signin__aside .signin__slides { animation-delay: .45s; }
.signin__aside .signin__dots { animation-delay: .6s; }

/* ---- Витрина главы: два живых фрагмента интерфейса -------------------------
   Абстрактных схем сети (созвездие, радар) на входе нет — заказчик отверг обе.
   Осталось то, что и должно продавать систему: её собственные экраны. У каждой
   главы своя пара — крупный экран слева-снизу и вспомогательный справа-сверху,
   с перекрытием и тенями, поэтому композиция читается объёмной. Фрагменты
   собраны нативно (не картинки): резкие на любом экране, живут в обеих темах и
   умеют оживать. Все пары лежат друг на друге, видна только пара активной главы. */
.signin__stage {
  position: relative;
  width: 100%;
  height: var(--stage-h, 348px);
  margin: 0 0 30px;
}
.signin__gallery { position: absolute; inset: 0; will-change: transform; }

.gal {
  /* Отступы вынесены в переменные: шапка уходит за них в минус и становится
     полноценным титлбаром окна — без лишней обёртки в разметке. */
  --gpx: 14px;
  --gpy: 12px;
  /* Световая кромка сверху и подложка титлбара — свои в каждой теме. */
  --gal-edge: color-mix(in srgb, #fff 78%, transparent);
  --gal-head: color-mix(in srgb, var(--color-surface-soft) 62%, transparent);
  --gal-line: color-mix(in srgb, var(--color-border) 82%, transparent);
  --gal-inset: color-mix(in srgb, var(--color-surface-soft) 70%, transparent);
  position: absolute;
  margin: 0;
  padding: var(--gpy) var(--gpx) 12px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  border-radius: 16px;
  font-size: 12.8px;
  line-height: 1.25;
  /* Не заливка, а градиент: окно ловит свет сверху и темнеет книзу — иначе
     карточка читается плоским прямоугольником, а не экраном системы. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--color-surface) 97%, transparent),
    color-mix(in srgb, var(--color-surface) 88%, transparent));
  border: 1px solid color-mix(in srgb, var(--color-border-strong) 58%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.35);
  backdrop-filter: blur(18px) saturate(1.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity .32s var(--ease), transform .38s var(--ease-out);
}
html[data-theme="dark"] .gal {
  --gal-edge: rgba(255, 255, 255, .09);
  --gal-head: rgba(255, 255, 255, .045);
  --gal-line: rgba(255, 255, 255, .07);
  --gal-inset: rgba(255, 255, 255, .035);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--color-surface) 94%, transparent),
    color-mix(in srgb, var(--color-bg-deep) 88%, transparent));
  border-color: color-mix(in srgb, var(--color-border-strong) 82%, transparent);
}
/* Главный экран главы — слева и снизу, он же перекрывает вспомогательный. */
.gal--main {
  left: 0;
  bottom: 0;
  width: var(--main-w, 352px);
  /* Доли высоты сцены, а не пиксели: пара всегда перекрывается одинаково,
     как бы ни отличалось содержимое глав. Высота именно задана (не min-):
     иначе она «неопределённая» и проценты внутри (столбцы оборотов) схлопываются
     в ноль. Содержимое всех шести глав в неё укладывается — проверять при правках. */
  height: 66%;
  /* …но содержимое не режем: на невысоких экранах окно доберёт недостающее. */
  min-height: fit-content;
  z-index: 2;
  box-shadow:
    0 34px 70px -32px rgba(12, 14, 18, .62),
    0 4px 12px -7px rgba(12, 14, 18, .32),
    inset 0 1px 0 var(--gal-edge);
  transform: translateY(16px) scale(.985);
}
/* Вспомогательный — справа и сверху, чуть меньше и «дальше». */
.gal--side {
  right: 0;
  top: 0;
  width: var(--side-w, 250px);
  /* Контент вспомогательной карточки прижат вверх: её низ-слева перекрывает
     главный экран главы, и туда не должно попадать ничего значимого. */
  min-height: 46%;
  z-index: 1;
  --gpy: 11px;
  padding: var(--gpy) var(--gpx) 11px;
  font-size: 12.2px;
  box-shadow:
    0 26px 56px -30px rgba(12, 14, 18, .55),
    0 3px 10px -7px rgba(12, 14, 18, .28),
    inset 0 1px 0 var(--gal-edge);
  transform: translateY(-14px) scale(.975);
}
/* В тёмной теме к тени добавлен холодный ореол: окна не «дыры» в небе,
   а светящиеся экраны над ним. */
html[data-theme="dark"] .gal--main {
  box-shadow:
    0 40px 84px -34px rgba(0, 0, 0, .82),
    0 4px 14px -8px rgba(0, 0, 0, .6),
    0 0 60px -22px color-mix(in srgb, var(--sg-blue) 45%, transparent),
    inset 0 1px 0 var(--gal-edge);
}
html[data-theme="dark"] .gal--side {
  box-shadow:
    0 30px 66px -32px rgba(0, 0, 0, .74),
    0 3px 12px -8px rgba(0, 0, 0, .5),
    0 0 46px -20px color-mix(in srgb, var(--sg-blue) 36%, transparent),
    inset 0 1px 0 var(--gal-edge);
}

.signin__aside[data-chapter="0"] .gal--1,
.signin__aside[data-chapter="1"] .gal--2,
.signin__aside[data-chapter="2"] .gal--3,
.signin__aside[data-chapter="3"] .gal--4,
.signin__aside[data-chapter="4"] .gal--5,
.signin__aside[data-chapter="5"] .gal--6 { opacity: 1; transform: none; }
/* Вспомогательный приезжает чуть позже главного — пара «собирается». */
.signin__aside[data-chapter="0"] .gal--main.gal--1,
.signin__aside[data-chapter="1"] .gal--main.gal--2,
.signin__aside[data-chapter="2"] .gal--main.gal--3,
.signin__aside[data-chapter="3"] .gal--main.gal--4,
.signin__aside[data-chapter="4"] .gal--main.gal--5,
.signin__aside[data-chapter="5"] .gal--main.gal--6 { transition-delay: .08s; }
.signin__aside[data-chapter="0"] .gal--side.gal--1,
.signin__aside[data-chapter="1"] .gal--side.gal--2,
.signin__aside[data-chapter="2"] .gal--side.gal--3,
.signin__aside[data-chapter="3"] .gal--side.gal--4,
.signin__aside[data-chapter="4"] .gal--side.gal--5,
.signin__aside[data-chapter="5"] .gal--side.gal--6 { transition-delay: .16s; }

/* Шапка фрагмента = титлбар окна: выходит за паддинги карточки, отделена
   волоском и своей подложкой. Без неё карточка читалась как плоский блок с
   жирной строкой сверху, а не как экран системы. */
.shot__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: calc(var(--gpy) * -1) calc(var(--gpx) * -1) 3px;
  padding: 9px var(--gpx) 8px;
  border-radius: 15px 15px 0 0;
  border-bottom: 1px solid var(--gal-line);
  background: linear-gradient(180deg, var(--gal-head), transparent);
}
.shot__title { font-weight: 750; font-size: 13px; letter-spacing: -.01em; color: var(--color-heading); }

/* Содержимое живёт сразу под шапкой, итог/подвал прижат к нижней кромке окна:
   при space-between шапка отрывалась от содержимого и окно выглядело пустым. */
.gal--main > *:last-child,
.gal--side .shot__foot { margin-top: auto; }
/* График и матрица растягиваются по высоте окна — как на настоящем экране. */
.shot__bars,
.shot__drop,
.shot__grid { flex: 1 1 auto; }
.shot__clock { font-weight: 750; color: var(--sg-blue); font-variant-numeric: tabular-nums; }
.shot__trend { font-weight: 750; color: var(--color-success); }
.shot__badge {
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--color-muted);
  background: color-mix(in srgb, var(--color-muted) 13%, transparent);
}

.shot__rows { display: flex; flex-direction: column; gap: 5px; }
.shot__row, .shot__log, .shot__money-row, .shot__calc {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  border-radius: 9px;
  background: var(--gal-inset);
  border: 1px solid var(--gal-line);
}
.shot__code { font-weight: 750; letter-spacing: -.01em; }
.shot__time { color: var(--color-muted); font-variant-numeric: tabular-nums; }
.shot__row .shot__time { margin-left: auto; }
/* Строка списка открытий: слева код и кто открыл, справа время и статус —
   середина строки не пустует, как в настоящей сводке. */
.shot__row {
  position: relative;
  overflow: hidden;
  padding-left: 11px;
}
.shot__row::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2.5px;
  background: var(--color-success);
  opacity: .6;
}
.shot__who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  color: var(--color-faint);
}
.shot__row.is-alert {
  background: color-mix(in srgb, var(--color-coral) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-coral) 34%, transparent);
}
.shot__row.is-alert::before { background: var(--color-coral); opacity: 1; }
.shot__pill { padding: 2px 8px; border-radius: 999px; font-size: 10.5px; font-weight: 700; white-space: nowrap; }
.shot__pill--ok { color: var(--color-success); background: color-mix(in srgb, var(--color-success) 16%, transparent); }
.shot__pill--late { color: var(--color-coral); background: color-mix(in srgb, var(--color-coral) 20%, transparent); }

.shot__foot { display: flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--color-muted); }
.shot__live {
  flex: none;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--color-success);
  animation: shot-live 2.6s var(--ease-out) infinite;
}

/* Импорт: файл в дропзоне и заполняющаяся полоса разбора. */
.shot__drop {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 12px 12px 18px;
  border-radius: 13px;
  border: 1.5px dashed color-mix(in srgb, var(--sg-blue) 48%, transparent);
  background: color-mix(in srgb, var(--sg-blue) 8%, transparent);
}
.shot__drop-icon {
  flex: none; width: 18px; height: 18px;
  fill: none; stroke: var(--sg-blue);
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
.shot__drop-file { font-weight: 650; }
.shot__prog {
  position: absolute; left: 12px; right: 12px; bottom: 8px;
  height: 4px; border-radius: 999px;
  background: color-mix(in srgb, var(--sg-blue) 20%, transparent);
  overflow: hidden;
}
.shot__prog-fill {
  display: block; height: 100%;
  border-radius: inherit;
  background: var(--sg-blue);
  transform: scaleX(0);
  transform-origin: left;
}
.signin__aside[data-chapter="1"] .shot__prog-fill { animation: shot-prog 3.6s var(--ease-out) .35s infinite; }
.shot__stats { display: flex; gap: 6px; }
.shot__stats div {
  flex: 1;
  padding: 7px 6px;
  border-radius: 10px;
  text-align: center;
  background: color-mix(in srgb, var(--color-surface-soft) 62%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-border) 48%, transparent);
}
.shot__stats b { display: block; font-size: 15px; font-weight: 800; letter-spacing: -.02em; }
.shot__stats span { display: block; margin-top: 1px; font-size: 10.5px; color: var(--color-muted); }

/* Обороты: столбцы вырастают каждый раз, когда глава становится активной. */
.shot__bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  min-height: 60px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--gal-line);
}
.shot__bar {
  flex: 1;
  height: var(--h);
  border-radius: 5px 5px 3px 3px;
  transform-origin: bottom;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--sg-blue) 72%, transparent),
    color-mix(in srgb, var(--sg-blue) 30%, transparent));
}
.shot__bar--now {
  background: linear-gradient(180deg, var(--sg-blue), color-mix(in srgb, var(--sg-blue) 52%, transparent));
  box-shadow: 0 0 16px -3px color-mix(in srgb, var(--sg-blue) 70%, transparent);
}
.signin__aside[data-chapter="2"] .shot__bar { animation: shot-bar .5s var(--ease-out) both; }
.signin__aside[data-chapter="2"] .shot__bar:nth-child(2) { animation-delay: .04s; }
.signin__aside[data-chapter="2"] .shot__bar:nth-child(3) { animation-delay: .08s; }
.signin__aside[data-chapter="2"] .shot__bar:nth-child(4) { animation-delay: .12s; }
.signin__aside[data-chapter="2"] .shot__bar:nth-child(5) { animation-delay: .16s; }
.signin__aside[data-chapter="2"] .shot__bar:nth-child(6) { animation-delay: .20s; }
.signin__aside[data-chapter="2"] .shot__bar:nth-child(7) { animation-delay: .24s; }
.shot__money-row {
  background: color-mix(in srgb, var(--color-coral) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-coral) 32%, transparent);
}
.shot__note { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; color: var(--color-muted); }
.shot__money { flex: none; font-weight: 800; white-space: nowrap; color: var(--color-coral); font-variant-numeric: tabular-nums; }

/* Расчёт зарплаты: строки формулы и итог — «за что именно эта сумма». */
.shot__calc b { margin-left: auto; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
.shot__calc b.is-plus { color: var(--color-success); }
.shot__calc b.is-minus { color: var(--color-coral); }
.shot__calc span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--color-muted); }
.shot__total {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 11px;
  border-radius: 11px;
  background: color-mix(in srgb, var(--sg-blue) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--sg-blue) 34%, transparent);
}
.shot__total span { font-weight: 650; }
.shot__total b { margin-left: auto; font-size: 15px; font-weight: 850; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.signin__aside[data-chapter="3"] .shot__calc { animation: sg-row-in .34s var(--ease-out) both; }
.signin__aside[data-chapter="3"] .shot__calc:nth-child(2) { animation-delay: .06s; }
.signin__aside[data-chapter="3"] .shot__calc:nth-child(3) { animation-delay: .12s; }
.signin__aside[data-chapter="3"] .shot__total { animation: sg-row-in .34s var(--ease-out) .18s both; }

/* Матрица смен: строка — ПВЗ, столбец — день, цвет — свой / замена / никого
   (светофор как в «Графиках»). Первая колонка строки — код точки. */
.shot__grid {
  display: grid;
  grid-template-columns: 52px repeat(12, 1fr);
  grid-auto-rows: 1fr;
  align-items: stretch;
  gap: 4px;
}
.shot__grid-code {
  align-self: center;
  font-size: 9.6px;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--color-faint);
}
.shot__cell { min-height: 11px; border-radius: 3px; }
.shot__cell.is-own,  .shot__legend i.is-own  { background: color-mix(in srgb, var(--sg-blue) 55%, transparent); }
.shot__cell.is-swap, .shot__legend i.is-swap { background: color-mix(in srgb, var(--color-warning) 62%, transparent); }
.shot__cell.is-none, .shot__legend i.is-none { background: color-mix(in srgb, var(--color-coral) 62%, transparent); }
/* Клетки проявляются волной слева направо: 13 колонок сетки = код + 12 дней. */
.signin__aside[data-chapter="4"] .shot__cell { animation: shot-cell .38s var(--ease-out) both; }
.signin__aside[data-chapter="4"] .shot__cell:nth-child(13n + 4) { animation-delay: .03s; }
.signin__aside[data-chapter="4"] .shot__cell:nth-child(13n + 6) { animation-delay: .06s; }
.signin__aside[data-chapter="4"] .shot__cell:nth-child(13n + 8) { animation-delay: .09s; }
.signin__aside[data-chapter="4"] .shot__cell:nth-child(13n + 10) { animation-delay: .12s; }
.signin__aside[data-chapter="4"] .shot__cell:nth-child(13n + 12) { animation-delay: .15s; }
.signin__aside[data-chapter="4"] .shot__cell:nth-child(13n + 13) { animation-delay: .18s; }
.shot__legend { display: flex; gap: 13px; font-size: 11px; color: var(--color-muted); }
.shot__legend span { display: inline-flex; align-items: center; gap: 5px; }
.shot__legend i { width: 9px; height: 9px; border-radius: 3px; }

/* Аудит: короткие строки журнала. */
.shot__rows--log { gap: 5px; }
.shot__log { font-size: 11.8px; }
.shot__log .shot__time { flex: none; color: var(--sg-blue); font-weight: 700; }
.shot__log span + span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- Вспомогательные фрагменты витрины ------------------------------------ */
.shot__foot--tight { font-size: 11px; }
.shot__badge--live {
  color: var(--color-coral);
  background: color-mix(in srgb, var(--color-coral) 16%, transparent);
}

/* Кадр камеры: не серый прямоугольник и не картинка, а нарисованный зал ПВЗ —
   вход с дневным светом, стеллажи с посылками и сотрудник, который только что
   открыл точку. Сверху — служебный слой камеры (виньетка, развёртка, время),
   поэтому кадр читается видеонаблюдением, а не иллюстрацией. */
.shot__frame {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 10px;
  overflow: hidden;
  background: #0c1120;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, .1),
    0 6px 16px -10px rgba(0, 0, 0, .7);
}
.shot__scene {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* Лёгкая расфокусировка и приглушённый цвет — как у сжатого кадра с камеры. */
  filter: saturate(.88) blur(.2px);
}
/* Виньетка + строчная развёртка поверх сцены, но под подписями (z-index). */
.shot__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, .022) 0 1px, transparent 1px 4px),
    radial-gradient(120% 104% at 50% 42%, transparent 38%, rgba(3, 6, 14, .62) 100%);
}
/* Сотрудник в кадре чуть размыт — так он читается человеком в видеопотоке,
   а не пиктограммой поверх рисунка. */
.shot__scene .cam-person { filter: blur(.55px); }
.shot__frame-time,
.shot__frame-tag {
  position: absolute;
  z-index: 2;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .06em;
  color: rgba(255, 255, 255, .88);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .75);
  font-variant-numeric: tabular-nums;
}
.shot__frame-time { top: 7px; right: 8px; display: inline-flex; align-items: center; gap: 5px; }
/* Точка записи — рядом со временем: кадр «живой», а не сохранённый скрин. */
.shot__frame-time::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: #ff5f7d;
  box-shadow: 0 0 7px rgba(255, 95, 125, .95);
  animation: shot-live 2.6s var(--ease-out) infinite;
}
/* Подпись — справа: левый край карточки перекрыт главным экраном главы. */
.shot__frame-tag { right: 8px; bottom: 7px; text-transform: uppercase; color: rgba(255, 255, 255, .68); }

/* Разобранный PDF: поле → значение. */
.shot__fields { display: flex; flex-direction: column; gap: 5px; }
.shot__fields div {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 9px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--color-surface-soft) 62%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-border) 48%, transparent);
}
.shot__fields span { color: var(--color-muted); }
.shot__fields b { margin-left: auto; font-weight: 750; font-variant-numeric: tabular-nums; }

/* Рейтинг точек по обороту: строка + доля от лидера полосой. */
.shot__rank { display: flex; flex-direction: column; gap: 9px; }
.shot__rank div { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 8px; }
.shot__rank-bar {
  flex-basis: 100%;
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(90deg,
    var(--sg-blue) 0 var(--w, 0%),
    color-mix(in srgb, var(--sg-blue) 15%, transparent) var(--w, 0%) 100%);
}
.shot__rank-bar--down {
  background: linear-gradient(90deg,
    var(--color-coral) 0 var(--w, 0%),
    color-mix(in srgb, var(--color-coral) 15%, transparent) var(--w, 0%) 100%);
}
.shot__rank b { margin-left: auto; font-weight: 750; font-variant-numeric: tabular-nums; }
.shot__rank i { flex: none; width: 42px; text-align: right; font-style: normal; font-weight: 700; font-size: 11px; }
.shot__rank i.is-up { color: var(--color-success); }
.shot__rank i.is-down { color: var(--color-coral); }

/* Балл KPI + шкала. */
.shot__score { display: flex; align-items: baseline; gap: 7px; }
.shot__score-value { font-size: 30px; font-weight: 850; line-height: 1; letter-spacing: -.03em; }
.shot__score-of { color: var(--color-muted); font-size: 11.5px; }
.shot__meter {
  height: 6px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--sg-blue) 16%, transparent);
  overflow: hidden;
}
.shot__meter span {
  display: block;
  width: var(--w, 0%);
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--sg-blue), color-mix(in srgb, var(--color-success) 60%, var(--sg-blue)));
  transform-origin: left;
}
.signin__aside[data-chapter="3"] .shot__meter span { animation: shot-meter .7s var(--ease-out) .2s both; }

/* Замена: кто просит → кто выходит. */
.shot__swap { display: flex; align-items: center; gap: 9px; }
.shot__swap div { min-width: 0; }
.shot__swap span { display: block; font-size: 10.5px; color: var(--color-muted); }
.shot__swap b { display: block; font-weight: 750; white-space: nowrap; }
.shot__swap-arrow {
  flex: none; width: 18px; height: 18px;
  fill: none; stroke: var(--sg-blue);
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}

/* Роли: что именно видит каждая. */
.shot__roles { display: flex; flex-direction: column; gap: 5px; }
.shot__roles div {
  padding: 6px 9px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--color-surface-soft) 62%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-border) 48%, transparent);
}
.shot__roles b { font-weight: 750; }
.shot__roles span { margin-left: 7px; color: var(--color-muted); font-size: 11.2px; }

/* ---- Aside copy: сменяющиеся «главы» -------------------------------------
   Слайды лежат в одной grid-ячейке: контейнер держит высоту самой высокой
   главы, поэтому смена не дёргает layout. Активную главу ведёт JS.
   Уходящая глава просто гаснет, а приходящая набирается построчно: рубрика →
   заголовок → лид → пункты. Каскад — CSS-анимации на .is-active, они сами
   перезапускаются при каждой смене класса. */
.signin__slides { display: grid; }
.signin__slide {
  grid-area: 1 / 1;
  min-width: 0;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .16s var(--ease), visibility 0s .16s;
}
/* Приходящая глава ждёт, пока уходящая догорит: два заголовка не наслаиваются. */
.signin__slide.is-active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition-delay: .16s, 0s;
}
.signin__slide.is-active .signin__kicker,
.signin__slide.is-active .signin__slogan,
.signin__slide.is-active .signin__lead,
.signin__slide.is-active .signin__point { animation: sg-chapter-in .42s var(--ease-out) .16s both; }
.signin__slide.is-active .signin__slogan { animation-delay: .21s; }
.signin__slide.is-active .signin__lead { animation-delay: .26s; }
.signin__slide.is-active .signin__point:nth-child(1) { animation-delay: .32s; }
.signin__slide.is-active .signin__point:nth-child(2) { animation-delay: .38s; }
.signin__slide.is-active .signin__point:nth-child(3) { animation-delay: .44s; }

.signin__dots { display: flex; gap: 8px; margin-top: 24px; }
.signin__dot {
  position: relative;
  width: 36px;
  height: 6px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  overflow: hidden;
  background: color-mix(in srgb, var(--sg-blue) 18%, transparent);
  transition: background .3s var(--ease);
}
.signin__dot:hover { background: color-mix(in srgb, var(--sg-blue) 34%, transparent); }
.signin__dot:focus-visible { outline: 0; box-shadow: var(--ring); }
.signin__dot-fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--sg-blue);
  transform: scaleX(0);
  transform-origin: left;
}
/* Заполнение активной точки = таймер главы: по animationend JS листает дальше. */
.signin__dot.is-active .signin__dot-fill { animation: sg-dotfill 5.5s linear forwards; }
.signin__aside:hover .signin__dot-fill,
.signin__aside:focus-within .signin__dot-fill { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  /* Без анимации нет и animationend — автолистание отключается само. */
  .signin__dot-fill { display: none; }
  .signin__dot.is-active { background: var(--sg-blue); }
}

.signin__kicker {
  margin: 0;
  font-size: 12.5px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--sg-blue);
}
.signin__slogan {
  margin: 14px 0 0;
  font-size: clamp(30px, calc(2.6vw * var(--vu)), 38px);
  line-height: 1.14;
  font-weight: 800;
  letter-spacing: -0.025em;
}
/* Ключевая часть заголовка — градиентом: глаз цепляется за смысл главы. */
.signin__slogan em {
  font-style: normal;
  background: linear-gradient(96deg,
    var(--sg-blue),
    color-mix(in srgb, var(--color-coral) 62%, var(--sg-blue)));
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}
.signin__lead {
  margin: 14px 0 0;
  max-width: 520px;
  font-size: 16.5px;
  line-height: 1.5;
  color: var(--color-muted);
}
.signin__points { list-style: none; margin: 28px 0 0; padding: 0; display: flex; flex-direction: column; gap: 16px; }
.signin__point { display: flex; gap: 14px; align-items: flex-start; }
.signin__point-icon {
  flex: none;
  width: 42px;
  height: 42px;
  border-radius: 13px;
  display: grid;
  place-items: center;
  color: var(--sg-blue);
  background: color-mix(in srgb, var(--color-surface) 62%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-border) 62%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 45%, transparent);
}
html[data-theme="dark"] .signin__point-icon { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05); }
.signin__point-icon svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.signin__point-title { display: block; font-size: 15.5px; font-weight: 700; line-height: 1.35; }
.signin__point-text { display: block; margin-top: 3px; max-width: 460px; font-size: 14.5px; line-height: 1.5; color: var(--color-muted); }

/* Большие мониторы: колонка и витрина растут — на 24″ вход выглядит как
   витрина продукта, а не как страница, сжатая до 1280. */
@media (min-width: 1248px) and (min-height: 800px) {
  .signin__layout { max-width: 1330px; grid-template-columns: minmax(0, 508px) minmax(0, 660px); }
  /* Правило выше идёт ПОЗЖЕ базового --solo и перебило бы его на широком
     мониторе: одноколоночная сетка снова стала бы двухколоночной, а форма
     уехала бы влево. Повторяем модификатор внутри этого медиазапроса. */
  .signin__layout--solo { max-width: 508px; grid-template-columns: minmax(0, 508px); }
  .signin__aside { --stage-h: 392px; --main-w: 386px; --side-w: 288px; }
  .signin__lead { max-width: 560px; }
  .signin__point-text { max-width: 520px; }
}
@media (max-width: 1136px) {
  .signin__layout { column-gap: clamp(44px, calc(5vw * var(--vu)), 84px); }
  .signin__slogan { font-size: clamp(28px, calc(2.5vw * var(--vu)), 34px); }
}
/* Небольшие экраны: визуальная зона не должна мешать входу — остаётся
   прежняя центрированная карточка. */
@media (max-width: 944px) {
  .signin__layout { grid-template-columns: minmax(0, 508px); }
  .signin__aside { display: none; }
}
/* Невысокие десктопы (вкл. частые 1440×900 / 1536×864): поджимаем витрину,
   чтобы обе колонки жили без вертикального скролла. */
@media (min-width: 945px) and (max-height: 752px) {
  .signin__aside { --stage-h: 330px; --main-w: 318px; --side-w: 244px; }
  .signin__stage { margin-bottom: 22px; }
  .signin__slogan { margin-top: 10px; }
  .signin__lead { margin-top: 10px; }
  .signin__points { margin-top: 18px; gap: 12px; }
  .signin__dots { margin-top: 16px; }
}
@media (min-width: 945px) and (max-height: 640px) {
  .signin__aside { --stage-h: 258px; --main-w: 330px; --side-w: 228px; }
  .signin__stage { margin-bottom: 16px; }
  .signin__slogan { font-size: clamp(25px, calc(2.1vw * var(--vu)), 29px); }
  .signin__lead { margin-top: 8px; font-size: 15px; }
  .signin__points { margin-top: 13px; gap: 9px; }
  .signin__point { gap: 11px; }
  .signin__point-icon { width: 36px; height: 36px; border-radius: 11px; }
  .signin__point-icon svg { width: 19px; height: 19px; }
  .signin__point-title { font-size: 14.6px; }
  .signin__point-text { font-size: 13.6px; }
  .signin__dots { margin-top: 13px; }
  .gal { font-size: 12px; padding: 11px 12px 10px; gap: 8px; }
  .gal--side { font-size: 11.6px; }
  .shot__bars { height: 46px; }
}
/* Никаких зацикленных анимаций внутри витрины, если пользователь их выключил:
   остаётся статичная, полностью читаемая картинка. */
@media (prefers-reduced-motion: reduce) {
  .signin__aside .shot__live,
  .signin__aside .shot__bar,
  .signin__aside .shot__cell,
  .signin__aside .shot__calc,
  .signin__aside .shot__total,
  .signin__aside .shot__meter span,
  .signin__slide.is-active .signin__kicker,
  .signin__slide.is-active .signin__slogan,
  .signin__slide.is-active .signin__lead,
  .signin__slide.is-active .signin__point { animation: none; }
  .signin__aside .shot__prog-fill { animation: none; transform: scaleX(1); }
}

.spark { height: 40px; margin-top: 8px; }
.spark svg { width: 100%; height: 100%; }
.spark__line { fill: none; stroke: var(--color-primary); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.spark--up .spark__line { stroke: var(--color-success); }
.bars { display: flex; align-items: flex-end; gap: 7px; height: 56px; margin-top: 6px; }
.bars__bar { flex: 1; background: linear-gradient(180deg, var(--color-primary), var(--color-primary-dark)); border-radius: 5px 5px 3px 3px; height: var(--h); }
.bar-chart { display: flex; align-items: flex-end; gap: 8px; min-height: 80px; }
.donut-mini { width: 58px; height: 58px; border-radius: 50%; flex: 0 0 58px; background: conic-gradient(var(--color-primary) calc(var(--val) * 1%), var(--color-surface-soft) 0); position: relative; }
.donut-mini::after { content: ""; position: absolute; inset: 9px; border-radius: 50%; background: var(--color-surface); }

/* =========================================================================
   Employee cabinet
   ========================================================================= */
.employee-menu {
  display: flex; gap: 8px; overflow-x: auto; padding: 2px 2px 8px;
  scrollbar-width: thin;
}
.employee-menu__item {
  flex: 0 0 auto; padding: 9px 13px; border-radius: var(--radius-pill);
  background: var(--color-surface); border: 1px solid var(--color-border);
  color: var(--color-muted); font-weight: 700; font-size: 13px;
  transition: background-color .18s var(--ease), color .18s var(--ease), transform .18s var(--ease);
}
.employee-menu__item:hover { color: var(--color-text); transform: translateY(-1px); }
.employee-menu__item.is-active { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-on-primary); }

.cabinet-hero { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(320px, .65fr); gap: 18px; }
.cabinet-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; }
.cabinet-eyebrow {
  display: inline-flex; margin-bottom: 7px; color: var(--color-primary-dark);
  font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
}
.cabinet-shift {
  background:
    radial-gradient(circle at 10% 0%, var(--color-primary-soft), transparent 45%),
    radial-gradient(circle at 100% 100%, var(--color-coral-soft), transparent 44%),
    var(--color-surface);
}
.cabinet-shift__top { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; }
.cabinet-shift__title { font-size: 24px; line-height: 1.15; letter-spacing: -0.015em; }
.cabinet-shift__address { margin-top: 7px; color: var(--color-muted); }
.cabinet-shift__meta {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 4px;
}
.cabinet-shift__meta div {
  padding: 13px; border-radius: var(--radius-lg); background: color-mix(in srgb, var(--color-surface) 76%, var(--color-surface-soft));
  border: 1px solid var(--color-border);
}
.cabinet-shift__meta span { display: block; color: var(--color-muted); font-size: 12px; margin-bottom: 4px; }
.cabinet-shift__meta strong { display: block; font-size: 13.5px; }
/* Строка ВТОРОСТЕПЕННЫХ действий кабинета. align-items обязателен: flex по
   умолчанию тянет элементы на высоту строки, и голая ссылка-кнопка рядом с
   формой подтверждения растягивалась во всю её высоту — а при --radius-pill
   это овал в ~200px (жалоба заказчика по «Моей смене», 03.09.2026; высоту
   задавала textarea.input со своим min-height: 118px). Форма подтверждения
   теперь живёт отдельным блоком, но выравнивание оставлено и здесь: на
   .cabinet-actions висят ещё четыре карточки кабинета, и ловушка обязана быть
   закрыта у всех, а не только там, где рвануло. flex-start, а не center: в
   соседних карточках в этом ряду стоят разновысокие формы, и center сдвинул
   бы их. */
.cabinet-actions {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px; margin-top: auto;
}

/* Главное действие смены — отдельным блоком: у формы своё поле комментария, и
   в общем ряду кнопок она диктовала высоту соседям. Заливка та же, что у
   плиток .cabinet-shift__meta, — блок должен читаться их роднёй, а не чужой
   вставкой. */
.shift-confirm {
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px; border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-surface) 76%, var(--color-surface-soft));
  border: 1px solid var(--color-border);
}
/* Тег в селекторе — чтобы перебить общий textarea.input (min-height: 118px):
   комментарий к выходу пишут строкой, а не абзацем. */
.shift-confirm textarea.input { min-height: 64px; }
/* align-self, иначе колонка растянет кнопку во всю ширину блока. */
.shift-confirm__submit { align-self: flex-start; min-height: var(--control-h); }
.shift-confirm__hint { font-size: 12.5px; color: var(--color-faint); }

/* Плитка состояния кабинета: «что сейчас» читается значком и строкой, а не
   абзацем серого текста. Тон несёт ТОЛЬКО значок — заливка у всех состояний
   одна, иначе карточка смены превращается в светофор. */
.state-tile {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 14px; border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-surface) 76%, var(--color-surface-soft));
  border: 1px solid var(--color-border);
}
.state-tile__icon {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: var(--radius-md);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--color-surface-soft); color: var(--color-muted);
}
.state-tile__icon svg { width: 18px; height: 18px; }
.state-tile__body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.state-tile__title { font-size: 13.5px; font-weight: 700; color: var(--color-text); }
.state-tile__text { font-size: 12.5px; color: var(--color-muted); }
.state-tile__side { margin-left: auto; flex: 0 0 auto; }
.state-tile--ok .state-tile__icon { background: var(--color-success-soft); color: var(--color-success); }
.state-tile--warn .state-tile__icon { background: var(--color-warning-soft); color: var(--color-warning); }
.state-tile--wait .state-tile__icon { background: var(--color-primary-soft); color: var(--color-primary-dark); }

.cabinet-kpi { align-items: center; text-align: center; }
.cabinet-kpi .card__head { width: 100%; }
.kpi-ring { width: 178px; height: 178px; margin: 2px auto; }

.progress-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; width: 100%; }
.progress-row { display: grid; grid-template-columns: minmax(0, 1fr) 110px 42px; gap: 10px; align-items: center; text-align: left; font-size: 13px; }
.progress-row__label { color: var(--color-muted); }
.progress-row__track { height: 8px; border-radius: var(--radius-pill); background: var(--color-surface-soft); overflow: hidden; }
.progress-row__fill { display: block; width: var(--w, 0); height: 100%; border-radius: inherit; }
.progress-row__fill--primary { background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark)); }
.progress-row__fill--success { background: linear-gradient(90deg, var(--color-success), color-mix(in srgb, var(--color-success) 60%, #fff)); }
.progress-row__fill--warning { background: linear-gradient(90deg, var(--color-warning), color-mix(in srgb, var(--color-warning) 60%, #fff)); }
.progress-row__fill--coral { background: linear-gradient(90deg, var(--color-coral), color-mix(in srgb, var(--color-coral) 60%, #fff)); }

.shift-list,
.claim-list { display: grid; gap: 10px; }
.shift-list__item,
.claim-list__item {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px; border-radius: var(--radius-lg); background: var(--color-surface-soft);
  border: 1px solid var(--color-border);
}
.shift-list__item { border-left: 4px solid var(--color-primary); }
.shift-list__item--beige { border-left-color: var(--color-beige); }
.shift-list__item strong,
.claim-list__item strong { display: block; }
.shift-list__item span,
.claim-list__item span:not(.badge) { display: block; color: var(--color-muted); font-size: 12px; margin-top: 2px; }

/* Строка «Мои смены» с быстрыми действиями: ссылка-тело + кнопки справа. */
.shift-list__item--row { transition: border-color .15s var(--ease); }
.shift-list__item--row:hover { border-color: var(--color-primary); }
.shift-list__link { flex: 1; min-width: 0; text-decoration: none; color: inherit; }
.shift-list__side { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
@media (max-width: 720px) {
  .shift-list__item--row { flex-direction: column; align-items: stretch; }
  .shift-list__side { justify-content: space-between; }
}

.notice-list,
.material-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.notice-list li,
.material-list li {
  position: relative; padding-left: 18px; color: var(--color-muted);
}
.notice-list li::before,
.material-list li::before {
  content: ""; position: absolute; left: 0; top: .62em; width: 7px; height: 7px;
  border-radius: 50%; background: var(--color-primary);
}

/* =========================================================================
   Error pages
   ========================================================================= */
.errorp { min-height: calc(100vh * var(--vu)); display: flex; align-items: center; justify-content: center; padding: 24px; text-align: center; }
.errorp__toggle { position: absolute; top: 22px; right: 24px; }
.errorp__card { max-width: 460px; align-items: center; }
.errorp__code { font-size: 96px; font-weight: 900; line-height: 1; letter-spacing: -0.04em; background: linear-gradient(135deg, var(--color-primary), var(--color-coral)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.errorp__title { font-size: 22px; font-weight: 800; margin-top: 12px; }
.errorp__text { color: var(--color-muted); margin: 12px 0 24px; }

/* =========================================================================
   Theme toggle
   ========================================================================= */
.theme-toggle {
  background: transparent; border: 0; padding: 0; display: inline-flex;
  border-radius: var(--radius-pill); cursor: pointer;
}
.theme-toggle__track {
  width: 60px; height: 32px; border-radius: var(--radius-pill); position: relative;
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  transition: background-color .38s var(--ease), border-color .38s var(--ease);
}
.theme-toggle:hover .theme-toggle__track { border-color: var(--color-border-strong); }
.theme-toggle__thumb {
  position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; border-radius: 50%;
  background: var(--color-surface); box-shadow: var(--shadow-card);
  display: inline-flex; align-items: center; justify-content: center; color: var(--color-warning);
  /* Лёгкий перелёт (1.2 в кривой) — бегунок «долетает» и осаживается назад.
     Больше брать нельзя: ход 28px, а до правого края остаётся ровно 30px. */
  transition: transform .46s cubic-bezier(.34, 1.2, .5, 1), color .3s var(--ease),
              background-color .3s var(--ease), box-shadow .3s var(--ease);
}
/* Сжатие в момент старта — ставится классом на время полёта (theme.js).
   Отдельным свойством `scale`, а не в transform: там уже едет translateX,
   и общий transform затёр бы ход бегунка. */
.theme-toggle.is-flipping .theme-toggle__thumb { animation: theme-thumb-squash .46s var(--ease-out); }
@keyframes theme-thumb-squash {
  0% { scale: 1 1; }
  32% { scale: 1.16 .84; }
  68% { scale: .93 1.07; }
  100% { scale: 1 1; }
}
.theme-toggle__icon {
  width: 15px; height: 15px; position: absolute;
  transition: opacity .26s var(--ease), transform .46s cubic-bezier(.34, 1.3, .5, 1);
}
.theme-toggle__icon--moon { opacity: 0; transform: rotate(-70deg) scale(.5); }
.theme-toggle__icon--sun { opacity: 1; transform: rotate(0) scale(1); }
/* Звёзды на освободившейся половине дорожки: загораются по очереди, когда
   бегунок уже ушёл вправо (задержки больше, чем половина его хода). */
.theme-toggle__star {
  position: absolute; border-radius: 50%; background: var(--color-primary-dark);
  opacity: 0; transform: scale(.2);
  transition: opacity .28s var(--ease), transform .4s var(--ease-out);
}
.theme-toggle__star:nth-of-type(1) { width: 3px; height: 3px; left: 9px; top: 8px; }
.theme-toggle__star:nth-of-type(2) { width: 2px; height: 2px; left: 17px; top: 19px; }
.theme-toggle__star:nth-of-type(3) { width: 2px; height: 2px; left: 21px; top: 11px; }
html[data-theme="dark"] .theme-toggle__track { background: var(--color-primary-soft); }
html[data-theme="dark"] .theme-toggle__thumb {
  transform: translateX(28px); color: var(--color-primary);
  box-shadow: var(--shadow-card), 0 0 12px rgba(65, 135, 255, 0.35);
}
html[data-theme="dark"] .theme-toggle__icon--sun { opacity: 0; transform: rotate(70deg) scale(.5); }
html[data-theme="dark"] .theme-toggle__icon--moon { opacity: 1; transform: rotate(0) scale(1); }
html[data-theme="dark"] .theme-toggle__star { opacity: .95; transform: scale(1); }
html[data-theme="dark"] .theme-toggle__star:nth-of-type(1) { transition-delay: .16s; }
html[data-theme="dark"] .theme-toggle__star:nth-of-type(2) { transition-delay: .23s; }
html[data-theme="dark"] .theme-toggle__star:nth-of-type(3) { transition-delay: .30s; }

/* =========================================================================
   Смена темы — круговая волна из переключателя (stage 157)

   Тема меняется через View Transitions: браузер снимает старый кадр, мы
   применяем data-theme, и новый кадр «наливается» кругом из центра нажатой
   кнопки. Старый кадр при этом стоит на месте — расходится ровно граница
   круга, а не две наложенные анимации.

   Почему именно так, а не поэлементные transition: раньше цвет ехал только у
   десятка классов из белого списка (карточка, кнопка, бейдж…), а всё
   остальное — графики, таблицы, тени, svg — щёлкало мгновенно. Снимок
   перехода накрывает страницу целиком, поэтому анимировать нечего перечислять.

   ВАЖНО: ::view-transition-new(root) в браузере живой, а не картинка, — внутри
   круга бегунок реально едет и звёзды загораются. Поэтому переключателю НЕ
   нужен свой view-transition-name.

   Точку и радиус ставит theme.js (--theme-x/y/r). Значения по умолчанию —
   центр экрана: так правило валидно и когда волну запускает не клик, а смена
   системной темы.
   ========================================================================= */
:root {
  /* Проценты, а не px/vw: у снимка перехода своя система координат (см.
     setWaveOrigin в theme.js) — в px круг уезжает от кнопки тем сильнее, чем
     выше DPR экрана. */
  --theme-x: 50%;
  --theme-y: 50%;
  --theme-r: 150%;
  --theme-wave: .45s;
  /* Почти равномерный ход края с мягким стартом и без длинного хвоста.
     var(--ease-out) здесь не годится: он проходит 93% радиуса за первую треть
     времени — круг не успевает «выйти» из иконки, а оставшиеся две трети
     страница уже дорисована и просто стоит замороженная (во время перехода
     она не реагирует на клики). Значения кривой: 8% радиуса на 10% времени,
     93% на 70%. */
  --theme-wave-ease: cubic-bezier(.3, .15, .35, 1);
}

html.theme-vt::view-transition-group(root) { animation-duration: var(--theme-wave); }
/* Пара снимков по умолчанию складывается в plus-lighter — для кроссфейда это
   верно, но у нас круг с резкой границей, и на стыке проступала бы засветка. */
html.theme-vt::view-transition-image-pair(root) { isolation: auto; }
html.theme-vt::view-transition-old(root),
html.theme-vt::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
  backface-visibility: hidden;
}
html.theme-vt::view-transition-old(root) { z-index: 0; }
html.theme-vt::view-transition-new(root) {
  z-index: 1;
  animation: theme-wave var(--theme-wave) var(--theme-wave-ease) forwards;
}
@keyframes theme-wave {
  from { clip-path: circle(0% at var(--theme-x) var(--theme-y)); }
  to { clip-path: circle(var(--theme-r) at var(--theme-x) var(--theme-y)); }
}

/* Фолбэк для браузеров без View Transitions: на полсекунды разрешаем плавный
   цвет вообще всему, а не белому списку выше. Постоянно так висеть нельзя —
   это переход на каждом узле дерева; класс живёт ровно время смены. */
html.theme-shift *,
html.theme-shift *::before,
html.theme-shift *::after {
  transition-property: background-color, color, border-color, outline-color, fill, stroke;
  transition-duration: .4s;
  transition-timing-function: var(--ease);
  transition-delay: 0s;
}
/* Правило выше перебивает ход бегунка (transition-property заменяет список
   целиком) — возвращаем его точечно, специфичностью повыше. */
html.theme-shift .theme-toggle__thumb {
  transition-property: transform, color, box-shadow;
  transition-duration: .46s;
}
html.theme-shift .theme-toggle__icon {
  transition-property: opacity, transform;
  transition-duration: .46s;
}

/* Общий блок reduced-motion ниже гасит анимации через `*`, а псевдоэлементы
   перехода под `*` не попадают — их надо выключать отдельно. */
@media (prefers-reduced-motion: reduce) {
  html.theme-vt::view-transition-old(root),
  html.theme-vt::view-transition-new(root) { animation: none; }
  html.theme-shift *,
  html.theme-shift *::before,
  html.theme-shift *::after { transition-duration: 0.001ms; }
}

/* =========================================================================
   Animations
   ========================================================================= */
@keyframes fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes slide-up { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes pop-in { from { opacity: 0; transform: scale(.92) translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes donut-pop { from { opacity: 0; transform: rotate(-90deg) scale(.85); } to { opacity: 1; transform: rotate(-90deg) scale(1); } }
@keyframes line-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes bar-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes fill-grow { from { width: 0; } }
/* Кольцо-просвет вокруг бейджа задаёт носитель: --pulse-ring (цвет) и
   --pulse-ring-w (толщина). Жёсткий --color-bg здесь означал, что box-shadow
   самого бейджа не применялся НИКОГДА: анимация перебивает обычное
   объявление, и «кольцо цветом шапки» у .topbar__bell-count было мёртвой
   строкой — вокруг счётчика на белой шапке оставался серый ободок цвета
   полотна (регрессия от 25.08.2026, когда шапка получила свой тон). */
@keyframes pulse-soft {
  0%, 100% { box-shadow: 0 0 0 var(--pulse-ring-w, 3px) var(--pulse-ring, var(--color-bg)), 0 0 0 var(--pulse-ring-w, 3px) transparent; }
  50%      { box-shadow: 0 0 0 var(--pulse-ring-w, 3px) var(--pulse-ring, var(--color-bg)), 0 0 0 calc(var(--pulse-ring-w, 3px) + 4px) var(--color-coral-soft); }
}
@keyframes dropdown-in { from { opacity: 0; transform: translateY(-6px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes signin-card-in {
  from { opacity: 0; transform: translateY(14px) scale(.99); filter: blur(4px); }
  to { opacity: 1; transform: none; filter: none; }
}
@keyframes sg-row-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}
@keyframes sg-drift-1 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(9%, 7%, 0) scale(1.14); }
}
@keyframes sg-drift-2 {
  from { transform: translate3d(0, 0, 0) scale(1.06); }
  to { transform: translate3d(-8%, -6%, 0) scale(1); }
}
@keyframes sg-drift-3 {
  from { transform: translate3d(-5%, 0, 0) scale(1); }
  to { transform: translate3d(7%, -7%, 0) scale(1.16); }
}
@keyframes sg-drift-4 {
  from { transform: translate3d(0, 4%, 0) scale(1.08); }
  to { transform: translate3d(6%, -4%, 0) scale(1); }
}
@keyframes sg-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-12px); }
}
@keyframes sg-pulse {
  0%, 100% { box-shadow: var(--shadow-soft); }
  50% { box-shadow: var(--shadow-soft), 0 0 0 6px color-mix(in srgb, var(--color-primary) 12%, transparent); }
}
@keyframes sg-sheen {
  0% { left: -60%; }
  55%, 100% { left: 130%; }
}
/* Строка главы: приезжает снизу с лёгким размытием — «набирается», а не мигает. */
@keyframes sg-chapter-in {
  from { opacity: 0; transform: translateY(10px); filter: blur(3px); }
  to   { opacity: 1; transform: none; filter: none; }
}
@keyframes sg-dotfill {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* Оживление фрагментов интерфейса на входе (см. .gal / .shot__*). */
@keyframes shot-live {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-success) 55%, transparent); }
  70%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-success) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-success) 0%, transparent); }
}
@keyframes shot-prog {
  0%   { transform: scaleX(.04); opacity: 1; }
  62%  { transform: scaleX(1); opacity: 1; }
  90%  { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0; }
}
@keyframes shot-bar {
  from { transform: scaleY(.08); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
@keyframes shot-meter {
  from { transform: scaleX(.05); }
  to   { transform: none; }
}
@keyframes shot-cell {
  from { opacity: 0; transform: scale(.72); }
  to   { opacity: 1; transform: none; }
}

/* Контент не «скачет»: появление страниц и карточек — только лёгкое затухание,
   без движения и без каскадных задержек (цифры метрик остаются на месте). */
@keyframes fade-in-soft { from { opacity: 0; } to { opacity: 1; } }
/* fill-mode: backwards (не both/forwards) — завершившаяся filling-анимация
   opacity навсегда оставляет stacking context, и fixed-оверлеи внутри .page
   (модалка камеры и т.п.) рисуются под sidebar/topbar. */
.fade-in { animation: fade-in-soft .18s var(--ease) backwards; }
.slide-up { animation: fade-in-soft .18s var(--ease) backwards; }
.stagger-item { animation: fade-in-soft .18s var(--ease) backwards; }

.hover-lift, .interactive-card { transition: transform .22s var(--ease), box-shadow .22s var(--ease), border-color .22s var(--ease); }
.hover-lift:hover, .interactive-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-pop); border-color: color-mix(in srgb, var(--color-primary) 35%, var(--color-border)); }

.animated-donut svg { animation: donut-pop .9s var(--ease) both; }
.animated-line { stroke-dasharray: 1; stroke-dashoffset: 1; animation: line-draw 1.5s var(--ease) .15s forwards; }
.animated-bar { transform-origin: bottom; animation: bar-grow .8s var(--ease) both; }
.animated-fill { animation: fill-grow 1.1s var(--ease) .1s both; }
.pulse-badge { animation: pulse-soft 2.6s ease-in-out infinite; }
.theme-transition { transition: background-color .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease); }
.focus-ring:focus-visible { outline: 0; box-shadow: var(--ring); }
.soft-glow { box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent), 0 16px 46px color-mix(in srgb, var(--color-primary) 16%, transparent); }

/* =========================================================================
   Responsive
   ========================================================================= */
@media (max-width: 1024px) {
  .metrics,
  .metric-grid { grid-template-columns: repeat(4, 1fr); }
  .grid--charts { grid-template-columns: 1fr 1fr; }
  .grid--bottom { grid-template-columns: 1fr 1fr; }
  .cabinet-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .filters-bar { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 819px) {
  .sidebar {
    position: fixed; left: 0; top: 0; transform: translateX(-100%);
    transition: transform .3s var(--ease); box-shadow: var(--shadow-pop);
  }
  .app-shell.is-open .sidebar { transform: translateX(0); }
  .sidebar-scrim { position: fixed; inset: 0; background: rgba(7, 17, 31, .45); z-index: 35; border: 0; }
  .topbar__menu { display: inline-flex; }
  .grid--charts, .grid--bottom, .grid--detail, .cabinet-hero { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .signin { padding: 18px; }
  .signin__card { padding: 34px 24px 30px; border-radius: 22px; }
  .signin__cursor { display: none; }
}
@media (max-width: 720px) {
  .metrics { grid-template-columns: repeat(2, 1fr); }
  .metric-grid { grid-template-columns: repeat(2, 1fr); }
  .page { padding: 16px; }
  .topbar { padding: 0 14px; }
  .topbar__search { max-width: none; }
  .topbar__user-meta { display: none; }
  .topbar__user-caret { display: none; }
  .dropdown-menu { right: -52px; width: min(260px, calc(calc(100vw * var(--vu)) - 24px)); }
  .auth-card { padding: 22px; }
  .form-grid, .filters-bar { grid-template-columns: 1fr; }
  .form-field--full { grid-column: auto; }
  .detail-list > div { grid-template-columns: 1fr; gap: 4px; }
  .cabinet-grid { grid-template-columns: 1fr; }
  .cabinet-shift__top { flex-direction: column; }
  .cabinet-shift__meta { grid-template-columns: 1fr; }
  .cabinet-actions .btn { width: 100%; }
  /* Главное действие на телефоне — во всю ширину: большой палец бьёт по краю. */
  .shift-confirm__submit { align-self: stretch; width: 100%; }
  /* Кнопка плитки уходит под текст и встаёт по его левому краю (значок 34 + зазор 12). */
  .state-tile { flex-wrap: wrap; }
  .state-tile__side { margin-left: 46px; }
  .progress-row { grid-template-columns: minmax(0, 1fr) 86px 38px; }
  .copy-secret-box__row { align-items: stretch; }
  .copy-secret-box__row .btn { width: 100%; }
  /* Lighter motion on small screens. */
  .interactive-card:hover, .hover-lift:hover { transform: none; }
}

@media (max-width: 520px) {
  .topbar__search { display: none; }
  .topbar__actions { gap: 8px; }
  .theme-toggle__track { width: 54px; height: 30px; }
  html[data-theme="dark"] .theme-toggle__thumb { transform: translateX(22px); }
  .metrics,
  .metric-grid { grid-template-columns: 1fr; }
  .page__actions,
  .page__actions .btn { width: 100%; }
  .signin__toggle,
  .auth-shell__toggle { top: 14px; right: 14px; }
}

/* =========================================================================
   Reduced motion — near-disable everything
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .animated-line { stroke-dashoffset: 0; }
}

/* ---------------------------------------------------------------------------
   Stage 3 — RBAC sidebar/navigation, disabled placeholders, access matrix
   --------------------------------------------------------------------------- */
.sidebar__section {
  margin: 12px 12px 4px; font-size: 10px; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: var(--color-faint);
}
.sidebar__nav > .sidebar__section:first-child { margin-top: 2px; }

.sidebar-item.is-disabled { opacity: .55; cursor: default; }
.sidebar-item.is-disabled:hover { background: transparent; color: inherit; }
.sidebar-item__soon {
  margin-left: auto; font-size: 10px; font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase; color: var(--color-muted);
  background: var(--color-surface-soft); border-radius: var(--radius-pill); padding: 2px 7px;
}

.errorp__actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }

.access-matrix th.access-matrix__role { text-align: center; }
.access-matrix__section { text-align: left; font-weight: 700; color: var(--color-text); }
.access-matrix__cell { text-align: center; }
.access-yes { color: var(--color-success); font-weight: 800; }
/* «○» — раздел не в роли, но собственник вправе открыть его галочкой прав. */
.access-maybe { color: var(--color-primary); font-weight: 800; }
.access-no { color: var(--color-muted); }

/* ---------------------------------------------------------------------------
   Stage 4 — PVZ directory
   --------------------------------------------------------------------------- */
/* Auto-fit, как у широких фильтр-баров (см. .filters-bar--opening): жёсткая
   7-колоночная сетка не влезала в контент рядом с сайдбаром на средних экранах. */
.filters-bar--pvz {
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.filters-bar--pvz .filters-bar__search { grid-column: 1 / -1; }

.metric-grid--pvz { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
/* Цвет числа зарезервирован за проблемой. «Внимание» числом не красим: в
   рабочем дне почти каждая плитка живёт в жёлтом состоянии, и экран целиком
   уходил в горчицу — сообщение «смотри сюда» переставало работать, потому
   что «сюда» оказывалось везде. Само состояние никуда не делось: оно на
   подписи под числом и на бейджах. */
.metric-card__value--success { color: var(--color-success); }
.metric-card__value--warning { color: var(--color-heading); }
.metric-card__value--danger { color: var(--color-danger); }

.pvz-form {
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: 980px;
  width: 100%;
  margin-inline: auto; /* центрируем: без этого справа огромное пустое поле */
}

.json-preview {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--color-muted);
  font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.pvz-assignment-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.pvz-assignment-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--color-border);
}

.pvz-assignment-list li:last-child { border-bottom: 0; }
/* Адрес обрезается ДВУМЯ строками. Раньше он переносился свободно и один этот
   столбец задавал высоту всей строки: «Россия, Псковская Область, Великие Луки,
   улица Ботвина, 7 корпус 1» это три строки, то есть +40 CSS на каждую запись
   реестра. Полный адрес остаётся в карточке ПВЗ и в подсказке браузера. */
.table-cell-address { max-width: 300px; min-width: 220px; }
/* Обрезка МНОГОТОЧИЕМ в одну строку, а не line-clamp: `display: -webkit-box`
   выбил бы <td> из табличной раскладки и развалил таблицу. Тот же приём уже
   работает в mobile.css — здесь он просто распространён на десктоп. */
.table td.table-cell-address,
.table td.table-cell-pvz {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

@media (max-width: 1024px) {
  .metric-grid--pvz { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 720px) {
  .filters-bar--pvz,
  .metric-grid--pvz { grid-template-columns: 1fr; }
  .pvz-form { max-width: none; }
}

/* ---------------------------------------------------------------------------
   Stage 6 — Schedules (shifts, calendar, shift types)
   --------------------------------------------------------------------------- */
/* Shift status badges (planned/confirmed/worked/no_show/cancelled). */
.badge--planned   { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--confirmed { background: color-mix(in srgb, var(--color-primary) 22%, transparent); color: var(--color-primary-dark); }
.badge--worked    { background: var(--color-success-soft); color: var(--color-success); }
.badge--no_show   { background: var(--color-danger-soft); color: var(--color-danger); }
.badge--cancelled { background: var(--color-surface-soft); color: var(--color-muted); }

.metric-grid--shift { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }

/* Auto-fit, как у широких фильтр-баров (см. .filters-bar--opening): жёсткая
   8-колоночная сетка (~1350px) вылезала за карточку рядом с сайдбаром. */
.filters-bar--shift {
  grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
}
.filters-bar--shift .filters-bar__search { grid-column: 1 / -1; }

.shift-form { display: flex; flex-direction: column; gap: 18px; max-width: 980px; }

/* ---- Work patterns (stage 40): графики работы + автогенерация смен ------- */
.pattern-generate { display: flex; flex-direction: column; gap: 14px; }
.pattern-generate__title { font-size: 16px; font-weight: 850; }
.pattern-generate__hint { margin-top: 6px; color: var(--color-muted); font-size: 12.5px; line-height: 1.55; max-width: 720px; }
.pattern-generate__form { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.pattern-generate__form .form-field { min-width: 170px; }
.pattern-form { display: flex; flex-direction: column; gap: 18px; max-width: 980px; }
/* hidden обязан скрывать блоки видов графика: display из .form-grid/.form-field
   иначе перебивает UA-правило [hidden]{display:none}. */
.pattern-form [hidden] { display: none !important; }
.pattern-form__kind { border: 0; padding: 0; margin: 0 0 14px; }
.pattern-form__kind ul { list-style: none; display: flex; gap: 18px; flex-wrap: wrap; padding: 0; margin: 0; }
.pattern-form__kind label { display: inline-flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 650; cursor: pointer; }
.pattern-form__weekdays ul { list-style: none; display: flex; gap: 14px; flex-wrap: wrap; padding: 0; margin: 6px 0 0; }
.pattern-form__weekdays label { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 650; cursor: pointer; }
@media (max-width: 760px) {
  .pattern-generate { align-items: stretch; }
  .pattern-generate__form { flex-direction: column; align-items: stretch; }
}

.stack-form { display: flex; flex-direction: column; gap: 12px; padding: 14px 0; border-top: 1px solid var(--color-border); }
.stack-form:first-of-type { border-top: 0; padding-top: 0; }
.form-subtitle { font-size: 13px; font-weight: 800; color: var(--color-text); }
.inline-form { display: inline; }
.color-chip { display: inline-block; width: 13px; height: 13px; border-radius: 4px; margin-right: 6px; vertical-align: middle; background: var(--chip, var(--color-border)); border: 1px solid var(--color-border); }

/* ---- Calendar ----------------------------------------------------------- */
.calendar-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap; margin-bottom: 16px;
}
.calendar-nav { display: inline-flex; align-items: center; gap: 10px; }
/* Строка контролов собирается в центр, а не растягивается по краям полотна.
   `space-between` хорош, пока групп две-три и они соразмерны; на «Открытиях»
   справа теперь живёт целое действие («Открылись в …», «Кроме», «Отметить
   открытие»), и разъехавшиеся по краям группы оставляли слева от даты дыру в
   треть экрана — строка читалась как незаполненная.

   Ровно центрировать саму дату нельзя: правая группа шире левой втрое, и
   «настоящий» центр сдвинул бы её за край. Поэтому центрируется вся сборка —
   гэпы одинаковые, пустота делится поровну по краям. */
.calendar-toolbar--center { justify-content: center; }

/* Дата ровно посередине панели (календарь графика). `space-between` делит
   пустоту между группами поровну, а не центрирует середину: слева четыре
   вкладки (418 px), справа одна кнопка (124) — и месяц уезжает вправо ровно на
   половину этой разницы, полтораста пикселей. Лечится равными боковыми
   колонками: у `flex: 1 1 0` базис нулевой, поэтому обе стороны получают
   одинаковую КОНЕЧНУЮ ширину (а не одинаковую долю остатка), и дата встаёт в
   геометрический центр карточки.
   Порог контейнерный, а не медиа-запрос: ширина панели зависит не от окна, а от
   того, свёрнуто ли меню, и от масштаба интерфейса — @container меряет саму
   панель и уже в масштабированных px, пересчитывать под --ui-scale не надо
   (ловушка проекта). 1120 = 418 × 2 (боковой колонке должно хватить на вкладки)
   + 254 (дата) + 28 (зазоры); ниже порога панель остаётся прежней, с переносом,
   а переключатель видов, как и раньше, прокручивается внутри себя. */
.calendar-toolbar--datecenter { container-type: inline-size; }
/* min-width/max-width — не про центр, а про телефон: обёртка сама флекс-элемент,
   и её автоминимум равен ширине вкладок. Без этого переключатель растягивал
   обёртку на всю ширину окна и вылезал из карточки, вместо того чтобы, как
   раньше, прокручиваться внутри себя (та же ловушка, что у .view-switch). */
.calendar-toolbar--datecenter > .calendar-toolbar__side { min-width: 0; max-width: 100%; }
@container (min-width: 1120px) {
  .calendar-toolbar--datecenter > .calendar-toolbar__side { flex: 1 1 0; }
}
/* Правая группа прижата к краю: колонка шире своей кнопки. */
.calendar-toolbar__side--end { justify-content: flex-end; }
/* 26/800 = 22px на экране: читается титулом месяца, но не раздувает панель до
   48px, как дали бы референсные 33px в строке с 32px-кнопками. */
.calendar-nav__label { font-size: 22px; font-weight: 800; letter-spacing: -0.015em; min-width: 200px; text-align: center; }

.calendar { width: 100%; }
.calendar__weekdays,
.calendar__week {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
}
/* Клетки разделены ЗАЗОРОМ, а не линией. Попытка нарисовать сплошную сетку
   хайрлайнами провалилась дважды: при zoom .85 линия имеет толщину 0,59
   экранного пикселя и размывается субпиксельным сглаживанием независимо от
   тона — на 9% её не видно вовсе, на 18% она грязная и клетку всё равно не
   держит. Заказчик сказал прямо: «календарь стал менее читаемым».
   Зазор плюс заливка дают границу, которую нельзя не увидеть, и это тот же
   язык блоков, что у карточек и плиток. Цена — колонка выходных распадается
   на столбик плашек; она остаётся читаемой по шапке недели. */
.calendar__weekdays { gap: 5px; margin-bottom: 2px; }
.calendar__weekday {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--color-muted); text-align: left; padding: 8px 0 10px 10px;
}
/* Заливки у субботы и воскресенья НЕТ — решение заказчика 03.09.2026: «давай
   суббота и воскресенье не будут тёмными». Пробовали колонкой от шапки донизу,
   как в референсе, — отвергнуто. Выходные читаются подписью в шапке, а в графике
   ПВЗ они к тому же далеко не всегда выходные: точки работают по своему
   расписанию, и нерабочий день помечен отдельно (--off в матрице). */
.calendar__week { gap: 5px; margin-bottom: 5px; }
/* Клетка — блок с заливкой и БЕЗ рамки: край рисует ступень заливки, как у
   карточек и плиток. Заливка на ступень ниже карточки, поэтому клетка читается
   вложенной, а не приподнятой.
   ВАЖНО: состояния дня (--gap, --warn) и ховер ниже кладутся СЛОЯМИ
   (background-image и box-shadow), а не background-color: цвет уже занят
   заливкой клетки и заливкой выходного, а третьего слоя фона у элемента нет. */
.calendar__cell {
  min-height: 124px; border-radius: var(--radius-md);
  border: 0; background: var(--color-surface-alt);
  padding: 9px 10px 11px; display: flex; flex-direction: column; gap: 6px;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
  /* Ячейка сама себе контейнер: содержимое ужимается по её ширине, а не по
     ширине экрана. Семь колонок на планшете дают ~68px на ячейку при вполне
     «десктопном» вьюпорте — по медиа-запросу такой случай не поймать. */
  container-type: inline-size;
  overflow: hidden;
}
/* Сетка рисуется в ДВА направления. Одной горизонтальной линии недели не
   хватило: заказчик сказал «календарь стал менее читаемым», и он прав —
   без вертикалей чип повисал между двумя числами, а неделя читалась не рядом
   клеток, а двумя полосами (числа отдельно, чипы отдельно).
   Линия ставится на левый край каждой клетки, кроме первой в неделе, — так
   внешняя рамка сетки не удваивается. */
/* Ховер — ВНУТРЕННЕЕ КОЛЬЦО, а не заливка. Заливкой он не годится: и выходной,
   и состояние дня уже заняли background-color и background-image, а третьего
   слоя фона у элемента нет — ховер-градиент просто ЗАМЕНИЛ БЫ красный на серый,
   то есть на самом важном дне гасил бы сигнал ровно в момент наведения. */
.calendar__cell:hover { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--color-primary) 55%, transparent); }
/* Хвост соседних месяцев (в августе — конец июля и начало сентября). Числа
   там были того же тона и веса, что и свои, и месяц, на который перешли
   стрелкой, ничем не выделялся: «27» и «1» читались одинаково (заказчик,
   25.08.2026). Разводим их двумя рычагами сразу — прозрачность всей ячейки
   плюс тон и вес самого числа ниже. */
/* opacity применялась ко всему поддереву как к группе и утаскивала вместе с
   фоном таблетки смен и чипы покрытия — а они кликаются: имя на них давало
   1,93-2,27:1 при кегле 11,5px, ниже даже порога для крупного текста.
   Гасим ровно то, что должно быть тише: само число. */
.calendar__cell--out { background: color-mix(in srgb, var(--color-text) 3%, transparent); }
/* Выходной день не залит — см. комментарий у .calendar__weekday выше. Правило
   оставлено пустым намеренно: класс в разметке есть, и следующий, кто захочет
   вернуть подсветку, должен сначала прочитать, почему её убрали. */
/* «Сегодня» переехало с клетки на ЧИСЛО. Рамка тут не только не по-новому, но и
   не работала: --today объявлен выше, чем --gap / --ok / --warn, и в сводных
   календарях сегодняшняя клетка получала рамку своего состояния, а от «сегодня»
   оставалось только кольцо. Цвет текста в кружке — var(--color-bg), а НЕ
   --color-on-primary: последний равен #FFFFFF в обеих темах, и белый на тёмном
   --color-primary #4187FF даёт 3,42:1 — провал AA. С --color-bg выходит 5,67:1
   в тёмной и 5,33:1 в светлой. */
.calendar__cell--today .calendar__date {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 26px; height: 26px; padding: 0 7px;
  border-radius: var(--radius-pill);
  background: var(--color-primary); color: var(--color-bg);
}
.calendar--coverage .calendar__cell--today .calendar__date,
.calendar--openings .calendar__cell--today .calendar__date { min-width: 22px; height: 22px; padding: 0 5px; }
/* Число дня — якорь ячейки: у своего месяца полный тон и максимальный вес. */
.calendar__date { font-size: 14.5px; font-weight: 800; color: var(--color-text); line-height: 1.1; }
.calendar__cell--out .calendar__date { font-weight: 700; color: color-mix(in srgb, var(--color-muted) 70%, transparent); }

.calendar__cell-shifts { display: flex; flex-direction: column; gap: 4px; }

.cal-pill {
  display: block; text-decoration: none; border-radius: var(--radius-sm);
  padding: 4px 7px; font-size: 11.5px; line-height: 1.3;
  border-left: 3px solid var(--color-primary);
  background: var(--color-surface-soft); color: var(--color-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: transform .12s var(--ease);
}
.cal-pill:hover { transform: translateX(1px); }
.cal-pill__time { font-weight: 700; }
.cal-pill__name { color: var(--color-muted); }
.cal-pill--planned   { border-left-color: var(--color-primary); }
.cal-pill--confirmed { border-left-color: var(--color-primary-dark); }
.cal-pill--worked    { border-left-color: var(--color-success); }
.cal-pill--no_show   { border-left-color: var(--color-danger); }
.cal-pill--cancelled { border-left-color: var(--color-muted); opacity: .6; text-decoration: line-through; }
.cal-more { font-size: 11px; font-weight: 700; color: var(--color-primary-dark); }

/* ---- Сводный календарь покрытия (сеть: смены vs норма ПВЗ) --------------- */
.calendar__cell--cov { text-decoration: none; color: inherit; cursor: pointer; }
.calendar__cell--cov:hover { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--color-primary) 55%, transparent); }
/* Состояния мешаются с transparent, а не с --color-surface: клетка теперь
   прозрачная, и непрозрачная заливка затёрла бы колонку выходных под собой.
   Доли подняты с 5-6% до 9-10% — рамки, которая раньше подпирала заливку, нет. */
/* Состояние дня рисуется background-IMAGE, а заливка выходного —
   background-COLOR. Раньше и то и другое было color, и состояние (объявленное
   ниже по файлу) стирало колонку выходных под собой. Теперь они складываются:
   красный день на субботе виден и красным, и субботой. */
.calendar__cell--gap {
  background-image: linear-gradient(
    color-mix(in srgb, var(--color-danger) 11%, transparent),
    color-mix(in srgb, var(--color-danger) 11%, transparent)
  );
}
.calendar__cell-top { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.cal-cov__total {
  font-size: 11px; font-weight: 800; font-variant-numeric: tabular-nums;
  color: var(--color-primary-dark); background: var(--color-primary-soft);
  border-radius: var(--radius-pill); padding: 1px 7px;
}
.cal-cov__legend { margin-top: 10px; font-size: 12.5px; color: var(--color-muted); }

/* Открыто/закрыто в ячейке сводного календаря */
/* «Всё хорошо» не помечаем ничем — и это осознанно, а не потеря при снятии
   рамок: управленец ищет проблему, а не подтверждение. Чем тише спокойный день,
   тем быстрее находится красный. */
.cal-oc { display: flex; flex-direction: column; gap: 4px; margin-top: auto; }
.cal-oc__chip {
  display: inline-flex; align-items: center; gap: 5px; align-self: flex-start;
  font-size: 11.5px; font-weight: 700; line-height: 1.3;
  border-radius: var(--radius-pill); padding: 3px 9px;
  white-space: nowrap; max-width: 100%;
}
.cal-oc__chip svg { width: 11px; height: 11px; flex: none; }
.cal-oc__chip b { font-weight: 800; font-variant-numeric: tabular-nums; }
.cal-oc__chip em { font-style: normal; font-weight: 700; }
.cal-oc__chip--open { background: var(--color-success-soft); color: var(--color-success); }
/* Белый цвет был зашит литералом, а --color-danger в тёмной теме это светло-
   розовый #F57A83: белым выходило 2,63:1 при кегле 11,5px/700 — провал AA на
   той самой метке, ради которой открывают сводный календарь.
   var(--color-bg) даёт 7,39:1 в тёмной и 5,14:1 в светлой (как было). */
.cal-oc__chip--closed { background: var(--color-danger); color: var(--color-bg); }
.cal-oc__none { font-size: 11px; color: var(--color-muted); }

/* ---- Календарь открытий (Контроль): вовремя / опоздали / не открыты ------ */
/* Те же клетки, что и в покрытии смен, но три степени тревоги вместо двух:
   опоздание — не провал, но и не «всё хорошо», поэтому у него свой, жёлтый
   чип, а не оттенок зелёного. */
.cal-oc__chip--late { background: var(--color-warning-soft); color: var(--color-warning); }
.cal-oc__chip--review { background: var(--color-primary-soft); color: var(--color-primary-dark); }
/* «Нет данных» — не тревога, а пробел: приглушённый, чтобы не спорить с
   красным «не открыты», но и не сливался с фоном клетки. */
.cal-oc__chip--nodata { background: var(--color-surface-soft); color: var(--color-muted); box-shadow: inset 0 0 0 1px var(--color-border); }
.badge--nodata { background: var(--color-surface-soft); color: var(--color-muted); box-shadow: inset 0 0 0 1px var(--color-border); }
/* Строка без события никуда не ведёт — гасим и курсор-указатель от data-row-href. */
.table tbody tr.is-nodata { cursor: default; }
.table tbody tr.is-nodata td { color: var(--color-muted); }
.calendar__cell--warn {
  background-image: linear-gradient(
    color-mix(in srgb, var(--color-warning) 12%, transparent),
    color-mix(in srgb, var(--color-warning) 12%, transparent)
  );
}
/* Клетка открытий выше клетки покрытия: чипов может быть до четырёх. */
.calendar--openings .calendar__cell { min-height: 124px; }
/* Телефон: клетка ужимается до ~46px, и «всего за день» у сети на 68 точек
   обрезался до одной цифры («65» → «6») — хуже, чем его отсутствие. Числа,
   ради которых сюда смотрят, остаются в чипах. Подпись пустого дня там же
   рвалась на «нет событ», поэтому тоже уходит. */
/* Без привязки к --openings: тот же .cal-cov__total выводит и календарь
   покрытия, и на телефоне «65» обрезалось до «6» — ровно так, как описывает
   комментарий выше. Оба календаря показывают одно и то же и должны вести себя
   одинаково. */
@container (max-width: 78px) {
  .cal-cov__total,
  .cal-oc__none { display: none; }
}

/* Прыжок на дату: стрелки ‹ › листают соседние дни, поле — любой день месяца
   (отправляется само, как «Показывать по» в подвале списка). Поле месяца
   (type="month") устроено так же и живёт в шапке месячных видов графика. */
.day-jump { display: inline-flex; align-items: center; }
.day-jump__input { max-width: 176px; font-weight: 700; text-align: center; }
/* Поле вместо подписи в шапке календаря: ширину держим прежней (было
   .calendar-nav__label с min-width: 168px), иначе строка контролов дёргалась
   бы на каждом перелистывании — «Август» короче «Сентября». */
/* Поле прыжка на месяц в графике играет ту же роль, что текстовый заголовок в
   открытиях, — и должно выглядеть так же. Но остаётся ПОЛЕМ: в этом его
   ценность, прыжок на любой месяц одним движением.
   ШИРИНА: я поднял кегль до 26px, оставив прежние 210px, и «сентябрь 2026 г.»
   стало обрезаться (заказчик: «месяц не помещается»). Считаем честно: самый
   длинный месяц «сентябрь 2026 г.» это 16 знаков, при 22px и Onest 800 это
   ~176px, плюс нативная иконка календаря ~24px и поля 12px = 212. Берём 240
   с запасом на локали, где месяц длиннее. */
.calendar-nav .day-jump__input {
  min-width: 240px; font-size: 22px; font-weight: 800; letter-spacing: -0.015em;
  border: 0; background: transparent; padding: 0 6px;
}

.calendar-toolbar__side { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Карточка-панель: внутри только органы управления, поэтому она прижата по
   вертикали и не отбирает у полотна высоту целой карточки. */
.card--bar { padding: 12px 16px; gap: 0; }
.card--bar .calendar-toolbar { margin-bottom: 0; }

/* Плашка «страница сужена фильтром из ссылки». Панели фильтров на странице
   нет, поэтому единственный способ снять фильтр — кнопка отсюда. */
.filter-note {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 14px; border-radius: var(--radius-md);
  background: var(--color-primary-soft);
  border: 1px solid color-mix(in srgb, var(--color-primary) 26%, transparent);
  color: var(--color-primary-dark);
}
.filter-note svg { width: 16px; height: 16px; flex: none; }
.filter-note__text { font-size: 13px; font-weight: 600; margin-right: auto; }
.filter-note__text b { font-weight: 800; }

/* Легенда под сводным календарём: образцы чипов + подсказка, а не абзац текста. */
.cal-legend {
  display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center;
  margin-top: 14px; padding-top: 12px;
  border-top: 1px solid var(--color-divider);
}
.cal-legend__item { display: inline-flex; align-items: center; gap: 8px; }
.cal-legend__item .cal-oc__chip { align-self: center; }
.cal-legend__text { font-size: 12.5px; color: var(--color-muted); line-height: 1.45; }
.cal-legend__item--hint svg { width: 15px; height: 15px; flex: none; color: var(--color-muted); }
@media (max-width: 819px) {
  .cal-legend { flex-direction: column; align-items: flex-start; gap: 8px; }
}

/* Компактный режим чипов — по ширине САМОЙ ячейки (см. container-type выше),
   поэтому одинаково срабатывает и на телефоне, и на планшете, и на узком
   окне десктопа. Легенда и тулбары матрицы живут вне ячеек и подписи
   сохраняют. Три ступени: сначала уходит слово «со сменами», потом иконка,
   потом чип сжимается до числа. */
@container (max-width: 132px) {
  .cal-oc__chip em { display: none; }
}
@container (max-width: 104px) {
  .cal-oc__chip { padding: 2px 7px; gap: 4px; }
  .cal-oc__chip svg { display: none; }
}
@container (max-width: 78px) {
  .cal-oc__chip { padding: 1px 6px; font-size: 11px; }
  .calendar__date { font-size: 11.5px; }
  .cal-cov__total { padding: 1px 5px; }
}

/* ---- Переключатель видов графика (календарь ↔ таблица) ------------------- */
/* Переключатель НИКОГДА не разгоняет страницу вбок: вкладок в разделе стало
   четыре (08.2026 — добавился «Отчёт»), и на 430px четвёртая вылезала из
   карточки, утаскивая за собой полотно. Прокрутка внутри самого переключателя
   вместо магического брейкпоинта — она верна на любой ширине, а на широком
   экране просто никогда не срабатывает. */
.view-switch {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  max-width: 100%; overflow-x: auto; scrollbar-width: none;
  /* min-width:0 обязателен: переключатель — flex-элемент тулбара, а его
     авто-минимум равен ширине содержимого, и одного max-width не хватает —
     ужаться ниже своих вкладок он всё равно не может. */
  min-width: 0;
}
.view-switch::-webkit-scrollbar { display: none; }
/* flex: none — иначе вкладки в прокручиваемой строке ужимаются до нечитаемого. */
.view-switch__btn { flex: none; }
.view-switch__btn {
  display: inline-flex; align-items: center; gap: 7px;
  border: 0; text-decoration: none; cursor: pointer;
  background: transparent; color: var(--color-muted);
  font-weight: 700; font-size: 13px; padding: 7px 13px;
  border-radius: calc(var(--radius-md) - 3px);
  transition: color .16s var(--ease), background .16s var(--ease), box-shadow .16s var(--ease);
}
.view-switch__btn svg { width: 15px; height: 15px; }
.view-switch__badge { margin-left: 1px; padding: 1px 7px; font-size: 11px; }
.view-switch__btn:hover { color: var(--color-text); }
.view-switch__btn.is-active {
  background: var(--color-surface); color: var(--color-primary-dark);
  box-shadow: var(--shadow-xs);
}

/* Переключатель видов на странице плоского списка (там нет calendar-toolbar). */
.list-view-switch { margin-bottom: 16px; }

/* Узкий телефон (360px и уже — Android-минимум): вкладки ужимаются, чтобы в
   прокрутку уходило как можно меньше. Сама прокрутка объявлена выше и работает
   на любой ширине. */
@media (max-width: 400px) {
  .view-switch__btn { padding: 7px 10px; gap: 5px; font-size: 12.5px; }
  .view-switch__btn svg { width: 14px; height: 14px; }
}

/* ---- Назначение смен (прямое, без генерации) ---------------------------- */
.assign { display: flex; flex-direction: column; gap: var(--gap-cards); }
/* align-items: start, а не end: при ошибке под одним полем «end» задирал
   соседние поля вверх и строка разъезжалась. Сверху же поля стоят ровно
   всегда — подпись ошибки просто уходит вниз, ничего не двигая. */
/* Пять полей жёсткой строкой требовали ~700px и на планшете вылезали за экран.
   auto-fit раскладывает их по месту сам — это не зависит от ui-scale, в
   отличие от медиа-запросов (они меряют реальный вьюпорт, а раскладка живёт
   в масштабированном). Полю ПВЗ — двойная колонка: там длинные адреса. */
.assign-params__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px 14px; align-items: start;
}
.assign-params__grid > :first-child { grid-column: span 2; }
@media (max-width: 819px) { .assign-params__grid { grid-template-columns: 1fr 1fr; } }
.assign-params .field__hint { margin-top: 10px; }
.assign-empty { text-align: center; padding: 26px; }
.assign-board__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; }
.assign-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
/* Без горизонтальных отступов: карточка/диалог уже задали поле содержимого, а
   лишние 8px уводили имена и бейджи из одной колонки с заголовком секции. */
/* Строка выбора сотрудника. Здесь было главное неудобство доски: состояния
   «отмечен» не существовало вовсе — единственным признаком был нативный чекбокс
   17px, то есть 14,5 экранных при zoom .85. Человек отмечает бригаду из шести и
   глазами не может проверить набор перед отправкой, а цена ошибки — лишняя смена
   × число дат периода (за месяц это 30 лишних смен).
   Отрицательные поля по бокам нужны, чтобы заливка отмеченной строки уходила за
   поля списка и читалась строкой, а не полосой посередине. */
.assign-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-height: 52px;
  padding: 8px 12px; margin-inline: -12px;
  border-radius: var(--radius-md);
  border-bottom: 1px solid var(--color-divider);
  transition: background-color .14s var(--ease);
}
.assign-row:last-child { border-bottom: 0; }
.assign-row:hover { background: color-mix(in srgb, var(--color-text) 5%, transparent); }
/* 34%, а не готовый --color-primary-soft: soft даёт 1,24:1 к карточке — ниже
   порога различимости. 34% дают 1,66:1 в тёмной и 1,70:1 в светлой. */
.assign-row:has(input:checked) { background: color-mix(in srgb, var(--color-primary) 34%, var(--color-surface)); }
.assign-row:has(input:checked) .assign-row__name { color: var(--color-heading); font-weight: 750; }
/* Ярлык растянут на всю строку: раньше он оборачивал только чекбокс и имя, а
   правая половина строки с бейджами была мертва — цель клика ~200px из ~500. */
.assign-row__pick { display: flex; align-items: center; gap: 11px; cursor: pointer; font-weight: 600; flex: 1 1 auto; min-width: 0; }
.assign-row__pick input { width: 20px; height: 20px; accent-color: var(--color-primary); flex: none; }
.assign-row.is-assigned { opacity: .72; }
.assign-row.is-assigned .assign-row__pick { cursor: default; }
/* Правило is-busy ниже обязано идти ПОСЛЕ :has(input:checked), иначе отметка
   занятого визуально не сработает. */
.assign-row.is-busy .assign-row__name { color: var(--color-text); }
/* «Свободен» — состояние по умолчанию, у него не должно быть такой же плашки,
   как у исключений «занят: КОД» и «уже стоит»: в списке из двенадцати человек
   глаз обрабатывал двенадцать серых плашек ради двух значащих. */
.assign-row__state .badge-muted,
.assign-row__state .badge-secondary { background: transparent; padding-inline: 0; color: var(--color-faint); font-weight: 600; }
.badge-muted { background: var(--color-surface-soft); color: var(--color-muted); }
.assign-board__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--color-border); flex-wrap: wrap;
}
.assign-board__hint { color: var(--color-muted); font-size: 12.5px; max-width: 62ch; }
/* Кнопка «Назначить» в развёрнутой ячейке дня матрицы. */
.mx-assign { margin-top: 8px; }

/* Повторение на период («каждый день» / «2 через 2» / «по дням недели»). */
.assign-repeat {
  display: grid; grid-template-columns: minmax(220px, 1fr) minmax(160px, 1fr) 2fr;
  gap: 12px 14px; align-items: start; margin-top: 14px; padding-top: 14px;
  border-top: 1px solid var(--color-divider);
}
@media (max-width: 819px) { .assign-repeat { grid-template-columns: 1fr; } }
.assign-repeat__weekdays > div { display: flex; flex-wrap: wrap; gap: 8px 14px; padding-top: 6px; }
.assign-repeat__weekdays label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; font-weight: 600; cursor: pointer;
}
.assign-repeat__weekdays input { width: 15px; height: 15px; accent-color: var(--color-primary); }

/* Секция «Сотрудники с других ПВЗ» — кросс-назначение с предупреждением. */
.assign-others__summary {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  font-weight: 700; font-size: 15px; list-style: none;
}
.assign-others__summary::-webkit-details-marker { display: none; }
.assign-others__summary::after {
  content: "▾"; margin-left: auto; color: var(--color-muted);
  transition: transform .16s var(--ease);
}
.assign-others[open] .assign-others__summary::after { transform: rotate(180deg); }
.assign-others > .field__hint { margin-top: 10px; }
.assign-others__search { margin: 12px 0 4px; max-width: 340px; }
.assign-others__empty { padding: 10px 0 0; }
.assign-row--transfer .assign-row__state { display: inline-flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }

/* Подвал с кнопкой — отдельная карточка (доска и «чужие» между параметрами и ним).
   flex-direction обязателен: .card объявляет column, и без переопределения
   подсказка с кнопкой вставали друг под другом по центру вместо строки. */
/* Липкий подвал. Кнопка «Назначить выбранных» лежит ПОД списком чужих
   сотрудников, а он ничем не ограничен: кандидатов на перевод отдают без limit,
   и у собственника сети из сорока точек это сотня строк. После отметки надо было
   прокрутить всё обратно вниз. z-index именно 12 — по правилу проекта липкая
   панель внутри страницы обязана быть ниже шапки (34); непрозрачный фон и тень
   даёт сама карточка. */
.assign-foot {
  position: sticky; bottom: 0; z-index: 12;
  display: flex; flex-direction: row; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
}
/* Потолок списка чужих: 360px = 306 экранных ≈ семь строк по 52px. Свою команду
   точки (обычно 3-6 человек) не ограничиваем — она должна быть видна целиком. */
.assign-others .assign-list { max-height: 360px; overflow: auto; }

/* ---- Мастер «Настроить график ПВЗ»: 3 шага, звенья А/Б, сводка ------------ */
.wizard { display: flex; flex-direction: column; gap: 16px; max-width: 1080px; }
/* [hidden] обязан скрывать шаги: display из .form-grid/.form-field иначе
   перебивает UA-правило [hidden]{display:none} (та же ловушка, что в .pattern-form). */
.wizard [hidden] { display: none !important; }

.wiz-steps { list-style: none; display: flex; gap: 10px; padding: 0; margin: 0; flex-wrap: wrap; }
.wiz-steps__item {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 8px 14px; border-radius: var(--radius-pill);
  border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-muted); font-size: 13px; font-weight: 700;
  transition: border-color .16s var(--ease), color .16s var(--ease), background .16s var(--ease);
}
.wiz-steps__item.is-active {
  border-color: var(--color-primary); color: var(--color-primary-dark);
  background: var(--color-primary-soft);
}
.wiz-steps__item.is-done {
  color: var(--color-success); cursor: pointer;
  border-color: color-mix(in srgb, var(--color-success) 35%, var(--color-border));
}
.wiz-steps__num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%; flex: none;
  background: var(--color-surface-soft); font-size: 12px; font-weight: 800;
}
.wiz-steps__item.is-active .wiz-steps__num { background: var(--color-primary); color: var(--color-on-primary); }
.wiz-steps__item.is-done .wiz-steps__num { background: var(--color-success-soft); color: var(--color-success); }

.wiz-panel { display: flex; flex-direction: column; gap: 14px; }
.wiz-panel__error { margin: 0; font-size: 13px; font-weight: 700; color: var(--color-danger); }
.wiz-nav { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 4px; }
.wiz-nav__spacer { flex: 1; }
.wiz-field--pvz { max-width: 460px; }

/* Сотрудники пункта (HTMX-партиал): карточки-чекбоксы. */
.wiz-emp-box {
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface-soft); padding: 12px;
  max-height: 340px; overflow: auto;
}
.wiz-emp { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 8px; }
.wiz-emp__row {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 9px 12px; border: 1px solid var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface);
  transition: border-color .14s var(--ease), background .14s var(--ease);
}
.wiz-emp__row:hover { border-color: var(--color-primary); }
.wiz-emp__row:has(input:checked) { border-color: var(--color-primary); background: var(--color-primary-soft); }
.wiz-emp__row input { flex: none; }
.wiz-emp__name { font-weight: 700; font-size: 13.5px; }
.wiz-emp__hint { margin-left: auto; font-size: 11.5px; color: var(--color-muted); white-space: nowrap; }
.wiz-emp__hint--none { font-style: italic; }
.wiz-emp__empty { margin: 0; padding: 8px 4px; font-size: 13px; color: var(--color-muted); }
.wiz-emp__bulk { margin-bottom: 10px; }

/* Ритм: две карточки-варианта (цикл / дни недели). */
.wiz-kind { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; max-width: 680px; }
.wiz-kind__card {
  position: relative; display: flex; flex-direction: column; gap: 4px;
  padding: 14px 16px; cursor: pointer;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface);
  transition: border-color .14s var(--ease), background .14s var(--ease), box-shadow .14s var(--ease);
}
.wiz-kind__card:hover { border-color: var(--color-primary); }
.wiz-kind__card:has(input:checked) {
  border-color: var(--color-primary); background: var(--color-primary-soft);
  box-shadow: var(--ring);
}
.wiz-kind__card input { position: absolute; opacity: 0; pointer-events: none; }
.wiz-kind__title { font-size: 14px; font-weight: 800; }
.wiz-kind__desc { font-size: 12.5px; color: var(--color-muted); line-height: 1.45; }

/* Дни недели — чипы-пилюли вместо голых чекбоксов. */
.wiz-days ul { list-style: none; display: flex; gap: 8px; flex-wrap: wrap; margin: 6px 0 0; padding: 0; }
.wiz-days label {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  min-width: 52px; padding: 8px 12px; cursor: pointer;
  border: 1px solid var(--color-border); border-radius: var(--radius-pill);
  background: var(--color-surface); font-size: 13px; font-weight: 700; color: var(--color-muted);
  transition: border-color .14s var(--ease), background .14s var(--ease), color .14s var(--ease);
}
.wiz-days label:hover { border-color: var(--color-primary); }
.wiz-days label:has(input:checked) {
  border-color: var(--color-primary); background: var(--color-primary-soft);
  color: var(--color-primary-dark);
}
.wiz-days input { position: absolute; opacity: 0; pointer-events: none; }

/* Звенья цикла: строки «сотрудник → А/Б». */
.wiz-slots { display: flex; flex-direction: column; gap: 8px; max-width: 560px; }
.wiz-slot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 12px; border: 1px solid var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface-soft);
}
.wiz-slot__name { font-weight: 700; font-size: 13.5px; }
.wiz-slot__toggle {
  display: inline-flex; padding: 3px; gap: 2px; flex: none;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
}
.wiz-slot__toggle button {
  border: 0; background: transparent; cursor: pointer;
  color: var(--color-muted); font-weight: 700; font-size: 12px;
  padding: 5px 12px; border-radius: var(--radius-pill);
  transition: background .14s var(--ease), color .14s var(--ease);
}
.wiz-slot__toggle button.is-on { background: var(--color-primary); color: var(--color-on-primary); }

/* Сводка перед запуском. */
.wiz-summary-box {
  border: 1px dashed var(--color-border); border-radius: var(--radius-md);
  padding: 14px 16px; background: var(--color-surface-soft);
}
.wiz-summary-box__title { margin: 0 0 10px; font-size: 13px; font-weight: 800; }
.wiz-summary { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; margin: 0; }
.wiz-summary dt { color: var(--color-muted); font-size: 12.5px; font-weight: 700; }
.wiz-summary dd { margin: 0; font-size: 13px; font-weight: 650; }

@media (max-width: 760px) {
  .wiz-emp { grid-template-columns: 1fr; }
  .wiz-emp__hint { white-space: normal; }
  .wiz-summary { grid-template-columns: 1fr; gap: 2px; }
  .wiz-summary dd { margin-bottom: 8px; }
  .wiz-slot { flex-direction: column; align-items: flex-start; gap: 8px; }
}

/* ---- Табличный график: матрица ПВЗ × даты -------------------------------- */
/* Читается как «простыня» из Google Sheets, которой пользуются администраторы,
   но в тонах системы: цвет несёт ячейка (кто закрывает день), а не заливка
   в упор. Зелёный — свой сотрудник, янтарный — замена, красный — никого. */
/* Фильтры — ОДИН блок, а не россыпь контролов в общем ряду (заказчик,
   03.09.2026: «у таблицы не красивые фильтры, они стоят как попало»).
   Причина беспорядка была в раскладке: контролы лежали прямыми детьми тулбара
   с justify-content: space-between, и при переносе строки разъезжались к краям
   непредсказуемо — на одной ширине окна поиск оказывался слева, на другой у
   правого края. Теперь блок всегда занимает СВОЮ строку целиком (flex-basis
   100%), лежит на собственной подложке и внутри выровнен по левому краю, а
   тумблер и «Развернуть» прижаты вправо. Ряд перестал зависеть от того,
   поместился он или нет. */
.matrix-toolbar-side {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  flex: 1 1 100%;
  padding: 10px 12px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-divider);
  background: var(--color-surface-alt);
}
/* Тумблер «показать закрытые» и «Развернуть» — это не фильтры, а режимы
   показа. Уводим их к правому краю, чтобы левая часть читалась одним рядом
   фильтров, а не смесью из фильтров и переключателей. */
.matrix-toolbar-side .mx-closedtoggle { margin-left: auto; }
/* Органов управления в тулбаре графика больше, чем в остальных: на 1440px
   строка переносится. При space-between перенос разводил переключатель видов и
   стрелки месяца по краям пустой строки — они должны стоять рядом, а правый
   блок уезжать вниз целиком. Правило заперто в :has, чтобы не трогать тулбары
   других разделов. */
.calendar-toolbar:has(.matrix-toolbar-side) { justify-content: flex-start; }
/* margin-left: auto здесь больше не нужен — блок занимает всю строку. */
/* Кнопка «развернуть»: иконка ведёт себя как в остальных кнопках тулбара. */
.mx-fullbtn svg { width: 15px; height: 15px; }
.matrix-filter { display: inline-flex; align-items: center; gap: 8px; }
.matrix-filter .input { max-width: 240px; }
/* Выбор ПВЗ: initPvzPick подменяет select оболочкой .pvz-pick, и без своей
   ширины она сжимается по тексту кнопки — «Выбрано: 2» превращалось в
   «Выбра…». Правило-близнец уже есть у .filters-row. */
.matrix-filter > .pvz-pick { flex: 0 1 auto; min-width: 168px; }
/* Порядок строк: подписи длинные («Область → город → улица»), и ужимать селект
   до одной стрелки нельзя — на телефоне он оставался пустым квадратом. Даём
   ему свою долю ширины и разрешаем строке переноситься: три контрола в 375px
   не помещаются и не должны. */
.matrix-filter { flex-wrap: wrap; }
.matrix-filter > .matrix-order { flex: 1 1 150px; min-width: 132px; }
/* Тулбар — компактный контекст: кнопки в нём идут btn-sm, поэтому и поле
   ужимаем до той же высоты. Иначе полноразмерный селект (42px) торчал над
   кнопками (31px) и строка выглядела «лесенкой». */
.calendar-toolbar .input,
.matrix-filter .input { min-height: var(--control-h-sm); padding-top: 6px; padding-bottom: 6px; }
/* Заглушка выбора ПВЗ до сборки виджета — той же компактной высоты, что и
   будущая кнопка: иначе строка фильтров всё равно дёргалась бы на 7 px
   (см. html.has-js select[data-pvz-pick][multiple]). */
.calendar-toolbar,
.matrix-filter { --pick-collapsed-h: var(--control-h-sm); }
.calendar-toolbar input[type="date"].input,
.calendar-toolbar input[type="time"].input { height: var(--control-h-sm); }
/* Переключатель видов — последний элемент той же строки, ужимаем и его
   (на других страницах .view-switch остаётся полноразмерным). */
.calendar-toolbar .view-switch { height: var(--control-h-sm); align-items: stretch; }
.calendar-toolbar .view-switch__btn { padding-top: 0; padding-bottom: 0; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Легенда-ключ к цветам + счётчики за видимое окно.
   Осей три (покрытие / оформление / оплата), и они независимы: одна клетка
   может нести все три сразу. Поэтому легенда разложена по осям и каждая
   подписана СВОИМ элементом — иначе семь образцов в строку читаются как один
   набор цветов, и понять, что чем задаётся, нельзя. */
.mx-legend {
  display: flex; align-items: flex-start; gap: 10px 22px; flex-wrap: wrap;
  margin: 0 0 12px;
}
/* Строка счётчиков — то, ради чего на легенду вообще смотрели. Стоит над
   таблицей всегда, подписи-заголовка у неё нет: три чипа со своими словами
   («Свой сотрудник», «Замена», «Никто не назначен») объясняют себя сами. */
.mx-legend--totals { margin: 0 0 10px; }
.mx-legend--totals .mx-legend__axis { flex-direction: row; align-items: center; }
/* Раскрытие с объяснениями и подсказкой про мышь. Раньше всё это лежало
   развёрнутым и съедало ~110px перед первой строкой данных — экран начинался
   не с графика. Читают такое один раз. */
.mx-help { margin: 0 0 12px; }
.mx-help__summary {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-size: 12.5px; font-weight: 650; color: var(--color-muted);
  padding: 5px 11px; border-radius: var(--radius-pill);
  list-style: none; transition: background-color .14s var(--ease), color .14s var(--ease);
}
.mx-help__summary::-webkit-details-marker { display: none; }
.mx-help__summary::after {
  content: "▾"; font-size: 10px; line-height: 1;
  transition: transform .16s var(--ease);
}
.mx-help[open] .mx-help__summary::after { transform: rotate(180deg); }
.mx-help__summary:hover { background: var(--color-surface-soft); color: var(--color-text); }
.mx-help__summary:focus-visible { outline: none; box-shadow: var(--ring-soft); }
.mx-legend--help { margin: 12px 0 0; }
.mx-help .mx-panhint { margin: 10px 0 0; }
.mx-legend__axis { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
/* Вертикальной черты между осями здесь нет намеренно: оси переносятся по
   ширине, а CSS не знает, встали ли соседи в одну строку, — `border-left` у
   перенесённой оси повис бы палкой в начале новой строки. Разделяют подпись
   и увеличенный отступ колонок, этого достаточно. */
.mx-legend__cap {
  font-size: 10.5px; font-weight: 800; letter-spacing: 0.02em;
  color: var(--color-muted); text-transform: uppercase;
}
.mx-legend__items { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.mx-legend__item {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px; border-radius: var(--radius-pill);
  border: 1px solid var(--color-border); background: var(--color-surface);
  font-size: 12px; font-weight: 650; color: var(--color-muted);
}
.mx-legend__item b {
  font-size: 12.5px; font-weight: 800; color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.mx-legend__swatch {
  width: 11px; height: 11px; border-radius: 3px; flex: none;
  background: var(--_mx-tint); border: 1px solid var(--_mx-line);
}
.mx-legend__item--regular     { --_mx-tint: var(--color-success-soft); --_mx-line: var(--color-success); }
.mx-legend__item--replacement { --_mx-tint: var(--color-warning-soft); --_mx-line: var(--color-warning); }
.mx-legend__item--gap         { --_mx-tint: var(--color-danger-soft);  --_mx-line: var(--color-danger); }
/* Отметки оплаты: образец повторяет заливку плашки в клетке, чтобы легенда
   читалась как ключ, а не как отдельный набор цветов. */
.mx-legend__item--pay-upfront {
  --_mx-tint: color-mix(in srgb, var(--color-cyan) 24%, var(--color-surface));
  --_mx-line: var(--color-cyan);
}
.mx-legend__item--pay-after {
  --_mx-tint: color-mix(in srgb, var(--color-magenta) 24%, var(--color-surface));
  --_mx-line: var(--color-magenta);
}

/* --- Оформление сотрудника: вторая, независимая ось цвета ------------------
   Кант слева у фамилии уже занят вопросом «свой или замена», а заказчику нужно
   видеть и другое — кто выходит без оформления. Поэтому оформление показываем
   точкой перед фамилией: две шкалы не спорят и читаются одновременно.
   Оформленные и те, у кого тип занятости не заполнен, точки не получают —
   красить по догадке хуже, чем не красить (см. employment_kind). */
.mx-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  display: inline-block; vertical-align: middle;
  margin-right: 5px; margin-bottom: 1px;
}
.mx-dot--official,
.mx-dot--unknown { display: none; }
.mx-dot--unofficial { background: var(--color-violet); }
/* Разовый выход — не «неоформленный сотрудник», а человек вне штата вообще:
   полый кружок отличает его от неоформленного и без опоры на один лишь цвет. */
.mx-dot--casual {
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--color-muted);
}
.mx-legend__item--emp .mx-dot { display: inline-block; margin-right: 2px; }

/* Высота простыни = экран минус липкая шапка приложения. Жёсткий потолок в
   780px отсюда убран: при плотности интерфейса 0.8 он срабатывал раньше
   вычисленной высоты и оставлял таблицу маленькой на любом мониторе. vh здесь
   умножается на --vu — единицы vh не учитывают zoom корня (см. ui-scale). */
.sched-matrix__wrap {
  /* Вычиталось 118px, а над простынёй лежит ~325: шапка приложения 64 + поле
     страницы 28 + заголовок 55 + gap 20 + поля карточки 24 + тулбар 36 + gap 16
     + легенда 54 + подсказка 28. Из-за этого на экране было ДВЕ вертикальные
     прокрутки, и, прокрутив страницу, человек уводил липкую шапку дней под шапку
     приложения. Нижний порог 360px оставлен. */
  overflow: auto; max-height: max(calc(100vh * var(--vu) - 300px), 360px);
  /* Линия здесь ОБЯЗАТЕЛЬНА и снимать её «как у карточек» нельзя: обёртка лежит
     на карточке того же цвета, и без линии край области прокрутки исчезает —
     непонятно, где кончается таблица. Правило «край рисует ступень заливки»
     работает только там, где ступень есть. */
  border: 1px solid var(--color-divider); border-radius: var(--radius-lg);
  background: var(--color-surface);
  scrollbar-gutter: stable;
}
/* Примагничивание по дате: колонка дня доводится до края липкой колонки ПВЗ,
   чтобы прокрутка никогда не замирала на «половинке» дня. Proximity, а не
   mandatory: мандатное притяжение отбирает у мыши свободу пролистать месяц.
   scroll-padding-left = ширина липкой первой колонки, иначе день «примагнитится»
   под неё. Точка притяжения одна на колонку — на ячейке шапки. */
.sched-matrix__wrap:not(.sched-matrix__wrap--day) {
  scroll-snap-type: x proximity;
  /* Держать в паре с min-width липкой колонки (.sched-matrix__pvz), иначе
     день «примагнитится» под неё. */
  scroll-padding-left: 190px;
}
.sched-matrix__wrap:not(.sched-matrix__wrap--day) .sched-matrix__day {
  scroll-snap-align: start;
}
@media (prefers-reduced-motion: reduce) {
  .sched-matrix__wrap { scroll-snap-type: none; }
}
/* Таблицу везут с зажатой кнопкой мыши (см. initMatrixPan). Курсор-«кулак»
   ставим только на время движения: в покое клетки кликабельны, и рука
   вместо указателя обещала бы не то. Выделение текста на время движения
   гасим — иначе вместо прокрутки таблица закрашивается синим. */
body.is-matrix-panning,
body.is-matrix-panning .sched-matrix__wrap { cursor: grabbing; }
body.is-matrix-panning { user-select: none; -webkit-user-select: none; }
body.is-matrix-panning .sched-matrix a,
body.is-matrix-panning .sched-matrix button { cursor: grabbing; }

/* Подсказка про способы двигаться по таблице. Показывает JS и только там,
   где есть мышь: пальцем простыню возят и без подсказок. */
.mx-panhint { margin: -4px 0 10px; font-size: 12px; color: var(--color-muted); }
.mx-panhint[hidden] { display: none; }
.mx-panhint kbd {
  font: inherit; font-weight: 700; color: var(--color-heading);
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  border-radius: 5px; padding: 1px 5px;
}
.sched-matrix {
  border-collapse: separate; border-spacing: 0; width: 100%;
  font-variant-numeric: tabular-nums;
}
.sched-matrix th, .sched-matrix td {
  /* Горизонталь — ровно как в .table, чтобы простыня и реестры говорили одним
     языком. Вертикаль тише: 31 колонка одинаковой силы превращается в частокол.
     Старый --color-border в тёмной теме давал 1,071 и 1,045 — сетки почти не
     было видно вовсе. */
  border-bottom: 1px solid var(--color-divider);
  border-right: 1px solid color-mix(in srgb, var(--color-text) 4%, transparent);
}
/* Граница НЕДЕЛИ — без единой правки разметки: понедельник это первая клетка
   после выходного. 31 одинаковый хайрлайн давал клетку, но не давал недели, и
   посчитать «это какая неделя» было нельзя. */
.sched-matrix__cell.is-weekend + .sched-matrix__cell:not(.is-weekend),
.sched-matrix__day.is-weekend + .sched-matrix__day:not(.is-weekend) {
  border-left: 1px solid var(--color-divider);
}
/* Липкая шапка рисует свою нижнюю линию тенью — собственная рамка ей не нужна.
   Ограничение thead обязательно: раньше здесь стоял голый `tr:last-child`, и
   правило снимало рамку у последней строки КАЖДОЙ группы (<tbody> на город) —
   последний пункт слипался со следующим заголовком. */
.sched-matrix thead th { border-bottom: 0; }

/* --- Первая колонка и шапка: липкие, чтобы не терять контекст при прокрутке. */
.sched-matrix__corner,
.sched-matrix__pvz {
  position: sticky; left: 0; z-index: 2;
  background: var(--color-surface);
  min-width: 190px; max-width: 260px;
  padding: 7px 12px; text-align: left; vertical-align: middle;
  box-shadow: 1px 0 0 var(--color-border);
}
.sched-matrix__corner {
  z-index: 4; top: 0;
  font-size: 11px; font-weight: 800; text-transform: uppercase;
  letter-spacing: .06em; color: var(--color-muted);
}
.sched-matrix thead th {
  position: sticky; top: 0; z-index: 3;
  background: var(--color-surface);
  box-shadow: 0 1px 0 var(--color-border);
}
/* Угол липкий по обеим осям, поэтому обязан лежать выше и шапки, и первой
   колонки. z-index задаём этим же селектором: правило `.sched-matrix thead th`
   специфичнее одиночного `.sched-matrix__corner` и перебивало его z-index —
   при горизонтальной прокрутке числа дней наезжали на подпись «ПВЗ». */
.sched-matrix thead th.sched-matrix__corner {
  z-index: 5;
  box-shadow: 1px 1px 0 var(--color-border);
}
/* Подпись пункта в первой колонке. Ссылкой она бывает не у всех ролей
   (см. templatetag pvz_card_url), поэтому вид держит класс, а не тег: у ролей
   без справочника ПВЗ здесь тот же текст, только без клика и подсветки. */
.sched-matrix__pvzname {
  display: block; font-size: 13px; font-weight: 700; color: var(--color-text);
  text-decoration: none; line-height: 1.3;
}
a.sched-matrix__pvzname:hover { color: var(--color-primary-dark); }
.sched-matrix__meta { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
/* Вторая строка — код пункта: держим его тише адреса и не даём растягивать
   колонку. Ширину не режем числом: место делит flex, а многоточие включается,
   только когда его действительно не хватает. */
.sched-matrix__code {
  font-size: 11px; font-weight: 650; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  flex: 0 1 auto; min-width: 0;
}
/* Город — сеть вышла за пределы одного города, и адрес сам по себе больше
   не отличает пункты; пилюля вместо простого текста, чтобы город считывался
   с одного взгляда среди десятков строк. */
.sched-matrix__city {
  font-size: 10.5px; font-weight: 700; color: var(--color-primary-dark);
  background: var(--color-primary-soft); border-radius: var(--radius-pill);
  padding: 1px 7px; white-space: nowrap; flex: 0 1 auto;
  overflow: hidden; text-overflow: ellipsis; max-width: 120px;
}
/* Таблица сгруппирована по городам — город уже написан в заголовке группы,
   и пилюля в каждой строке под ним только шумит и режет код в многоточие.
   Класс живёт на <table>, поэтому точечная перерисовка строки (hx-swap-oob)
   его не теряет. В сети из одного города группировки нет — пилюля остаётся. */
.sched-matrix--bycity .sched-matrix__city { display: none; }
/* «N без смен» — счётчик дыр месяца. Прижат к правому краю колонки: так
   на сотне строк он читается столбиком, а не пляшет за кодом разной длины. */
.mx-gapcount {
  font-size: 10.5px; font-weight: 700; color: var(--color-danger);
  background: var(--color-danger-soft); border-radius: var(--radius-pill);
  padding: 1px 7px; white-space: nowrap; flex: none; margin-left: auto;
}
/* Статус недействующего пункта в подписи строки — появляется только с
   тумблером «показать закрытые». Серая пилюля: это справка, а не тревога
   (тревога — красный «без смен» рядом), поэтому и не в цвете опасности. */
.mx-status {
  font-size: 10.5px; font-weight: 700; color: var(--color-muted);
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  border-radius: var(--radius-pill); padding: 0 7px; white-space: nowrap; flex: none;
}
.mx-status--suspended { color: var(--color-warning); background: var(--color-warning-soft); }
/* Строка закрытого пункта в целом приглушена: она здесь для проверки истории,
   а не для работы, и не должна спорить с живыми строками за внимание. */
.sched-matrix tbody tr:has(.mx-status--closed) .sched-matrix__pvzname { color: var(--color-muted); }

/* Тумблер «показать закрытые» в тулбаре таблицы. Ссылка, а не форма:
   состояние живёт в адресе (?closed=1) и переживает навигацию по месяцам и
   переключение видов. Дорожка и шайба — те же, что у переключателя открытия,
   только в нейтральном цвете: это фильтр вида, а не действие над сетью. */
.mx-closedtoggle {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: var(--control-h-sm); padding: 0 10px 0 8px;
  border-radius: var(--radius-pill); text-decoration: none;
  font-size: 12.5px; font-weight: 650; color: var(--color-muted);
  border: 1px solid var(--color-border); background: transparent;
  transition: color .15s var(--ease), background .15s var(--ease), border-color .15s var(--ease);
}
.mx-closedtoggle:hover { color: var(--color-text); background: var(--color-surface-soft); }
.mx-closedtoggle:focus-visible { outline: none; box-shadow: var(--ring-soft); }
.mx-closedtoggle__knob {
  position: relative; flex: 0 0 auto; width: 26px; height: 15px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, currentColor 24%, transparent);
  transition: background .18s ease;
}
.mx-closedtoggle__knob::after {
  content: ""; position: absolute; top: 2.5px; left: 2.5px; width: 10px; height: 10px;
  border-radius: 50%; background: currentColor;
  transition: transform .18s cubic-bezier(.3, 1.4, .5, 1);
}
.mx-closedtoggle__label b { font-weight: 800; color: inherit; margin-left: 2px; }
.mx-closedtoggle.is-on {
  color: var(--color-primary-dark); border-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
  background: var(--color-primary-soft);
}
.mx-closedtoggle.is-on .mx-closedtoggle__knob { background: var(--color-primary); }
.mx-closedtoggle.is-on .mx-closedtoggle__knob::after { background: #fff; transform: translateX(11px); }

/* Подсветка строки целиком — на 31 колонке глаз иначе съезжает на соседний ПВЗ.
   Тонируем слоем поверх заливки: background-color несёт состояние дня. */
.sched-matrix tbody tr:hover .sched-matrix__cell {
  background-image: linear-gradient(
    color-mix(in srgb, var(--color-primary) 8%, transparent),
    color-mix(in srgb, var(--color-primary) 8%, transparent)
  );
}
.sched-matrix tbody tr:hover .sched-matrix__pvz { background: var(--color-surface-soft); }

/* --- Шапка дней ---------------------------------------------------------- */
.sched-matrix__day { padding: 0; min-width: 88px; }
.sched-matrix__day a {
  display: flex; flex-direction: column; align-items: center; gap: 0;
  padding: 6px 6px; text-decoration: none; color: inherit;
  transition: background .15s var(--ease);
}
.sched-matrix__day a:hover { background: var(--color-primary-soft); }
.sched-matrix__daynum { font-size: 14px; font-weight: 800; line-height: 1.2; }
.sched-matrix__dayname {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--color-muted);
}
/* Шапка выходного не залита вовсе (заказчик, 03.09.2026). Подпись — faint, а не
   warning: янтарным на этом же экране залита клетка «день держит только
   заменяющий» и помечена замена в легенде, то есть один цвет нёс два смысла.
   Выходной для сети ПВЗ не тревога, это просто другой день. */
.sched-matrix__day.is-weekend .sched-matrix__dayname { color: var(--color-faint); }

/* --- Сегодня: колонка целиком, а не рамка вокруг каждой клетки -------------
   На 31 колонке одинаковых чисел «сегодня» нужно находить без чтения — поэтому
   число уходит в залитый кружок, подпись меняется на «сегодня», а по всей
   высоте колонки идут два вертикальных канта. Шапка липкая: её собственную
   нижнюю линию (0 1px 0 border) дописываем в тот же box-shadow, иначе она
   пропадёт именно у сегодняшнего дня. */
/* Шапка сегодняшнего дня — НАВЕРШИЕ колонки: скруглена сверху и залита
   вертикальным градиентом, который стекает в тон самой колонки. Отсюда
   ощущение, что подсвечен столбец целиком, а не что в таблицу вставлен
   отдельный цветной блок. Скругление только сверху: снизу колонка продолжается. */
.sched-matrix__day.is-today {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--color-primary) 16%, var(--color-surface)),
    color-mix(in srgb, var(--color-primary) 7%, var(--color-surface)));
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  box-shadow:
    inset 1px 0 0 color-mix(in srgb, var(--color-primary) 16%, transparent),
    inset -1px 0 0 color-mix(in srgb, var(--color-primary) 16%, transparent);
}
.sched-matrix__day.is-today a { padding-top: 6px; }
.sched-matrix__day.is-today a:hover { background: transparent; }
/* Кружок остался цветным — он и есть та самая подсветка, — но перестал быть
   плоским диском: 24px вместо 26, тень плотная и короткая (1px 3px), а не
   размытый ореол. Именно ореол вокруг насыщенной заливки и читался дёшево.
   Краска цифры разведена по темам НИЖЕ: --color-on-primary равен #FFFFFF в
   обеих, а белый на тёмном --color-primary #4187FF даёт 3,42:1 — провал AA. */
.sched-matrix__day.is-today .sched-matrix__daynum {
  display: grid; place-items: center;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--color-primary); color: #FFFFFF;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--color-primary) 40%, transparent);
  font-size: 12.5px;
  box-shadow: 0 2px 8px color-mix(in srgb, var(--color-primary) 45%, transparent);
}
.sched-matrix__day.is-today .sched-matrix__dayname {
  color: var(--color-primary-dark); letter-spacing: .04em; font-weight: 750;
}
/* В тёмной теме цифра в кружке — цвет полотна: белый на #4187FF даёт 3,42:1. */
html[data-theme="dark"] .sched-matrix__day.is-today .sched-matrix__daynum { color: var(--color-bg); }

/* --- Ячейки: цвет = кто закрывает день ------------------------------------ */
/* Содержимое по центру клетки, а не по верхнему краю: высоту строки задаёт
   подпись ПВЗ из двух строк, и прижатые вверх фамилии оставляли под собой
   полосу пустой заливки — таблица выглядела рваной. */
.sched-matrix__cell {
  /* position — под вуаль выходного (::after ниже). Ширина 98 → 88 и поля 5 → 4:
     месяц не помещался никуда, на 1440 было видно 11,5 дня из 31, стало 13,3;
     на 1920 — 17,2 против 19,8. Ни одной единицы информации при этом не теряем. */
  position: relative;
  padding: 4px; vertical-align: middle; min-width: 88px;
  background-color: var(--color-surface);
  transition: box-shadow .12s var(--ease);
}
/* Свой сотрудник — спокойный зелёный фон. */
.sched-matrix__cell.is-open {
  background-color: color-mix(in srgb, var(--color-success) 9%, var(--color-surface));
}
/* Заливки «замена» здесь БОЛЬШЕ НЕТ (03.09.2026, заказчик: «таблицу нужно
   переделать»). Ось «свой / замена» была нарисована дважды и в двух РАЗНЫХ
   смыслах: заливка ставилась при all_replacement (все в клетке заменяющие), а
   кант .mx-chip--replacement красит конкретного человека. В смешанной клетке
   «свой + заменяющий» клетка была зелёной, а один кант янтарным — два ответа
   на один вопрос. Плюс громкости почти не различались: 1,202 против 1,167,
   разница 3% — взгляд их не ранжировал.
   Класс is-replacement в разметке остаётся: он нужен под фильтрацию и
   data-атрибуты, просто перестал красить. Замена по-прежнему видна кантом
   плашки и счётчиком в строке над таблицей. */
/* Рабочий день без единой смены — красный, и это ЕДИНСТВЕННОЕ, что красится
   заливкой во всю площадь клетки. 12% → 14%: после снятия «замены» разрыв с
   «своим» стало чем держать, и он вырос с 3% до 10%. */
.sched-matrix__cell.is-closed {
  background-color: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface));
}
/* Выходной по графику ПВЗ — не проблема. --color-surface-alt, а не soft:
   на soft порядок громкости в СВЕТЛОЙ теме был перевёрнут — выходной (1,131)
   оказывался чуть громче «своего» (1,128), то есть нерабочий день кричал
   громче рабочего. Комментарий на этом месте утверждал обратное. */
.sched-matrix__cell.is-off {
  background-color: var(--color-surface-alt);
}
/* Тёмная тема считает доли ЗАНОВО, а не наследует светлые. На карточке #1A1B1E
   те же проценты дали перевёрнутый порядок громкости: замена 1,268 > выходной
   1,219 > дыра 1,195 > свой 1,183. То есть главный сигнал таблицы — рабочий
   день без единой смены — оказался тише и «замены», и «выходного», и взгляд
   первым цеплял янтарный, а не красный. В светлой теме порядок был верный.
   Здесь он восстановлен: 1,57 > 1,46 > 1,25 > 1,07. */
html[data-theme="dark"] .sched-matrix__cell.is-open {
  background-color: color-mix(in srgb, var(--color-success) 12%, var(--color-surface));
}
html[data-theme="dark"] .sched-matrix__cell.is-closed {
  background-color: color-mix(in srgb, var(--color-danger) 26%, var(--color-surface));
}
html[data-theme="dark"] .sched-matrix__cell.is-off {
  background-color: var(--color-surface-alt);
}
/* Вуали выходного в теле НЕТ — решение заказчика 03.09.2026: «давай суббота и
   воскресенье не будут тёмными». Границу недели ниже оставляем: она помогает
   считать недели, но ничего не затемняет. */
/* Колонка «сегодня» в теле: канты по бокам собираются в сплошную вертикаль,
   заливка состояния дня (зелёный/жёлтый/красный) остаётся читаемой под тонкой
   синей вуалью. Нижней линии здесь нет намеренно: `tbody tr:last-child`
   замыкал колонку в конце КАЖДОЙ группы, и вместо одной вертикали таблица
   рисовала цепочку синих коробок вокруг каждого города. */
/* Колонка «сегодня». Здесь было две крайности подряд, и обе неверные: сначала
   две сплошные синие линии в 2px через всю простыню (заказчик: «слишком дешёвое
   выделение»), потом почти невидимая нейтральная вуаль — подсветка пропала
   совсем (заказчик: «а можем сделать чтобы красиво подсвечивалось?»).
   Верное посередине: подсветка ЕСТЬ и она цветная, но держится тоном, а не
   линией. Заливка 6% даёт колонку, которую видно боковым зрением через всю
   таблицу; кромки — ХАЙРЛАЙН в 1px на 16% альфы вместо сплошных 2px: край
   читается, но не режет. Вуаль слоем background-image, а не background-color:
   цвет уже занят заливкой покрытия, и перекрасить её нельзя. */
.sched-matrix__cell.is-today {
  background-image: linear-gradient(
    color-mix(in srgb, var(--color-primary) 6%, transparent),
    color-mix(in srgb, var(--color-primary) 6%, transparent)
  );
  box-shadow:
    inset 1px 0 0 color-mix(in srgb, var(--color-primary) 16%, transparent),
    inset -1px 0 0 color-mix(in srgb, var(--color-primary) 16%, transparent);
}

/* Кнопка «назначить» на всю клетку: лежит под фамилиями, поэтому клик по
   свободному месту открывает попап, а клик по фамилии ведёт на карточку. */
.sched-matrix__cell { position: relative; }
.mx-cellbtn {
  position: absolute; inset: 0; z-index: 0;
  border: 0; background: transparent; cursor: pointer; padding: 0;
  display: grid; place-items: center;
  transition: box-shadow .12s var(--ease);
}
.mx-cellbtn__plus {
  font-size: 15px; font-weight: 800; line-height: 1;
  color: var(--color-primary-dark); opacity: 0;
  transition: opacity .12s var(--ease);
}
.mx-cellbtn:hover { box-shadow: inset 0 0 0 2px var(--color-primary); }
/* «+» подсказываем только там, где он не спорит с фамилиями. */
.sched-matrix__cell.is-closed .mx-cellbtn:hover .mx-cellbtn__plus,
.sched-matrix__cell.is-off .mx-cellbtn:hover .mx-cellbtn__plus { opacity: 1; }
/* На ЗАНЯТОЙ клетке «+» тоже нужен: добавить второго человека можно, но раньше
   об этом не было ни намёка — люди делали это через фамилию первого и узнавали
   случайно. В углу, чтобы не спорить с плашками фамилий. */
.sched-matrix__cell.is-open .mx-cellbtn { place-items: end end; padding: 0 4px 3px 0; }
.sched-matrix__cell.is-open .mx-cellbtn:hover .mx-cellbtn__plus { opacity: .7; font-size: 12px; }
.mx-cellbtn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }
/* Содержимое клетки — над кнопкой, чтобы ссылки оставались кликабельными. */
.sched-matrix__cell .mx-chip,
.sched-matrix__cell .mx-more { position: relative; z-index: 1; }

/* Фамилия в ячейке: белая плашка поверх заливки, кант — вид выхода. */
/* В тёмной теме плашка красится ВУАЛЬЮ поверх клетки, а не примесью к её цвету:
   82% карточки #1A1B1E поверх зелёной клетки давали #1B1E20 — плашка выходила
   ТЕМНЕЕ клетки и работала дырой вместо приподнятой карточки, а цвет покрытия
   под фамилиями почти пропадал. Именно он и есть ключ к этой таблице. */
html[data-theme="dark"] .mx-chip { background: color-mix(in srgb, #FFFFFF 9%, transparent); }
.mx-chip {
  display: block; text-decoration: none;
  font-size: 11.5px; font-weight: 650; line-height: 1.3; color: var(--color-text);
  background: color-mix(in srgb, var(--color-surface) 82%, transparent);
  border-left: 3px solid var(--color-success);
  /* Таблетка, а не прямоугольник: соседи по клетке (.sched-matrix__city,
     .mx-status, .mx-gapcount, .mx-norm, .mx-offer) уже переехали на pill, и
     плашка фамилии осталась единственным скруглением 9px. Кант вида выхода
     превращается на таблетке в цветной серп у левого края — он читается. */
  border-radius: var(--radius-pill); padding: 3px 8px; margin-bottom: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background .12s var(--ease);
}
.mx-chip:last-child { margin-bottom: 0; }
.mx-chip:hover { background: var(--color-surface); }
.mx-chip--regular     { border-left-color: var(--color-success); }
.mx-chip--replacement { border-left-color: var(--color-warning); }
/* Неоформленный — БЕЗ заливки (03.09.2026). Признак уже несёт точка
   .mx-dot--unofficial перед фамилией, а заливка была вторым, буквальным дублем
   и при этом непрозрачной: в светлой теме плашка #EEEAFD на клетке-дыре
   #F9E6E7 давала шаг 1,018 — граница плашки исчезала, а цвет покрытия под
   фамилией пропадал совсем. Если сигнала не хватит, усиливать надо ТОЧКУ
   (7 → 9px), а не площадь плашки. */
.mx-chip--emp-unofficial { color: var(--color-violet); }
.mx-chip--emp-casual { font-style: italic; }

/* --- Третья ось: когда за выход отдают деньги ------------------------------
   Отметка стоит на СМЕНЕ, а не на человеке (заказчик: «привязывать к дате, не
   к сотрудникам»), поэтому она красит плашку конкретного дня и не наследуется
   соседними.
   ЗАЛИВКА СНЯТА 03.09.2026 — это была самая громкая раскраска экрана: цветная
   плашка закрывает ~34% площади клетки, две плашки — 67%, то есть отметка
   оплаты перекрашивала клетку СИЛЬНЕЕ, чем сама заливка покрытия. И заливка
   была непрозрачной, от --color-surface: цвет покрытия под фамилией пропадал.
   Остался кант справа 3px — он физически не может спорить с заливкой во всю
   площадь, а прочитывается так же однозначно. */
.mx-chip--pay-upfront,
.mx-chip--pay-after_shift {
  border-right: 3px solid var(--_pay-line);
  font-weight: 750;
}
.mx-chip--pay-upfront    { --_pay-line: var(--color-cyan); }
.mx-chip--pay-after_shift { --_pay-line: var(--color-magenta); }

.mx-more {
  display: inline-block; font-size: 10.5px; font-weight: 700;
  color: var(--color-primary-dark); text-decoration: none; padding: 1px 4px;
}
.mx-more:hover { text-decoration: underline; }
.mx-closed {
  display: block; font-size: 12px; font-weight: 800; padding: 3px 0;
  color: color-mix(in srgb, var(--color-danger) 78%, transparent);
  text-align: center; line-height: 1.4;
}
.mx-off {
  display: block; font-size: 12px; padding: 3px 0;
  color: color-mix(in srgb, var(--color-muted) 55%, transparent);
  text-align: center; line-height: 1.4;
}
/* В месячной матрице те же классы несут не фразу, а один значок — и повторён
   он в сотнях клеток подряд. Сигнал там уже дан заливкой, поэтому значок
   гасим до подсказки: месяц без смен перестаёт рябить решёткой из крестов.
   В режиме одного дня (.sched-matrix--day) это по-прежнему текст — его не
   трогаем. */
/* В МЕСЯЧНОМ виде значки «✕» и «—» из клетки убраны совсем (03.09.2026,
   заказчик про вид таблицы). Причина: на 31 колонке они дублируют то, что уже
   сказала заливка клетки, — и на месяце, где смен нет, экран превращался в поле
   из сотен крестиков поверх красного. Смысл несёт цвет клетки, а не значок.
   Подпись для скринридера остаётся в разметке (.sr-only «нет смены»), так что
   доступность не страдает.
   В ДНЕВНОМ виде (.sched-matrix--day) значки остаются: там одна колонка, клетка
   крупная, и значок в ней — содержимое, а не рябь. */
.sched-matrix:not(.sched-matrix--day) .mx-closed,
.sched-matrix:not(.sched-matrix--day) .mx-off { display: none; }
/* Наведение показывает значок обратно — под курсором клетка «оживает» и можно
   убедиться, что она пустая, а не просто бледная. */
.sched-matrix:not(.sched-matrix--day) tbody tr:hover .mx-closed {
  display: block; font-size: 10px; font-weight: 700; padding: 2px 0;
  color: color-mix(in srgb, var(--color-danger) 70%, transparent);
}
.mx-norm {
  display: inline-block; font-size: 10.5px; font-weight: 700;
  color: var(--color-warning); background: var(--color-warning-soft);
  border-radius: var(--radius-pill); padding: 1px 7px;
}

/* --- Режим одного дня: развёрнутые карточки людей ------------------------- */
.sched-matrix__wrap--day { max-height: none; }
.sched-matrix__dayhead { padding: 9px 12px; text-align: left; font-size: 13px; font-weight: 800; }
.sched-matrix__cell--day { padding: 9px 12px; min-width: 0; }
.sched-matrix--day .sched-matrix__pvz { min-width: 220px; }
.sched-matrix__row--closed .sched-matrix__pvzname { color: var(--color-danger); }
.mx-people { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.mx-person {
  display: inline-flex; align-items: baseline; gap: 8px;
  text-decoration: none; color: var(--color-text);
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-success);
  border-radius: var(--radius-sm); padding: 5px 10px;
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.mx-person:hover { border-color: var(--color-primary); background: var(--color-primary-soft); }
.mx-person--regular     { border-left-color: var(--color-success); }
.mx-person--replacement { border-left-color: var(--color-warning); }
.mx-person__name { font-size: 12.5px; font-weight: 700; }
.mx-person__tag {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-warning); background: var(--color-warning-soft);
  border-radius: var(--radius-pill); padding: 1px 7px;
}
.mx-person__time { font-size: 11.5px; font-weight: 700; color: var(--color-muted); font-variant-numeric: tabular-nums; }
.mx-person__status { font-size: 11px; color: var(--color-muted); }
.mx-person__emp {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-violet); background: var(--color-violet-soft);
  border-radius: var(--radius-pill); padding: 1px 7px;
}
.mx-person--emp-casual .mx-person__emp { color: var(--color-muted); background: var(--color-surface-soft); }
/* Режим одного дня: места хватает на слова, поэтому отметка оплаты — плашка
   с подписью, а карточку человека помечает кант справа. */
.mx-person__pay {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  color: var(--_pay-line); background: color-mix(in srgb, var(--_pay-line) 16%, transparent);
  border-radius: var(--radius-pill); padding: 1px 7px;
}
.mx-person--pay-upfront,
.mx-person--pay-after_shift { border-right: 3px solid var(--_pay-line); }
.mx-person--pay-upfront     { --_pay-line: var(--color-cyan); }
.mx-person--pay-after_shift { --_pay-line: var(--color-magenta); }
/* Ручной вид оплаты без ставки: смена не рассчитается. Метка красная, но
   молчит о сумме — таблицу графика видит и управляющий, которому деньги
   не показывают. В месячной таблице места на слова нет, там знак «₽». */
.mx-person__rate {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-danger); background: var(--color-danger-soft);
  border-radius: var(--radius-pill); padding: 1px 7px;
}
/* Знак «₽» = «у смены не заполнена ставка». Проблема реальная, но другого
   ранга: красный на этом экране значит «на рабочий день никто не назначен», и
   на зелёной клетке «свой» тот же красный читался ложной тревогой того же веса.
   В дневном виде (.mx-person__rate) цвет остаётся — там есть место на слова. */
.mx-chip__rate {
  margin-left: 3px; font-style: normal; font-weight: 900; font-size: 11px;
  color: var(--color-text);
}

/* --- Иерархия «область → город» ------------------------------------------
   Сеть переросла один город, и сотня строк подряд перестала читаться. Группа —
   отдельный <tbody>: заголовок и его пункты живут одним блоком.
   Заголовок растянут на все колонки, поэтому липким делаем не саму ячейку
   (она шириной во всю таблицу), а внутреннюю обёртку — тогда подпись остаётся
   на виду при горизонтальной прокрутке месяца. */
/* Заголовок группы — тихий разделитель. Синего в нём не осталось нигде
   (03.09.2026, заказчик прямо указал на полосу «ПСКОВСКАЯ ОБЛАСТЬ»): ни заливки
   области, ни синего текста, ни цветной плашки счётчика. Уровень задаёт
   типографика и линия, а не цвет — на 58 точках в трёх областях и восемнадцати
   городах таких полос на экране до десятка, и любая заметная краска в них
   спорит с заливками покрытия, ради которых таблицу открывают.
   Заливка тоже снята: строка держится линией снизу и разрядкой подписи. */
.mx-grouprow th {
  padding: 0; text-align: left;
  background: transparent;
  border-bottom: 1px solid var(--color-divider);
}
/* Заголовок группы — разделитель, а не карточка: в сети, где на город часто
   приходится один пункт, толстая полоса на каждую пару строк съедала экран. */
.mx-grouprow__inner {
  position: sticky; left: 0;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 12px; max-width: 100%;
}
.mx-grouprow__label { font-weight: 800; letter-spacing: .01em; }
.mx-grouprow__count {
  font-size: 10.5px; font-weight: 700; color: var(--color-faint);
  background: transparent; border: 0;
  border-radius: var(--radius-pill); padding: 1px 0; white-space: nowrap;
}
/* Заливки у заголовка области НЕТ (03.09.2026): синяя лента во всю ширину
   тридцати одной колонки была самым громким элементом экрана и несла меньше
   всего информации. На новом языке уровень задаёт типографика и линия снизу,
   а не сплошная цветная полоса. */
.mx-grouprow--region .mx-grouprow__label {
  font-size: 12px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--color-muted);
}
.mx-grouprow--city .mx-grouprow__label { font-size: 12.5px; color: var(--color-text); }

.mx-grouptoggle {
  border: 0; background: transparent; cursor: pointer; font: inherit;
  color: inherit; text-align: left;
}
.mx-grouptoggle:hover .mx-grouprow__label { color: var(--color-primary-dark); }
.mx-grouptoggle:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }
.mx-grouptoggle__chevron { width: 14px; height: 14px; flex: none; transition: transform .15s var(--ease); }
.mx-group.is-collapsed .mx-grouptoggle__chevron { transform: rotate(-90deg); }
/* Свёрнутая группа прячет ПВЗ, но не свой заголовок — иначе её не развернуть. */
.mx-group.is-collapsed [data-matrix-row] { display: none; }

/* --- «Развернуть»: простыня во весь экран ----------------------------------
   58 пунктов × 31 день в карточку не помещаются, а меню, шапка и заголовок
   страницы в момент разбора графика не нужны. Карточка становится окном во
   весь экран, а таблица внутри тянется по остатку высоты (flex), поэтому
   считать высоту тулбара и легенды в пикселях не приходится.
   z-index ниже попапа назначения (120) — клетка по-прежнему открывается
   поверх развёрнутой таблицы. */
.card.is-matrix-full {
  position: fixed; inset: 0; z-index: 60;
  margin: 0; border-radius: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: 10px;
  /* На телефоне карточка перекрывает таб-панель (z-index 40) — выход только
     кнопкой «Свернуть» в тулбаре. Нижний отступ держим не меньше полоски
     «домой» на iPhone, иначе последняя строка уходит под неё. */
  padding: 12px 16px max(16px, env(safe-area-inset-bottom));
}
body.is-matrix-full { overflow: hidden; }
.card.is-matrix-full .sched-matrix__wrap {
  max-height: none; flex: 1 1 auto; min-height: 0;
}
.card.is-matrix-full .mx-legend { margin-bottom: 0; }

/* --- Поиск по таблице ------------------------------------------------------ */
.matrix-search { position: relative; display: inline-flex; align-items: center; }
.matrix-search svg {
  position: absolute; left: 9px; width: 15px; height: 15px;
  color: var(--color-muted); pointer-events: none;
}
.matrix-search .input {
  padding-left: 30px; max-width: 250px; min-height: var(--control-h-sm);
  padding-top: 6px; padding-bottom: 6px;
}
.mx-nohits td { padding: 22px 12px; color: var(--color-muted); font-size: 13px; }
/* Строка, спрятанная поиском: display:none, а не [hidden] — правило
   .sched-matrix для tr перебивало бы браузерное [hidden] (известная ловушка). */
[data-matrix-row].is-filtered-out,
.mx-group.is-filtered-out { display: none; }

@media (max-width: 819px) {
  .sched-matrix__corner, .sched-matrix__pvz { min-width: 150px; }
  .sched-matrix__day, .sched-matrix__cell { min-width: 86px; }
  .matrix-filter .input { max-width: 170px; }
  .matrix-search .input { max-width: 170px; }
}

/* ==== Отчёт по сотрудникам за период (вкладка «Отчёт» раздела «Графики») ====
   Обратный вопрос к таблице: не «кто закроет пункт», а «где был человек с 1 по
   15 августа». Поэтому здесь не сетка ПВЗ×даты, а список людей — строка на
   человека, разворот по дням. Список, а не таблица: у строки три разнородных
   блока (кто / где / сколько), и колонки бы им только мешали.
   Цвета — те же токены, что в клетках таблицы: один ключ на весь раздел. */
.srep-period { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.srep-presets {
  display: inline-flex; border-radius: var(--radius-pill); overflow: hidden;
  border: 1px solid var(--color-border); background: var(--color-surface-soft);
}
.srep-preset {
  border: 0; background: transparent; cursor: pointer;
  padding: 5px 12px; font-size: 12px; font-weight: 700; line-height: 1.5;
  color: var(--color-muted); white-space: nowrap;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.srep-preset + .srep-preset { border-left: 1px solid var(--color-border); }
.srep-preset:hover { background: var(--color-surface); color: var(--color-text); }
.srep-preset:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }
.srep-preset.is-on {
  background: var(--color-primary); color: var(--color-on-primary);
}
.srep-date .input { width: 148px; }
.srep-dash { color: var(--color-muted); }

/* Итоги периода — то, ради чего в отчёт заходят: до списка и крупно. */
.srep-totals {
  display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px;
}
.srep-total {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 7px 13px; border-radius: var(--radius-pill);
  border: 1px solid var(--color-border); background: var(--color-surface);
  border-left: 3px solid var(--_srep-line, var(--color-border));
}
.srep-total b { font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
.srep-total em { font-style: normal; font-size: 12px; font-weight: 650; color: var(--color-muted); }
.srep-total--multi       { --_srep-line: var(--color-warning); }
/* Неявка — плохая новость, и цвет у неё соответствующий: плитка стоит рядом с
   «выходов всего» и обязана читаться как оговорка к нему, а не как ещё один
   нейтральный счётчик. */
.srep-total--noshow      { --_srep-line: var(--color-danger); }
.srep-total--noshow b    { color: var(--color-danger); }
.srep-total--unofficial  { --_srep-line: var(--color-violet); }
.srep-total--upfront     { --_srep-line: var(--color-cyan); }
.srep-total--after       { --_srep-line: var(--color-magenta); }
.srep-total--unofficial b { color: var(--color-violet); }
.srep-total--upfront b    { color: var(--color-cyan); }
.srep-total--after b      { color: var(--color-magenta); }

.srep-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
/* [hidden] раньше собственного display — та же ловушка, что у попапов. */
.srep-nohits[hidden] { display: none; }
.srep-nohits {
  padding: 22px 12px; color: var(--color-muted); font-size: 13px; text-align: center;
}
.srep-card {
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface); overflow: hidden;
  transition: border-color .14s var(--ease), box-shadow .14s var(--ease);
}
.srep-card:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); }
.srep-card[open] { box-shadow: var(--shadow-sm); }
/* Неоформленный виден до раскрытия — кантом слева на всю карточку. */
.srep-row--emp-unofficial .srep-card { border-left: 3px solid var(--color-violet); }
.srep-row--emp-casual .srep-card { border-left: 3px solid var(--color-muted); }
.srep-card__head {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 11px 14px; cursor: pointer; list-style: none;
}
.srep-card__head::-webkit-details-marker { display: none; }
.srep-card__head:hover { background: var(--color-surface-soft); }
.srep-card__who { display: inline-flex; align-items: center; gap: 8px; min-width: 220px; }
.srep-card__name { font-size: 13.5px; font-weight: 750; }
.srep-emp {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-violet); background: var(--color-violet-soft);
  border-radius: var(--radius-pill); padding: 1px 7px;
}
.srep-emp--casual { color: var(--color-muted); background: var(--color-surface-soft); }

/* Где работал: точки по убыванию числа выходов — «в основном тут» читается
   первым, разовый выход на соседний пункт за место с ним не спорит. */
.srep-card__where { display: flex; flex-wrap: wrap; gap: 5px; flex: 1 1 260px; min-width: 0; }
.srep-point {
  display: inline-flex; align-items: baseline; gap: 5px;
  font-size: 11.5px; font-weight: 650; color: var(--color-text);
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  border-radius: var(--radius-pill); padding: 2px 9px;
}
.srep-point b { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--color-muted); }
.srep-point--replacement {
  color: var(--color-warning); border-color: color-mix(in srgb, var(--color-warning) 38%, transparent);
  background: var(--color-warning-soft);
}
.srep-point--replacement b { color: var(--color-warning); }

.srep-card__nums { display: inline-flex; align-items: center; gap: 12px; margin-left: auto; }
.srep-num { display: inline-flex; align-items: baseline; gap: 5px; }
.srep-num b { font-size: 14px; font-weight: 800; font-variant-numeric: tabular-nums; }
.srep-num em { font-style: normal; font-size: 11px; color: var(--color-muted); }
.srep-num--replacement b { color: var(--color-warning); }
.srep-num--upfront b     { color: var(--color-cyan); }
.srep-num--after b       { color: var(--color-magenta); }
.srep-card__chevron { width: 16px; height: 16px; color: var(--color-muted); transition: transform .16s var(--ease); }
.srep-card[open] .srep-card__chevron { transform: rotate(180deg); }

/* Разворот — по дням: дата, точка, чем этот выход был. Плитками в несколько
   колонок, а не строками в столбик: за полмесяца выходов бывает полтора
   десятка, и вертикальный список выталкивал бы следующего человека с экрана. */
.srep-days {
  list-style: none; margin: 0; padding: 8px;
  border-top: 1px solid var(--color-border); background: var(--color-surface-soft);
  display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 6px;
}
/* min-width:0 обязателен: без него авто-минимум трека грида равен min-content
   плитки, длинная метка распирала колонку, и плитки наезжали друг на друга. */
.srep-day { min-width: 0; }
.srep-day__link {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  text-decoration: none; color: var(--color-text);
  padding: 6px 10px; border-radius: var(--radius-sm);
  border: 1px solid var(--color-border); background: var(--color-surface);
  border-left: 3px solid var(--color-border);
}
.srep-day__link:hover { border-color: var(--color-primary); }
.srep-day--replacement .srep-day__link { border-left-color: var(--color-warning); }
.srep-day--pay-upfront .srep-day__link { border-left-color: var(--color-cyan); }
.srep-day--pay-after_shift .srep-day__link { border-left-color: var(--color-magenta); }
.srep-day__date {
  display: inline-flex; align-items: baseline; gap: 5px; flex: none;
  font-size: 12.5px; font-weight: 750; font-variant-numeric: tabular-nums;
}
.srep-day__date em { font-style: normal; font-size: 10.5px; font-weight: 650; color: var(--color-muted); }
.srep-day__pvz {
  font-size: 12px; color: var(--color-muted);
  flex: 1 1 90px; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Метки переносятся на вторую строку вместо того, чтобы съедать адрес до
   одной буквы: адрес — главное в этой плитке, статус — уточнение. */
.srep-day__tags { display: inline-flex; gap: 4px; margin-left: auto; flex-wrap: wrap; }
.srep-tag {
  font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .03em;
  border-radius: var(--radius-pill); padding: 1px 6px;
  color: var(--color-muted); background: var(--color-surface);
  border: 1px solid var(--color-border);
}
.srep-tag--replacement { color: var(--color-warning); background: var(--color-warning-soft); border-color: transparent; }
.srep-tag--pay-upfront {
  color: var(--color-cyan); border-color: transparent;
  background: color-mix(in srgb, var(--color-cyan) 18%, transparent);
}
.srep-tag--pay-after_shift {
  color: var(--color-magenta); border-color: transparent;
  background: color-mix(in srgb, var(--color-magenta) 18%, transparent);
}

/* Плашка отметки оплаты в карточке смены. */
.pay-badge {
  display: inline-block; font-size: 11.5px; font-weight: 800;
  border-radius: var(--radius-pill); padding: 2px 10px;
  color: var(--_pay-line); background: color-mix(in srgb, var(--_pay-line) 16%, transparent);
}
.pay-badge--upfront     { --_pay-line: var(--color-cyan); }
.pay-badge--after_shift { --_pay-line: var(--color-magenta); }

@media (max-width: 819px) {
  .srep-card__nums { margin-left: 0; }
  .srep-card__who { min-width: 0; }
  .srep-days { grid-template-columns: 1fr; }
}

/* ---- Попап «назначить смену» из клетки графика --------------------------- */
/* [hidden] объявлен ПЕРЕД display — иначе правило .cell-modal перебивает
   браузерное [hidden]{display:none} и попап висит открытым (ловушка). */
.cell-modal[hidden] { display: none; }
.cell-modal { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: 24px; }
/* 7/17/31 — остаток графитовой палитры до 03.09.2026: на нейтрально-чёрном
   полотне он давал синеватый ореол вокруг диалога. */
.cell-modal__backdrop { position: absolute; inset: 0; background: color-mix(in srgb, var(--color-bg-deep) 72%, transparent); backdrop-filter: blur(6px); }
.cell-modal__dialog {
  position: relative; width: min(560px, 100%); max-height: calc(88vh * var(--vu)); overflow: auto;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-pop); padding: 20px;
}
.cell-modal__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.cell-modal__eyebrow { color: var(--color-muted); font-size: 12.5px; font-weight: 700; }
.cell-modal__title { margin-top: 2px; font-size: 18px; font-weight: 850; }
.cell-modal__sub { margin-top: 2px; font-size: 12.5px; color: var(--color-muted); }
.cell-modal__note { margin-top: 12px; }
.cell-modal__section { margin-top: 18px; }
.cell-modal__subtitle {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 800; margin-bottom: 8px;
}
.cell-modal__assigned { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
/* Строка человека — две линии, а не одна длинная: имя, статус, сегмент оплаты
   и пара кнопок в 560 px рядом не помещались никогда, и обёртка действий с
   margin-left:auto срывалась на вторую строку, повисая у правого края в
   отрыве от того, к кому относится. «Кто» забирает первую линию целиком
   (flex-basis: 100%), оплата и действия делят вторую.
   Людей делит линейка, а не подложка: заливкой строка спорила бы с сегментом
   оплаты — он сам на surface-soft и растворялся в ней. Тот же приём, что у
   соседнего списка «Кого поставить» (.assign-row), — списки в одном окне. */
.cell-modal__assigned li {
  display: flex; align-items: center; gap: 6px 8px; font-size: 13px; flex-wrap: wrap;
  padding: 9px 0; border-bottom: 1px solid var(--color-border);
}
.cell-modal__assigned li:last-child { border-bottom: 0; }
.cell-assigned__who {
  flex: 1 0 100%; min-width: 0;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}

/* --- Отметка оплаты у того, кто уже стоит на этот день ---------------------
   Сегмент из трёх кнопок, а не селект: решение принимают на глаз и меняют
   подряд по нескольким дням, а лишний клик «раскрыть список» на каждом из них
   — ровно та ручная работа, от которой попап и избавляет. Активный сегмент
   красится тем же цветом, что и плашка в клетке, — ключ один на весь экран. */
.cell-payout {
  display: inline-flex; flex: none; border-radius: var(--radius-pill);
  border: 1px solid var(--color-border); background: var(--color-surface-soft);
  overflow: hidden;
}
/* Ключ к сегментам — одной строкой над списком (см. .cell-modal__paykey):
   ярлык внутри каждой строки ломал её на две. */
.cell-modal__paykey { margin: -2px 0 8px; }
.cell-payout__btn {
  border: 0; background: transparent; cursor: pointer;
  padding: 3px 9px; font-size: 11px; font-weight: 700; line-height: 1.5;
  color: var(--color-muted); white-space: nowrap;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.cell-payout__btn + .cell-payout__btn { border-left: 1px solid var(--color-border); }
.cell-payout__btn:hover { background: var(--color-surface); color: var(--color-text); }
.cell-payout__btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }
/* Активный сегмент — заливкой, а не оттенком поверхности: в тёмной теме
   surface и surface-soft почти неразличимы, и «включено» пропадало. */
.cell-payout__btn.is-on {
  color: var(--color-text); font-weight: 800;
  background: color-mix(in srgb, var(--color-text) 14%, var(--color-surface));
}
.cell-payout__btn--upfront.is-on {
  color: var(--color-cyan);
  background: color-mix(in srgb, var(--color-cyan) 26%, var(--color-surface));
}
.cell-payout__btn--after.is-on {
  color: var(--color-magenta);
  background: color-mix(in srgb, var(--color-magenta) 26%, var(--color-surface));
}
/* «Снять со смены» — действие разрушающее, поэтому держим его тихим и справа,
   у края строки: рука не попадает в него, пока не целится. */
.cell-unassign { margin-left: auto; color: var(--color-danger); flex: none; }
.cell-unassign svg { width: 13px; height: 13px; }
.cell-unassign:hover { background: var(--color-danger-soft); }
/* --- Массовое снятие: «Снять всех» по клетке и «Со всех смен» по человеку ---
   Пара кнопок живёт в общей обёртке, иначе margin-left:auto у второй растащил
   бы их по краям строки. Красное в строке одно — «Снять», которое отменяет
   смену сразу; «Со всех смен» лишь открывает панель со счётом, ничего не
   трогая, поэтому она тихая и краснеет только под курсором. Два одинаково
   красных пятна рядом спорили за клик и читались как одна кнопка. */
.cell-rowacts { margin-left: auto; display: inline-flex; align-items: center; gap: 4px; flex: none; }
.cell-rowacts .cell-unassign { margin-left: 0; }
.cell-releaseall { flex: none; }
.cell-releaseall:hover { color: var(--color-danger); background: var(--color-danger-soft); }
.cell-clearall { margin-left: auto; color: var(--color-danger); flex: none; }
.cell-clearall:hover { background: var(--color-danger-soft); }

/* --- Панель подтверждения массового снятия -------------------------------- */
.cell-modal__dialog--sm { width: min(480px, 100%); }
.cell-release__lead { margin-top: 12px; font-size: 12.5px; line-height: 1.5; }
.cell-release .form-field + .form-field { margin-top: 14px; }
/* Разбивка по пунктам: что именно снимется и в каких числах. Строка своего
   пункта подсвечена — с неё пришли, к ней и примеряют выбор охвата. */
.cell-release__list {
  margin: 12px 0 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 4px;
  max-height: 190px; overflow: auto;
}
.cell-release__list li {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 7px 10px; border-radius: var(--radius-sm);
  background: var(--color-surface-soft); font-size: 12.5px;
}
.cell-release__list li.is-here { background: var(--color-danger-soft); }
.cell-release__where { color: var(--color-muted); flex: 1 1 auto; min-width: 0; }
.cell-release__count { font-weight: 800; }
.cell-release__span { color: var(--color-muted); font-variant-numeric: tabular-nums; }
/* 240px висели на ОБОИХ списках, и своя команда точки (обычно 3-6 человек)
   оказывалась заперта в окошко внутри диалога, который сам прокручивается —
   вложенная прокрутка на самом частом действии продукта. Потолок оставляем
   только чужим: 312 = ровно шесть строк по 52px. */
.cell-modal__list { max-height: none; overflow: visible; margin-top: 8px; }
.cell-modal__others .cell-modal__list { max-height: 312px; overflow: auto; }
.cell-modal__others { margin-top: 12px; border-top: 1px solid var(--color-divider); padding-top: 12px; }
/* display: flex гасит браузерный треугольник у <summary>, а замены не поставили:
   блок «Сотрудники с других ПВЗ» выглядел жирной подписью со счётчиком, и по нему
   не догадывались кликнуть. На странице такой же блок раскрытие показывает —
   один и тот же элемент вёл себя по-разному в двух местах продукта. */
.cell-modal__others summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 13px; font-weight: 750;
  list-style: none;
}
.cell-modal__others summary::-webkit-details-marker { display: none; }
.cell-modal__others summary::after {
  content: "▾"; margin-left: auto; color: var(--color-muted);
  font-size: 11px; line-height: 1; transition: transform .16s var(--ease);
}
.cell-modal__others[open] summary::after { transform: rotate(180deg); }
.cell-modal__search { margin-top: 10px; }
.cell-modal__params { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 12px; }
/* Отметка оплаты — своей строкой во всю ширину: у неё есть подсказка на
   строку текста, и четвёртой колонкой она сплющила бы время смены. */
.cell-modal__payout { grid-column: 1 / -1; }
.cell-modal__repeat { display: grid; grid-template-columns: 1.4fr 1fr; gap: 12px; }
.cell-modal__repeat .field__hint { grid-column: 1 / -1; }

/* «Разовая замена» — карточка-переключатель, а не строка в списке типов смены:
   это решение о характере выхода, и по нему таблица красит день заменой. */
.cell-oneoff {
  display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
  /* Поля 11/13 → 13/15: при радиусе 18px прежний угол срезал бы галочку. */
  padding: 13px 15px; border-radius: var(--radius-lg);
  border: 0; background: var(--color-surface-soft);
  transition: box-shadow .14s var(--ease), background .14s var(--ease);
}
.cell-oneoff:hover { background: color-mix(in srgb, var(--color-warning) 7%, var(--color-surface-soft)); }
/* 18% вместо 12: рамки больше нет, и сигнал «включено» несёт одна заливка.
   Внутреннее кольцо остаётся ЕДИНСТВЕННЫМ местом, где рамка оправдана, —
   это переключаемое состояние, а не край карточки. */
.cell-oneoff.is-on {
  background: color-mix(in srgb, var(--color-warning) 18%, var(--color-surface));
  box-shadow: inset 0 0 0 1.5px var(--color-warning);
}
.cell-oneoff input { margin-top: 2px; width: 16px; height: 16px; accent-color: var(--color-warning); flex: none; }
.cell-oneoff__body { display: flex; flex-direction: column; gap: 2px; }
.cell-oneoff__title { font-size: 13.5px; font-weight: 800; }
.cell-oneoff.is-on .cell-oneoff__title { color: var(--color-warning-dark, var(--color-warning)); }
.cell-oneoff__hint { font-size: 12px; color: var(--color-muted); line-height: 1.35; }
/* Тип смены задан галкой: селект не прячем (значение всё равно уходит на сервер
   и там перекрывается), а гасим — иначе управляющий крутит бесполезный список. */
[data-oneoff-type].is-locked select { opacity: .5; pointer-events: none; }
/* [hidden] отдельным правилом с атрибутом — иначе display:block перебивает
   браузерное [hidden]{display:none} и подпись висит при снятой галке. */
.cell-oneoff__casual[hidden],
.cell-oneoff__lock[hidden] { display: none; }
.cell-oneoff__casual { display: block; margin-top: 10px; }
.cell-oneoff__casual .field__hint { line-height: 1.4; display: block; margin-top: 6px; }
/* ФИО и телефон — одна строка: телефон уже, он короче и подчинён имени. */
.cell-oneoff__fields { display: grid; grid-template-columns: 1.6fr 1fr; gap: 10px; }
@media (max-width: 620px) {
  .cell-oneoff__fields { grid-template-columns: 1fr; }
}
.cell-oneoff__lock { display: block; margin-top: 4px; color: var(--color-warning); font-weight: 700; }
/* Липкий подвал диалога. Сам диалог прокручивается (max-height 88vh), а над
   подвалом шесть секций: «Уже назначены», «Кого поставить», «Разовая замена»,
   подтверждение нахлёста, «Параметры смены», «Сменный график». На занятой клетке
   с раскрытыми «чужими» главная кнопка гарантированно уезжала ниже сгиба — на
   самом частом действии продукта. Числа 20 и 14 взяты из полей диалога и
   мобильной ветки, держать в паре с ними. */
.cell-modal__foot {
  position: sticky; bottom: -20px; z-index: 2;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 20px -20px -20px; padding: 16px 20px;
  background: var(--color-surface);
  border-top: 1px solid var(--color-divider);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  flex-wrap: wrap;
}
@media (max-width: 620px) {
  .cell-modal__foot { bottom: -14px; margin: 16px -14px -14px; padding: 14px; }
}
.cell-modal__foot-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }

@media (max-width: 620px) {
  .cell-modal { padding: 10px; }
  .cell-modal__dialog { padding: 14px; max-height: calc(94vh * var(--vu)); }
  .cell-modal__params { grid-template-columns: 1fr; }
}

/* ---- Cabinet shift list (mobile-first) ---------------------------------- */
.tab-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.tab {
  padding: 8px 14px; border-radius: var(--radius-pill); text-decoration: none;
  background: var(--color-surface); border: 1px solid var(--color-border);
  font-size: 13px; font-weight: 700; color: var(--color-muted);
  transition: border-color .15s var(--ease), color .15s var(--ease);
}
.tab:hover { border-color: var(--color-primary); color: var(--color-primary-dark); }
.tab--active { background: var(--color-primary-soft); color: var(--color-primary-dark); border-color: transparent; }

.shift-list__item--card { text-decoration: none; color: inherit; transition: border-color .15s var(--ease), transform .12s var(--ease); }
.shift-list__item--card:hover { border-color: var(--color-primary); transform: translateX(1px); }
.shift-accent--planned   { border-left-color: var(--color-primary); }
.shift-accent--confirmed { border-left-color: var(--color-primary-dark); }
.shift-accent--worked    { border-left-color: var(--color-success); }
.shift-accent--no_show   { border-left-color: var(--color-danger); }
.shift-accent--cancelled { border-left-color: var(--color-muted); }

@media (max-width: 1024px) {
  .metric-grid--shift { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 819px) {
  .calendar__cell { min-height: 78px; padding: 5px; }
  .cal-pill__name { display: none; }
}

@media (max-width: 720px) {
  .filters-bar--shift,
  .metric-grid--shift { grid-template-columns: 1fr; }
  .shift-form { max-width: none; }
  .calendar__weekday { font-size: 0; }
  .calendar__weekday::first-letter { font-size: 11.5px; }
}

/* ---------------------------------------------------------------------------
   Stage 8 — Opening control (events, statuses, problems, cabinet)
   --------------------------------------------------------------------------- */
/* Opening status badges (on_time/late/missing/manual_review/cancelled). */
.badge--on_time       { background: var(--color-success-soft); color: var(--color-success); }
.badge--late          { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--missing       { background: var(--color-danger-soft); color: var(--color-danger); }
.badge--manual_review { background: var(--color-primary-soft); color: var(--color-primary-dark); }

/* Processing badges (new/checking/confirmed/rejected/counted_kpi). */
.badge--proc_new       { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--proc_checking  { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--proc_confirmed { background: var(--color-success-soft); color: var(--color-success); }
.badge--proc_rejected  { background: var(--color-surface-soft); color: var(--color-muted); }
.badge--proc_counted   { background: color-mix(in srgb, var(--color-primary) 22%, transparent); color: var(--color-primary-dark); }

/* auto-fit, а не жёсткие шесть колонок: в дневном виде добавляется седьмая
   карточка «Нет данных», и фиксированная сетка выбрасывала бы её на свою
   строку во всю ширину. */
.metric-grid--opening { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }

/* Wide list filter bars (11 fields): auto-fit so fields wrap to new rows
   instead of overflowing the card on mid-size screens; the search field
   always takes the whole first row. */
.filters-bar--opening,
.filters-bar--acceptance,
.filters-bar--violations {
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
}
.filters-bar--opening .filters-bar__search,
.filters-bar--acceptance .filters-bar__search,
.filters-bar--violations .filters-bar__search { grid-column: 1 / -1; }
.filters-bar--cabinet { grid-template-columns: 1fr 1fr 1fr auto; }
.filters-bar__check {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; font-weight: 600; color: var(--color-muted); white-space: nowrap;
}

.field__error { display: block; color: var(--color-danger); font-size: 12px; margin-top: 4px; }
.form-check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; }

.opening-block {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 0; margin: 12px 0; border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}
/* Когда внутри блока лежит .state-tile, рельсы блока лишние — рамка в рамке.
   Базовое правило не трогаем: на нём же «Важные контакты точки» и на карточке
   смены, и на карточке свободной смены (cabinet/exchange_detail.html). */
.opening-block--state { padding: 0; margin: 12px 0 0; border-top: 0; border-bottom: 0; }

/* Cabinet opening cards — left accent by status. */
.opening-accent--on_time       { border-left-color: var(--color-success); }
.opening-accent--late          { border-left-color: var(--color-warning); }
.opening-accent--missing       { border-left-color: var(--color-danger); }
.opening-accent--manual_review { border-left-color: var(--color-primary); }
.opening-accent--cancelled     { border-left-color: var(--color-muted); }

@media (max-width: 1024px) {
  .metric-grid--opening { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 720px) {
  .filters-bar--opening,
  .filters-bar--cabinet,
  .metric-grid--opening { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
   Stage 9 — Acceptance control (15-minute deadline, statuses, problems, cabinet)
   --------------------------------------------------------------------------- */
/* Acceptance status badges. */
.badge--waiting_handover { background: var(--color-surface-soft); color: var(--color-muted); }
.badge--in_progress      { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--overdue          { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--missing_finish   { background: var(--color-danger-soft); color: var(--color-danger); }
/* on_time / manual_review / cancelled reuse the stage-8 badge styles above. */

/* auto-fit по той же причине, что у нарушений: восемь жёстких колонок жали
   подписи до обрезки на «средних» ширинах окна. */
.metric-grid--acceptance { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }

/* .filters-bar--acceptance использует общую auto-fit сетку широких фильтров
   (см. блок .filters-bar--opening выше). */

.acceptance-block {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 0; margin: 12px 0; border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

/* In-progress / overdue deadline banner inside the cabinet. */
.acceptance-timer {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 14px; border-radius: 12px;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
}
.acceptance-timer--missing_finish { background: var(--color-danger-soft); color: var(--color-danger); }
.acceptance-timer__label { font-size: 13px; font-weight: 600; }
.acceptance-timer__value { font-size: 16px; font-weight: 800; }
.acceptance-timer__value--danger { color: var(--color-danger); }

/* Cabinet acceptance cards — left accent by status. */
.acceptance-accent--waiting_handover { border-left-color: var(--color-muted); }
.acceptance-accent--in_progress      { border-left-color: var(--color-primary); }
.acceptance-accent--on_time          { border-left-color: var(--color-success); }
.acceptance-accent--overdue          { border-left-color: var(--color-warning); }
.acceptance-accent--missing_finish   { border-left-color: var(--color-danger); }
.acceptance-accent--manual_review    { border-left-color: var(--color-primary); }
.acceptance-accent--cancelled        { border-left-color: var(--color-muted); }

@media (max-width: 1024px) {
  .metric-grid--acceptance { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (max-width: 720px) {
  .filters-bar--acceptance { grid-template-columns: 1fr; }
  .metric-grid--acceptance { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.mono {
  font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--color-muted);
}

/* Yandex Maps card stores manual links only; no Yandex API/JS widget is loaded. */
.yandex-card { display: flex; flex-direction: column; gap: 14px; }
.yandex-card__actions { display: flex; gap: 10px; flex-wrap: wrap; }
.yandex-map-link-card {
  min-height: 178px; padding: 18px;
  display: flex; align-items: center; gap: 14px;
  color: inherit; text-decoration: none;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 10%, transparent), transparent 46%),
    var(--color-surface-soft);
  transition: transform .18s var(--ease), border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.yandex-map-link-card:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
  box-shadow: var(--shadow-card);
}
.yandex-map-link-card__icon {
  width: 52px; height: 52px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--color-primary-dark);
  background: var(--color-primary-soft); border-radius: var(--radius-md);
}
.yandex-map-link-card__icon svg { width: 30px; height: 30px; }
.yandex-map-link-card strong,
.yandex-map-link-card span span { display: block; }
.yandex-map-link-card span span { margin-top: 5px; color: var(--color-muted); font-size: 13px; }
.yandex-empty {
  min-height: 220px; border: 1px dashed var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface-soft);
}

/* Reviews block. */
.yandex-reviews { display: flex; flex-direction: column; gap: 14px; }
.yandex-reviews-widget {
  position: relative; min-height: 500px; width: 100%;
  overflow: hidden; border: 1px solid var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface-soft);
}
.yandex-reviews-widget__iframe {
  position: relative; z-index: 2;
  width: 100%; min-height: 500px; height: min(calc(72vh * var(--vu)), 680px);
  border: 0; display: block; background: transparent;
}
.yandex-reviews-widget__loader,
.yandex-reviews-widget__error {
  position: absolute; inset: 0; z-index: 1;
  display: flex; flex-direction: column; justify-content: center; gap: 12px;
  padding: 28px; color: var(--color-muted);
}
.yandex-reviews-widget__error {
  z-index: 3; align-items: center; text-align: center;
  background: color-mix(in srgb, var(--color-surface) 92%, transparent);
}
.skeleton {
  display: block; border-radius: 999px;
  background: linear-gradient(90deg, var(--color-surface-soft), color-mix(in srgb, var(--color-primary) 16%, var(--color-surface)), var(--color-surface-soft));
  background-size: 220% 100%;
  animation: skeleton-shimmer 1.35s linear infinite;
}
.skeleton--line { height: 18px; width: min(100%, 620px); }
.skeleton--short { width: min(64%, 380px); }
@keyframes skeleton-shimmer {
  from { background-position: 120% 0; }
  to { background-position: -120% 0; }
}

@media (max-width: 768px) {
  .yandex-map-link-card { align-items: flex-start; }
  .yandex-reviews-widget,
  .yandex-reviews-widget__iframe { min-height: 500px; height: 500px; }
}

/* ---------------------------------------------------------------------------
   Stage 10 — Violations (unified managerial layer over openings/acceptance/shifts)
   --------------------------------------------------------------------------- */
/* Processing-status badges (new/checking/confirmed/rejected/counted_kpi/ignored). */
.badge--v_new        { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--v_checking   { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--v_confirmed  { background: var(--color-success-soft); color: var(--color-success); }
.badge--v_rejected   { background: var(--color-danger-soft); color: var(--color-danger); }
.badge--v_counted_kpi { background: color-mix(in srgb, var(--color-primary) 22%, transparent); color: var(--color-primary-dark); }
.badge--v_ignored    { background: var(--color-surface-soft); color: var(--color-muted); }

/* Severity badges (low/medium/high/critical). */
.badge--sev_low      { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--sev_medium   { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--sev_high     { background: color-mix(in srgb, var(--color-warning) 30%, var(--color-danger-soft)); color: var(--color-danger); }
.badge--sev_critical { background: var(--color-danger-soft); color: var(--color-danger); }

/* Type badges. */
.badge--vt_opening_late, .badge--vt_acceptance_overdue { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--vt_opening_missing, .badge--vt_acceptance_missing_finish,
.badge--vt_shift_no_show, .badge--vt_claim_related { background: var(--color-danger-soft); color: var(--color-danger); }
.badge--vt_manual, .badge--vt_schedule_violation { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--vt_other { background: var(--color-surface-soft); color: var(--color-muted); }

/* auto-fit, а не жёсткие восемь колонок: между 880 и 1100px экрана восемь
   плиток давали по 108px, и подписи «Подтверждено», «Критические» обрезались
   краем карточки (.metric-card { overflow: hidden }). Ниже 148px колонка не
   опускается — auto-fit сам переносит лишние плитки на вторую строку. */
.metric-grid--violations { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }

/* .filters-bar--violations использует общую auto-fit сетку широких фильтров
   (см. блок .filters-bar--opening выше). */

@media (max-width: 880px) {
  .metric-grid--violations { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .filters-bar--violations { grid-template-columns: 1fr; }
  .metric-grid--violations { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------------------------------------------------------------------------
   Stage 11 — Claims (управленческие претензии над сетью ПВЗ)
   --------------------------------------------------------------------------- */
/* Status badges (new/in_review/linked/employee_response_requested/resolved/
   rejected/cancelled/counted_kpi). */
.badge--c_new        { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--c_in_review  { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--c_linked     { background: color-mix(in srgb, var(--color-primary) 22%, transparent); color: var(--color-primary-dark); }
.badge--c_employee_response_requested { background: color-mix(in srgb, var(--color-warning) 45%, var(--color-danger-soft)); color: var(--color-danger); }
.badge--c_resolved   { background: var(--color-success-soft); color: var(--color-success); }
.badge--c_rejected   { background: var(--color-danger-soft); color: var(--color-danger); }
.badge--c_cancelled  { background: var(--color-surface-soft); color: var(--color-muted); }
.badge--c_counted_kpi { background: color-mix(in srgb, var(--color-primary) 30%, transparent); color: var(--color-primary-dark); }

/* Category badges. */
.badge--cat_acceptance_deadline,
.badge--cat_opening_violation { background: color-mix(in srgb, var(--color-warning) 35%, var(--color-danger-soft)); color: var(--color-danger); }
.badge--cat_wrong_issue,
.badge--cat_wrong_acceptance,
.badge--cat_schedule_violation { background: var(--color-warning-soft); color: var(--color-warning); }
.badge--cat_lost_item,
.badge--cat_damaged_item { background: var(--color-danger-soft); color: var(--color-danger); }
.badge--cat_standard_violation { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.badge--cat_client_complaint { background: color-mix(in srgb, var(--color-danger) 35%, var(--color-warning-soft)); color: var(--color-danger); }
.badge--cat_other { background: var(--color-surface-soft); color: var(--color-muted); }

/* Девять плиток претензий — тоже auto-fit: на широком мониторе они по-прежнему
   стоят в один ряд, а на «полутора тысячах» переносятся, а не сжимаются в
   нечитаемые столбики (в одной из них живёт «724 тыс. ₽»). */
.metric-grid--claims { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }

/* Фильтры претензий, журнала действий и KPI: строка «каждый день» +
   сворачиваемая панель остального. Полтора десятка полей в общей auto-fit сетке
   занимали две плотные строки и выглядели тяжелее самого списка. */
/* Селектор из двух классов намеренно: ниже по файлу есть ещё одно правило
   `.filters-bar` (flex-wrap/align-items/gap), и одноклассовый модификатор
   проиграл бы ему по порядку — поля сжимались бы к правому краю. */
.filters-bar.filters-bar--claims,
.filters-bar.filters-bar--audit,
.filters-bar.filters-bar--kpi {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 10px; padding: 14px 0 4px;
}
.filters-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
}
.filters-row > .input { flex: 0 1 auto; width: auto; min-width: 148px; }
/* Выбор ПВЗ: initPvzPick подменяет select оболочкой .pvz-pick, и без своей
   ширины она сжималась по тексту кнопки — «Все ПВЗ» стояло вдвое уже соседних
   полей. */
.filters-row > .pvz-pick { flex: 0 1 auto; min-width: 168px; }
.filters-row > .btn { margin-left: auto; }
/* Вторая кнопка ряда («Сбросить» в журнале) идёт вплотную к первой, а не
   отталкивается от неё вторым `auto`. Специально не `:not(.btn + .btn)` на
   первой: такой селектор перебил бы по весу мобильное правило ниже, где
   кнопки становятся полноширинными. */
.filters-row > .btn + .btn { margin-left: 0; }
/* Селект шириной по самому длинному варианту: в журнале это «Тип события» со
   списком всех известных событий, в KPI — ФИО сотрудника и адрес ПВЗ. Без
   потолка один такой селект занимал полстроки. */
.filters-bar--audit .filters-row > select.input,
.filters-bar--audit .filters-more__grid > select.input,
.filters-bar--kpi .filters-row > select.input { max-width: 230px; }
/* Диапазон (даты, суммы) — один визуальный контрол: подпись, два поля и тире.
   Так «с/по» не читаются как два независимых фильтра. */
.filters-range { display: inline-flex; align-items: center; gap: 6px; flex: 0 1 auto; }
.filters-range__label {
  font-size: 12.5px; font-weight: 650; color: var(--color-muted); white-space: nowrap; padding-right: 2px;
}
.filters-range__dash { color: var(--color-faint); }
.filters-range .input { width: auto; flex: 0 1 auto; min-width: 118px; }

/* Быстрые периоды рядом с датами: сегмент-переключатель, а не набор кнопок —
   выбран всегда ровно один вариант, включая «Весь период». */
.period-quick {
  display: inline-flex; align-items: center; flex: 0 1 auto;
  height: var(--control-h); padding: 3px; gap: 2px;
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.period-quick__item {
  display: inline-flex; align-items: center; padding: 0 11px; height: 100%;
  border-radius: calc(var(--radius-md) - 4px);
  font-size: 12.5px; font-weight: 650; color: var(--color-muted);
  white-space: nowrap; text-decoration: none;
  transition: background-color .16s var(--ease), color .16s var(--ease);
}
.period-quick__item:hover { color: var(--color-text); background: var(--color-surface); }
.period-quick__item.is-active {
  background: var(--color-primary); color: var(--color-on-primary);
}
@media (max-width: 640px) {
  .period-quick { width: 100%; justify-content: space-between; }
  .period-quick__item { flex: 1 1 0; justify-content: center; padding: 0 6px; }
}

.filters-more { border-top: 1px solid var(--color-border); padding-top: 8px; }
.filters-more__toggle {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-size: 13px; font-weight: 650; color: var(--color-muted);
  list-style: none; user-select: none; width: max-content;
  padding: 4px 2px; border-radius: var(--radius-sm);
}
.filters-more__toggle::-webkit-details-marker { display: none; }
.filters-more__toggle:hover { color: var(--color-text); }
.filters-more__toggle svg { width: 15px; height: 15px; transition: transform .18s var(--ease); }
.filters-more[open] > .filters-more__toggle { color: var(--color-text); }
.filters-more[open] > .filters-more__toggle svg { transform: rotate(180deg); }
.filters-more__count {
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--radius-pill);
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-size: 11px; font-weight: 800; display: inline-flex; align-items: center; justify-content: center;
}
.filters-more__grid {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 0 4px;
}
.filters-more__grid > .input { flex: 0 1 auto; width: auto; min-width: 158px; }

@media (max-width: 880px) {
  .metric-grid--claims { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .metric-grid--claims { grid-template-columns: 1fr; }
  .filters-row > .input,
  .filters-row > .pvz-pick,
  .filters-more__grid > .input,
  .filters-range { flex: 1 1 100%; width: 100%; }
  .filters-row > .btn { margin-left: 0; width: 100%; }
  /* Потолок ширины селекта нужен только широкому экрану: на телефоне поля и так
     во всю строку, и 230px превратили бы селекты в куцые огрызки. */
  .filters-bar--audit .filters-row > select.input,
  .filters-bar--audit .filters-more__grid > select.input,
  .filters-bar--kpi .filters-row > select.input { max-width: none; }
  /* Диапазон в одну строку не помещается на телефоне: min-content его полей
     растягивал всю форму шире экрана. Подпись уходит на свою строку, поля
     делят остаток и могут сжиматься (min-width: 0 снимает auto у флекс-детей). */
  .filters-range { flex-wrap: wrap; }
  .filters-range__label { flex: 1 1 100%; }
  .filters-range .input { flex: 1 1 120px; min-width: 0; }
}

/* ---------------------------------------------------------------------------
   PVZ video surveillance
   --------------------------------------------------------------------------- */
.camera-panel__head { align-items: flex-start; }
.camera-panel__subtitle { margin-top: 3px; color: var(--color-muted); font-size: 12.5px; }
.camera-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.camera-card {
  display: flex; flex-direction: column; gap: 12px;
  min-width: 0; padding: 14px; border: 1px solid var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface-soft);
}
.camera-card__media {
  position: relative; min-height: 126px; border-radius: var(--radius-md);
  display: flex; align-items: center; justify-content: center;
  color: rgba(255,255,255,.82); background:
    linear-gradient(150deg, rgba(16,18,22,.97), rgba(23,32,50,.9)),
    radial-gradient(circle at 80% 12%, rgba(0,91,255,.45), transparent 48%);
  overflow: hidden;
}
.camera-card__media::after {
  content: ""; position: absolute; inset: 0;
  background: repeating-linear-gradient(0deg, rgba(255,255,255,.04) 0 1px, transparent 1px 5px);
}
.camera-card__icon { position: relative; z-index: 1; width: 54px; height: 54px; display: inline-flex; align-items: center; justify-content: center; }
.camera-card__icon svg { width: 54px; height: 54px; }
/* Плашка эфира. Красная пульсирующая «LIVE» — только состояние --live, то есть
   картинка реально идёт: в сетке это подтверждает свежий превью-кадр, в плеере —
   события <video>. Остальные состояния гаснут в серую пилюлю. */
.camera-live {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 9px; border-radius: var(--radius-pill);
  border: 1px solid transparent;
  background: color-mix(in srgb, var(--color-danger) 88%, #000);
  color: #fff; font-size: 11px; font-weight: 850; letter-spacing: .08em;
}
.camera-live::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%; background: #fff;
  box-shadow: 0 0 0 0 rgba(255,255,255,.68); animation: live-pulse 1.45s infinite;
}
.camera-live--checking,
.camera-live--off {
  background: color-mix(in srgb, var(--color-surface) 84%, var(--color-border));
  border-color: var(--color-border); color: var(--color-muted);
  font-weight: 750; letter-spacing: .02em;
}
.camera-live--checking::before,
.camera-live--off::before { background: currentColor; box-shadow: none; animation: none; }
.camera-live--checking::before { animation: live-blink 1.5s ease-in-out infinite; }
/* Плашка лежит поверх тёмного кадра — светлая пилюля там теряется. */
.camera-card__media .camera-live--checking,
.camera-card__media .camera-live--off {
  background: rgba(7,11,18,.6); border-color: rgba(255,255,255,.18); color: rgba(255,255,255,.84);
}
@keyframes live-blink { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
.camera-card__media .camera-live { position: absolute; z-index: 2; top: 10px; left: 10px; }
/* Всё окно превью — кликабельная зона «Смотреть» (прозрачная кнопка поверх кадра). */
.camera-card__play {
  position: absolute; inset: 0; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  border: 0; padding: 0; cursor: pointer; color: #fff;
  background: transparent; transition: background .18s ease;
}
.camera-card__play:hover,
.camera-card__play:focus-visible {
  outline: none;
  background: linear-gradient(to top, rgba(0,91,255,.30), rgba(7,11,18,.10));
}
.camera-card__play-icon {
  display: grid; place-items: center; width: 52px; height: 52px;
  border-radius: 50%; background: rgba(7,11,18,.55); backdrop-filter: blur(3px);
  border: 1.5px solid rgba(255,255,255,.55);
  opacity: 0; transform: scale(.9);
  transition: opacity .18s ease, transform .18s ease;
}
.camera-card__play:hover .camera-card__play-icon,
.camera-card__play:focus-visible .camera-card__play-icon { opacity: 1; transform: scale(1); }
.camera-card__play-icon svg { width: 20px; height: 20px; margin-left: 2px; }
@media (hover: none) { .camera-card__play-icon { opacity: .8; } }
.camera-card__body { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.camera-card__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.camera-card__title { min-width: 0; font-size: 15px; font-weight: 800; overflow-wrap: anywhere; }
.camera-card__description { color: var(--color-muted); font-size: 13px; min-height: 38px; }
.camera-card__meta { display: flex; gap: 8px; flex-wrap: wrap; color: var(--color-muted); font-size: 12px; }
.camera-card__meta span {
  display: inline-flex; padding: 4px 8px; border-radius: var(--radius-pill);
  background: var(--color-surface); border: 1px solid var(--color-border);
}
.camera-card__actions,
.camera-card__admin { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.camera-card__admin { padding-top: 10px; border-top: 1px solid var(--color-border); }
/* Результат последней проверки камеры — только в форме ПВЗ (админский список).
   В плеере и в сетке статус не показывается: там говорит сама плашка эфира. */
.camera-status {
  display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
  padding: 4px 9px; border-radius: var(--radius-pill); font-size: 11px;
  font-weight: 800;
}
.camera-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.camera-status--online { background: var(--color-success-soft); color: var(--color-success); }
.camera-status--offline { background: var(--color-surface); color: var(--color-muted); }
.camera-status--unknown { background: var(--color-warning-soft); color: var(--color-warning); }
.camera-status--error { background: var(--color-danger-soft); color: var(--color-danger); }
.camera-modal[hidden] { display: none; }
/* z-index 120: выше sidebar (40) и дропдаунов topbar (60), ниже палитры (150) и тостов (200). */
.camera-modal { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: 24px; }
.camera-modal__backdrop { position: absolute; inset: 0; background: rgba(7,17,31,.72); backdrop-filter: blur(9px); }
.camera-modal__dialog {
  position: relative; z-index: 1; width: min(1120px, 100%); max-height: min(calc(86vh * var(--vu)), 860px);
  display: flex; flex-direction: column; gap: 14px; padding: 18px;
  border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
  border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-pop);
}
.camera-modal__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; flex-wrap: wrap;
}
.camera-modal__pvz { color: var(--color-muted); font-size: 12.5px; font-weight: 650; }
.camera-modal__title { margin-top: 3px; font-size: 20px; font-weight: 850; }
.camera-modal__head-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.camera-player {
  position: relative; min-height: 420px; border-radius: var(--radius-md);
  overflow: hidden; background: var(--color-bg-deep); border: 1px solid rgba(255,255,255,.08);
}
.camera-player--page { min-height: min(calc(68vh * var(--vu)), 720px); }
/* Один индикатор загрузки: компактная пилюля поверх постера (последнего кадра
   камеры). Полноэкранный шиммер убран — вместе с постером он читался как
   «вторая картинка загрузки» и прятал живой кадр. */
.camera-player__loader {
  position: absolute; inset: 0; z-index: 2; display: grid; place-items: center;
  color: rgba(255,255,255,.82); pointer-events: none;
}
.camera-player__loader[hidden] { display: none; }
/* Stage прибит к рамке плеера: иначе video берёт высоту по своим пропорциям,
   вырастает выше плеера и низ кадра срезается overflow'ом. */
.camera-player__stage { position: absolute; inset: 0; }
.camera-player__stage,
.camera-player__stage video,
.camera-player__stage iframe,
.camera-player__stage img { width: 100%; height: 100%; min-height: inherit; }
.camera-player__stage video,
.camera-player__stage iframe,
.camera-player__stage img { display: block; border: 0; object-fit: contain; background: var(--color-bg-deep); }
.camera-player__message {
  min-height: inherit; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 4px; padding: 40px 28px; text-align: center;
  color: rgba(255,255,255,.72);
  background:
    radial-gradient(120% 90% at 50% 30%, rgba(255,255,255,.05), transparent 62%),
    var(--color-bg-deep);
}
.camera-player__message-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 64px; height: 64px; margin-bottom: 12px; border-radius: 50%;
  color: rgba(255,255,255,.68);
  background: rgba(255,255,255,.06);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.09);
}
.camera-player__message-icon svg { width: 30px; height: 30px; }
.camera-player__message-title {
  font-size: 17px; font-weight: 800; letter-spacing: -0.01em;
  color: rgba(255,255,255,.94);
}
.camera-player__message-text {
  max-width: 42ch; font-size: 13.5px; line-height: 1.55;
  color: rgba(255,255,255,.6);
}
.camera-player__message-retry,
.camera-player__message > .btn { margin-top: 16px; }
.camera-player__message--warning .camera-player__message-icon {
  color: #f7d28d;
  background: rgba(247,210,141,.13);
  box-shadow: inset 0 0 0 1px rgba(247,210,141,.28);
}
.camera-player__message--error .camera-player__message-icon {
  color: #ff9da0;
  background: rgba(255,157,160,.13);
  box-shadow: inset 0 0 0 1px rgba(255,157,160,.28);
}
.camera-watch { gap: 16px; }
.camera-modal-open, .quick-modal-open { overflow: hidden; }

/* ---- Камеры всей сети (вкладка «Камеры» раздела «Сеть») ----
   Одна строка = один ПВЗ: заголовок (код · название + статус открытия за
   сегодня), под ним карточки камер этого ПВЗ. Обводка карточек повторяет
   статус: зелёная — открыт вовремя, оранжевая — с опозданием, красная — нет. */
.camrow-list { display: flex; flex-direction: column; gap: 18px; }
.camrow {
  padding: 14px 16px 16px; border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); background: var(--color-surface);
}
.camrow__head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding-bottom: 12px; margin-bottom: 14px;
  border-bottom: 1px solid var(--color-border);
}
.camrow__pvz {
  display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  color: var(--color-text); font-weight: 800; text-decoration: none;
}
.camrow__pvz:hover .camrow__name { color: var(--color-primary); }
.camrow__pvz svg { width: 17px; height: 17px; flex: 0 0 auto; color: var(--color-muted); }
.camrow__code {
  flex: 0 0 auto; padding: 3px 9px; border-radius: var(--radius-pill);
  border: 1px solid var(--color-border); background: var(--color-surface-soft);
  font-size: 12px; font-weight: 800; letter-spacing: .02em;
}
.camrow__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; }
.camrow__count { margin-left: auto; color: var(--color-muted); font-size: 12.5px; font-weight: 650; }
.camrow__cameras { display: flex; flex-wrap: wrap; gap: 14px; }
.camrow__cameras .camera-card--net { flex: 1 1 300px; max-width: 460px; min-width: 260px; }

/* Заметная обводка карточек: цвет = статус открытия ПВЗ сегодня. */
.camera-card--net { border-width: 2px; transition: border-color .2s ease, box-shadow .2s ease; }
.camera-card--opened {
  border-color: color-mix(in srgb, var(--color-success) 62%, var(--color-border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-success) 28%, transparent),
              0 10px 26px -14px color-mix(in srgb, var(--color-success) 55%, transparent);
}
.camera-card--late {
  border-color: color-mix(in srgb, var(--color-warning) 70%, var(--color-border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-warning) 32%, transparent),
              0 10px 26px -14px color-mix(in srgb, var(--color-warning) 60%, transparent);
}
.camera-card--unopened {
  border-color: color-mix(in srgb, var(--color-danger) 70%, var(--color-border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-danger) 34%, transparent),
              0 10px 26px -14px color-mix(in srgb, var(--color-danger) 60%, transparent);
}
/* Строка тоже слегка подсвечивается статусом. */
.camrow--late { border-color: color-mix(in srgb, var(--color-warning) 40%, var(--color-border)); }
.camrow--unopened { border-color: color-mix(in srgb, var(--color-danger) 40%, var(--color-border)); }

/* «+ Камера» в заголовке строки: добавление точно в этот ПВЗ. Кнопка тихая —
   рядом переключатель открытия, и она не должна с ним спорить за внимание. */
.camrow__add {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  padding: 5px 11px 5px 8px; border-radius: var(--radius-pill);
  border: 1px dashed var(--color-border); background: transparent;
  color: var(--color-muted); font-size: 12.5px; font-weight: 750; cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.camrow__add svg { width: 14px; height: 14px; }
.camrow__add:hover {
  color: var(--color-primary-dark); border-color: var(--color-primary);
  border-style: solid; background: var(--color-primary-soft);
}
.camrow__add:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
/* Только что добавленная камера (редирект с ?added=<pk>) — короткая подсветка,
   иначе в сетке из десятков карточек новую пришлось бы искать глазами. */
.camera-card--fresh { animation: camera-card-fresh 1.6s var(--ease) backwards; }
@keyframes camera-card-fresh {
  0%   { box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 55%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* ---- Точки без камер ----------------------------------------------------
   Пункт без камеры в сетку не попадает (показывать нечего), а камера ему как
   раз и нужна: отдельный блок — единственный путь к нему из этой вкладки. */
/* Блок — такая же карточка, как строка ПВЗ выше: иначе он висит на полотне
   голым текстом и страница выглядит недоделанной. */
.camgap {
  margin-top: 18px; padding: 14px 16px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface);
}
.camgap[open] { padding-bottom: 16px; }
.camgap__summary {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  cursor: pointer; list-style: none;
}
.camgap[open] > .camgap__summary {
  margin-bottom: 14px; padding-bottom: 12px;
  border-bottom: 1px solid var(--color-border);
}
.camgap__summary::-webkit-details-marker { display: none; }
/* Своя стрелка: у нативного маркера нет ни поворота, ни цвета темы. */
.camgap__summary::before {
  content: "›"; color: var(--color-muted); font-size: 16px; font-weight: 800;
  transition: transform .15s var(--ease); transform-origin: center;
}
.camgap[open] > .camgap__summary::before { transform: rotate(90deg); }
.camgap__title { font-size: 15px; font-weight: 800; }
.camgap__sub { color: var(--color-muted); font-size: 12.5px; font-weight: 650; }
.camgap__search { max-width: 340px; margin-bottom: 10px; }
.camgap__empty { color: var(--color-muted); font-size: 13px; margin-top: 10px; }
.camgap__empty[hidden] { display: none; }
.camgap__grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.camgap__item {
  display: flex; align-items: center; gap: 9px; width: 100%; min-width: 0;
  padding: 9px 10px 9px 11px; border: 1px dashed var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface-soft);
  color: var(--color-text); font-family: inherit; text-align: left; cursor: pointer;
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.camgap__item:hover, .camgap__item:focus-visible {
  border-color: var(--color-primary); border-style: solid;
  background: var(--color-primary-soft); outline: none;
}
.camgap__name {
  min-width: 0; flex: 1 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13.5px; color: var(--color-muted);
}
.camgap__item:hover .camgap__name { color: var(--color-primary-dark); }
.camgap__plus {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--color-surface); color: var(--color-muted);
}
.camgap__plus svg { width: 13px; height: 13px; }
.camgap__item:hover .camgap__plus { background: var(--color-primary); color: var(--color-on-primary); }

/* Шапка вкладки «Камеры»: справа только «Добавить камеру». Отметку открытия
   отсюда убрали — поле времени и переключатель рядом с кнопкой читались тремя
   случайными контролами разного роста и формы. */
.page__actions--cams { align-items: flex-start; }
.cams-add svg { width: 16px; height: 16px; }

/* Панель отметки открытия над сеткой: рамка связывает подпись, поле времени и
   переключатель в одно действие и объясняет, что это за «09:00». Переключатель
   внутри — прежний партиал (те же id и HTMX-обмен со строками камер). */
.cams-openbar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin-bottom: 16px; padding: 12px 16px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface);
}
.cams-openbar__icon {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: var(--radius-md);
  background: var(--color-surface-soft); color: var(--color-muted);
}
.cams-openbar__icon svg { width: 19px; height: 19px; }
.cams-openbar__lead { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 260px; }
.cams-openbar__title { font-size: 14px; font-weight: 800; }
.cams-openbar__hint { color: var(--color-muted); font-size: 12.5px; line-height: 1.35; }
/* Внутри панели блок переключателя прижат вправо и стоит по центру строки. */
.cams-openbar .open-all { flex: 0 0 auto; align-items: flex-end; }

@media (max-width: 720px) {
  .cams-openbar { align-items: flex-start; }
  .cams-openbar .open-all { width: 100%; align-items: flex-start; }
  /* Базис 260px не влезал рядом с иконкой на узких телефонах, и та срывалась на
     собственную строку — панель начиналась с одинокого квадратика. */
  .cams-openbar__lead { flex-basis: 0; }
}

/* ---- Попап быстрого добавления (.camadd — камеры, .qadd — сотрудники) ----
   Заводить запись уходом на отдельную страницу неудобно: и камер на точку, и
   людей на ПВЗ заводят пачкой. Форма приезжает поверх списка, ПВЗ уже выбран.
   Оболочка общая на оба попапа — расходятся только внутренности.
   [hidden] объявлен ПЕРЕД display — иначе правило .camadd/.qadd перебивает
   браузерное [hidden]{display:none} и попап висит открытым (известная ловушка). */
.camadd[hidden], .qadd[hidden] { display: none; }
/* z-index 122: над плеером камеры (120), под палитрой (150) и тостами (200). */
.camadd, .qadd { position: fixed; inset: 0; z-index: 122; display: grid; place-items: center; padding: 24px; }
.camadd__backdrop, .qadd__backdrop { position: absolute; inset: 0; background: rgba(7,17,31,.66); backdrop-filter: blur(7px); }
.camadd__dialog, .qadd__dialog {
  position: relative; z-index: 1; width: min(680px, 100%);
  max-height: calc(90vh * var(--vu)); overflow: auto;
  padding: 20px 22px 18px; background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop);
  animation: camadd-in .18s var(--ease);
}
@keyframes camadd-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.camadd__head, .qadd__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.camadd__eyebrow, .qadd__eyebrow { color: var(--color-muted); font-size: 12.5px; font-weight: 700; }
.camadd__title, .qadd__title { margin-top: 2px; font-size: 18px; font-weight: 850; }
.camadd__note, .qadd__note { margin-top: 12px; }
/* Итог «добавили» остаётся в попапе, пока его не закрыли: подтверждение нужно
   здесь же, страница под попапом обновится при закрытии. У сотрудников тут же
   логин и временный пароль — унести их редиректом нельзя, показываются раз. */
.camadd__done, .qadd__done {
  display: flex; align-items: flex-start; gap: 8px; margin-top: 14px;
  padding: 9px 12px; border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--color-success) 12%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-success) 38%, var(--color-border));
  color: var(--color-text); font-size: 12.5px; font-weight: 650; line-height: 1.4;
}
.camadd__done-mark, .qadd__done-mark { color: var(--color-success); font-weight: 900; }
.qadd__creds, .qadd__hint { display: block; margin-top: 4px; }
.qadd__creds code {
  padding: 1px 6px; border-radius: var(--radius-sm);
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  font-weight: 800; user-select: all;
}
.qadd__hint { color: var(--color-muted); font-weight: 600; }
.camadd__grid, .qadd__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 16px; }
.camadd__grid--more, .qadd__grid--more { margin-top: 12px; }
.camadd__cell--full, .qadd__cell--full { grid-column: 1 / -1; }
.camadd__grid .form-field, .qadd__grid .form-field { min-width: 0; }
/* Шаблоны ссылок: чаще всего камера — Hikvision или Dahua, и человек всё равно
   гуглит формат RTSP-строки. Подставляем скелет, остаётся вписать своё. */
.camadd__presets { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.camadd__presets-label { color: var(--color-muted); font-size: 12px; font-weight: 650; }
.camadd__preset {
  padding: 3px 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--color-border); background: var(--color-surface-soft);
  color: var(--color-muted); font-size: 12px; font-weight: 700; cursor: pointer;
  transition: color .14s var(--ease), border-color .14s var(--ease);
}
.camadd__preset:hover { color: var(--color-primary-dark); border-color: var(--color-primary); }
.camadd__more, .qadd__more { margin-top: 16px; border-top: 1px solid var(--color-border); padding-top: 12px; }
.camadd__more-summary, .qadd__more-summary { cursor: pointer; font-size: 13px; font-weight: 750; }
.camadd__foot, .qadd__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--color-border);
  flex-wrap: wrap;
}
.camadd__policy { color: var(--color-muted); font-size: 12px; line-height: 1.35; max-width: 46ch; }
.camadd__actions, .qadd__actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }

/* Ссылка на полную форму — единственная длинная подпись в подвале попапа.
   У .btn стоит white-space: nowrap, и на узком экране она вылезала за край
   диалога вместе с горизонтальной прокруткой. */
.qadd__full { min-width: 0; white-space: normal; text-align: left; line-height: 1.3; }

/* Блок ручного пароля в создании пользователя: скрыт классом, а не [hidden] —
   у него своё display, и оно перебило бы атрибут (известная ловушка). */
.uadd-pw { display: none; }
.uadd-pw.is-open { display: grid; gap: 12px; margin-top: 12px; }
.uadd-creds__row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 4px; }

@media (max-width: 620px) {
  .camadd, .qadd { padding: 10px; }
  .camadd__dialog, .qadd__dialog { padding: 14px; max-height: calc(94vh * var(--vu)); }
  .camadd__grid, .qadd__grid { grid-template-columns: 1fr; }
  .camadd__actions, .qadd__actions { width: 100%; }
  .camadd__actions .btn, .qadd__actions .btn { flex: 1 1 auto; }
}

/* Строка только что заведённого сотрудника: в списке на сотню фамилий иначе
   непонятно, куда он встал (сортировка по ФИО, а не по времени создания). */
/* Подсветка идёт СВОИМ слоем (inset-тень во всю ячейку), а не свойством
   background. Причина: анимация в каскаде стоит выше обычных объявлений, и все
   четыре секунды правило ховера не работало — человек только что завёл учётку,
   ведёт мышь к её строке, а единственная строка во всей таблице не отзывается.
   Ровно её он в этот момент и ищет глазами. Цвет прозрачный, поэтому ховер
   ложится поверх, а не вместо. */
.row-just-added > td {
  box-shadow: inset 0 0 0 999px color-mix(in srgb, var(--color-success) 13%, transparent);
  animation: row-just-added-fade 4s var(--ease) forwards;
}
@keyframes row-just-added-fade {
  0%, 55% { box-shadow: inset 0 0 0 999px color-mix(in srgb, var(--color-success) 13%, transparent); }
  100% { box-shadow: inset 0 0 0 999px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .row-just-added > td { animation: none; }
}

/* Превью-кадр камеры поверх подложки; пока не загрузился — видна иконка. */
.camera-card__frame {
  position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%;
  object-fit: cover; background: var(--color-bg-deep);
}
.camera-card__frame[hidden] { display: none; }
/* Первое появление кадра — мягкий fade вместо резкой подмены иконки. */
.camera-card__frame--in { animation: camera-frame-in .45s ease; }
@keyframes camera-frame-in { from { opacity: 0; } to { opacity: 1; } }
/* Кадр загружен — заглушка-иконка не должна висеть поверх изображения. */
.camera-card__frame:not([hidden]) ~ .camera-card__icon { opacity: 0; }
.camera-card--net .camera-card__media { aspect-ratio: 16 / 9; min-height: 0; }
.camera-card__foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.camera-card__foot .camera-card__title { font-size: 14.5px; }
.camera-card__foot .camera-card__actions { flex: 0 0 auto; }
.camera-card__foot .btn-sm svg { width: 15px; height: 15px; }

/* Плашка статуса открытия в заголовке строки ПВЗ. */
.cam-open-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 12px; border-radius: var(--radius-pill);
  border: 1.5px solid currentColor;
  font-size: 12px; font-weight: 850; letter-spacing: .02em;
}
.cam-open-pill::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor;
}
.cam-open-pill--opened {
  color: var(--color-success);
  background: color-mix(in srgb, var(--color-success) 12%, transparent);
}
.cam-open-pill--late {
  color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 13%, transparent);
}
.cam-open-pill--unopened {
  color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 12%, transparent);
}
.cam-open-pill--unopened::before {
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-danger) 55%, transparent);
  animation: live-pulse 1.45s infinite;
}
/* ---- Выбор режима фиксации открытий (карточки-радио) ---- */
.mode-option {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 16px; margin-top: 10px;
  border: 1.5px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface); cursor: pointer;
  transition: border-color .18s ease, background .18s ease;
}
.mode-option:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); }
.mode-option.is-active {
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 7%, var(--color-surface));
}
.mode-option input { margin-top: 3px; flex: 0 0 auto; accent-color: var(--color-primary); }
.mode-option__body { display: flex; flex-direction: column; gap: 5px; }
.mode-option__title {
  display: flex; align-items: center; gap: 8px;
  font-weight: 750; font-size: 14.5px; color: var(--color-text);
}
.mode-option__desc { font-size: 13px; line-height: 1.5; color: var(--color-muted); }

/* ---- Переключатель «ПВЗ открыт» в заголовке строки камер ----
   Тот же силуэт, что и у плашки статуса (.cam-open-pill): админ листает сетку
   и должен видеть один и тот же объект, просто нажимаемый. */
.open-switch { display: inline-flex; align-items: center; gap: 10px; margin: 0; }
.open-switch__btn {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 4px 13px 4px 5px; border-radius: var(--radius-pill);
  border: 1.5px solid currentColor; background: color-mix(in srgb, var(--color-danger) 12%, transparent);
  color: var(--color-danger);
  font: inherit; font-size: 12px; font-weight: 850; letter-spacing: .02em;
  cursor: pointer; transition: background .18s ease, color .18s ease, border-color .18s ease;
}
.open-switch__btn:hover { background: color-mix(in srgb, var(--color-danger) 20%, transparent); }
.open-switch__btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.open-switch.is-on .open-switch__btn {
  color: var(--color-success);
  background: color-mix(in srgb, var(--color-success) 12%, transparent);
}
.open-switch.is-on .open-switch__btn:hover {
  background: color-mix(in srgb, var(--color-success) 20%, transparent);
}
/* Дорожка со скруглённым бегунком — читается как switch, а не как кнопка. */
.open-switch__track {
  position: relative; flex: 0 0 auto; width: 30px; height: 18px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, currentColor 24%, transparent);
  transition: background .18s ease;
}
.open-switch__knob {
  position: absolute; top: 3px; left: 3px; width: 12px; height: 12px;
  border-radius: 50%; background: currentColor;
  transition: transform .18s cubic-bezier(.3, 1.4, .5, 1);
}
.open-switch.is-on .open-switch__knob { transform: translateX(12px); }
.open-switch:not(.is-on) .open-switch__knob {
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-danger) 55%, transparent);
  animation: live-pulse 1.45s infinite;
}
/* Пока летит HTMX-запрос: кнопка заблокирована (hx-disabled-elt) — гасим её,
   чтобы двойной клик по строке не читался как «не сработало». */
.open-switch__btn[disabled] { opacity: .55; cursor: progress; }
.open-switch__error {
  font-size: 12px; font-weight: 650; color: var(--color-danger);
  max-width: 320px; line-height: 1.35;
}

/* ---- «Открыть все ПВЗ»: тот же переключатель, но в шапке страницы ----
   Крупнее строкового: это решение по всей сети, и оно не должно теряться
   среди двадцати одинаковых кнопок под ним. */
.open-all {
  display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
  max-width: min(420px, 100%);
}
.open-all:empty { display: none; }
/* Тот же рост, что у поля времени и кнопки «Добавить камеру» рядом
   (--control-h-sm): три разных высоты в одной строке шапки читались как брак.
   nowrap — из-за счётчика: «Открыть все ПВЗ (66)» переносился на вторую
   строку и один переключатель становился выше всей строки. */
.open-switch--all .open-switch__btn {
  padding: 6px 16px 6px 7px; font-size: 12.5px; min-height: var(--control-h-sm);
}
.open-switch__label { white-space: nowrap; }
.open-switch--all .open-switch__track { width: 34px; height: 20px; }
.open-switch--all .open-switch__knob { width: 14px; height: 14px; }
.open-switch--all.is-on .open-switch__knob { transform: translateX(14px); }
.open-all__count {
  font-size: 12px; font-weight: 700; color: var(--color-muted); letter-spacing: .01em;
}
/* Сводка после нажатия: сколько реально отмечено, сколько пропущено и почему. */
.open-all__note {
  font-size: 12px; font-weight: 650; line-height: 1.35; text-align: right;
  color: var(--color-muted);
}
.open-all__note--ok { color: var(--color-success); }
.open-all__note--warn { color: var(--color-warning); }
.open-all__note--error { color: var(--color-danger); }
/* Поле времени на вкладке «Камеры» — общее с панелью «Открытий»
   (.open-bar__field--sm): голое «09:00» без подписи не объясняло само себя. */

/* ---- «Кроме…»: исключения массовой отметки (.oexcept) ---------------------
   Список живёт в попапе, а не разложен на странице: исключают одну-две точки
   из десятков, и постоянно висящий столбец галочек забирал бы экран у того,
   ради чего сюда пришли (камеры, таблица событий). Свёрнутый вид несёт то
   единственное, что важно снаружи, — сколько точек снято. */
.oexcept { position: relative; flex: 0 0 auto; }
/* Кнопка приставлена к главной вплотную (.open-bar__pair) и намеренно тише её:
   это не второе равное действие, а оговорка к первому — «отметить всех, но не
   этих». Раньше она стояла отдельной серой пилюлей вровень по весу с «Отметить
   открытие», да ещё и с многоточием в подписи, из-за которого читалась как
   обрезанный текст. Число снятых теперь несёт счётчик, а обещание списка —
   стрелка вниз. */
.oexcept__trigger {
  display: inline-flex; align-items: center; gap: 7px;
  height: var(--control-h); padding: 0 13px;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface); color: var(--color-muted);
  font: inherit; font-size: 13px; font-weight: 700; white-space: nowrap;
  cursor: pointer;
  transition: border-color .16s var(--ease), color .16s var(--ease),
              background .16s var(--ease), box-shadow .16s var(--ease);
}
.oexcept__trigger svg { width: 15px; height: 15px; flex: none; color: inherit; }
.oexcept__caret { width: 12px; height: 12px; margin-left: -1px; opacity: .8; transition: transform .18s var(--ease); }
.oexcept__trigger:hover { background: var(--color-surface-soft); color: var(--color-text); }
.oexcept__trigger:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
/* Список раскрыт — кнопка «нажата»: рамка primary и перевёрнутая стрелка. */
.oexcept.is-open .oexcept__trigger { border-color: var(--color-primary); color: var(--color-primary); }
.oexcept.is-open .oexcept__caret { transform: rotate(180deg); }
/* Выбор сделан — это уже не приглашение, а действующее ограничение соседней
   кнопки, поэтому цвет предупреждающий, а не нейтральный. */
.oexcept.is-set .oexcept__trigger {
  border-color: color-mix(in srgb, var(--color-warning) 55%, transparent);
  background: var(--color-warning-soft); color: var(--color-warning);
}
/* Счётчик снятых точек: число в подписи («Кроме 3») скакало по ширине и дёргало
   соседнюю кнопку при каждой галочке. */
.oexcept__badge[hidden] { display: none; }
.oexcept__badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 19px; height: 19px; padding: 0 5px; border-radius: var(--radius-pill);
  background: var(--color-warning); color: #fff;
  font-size: 11.5px; font-weight: 800; line-height: 1;
}
/* Компактная шкура — рядом с переключателем на вкладке «Камеры», где рост всей
   строки задан --control-h-sm. */
.oexcept--sm .oexcept__trigger { height: var(--control-h-sm); padding: 0 12px; font-size: 12.5px; }

/* [hidden] объявлен ПЕРЕД display — иначе правило перебивает браузерное
   [hidden]{display:none} и список висит раскрытым (ловушка проекта). */
.oexcept__pop[hidden] { display: none; }
.oexcept__pop {
  position: absolute; z-index: 60; top: calc(100% + 6px); right: 0;
  /* Шире прежних 360px: у снятой строки появилось поле времени, и в 360 код,
     адрес, состояние и «в 09:40» жили друг у друга на голове. */
  width: min(400px, calc(100vw - 32px));
  display: flex; flex-direction: column;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-pop);
  overflow: hidden;
}
.oexcept__head { padding: 12px 14px 10px; border-bottom: 1px solid var(--color-border); }
/* Заголовок и крестик в одной строке: закрытие ищут глазами в углу списка, а
   не среди кнопок подвала рядом с действием (27.08.2026). */
.oexcept__headline { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.oexcept__title { display: block; font-size: 13.5px; font-weight: 800; color: var(--color-heading); }
.oexcept__close {
  flex: none; display: grid; place-items: center; width: 24px; height: 24px; margin: -3px -4px 0 0;
  border: 0; border-radius: var(--radius-sm); background: none; color: var(--color-faint);
  cursor: pointer; transition: background-color .12s var(--ease), color .12s var(--ease);
}
.oexcept__close svg { width: 15px; height: 15px; }
.oexcept__close:hover { background: var(--color-surface-soft); color: var(--color-text); }
.oexcept__close:focus-visible { outline: none; box-shadow: var(--ring); }
.oexcept__hint { margin: 3px 0 9px; font-size: 12px; line-height: 1.4; color: var(--color-muted); }
.oexcept__tools { display: flex; flex-direction: column; gap: 8px; }
input.oexcept__search { min-height: var(--control-h-sm); height: auto; padding: 6px 10px; font-size: 13px; }
.oexcept__only {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; font-weight: 650; color: var(--color-muted); cursor: pointer; user-select: none;
}
.oexcept__only input { accent-color: var(--color-primary); }

.oexcept__list { max-height: min(320px, 46vh); overflow: auto; margin: 0; padding: 6px; list-style: none; }
.oexcept__list li[hidden] { display: none; }
/* Строка = сама точка (label с галочкой) + поле времени рядом с ней. Поле не
   внутри label намеренно: клик по стрелкам часов снимал бы галочку строки. */
.oexcept__row { display: flex; align-items: center; gap: 6px; }
.oexcept__item {
  display: flex; align-items: center; gap: 10px; flex: 1 1 auto; min-width: 0;
  padding: 7px 8px; border-radius: var(--radius-sm); cursor: pointer;
  transition: background .14s var(--ease);
}
.oexcept__item:hover { background: var(--color-surface-soft); }
.oexcept__item input { position: absolute; opacity: 0; width: 0; height: 0; }
.oexcept__box {
  position: relative; flex: none; width: 18px; height: 18px;
  border: 1.5px solid var(--color-border); border-radius: 6px;
  background: var(--color-surface); transition: all .16s var(--ease);
}
/* Галочка означала бы «выбрано», а точка тут наоборот выбывает: рисуем минус —
   тот же знак, что и на кнопке «Кроме». */
.oexcept__box::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 9px; height: 2px; border-radius: 1px; background: #fff;
  transform: scaleX(0); transition: transform .16s var(--ease);
}
.oexcept__item input:checked + .oexcept__box {
  background: var(--color-warning); border-color: var(--color-warning);
}
.oexcept__item input:checked + .oexcept__box::after { transform: scaleX(1); }
.oexcept__item input:focus-visible + .oexcept__box { box-shadow: var(--ring); }
.oexcept__body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.oexcept__code { font-size: 13px; font-weight: 700; color: var(--color-text); }
.oexcept__addr {
  font-size: 11.5px; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Исключённая строка гаснет и зачёркивается: состояние видно взглядом по всему
   списку, а не по положению квадратиков. */
.oexcept__item:has(input:checked) .oexcept__code { text-decoration: line-through; }
.oexcept__item:has(input:checked) .oexcept__body { opacity: .55; }
.oexcept__state {
  flex: none; font-size: 11px; font-weight: 750; letter-spacing: .01em;
  padding: 2px 8px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, currentColor 13%, transparent);
}
.oexcept__state--on_time { color: var(--color-success); }
.oexcept__state--late { color: var(--color-warning); }
.oexcept__state--not_opened { color: var(--color-danger); }
.oexcept__empty { margin: 0; padding: 14px; font-size: 12.5px; color: var(--color-muted); text-align: center; }

/* Время конкретной точки. Показывается только у снятой строки: 68 полей разом
   — это не список точек, а бланк, который никто не заполнит. У снятой строки
   «не открыт» одновременно с полем времени не нужен — про это она и есть. */
.oexcept__time { display: none; }
.oexcept__row:has([data-oexcept-check]:checked) .oexcept__time {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  height: 28px; padding: 0 9px;
  border: 1px solid color-mix(in srgb, var(--color-warning) 45%, var(--color-border));
  border-radius: var(--radius-sm); background: var(--color-surface);
}
.oexcept__row:has([data-oexcept-check]:checked) .oexcept__state--not_opened { display: none; }
.oexcept__time:focus-within {
  border-color: var(--color-warning); box-shadow: var(--ring-soft);
}
.oexcept__time-label { font-size: 11px; font-weight: 750; color: var(--color-muted); }
/* Собственные border/background — иначе .input тянет свои 42px и рамку в рамке. */
input[type="time"].oexcept__time-input {
  width: auto; min-width: 56px; height: auto; min-height: 0; padding: 0;
  border: 0; background: transparent; box-shadow: none; outline: none;
  font-size: 12.5px; font-weight: 750; color: var(--color-text);
}

/* Второй заход «эти открылись позже»: время + отметить только снятых.
   Выделен фоном — это отдельное действие, а не продолжение списка. */
.oexcept__apply[hidden] { display: none; }
.oexcept__apply {
  display: flex; flex-direction: column; gap: 8px;
  padding: 11px 12px; border-top: 1px solid var(--color-border);
  background: color-mix(in srgb, var(--color-warning) 8%, transparent);
}
.oexcept__apply-lead { font-size: 12px; font-weight: 700; color: var(--color-warning); }
.oexcept__apply-row { display: flex; align-items: center; gap: 8px; }
.oexcept__apply-row .open-bar__field { flex: 0 0 auto; background: var(--color-surface); }
.oexcept__apply-row .btn { flex: 1 1 auto; }
.oexcept__apply-row .btn[disabled] { opacity: .5; cursor: default; }
/* «Из них уже отмечено: 2 — им исправим время». Тем же цветом, что и заголовок
   блока: это не примечание мелким шрифтом, а предупреждение о том, что нажатие
   перепишет уже записанный факт. [hidden] прописан явно — иначе его перебило бы
   собственное display. */
.oexcept__apply-note { margin: 0; font-size: 11.5px; line-height: 1.35; color: var(--color-muted); }
.oexcept__apply-note b { color: var(--color-warning); font-weight: 800; }
.oexcept__apply-note[hidden] { display: none; }
.oexcept__foot {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-top: 1px solid var(--color-border);
  background: var(--color-surface-alt);
}
/* Счётчик у правого края: слева от него теперь одна кнопка, и центрирование
   оставляло бы дыру между ними. */
.oexcept__sum { flex: 1 1 auto; text-align: right; font-size: 12px; font-weight: 700; color: var(--color-muted); }
/* Счётчик «кроме N» в подписи под кнопкой (обе шкуры). */
[data-oexcept-note] { color: var(--color-warning); font-weight: 750; }
[data-oexcept-note][hidden] { display: none; }
[data-oexcept-blocked][hidden] { display: none; }

@media (max-width: 720px) {
  /* На телефоне кнопка «Кроме…» встаёт в общий столбец контролов панели, а
     список раскрывается на всю её ширину — 360px сбоку от узкого экрана уже
     не помещаются. Якорем остаётся сама кнопка (position: relative), иначе
     попап отрывается от неё и накрывает заголовок панели. */
  .oexcept { width: 100%; }
  .oexcept__trigger { width: 100%; justify-content: center; }
  .oexcept__pop { left: 0; right: 0; width: auto; }
  /* Ниже — подвал со «Сбросить»: при 46vh он уезжал за нижний край. */
  .oexcept__list { max-height: 38vh; }
  /* В одну строку подвал не помещается: «Отметим точек: 66» ломалось пополам
     и сжимало кнопку. Счётчик уходит на свою строку над ней. */
  .oexcept__foot { flex-wrap: wrap; }
  .oexcept__sum { order: -1; flex: 1 0 100%; text-align: left; }
  /* На телефоне контролы страницы растягиваются на всю ширину — «Сбросить»
     иначе занимает её целиком. Третий класс в селекторе — чтобы перебить тот
     блок весом. */
  .oexcept .oexcept__foot .btn { flex: 0 0 auto; width: auto; min-width: 0; }
}

/* ---- «Открыть все ПВЗ» в шапке вкладки «Открытия» (.open-tools) ------------
   Раньше здесь была широкая карточка с иконкой, заголовком и полоской
   прогресса. Она стояла ровно над рядом плиток, который говорит то же самое
   («Точек в сети», «Вовремя», «С опозданием»), и на полотне во всю ширину
   половину её занимала пустота. Действие переехало в строку выбора дня, где и
   живут все остальные органы управления экраном; числа остались плиткам.

   Отсюда и размеры: всё в --control-h-sm, как соседние «Весь месяц» и
   «Отметить вручную», — три разных высоты в одной строке читались бы как брак. */
.open-all--tools { display: contents; }
.open-tools { display: inline-flex; align-items: center; gap: 8px; flex: none; }
/* Пояснение «отмечать нечего» — не постоянная подсказка, а ответ на действие
   человека: он снял в списке все оставшиеся точки, и кнопка обнулилась. */
.open-tools__blocked[hidden] { display: none; }
.open-tools__blocked {
  flex: 1 1 100%; margin-top: 2px;
  font-size: 12px; font-weight: 650; color: var(--color-warning);
}
/* Поле времени с подписью ВНУТРИ рамки: вынесенная наверх подпись стояла над
   одним контролом из трёх и ломала общую линию строки. */
.open-bar__field {
  display: inline-flex; align-items: center; gap: 9px; flex: none;
  height: var(--control-h); padding: 0 13px;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface-soft);
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease);
}
.open-bar__field:focus-within { border-color: var(--color-primary); box-shadow: var(--ring-soft); }
/* Компактная шкура — строка шапки на вкладке «Камеры» живёт в --control-h-sm. */
.open-bar__field--sm { height: var(--control-h-sm); padding: 0 11px; gap: 7px; background: var(--color-surface); }
.open-bar__field--sm .open-bar__field-input { font-size: 13px; }
.open-bar__field-label {
  font-size: 11.5px; font-weight: 700; color: var(--color-muted);
  white-space: nowrap; letter-spacing: .01em;
}
/* Собственные border/background/height — иначе .input тянет свои 42px и рамку
   внутри рамки. */
input[type="time"].open-bar__field-input {
  width: auto; min-width: 68px; height: auto; min-height: 0; padding: 0;
  border: 0; background: transparent; box-shadow: none; outline: none;
  font-size: 14px; font-weight: 700; color: var(--color-text);
}
/* Разное плановое время у точек: одно значение подходит не всем, и об этом
   надо сказать не текстом под шапкой, а самим полем. */
.open-bar__field--warn { border-color: color-mix(in srgb, var(--color-warning) 60%, transparent); }
/* «Кроме» и главное действие — одной сборкой, встык: два отдельных контрола
   одинакового веса читались как выбор из двух кнопок, хотя левая только
   уточняет правую. */
.open-bar__pair { display: inline-flex; align-items: stretch; flex: none; }/* Единственная настоящая «склейка» двух кнопок в продукте. После перевода .btn
   на таблетку стык надо было пересобрать: внешние кромки берут полный радиус
   таблетки, внутренние остаются прямыми. Разделительная линия переехала с
   border-left на inset-тень — при скруглении в 999px рамка встаёт по хорде и
   торчит уголком за пределы дуги. Левой половине добавлено горизонтальное поле:
   на прямоугольнике текст стоял у самого края, на таблетке он сел бы на дугу. */
.open-bar__pair .oexcept__trigger {
  border-radius: var(--radius-pill) 0 0 var(--radius-pill);
  border-right-width: 0; padding-left: 18px; padding-right: 14px;
}
.open-bar__pair > .btn { border-radius: 0 var(--radius-pill) var(--radius-pill) 0; padding-right: 22px; }
.open-bar__pair > .btn-primary { box-shadow: inset 1px 0 0 var(--color-primary-dark); }
.open-bar__pair > .btn-ghost { border: 1px solid var(--color-border); border-left: 0; }
/* Невидимый дублёр главного действия: он стоит в форме первым, поэтому Enter в
   любом поле делает «Отметить открытие», а не действие из попапа «Кроме»
   (браузер выбирает первую submit-кнопку в DOM). */
.open-bar__default-submit {
  position: absolute; width: 1px; height: 1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

@media (max-width: 720px) {
  /* На телефоне строка шапки и так собрана в столбик — контролы тянутся на всю
     ширину, как остальные кнопки страницы. */
  .open-tools { flex-wrap: wrap; width: 100%; }
  .open-bar__field { width: 100%; }
  .open-bar__field-input { flex: 1 1 auto; }
  /* Встык только на широком экране: в столбик сборка развалилась бы на две
     кнопки со срезанными углами. */
  .open-bar__pair { flex-direction: column; align-items: stretch; gap: 8px; width: 100%; }
  .open-bar__pair .oexcept__trigger { border-radius: var(--radius-pill); border-right-width: 1px; }
  .open-bar__pair > .btn { border-radius: var(--radius-pill); border-left: 0; box-shadow: none; width: 100%; }

  /* Вторая шкура того же действия — переключатель «Открыть все ПВЗ» на вкладке
     «Камеры». Про столбик знала только .open-tools выше, а .open-switch не
     переносит строки вовсе: правило .open-bar__field{width:100%} растягивало
     поле времени внутри неразрывного ряда, ряд разъезжался на 1300+ пикселей и
     выталкивал главную кнопку далеко за правый край — нажать её было нечем,
     страница просто уезжала вбок. */
  .open-switch--all { flex-wrap: wrap; width: 100%; gap: 8px; }
  .open-switch--all > .open-bar__field,
  .open-switch--all .oexcept,
  .open-switch--all .oexcept__trigger,
  .open-switch--all .open-switch__btn { width: 100%; }
  .open-switch--all .oexcept__trigger,
  .open-switch--all .open-switch__btn { justify-content: center; }
}

@media (max-width: 720px) {
  .camrow { padding: 12px; }
  .camrow__count { display: none; }
  .camrow__cameras .camera-card--net { max-width: none; }
  .open-switch__error { max-width: none; }
  .open-all { align-items: flex-start; max-width: none; }
  .open-all__note { text-align: left; }
}

/* ---- PVZ form: schedule editor (Пн–Вс вместо JSON) ----
   Плитками, а не столбцом: семь строк подряд занимали пол-экрана формы, при
   этом шесть из них — почти пустые. auto-fit сам решает, сколько колонок
   помещается в карточку. */
.schedule-editor {
  display: grid;
  /* min(): без него на узком экране колонка остаётся 380px и вылезает за карточку.
     380, а не 320: в 320 не помещался ряд целиком (галочка + «Понедельник» +
     два поля времени + тире), и на широкой форме в две колонки название дня
     обрезалось соседним полем — «Понедель», «Воскресе». */
  grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr));
  gap: 8px;
}
.schedule-editor__row {
  display: flex; align-items: center; gap: 12px; padding: 7px 12px; min-height: 46px;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface-soft);
  transition: border-color .16s var(--ease), background .16s var(--ease);
}
/* Рабочий день читается кромкой, а не только галочкой. */
.schedule-editor__row.is-open {
  border-color: color-mix(in srgb, var(--color-primary) 42%, var(--color-border));
  background: var(--color-primary-soft, rgba(0, 91, 255, .09));
}
.schedule-editor__day {
  display: flex; align-items: center; gap: 10px; flex: 1 1 auto; min-width: 0;
  font-size: 13.5px; font-weight: 650; cursor: pointer;
  white-space: nowrap;
}
.schedule-editor__day input { width: 16px; height: 16px; flex: 0 0 auto; accent-color: var(--color-primary); }
.schedule-editor__times { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.schedule-editor__times[hidden] { display: none; }
.schedule-editor__times .input {
  width: 92px; padding: 5px 8px; min-height: var(--control-h-sm); font-size: 13px;
}
.schedule-editor__dash { color: var(--color-muted); }
.schedule-editor__closed { color: var(--color-muted); font-size: 13px; flex: 0 0 auto; }
.schedule-editor__closed[hidden] { display: none; }
.schedule-editor__head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
}
.schedule-editor__actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- PVZ form: нередактируемое значение (код точки) ----
   Раньше здесь стоял disabled-input: он выглядел как поле, в которое просто
   не пускают. Плашка честнее — сразу видно, что это справочное значение. */
.field-static {
  margin: 0; display: flex; align-items: center; min-height: var(--control-h);
  padding: 8px 13px; border-radius: var(--radius-md);
  border: 1px dashed var(--color-border-strong, var(--color-border));
  background: var(--color-surface-soft); color: var(--color-text);
  font-size: 14px; font-weight: 700; letter-spacing: .01em; overflow-wrap: anywhere;
}

/* Пустое состояние внутри карточки формы — рамкой, а не «забытым» абзацем. */
.form-empty {
  margin: 0; padding: 16px; text-align: center;
  border: 1px dashed var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface-soft); color: var(--color-muted); font-size: 13px;
}
.form-empty[hidden] { display: none; }

/* ---- PVZ form: важные контакты (название + телефон строками) ---- */
.contacts-editor { display: flex; flex-direction: column; gap: 10px; }
.contacts-editor:empty { display: none; }
.contacts-editor__row {
  display: flex; align-items: flex-end; gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface-soft);
}
.contacts-editor__field { display: flex; flex-direction: column; gap: 5px; flex: 1 1 0; min-width: 0; }
/* Телефон уже узкой «Названия»: номер короче и предсказуемой длины. */
.contacts-editor__field:last-of-type { flex: 0 0 210px; }
.contacts-editor__caption { font-size: 12px; font-weight: 600; color: var(--color-muted); }
.contacts-editor__field .input { width: 100%; }
.contacts-editor__remove { flex: 0 0 auto; }

@media (max-width: 720px) {
  .contacts-editor__row { flex-direction: column; align-items: stretch; }
  .contacts-editor__field:last-of-type { flex: 1 1 auto; }
  .contacts-editor__remove { align-self: flex-end; }
}

/* ---- PVZ card: важные контакты столбцом ---- */
.pvz-contacts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.pvz-contacts__item {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--color-border);
}
.pvz-contacts__item:last-child { border-bottom: 0; }
.pvz-contacts__icon {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--color-primary-soft, rgba(0,91,255,.09));
  color: var(--color-primary);
}
.pvz-contacts__icon svg { width: 16px; height: 16px; }
.pvz-contacts__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pvz-contacts__label {
  font-size: 13.5px; font-weight: 650; color: var(--color-text);
  overflow-wrap: anywhere;
}
.pvz-contacts__phone {
  font-size: 14px; font-variant-numeric: tabular-nums; letter-spacing: .01em;
  color: var(--color-text); text-decoration: none;
}
/* Кликабельным выглядит только реальный tel:-номер, не «сырая» строка. */
a.pvz-contacts__phone { color: var(--color-primary); }
a.pvz-contacts__phone:hover { text-decoration: underline; }

/* ---- PVZ form: address autocomplete (Nominatim) ---- */
.address-autocomplete { position: relative; display: block; }
.address-autocomplete .input { width: 100%; }
.address-suggest {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 30;
  display: flex; flex-direction: column; padding: 6px; max-height: 260px; overflow-y: auto;
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-md);
  background: var(--color-surface); box-shadow: var(--shadow-pop);
}
.address-suggest[hidden] { display: none; }
.address-suggest__item {
  display: block; width: 100%; padding: 9px 11px; border: 0; border-radius: 8px;
  background: transparent; color: var(--color-text); text-align: left;
  font: inherit; font-size: 13.5px; line-height: 1.4; cursor: pointer;
}
.address-suggest__item:hover,
.address-suggest__item.is-active { background: var(--color-primary-soft, rgba(0,91,255,.09)); }

/* ---- PVZ form: map picker (Яндекс Карты JS API) ---- */
/* min-width:0 — грид-ячейка по умолчанию не уже своего min-content, а разметка
   Яндекс-карты держит инлайновую ширину от момента инициализации. Без этого при
   сужении окна карта распирала колонку и всю форму уводило вправо. */
.pvz-form .form-field--full { min-width: 0; }
.map-picker {
  height: 320px; width: 100%; min-width: 0;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  overflow: hidden; background: var(--color-surface-soft);
  /* Изолирующий stacking context: внутренние z-index карты не должны
     спорить с topbar/дропдаунами страницы. */
  position: relative; z-index: 0;
}
/* Тёмная тема: приглушаем тайлы (у Яндекса подложка — ymaps-*-ground-pane). */
[data-theme="dark"] .map-picker [class*="ground-pane"] { filter: brightness(.82) contrast(1.04) saturate(.9); }
.map-picker--unavailable {
  display: grid; place-items: center; padding: 20px;
  color: var(--color-muted); font-size: 13px; text-align: center;
}

.form-grid--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 819px) {
  .form-grid--three { grid-template-columns: 1fr; }
}

/* ---- PVZ form: всплывающий гайд по виджету отзывов и мини-список камер ---- */
.guide-modal[hidden] { display: none; }
/* z-index 120: как у camera-modal — выше sidebar (40) и дропдаунов topbar (60). */
.guide-modal { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: 24px; }
.guide-modal__backdrop { position: absolute; inset: 0; background: rgba(7,17,31,.72); backdrop-filter: blur(9px); }
.guide-modal__dialog {
  position: relative; z-index: 1; width: min(560px, 100%); max-height: min(calc(86vh * var(--vu)), 640px);
  overflow: auto; display: flex; flex-direction: column; gap: 14px; padding: 20px;
  border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
  border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-pop);
}
.guide-modal__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 14px;
}
.guide-modal__title { font-size: 18px; font-weight: 850; }
.guide-modal__steps {
  margin: 0; padding-left: 22px; display: flex; flex-direction: column; gap: 10px;
  font-size: 14px; line-height: 1.5;
}
.guide-modal__note { color: var(--color-muted); font-size: 12.5px; line-height: 1.5; }
.guide-modal-open { overflow: hidden; }
@media (max-width: 640px) {
  .guide-modal { padding: 10px; }
  .guide-modal__dialog { max-height: calc(94vh * var(--vu)); padding: 14px; }
}
.camera-mini-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.camera-mini-list__row {
  display: flex; align-items: center; gap: 10px; padding: 9px 2px; min-height: 46px;
}
.camera-mini-list__row + .camera-mini-list__row { border-top: 1px solid var(--color-border); }
.camera-mini-list__name { font-weight: 650; font-size: 14px; min-width: 0; overflow-wrap: anywhere; }
.camera-mini-list__edit { margin-left: auto; }
.role-check-grid ul {
  list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;
}
.role-check-grid label {
  display: flex; align-items: center; gap: 8px; min-height: 38px;
  padding: 8px 10px; border: 1px solid var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface-soft);
}
@keyframes live-pulse {
  70% { box-shadow: 0 0 0 8px rgba(255,255,255,0); }
  100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); }
}
@media (max-width: 944px) {
  .camera-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .camera-grid,
  .role-check-grid ul { grid-template-columns: 1fr; }
  .camera-card__actions .btn,
  .camera-card__admin .btn,
  .camera-card__admin form { width: 100%; }
  .camera-card__admin .btn,
  .camera-card__actions .btn { justify-content: center; }
  /* Подвал карточки в сетке камер. Правило .camera-card__actions .btn{width:100%}
     выше писалось для админского списка камер в форме ПВЗ, где кнопки и должны
     идти столбиком. Здесь же подвал — ряд «название · действия», и стопроцентная
     ширина ломала его лесенкой: «Смотреть» и ссылка вставали друг под друга и
     наползали на кнопку чата. Название занимает свою строку, кнопки — свою:
     «Смотреть» тянется по ширине, иконка «в отдельном окне» остаётся при ней. */
  .camera-card--net .camera-card__foot { flex-wrap: wrap; }
  .camera-card--net .camera-card__title { flex: 1 1 100%; }
  .camera-card--net .camera-card__actions { flex: 1 1 100%; flex-wrap: nowrap; }
  .camera-card--net .camera-card__actions .btn { width: auto; flex: 1 1 auto; }
  .camera-card--net .camera-card__actions .btn-secondary { flex: 0 0 auto; }
  .camera-modal { padding: 10px; }
  .camera-modal__dialog { max-height: calc(94vh * var(--vu)); padding: 12px; }
  .camera-player { min-height: 300px; }
  .schedule-editor__row { flex-wrap: wrap; row-gap: 8px; }
  .schedule-editor__day { flex: 1 0 100%; }
  .schedule-editor__times .input { width: 96px; }
}

/* =========================================================================
   Reports module (stage 14)
   ========================================================================= */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 18px;
}
.report-card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 14px;
  padding: 20px;
  min-height: 148px;
  text-decoration: none;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  transition: transform 0.18s var(--ease), box-shadow 0.18s var(--ease),
    border-color 0.18s var(--ease);
}
.report-card:hover,
.lift-hover:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-pop);
  border-color: var(--color-primary);
}
.report-card__title {
  margin: 0 0 6px;
  font-size: 1.05rem;
  font-weight: 700;
}
.report-card__desc {
  margin: 0;
  color: var(--color-muted);
  font-size: 0.9rem;
  line-height: 1.4;
}
.report-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.report-card__cta {
  color: var(--color-primary-dark);
  font-weight: 600;
  font-size: 0.9rem;
}

/* ---- Отчёты: плашка «почему тут нули» -----------------------------------
   Бухгалтерская выгрузка не считает деньги сама — она показывает расчётные
   листы. Если расчёт за период не запускали, вся денежная половина таблицы
   печатает 0,00 ₽, и без подписи это читается как «система не считает».
   ------------------------------------------------------------------------ */
.report-notice {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 10px 16px;
  margin-bottom: 16px; padding: 12px 16px;
  border: 1px solid color-mix(in srgb, var(--color-warning) 45%, var(--color-border));
  border-left-width: 3px; border-radius: var(--radius-md);
  background: var(--color-warning-soft); color: var(--color-warning);
  font-size: 13px; line-height: 1.5;
}
.report-notice--info {
  border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
  background: var(--color-primary-soft); color: var(--color-primary-dark);
}
.report-notice__text { flex: 1 1 320px; min-width: 0; }
/* Ghost-кнопка на цветной подложке читается как обычный текст — плашке нужна
   своя: белая подложка и рамка в тон. */
.report-notice .btn {
  flex: none;
  background: var(--color-surface);
  border-color: color-mix(in srgb, var(--color-warning) 40%, var(--color-border));
  color: var(--color-warning);
}
.report-notice .btn:hover { background: color-mix(in srgb, var(--color-warning) 12%, var(--color-surface)); }
.report-notice--info .btn {
  border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-border));
  color: var(--color-primary-dark);
}
.report-notice--info .btn:hover { background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface)); }

/* ---- Отчёты: карточки по смысловым группам (stage 125) ------------------
   Четырнадцать одинаковых карточек стеной не читались: глаз искал «где про
   деньги», а находил алфавит. Группа = заголовок с иконкой и своим акцентом,
   акцент наследуется карточками группы через --rep-accent.
   ------------------------------------------------------------------------ */
.rep-groups { display: flex; flex-direction: column; gap: 34px; }

.rep-group { --rep-accent: var(--color-primary); --rep-accent-soft: var(--color-primary-soft); }
.rep-group[data-accent="money"] { --rep-accent: var(--color-warning); --rep-accent-soft: var(--color-warning-soft); }
.rep-group[data-accent="people"] { --rep-accent: var(--color-violet); --rep-accent-soft: var(--color-violet-soft); }
.rep-group[data-accent="network"] { --rep-accent: var(--color-success); --rep-accent-soft: var(--color-success-soft); }

.rep-group__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-bottom: 12px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--color-border);
}
/* Подложки под знаком БОЛЬШЕ НЕТ (09.2026). Пастельный сквиркл под каждой
   иконкой — приём, повторённый в системе 22 раза по одному рецепту (радиус
   ≈30% стороны, глиф ≈52%), и на этой странице их одновременно 19 штук: глаз
   читает не девятнадцать знаков, а один орнамент. Плюс сами пастельные токены
   неразличимы — все шесть *-soft лежат в коридоре светимости 0,878-0,885,
   контраст к белой карточке 1,12:1, то есть акцент группы не группировал
   ничего. Знак теперь стоит прямо на карточке и держится собственной массой:
   глифы залиты (app/common/icons.py), им подложка не нужна.

   Прецедент в продукте уже был принят: .dsec .metric-card__icon { display:
   none } (ниже по файлу) сняли по этой же жалобе 10.08.2026. */
.rep-group__mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 26px;
  height: 26px;
  color: var(--rep-accent);
}
.rep-group__mark svg { width: 24px; height: 24px; }
.rep-group__text { min-width: 0; flex: 1 1 auto; }
.rep-group__title {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--color-heading);
}
.rep-group__hint { margin: 2px 0 0; font-size: 0.82rem; color: var(--color-muted); }
.rep-group__count {
  flex: none;
  min-width: 24px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--color-surface-soft);
  color: var(--color-faint);
  font-size: 0.78rem;
  font-weight: 700;
  text-align: center;
}

.rep-group__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
  gap: 14px;
}

/* Базовая .report-card распределяет содержимое space-between — в ряду с
   разной длиной описаний из-за этого разъезжались иконки и названия.
   Здесь всё прижато к верху, а вниз уходит только подвал. */
.rep-card { gap: 12px; padding: 18px; min-height: 0; justify-content: flex-start; }
.rep-card__icon {
  display: grid;
  place-items: center;
  justify-self: start;
  width: 26px;
  height: 26px;
  color: var(--rep-accent);
  transition: transform 0.18s var(--ease);
}
.rep-card__icon svg { width: 25px; height: 25px; }
/* .report-card__body — flex с gap:12px; в компактной карточке подзаголовок
   должен идти сразу под названием, поэтому расстояние задаёт только margin. */
.rep-card .report-card__body { gap: 0; }
.rep-card .report-card__title { font-size: 1rem; margin-bottom: 5px; }
.rep-card .report-card__desc { font-size: 0.86rem; }
.rep-card .report-card__foot { margin-top: auto; padding-top: 2px; }
.rep-card .report-card__cta { display: inline-flex; align-items: center; gap: 5px; margin-left: auto; }
.rep-card__arrow { transition: transform 0.18s var(--ease); }
/* Отклик на наведение раньше держала ЗАЛИВКА плитки — единственное, что у
   карточки менялось. Плитки нет, поэтому отклик собран заново: рамка в акцент
   группы, лёгкий подъём знака и стрелка. Без этого карточка молчала бы на
   наведение, а кликается она целиком. */
.rep-card:hover { border-color: var(--rep-accent); }
.rep-card:hover .rep-card__icon { transform: translateY(-2px); }
.rep-card:hover .rep-card__arrow { transform: translateX(3px); }

/* «Главное» — две самые частые точки входа в раздел, поэтому строка отдана им
   целиком: карточка лежит в строку (иконка — текст — «Открыть»). auto-fit, а
   не auto-fill: пустые дорожки схлопываются, и две карточки делят ширину
   пополам, сколько бы их ни осталось после проверки прав. */
.rep-group[data-accent="main"] .rep-group__grid {
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.rep-group[data-accent="main"] .rep-card {
  flex-direction: row;
  align-items: center;
  gap: 16px;
  padding: 20px;
}
/* В «Главном» карточка лежит в строку, и ширину первой колонки задавала
   плитка 42px. Плитки нет — ширину держит сам знак, поэтому он тут крупнее:
   иначе текст прижался бы к краю. */
.rep-group[data-accent="main"] .rep-card__icon { width: 32px; height: 32px; }
.rep-group[data-accent="main"] .rep-card__icon svg { width: 30px; height: 30px; }
.rep-group[data-accent="main"] .report-card__body { flex: 1 1 auto; }
.rep-group[data-accent="main"] .report-card__foot { margin: 0; padding: 0; flex: none; }

@media (max-width: 720px) {
  .rep-group[data-accent="main"] .rep-card { flex-direction: column; align-items: stretch; }
  .rep-group[data-accent="main"] .report-card__foot { margin-top: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .rep-card:hover { transform: none; }
  .rep-card__arrow { transition: none; }
}

.filter-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 150px;
}
.filter-field__label {
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--color-muted);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.filter-field__error {
  color: var(--color-danger);
  font-size: 0.74rem;
}
.filter-field--actions {
  flex-direction: row;
  align-items: flex-end;
  gap: 8px;
}
/* Галочки отчёта («Учитывать KPI», «Учитывать претензии») живут ОДНОЙ ячейкой
   сетки фильтров: поодиночке каждая занимала колонку в 190px, галочки
   разъезжались по разным строкам и тянули за собой пустоту. Высота — как у
   поля ввода, иначе группа висит на базовой линии сетки, а не рядом с ними. */
.filter-checks {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 6px 18px; min-height: var(--control-h); min-width: 0;
}
.filter-checks:empty { display: none; }
/* Подпись «цифры на лету, фиксация — выгрузка» под заголовком денежного
   отчёта. Мельче подзаголовка: это сноска о природе чисел, а не описание
   раздела, и она не должна спорить с ним за внимание. */
.page__hint {
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--color-muted);
  max-width: 74ch;
}
.filter-check {
  display: inline-flex; align-items: center; gap: 8px;
  cursor: pointer; white-space: nowrap;
}
.filter-check__input { width: 16px; height: 16px; margin: 0; flex: none; cursor: pointer; }
.filter-check__label { font-size: 13.5px; font-weight: 600; color: var(--color-text); }
.filter-check:hover .filter-check__label { color: var(--color-primary-dark); }
/* Кнопки — своей строкой во всю ширину и вправо: их три, и в обычной ячейке
   сетки они не помещались, вылезая на соседние поля. Тот же приём уже описан
   выше для .filters-bar .form-actions. */
.filters-bar .filter-field--actions { grid-column: 1 / -1; justify-content: flex-end; }
.filters-bar {
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
}

.summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 18px;
  margin-top: 18px;
}
.breadcrumbs {
  font-size: 0.82rem;
  color: var(--color-muted);
  margin-bottom: 4px;
}
.breadcrumbs a {
  color: var(--color-primary-dark);
  text-decoration: none;
}
.breadcrumbs a:hover {
  text-decoration: underline;
}
.table-note {
  margin: 12px 4px 0;
  color: var(--color-muted);
  font-size: 0.82rem;
}
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  margin: 0 0 12px;
}
.detail-grid dt {
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--color-muted);
  text-transform: uppercase;
}
.detail-grid dd {
  margin: 2px 0 0;
  font-weight: 600;
  /* Значения бывают машинными токенами без пробелов
     («import_completed_with_errors»): без переноса «где угодно» такая строка
     не помещалась в 220px-колонку и обрезалась краем карточки. */
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
   Stage 15 — Notifications module (inbox, items, bell dropdown, badges)
   --------------------------------------------------------------------------- */
.badge-muted { background: var(--color-surface-soft); color: var(--color-muted); }
.badge-outline { background: transparent; color: var(--color-muted); border: 1px solid var(--color-border); }

.ta-center { text-align: center; }
.cell-hint { display: block; font-size: 10.5px; margin-top: 2px; }
.field--check { flex-direction: row; align-items: center; gap: 9px; }

.status-badge--unread { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.status-badge--read { background: var(--color-surface-soft); color: var(--color-muted); }
.status-badge--archived { background: var(--color-surface-soft); color: var(--color-muted); opacity: .8; }

/* Filter card ------------------------------------------------------------- */
.filters { display: flex; flex-direction: column; gap: 12px; }
.filters__row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.filters__actions { display: flex; gap: 10px; }

/* Inbox list -------------------------------------------------------------- */
.notif-list { display: flex; flex-direction: column; gap: 10px; }
/* Панель массовых действий над списком (чекбоксы строк — form="notif-bulk-form"). */
.notif-bulk {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 10px 14px; margin-bottom: 10px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-alt);
}
/* wrap обязателен: селектор действия + кнопка в одну строку не помещались на
   360px и растягивали страницу вбок. */
.notif-bulk__actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-left: auto; }
.notif-bulk__select { width: auto; padding-top: 8px; padding-bottom: 8px; }

/* Обобщённая панель массовых действий (KPI; чекбоксы строк — form="...-bulk-form"). */
.bulk-bar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 10px 14px; margin-bottom: 10px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-alt);
}
/* Панель массового действия — строка формы, а не компактный тулбар: кнопки
   держим высотой поля, иначе комментарий (42px) торчит над ними. */
.bulk-bar__actions { display: flex; align-items: center; gap: 10px; margin-left: auto; flex-wrap: wrap; }
.bulk-bar__actions .btn { min-height: var(--control-h); }
.bulk-bar__comment { width: auto; min-width: 220px; padding-top: 8px; padding-bottom: 8px; }
/* Подпись + поле одной строкой: «Дата оплаты» рядом с полем, а не над ним, —
   панель обязана остаться в одну строку, иначе она сама станет «этажом». */
.bulk-bar__field { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.bulk-bar__date { width: auto; min-width: 156px; }
/* Предупреждение о низких баллах среди выбранных — KPI влияет на премию,
   поэтому выброс должен броситься в глаза до нажатия «Утвердить». */
.bulk-bar__warn { color: var(--color-danger); font-size: 13px; font-weight: 600; }
.bulk-bar__warn[hidden] { display: none; }
/* Галочка строки — вне .checkbox намеренно: там input скрыт (opacity: 0)
   под кастомную отрисовку, здесь нужен обычный нативный чекбокс. */
.kpi-check-col { width: 36px; }
.kpi-check-col input { width: 16px; height: 16px; accent-color: var(--color-primary); cursor: pointer; }

/* ---- Выбор точек управляющего (.scope-pick) --------------------------------
   Решение принимается сразу обо всей сети, поэтому точки лежат плиткой, а не
   списком: два десятка ПВЗ должны помещаться на экран целиком — иначе «отметить
   все» и «отметить пять» перестают быть одинаково быстрыми. */
.scope-pick__bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; margin-bottom: 12px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-alt);
}
.scope-pick__bulk { display: flex; align-items: center; gap: 8px; }
.scope-pick__search { flex: 1 1 220px; min-width: 180px; }
.scope-pick__search .input { width: 100%; }
.scope-pick__count { font-size: 13px; font-weight: 600; color: var(--color-muted); margin-left: auto; }
.scope-pick__grid {
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.scope-pick__empty { margin: 12px 2px; color: var(--color-muted); font-size: 13px; }
.scope-pick__empty[hidden] { display: none; }
.scope-item {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; min-width: 0;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface);
  transition: border-color .14s var(--ease), background .14s var(--ease);
}
.scope-item:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); }
.scope-item.is-on { border-color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface)); }
.scope-item[hidden] { display: none; }
.scope-item__body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.scope-item__code { font-size: 13px; font-weight: 600; color: var(--color-text); }
.scope-item__addr {
  font-size: 12px; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.scope-item .badge { margin-left: auto; flex: none; }
/* Кнопки идут сразу за последней карточкой списка — без этого отступа
   «Сохранить» наезжает на её рамку (у .form-actions своих полей нет).
   Правило общее для обоих экранов выбора: и точек управляющего, и прав. */
.scope-pick .form-actions { margin-top: 18px; }

/* ---- Права доступа (.caps) --------------------------------------------------
   Соседняя механика к .scope-pick, но строкой, а не плиткой: у права есть
   пояснение в две строки, и ради него читаемость важнее плотности — собственник
   решает «доверить или нет», а не «отметить двадцать штук быстро». */
.caps-group + .caps-group { margin-top: 20px; }
.caps-group__title {
  margin: 0 0 8px; font-size: 12px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--color-muted);
}
.caps-list { display: grid; gap: 8px; }
.caps-item {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 14px; min-width: 0;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface);
  transition: border-color .14s var(--ease), background .14s var(--ease);
}
.caps-item:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); }
.caps-item.is-on { border-color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface)); }
.caps-item .checkbox__box { margin-top: 2px; flex: none; }
.caps-item__body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.caps-item__label { font-size: 14px; font-weight: 600; color: var(--color-text); }
.caps-item__hint { font-size: 12.5px; line-height: 1.45; color: var(--color-muted); }
.access-list { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
.access-list li { font-size: 13px; line-height: 1.45; }
.notif-item {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 14px 16px; border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-card);
  transition: transform .16s var(--ease), box-shadow .16s var(--ease);
  cursor: pointer; /* клик по строке открывает уведомление (и авто-читает) */
}
.notif-item:hover { transform: translateY(-1px); box-shadow: var(--shadow-pop); }
.notif-item__check { display: inline-flex; align-items: center; margin-top: 2px; cursor: pointer; }
.notif-item__check input { width: 16px; height: 16px; accent-color: var(--color-primary); cursor: pointer; }
.notif-item__dot {
  flex: 0 0 auto; width: 9px; height: 9px; margin-top: 6px; border-radius: 50%;
  background: var(--color-border);
}
.notif-item--unread { border-color: color-mix(in srgb, var(--color-primary) 34%, var(--color-border)); }
.notif-item--unread .notif-item__dot { background: var(--color-primary); }
.notif-item--unread .notif-item__title { font-weight: 800; }
.notif-item--danger.notif-item--unread .notif-item__dot,
.notif-item--critical.notif-item--unread .notif-item__dot { background: var(--color-danger); }
.notif-item--warning.notif-item--unread .notif-item__dot { background: var(--color-warning); }
.notif-item--success.notif-item--unread .notif-item__dot { background: var(--color-success); }
.notif-item--urgent .notif-item__dot { animation: pulse-soft 2.2s ease-in-out infinite; }
.notif-item__body { flex: 1; min-width: 0; }
.notif-item__head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.notif-item__title { font-size: 14.5px; font-weight: 650; color: var(--color-text); }
.notif-item__title:hover { color: var(--color-primary-dark); }
.notif-item__message { margin: 5px 0 0; color: var(--color-muted); font-size: 13.5px; line-height: 1.5; }
.notif-item__meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 7px; font-size: 12px; color: var(--color-muted); }
.notif-item__action { color: var(--color-primary-dark); font-weight: 650; }
/* Правая точка «новое» — та же метка, что в выпадашке колокольчика. */
.notif-item__new {
  flex: 0 0 auto; align-self: flex-start; margin-top: 6px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}
.notif-item__controls { display: flex; flex-direction: column; gap: 5px; flex: 0 0 auto; }
.notif-item__controls .inline-form,
.inline-form { display: inline; }

/* Bell dropdown ----------------------------------------------------------- */
.topbar__bell-wrap { position: relative; }
/* Панель шире прежней (380 против 320) и БЕЗ внутреннего поля: строки идут от
   края до края и разделяются волосяной линией, а не воздухом вокруг карточки —
   восемь уведомлений так читаются одним списком, а не стопкой плиток.
   Ширина через min(): базовое .dropdown-menu на ≤720px сдвигает меню шапки на
   52px вправо, и фиксированные 380px уехали бы за край узкого экрана.
   overflow: hidden обязателен — иначе разделители строк и заливка
   непрочитанного вылезают за скруглённый угол панели. */
.topbar__notif-dropdown {
  width: min(380px, calc(92vw * var(--vu))); padding: 0;
  overflow: hidden; border-radius: var(--radius-xl);
}
.notif-drop__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 15px 11px; }
.notif-drop__head form { margin: 0; }
.notif-drop__head-title { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; color: var(--color-heading); }
/* «Прочитать все» — тихая ссылка справа, а не полноширинный пункт меню:
   глобальное .dropdown-menu button растянуло бы её на всю панель и увело
   заголовок вниз. Приглушена намеренно: это не то действие, ради которого
   открывают колокольчик. */
.topbar__notif-dropdown .notif-drop__read-all {
  width: auto; min-height: 0; padding: 2px 0; border-radius: 0;
  color: var(--color-muted); font-size: 12.5px; font-weight: 600; white-space: nowrap;
}
.topbar__notif-dropdown .notif-drop__read-all:hover { background: transparent; color: var(--color-primary-dark); text-decoration: underline; }
/* Ряд вкладок служит и границей шапки: отдельная линия под заголовком рядом с
   этой читалась бы как вторая шапка. */
.notif-drop__tabs { display: flex; align-items: stretch; justify-content: space-between; gap: 12px; padding: 0 15px; border-bottom: 1px solid var(--color-border); }
.notif-drop__tabs-group { display: flex; align-items: stretch; gap: 18px; }
.notif-drop__tab { position: relative; display: inline-flex; align-items: center; gap: 6px; padding: 3px 0 10px; cursor: pointer; }
/* Радиокнопка растянута по всей вкладке и невидима, но остаётся в потоке и в
   дереве доступности: фильтром управляет ТОЛЬКО она (правила :has в конце
   блока), поэтому убирать её из фокуса нельзя — вкладка перестала бы
   переключаться с клавиатуры, а стрелки внутри группы радио — ровно то
   поведение, которого от ряда вкладок и ждут. */
.notif-drop__filter { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; opacity: 0; appearance: none; cursor: pointer; }
.notif-drop__tab-text { font-size: 13px; font-weight: 650; color: var(--color-muted); }
.notif-drop__tab:hover .notif-drop__tab-text { color: var(--color-text); }
.notif-drop__tab:has(.notif-drop__filter:checked) .notif-drop__tab-text { color: var(--color-heading); font-weight: 750; }
/* Подчёркивание рисуем всегда, прозрачным: появись оно только у активной
   вкладки, соседи прыгали бы на два пикселя при каждом переключении. */
.notif-drop__tab::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; border-radius: 2px 2px 0 0; background: transparent; }
.notif-drop__tab:has(.notif-drop__filter:checked)::after { background: var(--color-heading); }
.notif-drop__filter:focus-visible ~ .notif-drop__tab-text { outline: 2px solid var(--color-primary); outline-offset: 3px; border-radius: 3px; }
/* Счётчик новых — самая контрастная метка панели (в референсе это чёрная
   пилюля). Пара heading/surface переворачивается вместе с темой, поэтому в
   тёмной пилюля становится светлой без единого дополнительного правила. */
.notif-drop__tab-count {
  min-width: 18px; padding: 0 6px; border-radius: var(--radius-pill);
  background: var(--color-heading); color: var(--color-surface);
  font-size: 10.5px; font-weight: 800; line-height: 18px; text-align: center;
}
/* Шестерёнка — второй выход из панели, кроме списка. Правило идёт от
   .topbar__notif-dropdown: общее .dropdown-menu a растянуло бы её на всю
   ширину ряда вкладок. */
.topbar__notif-dropdown .notif-drop__gear {
  width: 28px; height: 28px; min-height: 0; padding: 0; margin: 1px 0 6px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); color: var(--color-faint);
}
.topbar__notif-dropdown .notif-drop__gear:hover { background: var(--color-surface-soft); color: var(--color-text); }
.notif-drop__gear svg { width: 16px; height: 16px; }
/* vh обязана идти через --vu: html { zoom: var(--ui-scale) } ужимает и
   вьюпортные единицы, без множителя список на ноутбуке занял бы полэкрана.
   overscroll-behavior — чтобы, докрутив список до конца, не утащить за собой
   страницу под открытым меню. */
.notif-drop__list {
  display: flex; flex-direction: column;
  max-height: min(400px, calc(56vh * var(--vu)));
  overflow-y: auto; overscroll-behavior: contain;
}
/* Заголовки групп нужны вкладке «Все»: там новое и прочитанное идут одним
   потоком, и границу иначе пришлось бы искать по оттенку заливки. Липкие —
   иначе в прокрутке теряется ответ «это ещё новое или уже разобранное»; фон
   непрозрачный обязателен, иначе под заголовок уезжают подсвеченные строки. */
.notif-drop__group {
  position: sticky; top: 0; z-index: 1; flex: 0 0 auto;
  padding: 11px 15px 5px; background: var(--color-surface);
  font-size: 10px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  color: var(--color-faint);
}
.notif-drop__group--new { color: var(--color-primary-dark); }
/* Селекторы строк намеренно идут от .notif-drop__list: общее правило
   .dropdown-menu a специфичнее одиночного класса (класс + тег), и оно молча
   съедало и фон непрочитанного, и рамку — подсветка строк не работала вовсе. */
.notif-drop__list .notif-drop__item {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 11px 15px; border-radius: 0;
  border-bottom: 1px solid var(--color-border);
  color: var(--color-text);
  background: var(--color-surface);
  /* Список — колоночный флекс с max-height и прокруткой, а флекс-дети по
     умолчанию СЖИМАЮТСЯ, когда содержимое переросло контейнер: без запрета
     нижняя строка текста наезжает на соседнюю. Обрезать нужно список целиком,
     он для того и прокручивается. */
  flex: 0 0 auto;
}
.notif-drop__list .notif-drop__item:last-child { border-bottom: 0; }
/* Ховер не перекрашивает текст: строка — это событие, а не пункт меню, и общее
   .dropdown-menu a:hover уводило весь заголовок в синий. */
.notif-drop__list .notif-drop__item:hover { background: var(--color-surface-soft); color: var(--color-text); }
/* Заливка непрочитанного намеренно едва заметна — работает контраст с
   соседями, а не сам оттенок. Мешаем акцент с НЕПРОЗРАЧНОЙ поверхностью:
   --color-primary-soft в тёмной теме сам полупрозрачный, и любое разбавление
   прозрачным уводило подсветку в невидимое. */
.notif-drop__list .notif-drop__item--unread { background: color-mix(in srgb, var(--color-primary) 7%, var(--color-surface)); }
.notif-drop__list .notif-drop__item--unread:hover { background: color-mix(in srgb, var(--color-primary) 13%, var(--color-surface)); }
/* Фокус — внутренним outline, а не общим кольцом-тенью: список прокручивается
   (overflow-y), и тень в 3px за краем строки просто срезается. */
.notif-drop__list .notif-drop__item:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; box-shadow: none; }
/* Кружок слева — знак категории в цвете важности (почему не аватар — в
   шаблоне). Заливка мешается с --color-surface, а не с прозрачным: на
   подсвеченной строке непрочитанного прозрачная плитка сливалась бы с фоном. */
/* Пастельный кружок снят (09.2026). Он кодировал СРАЗУ ДВА измерения — цвет
   важности и глиф категории — и в списке из восьми строк читался как
   декоративный столбец одинаковых пятен, а не как признак. Осталось по одному
   носителю на измерение: сам знак несёт категорию, его цвет — важность.
   Заливка не нужна: глифы залиты и держат форму сами. */
.notif-drop__icon {
  flex: 0 0 auto; width: 26px; height: 26px;
  display: grid; place-items: center;
  color: var(--color-muted);
}
.notif-drop__icon svg { width: 22px; height: 22px; }
.notif-drop__icon--info { color: var(--color-primary-dark); }
.notif-drop__icon--success { color: var(--color-success); }
.notif-drop__icon--warning { color: var(--color-warning); }
.notif-drop__icon--danger,
.notif-drop__icon--critical { color: var(--color-danger); }
/* Прочитанное тише непрочитанного, но важность у него НЕ гасится: цвет знака —
   единственный зрячий носитель тяжести в этой панели, и «критично, но
   прочитано» обязано остаться красным. Разницу между строками держит подложка
   непрочитанного (.notif-drop__item--unread выше) — она для этого и введена.
   Прозрачность на знак не вешаем: она роняла success и warning ниже 3:1 на
   светлой теме (эту ошибку в блоке уже ловили однажды). Поэтому у прочитанного
   СВОЕГО правила цвета нет вовсе — знак остаётся в цвете важности. */
/* Цветная точка тяжести отдельным элементом в выпадашке больше не рисуется —
   её роль взял цвет кружка. Правила НЕ удалены: этот же класс носит точка в
   модалке непрочитанных после входа (login_notifications_modal.html), и её
   цвета молча погасли бы вместе с ними. */
.notif-drop__sev { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: var(--color-border); }
.notif-drop__sev--info { background: var(--color-primary); }
.notif-drop__sev--success { background: var(--color-success); }
.notif-drop__sev--warning { background: var(--color-warning); }
.notif-drop__sev--danger, .notif-drop__sev--critical { background: var(--color-danger); }
.notif-drop__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
/* Заголовок в две строки: панель стала шире, и обрывать «Опоздание с открытием
   ПВЗ» многоточием на середине больше незачем. Третья строка уже лишняя —
   дальше читают в самом уведомлении. */
.notif-drop__title {
  font-size: 13px; font-weight: 600; line-height: 1.35; color: var(--color-text);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
.notif-drop__item--unread .notif-drop__title { font-weight: 750; color: var(--color-heading); }
.notif-drop__item--read .notif-drop__title { font-weight: 550; color: var(--color-muted); }
/* Мета одной строкой «когда · откуда»: два поля в столбик делали строку в три
   этажа, и восемь уведомлений переставали помещаться в панель. */
.notif-drop__meta { display: flex; align-items: baseline; min-width: 0; font-size: 11.5px; }
/* Мета целиком приглушена, в том числе у непрочитанного: акцентный синий на
   времени перетягивал взгляд с заголовка, а «что случилось» важнее, чем
   «когда». Новизну и без того держат жирный заголовок, заливка строки и точка
   справа — четвёртый сигнал был бы лишним. */
/* Мета на --color-muted, а не на --color-faint: на подсвеченной ховером
   строке faint давал 3.98:1 в светлой теме — ниже AA. Иерархию держат размер
   (11.5 против 13) и вес, а не выцветание. */
.notif-drop__time { flex: 0 0 auto; color: var(--color-muted); }
.notif-drop__item--unread .notif-drop__time { color: var(--color-text); font-weight: 650; }
/* Вторая половина меты — «откуда пришло» (точка, человек, документ). Тише
   заголовка: это то, чем одно «Опоздание с открытием ПВЗ» отличается от
   другого. Одна строка с многоточием — полный текст лежит в title=. */
.notif-drop__source { min-width: 0; color: var(--color-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Разделитель рисуем у источника, а не отдельным узлом: у части событий
   источника нет, и «·» повисала бы в конце строки одна. */
.notif-drop__time + .notif-drop__source::before { content: "·"; margin: 0 5px; color: var(--color-muted); }
/* Точка справа = непрочитанное: видно, что нового, не вчитываясь в текст. */
.notif-drop__new {
  flex: 0 0 auto; align-self: center; width: 8px; height: 8px; border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}
.notif-drop__empty { padding: 30px 16px; text-align: center; color: var(--color-muted); font-size: 13px; }
/* «Всё прочитано» — пустое состояние вкладки «Новые», и показывает его только
   правило фильтра ниже. */
.notif-drop__empty--new { display: none; }
.notif-drop__foot { border-top: 1px solid var(--color-border); }
.topbar__notif-dropdown .notif-drop__foot a {
  display: flex; align-items: center; justify-content: center; gap: 3px;
  min-height: 0; padding: 11px 15px; border-radius: 0;
  color: var(--color-muted); font-size: 12.5px; font-weight: 650;
}
.topbar__notif-dropdown .notif-drop__foot a:hover { background: var(--color-surface-soft); color: var(--color-primary-dark); }
/* Ссылка растянута на всю ширину панели, а панель обрезает всё за краем
   (overflow: hidden) — общее кольцо-тень фокуса срезалось с трёх сторон и
   от индикатора оставалась полоска сверху. Внутренний outline, как у строк. */
.topbar__notif-dropdown .notif-drop__foot a:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; box-shadow: none; }
.notif-drop__foot svg { width: 14px; height: 14px; }
/* Фильтр вкладок целиком на CSS: состояние держит радиокнопка внутри вкладки,
   а :has поднимает его до соседнего списка. Полная цепочка селектора взята не
   для красоты — она обязана перебивать общее .dropdown-menu a (класс + тег),
   иначе display: flex у строки победит скрытие. Без поддержки :has пара
   правил просто не применяется, и панель показывает полный список — то есть
   деградирует до прежнего поведения, а не ломается. */
.notif-drop__tabs:has(.notif-drop__filter--new:checked) ~ .notif-drop__list .notif-drop__item--read,
.notif-drop__tabs:has(.notif-drop__filter--new:checked) ~ .notif-drop__list .notif-drop__group { display: none; }
/* Общее «Уведомлений пока нет» на вкладке «Новые» лишнее: там своё пустое
   состояние. Без этого правила в совсем пустом ящике встречались бы обе
   плашки сразу — список пуст, значит отрендерены и та, и другая. */
.notif-drop__tabs:has(.notif-drop__filter--new:checked) ~ .notif-drop__list .notif-drop__empty:not(.notif-drop__empty--new) { display: none; }
.notif-drop__tabs:has(.notif-drop__filter--new:checked) ~ .notif-drop__list .notif-drop__empty--new { display: block; }
/* Последняя видимая строка вкладки «Новые» — это не :last-child списка
   (за ней стоят скрытые прочитанные), поэтому её собственная граница
   ложилась на границу подвала и линия выходила двойной. За последним
   непрочитанным в разметке всегда идёт заголовок группы «Прочитанные». */
.notif-drop__tabs:has(.notif-drop__filter--new:checked) ~ .notif-drop__list .notif-drop__item--unread:has(+ .notif-drop__group) { border-bottom: 0; }

@media (max-width: 640px) {
  .notif-item { flex-wrap: wrap; }
  .notif-item__controls { flex-direction: row; width: 100%; }
}
/* =========================================================================
   Dashboards (stage 17) — role-based dashboards, universal widgets
   ========================================================================= */
.dash-filters {
  padding: 12px 14px; border-radius: var(--radius-lg);
  background: var(--color-surface-alt); box-shadow: var(--shadow-xs);
}
/* Панель фильтров скрыта (21.07.2026) — форма остаётся носителем полей для
   поповера-календаря. Правило явное: без него любой display у .dash-filters
   победил бы атрибут [hidden] и вернул пустую карточку на дашборд. */
.dash-filters[hidden] { display: none; }
.dash-filters__row { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: flex-end; }
.dash-filters .filter-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.dash-filters .filter-field__label { font-size: 10.5px; color: var(--color-faint); font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
.dash-filters .input { min-width: 140px; height: 40px; }
.dash-filters .filter-field--actions { margin-left: auto; }
/* Пилюли в шапке сводной панели — бейдж периода (открывает календарь, stage
   35), «Настроить» и переключатель сборок. Устроены одинаково (иконка + <b> +
   каретка) и ведут себя одинаково: тихая пилюля проявляет рамку под курсором
   и держит подсветку, пока открыто её меню. Правила лежат общим списком, а не
   у одного бейджа: пока их не было у соседей, браузер рисовал тем свою рамку
   outset и две кнопки выпадали из общего вида (25.08.2026). */
.dash-period,
.dcat__open,
.lswitch__toggle {
  gap: 8px; cursor: pointer; border: 1px solid transparent; font: inherit; line-height: 1;
  transition: background-color .18s var(--ease), border-color .18s var(--ease),
              box-shadow .18s var(--ease), transform .12s var(--ease);
}
.dash-period b,
.dcat__open b,
.lswitch__toggle b { color: var(--color-text); }
.dash-period svg,
.dcat__open svg,
.lswitch__toggle svg { flex: none; }
.dash-period__icon { color: var(--color-primary); flex: none; }
.dash-period__caret { color: var(--color-faint); flex: none; margin-left: 1px; transition: transform .2s var(--ease); }
.dash-period:hover,
.dcat__open:hover,
.lswitch__toggle:hover { background: var(--color-surface); border-color: var(--color-border-strong); box-shadow: var(--shadow-xs); }
.dash-period:hover svg:first-child,
.dcat__open:hover svg:first-child,
.lswitch__toggle:hover svg:first-child { color: var(--color-primary); }
.dash-period:focus-visible,
.dcat__open:focus-visible,
.lswitch__toggle:focus-visible { outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-soft); }
.dash-period[aria-expanded="true"],
.dcat__open[aria-expanded="true"],
.lswitch__toggle[aria-expanded="true"] {
  background: var(--color-surface); border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-soft);
}
.dash-period[aria-expanded="true"] .dash-period__caret,
.lswitch__toggle[aria-expanded="true"] .dash-period__caret { transform: rotate(180deg); color: var(--color-primary); }
.dcat__open[aria-expanded="true"] svg:first-child,
.lswitch__toggle[aria-expanded="true"] svg:first-child { color: var(--color-primary); }
.dash-period:active,
.dcat__open:active,
.lswitch__toggle:active { transform: translateY(0.5px); }

/* ---- Period calendar popover -------------------------------------------
   Справочный календарь (stage 35, переделан в stage 110): месяц, сегодня и
   подсвеченные числа прихода документов OZON. Ничего не выбирается — дни не
   кнопки, поэтому ни hover-обводки диапазона, ни подвала с «Применить». */
.periodcal {
  position: fixed; z-index: 80; display: flex; overflow: hidden;
  border: 1px solid var(--color-border); border-radius: var(--radius-xl);
  background: var(--color-surface); box-shadow: var(--shadow-pop);
  opacity: 0; transform: translateY(-6px) scale(.985); transform-origin: top right;
  transition: opacity .16s var(--ease-out), transform .16s var(--ease-out);
}
.periodcal.is-open { opacity: 1; transform: none; }
.periodcal[hidden] { display: none; }

.periodcal__main { display: flex; flex-direction: column; padding: 12px; width: 300px; max-width: 100%; }
.periodcal__nav { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 8px; }
.periodcal__month { font-weight: 800; font-size: 14px; color: var(--color-heading); letter-spacing: -0.01em; }
.periodcal__navbtn {
  display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-surface); color: var(--color-muted); cursor: pointer;
  transition: background-color .15s var(--ease), color .15s var(--ease), border-color .15s var(--ease);
}
.periodcal__navbtn:hover { background: var(--color-surface-soft); color: var(--color-primary-dark); border-color: var(--color-border-strong); }

.periodcal__week {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 4px;
}
.periodcal__week span {
  text-align: center; font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--color-faint); padding: 4px 0;
}
.periodcal__days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.periodcal__day {
  position: relative; display: grid; place-items: center; height: 36px;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--color-text); font-size: 13px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* Та же развязка, что и в большой сетке: свой месяц крупнее по весу, соседние
   числа уходят в прозрачность, а не просто в серый. */
.periodcal__day.is-outside { color: var(--color-faint); font-weight: 500; opacity: .55; }
.periodcal__day.is-today {
  background: var(--color-primary); color: var(--color-on-primary); font-weight: 800;
  border-color: var(--color-primary);
}

/* Важное число: приходит документ OZON. Фиолетовый тон намеренно не совпадает
   с синим «сегодня» — иначе две разные вещи читались бы как одна. */
.periodcal__day.is-mark {
  background: var(--color-violet-soft);
  border-color: color-mix(in srgb, var(--color-violet) 38%, transparent);
  color: var(--color-violet); font-weight: 800;
}
.periodcal__day.is-mark::after {
  content: ""; position: absolute; left: 50%; bottom: 4px; width: 4px; height: 4px;
  border-radius: 50%; background: currentColor; transform: translateX(-50%);
}
.periodcal__day.is-mark.is-outside { opacity: .5; }
/* Совпало с сегодня — «сегодня» сильнее, метку держит только точка. */
.periodcal__day.is-mark.is-today { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-on-primary); }
/* Недельная метка (рейтинг ПВЗ, каждая среда) — тише месячной: их в сетке
   четыре-пять, и полная заливка перебила бы даты документов. */
.periodcal__day.is-mark-weekly {
  background: transparent; color: var(--color-text);
  border-color: color-mix(in srgb, var(--color-violet) 24%, transparent);
  font-weight: 700;
}
.periodcal__day.is-mark-weekly::after { background: var(--color-violet); opacity: .75; }

.periodcal__legend {
  display: grid; gap: 6px; margin-top: 12px; padding-top: 11px;
  border-top: 1px solid var(--color-border);
}
.periodcal__legend-item { display: flex; align-items: center; gap: 8px; min-width: 0; }
.periodcal__legend-day {
  flex: none; display: grid; place-items: center; width: 22px; height: 22px;
  border-radius: var(--radius-sm);
  border: 1px solid color-mix(in srgb, var(--color-violet) 38%, transparent);
  background: var(--color-violet-soft);
  font-size: 11.5px; font-weight: 800; font-variant-numeric: tabular-nums;
  color: var(--color-violet);
}
.periodcal__legend-title {
  font-size: 12px; font-weight: 600; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

@media (max-width: 560px) {
  /* Ширину задаёт сам календарь (main), поповеру остаётся не вылезти за экран:
     колонки пресетов больше нет, и width:100% схлопывал бы его в ничто. */
  .periodcal { max-width: calc(calc(100vw * var(--vu)) - 24px); }
}

.dash-group { display: flex; flex-direction: column; gap: 14px; margin-top: 6px; }
.dash-group__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.dash-group__title { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; color: var(--color-text); }

/* Opening quick action (stage 36) — hero card «ПВЗ открыт» on the employee
   dashboard: the very first thing after login, one tap records the opening. */
.opening-quick {
  display: flex; flex-direction: column; gap: 12px;
  padding: 18px; margin-bottom: 18px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-primary);
  border-radius: 14px;
  box-shadow: 0 10px 26px -20px rgba(0,0,0,.35);
}
.opening-quick--recorded { border-left-color: var(--color-success); }
.opening-quick--late { border-left-color: var(--color-warning); }
.opening-quick__eyebrow {
  font-size: 11.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-primary);
}
.opening-quick--recorded .opening-quick__eyebrow { color: var(--color-success); }
.opening-quick--late .opening-quick__eyebrow { color: var(--color-warning); }
.opening-quick__title { font-size: 18px; font-weight: 800; letter-spacing: -0.01em; color: var(--color-heading); margin-top: 2px; }
.opening-quick__meta { font-size: 13px; color: var(--color-muted); margin-top: 4px; }
.opening-quick__warn { display: block; margin-top: 4px; font-weight: 700; color: var(--color-warning); }
.opening-quick__form { display: flex; }
.opening-quick__btn {
  width: 100%; min-height: 52px; font-size: 16px; font-weight: 800;
  border-radius: 12px;
}
.opening-quick__hint { font-size: 12px; color: var(--color-faint); }
.opening-quick__hint a { color: var(--color-muted); }
@media (min-width: 720px) {
  .opening-quick__form { max-width: 380px; }
}

/* Плашка «поставьте программу на ПК точки» (stage 83) — посадочные страницы
   кабинета. Задача, а не тревога: тон тёплый, но заметный, и на телефоне
   кнопка уходит вниз во всю ширину — сотрудник читает её с мобильного. */
.agent-cta {
  display: grid; grid-template-columns: auto 1fr; gap: 12px 14px;
  padding: 16px 18px; margin-bottom: 18px;
  background: color-mix(in srgb, var(--color-warning) 6%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-warning) 28%, var(--color-border));
  border-left: 4px solid var(--color-warning);
  border-radius: 14px;
  box-shadow: 0 10px 26px -20px rgba(0,0,0,.35);
}
.agent-cta__icon {
  /* По верху, а не по центру: иконка читается как маркер заголовка, иначе на
     трёхстрочном тексте она уезжает к середине абзаца. */
  align-self: start; margin-top: 2px;
  flex: none; width: 38px; height: 38px; border-radius: 11px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-warning) 18%, transparent);
  color: var(--color-warning);
}
.agent-cta__icon svg { width: 20px; height: 20px; }
.agent-cta__eyebrow {
  font-size: 11.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-warning);
}
.agent-cta__title {
  font-size: 17px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--color-heading); margin-top: 2px;
}
.agent-cta__text { font-size: 13px; color: var(--color-muted); margin-top: 6px; }
.agent-cta__btn { grid-column: 1 / -1; min-height: var(--control-h); font-weight: 750; }
@media (min-width: 720px) {
  .agent-cta { grid-template-columns: auto 1fr auto; align-items: center; }
  .agent-cta__btn { grid-column: auto; align-self: center; white-space: nowrap; }
}

/* Окно «появилась свободная смена» (04.09.2026) — по центру экрана, а не
   тостом в углу: угол человек пропускал так же, как цифру в меню. Слой и
   каркас те же, что у остальных диалогов продукта (.cell-modal, z-index 120):
   над дропдаунами и поповерами, под тостами. */
.xchg-alert[hidden] { display: none; }
.xchg-alert { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: 24px; }
.xchg-alert__backdrop {
  position: absolute; inset: 0;
  background: color-mix(in srgb, var(--color-bg-deep) 72%, transparent);
  backdrop-filter: blur(6px);
}
.xchg-alert__dialog {
  position: relative; width: min(460px, 100%);
  display: grid; justify-items: center; text-align: center; gap: 6px;
  padding: 30px 28px 24px;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-xl); box-shadow: var(--shadow-pop);
  animation: xchg-alert-in .22s var(--ease) both;
}
@keyframes xchg-alert-in {
  from { opacity: 0; transform: translateY(10px) scale(.97); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .xchg-alert__dialog { animation: none; }
}
.xchg-alert__x {
  position: absolute; top: 12px; right: 12px;
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--color-muted); cursor: pointer;
}
.xchg-alert__x:hover { background: var(--color-surface-soft); color: var(--color-text); }
.xchg-alert__x svg { width: 18px; height: 18px; }
.xchg-alert__icon {
  width: 56px; height: 56px; border-radius: 50%; margin-bottom: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-primary) 16%, transparent);
  color: var(--color-primary);
}
.xchg-alert__icon svg { width: 26px; height: 26px; }
.xchg-alert__eyebrow {
  font-size: 11.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-primary);
}
.xchg-alert__title { font-size: 21px; font-weight: 850; letter-spacing: -0.015em; color: var(--color-heading); }
/* Сама смена — плиткой: человек решает по ней, а не по заголовку. */
.xchg-alert__card {
  width: 100%; display: grid; gap: 2px; margin-top: 12px; padding: 14px 16px;
  border-radius: var(--radius-lg); border: 1px solid var(--color-border);
  background: color-mix(in srgb, var(--color-surface) 76%, var(--color-surface-soft));
}
.xchg-alert__day { font-size: 17px; font-weight: 800; color: var(--color-heading); }
.xchg-alert__time { font-size: 15px; font-weight: 750; color: var(--color-primary-dark); font-variant-numeric: tabular-nums; }
.xchg-alert__place { margin-top: 4px; font-size: 13px; color: var(--color-text); }
.xchg-alert__code { color: var(--color-muted); }
.xchg-alert__note { margin-top: 8px; font-size: 12.5px; color: var(--color-muted); }
.xchg-alert__text { margin-top: 12px; font-size: 13px; color: var(--color-muted); }
.xchg-alert__actions {
  display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;
  width: 100%; margin-top: 18px;
}
@media (max-width: 520px) {
  .xchg-alert { padding: 16px; }
  .xchg-alert__actions { flex-direction: column-reverse; }
  .xchg-alert__actions .btn { width: 100%; }
}

/* Плашка «на бирже есть свободные смены» (09.2026) — посадочные страницы
   кабинета. Тот же каркас, что у .agent-cta, но цвет primary: это не долг и
   не тревога, а возможность заработать. */
.xchg-cta {
  display: grid; grid-template-columns: auto 1fr; gap: 12px 14px;
  padding: 16px 18px; margin-bottom: 18px;
  background: color-mix(in srgb, var(--color-primary) 6%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
  border-left: 4px solid var(--color-primary);
  border-radius: 14px;
  box-shadow: 0 10px 26px -20px rgba(0, 0, 0, .35);
}
.xchg-cta__icon {
  align-self: start; margin-top: 2px;
  flex: none; width: 38px; height: 38px; border-radius: 11px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-primary) 18%, transparent);
  color: var(--color-primary);
}
.xchg-cta__icon svg { width: 20px; height: 20px; }
.xchg-cta__eyebrow {
  font-size: 11.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-primary);
}
.xchg-cta__title {
  font-size: 17px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--color-heading); margin-top: 2px;
}
.xchg-cta__text { font-size: 13px; color: var(--color-muted); margin-top: 6px; }
.xchg-cta__btn { grid-column: 1 / -1; min-height: var(--control-h); font-weight: 750; }
@media (min-width: 720px) {
  .xchg-cta { grid-template-columns: auto 1fr auto; align-items: center; }
  .xchg-cta__btn { grid-column: auto; align-self: center; white-space: nowrap; }
}

/* Плашка «укажите свою почту» (stage 116). Тот же язык, что у .agent-cta, но
   с полем ввода вместо кнопки-ссылки: адрес вписывают прямо здесь — переход
   «в настройки» люди откладывают навсегда, а без почты пароль не вернуть.
   Цвет — danger, а не warning: это не совет, а единственный путь назад. */
.mail-cta {
  display: grid; grid-template-columns: auto 1fr; gap: 12px 14px;
  padding: 16px 18px; margin-bottom: 18px;
  background: color-mix(in srgb, var(--color-danger) 6%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-danger) 28%, var(--color-border));
  border-left: 4px solid var(--color-danger);
  border-radius: 14px;
  box-shadow: 0 10px 26px -20px rgba(0,0,0,.35);
}
.mail-cta__icon {
  align-self: start; margin-top: 2px;
  flex: none; width: 38px; height: 38px; border-radius: 11px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-danger) 18%, transparent);
  color: var(--color-danger);
}
.mail-cta__icon svg { width: 20px; height: 20px; }
.mail-cta__eyebrow {
  font-size: 11.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-danger);
}
.mail-cta__title {
  font-size: 17px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--color-heading); margin-top: 2px;
}
.mail-cta__text { font-size: 13px; color: var(--color-muted); margin-top: 6px; }
.mail-cta__form {
  grid-column: 1 / -1;
  display: flex; gap: 8px; flex-wrap: wrap;
}
.mail-cta__input { flex: 1 1 220px; min-width: 0; }
.mail-cta__btn { min-height: var(--control-h); font-weight: 750; white-space: nowrap; }
@media (min-width: 860px) {
  .mail-cta { grid-template-columns: auto 1fr auto; align-items: center; }
  /* Поле уезжает в третью колонку и перестаёт растягиваться на всю ширину:
     на широком экране строка ввода длиной в экран выглядит как поиск. */
  .mail-cta__form { grid-column: auto; align-self: center; flex-wrap: nowrap; }
  .mail-cta__input { flex: 0 1 240px; }
}

/* Строка «Программа на ПК» в карточке ПВЗ и в списках управляющего. */
.agent-state { display: flex; align-items: center; gap: 10px; }
.agent-state__dot {
  flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: var(--color-muted);
}
.agent-state--on .agent-state__dot { background: var(--color-success); }
.agent-state--off .agent-state__dot { background: color-mix(in srgb, var(--color-muted) 60%, transparent); }
.agent-state__text { font-weight: 650; }
.agent-state__sub { display: block; font-size: 12.5px; font-weight: 400; color: var(--color-muted); }

.dash-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; }
/* Cards stretch to the row height (no align-items: start) so a row of mixed
   widgets lines up instead of leaving ragged bottoms. */
.dash-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 18px; }

.dash-metric { transition: transform .18s var(--ease), box-shadow .18s var(--ease); }
.dash-metric:hover { transform: translateY(-2px); box-shadow: 0 12px 30px -18px rgba(0,0,0,.4); }
.dash-metric--critical { border-color: var(--color-danger); box-shadow: 0 0 0 1px var(--color-danger) inset; }
.dash-metric--critical .metric-card__value { color: var(--color-danger); }

.dash-widget { display: flex; flex-direction: column; gap: 12px; }
.dash-widget--critical { border-color: var(--color-danger); }
/* Карточка целиком ведёт в раздел. Наведение обязано это выдавать: без отклика
   человек не догадается, что кликать можно куда угодно, и будет по-прежнему
   целиться в маленькую кнопку «Открыть». Подсвечиваем и её саму — ясно, куда
   именно уводит карточка. */
.dash-widget.row-link:hover { border-color: var(--color-primary); box-shadow: var(--shadow-soft); }
.dash-widget.row-link:hover .card__head-side .btn { color: var(--color-primary); }
/* Плитка кликабельна с самого начала (наложенная ссылка ``metric-card__overlay``),
   но молчала об этом. В одном ряду с карточками, которые теперь на наведение
   отвечают, это читалось бы как «а эта не работает» — отклик у всех один. */
.dsec .metric-card:has(.metric-card__overlay):hover {
  border-color: var(--color-primary); box-shadow: var(--shadow-soft);
}
.dash-widget__body { display: flex; flex-direction: column; gap: 10px; flex: 1; }
/* In a stretched card an empty state / donut centres itself in the free space. */
.dash-widget__body > .empty-state,
.dash-widget__body > .dash-donut { margin-block: auto; }

/* Список виджета раскладывается колонками сам. После снятия потолка ширины
   (--page-max: 100%) блок на всю строку сетки — это 1300–2000px, и строка
   «ПВЗ … 7 дней» превращалась в два слова по краям карточки с пустотой между
   ними. auto-fill решает по месту: в узком блоке (span 3) остаётся одна
   колонка и прежний вид, на полной ширине получается три-четыре.
   Строки при этом стали плитками — как сигналы в «Требует внимания» рядом:
   разделительная линия снизу в несколько колонок оставляла обрывки рамок в
   недозаполненном последнем ряду. */
.dash-list {
  width: 100%;
  display: grid;
  /* min(): иначе на узком экране колонка держит 340px и вылезает за карточку.
     340, а не 260: в узкой плитке адрес переносился на вторую строку, и ряд
     превращался в лесенку из плиток разной высоты. */
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  gap: 8px;
}
.dash-list__item {
  align-items: center;
  padding: 9px 12px;
  border-bottom: 0;
  border-radius: var(--radius-md);
  background: var(--color-surface-soft);
}
/* flex: 1 — текст забирает всю ширину, значение и стрелка держатся правого края
   вместе. Без этого space-between растаскивал три элемента по всей плитке, и
   «7 дней» повисало посередине. */
.dash-list__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.dash-list__label { font-weight: 700; font-size: 13.5px; color: var(--color-text); text-decoration: none; }
a.dash-list__label:hover { color: var(--color-primary-dark); text-decoration: underline; }
.dash-list__sub { font-size: 12px; color: var(--color-muted); }
/* Стрелка — знак «плитка открывается». Видна всегда (бледно), а не только при
   наведении: ссылкой было одно название, и о клике никто не догадывался. */
.dash-list__item[data-row-href] { cursor: pointer; }
.dash-list__go { flex: none; width: 15px; height: 15px; margin-left: -4px; color: var(--color-faint); opacity: .5; transition: opacity .16s var(--ease), transform .16s var(--ease); }
.dash-list__item[data-row-href]:hover .dash-list__go { opacity: 1; transform: translateX(2px); }
.dash-list__value { font-weight: 800; font-size: 15px; white-space: nowrap; }
.dash-list__value--info { color: var(--color-text); }
.dash-list__value--success { color: var(--color-success); }
.dash-list__value--warning { color: var(--color-warning); }
.dash-list__value--danger { color: var(--color-danger); }
.dash-list__value--critical { color: var(--color-danger); }
/* Алерт-лист сводной («Требует внимания»): сигналы плитками, по две в ряд на
   десктопе. Счётчик держится рядом с названием, стрелка отмечает правый край —
   поэтому единственная проблема выглядит как баннер, а не как строка отчёта,
   растянутая пустотой на всю карточку. */
.dash-alerts { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
@media (min-width: 900px) {
  .dash-alerts { grid-template-columns: 1fr 1fr; }
  /* Одна проблема — во всю ширину: половина карточки в пустоте выглядит обрывом. */
  .dash-alert:only-child { grid-column: 1 / -1; }
}
.dash-alert { min-width: 0; }
.dash-alert__link {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: 12px; padding: 11px 14px 11px 12px; border-radius: var(--radius-md);
  background: var(--color-surface-soft); border-left: 3px solid var(--color-border-strong);
  text-decoration: none; height: 100%;
  transition: background .16s var(--ease), border-color .16s var(--ease);
}
a.dash-alert__link:hover { background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface-soft)); }
.dash-alert__value {
  font-size: 19px; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums;
  min-width: 1.6em; text-align: center; color: var(--color-heading);
}
.dash-alert__body { display: grid; gap: 2px; min-width: 0; }
.dash-alert__label { font-size: 13.5px; font-weight: 700; color: var(--color-text); overflow-wrap: anywhere; }
.dash-alert__sub { font-size: 12px; color: var(--color-muted); }
.dash-alert__go { width: 16px; height: 16px; color: var(--color-faint); opacity: 0; transition: opacity .16s var(--ease); }
a.dash-alert__link:hover .dash-alert__go { opacity: 1; }
.dash-alert--danger .dash-alert__link { border-left-color: var(--color-danger); background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface-soft)); }
.dash-alert--danger a.dash-alert__link:hover { background: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface-soft)); }
.dash-alert--danger .dash-alert__value { color: var(--color-danger); }
.dash-alert--warning .dash-alert__link { border-left-color: var(--color-warning); background: color-mix(in srgb, var(--color-warning) 8%, var(--color-surface-soft)); }
.dash-alert--warning a.dash-alert__link:hover { background: color-mix(in srgb, var(--color-warning) 14%, var(--color-surface-soft)); }
.dash-alert--warning .dash-alert__value { color: var(--color-warning); }
.dash-alert--info .dash-alert__link { border-left-color: var(--color-primary); }
.dash-alert--info .dash-alert__value { color: var(--color-primary-dark); }

.dash-chart { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.dash-chart__row { position: relative; display: grid; grid-template-columns: minmax(88px, 42%) 1fr minmax(52px, max-content); gap: 10px; align-items: center; font-size: 13px; padding: 5px 8px; margin: 0 -8px; border-radius: var(--radius-md); transition: background .16s var(--ease); }
/* Подпись в 2 строки вместо жёсткого «Задержка отпр…»: длинные категории и
   коды ПВЗ с «_» различимы, остаток договаривает title. */
.dash-chart__label { color: var(--color-muted); overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow-wrap: anywhere; line-height: 1.3; }
.dash-chart__track { height: 9px; border-radius: var(--radius-pill); background: var(--chart-track); overflow: hidden; }
.dash-chart__fill { display: block; width: var(--w, 0); height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark)); transition: width .5s var(--ease), filter .16s var(--ease); }
.dash-chart__value { font-weight: 800; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Whole-row drill-down: a transparent stretched link over the row. */
.dash-chart__hit { position: absolute; inset: 0; border-radius: var(--radius-md); z-index: 1; }
.dash-chart__hit:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -1px; }
.dash-chart__row--link { cursor: pointer; }
.dash-chart__row--link:hover { background: var(--color-surface-soft); }
.dash-chart__row--link:hover .dash-chart__label { color: var(--color-text); }
.dash-chart__row--link:hover .dash-chart__fill { filter: brightness(1.08) saturate(1.05); }

/* Chart tone helpers: each maps a series/segment to a design-system colour.
   Referenced by SVG charts (line, donut) and legends via --seg. */
.tone--primary { --seg: var(--color-primary); }
.tone--success { --seg: var(--color-success); }
.tone--warning { --seg: var(--chart-amber, var(--color-warning)); }
.tone--danger { --seg: var(--color-danger); }
.tone--coral { --seg: var(--color-coral); }
.tone--beige { --seg: var(--chart-sand, var(--color-beige)); }
.tone--violet { --seg: var(--color-violet); }
.tone--muted { --seg: var(--color-faint); }

/* Line trend chart (SVG, viewBox 100x44, preserveAspectRatio="none").
   Markers/crosshair/tooltip live in an HTML overlay (percentage-positioned) so
   they stay round despite the stretched SVG. */
.dash-linechart { display: flex; flex-direction: column; gap: 9px; }
.dash-linechart__plot { position: relative; display: block; border-radius: var(--radius-md); text-decoration: none; color: inherit; }
a.dash-linechart__plot { cursor: pointer; }
a.dash-linechart__plot:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }
.dash-linechart__plot svg { display: block; width: 100%; height: 158px; overflow: visible; }
/* Сетка больше НЕ берёт --color-border: рамки карточек сняты, а сетке референс
   оставляет тонкую линию 8-10%. Один токен на оба — и они бы поехали вместе. */
.dash-linechart__grid { stroke: var(--color-divider); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* Заливка под линией плотнее и растворяется книзу — маской, а не градиентом в
   разметке: fill остаётся сплошным, маска гасит его к нижней кромке. */
.dash-linechart__area {
  fill: var(--seg); opacity: 0.26; transition: opacity .16s var(--ease);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, .45) 55%, transparent 92%);
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, .45) 55%, transparent 92%);
}
.dash-linechart__line { fill: none; stroke: var(--seg); stroke-width: 3.25; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
a.dash-linechart__plot:hover .dash-linechart__area { opacity: 0.34; }

/* Y-axis value labels sit against the gridlines (top edge of each line). */
.dash-linechart__ylabels { position: absolute; inset: 0; pointer-events: none; }
.dash-linechart__ylabels span { position: absolute; left: 0; transform: translateY(-50%); font-size: 10.5px; font-weight: 600; color: var(--color-faint); background: color-mix(in srgb, var(--color-surface) 72%, transparent); padding: 0 4px; border-radius: 5px; font-variant-numeric: tabular-nums; }

/* Overlay: end markers (server-rendered) + JS crosshair & focus dots. */
.dash-linechart__overlay { position: absolute; inset: 0; pointer-events: none; }
.dash-linechart__cross { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--color-border-strong); transform: translateX(-0.5px); }
.dash-linechart__dot { position: absolute; width: 9px; height: 9px; border-radius: 50%; background: var(--seg); box-shadow: 0 0 0 2px var(--color-surface); transform: translate(-50%, -50%); }
.dash-linechart__dot--end { width: 8px; height: 8px; }
.dash-linechart__dot--focus { width: 11px; height: 11px; box-shadow: 0 0 0 2px var(--color-surface), 0 2px 6px rgba(0, 0, 0, .25); }

/* Tooltip: value leads, series name follows; one card lists every series. */
.dash-linechart__tip { position: absolute; z-index: 5; min-width: 132px; max-width: 220px; padding: 8px 10px; border-radius: var(--radius-md); background: var(--color-surface); border: 1px solid var(--color-border-strong); box-shadow: var(--shadow-md); font-size: 12px; pointer-events: none; transform: translate(-50%, calc(-100% - 12px)); }
.dash-linechart__tip.is-below { transform: translate(-50%, 12px); }
.dash-linechart__tip-date { font-weight: 700; color: var(--color-heading); margin-bottom: 5px; font-variant-numeric: tabular-nums; }
.dash-linechart__tip-row { display: flex; align-items: center; gap: 7px; padding: 2px 0; }
.dash-linechart__tip-key { width: 12px; height: 2px; border-radius: 2px; background: var(--seg); flex: none; }
.dash-linechart__tip-name { color: var(--color-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-linechart__tip-val { margin-left: auto; font-weight: 800; color: var(--color-text); font-variant-numeric: tabular-nums; }

.dash-linechart__axis { display: flex; justify-content: space-between; gap: 8px; font-size: 11px; color: var(--color-faint); font-variant-numeric: tabular-nums; padding-left: 2px; }

/* Donut structure chart (SVG circles with pathLength=100). Segments are hover /
   click targets: the hovered one thickens, the rest dim, and the centre swaps to
   that segment's read-out. */
.dash-donut { display: flex; align-items: center; justify-content: center; gap: 20px; flex-wrap: wrap; }
/* 176px, а не 152: в дырке кольца живёт двухстрочная подпись, и при 152px
   длинное слово («рассмотрении» при наведении на сегмент) выезжало прямо на
   кольцо. Легенда рядом всё равно выше кольца, так что карточка не выросла. */
/* Легенда не растягивается на всю карточку: в блоке во всю ширину она уводила
   кольцо к левому краю, и половина карточки оставалась пустой. */
.dash-donut .chart-legend { max-width: 340px; }
.dash-donut__ring { position: relative; width: 176px; height: 176px; flex: none; }
.dash-donut__ring svg { display: block; width: 100%; height: 100%; transform: rotate(0); }
.dash-donut__bg { fill: none; stroke: var(--chart-track); stroke-width: 10.6; }
/* 10,6 при r=16 (09.2026). Прежние 9,5 при r=17 были ПОТОЛКОМ: внешняя кромка
   кольца это r + stroke-width/2, и на r=17 больше 10 не влезало — 21,6%
   размера против референсных 26-30%. Радиус опущен до 16 в
   widget_chart.html (ОБЕ окружности, подложка и сегменты — иначе фон окажется
   не под сегментами) и в DONUT_R в app.js, где по нему считают, влезает ли
   число в дырку. Итог: 24,9% толщины, закрашенной площади почти вдвое больше,
   дырка при этом остаётся достаточной под денежную строку.
   Активный сегмент — 11,8: 12 давало кромку ровно 22,0 и срез в вершине. */
.dash-donut__seg { fill: none; stroke: var(--seg); stroke-width: 10.6; transition: stroke-width .16s var(--ease), opacity .16s var(--ease); }
.dash-donut__seg.is-link { cursor: pointer; }
/* Соседи активного гаснут мягко (сильное затухание на тёмном фоне даёт
   грязно-бурые кольца), активный чуть толще. Никаких filter/drop-shadow на
   сегментах: фильтр на SVG-элементе рисует свечение прямоугольником
   bounding box — «квадрат» за кольцом. */
.dash-donut.has-active .dash-donut__seg:not(.is-active) { opacity: 0.45; }
.dash-donut__seg.is-active { stroke-width: 11.8; }
.dash-donut__center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; text-align: center; padding: 0 36px; }
.dash-donut__center strong { font-size: 23px; font-weight: 800; letter-spacing: -0.02em; color: var(--color-heading); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Long totals (money strings) step down so they stay inside the ring; the modifier
   comes from the widget payload and fitDonutCenter (app.js) re-fits hover swaps. */
.dash-donut__center--md strong { font-size: 17px; }
.dash-donut__center--sm strong { font-size: 14px; }
.dash-donut__center--xs strong { font-size: 12px; }
/* Подпись в центре: до двух строк вместо обрезки многоточием посреди слова.
   Ширина ограничена не всей серединой, а хордой кольца на высоте подписи
   (она ниже центра, там дырка уже) — иначе длинное слово ложилось на кольцо.
   overflow-wrap — страховка: слово, которое не влезло даже так, рвётся внутри
   круга, а не вылезает из графика. */
.dash-donut__center span {
  font-size: 11px; color: var(--color-muted); max-width: 92px; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical; line-height: 1.25; overflow-wrap: anywhere;
}
.dash-donut__center.is-active strong { color: var(--seg, var(--color-heading)); }

/* Shared chart legend (donut: vertical, line: horizontal row). */
.chart-legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; flex: 1; min-width: 160px; }
/* font-size и color здесь объявлены ЯВНО (03.09.2026). Раньше живая легенда
   брала их каскадом из мёртвого блока .chart-legend выше по файлу; удаление
   того блока молча перекрасило бы легенду всех графиков в --color-text и
   подняло кегль до 14px, и ни один тест этого бы не поймал. */
.chart-legend { font-size: 12.5px; color: var(--color-muted); }
.chart-legend li { display: flex; align-items: center; gap: 8px; font-size: 12px; min-width: 0; border-radius: var(--radius-sm); }
.chart-legend__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--seg); flex: none; }
/* Трек доли в строке легенды по умолчанию НЕ рисуется. Он придуман под
   широкую карточку сводной (.dsec), где легенде отдана вся оставшаяся ширина;
   тот же include с донатом подключён ещё на шести страницах зачётов и
   приёмки, где раскладка другая и потолок ширины легенды сохранён. Включать
   его там — отдельное решение, а не побочный эффект этой правки. */
.chart-legend__track { display: none; }
.chart-legend a { color: inherit; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-legend a:hover { color: var(--color-primary); text-decoration: underline; }
.chart-legend li > span:not(.chart-legend__dot):not(.chart-legend__val):not(.chart-legend__track) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-legend__val { margin-left: auto; font-weight: 800; white-space: nowrap; font-variant-numeric: tabular-nums; }
.chart-legend__pct { color: var(--color-muted); font-weight: 600; }
.chart-legend--row { display: flex; flex-wrap: wrap; gap: 6px 18px; }
.chart-legend--row .chart-legend__val { margin-left: 2px; }
/* Donut legend rows are hover/click twins of the segments. */
[data-donut-legend] li { padding: 4px 7px; margin: 0 -7px; transition: background .16s var(--ease); flex-wrap: wrap; }
[data-donut-legend] li[data-url] { cursor: pointer; }
[data-donut-legend] li.is-active { background: var(--color-surface-soft); }
[data-donut-legend] li.is-active .chart-legend__dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--seg) 22%, transparent); }
/* В узкой карточке длинная сумма не обрезает название («Пр…»): подпись
   занимает первую строку целиком, сумма переносится на вторую вправо. */
[data-donut-legend] li > span:not(.chart-legend__dot):not(.chart-legend__val),
[data-donut-legend] li > a { white-space: normal; overflow: visible; flex: 1 1 auto; min-width: 0; }
/* Цифры — всегда строкой ПОД подписью и слева, вровень с её текстом
   (просьба заказчика: значения не разъезжаются вправо). */
[data-donut-legend] .chart-legend__val {
  margin-left: 17px; flex-basis: calc(100% - 17px); text-align: left;
}

/* Плитка в 150px была уже самой длинной подписи («Проблемы открытий»), а у
   кнопки ``white-space: nowrap`` — текст вылезал за карточку и наезжал на
   соседа. Колонка держит самую длинную подпись в одну строку, а ``min(100%,…)``
   не даёт колее вылезти из узкого блока. Перенос — страховка на будущие
   подписи: длинная не порвёт вёрстку, а станет в две строки. */
.dash-actions {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 10px;
}
.dash-action {
  justify-content: flex-start; gap: 9px; min-width: 0;
  white-space: normal; text-align: left; line-height: 1.25;
}
.dash-action .ui-icon { flex: none; display: inline-flex; }
.dash-action .ui-icon svg { width: 17px; height: 17px; }

.dash-status { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.dash-status__badge { font-size: 13px; padding: 6px 12px; }
.dash-status__list { width: 100%; }
.dash-status__link { align-self: flex-start; }

.dash-table th { text-align: left; }

/* Employee mobile-first: single column, larger touch targets. */
.dash-employee .dash-cards { grid-template-columns: 1fr; }
.dash-employee .dash-actions { grid-template-columns: 1fr 1fr; }

@media (max-width: 640px) {
  .dash-metrics { grid-template-columns: repeat(2, 1fr); }
  .dash-cards { grid-template-columns: 1fr; }
  .dash-actions { grid-template-columns: 1fr; }
  .dash-filters__row { flex-direction: column; align-items: stretch; }
  .dash-filters .input { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .dash-metric, .dash-chart__fill, .dash-chart__row, .dash-donut__seg,
  .dash-linechart__area, [data-donut-legend] li { transition: none; }
  .dash-metric:hover { transform: none; }
}

/* =========================================================================
   Шкала «сколько из ста» (gauge). Кольцо то же, что у доната, но один отрезок
   с круглым торцом — так видно, что это доля до нормы, а не структура целого.
   ========================================================================= */
.dash-gauge { display: flex; align-items: center; justify-content: center; padding: 4px 0; }
.dash-gauge__ring { position: relative; width: 152px; height: 152px; flex: none; }
.dash-gauge__ring svg { display: block; width: 100%; height: 100%; }
.dash-gauge__bg { fill: none; stroke: var(--chart-track); stroke-width: 9.5; }
/* Дуга той же толщины, что и её жёлоб. Было 7 против 9.5 — вокруг цветной
   дуги оставался серый ореол в ~4 px с каждой стороны, и шкала выглядела
   недокрашенной. На сводной дефект был не виден: там обе строки перебиты
   правилом .dsec ниже по файлу, а на «Расчётах OZON» и в претензиях — виден. */
.dash-gauge__arc {
  fill: none; stroke: var(--seg); stroke-width: 9.5; stroke-linecap: round;
  transition: stroke-dasharray .6s var(--ease);
}
.dash-gauge__center {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px; text-align: center; padding: 0 30px;
}
.dash-gauge__center strong {
  font-size: 28px; font-weight: 800; letter-spacing: -0.02em; color: var(--color-heading);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.dash-gauge__center--md strong { font-size: 21px; }
.dash-gauge__center--sm strong { font-size: 17px; }
.dash-gauge__center--xs strong { font-size: 14px; }
.dash-gauge__center span {
  font-size: 11.5px; color: var(--color-muted); line-height: 1.25;
  max-width: 88px; overflow-wrap: anywhere;
}

/* =========================================================================
   Календарь-хитмап: неделя — колонка, день — клетка. Число внутри клетки
   появляется на широком экране: на телефоне оно нечитаемо и превращает
   календарь в кашу, а тон клетки и подсказка работают везде.
   ========================================================================= */
.dash-heat { display: flex; flex-direction: column; gap: 8px; --heat-cell: 20px; --heat-max: 36px; }
.dash-heat__months {
  display: grid; grid-auto-flow: column;
  grid-auto-columns: minmax(var(--heat-cell), var(--heat-max)); gap: 3px;
  margin-left: 26px; font-size: 10.5px; color: var(--color-faint); height: 12px;
  justify-content: center;
}
/* Подпись месяца стоит над своей колонкой и не растягивает сетку: она вылезает
   вправо поверх соседних (свободных) колонок. */
.dash-heat__months span { grid-column: calc(var(--i) + 1); white-space: nowrap; overflow: visible; }
/* Сетка стоит по центру карточки, а не прижата к левому краю. Календарь не
   может заполнить произвольную ширину: клетка квадратная, и растянуть девять
   колонок на 1030 px — значит получить клетку в 109 px и календарь высотой в
   763. Поэтому ширина у него своя, а лишнее уходит в симметричные поля: так это
   читается как выровненный блок, а не как «съехал в угол» (заказчик,
   11.08.2026: календарь «не красивый»). */
.dash-heat__body { display: flex; gap: 6px; justify-content: center; }
.dash-heat__days {
  display: grid; grid-template-rows: repeat(7, 1fr); gap: 3px;
  font-size: 10px; color: var(--color-faint); width: 20px; align-items: center;
}
/* Колонки тянутся под ширину карточки, но не больше потолка: в блоке во всю
   ширину календарь из 20px клеток терялся в левом углу, а без потолка клетка
   раздувалась бы в плитку размером с кнопку. Высота строк не задана: её держит
   aspect-ratio клетки, иначе при растущей ширине клетка стала бы прямоугольной. */
.dash-heat__grid {
  display: grid; grid-auto-flow: column;
  grid-template-rows: repeat(7, auto);
  grid-auto-columns: minmax(var(--heat-cell), var(--heat-max));
  gap: 3px; min-width: 0; overflow-x: auto; padding-bottom: 2px;
}
.dash-heat__cell {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  border-radius: 5px; background: color-mix(in srgb, var(--seg) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--seg) 34%, transparent);
  color: var(--color-muted); font-size: 9px; text-decoration: none;
  transition: transform .14s var(--ease), background-color .14s var(--ease);
}
.dash-heat__cell span { opacity: 0; }
a.dash-heat__cell:hover { transform: scale(1.18); background: color-mix(in srgb, var(--seg) 46%, transparent); z-index: 1; }
.dash-heat__cell--void { background: none; border-color: transparent; }
.dash-heat__legend { margin-top: 2px; }
/* Число дней в легенде — главная цифра блока, и набрано оно как цифра, а не
   как подпись (заказчик, 11.08.2026: календарь «неинформативный»). */
.dash-heat__legend .chart-legend__val { margin-left: 4px; font-weight: 800; }
@media (min-width: 900px) {
  .dash-heat { --heat-cell: 24px; }
  .dash-heat__cell span { opacity: .75; }
}
/* В блоке во всю ширину календарю есть где развернуться — клетка крупнее и
   числа читаются без прищура. */
.dblock--l .dash-heat { --heat-max: 48px; }

/* =========================================================================
   Конструктор сводной панели (07.2026)
   Холст: сетка в 12 колонок, ширина блока — из раскладки пользователя.
   На узких экранах колонок меньше, и «компактный» блок честно занимает
   половину строки, а не ниточку в четверть.
   ========================================================================= */
/* position: relative — точка отсчёта для «ямки» ведомого блока: она рисуется
   абсолютной плашкой поверх холста, чтобы не попасть в сетку (см. .dcanvas__slot). */
.dcanvas { position: relative; display: flex; flex-direction: column; gap: 22px; }
.dcanvas__section { gap: 12px; }
.dcanvas__head { min-height: 0; }
.dcanvas__head.is-empty { display: none; }
.dcanvas__grid {
  display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--gap-cards);
  align-items: stretch;
}
/* Дыры в строках не лечатся раскладкой браузера (dense переставляет блоки и
   ломает порядок, который человек собрал руками) — их закрывает сервер,
   растягивая блоки неполной строки: см. blocks._fill_rows. */
/* Ширину блока (--span) и её планшетную пару (--span-sm) ставит инлайновый
   стиль из раскладки: это единственная величина, которую человек тянет мышью,
   и держать её классами значило бы вернуть ступени «S/M/L». */
.dblock { grid-column: span var(--span, 3); display: flex; flex-direction: column; min-width: 0; }
/* Плитка с числом не тянется под соседний график: в строке «две метрики + график»
   она иначе превращалась в карточку в пол-экрана с пустотой под цифрой. Графики
   и списки по-прежнему растягиваются друг под друга (align-items: stretch у
   сетки), поэтому ряд из карточек остаётся ровным. */
.dblock--metric { align-self: start; }
.dblock__body { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.dblock__body > * { flex: 1; }
/* Блок, которому нечего показать, не растягивается на высоту соседей: карточка
   в пол-экрана с крошечным «всё в порядке» посередине — главный источник пустоты
   на панели молодой сети, где данных ещё нет. Само пустое состояние внутри
   виджета тоже ужимается: полноразмерное рассчитано на пустую СТРАНИЦУ. */
.dblock:has(.dash-widget__body > .empty-state) { align-self: start; }
.dash-widget__body > .empty-state { padding: 14px 12px; gap: 8px; }
.dash-widget__body > .empty-state .empty-state__icon { width: 40px; height: 40px; border-radius: 13px; }
.dash-widget__body > .empty-state .empty-state__icon svg { width: 21px; height: 21px; }
.dash-widget__body > .empty-state .empty-state__text { font-size: 13px; }

/* Блок без данных держит своё МЕСТО, но не претендует на внимание: одна тихая
   строка вместо карточки с иконкой (заказчик, 10.08.2026 — «оставлять место с
   короткой заглушкой»). Иконка тут была бы третьим по яркости объектом раздела
   и обещала бы содержимое, которого нет.

   Полосу плиток это не касается: там заглушка встаёт на место плитки и живёт
   по её правилам высоты. */
.dsec .dash-widget:has(.empty-state) { min-height: 0; }
.dsec .dash-widget__body > .empty-state { padding: 6px 2px 2px; gap: 0; text-align: left; }
.dsec .dash-widget__body > .empty-state .empty-state__icon { display: none; }
.dsec .dash-widget__body > .empty-state .empty-state__text {
  font-size: 12.5px; color: var(--color-faint);
}

/* Планшет: сетка вдвое уже, поэтому в дело идёт вторая ширина — четверть
   становится половиной, половина и шире — полной строкой. Пересчёт делает
   бэкенд (blocks.tablet_span), в CSS нет арифметики над --span. */
@media (max-width: 1000px) {
  .dcanvas__grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .dblock { grid-column: span var(--span-sm, 6); }
}
@media (max-width: 640px) {
  .dcanvas__grid { grid-template-columns: 1fr; }
  .dblock { grid-column: 1 / -1; }
}

/* ---- Переключатель сборок в шапке --------------------------------------- */
.lswitch__toggle { max-width: 240px; }
.lswitch__toggle b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lswitch__menu {
  right: 0; min-width: 288px; max-width: 340px; padding: 6px;
  max-height: min(calc(70vh * var(--vu)), 520px); overflow-y: auto;
}
.lswitch__group {
  padding: 8px 10px 4px; font-size: 10.5px; font-weight: 800;
  letter-spacing: .07em; text-transform: uppercase; color: var(--color-faint);
}
/* align-items обязателен: базовое правило .dropdown-menu button ставит center,
   и в колоночном флексе оно уводит название пункта на середину карточки. */
.lswitch__item {
  position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  width: 100%; padding: 8px 30px 8px 10px; text-align: left;
  border: none; background: none; cursor: pointer; border-radius: var(--radius-sm);
  color: var(--color-text); text-decoration: none;
  transition: background-color .14s var(--ease);
}
.lswitch__item:hover { background: var(--color-surface-soft); }
.lswitch__item.is-on { background: color-mix(in srgb, var(--color-primary) 10%, transparent); }
.lswitch__name { font-size: 13px; font-weight: 700; }
.lswitch__desc { font-size: 11.5px; color: var(--color-muted); line-height: 1.35; }
.lswitch__mark { position: absolute; right: 10px; top: 9px; color: var(--color-primary); font-weight: 800; }
.lswitch__item--action {
  flex-direction: row; align-items: center; gap: 8px;
  margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--color-border);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm); color: var(--color-primary);
}
.lswitch__item--action .lswitch__name { color: var(--color-primary); }

/* ---- Готовые сборки в режиме правки ------------------------------------- */
.dpresets { display: flex; flex-direction: column; gap: 10px; margin-bottom: 18px; }
.dpresets__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.dpresets__head b { font-size: 15px; font-weight: 800; color: var(--color-heading); }
.dpresets__head span { font-size: 12.5px; color: var(--color-muted); }
.dpresets__row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.dpreset {
  display: flex; flex-direction: column; gap: 4px; padding: 12px 14px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface); text-decoration: none; color: inherit; cursor: pointer;
  transition: border-color .16s var(--ease), transform .16s var(--ease), box-shadow .16s var(--ease);
}
.dpreset:hover {
  border-color: var(--color-primary); transform: translateY(-2px);
  box-shadow: 0 12px 28px -20px rgba(0, 0, 0, .5);
}
.dpreset.is-on { border-color: var(--color-primary); box-shadow: 0 0 0 1px var(--color-primary) inset; }
.dpreset__name { font-size: 13.5px; font-weight: 800; color: var(--color-heading); }
.dpreset__desc { font-size: 11.5px; color: var(--color-muted); line-height: 1.35; }
/* Своя сборка — это форма (POST), но выглядеть должна такой же карточкой. */
.dpreset--own { padding: 0; }
.dpreset__hit {
  display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  width: 100%; padding: 12px 14px; border: none; background: none;
  cursor: pointer; text-align: left; border-radius: inherit;
}

/* ---- Верхняя панель режима сборки -------------------------------------- */
.dbuild { display: flex; flex-direction: column; gap: 12px; }
.dbuild__bar {
  /* Липнет под шапкой, а не под верхом окна: страница скроллится всем окном, и
     с top: 8px панель наезжала на шапку. z-index ниже шапки (34) — её меню
     аккаунта и уведомлений должны накрывать панель, а не уходить под неё. */
  position: sticky; top: calc(var(--topbar-h) + 8px); z-index: 20;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; flex-wrap: wrap;
  padding: 12px 16px; border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-primary) 26%, var(--color-border));
  box-shadow: 0 14px 34px -26px rgba(0, 0, 0, .5);
}
.dbuild__name { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 220px; max-width: 320px; }
.dbuild__name-label { font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--color-muted); }
.dbuild__hint { font-size: 12.5px; color: var(--color-muted); padding: 0 2px; }
.dbuild__hint b { color: var(--color-heading); }
.dbuild__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dbuild__foot { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.dash-customize { white-space: nowrap; }

/* ---- Блок в режиме правки ---------------------------------------------- */
.dcanvas--edit .dblock {
  position: relative; padding: 6px; border-radius: var(--radius-lg);
  border: 1px dashed var(--color-border-strong);
  background: color-mix(in srgb, var(--color-primary) 3%, transparent);
  transition: border-color .16s var(--ease), background-color .16s var(--ease), opacity .16s var(--ease);
}
/* Появление проигрывается РОВНО ОДИН раз. Перестановка блока — это insertBefore,
   а для DOM это «вынули и вставили»: браузер запускает анимацию появления
   заново. Конструктор переставляет блоки раз в 90 мс, пока ведут рукой, и блок
   под курсором мигал с нуля прозрачности и прыгал на 8 px — «когда его хватаю,
   он начинает быстро дёргаться» (заказчик, 21.08.2026). Класс вешает скрипт на
   те блоки, что уже на экране, поэтому новый блок из библиотеки по-прежнему
   приезжает с появлением. */
.dcanvas--edit .dblock:not(.is-settled) {
  animation: dblock-in .22s var(--ease) backwards;
}
.dcanvas--edit .dblock:hover { border-color: var(--color-primary); }
/* Начинка блока в правке — картинка, а не рабочий инструмент: ни ссылки внутри
   списка, ни кнопка «Открыть», ни сегмент кольца не должны уводить со страницы
   посреди несохранённой сборки. Одним правилом вместо перечисления всего
   кликабельного: событие проходит насквозь и достаётся самому блоку, поэтому
   перетаскивание с любого места карточки продолжает работать. */
.dcanvas--edit .dblock__body { pointer-events: none; }
/* Ямка: место, куда блок встанет, когда его отпустят. Сам блок в этот момент
   едет под курсором, и его ячейка без плашки читалась бы просто дырой в
   раскладке. В сетке ямка не участвует — абсолютная поверх холста, размер ей
   ставит скрипт по ячейке блока. */
.dcanvas__slot {
  position: absolute; z-index: 0; pointer-events: none;
  border: 2px dashed var(--color-primary); border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-primary) 8%, transparent);
}

/* Ведомый блок ПОДСВЕЧЕН, а не приглушён (25.08.2026). Бледная тень осталась от
   родного drag&drop: там под курсором ехала копия, нарисованная браузером, и
   две одинаково яркие карточки читались бы как две разные. С тягой на pointer
   events копии нет вовсе — блок переезжает по ячейкам сам, и приглушать надо
   было бы что угодно, только не его: человек ведёт рукой и должен видеть, ЧТО
   именно он ведёт.

   Подсветка — цветом и тенью, без transform: сдвинутый или масштабированный
   блок соврал бы о своих координатах перестановке соседей (она меряет их
   по-настоящему, см. layoutBox). */
.dcanvas--edit .dblock.is-dragging {
  /* Блок едет в руке: он поверх соседей и приподнят тенью. Сам сдвиг ставит
     скрипт инлайновым transform — здесь только вид. */
  z-index: 5; will-change: transform;
  border-style: solid; border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
  box-shadow:
    0 0 0 1px var(--color-primary),
    0 22px 44px -20px rgba(0, 0, 0, .55);
  /* Тянущийся блок не ловит указатель: иначе он закрывал бы собой ту самую
     сетку, в которую едет, и `elementFromPoint` возвращал бы его самого — то
     есть «куда класть» вычислялось бы по блоку, который как раз и убирают. */
  pointer-events: none;
}
/* Тяга на pointer events: жест не должен превращаться в прокрутку страницы или
   в выделение текста. Ручка помечена всегда (с неё тянут пальцем), остальное —
   только пока тяга идёт. */
.dcanvas--edit .dblock__grab { touch-action: none; }
body.is-dragging-block { user-select: none; -webkit-user-select: none; }
body.is-dragging-block .dcanvas--edit,
body.is-dragging-block .dlib { touch-action: none; }
/* Пока тащат — соседи не мигают ховером под курсором, а ручки ширины уходят:
   во время переноса они всё равно не работают и только мельтешат. */
.dcanvas--edit.is-dragging-block .dblock:hover { border-color: var(--color-border-strong); }
.dcanvas--edit.is-dragging-block .dblock__resize { opacity: 0; }
/* Место под блок из библиотеки: его на холсте ещё нет, поэтому пустую ячейку
   держит заглушка — соседи расступаются заранее, и панель во время переноса
   выглядит ровно так, как после него. */
.dcanvas__ghost {
  grid-column: span var(--span, 3); min-height: 96px;
  /* Заглушка едет под курсором и по той же причине, что и тянущийся блок, не
     должна попадать в `elementFromPoint`. */
  pointer-events: none;
  border: 2px dashed var(--color-primary); border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-primary) 8%, transparent);
  animation: dghost-in .18s var(--ease-out) backwards;
}
@media (max-width: 1000px) { .dcanvas__ghost { grid-column: span var(--span-sm, 6); } }
@media (max-width: 640px) { .dcanvas__ghost { grid-column: 1 / -1; } }
@keyframes dghost-in {
  from { opacity: 0; transform: scale(.94); }
  to { opacity: 1; transform: none; }
}
/* Приземление: короткая волна по контуру приехавшего блока. Нужна потому, что
   блоки похожи друг на друга — без неё после перестановки взгляд ищет, какой
   именно из них только что переехал. */
.dcanvas--edit .dblock.is-landed { animation: dblock-land .6s var(--ease) both; }
@keyframes dblock-land {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 55%, transparent); }
  60% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--color-primary) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent); }
}
@keyframes dblock-in {
  from { opacity: 0; transform: translateY(8px) scale(.98); }
  to { opacity: 1; transform: none; }
}
.dblock.is-leaving { opacity: 0; transform: scale(.94); transition: opacity .18s var(--ease), transform .18s var(--ease); }

.dblock__bar {
  display: flex; align-items: center; gap: 6px; padding: 0 4px 6px;
  color: var(--color-muted); font-size: 11.5px;
}
.dblock__grab { cursor: grab; color: var(--color-faint); display: inline-flex; }
.dcanvas--edit .dblock:active .dblock__grab { cursor: grabbing; }
.dblock__name {
  font-weight: 700; color: var(--color-text); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.dblock__tools { display: flex; align-items: center; gap: 4px; margin-left: auto; }
.dblock__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 6px; border: 1px solid transparent;
  background: none; color: var(--color-muted); cursor: pointer;
  transition: background-color .14s var(--ease), color .14s var(--ease);
}
.dblock__btn:hover { background: var(--color-surface-soft); color: var(--color-text); }
.dblock__btn--drop:hover { background: color-mix(in srgb, var(--color-danger) 14%, transparent); color: var(--color-danger); }
/* ---- Ручка ширины: тянут за правый край блока --------------------------- */
/* Полоса во всю высоту блока, а не уголок: целиться в уголок мышью тяжело, а
   «край карточки» — привычный жест из любого редактора. Полоса шире, чем
   выглядит (12px), чтобы курсор ловил её без прицеливания. */
/* Мишень 24 px и по центру края блока, а не 14 px, наполовину свисающие
   наружу: в прежнюю ручку приходилось целиться, и заказчик (10.08.2026) счёл
   растягивание неработающим. Ширина мишени больше видимой полоски — целятся в
   край карточки, а не в четырёхпиксельную чёрточку. */
.dblock__resize {
  position: absolute; top: 6px; right: -12px; bottom: 6px; width: 24px;
  display: flex; align-items: center; justify-content: center;
  cursor: col-resize; touch-action: none; z-index: 2;
  /* Не ноль: невидимую ручку нельзя найти, а искать её будут ровно один раз —
     первый. Бледная полоска на краю читается как «за это тянут» и не спорит с
     содержимым карточки. */
  opacity: .45; transition: opacity .14s var(--ease);
}
.dcanvas--edit .dblock:hover .dblock__resize,
.dblock__resize:focus-visible,
.dblock.is-resizing .dblock__resize { opacity: 1; }
.dblock__resize:focus-visible { outline: none; }
.dblock__resize-grip {
  width: 5px; height: 34px; border-radius: 3px;
  background: var(--color-border-strong);
  transition: background-color .14s var(--ease), height .14s var(--ease);
}
.dblock__resize:hover .dblock__resize-grip,
.dblock__resize:focus-visible .dblock__resize-grip,
.dblock.is-resizing .dblock__resize-grip { background: var(--color-primary); height: 46px; }
/* Пока тянут — блок подсвечен, а соседи не мигают ховером под курсором. */
.dcanvas--edit.is-resizing .dblock:hover { border-color: var(--color-border-strong); }
.dcanvas--edit .dblock.is-resizing { border-color: var(--color-primary); border-style: solid; }
/* Плашки с подписью ширины («половина», «вся строка») больше нет: ступеней
   снова десять, назвать их словами нечем, а плашка вдобавок закрывала собой
   верх блока — то есть ровно тот результат, ради которого её и тянули
   (заказчик, 10.08.2026). Что получилось, видно по самому блоку и по подложке
   сетки ниже. */
/* Подложка сетки проступает во время тяги — видно, куда встанет край. */
.dcanvas__grid.is-resizing {
  background-image: repeating-linear-gradient(
    to right,
    color-mix(in srgb, var(--color-primary) 12%, transparent) 0,
    color-mix(in srgb, var(--color-primary) 12%, transparent) 1px,
    transparent 1px,
    transparent calc((100% + 16px) / 12)
  );
  background-position: 0 0;
}

/* ---- Раздел в режиме правки -------------------------------------------- */
.dcanvas__title-input {
  flex: 1; min-width: 0; max-width: 340px;
  padding: 5px 10px; border-radius: var(--radius-sm);
  border: 1px solid transparent; background: none;
  font-size: 15px; font-weight: 800; letter-spacing: -0.01em; color: var(--color-heading);
}
.dcanvas__title-input:hover { border-color: var(--color-border); }
.dcanvas__title-input:focus { border-color: var(--color-primary); background: var(--color-surface); outline: none; }
.dcanvas__title-input::placeholder { color: var(--color-faint); font-weight: 600; }
.dcanvas__section-tools { display: flex; align-items: center; gap: 4px; }

.dcanvas__add {
  /* Ровно половина строки раздела. Раньше здесь стояло `span var(--span-s)` —
     переменной с таким именем в системе нет вовсе (ширина зовётся `--span`), а
     несуществующий токен убивает объявление целиком: кнопка сваливалась в одну
     колонку и выглядела обрезанной. */
  grid-column: span 6;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 88px; padding: 14px; cursor: pointer;
  border: 1px dashed var(--color-border-strong); border-radius: var(--radius-lg);
  background: none; color: var(--color-muted); font-size: 13px; font-weight: 650;
  transition: border-color .16s var(--ease), color .16s var(--ease), background-color .16s var(--ease);
}
/* Мишень зажигается МГНОВЕННО, а гаснет плавно (transition-duration стоит
   только в :hover). Пока цвет наливался общие 0.16 s, курсор успевал пройти
   половину 282-пиксельной плитки — и задержка читалась как размер: «навожу на
   зону плюса, а выделяется он, только когда дойду до середины» (заказчик,
   25.08.2026). Уход мыши торопить незачем, поэтому гашение прежнее. */
.dcanvas__add:hover {
  border-color: var(--color-primary); color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 6%, transparent);
  transition-duration: 0s;
}
.dcanvas__add--section { min-height: 56px; width: 100%; }
/* Раздел под курсором подсвечивается целиком: жёсткая пунктирная рамка спорила
   с рамками самих блоков, поэтому тут мягкая заливка и тонкий контур. */
.dcanvas__grid {
  border-radius: var(--radius-lg);
  transition: background-color .18s var(--ease), box-shadow .18s var(--ease);
}
.dcanvas__grid.is-drop-target {
  background: color-mix(in srgb, var(--color-primary) 5%, transparent);
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-primary) 5%, transparent);
}

/* ---- Библиотека блоков -------------------------------------------------- */
.dlib {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 60;
  width: min(400px, 92vw); display: flex; flex-direction: column;
  background: var(--color-surface); border-left: 1px solid var(--color-border);
  box-shadow: -24px 0 60px -40px rgba(0, 0, 0, .7);
  animation: dlib-in .24s var(--ease);
}
/* [hidden] стоит ПЕРЕД display: flex выше по каскаду? Нет — правило ниже, и
   именно поэтому оно перебивает display у .dlib (известная ловушка проекта). */
.dlib[hidden] { display: none; }
@keyframes dlib-in { from { transform: translateX(100%); } to { transform: none; } }
/* Уход — тот же сдвиг за правый край, только назад (25.08.2026: раньше панель
   просто исчезала в кадре). Класс снимает скрипт, когда сдвиг доиграл: `hidden`
   выкидывает узел из отрисовки мгновенно, и анимации негде было бы играть.
   Пока панель уезжает, она не ловит указатель — под ней уже холст. */
.dlib.is-closing {
  animation: dlib-out .2s var(--ease) forwards;
  pointer-events: none;
}
@keyframes dlib-out { from { transform: none; } to { transform: translateX(100%); } }
.dlib__head { display: flex; align-items: flex-start; gap: 12px; padding: 18px 18px 12px; border-bottom: 1px solid var(--color-border); }
.dlib__title { font-size: 16px; font-weight: 800; color: var(--color-heading); }
.dlib__sub { font-size: 12.5px; color: var(--color-muted); margin-top: 2px; }
.dlib__close {
  margin-left: auto; flex: none; width: 30px; height: 30px; border-radius: 8px;
  border: 1px solid var(--color-border); background: none; color: var(--color-muted); cursor: pointer;
}
.dlib__close:hover { background: var(--color-surface-soft); color: var(--color-text); }
.dlib__search { position: relative; padding: 12px 18px 8px; }
.dlib__search svg { position: absolute; left: 30px; top: 50%; transform: translateY(-25%); color: var(--color-faint); pointer-events: none; }
.dlib__input { width: 100%; padding-left: 32px; }
.dlib__body { flex: 1; overflow-y: auto; padding: 6px 12px 20px; }
.dlib__group + .dlib__group { margin-top: 14px; }
.dlib__group-title {
  font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  color: var(--color-faint); padding: 8px 6px 6px;
}
.dlib__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
/* Курсора «захвата» на строке нет намеренно (снят 25.08.2026). Перетаскивание
   с карточки работает по-прежнему, но `cursor: grab` на всю строку обещал
   единственный способ обращения с ней — и человек, шедший к «плюсу», всю
   дорогу видел «перетащить» и не понимал, где начинается кнопка (заказчик,
   25.08.2026). Что строку можно тянуть, сказано словами в подзаголовке
   библиотеки, а кнопки говорят за себя своим курсором. */
.dlib__item {
  display: flex; align-items: flex-start; gap: 10px; padding: 9px 10px;
  border-radius: var(--radius-md); border: 1px solid transparent;
  transition: background-color .14s var(--ease), border-color .14s var(--ease);
}
.dlib__item:hover { background: var(--color-surface-soft); border-color: var(--color-border); transition-duration: 0s; }
/* Строку, спрятанную поиском, прячет ОТДЕЛЬНОЕ правило с атрибутом: `display: flex`
   выше перебивает браузерное [hidden]{display:none} (известная ловушка проекта), и
   до 25.08.2026 поиск по библиотеке не убирал из списка ни одного блока — уходили
   только целые разделы, а внутри оставшегося стоял весь каталог подряд. */
.dlib__item[hidden] { display: none; }
/* Раздел целиком — по той же причине, хотя своего display у него нет: правило
   рядом страхует от «добавили .dlib__group { display: grid } и поиск снова врёт». */
.dlib__group[hidden] { display: none; }
/* Карточка, которую унесли на холст, остаётся в списке приглушённой: она никуда
   не делась, её копируют — и это должно быть видно. */
.dlib__item.is-dragging {
  opacity: .4; border-color: var(--color-primary); border-style: dashed;
}
/* Библиотека блоков — список из десятков строк; подложка под каждым значком
   превращала его в фактуру. Цвет типа блока остаётся, заливка снята. */
.dlib__icon {
  flex: none; width: 22px; height: 22px; margin-top: 3px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--color-primary);
}
.dlib__icon svg { width: 20px; height: 20px; }
.dlib__icon--chart { color: var(--color-success); }
.dlib__icon--list { color: var(--color-violet); }
.dlib__text { min-width: 0; flex: 1; }
.dlib__name { display: block; font-size: 13px; font-weight: 700; color: var(--color-text); }
.dlib__desc { display: block; font-size: 11.5px; color: var(--color-muted); line-height: 1.35; margin-top: 1px; }
.dlib__item.is-used .dlib__add { display: none; }
.dlib__item.is-used .dlib__drop { display: grid; }
/* Приглушаем ЗНАЧОК И ТЕКСТ, а не строку целиком: с 25.08.2026 на строке
   «уже на панели» живёт рабочая кнопка «убрать», а `opacity` на родителе
   гасит и её — ребёнок не может быть ярче своей группы. */
.dlib__item.is-used .dlib__icon,
.dlib__item.is-used .dlib__text { opacity: .55; }
.dlib__add {
  align-self: center; flex: none; width: 26px; height: 26px; border-radius: 8px;
  border: 1px solid var(--color-border); background: none; color: var(--color-primary); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  position: relative;
  transition: background-color .14s var(--ease), color .14s var(--ease);
}
/* Мишень БОЛЬШЕ нарисованного квадрата. Строку библиотеки тянут мышью, у неё
   cursor: grab — и до 25.08.2026 курсор становился «указателем» ровно на
   26 пикселях плюса. Человек вёл мышь к плюсу, всю дорогу видел «перетащить» и
   на всякий случай доводил курсор до середины квадратика: «навожу на зону
   плюса, а нажать можно только в середине» (заказчик, 25.08.2026).
   Невидимая рамка вокруг кнопки снимает прицеливание, вид остаётся прежним.
   Тягу она не крадёт: нажатие на кнопке её и так не начинает (interactive()
   в dashboard_builder.js), а вертикально рамка не выходит за свою строку. */
.dlib__add::after { content: ""; position: absolute; inset: -8px; border-radius: 12px; }
/* Тот же приём, что у плитки «Добавить блок»: мишень маленькая, целятся в неё
   точно, и подтверждение «попал» обязано прийти в том же кадре. */
.dlib__add:hover { background: var(--color-primary); color: var(--color-on-primary); border-color: var(--color-primary); transition-duration: 0s; }
/* Отметка «на панели» — она же кнопка «убрать»: блок снимают оттуда же, откуда
   добавляли (заказчик, 25.08.2026). Сначала это были две вещи подряд — зелёная
   плашка и квадратик с минусом; минус читался не как кнопка, а как чёрточка,
   и конец строки выглядел сором. Теперь состояние и действие занимают одно
   место, надпись меняется под курсором.

   Обе надписи лежат в ОДНОЙ ячейке грида: ширина пилюли считается по самой
   длинной, и под курсором строка не дёргается. */
.dlib__drop {
  display: none;
  align-self: center; flex: none;
  padding: 4px 9px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent);
  background: color-mix(in srgb, var(--color-success) 12%, transparent);
  color: var(--color-success);
  font-size: 10.5px; font-weight: 700; white-space: nowrap; cursor: pointer;
  position: relative;
  transition: background-color .14s var(--ease), color .14s var(--ease), border-color .14s var(--ease);
}
/* Мишень чуть больше пилюли: целиться в двадцать пикселей высоты не надо. */
.dlib__drop::after { content: ""; position: absolute; inset: -6px; border-radius: 999px; }
.dlib__drop-state,
.dlib__drop-act {
  grid-area: 1 / 1;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
}
.dlib__drop-act { opacity: 0; }
.dlib__drop:hover,
.dlib__drop:focus-visible {
  background: color-mix(in srgb, var(--color-danger) 15%, transparent);
  border-color: color-mix(in srgb, var(--color-danger) 40%, transparent);
  color: var(--color-danger);
  transition-duration: 0s;
}
.dlib__drop:focus-visible { outline: none; }
.dlib__drop:hover .dlib__drop-state,
.dlib__drop:focus-visible .dlib__drop-state { opacity: 0; }
.dlib__drop:hover .dlib__drop-act,
.dlib__drop:focus-visible .dlib__drop-act { opacity: 1; }
/* На тач-экране наведения не бывает: пилюля сразу показывает действие, иначе
   палец жал бы «на панели», а блок неожиданно исчезал. */
@media (hover: none) {
  .dlib__drop {
    color: var(--color-danger);
    border-color: color-mix(in srgb, var(--color-danger) 30%, transparent);
    background: color-mix(in srgb, var(--color-danger) 12%, transparent);
  }
  .dlib__drop-state { opacity: 0; }
  .dlib__drop-act { opacity: 1; }
}
.dlib__empty { padding: 20px 8px; font-size: 13px; color: var(--color-muted); text-align: center; }

@media (max-width: 1000px) {
  /* Тянуть ширину есть смысл только на полной сетке в 12 колонок: на планшете
     и телефоне раскладка и так упрощается до половин и полных строк, и ручка
     обещала бы точность, которой на этой ширине не существует. */
  .dblock__resize { display: none; }
}
@media (max-width: 640px) {
  .dbuild__bar { position: static; }
  .dblock__name { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Перестановку блоков (FLIP) гасит сам конструктор — он спрашивает
     matchMedia и просто не запускает анимацию. Здесь снимаем то, что живёт
     чистым CSS: появление, приземление и заглушку под блоком. */
  .dlib, .dlib.is-closing, .dcanvas--edit .dblock:not(.is-settled), .dcanvas__ghost { animation: none; }
  .dcanvas--edit .dblock.is-landed { animation: none; }
  .dcanvas__grid, .dash-gauge__arc, .dash-heat__cell { transition: none; }
}

/* =========================================================================
   Stage 19.1 — global design polish
   Fill styling for classes already referenced in templates but never defined,
   so the design system is complete and every component renders consistently.
   Additive only — no markup, RBAC, scoping or calculation changes.
   ========================================================================= */

/* ---- Table cell utilities ---------------------------------------------- */
/* Danger cells: import row errors and negative payroll amounts. */
.cell-danger  { color: var(--color-danger); font-weight: 700; }
.cell-success { color: var(--color-success); font-weight: 700; }
/* «Ответа нет, и это нормально»: ставка ручного вида оплаты живёт в смене, а
   не в справочнике, — не ошибка, но и не сумма. */
.cell-warning { color: var(--color-warning); font-weight: 700; }
.cell-right   { text-align: right; }

/* ---- Payroll money display (sensitive) --------------------------------- */
/* Money is Decimal on the backend — the template only renders it. tabular-nums
   keeps digits aligned; the sensitive variant marks financial data visually. */
.money {
  display: inline-flex; align-items: baseline; gap: 8px;
  font-variant-numeric: tabular-nums;
}
.money__label { color: var(--color-muted); font-size: 12px; font-weight: 600; }
.money__value {
  font-weight: 800; font-size: 15px; letter-spacing: -0.01em; white-space: nowrap;
}
.money--sensitive .money__value { color: var(--color-text); }
.money--negative .money__value,
.money__value--negative { color: var(--color-danger); }

/* ---- Card body wrappers ------------------------------------------------- */
.card__body,
.report-card__body { display: flex; flex-direction: column; gap: 12px; min-width: 0; }

/* ---- Notification detail / item bits ----------------------------------- */
.notif-detail__badges { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.notif-detail__message {
  color: var(--color-text); font-size: 14.5px; line-height: 1.6; white-space: pre-wrap;
}
.notif-item__object {
  display: block; margin-top: 4px; font-size: 12.5px; color: var(--color-muted);
}

/* =========================================================================
   Stage 20 — Redesign v2  (Indigo-SaaS)
   New, additive UI primitives: skeletons, toasts, command palette, HTMX
   loading states, tooltips, keyboard hints, glass utilities. No markup,
   RBAC, scoping or business-logic changes — purely presentational classes
   that templates/JS can opt into. Everything consumes the design tokens
   above, so light/dark work automatically.
   ========================================================================= */

/* ---- Page rhythm -------------------------------------------------------
   Ширина колонки: потолок убран совсем (решение заказчика 02.08.2026).
   История вопроса: сначала было жёсткие 1440px, потом clamp(1440, 70%, 1760)
   — и на 24" полотно заполнялось на 68%, на 27" на 58%: страница висела
   лентой посередине с пустыми полями по 300–600px с каждой стороны. Система
   операционная, а не читальная: таблицы смен, матрица ПВЗ и сводка выигрывают
   от каждого лишнего пикселя ширины. Переменная сохранена (её использует
   шапка и она же — точка отката, если потолок понадобится вернуть), но теперь
   это 100% полотна без сайдбара.
   Длину строк текста ограничивают сами компоненты (.card--form, .card--narrow,
   .prose-narrow), а не колонка целиком. */
:root { --page-max: 100%; }
.page { max-width: var(--page-max); width: 100%; margin-inline: auto; }
.page__title { font-size: 24px; }
.page__eyebrow {
  display: inline-flex; align-items: center; gap: 6px; margin-bottom: 8px;
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--color-primary-dark);
}

/* ---- Glass / elevated surface utilities -------------------------------- */
.glass {
  background: color-mix(in srgb, var(--color-surface) 70%, transparent);
  backdrop-filter: saturate(150%) blur(12px);
  border: 1px solid color-mix(in srgb, var(--color-border) 80%, transparent);
}
.surface-alt { background: var(--color-surface-alt); }
.divider { height: 1px; background: var(--color-border); border: 0; margin: 4px 0; }
.divider--dashed { background: repeating-linear-gradient(90deg, var(--color-border) 0 6px, transparent 6px 12px); }

/* ---- Keyboard hint ----------------------------------------------------- */
.kbd {
  display: inline-flex; align-items: center; justify-content: center; gap: 2px;
  min-width: 20px; height: 20px; padding: 0 6px;
  font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--color-muted); background: var(--color-surface-soft);
  border: 1px solid var(--color-border-strong); border-bottom-width: 2px;
  border-radius: 6px;
}

/* ---- Skeleton loaders -------------------------------------------------- */
.skeleton,
.skeleton-text,
.skeleton-line,
.skeleton-circle,
.skeleton-block {
  position: relative; overflow: hidden;
  background: color-mix(in srgb, var(--color-muted) 16%, var(--color-surface-soft));
  border-radius: var(--radius-sm);
}
.skeleton::after,
.skeleton-text::after,
.skeleton-line::after,
.skeleton-circle::after,
.skeleton-block::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--color-surface) 60%, transparent), transparent);
  transform: translateX(-100%); animation: skeleton-sweep 1.4s var(--ease) infinite;
}
.skeleton-line { height: 12px; margin-bottom: 8px; }
.skeleton-line--sm { height: 10px; }
.skeleton-line--lg { height: 18px; }
.skeleton-line--60 { width: 60%; } .skeleton-line--40 { width: 40%; } .skeleton-line--80 { width: 80%; }
.skeleton-circle { border-radius: 50%; width: 40px; height: 40px; }
.skeleton-block { height: 120px; border-radius: var(--radius-md); }
.skeleton-card {
  padding: 20px; border-radius: var(--radius-xl);
  border: 1px solid var(--color-border); background: var(--color-surface);
  display: flex; flex-direction: column; gap: 12px;
}
/* Отдельное имя: второй @keyframes skeleton-shimmer перекрывал первый
   (background-position) по каскаду, и все его потребители — включая оверлей
   загрузки камеры — начинали ЦЕЛИКОМ уезжать вбок через transform. */
@keyframes skeleton-sweep { to { transform: translateX(100%); } }

/* ---- HTMX loading states ----------------------------------------------- */
.htmx-indicator { opacity: 0; transition: opacity .2s var(--ease); }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; }
/* Subtle top progress line while any swap is in flight. */
.htmx-request { position: relative; }
[aria-busy="true"] { cursor: progress; }
.spinner {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--color-muted) 35%, transparent);
  border-top-color: var(--color-primary); animation: spin .7s linear infinite; display: inline-block;
}
.spinner--lg { width: 30px; height: 30px; border-width: 3px; }
.loading-overlay {
  position: absolute; inset: 0; display: grid; place-items: center; z-index: 5;
  background: color-mix(in srgb, var(--color-surface) 55%, transparent);
  backdrop-filter: blur(2px); border-radius: inherit;
}

/* ---- Toasts ------------------------------------------------------------ */
.toast-stack {
  position: fixed; z-index: 200; right: 20px; bottom: 20px;
  display: flex; flex-direction: column; gap: 10px; width: min(360px, calc(calc(100vw * var(--vu)) - 32px));
  pointer-events: none;
}
.toast {
  pointer-events: auto; display: flex; align-items: flex-start; gap: 12px;
  padding: 13px 14px; border-radius: var(--radius-lg);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-pop); color: var(--color-text);
  animation: toast-in .32s var(--ease-out) both;
}
.toast.is-leaving { animation: toast-out .28s var(--ease) both; }
.toast__icon { flex: 0 0 auto; width: 20px; height: 20px; margin-top: 1px; }
.toast__icon svg { width: 20px; height: 20px; }
.toast__body { flex: 1; min-width: 0; }
.toast__title { font-weight: 700; font-size: 13.5px; }
.toast__msg { font-size: 12.5px; color: var(--color-muted); margin-top: 2px; }
.toast__close { flex: 0 0 auto; background: transparent; border: 0; color: var(--color-faint); padding: 2px; border-radius: 6px; }
.toast__close:hover { color: var(--color-text); background: var(--color-surface-soft); }
.toast::before { content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px; border-radius: 3px; }
.toast { position: relative; overflow: hidden; padding-left: 16px; }
.toast--success .toast__icon { color: var(--color-success); } .toast--success::before { background: var(--color-success); }
.toast--danger  .toast__icon { color: var(--color-danger); }  .toast--danger::before  { background: var(--color-danger); }
.toast--warning .toast__icon { color: var(--color-warning); } .toast--warning::before { background: var(--color-warning); }
.toast--info    .toast__icon { color: var(--color-primary); } .toast--info::before,
.toast::before  { background: var(--color-primary); }
@keyframes toast-in  { from { opacity: 0; transform: translateY(12px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateX(24px); } }

/* ---- Мессенджер-попап нового сообщения чата ----------------------------
 * Живёт в общем .toast-stack (правый нижний угол) поверх базы .toast.
 * Вся карточка кликабельна (переход в чат), крестик закрывает. */
.chat-popup { cursor: pointer; align-items: center; }
.chat-popup:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); }
.chat-popup:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.chat-popup__avatar {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  background: color-mix(in srgb, var(--color-primary) 16%, var(--color-surface));
  color: var(--color-primary); font-weight: 700; font-size: 15px;
}
.chat-popup__avatar--img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.chat-popup__text {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}

/* ---- Command palette (⌘K) --------------------------------------------- */
.cmdk-backdrop {
  position: fixed; inset: 0; z-index: 150;
  background: color-mix(in srgb, var(--color-bg-deep) 55%, transparent);
  backdrop-filter: blur(4px); display: grid; place-items: start center; padding-top: calc(12vh * var(--vu));
  animation: fade-in .16s var(--ease) both;
}
.cmdk-backdrop[hidden] { display: none; }
.cmdk {
  width: min(600px, calc(calc(100vw * var(--vu)) - 32px)); max-height: calc(62vh * var(--vu)); display: flex; flex-direction: column;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-xl); box-shadow: var(--shadow-pop); overflow: hidden;
  animation: pop-in .2s var(--ease-out) both;
}
.cmdk__search { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--color-border); }
.cmdk__search svg { width: 18px; height: 18px; color: var(--color-faint); flex: 0 0 auto; }
.cmdk__input { flex: 1; border: 0; background: transparent; outline: none; color: var(--color-text); font-size: 15px; }
.cmdk__input::placeholder { color: var(--color-faint); }
.cmdk__list { list-style: none; margin: 0; padding: 8px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.cmdk__group { padding: 10px 10px 4px; font-size: 10.5px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--color-faint); }
.cmdk__item {
  display: flex; align-items: center; gap: 11px; padding: 9px 11px; border-radius: var(--radius-md);
  color: var(--color-text); font-size: 13.5px; font-weight: 550; cursor: pointer;
}
.cmdk__item svg { width: 17px; height: 17px; color: var(--color-muted); flex: 0 0 auto; }
.cmdk__item.is-active,
.cmdk__item:hover { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.cmdk__item.is-active svg,
.cmdk__item:hover svg { color: var(--color-primary-dark); }
.cmdk__item mark { background: color-mix(in srgb, var(--color-primary) 26%, transparent); color: inherit; border-radius: 3px; padding: 0 1px; }
.cmdk__hint { margin-left: auto; font-size: 11px; color: var(--color-faint); }
.cmdk__empty { padding: 26px; text-align: center; color: var(--color-muted); font-size: 13.5px; }
.cmdk__foot { display: flex; gap: 14px; padding: 9px 14px; border-top: 1px solid var(--color-border); font-size: 11.5px; color: var(--color-faint); }
.cmdk__foot span { display: inline-flex; align-items: center; gap: 5px; }

/* Topbar search shows a ⌘K hint chip on wide screens. */
.topbar__kbd { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; }
@media (max-width: 720px) { .topbar__kbd { display: none; } }

/* ---- Tooltip (CSS-only, data-tooltip attribute) ------------------------ */
[data-tooltip] { position: relative; }
[data-tooltip]::after {
  content: attr(data-tooltip); position: absolute; bottom: calc(100% + 8px); left: 50%;
  transform: translateX(-50%) translateY(4px); white-space: nowrap;
  background: var(--color-heading); color: var(--color-bg);
  font-size: 11.5px; font-weight: 600; padding: 5px 9px; border-radius: 7px;
  opacity: 0; pointer-events: none; transition: opacity .16s var(--ease), transform .16s var(--ease); z-index: 90;
  box-shadow: var(--shadow-sm);
}
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ---- Chart tooltip (JS-positioned) ------------------------------------- */
.chart-tip {
  position: absolute; z-index: 20; pointer-events: none; padding: 8px 11px;
  background: var(--color-heading); color: var(--color-bg);
  border-radius: 9px; font-size: 12px; box-shadow: var(--shadow-pop);
  transform: translate(-50%, -120%); white-space: nowrap; opacity: 0; transition: opacity .12s var(--ease);
}
.chart-tip.is-visible { opacity: 1; }
.chart-tip__val { font-weight: 800; }

/* ---- Segmented control ------------------------------------------------- */
.segmented { display: inline-flex; padding: 3px; gap: 2px; background: var(--color-surface-soft); border: 1px solid var(--color-border); border-radius: var(--radius-md); }
.segmented__btn { border: 0; background: transparent; color: var(--color-muted); font-weight: 600; font-size: 13px; padding: 6px 12px; border-radius: calc(var(--radius-md) - 3px); transition: all .16s var(--ease); }
.segmented__btn:hover { color: var(--color-text); }
.segmented__btn.is-active { background: var(--color-surface); color: var(--color-primary-dark); box-shadow: var(--shadow-xs); }

/* ---- New-page enter: без по-секционных slide-up — контент не скачет.
   Страница целиком получает мягкий fade через .fade-in выше. ---- */
@media (prefers-reduced-motion: reduce) {
  .skeleton::after, .skeleton-text::after, .skeleton-line::after,
  .skeleton-circle::after, .skeleton-block::after { animation: none !important; }
  .toast, .cmdk, .cmdk-backdrop { animation: none !important; }
}

/* =========================================================================
   Stage 21 — Layout & structure
   Reusable page-header, list-toolbar and form patterns so every screen shares
   the same skeleton: a clear header (title + count + actions), a light filter
   toolbar, a full-bleed data card, and a sticky form action bar. Additive.
   ========================================================================= */

/* ---- Page header ------------------------------------------------------- */
.page__head { padding-bottom: 4px; }
.page__title-row { display: inline-flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.title-chip {
  display: inline-flex; align-items: center; height: 24px; padding: 0 10px;
  border-radius: var(--radius-pill); font-size: 13px; font-weight: 700;
  color: var(--color-primary-dark); background: var(--color-primary-soft);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
}

/* ---- Full-bleed data card (table fills to the edges) ------------------- */
.data-card { padding: 0; overflow: hidden; gap: 0; }
.data-card > .list-toolbar { padding: 16px 20px; border-bottom: 1px solid var(--color-divider); }
.data-card > .table-wrap { border-radius: 0; }
/* Заливка шапки здесь ПОВТОРЯЛА снятую в .table thead th и перебивала её по
   специфичности — на всех реестрах в .data-card микро-шапка так и оставалась
   на плашке. Осталось только гашение скруглений: карточка режет углы сама. */
.data-card > .table-wrap .table thead th:first-child,
.data-card > .table-wrap .table thead th:last-child { border-radius: 0; }
/* Липкой шапке внутри .data-card заливка нужна — карточка её не даёт. */
.data-card > .table-wrap .table--sticky thead th { background: var(--color-surface); }
.data-card > .card__foot { padding: 16px 20px; margin-top: 0; }
.data-card .table tbody td { padding-left: 18px; }
.data-card .table thead th { padding-left: 18px; }

/* ---- List toolbar (above a table) -------------------------------------- */
.list-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.list-toolbar__title { font-size: 14px; font-weight: 700; color: var(--color-heading); }
.list-toolbar__meta { color: var(--color-muted); font-size: 13px; }
.list-toolbar__actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }

/* ---- Row action buttons ------------------------------------------------ */
.row-actions { display: inline-flex; gap: 6px; justify-content: flex-end; }

/* ---- Form shell + sticky action bar ------------------------------------ */
.form-shell { max-width: 920px; width: 100%; display: flex; flex-direction: column; gap: 18px; }

/* Формы на широком мониторе (02.08.2026). Полотно теперь во всю ширину окна,
   и форма из нескольких карточек висела узкой лентой по центру с пустыми
   полями по 500px с каждой стороны. На широком экране карточки формы встают
   в две колонки — читается как один разворот, а не как обрывок страницы.
   Условия намеренно жёсткие:
   * ТОЛЬКО если карточек больше одной (`:has(> .card ~ .card)`) — единственную
     карточку растягивать нельзя, поля ввода превратились бы в строки на
     полтора метра;
   * ровно ДВЕ колонки, а не auto-fit: на 24" auto-fit давал три, и короткие
     карточки («Важные контакты») оставляли под собой дыры в полэкрана —
     сетка выравнивает строки по самой высокой карточке;
   * медиазапрос меряет РЕАЛЬНЫЙ вьюпорт, а полотно ужато zoom'ом (см. UI
     scale): 1200px экрана ≈ 1500px полотна ≈ две колонки по 600px + сайдбар.
   Форма из ОДНОЙ карточки в две колонки не раскладывается — ей вместо этого
   поднят потолок ширины, а поля внутри переходят на auto-fit (правило ниже).
   Кнопки сохранения и общие ошибки всегда во всю ширину: они относятся ко
   всей форме, а не к левой её половине. */
@media (min-width: 1200px) {
  .form-shell:has(> .card ~ .card),
  .pvz-form:has(> .card ~ .card) {
    max-width: none;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
  .form-shell:has(> .card ~ .card) > .form-actions,
  .form-shell:has(> .card ~ .card) > .alert,
  .form-shell:has(> .card ~ .card) > .card--wide,
  .pvz-form:has(> .card ~ .card) > .form-actions,
  .pvz-form:has(> .card ~ .card) > .alert,
  .pvz-form:has(> .card ~ .card) > .card--wide { grid-column: 1 / -1; }

  /* Форма из одной карточки: колонок ей не нарезать (это одна смысловая
     группа полей), поэтому просто снимаем тесный потолок и даём полям встать
     по месту. auto-fit вместо жёстких двух колонок — в карточке на 1180px
     помещается три поля в ряд, а в узкой (например, в левой колонке
     двухколоночной формы) остаётся два или одно. */
  .form-shell { max-width: 1180px; }
  .card--form { max-width: 1180px; }
  .form-grid { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
}
/* Панель «Сохранить» едет за прокруткой поверх длинной формы. Фон именно
   непрозрачный: полупрозрачная панель поверх карточки читалась как сломанная
   вёрстка — сквозь кнопку просвечивал текст следующего блока. */
.form-actions--sticky {
  position: sticky; bottom: 0; z-index: 12;
  /* без горизонтального «вылета»: он давал 4px overflow у колонки-родителя */
  margin: 6px 0 -4px; padding: 14px 18px;
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  box-shadow: 0 -10px 28px -18px rgba(15, 20, 35, .55), var(--shadow-sm);
}
.form-actions--sticky .btn { min-width: 150px; }
.form-actions--sticky .cell-muted { font-size: 12.5px; line-height: 1.4; }

/* =========================================================================
   Stage 22 — «Litten Graphite» additions
   Embedded Yandex map, human-readable work schedule, camera preload UI.
   Purely presentational; consumes the tokens above (light/dark automatic).
   ========================================================================= */

/* ---- Embedded Yandex map ------------------------------------------------ */
.map-embed {
  position: relative; overflow: hidden;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-soft);
  min-height: 320px;
}
.map-embed__iframe {
  position: relative; z-index: 2; display: block; border: 0;
  width: 100%; height: min(calc(46vh * var(--vu)), 420px); min-height: 320px;
  background: transparent;
  opacity: 0; transition: opacity .4s var(--ease);
}
.map-embed__iframe.is-loaded { opacity: 1; }
.map-embed__loader {
  position: absolute; inset: 0; z-index: 1;
  display: grid; place-items: center; gap: 10px;
  color: var(--color-muted); font-size: 13px;
}
.map-embed__loader::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg,
    transparent 30%,
    color-mix(in srgb, var(--color-surface) 65%, transparent) 50%,
    transparent 70%);
  background-size: 220% 100%;
  animation: skeleton-shimmer-bg 1.4s linear infinite;
}
@keyframes skeleton-shimmer-bg {
  from { background-position: 120% 0; }
  to { background-position: -120% 0; }
}
.map-embed__badge {
  position: absolute; z-index: 3; top: 12px; left: 12px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-surface) 88%, transparent);
  backdrop-filter: blur(6px);
  border: 1px solid var(--color-border);
  color: var(--color-text); font-size: 12px; font-weight: 600;
  box-shadow: var(--shadow-sm); pointer-events: none;
}
.map-embed__badge svg { width: 14px; height: 14px; color: var(--color-primary); }

/* ---- Human-readable work schedule (no raw JSON) ------------------------- */
.schedule-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.schedule-list__row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 6px 10px; border-radius: var(--radius-sm); font-size: 13px;
}
.schedule-list__row:nth-child(odd) { background: var(--color-surface-alt); }
.schedule-list__row.is-today {
  background: var(--color-primary-soft);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent);
}
.schedule-list__days { color: var(--color-muted); font-weight: 500; }
.schedule-list__row.is-today .schedule-list__days { color: var(--color-primary-dark); font-weight: 600; }
.schedule-list__hours { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.schedule-list__hours--closed { color: var(--color-faint); font-weight: 500; }

/* ---- Camera preload / loading pill -------------------------------------- */
.camera-player__loader-pill {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 10px 16px; border-radius: 999px;
  background: rgba(7, 11, 18, 0.72); backdrop-filter: blur(6px);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.12), 0 8px 24px rgba(0,0,0,.35);
}
.camera-player__loader .spinner {
  width: 20px; height: 20px; border-width: 2.5px;
  border-color: rgba(255,255,255,.18); border-top-color: #fff;
}
.camera-player__loader-label { font-size: 13px; color: rgba(255,255,255,.85); white-space: nowrap; }
.camera-card.is-loading .camera-card__media::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,.08) 50%, transparent 70%);
  background-size: 220% 100%;
  animation: skeleton-shimmer-bg 1.2s linear infinite;
}

/* ---- Detail page: compact right column ----------------------------------- */
/* Cards in a detail grid keep natural height (no stretching a one-line card
   to the height of its tall neighbour); the right column stacks compact cards. */
.grid--detail { align-items: start; }
.detail-stack { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
/* Стопка нужна только рядом со второй колонкой: без неё (карточка сотрудника
   без панели оплаты — роль без денег) карточки возвращаются в сетку сами и
   снова встают рядом, а не одна под другой в половину ширины. */
.detail-stack--unwrap { display: contents; }
/* В один столбец стопка не нужна вовсе: карточки возвращаются в сетку, и
   «Оплата труда» встаёт сразу за кадровыми полями — примечания и аудит уходят
   в хвост страницы, а не разделяют собой то, ради чего карточку открывают. */
@media (max-width: 819px) {
  .detail-stack--employee { display: contents; }
  .card--emp-notes { order: 2; }
}
.pvz-assignment-list--compact li { padding: 8px 0; }

/* ---------------------------------------------------------------------------
   Чат «сотрудник ↔ администрация ПВЗ»
   --------------------------------------------------------------------------- */
/* Компактная шапка страниц чата — как в мессенджере: слева стрелка «назад» и
   кружок собеседника во всю высоту, справа от них два этажа — имя сверху, одна
   приглушённая строка контекста снизу. До 03.09.2026 здесь стоял flex по
   БАЗОВОЙ ЛИНИИ, и всё содержимое шапки (стрелка, имя, адрес точки, кнопка,
   получатели) выстраивалось одной длинной лентой разной насыщенности —
   заказчик назвал это свалкой. Раскладка теперь общая с мобильной: mobile.css
   только подрезает величины. */
.page__head--compact { align-items: center; padding-bottom: 0; flex-shrink: 0; row-gap: 6px; }
/* ЛОВУШКА: раскладку держит именно «первый div». Стрелка, аватар, заголовок и
   подпись обязаны лежать ВНУТРИ него; кнопки шапки — только соседями
   (.page__actions), иначе они снова встанут посреди текста. */
.page__head--compact > div:first-child {
  display: grid; flex: 1 1 auto; min-width: 0;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "back title" "back sub";
  align-items: center; column-gap: 10px;
}
/* Аватар есть не у всех переписок: без :has пустая колонка всё равно съедала
   бы свой column-gap, и стрелка отъезжала бы от заголовка на двойной отступ. */
.page__head--compact > div:first-child:has(> .chat-head-avatar) {
  grid-template-columns: auto auto minmax(0, 1fr);
  grid-template-areas: "back avatar title" "back avatar sub";
}
/* Подписи нет (личный диалог, режим просмотра) — вторая строка лишняя: пустой
   трек «sub» забирает часть высоты, которую распирает аватар, и заголовок
   встаёт ВЫШЕ центра кружка. */
.page__head--compact > div:first-child:not(:has(> .page__subtitle)) {
  grid-template-areas: "back title";
}
.page__head--compact > div:first-child:has(> .chat-head-avatar):not(:has(> .page__subtitle)) {
  grid-template-areas: "back avatar title";
}
.page__head--compact > div:first-child > .chat-back { grid-area: back; }
.page__head--compact > div:first-child > .chat-head-avatar { grid-area: avatar; }
.page__head--compact .page__title { grid-area: title; font-size: 16px; min-width: 0; }
/* Контекст — одной строкой с многоточием: длинный адрес или список адресатов
   не имеет права раздуть шапку на три этажа. Заголовок при этом НЕ режем: у
   staff-переписок внутри него лежит бейдж роли, а inline-flex обрезается без
   многоточия — «с кем я говорю» дороже сэкономленных пикселей. */
.page__head--compact .page__subtitle {
  grid-area: sub; margin-top: 0; font-size: 12.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Стрелка «назад» слева от заголовка чата — как в мессенджерах.
   Лежит внутри первого div шапки (иначе сломается селектор div:first-child выше). */
.chat-back {
  width: 34px; height: 34px; border-radius: 50%;
  flex-shrink: 0; align-self: center; margin-right: 2px;
}
.chat-back svg { width: 18px; height: 18px; }

/* Картинка группы в шапке чата: рядом со стрелкой «назад», как в мессенджерах.
   Лежит внутри первого div шапки (см. ловушку селектора div:first-child выше). */
.chat-head-avatar {
  flex-shrink: 0; align-self: center; display: inline-flex; border-radius: 50%;
  transition: transform .18s var(--ease), box-shadow .18s var(--ease);
}
a.chat-head-avatar:hover { transform: scale(1.06); box-shadow: var(--ring-soft); }
/* Имя в шапке чата — такая же ссылка в карточку, как аватар: выглядит обычным
   заголовком, подчёркивается только под курсором (заказчик, 21.08.2026 —
   «клик не только по аватарке, но и по имени»). */
.chat-head-name { color: inherit; text-decoration: none; }
.chat-head-name:hover { text-decoration: underline; text-underline-offset: 3px; }
.chat-head-name:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; border-radius: 4px; }
.chat-head-avatar__img { width: 34px; height: 34px; font-size: 14px; }

/* Страница чата занимает ровно высоту окна; ленту растягивает flex —
   высота шапки/композера может быть любой, без ручных calc-констант. */
.page:has(> .chat-card) {
  height: calc(calc(100vh * var(--vu)) - var(--topbar-h));
  height: calc(calc(100dvh * var(--vu)) - var(--topbar-h));
  padding-top: 12px; padding-bottom: 16px; gap: 12px;
}
/* gap 18, а не 12: между последней репликой и полем ввода нужен воздух —
   иначе пузырь «лежит» на композере (заказчик, 22.08.2026). */
.chat-card { display: flex; flex-direction: column; gap: 18px; padding: 14px; flex: 1 1 auto; min-height: 0; }
/* Единственное исключение из «полотно во всю ширину» (02.08.2026): переписка.
   На 27" пузыри уезжали к правому краю в трёх метрах от аватара собеседника, а
   между ними висело 2000px пустоты — читать это невозможно. Диалог и список
   диалогов держат колонку разговора; всё остальное в системе тянется во всю
   ширину. Класс ставится через {% block page_class %} в base_app.html. */
.page--reading > * { width: 100%; max-width: 1240px; margin-inline: auto; }
.page:has(> .chat-card) > * { width: 100%; max-width: 1240px; margin-inline: auto; }
/* Правый отступ снят со скроллера и перенесён на сами сообщения (заказчик,
   22.08.2026: «полосу прокрутки правее, за чат»): полоса едет по внешней
   кромке ленты, а не вплотную к пузырям. Побочно это чинит обрезанный хвост
   своего пузыря — ему нужно 7px справа, а их не было. */
.chat-box {
  display: flex; flex-direction: column; gap: 3px;
  flex: 1 1 auto; min-height: 300px; overflow-y: auto;
  /* Кромки у ленты нет (03.09.2026): в макете переписка лежит прямо на
     карточке. Рамка была нужна, пока полотно ленты отличалось от карточки
     (#E4E8F0 в светлой, #121316 в тёмной) — теперь полотно РАВНО карточке в
     обеих темах, и рамка рисовала бы пустую коробку внутри коробки. Ленту от
     карточки больше ничто не отделяет, и это ровно то, что в макете: границу
     держат сами пузыри. В двух окнах и на телефоне рамка и так снималась. */
  padding: 14px 4px 20px 14px; border: 0;
  border-radius: var(--radius-md); background: var(--chat-canvas);
  /* Прокрутка ленты не должна утягивать за собой страницу (и pull-to-refresh
     на телефоне): лента доскроллила до края — значит, доскроллила. */
  overscroll-behavior: contain;
}
.chat-msg { display: flex; padding-right: 10px; }
.chat-msg--own { justify-content: flex-end; }
/* Разговор начинается у нижней кромки, а не у верхней: в коротком чате два
   сообщения висели под шапкой, а под ними до самого композера зияла пустота —
   в мессенджерах так не бывает. Именно margin-top: auto у первого элемента, а
   не justify-content: flex-end у ленты: auto-отступ схлопывается в ноль, как
   только сообщения перестают помещаться, и прокрутка вверх остаётся честной
   (у flex-end верх переполнения становится недосягаем). */
.chat-box > :first-child { margin-top: auto; }
/* Отдельных «margin-top: 0 у первого» тут быть не должно: та же
   специфичность ниже по файлу перебивает auto, и лента снова липнет к
   верхней кромке. auto и так схлопывается в ноль, когда места нет. */
/* Пустой ленте auto-отступ достаётся с обеих сторон — заглушка по центру. */
.chat-box > .chat-empty { margin-block: auto; }
/* Серия — реплики одного человека подряд (starts_group / ends_group ставит
   _mark_own). Внутри серии пузыри стоят вплотную (gap ленты), между сериями
   отступ больше: так читается, где кончается одна реплика и начинается
   ответ. Аватар рисуется у каждого пузыря, но виден только у последнего —
   без него пузыри серии съехали бы к краю ленты. */
/* Воздух между сериями. С 04.09.2026 под каждым пузырём стоит своя подпись
   (18px), и прежние 15 сверху давали между репликами разных людей уже 33 —
   лента рассыпалась. Оставляем 8: вместе с полосой подписи это те же ~26
   экранных px, что в макете. */
.chat-msg--start { margin-top: 8px; }
.chat-msg:not(.chat-msg--end) .chat-msg__avatar { visibility: hidden; }
.chat-msg:not(.chat-msg--start) .chat-msg__meta { display: none; }
/* Своё имя над своим же пузырём не подписываем — как в мессенджерах. Только
   там, где «своё» значит «моё» (группа, личный диалог); в треде сотрудника
   «своя» сторона — вся администрация, и подпись там нужна (см. _name_style). */
.chat-msg--noauthor .chat-msg__meta { display: none; }

/* Разделитель дня — «пилюля» по центру ленты, липнет к верхней кромке при
   прокрутке (как в мессенджерах: видно, какой день читаешь). */
.chat-day {
  position: sticky; top: -4px; z-index: 1;
  display: flex; justify-content: center; margin: 12px 0 6px;
  pointer-events: none;
}
.chat-day__label {
  padding: 3px 12px; border-radius: var(--radius-pill);
  /* Пилюля едет по той же лестнице, что и пузыри, а не по общей поверхности
     (26.08.2026): на посветлевшем полотне карточка #191A1D давала 1.12:1 и
     разделитель дня терялся, тогда как цвет чужого пузыря даёт 1.64:1. */
  background: color-mix(in srgb, var(--chat-bubble) 88%, transparent);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  /* Кромки нет (03.09.2026): в макете разделитель дня — подпись по центру
     ленты, а рамка превращала её в чип. Форму держит заливка. */
  border: 0;
  font-size: 11.5px; font-weight: 650; color: var(--chat-muted);
  white-space: nowrap;
}
/* Появление только что отправленного сообщения (класс ставит сервер в ответе на hx-post). */
@keyframes chat-msg-in {
  from { opacity: 0; transform: translateY(14px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.chat-msg--in { animation: chat-msg-in .3s var(--ease) both; }
.chat-msg__bubble {
  position: relative;
  max-width: min(78%, 560px);
  /* Геометрия по макету 03.09.2026: там радиус 18 и поля 13/8 — при
     html{zoom:.85} в файл пишем 21 и 15/9, иначе на экране выйдет на 15%
     мельче задуманного. Нижнее поле — это полоса ПОД пузырём, в которой
     стоит подпись со временем (см. .chat-msg__stamp): менять их в паре. */
  padding: 9px 15px 8px; border-radius: 21px; margin-bottom: 18px;
  background: var(--chat-bubble);
  /* Пузырь — собственная поверхность, и вторичные тона на нём свои: общие
     токены считаны от карточки, а пузырь ни в одной теме карточкой не
     является (26.08.2026, вместе с разведением ступеней контраста). Меняем
     сами --color-*, а не заводим новые имена: внутри пузыря живут чужие
     компоненты (карточка ссылки, чип реакции, строка файла, галочки
     прочтения), и они читают именно общие токены — так поднимаются все
     разом, включая те, что появятся потом. */
  --color-muted: var(--chat-muted);
  --color-faint: var(--chat-faint);
  --color-primary: var(--chat-accent);
  /* Кромки у пузыря нет (22.08.2026, «как в телеграме»): рамка вокруг каждой
     реплики дробила ленту на карточки. Форму держит заливка, а в светлой теме
     ещё и мягкая тень — на светлом полотне одной заливки мало. */
  border: 0; box-shadow: var(--chat-bubble-shadow);
}
/* «Хвост» у последнего пузыря серии (крючок, как в мессенджерах) заказчик
   попросил 22.08.2026 и прислал скриншот. С 04.09.2026 он НЕ РИСУЕТСЯ: в
   новом макете хвоста нет вовсе, а подпись со временем уехала под пузырь —
   ровно туда, куда крючок показывал остриём, и они дрались за одно место.
   Код оставлен целиком: вернуть хвост — снять display: none ниже. От серии
   остаётся распрямлённый угол со стороны говорящего, он и держит «кто
   сказал» (см. два правила под этим). */
.chat-msg--end .chat-msg__bubble::after {
  content: ""; position: absolute; bottom: 0;
  width: 8px; height: 14px;
  background: inherit;
  clip-path: path("M8 14 L8 0 C8 7 5 11.5 0 14 Z");
  display: none;
}
.chat-msg--end:not(.chat-msg--own) .chat-msg__bubble { border-bottom-left-radius: 8px; }
.chat-msg--end:not(.chat-msg--own) .chat-msg__bubble::after { left: -7px; }
.chat-msg--end.chat-msg--own .chat-msg__bubble { border-bottom-right-radius: 8px; }
/* Свой пузырь — зеркально: тот же крючок, отражённый по горизонтали. */
.chat-msg--end.chat-msg--own .chat-msg__bubble::after { right: -7px; transform: scaleX(-1); }
/* Где clip-path с path() не поддержан, псевдоэлемент был бы прямоугольным
   огрызком у кромки — там хвоста не показываем вовсе, остаётся прежний
   скруглённый уголок. */
@supports not (clip-path: path("M0 0 L1 0 Z")) {
  .chat-msg--end .chat-msg__bubble::after { display: none; }
  .chat-msg--end:not(.chat-msg--own) .chat-msg__bubble { border-bottom-left-radius: 5px; }
  .chat-msg--end.chat-msg--own .chat-msg__bubble { border-bottom-right-radius: 5px; }
}
.chat-msg--own .chat-msg__bubble {
  background: var(--chat-bubble-own);
  /* Свой пузырь заметно синее чужого — вторичные тона на нём тоже свои. */
  --color-muted: var(--chat-muted-own);
  --color-faint: var(--chat-faint-own);
  --color-primary: var(--chat-accent-own);
}
.chat-msg__meta {
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 2px;
  font-size: 12px; color: var(--color-muted);
}
.chat-msg__meta strong { color: var(--color-text); font-size: 12.5px; }
/* Палитра имён (22.08.2026): цвет закреплён за отправителем (id % 7), поэтому
   в группе взгляд отличает реплики по подписи, не читая её. Берём токены
   системы — они уже приведены к контрасту AA в обеих темах. c0 — нейтральный:
   им подписана замаскированная «Администрация» (см. _name_style). */
.chat-msg__meta--c1 strong, .chat-msg__meta--c1 .chat-msg__author { color: var(--color-primary); }
.chat-msg__meta--c2 strong, .chat-msg__meta--c2 .chat-msg__author { color: var(--color-coral); }
.chat-msg__meta--c3 strong, .chat-msg__meta--c3 .chat-msg__author { color: var(--color-violet); }
.chat-msg__meta--c4 strong, .chat-msg__meta--c4 .chat-msg__author { color: var(--color-cyan); }
.chat-msg__meta--c5 strong, .chat-msg__meta--c5 .chat-msg__author { color: var(--color-success); }
.chat-msg__meta--c6 strong, .chat-msg__meta--c6 .chat-msg__author { color: var(--color-warning); }
.chat-msg__meta--c7 strong, .chat-msg__meta--c7 .chat-msg__author { color: var(--color-magenta); }
/* Свой пузырь — сплошная синяя заливка в обеих темах (03.09.2026), текст на
   нём белый. Переопределяем САМ --color-text, а не только цвет реплики:
   внутри пузыря живут карточка ссылки, чип реакции, строка файла, цитата и
   опрос — все они считают свои полупрозрачные заливки именно от него, и одним
   токеном поднимаются разом. */
.chat-msg--own .chat-msg__bubble {
  --color-text: #FFFFFF;
  color: #FFFFFF;
}
/* Палитра имён на синем. Тона у тем РАЗНЫЕ, потому что разные заливки: в
   светлой это бренд-синий #005BFF, в тёмной — приглушённый #1E56B8 (заказчик,
   04.09.2026: «цвет слишком яркий»). Чем светлее заливка, тем бледнее обязана
   быть подпись: на #005BFF пастель вынужденно почти белая, на #1E56B8 тона
   заметно живее. У каждого из шести проверено 4.60–4.75 ✓ AA.
   Соответствие «номер цвета → отправитель» не трогаем, меняются только тона. */
html[data-theme="light"] .chat-msg--own .chat-msg__bubble,
:root:not([data-theme="dark"]) .chat-msg--own .chat-msg__bubble {
  --color-coral: #FEEDF4;
  --color-violet: #F3F0FF;
  --color-cyan: #D2F5FA;
  --color-success: #C1FBE1;
  --color-warning: #FDECD5;
  --color-magenta: #FCEDF8;
}
html[data-theme="dark"] .chat-msg--own .chat-msg__bubble {
  --color-coral: #FBC9DC;
  --color-violet: #D8CEFF;
  --color-cyan: #80E4F2;
  --color-success: #3AF19E;
  --color-warning: #FACD90;
  --color-magenta: #F5C8EB;
}
/* Наведение на имя-ссылку только подчёркивает: цвет уже несёт смысл. */
.chat-msg__author:hover { color: inherit; }
/* flow-root — чтобы плавающая подпись времени осталась ВНУТРИ текста и не
   вылезала на реакции под ним. */
.chat-msg__text { display: flow-root; font-size: 15px; line-height: 1.4; word-break: break-word; }

/* Ссылка внутри текста сообщения (26.08.2026). До этого присланный адрес
   оставался буквами: его выделяли мышью и копировали руками. Подпись —
   полный адрес, как его набрали: контекст-меню пузыря копирует именно текст,
   и обрезанный «https://ozon.ru/…» в буфере обмена был бы хуже длинного.
   overflow-wrap: anywhere — чтобы адрес без пробелов не растягивал пузырь. */
.chat-link {
  color: var(--color-primary); overflow-wrap: anywhere;
  text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px;
}
.chat-link:hover { text-decoration-thickness: 2px; }

/* Карточка ссылки под текстом (превью). Обложку раздаёт наша вьюха — чужих
   доменов в разметке нет, CSP не трогали. Вертикальная черта слева, как в
   мессенджерах: карточка читается частью пузыря, а не отдельным блоком.
   Ширина заперта: в узком мини-окне чата 320px превращаются в 100%. */
.chat-link-card {
  display: block; margin: 6px 0 2px; overflow: hidden;
  /* Ширина фиксированная, а не по содержимому: иначе карточка короткой
     ссылки съёживалась в узкий столбик с обрезанной обложкой. Пузырь она
     при необходимости растягивает — как в мессенджерах; на узком экране
     ужимается сама. */
  width: 300px; max-width: 100%;
  border-radius: 12px; border-left: 3px solid var(--color-primary);
  background: color-mix(in srgb, var(--color-text) 6%, transparent);
  color: inherit; text-decoration: none;
  transition: background-color .15s var(--ease);
}
.chat-link-card:hover { background: color-mix(in srgb, var(--color-primary) 14%, transparent); }
.chat-link-card:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.chat-link-card__cover { position: relative; display: block; }
.chat-link-card__cover img {
  display: block; width: 100%; max-height: 180px; object-fit: cover;
}
/* Обложка ролика — строго 16:9. YouTube отдаёт кадр 480×360 с чёрными
   полями сверху и снизу (сам кадр внутри — 16:9), и без этого правила поля
   оставались бы в карточке чёрными рамками. */
.chat-link-card--video .chat-link-card__cover img { aspect-ratio: 16 / 9; max-height: none; }
/* Ссылка ведёт прямо на картинку — показываем её целиком, а не обрезанной. */
.chat-link-card--image .chat-link-card__cover img { max-height: 300px; object-fit: contain; }
/* «Плей» поверх обложки: за ссылкой видео. Кружок тёмный в обеих темах —
   он лежит на кадре, а не на полотне страницы. */
.chat-link-card__play {
  position: absolute; inset: 0; margin: auto;
  display: flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; border-radius: 50%;
  background: rgba(8, 10, 14, .62); color: #fff;
  transition: background-color .15s var(--ease), transform .15s var(--ease);
}
.chat-link-card__play svg { width: 26px; height: 26px; margin-left: 2px; }
.chat-link-card:hover .chat-link-card__play {
  background: rgba(8, 10, 14, .78); transform: scale(1.06);
}
.chat-link-card__body { display: flex; flex-direction: column; gap: 2px; padding: 7px 10px 8px; }
.chat-link-card__site {
  font-size: 11.5px; font-weight: 700; color: var(--color-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-link-card__title { font-size: 13.5px; font-weight: 650; line-height: 1.3; }
/* Описание — не больше трёх строк: карточка не должна перевешивать реплику. */
.chat-link-card__text {
  font-size: 12.5px; line-height: 1.35; color: var(--color-muted);
  display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3;
  -webkit-box-orient: vertical; overflow: hidden;
}
/* Блок-невидимка: он существует ровно для того, чтобы дозапросить карточку
   (hx-trigger="load") и заменить себя ответом. */
.chat-link-lazy { display: none; }

/* Реакции под текстом пузыря (22.08.2026): ставятся двойным щелчком (сердечко)
   и панелью эмодзи над контекст-меню. Заливка чипа — полупрозрачная от цвета
   текста, а не токен поверхности: пузыри бывают двух цветов (свой —
   --color-primary-soft, чужой — --color-surface), и любая сплошная заливка
   пропадала бы на одном из них. Пустой блок скрыт (:empty) — он остаётся в
   разметке, чтобы app.js было куда вставить ответ сервера. */
.chat-msg__reacts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.chat-msg__reacts:empty { display: none; }
.chat-react {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px; border-radius: var(--radius-pill);
  border: 1px solid transparent;
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
  color: var(--color-text); font: inherit; font-size: 12px; font-weight: 700;
  line-height: 1.5; cursor: pointer;
  transition: background-color .14s var(--ease), border-color .14s var(--ease),
              transform .16s var(--ease-out);
}
.chat-react:hover { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.chat-react:active { transform: scale(0.94); }
/* Своя реакция подсвечена фирменным — «этот голос мой». */
.chat-react--mine {
  background: color-mix(in srgb, var(--color-primary) 20%, transparent);
  border-color: color-mix(in srgb, var(--color-primary) 55%, transparent);
  color: var(--color-primary);
}
.chat-react--mine:hover { background: color-mix(in srgb, var(--color-primary) 28%, transparent); }
/* Зритель (тихий просмотр, чужая переписка) реакции видит, но не жмёт. */
.chat-react--static { cursor: default; }
.chat-react--static:hover { background: color-mix(in srgb, var(--color-text) 8%, transparent); }
.chat-react__emoji { font-size: 13px; line-height: 1; }
.chat-react__count { font-variant-numeric: tabular-nums; }
/* Прилетевшая реакция коротко «пружинит»: без этого свой клик и чужая реакция
   из поллинга появляются одинаково незаметно. */
@keyframes chat-react-pop {
  0%   { transform: scale(0.72); opacity: 0; }
  45%  { transform: scale(1.1); opacity: 1; }
  72%  { transform: scale(0.985); }
  100% { transform: scale(1); }
}
.chat-react--pop { animation: chat-react-pop .24s cubic-bezier(.22, .7, .3, 1) both; }
/* Прилёт реакции НА СВОЁ сообщение (как в Telegram): пузырь коротко качается,
   а от подписи вверх разлетаются несколько копий эмодзи. Частицы рисует
   app.js, живут ~900мс и убираются сами. Всё это выключено при
   prefers-reduced-motion: пляшущий интерфейс — не украшение, а помеха. */
@keyframes chat-bubble-cheer {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.028); }
  55%  { transform: scale(0.995); }
  78%  { transform: scale(1.006); }
  100% { transform: scale(1); }
}
.chat-msg__bubble--cheer { animation: chat-bubble-cheer .38s cubic-bezier(.3, .6, .3, 1) both; }
/* Рой живых эмодзи на реакцию (lottie-web + Noto Animated Emoji, см.
   static/anim/reactions/ATTRIBUTION.txt). Копии рождаются НА ЗНАЧКЕ и
   разлетаются по своим лучам; центр, размер, вектор полёта, наклон и задержку
   ставит app.js переменными. Живой — только центральный эмодзи, разлетающиеся
   стоят на одном кадре (см. playReactionBurst).

   Три слоя на копию, по одному на составляющую движения:
     .chat-react-burst        — едет по X и гаснет;
     .chat-react-burst__mid   — идёт по Y: вверх, потом чуть проседает;
     .chat-react-burst__body  — вспыхивает в размере и вращается.
   Разнесено намеренно: у каждого слоя своя кривая и 2–3 кадра, поэтому
   скорость нигде не рвётся. В одной keyframe-анимации с четырьмя стопами
   easing перезапускался на КАЖДОМ стопе — на стыках движение дёргалось, и это
   читалось как «нет плавности» (23.08.2026). Разные кривые по X и по Y дают
   дугу: копия летит не по линейке, а по параболе, как настоящая частица.
   Без filter (drop-shadow считался бы на каждый кадр поверх живого SVG). */
.chat-react-burst {
  position: fixed; z-index: 141; pointer-events: none;
  width: var(--burst-size, 74px); height: var(--burst-size, 74px);
  margin-top: calc(var(--burst-size, 74px) / -2);
  margin-left: calc(var(--burst-size, 74px) / -2);
  will-change: transform, opacity;
  animation:
    chat-react-burst-x var(--burst-time, 1.05s) cubic-bezier(.12, .72, .22, 1) both,
    chat-react-burst-fade var(--burst-time, 1.05s) linear both;
}
.chat-react-burst__mid {
  width: 100%; height: 100%;
  will-change: transform;
  animation: chat-react-burst-y var(--burst-time, 1.05s) linear both;
}
.chat-react-burst__body {
  width: 100%; height: 100%;
  will-change: transform;
  animation: chat-react-burst-pop var(--burst-time, 1.05s) linear both;
}
/* Клонированный кадр приходит со своими width/height от плеера — растягиваем
   его по слою, иначе мелкие копии рисуются в натуральные 800–1024px. */
.chat-react-burst svg { display: block; width: 100%; height: 100%; }
/* Разбег по X — единственный отрезок с сильным ease-out: быстрый старт от
   значка и долгий плавный выкат. */
@keyframes chat-react-burst-x {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(var(--burst-x, 0px), 0, 0); }
}
/* Подъём по Y: разгон и торможение до нуля к 70%, затем лёгкое проседание —
   «тяжесть» в конце пути. Скорость на стыке непрерывна: ease-out приходит в
   ноль, ease-in из нуля и стартует. */
@keyframes chat-react-burst-y {
  0%   { transform: translate3d(0, 0, 0); animation-timing-function: cubic-bezier(.1, .7, .2, 1); }
  70%  { transform: translate3d(0, var(--burst-y, -78px), 0); animation-timing-function: cubic-bezier(.5, 0, .8, .6); }
  100% { transform: translate3d(0, calc(var(--burst-y, -78px) + 14px), 0); }
}
/* Вспышка в размере — выстрел до 1.08 и плавное остывание до 0.82; поворот
   набирается на всём пути. */
@keyframes chat-react-burst-pop {
  /* Начальный масштаб 0.62, а не 0.3: копии появляются все разом, и глубокий
     «выстрел из точки» у каждой складывался в мельтешение. */
  0%   { transform: scale(0.62) rotate(0deg); animation-timing-function: cubic-bezier(.2, .9, .35, 1.12); }
  20%  { transform: scale(1.06) rotate(calc(var(--burst-turn, 0deg) * 0.4)); animation-timing-function: cubic-bezier(.4, 0, .4, 1); }
  100% { transform: scale(0.82) rotate(var(--burst-turn, 0deg)); }
}
/* Проявляется почти мгновенно (первые 4%): все копии стартуют в одном кадре,
   и растянутое проявление читалось бы как «спавн по одной». Тает в последней
   трети. */
@keyframes chat-react-burst-fade {
  0%   { opacity: 0; }
  4%   { opacity: 1; }
  64%  { opacity: 1; }
  100% { opacity: 0; }
}


/* Строка реакций вырастает, а не выскакивает (swapReacts в app.js): первая
   реакция раздвигает пузырь на целую строку, и без перехода вся лента под ним
   прыгала — самая грубая «неплавность», и не в анимации, а в раскладке.
   Переходы сидят на самой строке; стартовое состояние ставит класс --enter на
   один кадр, --leave — схлопывание перед подменой на пустую. */
.chat-msg__reacts {
  max-height: 120px;
  transition: max-height .28s cubic-bezier(.2, .8, .2, 1), opacity .2s var(--ease),
              margin-top .28s cubic-bezier(.2, .8, .2, 1);
}
.chat-msg__reacts--enter,
.chat-msg__reacts--leave { max-height: 0; opacity: 0; margin-top: 0; overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .chat-react--pop, .chat-msg__bubble--cheer { animation: none; }
  .chat-react-burst { display: none; }
  .chat-msg__reacts { transition: none; }
}

/* Подпись пузыря: время и галочки. С 04.09.2026 она стоит ПОД пузырём, на
   полотне (макет заказчика), а не внутри him — раньше время плавало в
   последней строке текста по схеме Telegram.

   Сделано абсолютом, а не переносом в разметку: лента перерисовывается
   поллингом каждые 10-20 секунд и собирается в четырёх местах (страница,
   ответ на отправку, окно дока, монитор диалогов) — лишний узел в шаблоне
   стоил бы правок во всех. Якорь — сам пузырь (у него position: relative),
   место под подпись — его margin-bottom. Тона берём с ПОЛОТНА
   (--chat-stamp): внутри своего пузыря общие вторичные токены почти белые. */
.chat-msg__stamp,
.chat-msg__stamp--inline {
  position: absolute; top: calc(100% + 3px); right: 2px; left: auto;
  display: flex; align-items: center; justify-content: flex-end; gap: 5px;
  float: none; margin: 0; padding: 0;
  font-size: 11.5px; line-height: 1.3; color: var(--chat-stamp);
  font-variant-numeric: tabular-nums;
}
/* Чужая реплика — подпись под левым краем пузыря, своя — под правым. */
.chat-msg:not(.chat-msg--own) .chat-msg__stamp {
  left: 2px; right: auto; justify-content: flex-start;
}
.chat-msg__time { white-space: nowrap; }

/* Слой действий над пузырём БОЛЬШЕ НЕ ПОКАЗЫВАЕТСЯ (заказчик, 22.08.2026):
   всплывая при наведении, он накрывал верхний угол пузыря — а вместе с ним
   имя автора и подпись «Переслано от…». Те же действия целиком есть в
   контекст-меню по правой кнопке (пальцем — долгое нажатие).

   Из разметки кнопки при этом НЕ убраны: меню собирает свои пункты именно из
   них (`[data-chat-forward]`, `.chat-msg__delete`) — так права остаются на
   сервере, а меню лишь дублирует уже отрендеренное. visibility: hidden, а не
   display: none: элемент остаётся в дереве и находится querySelector'ом,
   но исчезает с экрана и выпадает из обхода табом. */
.chat-msg__actions {
  position: absolute; top: -10px; right: 8px; z-index: 2;
  display: flex; align-items: center; gap: 2px; padding: 2px 4px;
  border-radius: var(--radius-pill);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
  opacity: 0; pointer-events: none;
  transition: opacity .15s var(--ease);
}
/* Единственный случай, когда слой всё же показывается, — обход с клавиатуры:
   мышью до него не добраться (pointer-events: none), а у клавиатуры другого
   пути к удалению и пересылке нет — контекст-меню открывает указатель.
   opacity, а не visibility: скрытый visibility выпал бы и из обхода табом. */
.chat-msg__actions:focus-within { opacity: 1; pointer-events: auto; }
/* Кнопка удаления (только owner/tech_admin): живёт в слое .chat-msg__actions. */
.chat-msg__delete {
  width: 22px; height: 22px; padding: 0;
  color: var(--color-faint);
  transition: color .15s var(--ease);
}
.chat-msg__delete svg { width: 14px; height: 14px; }
.chat-msg__delete:hover { color: var(--color-danger); }
/* Кнопка пересылки (staff-ленты): там же, в слое действий. */
.chat-msg__forward {
  width: 22px; height: 22px; padding: 0;
  color: var(--color-faint);
  transition: color .15s var(--ease);
}
.chat-msg__forward svg { width: 14px; height: 14px; }
.chat-msg__forward:hover { color: var(--color-primary); }
/* Подпись пересланного сообщения — как в Telegram, над содержимым пузыря. */
.chat-msg__fwd {
  display: flex; align-items: center; gap: 6px; margin: 0 0 4px;
  font-size: 12px; font-weight: 600; color: var(--color-primary);
}
.chat-msg__fwd svg { width: 13px; height: 13px; flex: 0 0 auto; }
/* Имя автора оригинала в подписи — ссылка на его кадровую карточку. */
.chat-msg__fwd-link { color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; }
.chat-msg__fwd-link:hover { border-bottom-style: solid; }
/* Цитата ответа внутри пузыря (как в мессенджерах): вертикальная черта, автор,
   одна строка оригинала. Цвета — только общие токены: внутри пузыря
   --color-primary/--color-muted переопределены под своё и чужое облачко, и
   блок получает верный контраст в обеих темах сам. Заливка — color-mix по
   тексту: сплошные токены поверхности внутри пузыря запрещены. */
.chat-msg__quotes { display: flex; flex-direction: column; gap: 4px; margin: 0 0 6px; }
.chat-msg__quote {
  display: flex; flex-direction: column; gap: 1px; min-width: 0; max-width: 100%;
  padding: 4px 8px; text-align: left; font: inherit; font-size: 12px;
  border: 0; border-left: 3px solid var(--color-primary);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
  color: inherit; cursor: pointer;
  transition: background-color .15s var(--ease);
}
.chat-msg__quote:hover { background: color-mix(in srgb, var(--color-primary) 14%, transparent); }
.chat-msg__quote:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.chat-msg__quote--gone { cursor: default; color: var(--color-muted); font-style: italic; }
.chat-msg__quote-author { font-weight: 600; color: var(--color-primary); }
/* Одна строка с многоточием: пузырь узкий (max-width 78%), и длинная цитата
   раздувала бы короткую реплику на всю ленту. */
.chat-msg__quote-author,
.chat-msg__quote-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-msg__quote-text { color: var(--color-muted); }
/* Переход к оригиналу подсвечивает его на секунду. Держим заметно меньше 10 с:
   лента перерисовывается поллингом и класс всё равно снесёт ближайшим swap'ом. */
.chat-msg--found .chat-msg__bubble {
  animation: chat-msg-found 1.2s var(--ease);
}
@keyframes chat-msg-found {
  /* --chat-accent, а не --color-primary: внутри своего пузыря последний с
     03.09.2026 перекрашен в белый (пузырь стал сплошным синим), и обводка
     «нашли оригинал» на белом полотне пропадала бы вовсе. Токен переписки
     на пузыре не переопределяется и остаётся акцентом полотна. */
  0%, 60% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--chat-accent) 45%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .chat-msg--found .chat-msg__bubble { animation: none; }
}
/* Режим выбора сообщений: зажать пузырь → отметить → переслать пачкой. */
.chat-select-mode .chat-box { user-select: none; }
.chat-select-mode .chat-msg[data-msg-id] .chat-msg__bubble { cursor: pointer; }
/* Режим выбора — как в мессенджере (заказчик, 22.08.2026): строка отъезжает
   влево, освобождая колонку под кружок у правого края; выбранная подсвечена
   целиком, а кружок залит фирменным и с галочкой.

   Кружок и галочку рисуют псевдоэлементы САМОЙ строки, а не разметка: пузыри
   пересоздаются поллингом, и любой добавленный в них узел пришлось бы
   восстанавливать после каждой перерисовки. Обводки у пузыря больше нет —
   она перекрашивала чужой пузырь в «свой» и путала стороны. */
/* Селектор с .chat-box не для красоты: у ленты в панели правый отступ строки
   задан как «.chat-split__view .chat-box > .chat-msg» — три класса, и ровно
   такая же специфичность у «.chat-select-mode .chat-msg[...]» проигрывала ему
   по порядку в файле. Здесь классов четыре, и режим выбора выигрывает всегда. */
.chat-select-mode .chat-box > .chat-msg[data-msg-id] {
  position: relative;
  padding-right: 46px;
  border-radius: var(--radius-md);
  transition: background-color .16s var(--ease);
}
.chat-select-mode .chat-box > .chat-msg[data-msg-id]::before {
  content: ""; position: absolute; right: 12px; top: 50%; margin-top: -20px;
  /* -20, а не -11: с 04.09.2026 под пузырём есть полоса подписи (18px), и
     центр СТРОКИ ниже центра пузыря ровно на её половину. Держать в паре с
     margin-bottom у .chat-msg__bubble. */
  width: 22px; height: 22px; border-radius: 50%;
  /* Заливка кружка — ПОЛОТНО, а не пузырь (03.09.2026): с тех пор как чужой
     пузырь стал тонированным, пустой кружок его цвета читался уже залитым. */
  border: 2px solid var(--color-border-strong); background: var(--chat-canvas);
  transition: background-color .16s var(--ease), border-color .16s var(--ease);
}
.chat-msg--selected { background: color-mix(in srgb, var(--color-primary) 13%, transparent); }
/* [data-msg-id] здесь ради веса: у правила пустого кружка выше он уже есть, и
   без него «выбранный» проигрывал бы ему по специфичности, а не по порядку. */
.chat-select-mode .chat-box > .chat-msg--selected[data-msg-id]::before {
  background: var(--color-primary); border-color: var(--color-primary);
}
/* Галочка внутри кружка — две кромки повёрнутого прямоугольника: рисунок без
   картинки и без иконочного шрифта. */
.chat-select-mode .chat-box > .chat-msg--selected[data-msg-id]::after {
  content: ""; position: absolute; right: 20px; top: 50%; margin-top: -6px;
  width: 6px; height: 10px;
  border: solid var(--color-on-primary); border-width: 0 2px 2px 0;
  transform: rotate(43deg);
}
.chat-select-bar {
  position: fixed; left: 50%; bottom: 22px; z-index: 125;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  /* --shadow-lg в системе нет, а неизвестный токен делает невалидным всё
     свойство: панель висела над лентой вообще без тени. */
  box-shadow: var(--shadow-pop);
  opacity: 0; transform: translateX(-50%) translateY(8px);
  transition: opacity .16s var(--ease), transform .18s var(--ease-out);
}
.chat-select-bar.is-open { opacity: 1; transform: translateX(-50%); }
.chat-select-bar[hidden] { display: none; }
/* Открыт пикер получателя — панель набора прячется: она висит выше модалки
   (125 против 120), а её «Переслать» в этот момент обманка — отправляет уже
   подвал модалки. Специфичность выше, чем у .chat-select-bar.is-open. */
html.chat-fwd-open .chat-select-bar.is-open { opacity: 0; pointer-events: none; }
.chat-select-bar__count { font-size: 13px; font-weight: 700; white-space: nowrap; }
/* Почему в наборе нет «Переслать»: у выключенной кнопки (pointer-events: none)
   не показать ни курсора, ни подсказки — объясняем подписью. */
.chat-select-bar__note { font-size: 12px; color: var(--color-muted); white-space: nowrap; }
.chat-select-bar__note[hidden] { display: none; }
.chat-select-bar .btn[hidden] { display: none; }
/* Имя автора — ссылка на кадровую карточку (staff-ленты). */
.chat-msg__author { color: inherit; text-decoration: none; }
.chat-msg__author:hover { color: var(--color-primary); text-decoration: underline; }
/* Контекст-меню сообщения — правая кнопка мыши (пальцем долгое нажатие).
   Собирает app.js. Вид взят с настольных мессенджеров: строки крупные, во всю
   ширину, иконка слева, опасное действие отбито разделителем и красным.
   Раскрывается из того угла, где стоял курсор (transform-origin ставит JS). */
.chat-msg-menu {
  position: fixed; z-index: 130; min-width: 236px; max-width: 320px; padding: 6px;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-pop);
  transform-origin: left top;
  opacity: 0; transform: scale(0.92) translateY(4px);
  transition: opacity .13s var(--ease), transform .18s var(--ease-out);
}
.chat-msg-menu.is-open { opacity: 1; transform: none; }
.chat-msg-menu[hidden] { display: none; }
.chat-msg-menu:focus { outline: none; }
/* Подложка «как у системного меню»: полотно чуть просвечивает и размывается.
   Отдельным @supports — там, где размытия нет, остаётся плотная поверхность
   из правила выше (прозрачность без блюра читалась бы как грязь). */
@supports ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
  .chat-msg-menu {
    background: color-mix(in srgb, var(--color-surface) 88%, transparent);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
    backdrop-filter: blur(20px) saturate(140%);
  }
}
.chat-msg-menu__item {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 9px 12px; border: 0; border-radius: var(--radius-md);
  background: transparent; color: var(--color-text); text-align: left;
  font-family: inherit; font-size: 14px; font-weight: 550; line-height: 1.3;
  cursor: pointer; text-decoration: none;
  transition: background-color .12s var(--ease), color .12s var(--ease);
}
/* Подсветка нейтральная, не синяя: пунктов в меню до шести подряд, и
   фирменный тон на каждом наведении превращает список в мигалку. */
.chat-msg-menu__item:hover,
.chat-msg-menu__item:focus-visible { background: var(--color-surface-soft); outline: none; }
.chat-msg-menu__item svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--color-muted); }
.chat-msg-menu__item:hover svg,
.chat-msg-menu__item:focus-visible svg { color: var(--color-text); }
.chat-msg-menu__label { flex: 1 1 auto; }
/* Стрелка «дальше» (пересылка спрашивает получателя) — мельче и бледнее
   основной иконки: это не действие, а знак второго шага. */
.chat-msg-menu__item .chat-msg-menu__more { width: 14px; height: 14px; color: var(--color-faint); }
.chat-msg-menu__item--danger { color: var(--color-danger); }
.chat-msg-menu__item--danger svg,
.chat-msg-menu__item--danger:hover svg,
.chat-msg-menu__item--danger:focus-visible svg { color: var(--color-danger); }
.chat-msg-menu__item--danger:hover,
.chat-msg-menu__item--danger:focus-visible { background: var(--color-danger-soft); color: var(--color-danger); }
.chat-msg-menu__sep { height: 1px; margin: 5px 10px; background: var(--color-border); }
/* Ряд реакций первой строкой меню — как в мессенджерах. Отдельной «пилюлей»
   над меню он не сделан намеренно: второй плавающий слой пришлось бы отдельно
   переворачивать у краёв экрана, а выигрыш — только косметический. */
.chat-msg-menu__reacts {
  display: flex; align-items: center; gap: 2px;
  padding: 2px 2px 6px; margin-bottom: 2px;
  border-bottom: 1px solid var(--color-border);
}
.chat-msg-menu__react {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; border: 0; border-radius: 50%;
  background: transparent; font-size: 19px; line-height: 1; cursor: pointer;
  transition: background-color .12s var(--ease), transform .16s var(--ease-out);
}
.chat-msg-menu__react:hover,
.chat-msg-menu__react:focus-visible {
  background: var(--color-surface-soft); outline: none; transform: scale(1.18);
}
/* Уже поставленная реакция обведена: повторный клик её снимет. */
.chat-msg-menu__react--mine {
  background: color-mix(in srgb, var(--color-primary) 20%, transparent);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--color-primary) 60%, transparent);
}
/* Пузырь с действиями намекает курсором на меню по правой кнопке; текст в нём
   выделяется мышью как обычно — левый клик меню больше не перехватывает. */
.chat-msg__bubble { cursor: default; }
.chat-msg--actionable .chat-msg__bubble { cursor: context-menu; }
.chat-msg__text { cursor: text; }
/* Кнопка «к последним сообщениям»: всплывает, когда ленту увели далеко вверх,
   и считает пришедшее, пока читали историю. Кнопку создаёт app.js РЯДОМ с
   лентой, а не внутри: поллинг переписывает ленту целиком и унёс бы её с
   собой. Отступы bottom/right тоже ставит JS — под лентой композер, высота
   которого гуляет вместе с текстом. */
.chat-feed-host { position: relative; }
.chat-jump {
  position: absolute; z-index: 6;
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; padding: 0;
  border: 1px solid var(--color-border); border-radius: var(--radius-pill);
  background: var(--color-surface); color: var(--color-muted);
  box-shadow: var(--shadow-pop); cursor: pointer;
  opacity: 0; transform: translateY(10px) scale(0.94);
  transition: opacity .18s var(--ease), transform .22s var(--ease-out),
              color .15s var(--ease), border-color .15s var(--ease);
}
.chat-jump[hidden] { display: none; }
.chat-jump.is-visible { opacity: 1; transform: none; }
.chat-jump:hover {
  color: var(--color-primary);
  border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-border));
}
.chat-jump:active { transform: translateY(1px) scale(0.97); }
.chat-jump svg { width: 19px; height: 19px; }
.chat-jump__count {
  position: absolute; top: -5px; right: -5px;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 19px; height: 19px; padding: 0 5px;
  border-radius: var(--radius-pill); background: var(--color-coral); color: #fff;
  font-size: 11px; font-weight: 800; line-height: 1;
  box-shadow: 0 0 0 2px var(--color-surface);
}
.chat-jump__count[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .chat-jump { transform: none; transition: opacity .12s linear; }
  .chat-jump:active { transform: none; }
}
/* Вложения-документы в пузыре: строка-ссылка «иконка + имя + размер», скачивание. */
.chat-msg__files { display: flex; flex-direction: column; gap: 6px; margin: 2px 0 6px; }
.chat-msg__file {
  display: flex; align-items: center; gap: 8px; max-width: 320px;
  padding: 8px 10px; border-radius: 10px; text-decoration: none;
  /* Заливка и кромка — полупрозрачные от цвета текста, как у чипа реакции и
     карточки ссылки, а не токены поверхности (26.08.2026): пузыри бывают двух
     цветов, и сплошной --color-surface-soft лежал на своём пузыре серым
     кирпичом. С разведёнными ступенями контраста это стало видно в упор. */
  border: 1px solid color-mix(in srgb, var(--color-text) 14%, transparent);
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
  color: var(--color-text);
  transition: border-color .15s var(--ease), background-color .15s var(--ease);
}
.chat-msg__file:hover {
  border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
  background: color-mix(in srgb, var(--color-primary) 16%, transparent);
}
.chat-msg__file svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--color-primary); }
.chat-msg__file-name {
  font-size: 13px; font-weight: 550; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-msg__file-size { font-size: 11.5px; color: var(--color-muted); flex: 0 0 auto; }
/* Галочки прочитанности (только на своих пузырях): одна серая — не прочитано,
   две синие + подпись «прочитано» — прочитано (как в Telegram). Тона — с
   полотна: подпись с 04.09.2026 лежит под пузырём, а не в нём. */
.chat-msg__status { display: inline-flex; align-items: center; gap: 4px; align-self: center; color: var(--chat-stamp); }
.chat-msg__status svg { width: 15px; height: 15px; flex: 0 0 auto; }
.chat-msg__status--read { color: var(--chat-stamp-read); }
.chat-msg__read-note { font-size: 11px; line-height: 1; white-space: nowrap; }
/* Вложения-изображения в пузыре. Одна картинка — крупно и в своих
   пропорциях; несколько — мозаика плиток, как в мессенджерах: две в ряд,
   при трёх первая во всю высоту слева, от пяти — по три в ряд. Скругление
   живёт на самом альбоме, у плиток углы прямые: мозаика читается одним
   блоком, а зазоры показывают цвет пузыря (у своего он синий, у чужого
   светлый). Клик открывает просмотрщик с масштабом (initImageViewer). */
.chat-msg__album {
  display: grid; gap: 3px; margin: 2px 0 6px;
  /* fit-content + АБСОЛЮТНЫЙ предел, а не `width: max-content` с
     `max-width: min(100%, 320px)` (04.09.2026, заказчик: «у картинки много
     пустого пространства»). Процент внутри max-width не участвует в расчёте
     собственной ширины элемента: наружу уезжала ширина растра, ужатого по
     max-height (у кадра 1280×720 это 533px), пузырь раздувался до своего
     предела в 560 и справа от картинки оставалась пустая заливка в треть
     пузыря. С fit-content наружу уходит уже обрезанное значение, и пузырь
     обнимает картинку; узкому окну fit-content отдаёт доступную ширину сам. */
  width: fit-content; max-width: 320px;
  border-radius: 14px; overflow: hidden;
}
.chat-msg__image { display: block; position: relative; cursor: zoom-in; min-width: 0; min-height: 0; }
.chat-msg__image img { display: block; }
.chat-msg__album > .chat-msg__image > img { max-width: 100%; max-height: 300px; width: auto; height: auto; }
/* Затемнение по наведению — слоем поверх, а не filter/transform на самом
   <img>: фильтр заставлял браузер перерисовывать растр на каждый ховер. */
.chat-msg__image::after {
  content: ""; position: absolute; inset: 0;
  background: rgba(8, 10, 14, 0); pointer-events: none;
  transition: background-color .16s var(--ease);
}
.chat-msg__image:hover::after { background: rgba(8, 10, 14, .2); }
/* Кольцо фокуса — ВНУТРЕННИМ контуром: у альбома overflow: hidden, и
   обычная тень-кольцо снаружи плитки была бы срезана. */
.chat-msg__image:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }

/* Две и больше — плитки. Картинку растягиваем абсолютом: высоту ряда задаёт
   сетка, а не сам растр, иначе один вертикальный кадр ломал бы всю мозаику. */
.chat-msg__album:has(> .chat-msg__image:nth-child(2)) {
  width: 272px; max-width: 100%;
  grid-template-columns: 1fr 1fr; grid-auto-rows: 128px;
}
.chat-msg__album:has(> .chat-msg__image:nth-child(2)) > .chat-msg__image { overflow: hidden; }
.chat-msg__album:has(> .chat-msg__image:nth-child(2)) > .chat-msg__image > img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  max-width: none; max-height: none; object-fit: cover;
}
/* Ровно три: слева высокая, справа две. */
.chat-msg__album:has(> .chat-msg__image:nth-child(3)):not(:has(> .chat-msg__image:nth-child(4))) {
  grid-template-columns: 1.6fr 1fr; grid-auto-rows: 85px;
}
.chat-msg__album:has(> .chat-msg__image:nth-child(3)):not(:has(> .chat-msg__image:nth-child(4))) > .chat-msg__image:first-child {
  grid-row: span 2;
}
/* Пять и больше — по три в ряд. Колонок шесть, а не три, ради «добора»
   неполного последнего ряда: одна оставшаяся плитка занимает шесть колонок,
   две — по три. Иначе под мозаикой зияла бы дыра на треть ширины.
   Больше десяти вложений сервер не примет (CHAT_IMAGE_MAX_COUNT). */
.chat-msg__album:has(> .chat-msg__image:nth-child(5)) {
  grid-template-columns: repeat(6, 1fr); grid-auto-rows: 88px;
}
.chat-msg__album:has(> .chat-msg__image:nth-child(5)) > .chat-msg__image { grid-column: span 2; }
.chat-msg__album:has(> .chat-msg__image:nth-child(5)) > .chat-msg__image:last-child:nth-child(3n + 1) { grid-column: span 6; }
.chat-msg__album:has(> .chat-msg__image:nth-child(5)) > .chat-msg__image:nth-last-child(2):nth-child(3n + 1),
.chat-msg__album:has(> .chat-msg__image:nth-child(5)) > .chat-msg__image:last-child:nth-child(3n + 2) { grid-column: span 3; }
.chat-empty { display: grid; place-items: center; min-height: 180px; }
/* Метка «только чтение» в списке чатов — рядом с бейджем роли. */
.chat-thread__archived { font-size: 11px; font-weight: 700; color: var(--color-muted); white-space: nowrap; }
/* Композер — колонка: превью вложения над строкой «поле + кнопки». */
.chat-form { display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
.chat-form__row { display: flex; gap: 10px; align-items: flex-end; }
/* Селектор с тегом — чтобы перебить textarea.input (min-height/resize выше по файлу).
   Высоту ставит JS-autogrow (app.js), transition делает рост/схлопывание плавным;
   MAX_INPUT_H в app.js синхронизирован с max-height. */
textarea.chat-form__input {
  flex: 1 1 auto; resize: none;
  min-height: 44px; max-height: 168px; overflow-y: hidden;
  font-family: inherit; line-height: 1.45; border-radius: 20px;
  transition: height .2s var(--ease), border-color .18s var(--ease),
              box-shadow .22s var(--ease), background-color .18s var(--ease);
}
.chat-form__tools { display: flex; gap: 10px; align-items: center; flex: 0 0 auto; }
/* Кнопка отправки. База — подпись словом, самолётик спрятан: так её видят
   старые браузеры без медиазапросов и печать. И телефон (mobile.css), и
   широкий экран (блок ниже) переворачивают это в круглую кнопку со значком —
   с 03.09.2026 композер везде «таблетка», и слово в неё не влезает. */
.chat-send { display: inline-flex; align-items: center; gap: 8px; }
/* Лупа в шапке списка чатов — элемент только телефона (см. mobile.css). */
.chat-search-toggle { display: none; }
.chat-send__icon { display: none; width: 18px; height: 18px; flex: 0 0 auto; }

/* Композер по макету 03.09.2026 — одна «таблетка»: поле без собственной
   кромки, значки и кнопка отправки лежат ВНУТРИ неё. Порог 820px, а не
   правка mobile.css: ниже него композер уже свой (круглая отправка, поле-
   пилюля, вырез под «домой»), и вкладывать одну пилюлю в другую незачем. */
@media (min-width: 820px) {
  .chat-form__row {
    gap: 6px; align-items: center;
    padding: 5px 6px 5px 12px; border-radius: 28px;
    border: 1px solid var(--color-border); background: var(--color-surface-alt);
    transition: border-color .18s var(--ease), box-shadow .22s var(--ease);
  }
  /* Подсветку фокуса держит строка целиком — иначе на экране две вложенные
     рамки: своя у поля и своя у таблетки. */
  .chat-form__row:focus-within { border-color: var(--color-primary); box-shadow: var(--ring-soft); }
  textarea.chat-form__input {
    min-height: 36px; padding: 8px 2px; border-radius: 0;
    border: 0; background: transparent;
  }
  textarea.chat-form__input:hover,
  textarea.chat-form__input:focus { border: 0; background: transparent; box-shadow: none; }
  /* Значки внутри таблетки — без кромок: рамка у каждого превращала строку
     ввода в ряд кнопок. Селектор от строки, а не от .chat-form__tools: смайлик
     с 03.09.2026 стоит СЛЕВА от поля и в tools больше не лежит. */
  .chat-form__row .btn-icon { border-color: transparent; background: transparent; }
  .chat-form__row .btn-icon:hover {
    border-color: transparent; background: var(--color-surface-soft); transform: none;
  }
  /* Кнопка отправки — круг со значком, как в макете. Подпись словом остаётся
     в разметке для скринридера (то же решение, что на телефоне). */
  .chat-send {
    width: 42px; height: 42px; min-height: 42px; padding: 0;
    border-radius: 50%; flex: 0 0 auto; justify-content: center;
  }
  .chat-send__icon { display: block; width: 18px; height: 18px; }
  .chat-send__label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}

/* Бейдж чата: [hidden] обязан прятать (класс .badge задаёт display). */
.chat-unread-badge[hidden] { display: none; }
/* Живые бейджи уведомлений (сайдбар и колокольчик): та же ловушка display. */
.notif-unread-badge[hidden] { display: none; }
.topbar__bell-count[hidden] { display: none; }
/* Бейдж «нужна замена» на «Графиках»: нет замен — не светим ноль. */
.repl-pending-badge[hidden] { display: none; }
/* Бейджи биржи: отклики у staff, свободные смены у сотрудника. Та же ловушка:
   .badge задаёт display, и без этой строки [hidden] бессилен. */
.xchg-claims-badge[hidden] { display: none; }
.xchg-free-badge[hidden] { display: none; }
/* «новое» на «Интеграциях»: расширение подтверждено — бейджа нет. Та же
   ловушка display: без этого правила [hidden] не перебивает .badge. */
.ext-update-badge[hidden] { display: none; }

/* Вложения в композере: скрепка + полоса мини-миниатюр (без имён) над полем. */
.chat-attach { color: var(--color-muted); flex: 0 0 auto; }
@keyframes chat-attach-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.chat-attach-preview {
  position: relative; /* якорь для полосы прогресса загрузки (::after) */
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 10px 12px; border-radius: var(--radius-lg);
  border: 1px solid var(--color-border); background: var(--color-surface-soft);
  animation: chat-attach-in .2s var(--ease) both;
  transition: border-color .2s var(--ease), background-color .2s var(--ease);
}
/* display на классе перебивает UA-стиль [hidden] — возвращаем его явно. */
.chat-attach-preview[hidden] { display: none; }
/* Вложения появляются лесенкой (задержка из --i) — как карты, легшие в стопку. */
@keyframes chat-attach-pop {
  from { opacity: 0; transform: translateY(9px) scale(.86); }
  to   { opacity: 1; transform: none; }
}
.chat-attach-thumb {
  position: relative;
  animation: chat-attach-pop .24s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 35ms);
}
.chat-attach-thumb img {
  display: block; width: 56px; height: 56px; object-fit: cover;
  border-radius: 10px; border: 1px solid var(--color-border);
}
.chat-attach-thumb__clear {
  position: absolute; top: -7px; right: -7px;
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border: 0; border-radius: 50%;
  background: var(--color-text); color: var(--color-surface);
  font-size: 13px; line-height: 1; cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background-color .15s var(--ease), transform .15s var(--ease);
}
.chat-attach-thumb__clear:hover { background: var(--color-danger); color: #fff; transform: scale(1.1); }
/* Документ в превью композера: чип «расширение + имя» с тем же крестиком. */
.chat-attach-doc {
  position: relative; display: flex; align-items: center; gap: 7px;
  max-width: 220px; height: 56px; padding: 0 12px 0 8px;
  border-radius: 10px; border: 1px solid var(--color-border);
  background: var(--color-surface);
  animation: chat-attach-pop .24s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 35ms);
}
.chat-attach-doc__ext {
  flex: 0 0 auto; padding: 3px 6px; border-radius: 6px;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-size: 10.5px; font-weight: 700; letter-spacing: .04em;
}
.chat-attach-doc__name {
  min-width: 0; font-size: 12.5px; font-weight: 550;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- Загрузка вложений: прогресс, блик и «улёт» в ленту -------------------
   Состояния ставит app.js: .is-uploading (идёт upload, процент в --upload-p и
   data-progress), .is-indeterminate (длина ещё неизвестна), .is-sent (сервер
   принял — миниатюры улетают), .is-failed (не ушло — короткая тряска).
   Лесенка задержек — из --i (порядковый номер вложения). */
.chat-attach-preview.is-uploading {
  border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
  background: color-mix(in srgb, var(--color-primary) 6%, var(--color-surface-soft));
}
/* Строка статуса над миниатюрами: «Отправка… 62%» (процент — из data-progress). */
.chat-attach-preview.is-uploading::before {
  content: "Отправка… " attr(data-progress);
  flex: 1 0 100%; margin-bottom: -2px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .01em;
  color: var(--color-primary-dark);
}
/* Успех словом не подписываем: миниатюры улетают вверх в ленту — это и есть
   сообщение о том, что файлы ушли. Строка статуса гаснет вместе с ними, но
   место за собой держит, иначе панель дёргается прямо во время «улёта».
   Раньше тут вспыхивала зелёная надпись «Отправлено» — лишний шум под полем
   ввода в момент, когда пользователь уже смотрит на новое сообщение. */
.chat-attach-preview.is-sent::before { content: ""; }
/* Полоса прогресса по низу панели: масштабируем, а не меняем width — плавнее. */
.chat-attach-preview.is-uploading::after {
  content: ""; position: absolute; left: 12px; right: 12px; bottom: 5px; height: 3px;
  border-radius: 2px; background: var(--gradient-brand);
  transform: scaleX(var(--upload-p, 0)); transform-origin: left center;
  transition: transform .18s linear;
}
.chat-attach-preview.is-indeterminate::after {
  right: auto; width: 26%; transform: none;
  animation: chat-upload-slide 1.05s var(--ease) infinite;
}
@keyframes chat-upload-slide {
  from { transform: translateX(0); }
  to   { transform: translateX(285%); }
}
/* Блик, пробегающий по каждой миниатюре, пока файлы уходят. */
.chat-attach-preview.is-uploading .chat-attach-thumb::after,
.chat-attach-preview.is-uploading .chat-attach-doc::after {
  content: ""; position: absolute; inset: 0; border-radius: 10px; pointer-events: none;
  background: linear-gradient(115deg, transparent 22%, rgba(255, 255, 255, .5) 50%, transparent 78%);
  background-size: 220% 100%;
  animation: chat-upload-sheen 1.15s var(--ease) infinite;
}
@keyframes chat-upload-sheen {
  from { background-position: 170% 0; }
  to   { background-position: -70% 0; }
}
/* Пока файл уходит, отменить его уже нельзя — крестик честно прячется. */
.chat-attach-preview.is-uploading .chat-attach-thumb__clear { opacity: 0; pointer-events: none; }
.chat-attach-preview.is-uploading .chat-attach-thumb img { filter: brightness(.94); }
/* Успех: миниатюры улетают вверх, в ленту сообщений. */
@keyframes chat-attach-fly {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-26px) scale(.68); }
}
.chat-attach-preview.is-sent .chat-attach-thumb,
.chat-attach-preview.is-sent .chat-attach-doc {
  animation: chat-attach-fly .3s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 45ms);
}
.chat-attach-preview.is-sent .chat-attach-thumb::after,
.chat-attach-preview.is-sent .chat-attach-doc::after { animation: none; opacity: 0; }
/* Не ушло: панель коротко вздрагивает, вложения остаются на месте. */
@keyframes chat-attach-shake {
  0%, 100% { transform: none; }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}
.chat-attach-preview.is-failed {
  animation: chat-attach-shake .32s var(--ease) both;
  border-color: color-mix(in srgb, var(--color-danger) 55%, var(--color-border));
}

@media (prefers-reduced-motion: reduce) {
  .chat-attach-thumb,
  .chat-attach-doc,
  .chat-attach-preview.is-indeterminate::after,
  .chat-attach-preview.is-uploading .chat-attach-thumb::after,
  .chat-attach-preview.is-uploading .chat-attach-doc::after,
  .chat-attach-preview.is-failed { animation: none; }
  .chat-attach-preview.is-uploading .chat-attach-thumb::after,
  .chat-attach-preview.is-uploading .chat-attach-doc::after { opacity: 0; }
  .chat-attach-preview.is-sent .chat-attach-thumb,
  .chat-attach-preview.is-sent .chat-attach-doc { animation: none; opacity: .35; }
  .avatar-card__ring.is-fresh { animation: none; }
}

/* Drag&drop вложения: при перетаскивании файла вся карточка чата подсвечивается.
   pointer-events: none — оверлей не должен перехватывать сам drop. */
.chat-card { position: relative; }
.chat-card.is-dragover::after {
  content: "Отпустите, чтобы прикрепить файл";
  position: absolute; inset: 6px; z-index: 50;
  display: flex; align-items: center; justify-content: center;
  border: 2px dashed var(--color-primary); border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-primary) 14%, transparent);
  backdrop-filter: blur(2px);
  color: var(--color-primary-dark); font-weight: 650; font-size: 14.5px;
  pointer-events: none;
  animation: chat-attach-in .15s var(--ease) both;
}

/* Эмодзи в композере чата. */
.chat-emoji { position: relative; flex: 0 0 auto; }
.chat-emoji__toggle { color: var(--color-muted); }
.chat-emoji__toggle[aria-expanded="true"] { color: var(--color-primary-dark); background: var(--color-primary-soft); }
.chat-emoji__pop {
  position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 60;
  display: grid; grid-template-columns: repeat(7, 34px); gap: 2px; padding: 8px;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-pop);
}
.chat-emoji__pop[hidden] { display: none; }
/* Смайлик переехал к левой кромке композера (03.09.2026) — поповер обязан
   раскрываться вправо, иначе он уезжает за край окна переписки. Селектор от
   строки: в других местах пикера нет, но правило именно про «кнопку слева». */
.chat-form__row > .chat-emoji .chat-emoji__pop { left: 0; right: auto; }
.chat-emoji__item {
  width: 34px; height: 34px; border: 0; border-radius: var(--radius-md);
  background: transparent; font-size: 19px; line-height: 1; cursor: pointer;
}
.chat-emoji__item:hover { background: var(--color-surface-soft); }

/* ==== Просмотрщик вложений (25.08.2026) ==================================
   Один оверлей на весь продукт: картинки чата, скриншоты инструкций и
   скриншоты обращений. Разметку создаёт app.js (imageViewer), [hidden]
   обязан прятать при display на классе. Прежний .chat-lightbox снят —
   он умел только показать кадр целиком, а мелкий текст на скриншоте и
   номер накладной на фото разглядеть было нечем.

   Слои. Сцена с картинкой лежит подложкой во весь экран, а панель, стрелки
   и лента миниатюр плавают НАД ней на градиентных подложках. Поэтому поля
   сцены симметричны: картинка стоит ровно в центре окна, и вся арифметика
   масштаба в app.js (границы сдвига, привязка точки под курсором) считается
   от центра окна без поправок на разную высоту панелей. Меняешь padding —
   оставляй его симметричным, иначе зум «поедет».

   z-index 140 — над доком чатов (110), модалками (120) и меню сообщения
   (130), под палитрой команд (150) и тостами (200). */
.imgview {
  position: fixed; inset: 0; z-index: 140;
  /* Свайп «утащить вниз, чтобы закрыть» гасит затемнение вслед за пальцем;
     значение по умолчанию обязано быть здесь — незаданная переменная делает
     невалидным всё объявление background. */
  --imgview-dim: 1;
  background: rgba(8, 10, 14, 0);
  transition: background-color .28s var(--ease), backdrop-filter .28s var(--ease);
  overscroll-behavior: contain;
}
.imgview[hidden] { display: none; }
.imgview.is-open {
  background: rgba(8, 10, 14, calc(.88 * var(--imgview-dim, 1)));
  backdrop-filter: blur(14px) saturate(1.15);
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
}
/* Страница под оверлеем не должна прокручиваться колесом: колесо здесь — зум. */
body.is-imgview-open { overflow: hidden; }

/* Поля сцены: по бокам — воздух, сверху и снизу — место под панель, полосу
   масштаба и ленту миниатюр, чтобы кадр под ними НЕ прятался. Вертикальные
   поля обязаны оставаться равными друг другу (см. про арифметику выше),
   поэтому снизу их задаёт самая высокая стопка (полоса над лентой), а сверху
   ровно столько же — хотя одной панели хватило бы меньшего. */
.imgview__stage {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 64px clamp(14px, calc(3.4vw * var(--vu)), 34px);
  overflow: hidden;
  /* Жесты забирает app.js целиком: без этого браузер сам начнёт скроллить
     и пинчить страницу, и пальцем масштаб не взять. */
  touch-action: none;
  opacity: 0; transform: scale(.94);
  transition: opacity .24s var(--ease), transform .3s var(--ease-out);
}
.imgview.is-open .imgview__stage { opacity: 1; transform: none; }
.imgview--album .imgview__stage { padding-block: 124px; }

.imgview__img {
  display: block; max-width: 100%; max-height: 100%; width: auto; height: auto;
  border-radius: 8px; box-shadow: 0 30px 90px rgba(0, 0, 0, .55);
  /* Центр — точка отсчёта всей арифметики зума в app.js. */
  transform-origin: 50% 50%;
  transition: transform .26s var(--ease-out);
  will-change: transform;
  -webkit-user-select: none; user-select: none;
}
/* Проявление кадра при листании альбома. Только opacity: transform занят
   масштабом и сдвигом (анимация перебила бы inline-стиль). */
.imgview__img--in { animation: imgview-fade .26s var(--ease) both; }
@keyframes imgview-fade { from { opacity: 0; } to { opacity: 1; } }

.imgview:not(.is-zoomed) .imgview__img { cursor: zoom-in; }
.imgview.is-zoomed .imgview__stage { cursor: grab; }
.imgview.is-zoomed .imgview__stage:active { cursor: grabbing; }

/* Крутилка на время загрузки полного кадра: в чате превью и оригинал — один
   и тот же файл, но у скриншотов инструкций он тяжелее миниатюры. */
.imgview__spin {
  position: absolute; left: 50%; top: 50%; width: 34px; height: 34px;
  margin: -17px 0 0 -17px; border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, .22); border-top-color: #fff;
  opacity: 0; pointer-events: none; transition: opacity .2s var(--ease);
}
.imgview.is-loading .imgview__spin { opacity: 1; animation: imgview-spin .8s linear infinite; }
@keyframes imgview-spin { to { transform: rotate(360deg); } }

/* Верхняя панель. Сама она не ловит указатель (иначе широкая градиентная
   полоса мешала бы возить картинку у верхнего края) — его ловят только дети. */
.imgview__top {
  position: absolute; top: 0; left: 0; right: 0;
  display: flex; align-items: flex-start; justify-content: space-between; gap: 14px;
  padding: 13px 15px 34px; color: #fff; pointer-events: none;
  background: linear-gradient(180deg, rgba(6, 8, 12, .6), rgba(6, 8, 12, 0));
  opacity: 0; transform: translateY(-10px);
  transition: opacity .3s var(--ease) .06s, transform .34s var(--ease-out) .06s;
}
.imgview.is-open .imgview__top { opacity: 1; transform: none; }
.imgview__top > * { pointer-events: auto; }

.imgview__title {
  display: flex; flex-direction: column; gap: 2px; min-width: 0; padding: 7px 4px 0 8px;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .55);
}
.imgview__count { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
.imgview__cap {
  font-size: 12px; color: rgba(255, 255, 255, .74);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.imgview__count[hidden], .imgview__cap[hidden] { display: none; }

/* Стеклянная пилюля с инструментами. border на кнопках задан намеренно:
   без него браузер рисует <button> собственную рамку outset. */
.imgview__tools {
  display: flex; align-items: center; gap: 2px; flex: 0 0 auto;
  padding: 4px; border-radius: 999px;
  background: rgba(255, 255, 255, .12); border: 1px solid rgba(255, 255, 255, .14);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.imgview__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  border: 1px solid transparent; border-radius: 50%;
  background: transparent; color: #fff; cursor: pointer; text-decoration: none;
  transition: background-color .15s var(--ease), transform .15s var(--ease), opacity .15s var(--ease);
}
.imgview__btn svg { width: 18px; height: 18px; }
/* Заказанные действия (Изменить / Удалить) стоят в той же пилюле, что и
   «оригинал» с крестиком, но отделены от них тонкой чертой. */
.imgview__extras { display: flex; align-items: center; gap: 2px; }
.imgview__extras:not(:empty) { padding-right: 4px; margin-right: 2px; border-right: 1px solid rgba(255, 255, 255, .18); }
.imgview__btn--danger:hover { color: var(--color-danger); }
.imgview__btn[hidden] { display: none; }
.imgview__btn:hover { background: rgba(255, 255, 255, .18); }
.imgview__btn:active { transform: scale(.94); }
.imgview__btn:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255, 255, 255, .8); }
.imgview__btn[disabled] { opacity: .32; cursor: default; }
.imgview__btn[disabled]:hover { background: transparent; }
.imgview__btn--close:hover { background: rgba(255, 255, 255, .26); }
.imgview__zoom {
  min-width: 58px; height: 34px; padding: 0 8px;
  border: 1px solid transparent; border-radius: 999px;
  background: transparent; color: #fff; cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
  transition: background-color .15s var(--ease);
}
.imgview__zoom:hover { background: rgba(255, 255, 255, .18); }
.imgview__zoom:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255, 255, 255, .8); }
.imgview__sep { width: 1px; height: 20px; margin: 0 4px; background: rgba(255, 255, 255, .22); }

/* Полоса масштаба под кадром: «−», ползунок, «+», процент и поворот. Стоит
   по центру — до неё одинаково близко с любого края кадра, и она не спорит с
   панелью сверху, где остались только оригинал и крестик. В альбоме полоса
   поднимается над лентой миниатюр (поля сцены под это уже разведены). */
.imgview__zoombar {
  position: absolute; left: 50%; bottom: 14px; z-index: 1;
  display: flex; align-items: center; gap: 2px;
  padding: 4px; border-radius: 999px; color: #fff;
  /* Стекло тёмное, а не белое как у панели сверху: та лежит на градиентной
     подложке, а полоса висит прямо над кадром — на светлом снимке белое
     полупрозрачное стекло сливалось бы с картинкой вместе с иконками. */
  background: rgba(12, 15, 20, .66); border: 1px solid rgba(255, 255, 255, .16);
  backdrop-filter: blur(10px) saturate(1.1); -webkit-backdrop-filter: blur(10px) saturate(1.1);
  box-shadow: 0 8px 26px rgba(0, 0, 0, .45);
  opacity: 0; transform: translate(-50%, 12px);
  transition: opacity .3s var(--ease) .06s, transform .34s var(--ease-out) .06s;
}
.imgview.is-open .imgview__zoombar { opacity: 1; transform: translate(-50%, 0); }
.imgview--album .imgview__zoombar { bottom: 78px; }

/* Ползунок рисуем сами: родной вид не читается на тёмном стекле. Дорожка и
   бегунок задаются отдельно для WebKit и Gecko — общего селектора нет, а
   объединять их в один список нельзя: непонятый префикс убивает всё правило. */
.imgview__range {
  -webkit-appearance: none; appearance: none;
  flex: 0 1 auto; width: 168px; min-width: 72px; height: 20px; margin: 0 6px;
  background: transparent; cursor: pointer;
}
.imgview__range::-webkit-slider-runnable-track {
  height: 4px; border-radius: 999px; background: rgba(255, 255, 255, .3);
}
.imgview__range::-moz-range-track {
  height: 4px; border-radius: 999px; background: rgba(255, 255, 255, .3);
}
.imgview__range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 14px; height: 14px; margin-top: -5px;
  border: none; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .5);
  transition: transform .14s var(--ease), box-shadow .14s var(--ease);
}
.imgview__range::-moz-range-thumb {
  width: 14px; height: 14px;
  border: none; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .5);
}
.imgview__range:hover::-webkit-slider-thumb { transform: scale(1.16); }
.imgview__range:hover::-moz-range-thumb { transform: scale(1.16); }
.imgview__range:focus-visible { outline: none; }
.imgview__range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px rgba(255, 255, 255, .6); }
.imgview__range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px rgba(255, 255, 255, .6); }

/* Стрелки альбома. На краю ленты кнопка не гаснет, а исчезает: «мёртвая»
   кнопка под курсором врёт сильнее, чем её отсутствие. */
.imgview__nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; padding: 0;
  border: 1px solid rgba(255, 255, 255, .14); border-radius: 50%;
  background: rgba(255, 255, 255, .12); color: #fff; cursor: pointer;
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity .25s var(--ease), background-color .15s var(--ease), transform .18s var(--ease);
}
.imgview__nav[hidden] { display: none; }
.imgview__nav svg { width: 22px; height: 22px; }
.imgview__nav--prev { left: 16px; }
.imgview__nav--next { right: 16px; }
.imgview.is-open .imgview__nav { opacity: 1; }
.imgview.is-open .imgview__nav[disabled] { opacity: 0; pointer-events: none; }
.imgview__nav:hover { background: rgba(255, 255, 255, .22); }
.imgview__nav:active { transform: translateY(-50%) scale(.94); }
.imgview__nav:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255, 255, 255, .8); }

/* Лента миниатюр альбома — как плёнка внизу. */
.imgview__thumbs {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 34px 16px 15px; overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none;
  background: linear-gradient(0deg, rgba(6, 8, 12, .6), rgba(6, 8, 12, 0));
  opacity: 0; transform: translateY(12px);
  transition: opacity .3s var(--ease) .06s, transform .34s var(--ease-out) .06s;
}
.imgview__thumbs::-webkit-scrollbar { display: none; }
.imgview__thumbs[hidden] { display: none; }
.imgview.is-open .imgview__thumbs { opacity: 1; transform: none; }
.imgview__thumb {
  flex: 0 0 auto; width: 54px; height: 54px; padding: 0;
  border: 1px solid transparent; border-radius: 10px; overflow: hidden;
  background: rgba(255, 255, 255, .1); cursor: pointer; opacity: .55;
  transition: opacity .16s var(--ease), transform .16s var(--ease), border-color .16s var(--ease);
}
.imgview__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.imgview__thumb:hover { opacity: .85; }
.imgview__thumb.is-active { opacity: 1; border-color: #fff; transform: translateY(-2px); }
.imgview__thumb:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255, 255, 255, .8); }

@media (max-width: 819px) {
  /* Телефон: панель ближе к краю, подпись снята (места мало), стрелок нет —
     листают свайпом, закрывают свайпом вниз (см. app.js). */
  .imgview__stage { padding: 56px 8px; }
  .imgview--album .imgview__stage { padding-block: 108px; }
  .imgview__top { padding: 10px 10px 26px; }
  .imgview__zoombar {
    bottom: 10px; padding: 3px; gap: 0;
    max-width: calc(100vw - 16px);
  }
  .imgview--album .imgview__zoombar { bottom: 64px; }
  .imgview__zoombar .imgview__btn { width: 32px; height: 32px; }
  .imgview__zoombar .imgview__zoom { min-width: 50px; height: 32px; padding: 0 6px; }
  .imgview__sep { margin: 0 2px; }
  .imgview__range { width: 96px; margin: 0 4px; }
  .imgview__title { padding: 8px 2px 0 6px; }
  .imgview__cap { display: none; }
  .imgview__nav { display: none; }
  .imgview__thumbs { justify-content: flex-start; gap: 6px; padding: 26px 10px 11px; }
  .imgview__thumb { width: 46px; height: 46px; }
}

@media (prefers-reduced-motion: reduce) {
  .imgview, .imgview__stage, .imgview__top, .imgview__thumbs, .imgview__zoombar,
  .imgview__img, .imgview__nav, .imgview__thumb { transition-duration: .01ms; }
  .imgview__img--in { animation: none; }
  .imgview.is-loading .imgview__spin { animation-duration: 2.4s; }
}

/* Опрос в пузыре группового чата (заказчик, 28.08.2026). Варианты — кнопки
   формы: голос уходит htmx-ом, блок перерисовывает сам себя. Заливка доли —
   отдельный слой под содержимым строки, поэтому текст по ней не «плывёт». */
.chat-poll { display: flex; flex-direction: column; gap: 6px; margin: 2px 0 4px; }
.chat-poll__question { font-weight: 700; line-height: 1.35; }
.chat-poll__note { font-size: 11.5px; color: var(--color-muted); }
.chat-poll__opts { display: flex; flex-direction: column; gap: 4px; margin-top: 2px; }
/* Карточка варианта: рамка, заливка доли и подсветка — на обёртке, потому что
   строка «кто ответил» с кнопкой «ещё N» лежит РЯДОМ с кнопкой голосования
   (вложенная кнопка — невалидная разметка). */
.chat-poll__item {
  position: relative; isolation: isolate; overflow: hidden;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-surface);
  transition: border-color .12s var(--ease);
}
.chat-poll__item.is-live:hover { border-color: var(--color-primary); }
.chat-poll__item.is-mine { border-color: var(--color-primary); }
.chat-poll__opt {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: 8px; width: 100%; padding: 7px 10px; text-align: left;
  border: 0; border-radius: inherit; background: transparent; color: inherit;
  cursor: pointer; font: inherit; font-size: 13px;
}
.chat-poll__opt:disabled { cursor: default; }
/* Заливка доли: под текстом (z-index -1 внутри своего слоя из-за isolation). */
.chat-poll__bar {
  position: absolute; inset: 0 auto 0 0; z-index: -1;
  background: var(--color-primary-soft);
  transition: width .25s var(--ease-out);
}
.chat-poll__mark {
  display: grid; place-items: center; width: 17px; height: 17px; flex: 0 0 auto;
  border: 1.5px solid var(--color-border); border-radius: 50%; color: transparent;
}
.chat-poll__mark--box { border-radius: 5px; }
.chat-poll__mark svg { width: 11px; height: 11px; }
/* Отметка «мой ответ» висит на КАРТОЧКЕ варианта (.chat-poll__item), а не на
   кнопке: с 28.08.2026 строка «кто ответил» лежит рядом с кнопкой, и класс
   переехал на общую обёртку. */
.chat-poll__item.is-mine .chat-poll__mark {
  background: var(--color-primary); border-color: var(--color-primary);
  color: var(--color-on-primary);
}
/* Свой пузырь перекрашивает --color-primary в светлый тон (см. .chat-msg--own),
   и белая галочка на нём в тёмной теме почти не видна. Берём цвет самого
   пузыря: он контрастен акценту в обеих темах (светлый на тёмном и наоборот). */
.chat-msg--own .chat-poll__item.is-mine .chat-poll__mark { color: var(--chat-bubble-own); }
/* Вариант опроса — единственное место внутри пузыря со СПЛОШНОЙ заливкой
   (--color-surface), и на своём синем пузыре (03.09.2026) он превращался в
   белую плашку с белым же текстом. Возвращаем общее правило переписки:
   заливка считается от --color-text, то есть от белого на своём пузыре и от
   тёмного на чужом, — одно правило годится в обеих темах. */
.chat-msg--own .chat-poll__item {
  background: color-mix(in srgb, var(--color-text) 16%, transparent);
  border-color: color-mix(in srgb, var(--color-text) 34%, transparent);
}
.chat-msg--own .chat-poll__bar { background: color-mix(in srgb, var(--color-text) 24%, transparent); }
.chat-msg--own .chat-poll__item.is-live:hover,
.chat-msg--own .chat-poll__item.is-mine { border-color: var(--color-primary); }
.chat-poll__label { min-width: 0; word-break: break-word; }
.chat-poll__share { font-size: 12px; font-weight: 700; white-space: nowrap; }
.chat-poll__count { color: var(--color-muted); font-weight: 600; }
/* Кто ответил — отдельной строкой под вариантом (только у неанонимных):
   кружок с фото и имя, по человеку на «таблетку». Заказчик (28.08.2026)
   попросил именно аватарки с именами и без кнопки «ещё N» — поэтому строка
   переносится, а не обрезается. */
.chat-poll__voters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
  /* Отступ слева равен «кружок отметки + зазор»: имена встают ровно под текстом
     варианта, а не под его галочкой. */
  padding: 0 10px 7px 35px;
}
.chat-poll__voter {
  display: inline-flex; align-items: center; gap: 5px; min-width: 0;
  font-size: 11.5px; line-height: 1.2; color: var(--color-muted);
}
/* Кружок мельче лентового: строка стоит внутри варианта и не должна его растить.
   Селектор с родителем — не для красоты: базовый .chat-monitor__avatar (38px)
   стоит НИЖЕ по файлу, и при равной специфичности выигрывал бы он. */
.chat-poll__voter .chat-poll__ava {
  width: 18px; height: 18px; font-size: 9.5px; font-weight: 700;
}
/* В своём пузыре --color-primary перекрашен в светлый тон, и белый инициал на
   нём не читается (та же история, что с галочкой варианта) — берём цвет
   самого пузыря. Фото это не трогает: у него кружок перекрыт картинкой. */
.chat-msg--own .chat-poll__voter .chat-poll__ava { color: var(--chat-bubble-own); }
.chat-poll__voter-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-poll__voter[hidden] { display: none; }
/* «ещё N» / «свернуть» — не кнопка-таблетка, а подпись со ссылочным поведением:
   в ряду имён кнопка выглядела бы тяжелее самих имён. */
.chat-poll__more {
  border: 0; background: none; padding: 0; font: inherit; font-size: 11.5px;
  font-weight: 700; color: var(--color-primary); cursor: pointer;
}
.chat-poll__more:hover { text-decoration: underline; }
.chat-poll__foot {
  display: flex; align-items: center; gap: 8px;
  font-size: 11.5px; color: var(--color-muted);
}
/* В подвале опроса осталась одна строка — «Ответили: N»: кнопку «Завершить»
   заказчик снял 26.08.2026 как ненужную. */
.chat-poll__total { margin-right: auto; }
.chat-poll--closed .chat-poll__question { opacity: .85; }

/* Модалка «Новый опрос» — рядом с композером группы (см. poll_modal.html). */
.chat-pollnew {
  position: fixed; inset: 0; z-index: 128;
  display: grid; place-items: center; padding: 24px;
  background: rgba(7, 17, 31, 0);
  transition: background-color .16s var(--ease);
}
.chat-pollnew[hidden] { display: none; }
.chat-pollnew.is-open { background: rgba(7, 17, 31, 0.62); }
.chat-pollnew__panel {
  display: flex; flex-direction: column; gap: 12px;
  width: min(460px, 100%); max-height: min(620px, calc(88vh * var(--vu)));
  overflow-y: auto; padding: 16px;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-pop);
  opacity: 0; transform: translateY(12px) scale(0.98);
  transition: opacity .16s var(--ease), transform .18s var(--ease-out);
}
.chat-pollnew.is-open .chat-pollnew__panel { opacity: 1; transform: none; }
.chat-pollnew__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.chat-pollnew__title { font-size: 15px; font-weight: 800; }
.chat-pollnew__close { width: 28px; height: 28px; color: var(--color-muted); }
.chat-pollnew__close svg { width: 16px; height: 16px; }
.chat-pollnew__form { display: flex; flex-direction: column; gap: 12px; }
/* Вопрос — две строки, а не общая для textarea высота 118px: в модалке она
   съедала пол-экрана телефона, а вопрос опроса почти всегда одна строка. */
.chat-pollnew__q { width: 100%; min-height: 64px; resize: vertical; }
.chat-pollnew__opts { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.chat-pollnew__row { display: flex; align-items: center; gap: 6px; }
.chat-pollnew__row .input { flex: 1 1 auto; min-width: 0; }
.chat-pollnew__drop { width: 28px; height: 28px; flex: 0 0 auto; color: var(--color-muted); }
.chat-pollnew__drop[hidden] { display: none; }
.chat-pollnew__drop svg { width: 14px; height: 14px; }
.chat-pollnew__drop:hover { color: var(--color-danger); }
.chat-pollnew__add { align-self: flex-start; margin-top: 6px; }
.chat-pollnew__add[hidden] { display: none; }
.chat-pollnew__foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 2px; }

/* Модалка «Переслать сообщение»: overlay + панель с поиском и списком
   контактов (контент отдаёт сервер, открывает/фильтрует app.js). */
.chat-fwd {
  position: fixed; inset: 0; z-index: 120;
  display: grid; place-items: center; padding: 24px;
  background: rgba(7, 17, 31, 0);
  transition: background-color .18s var(--ease);
}
.chat-fwd[hidden] { display: none; }
.chat-fwd.is-open { background: rgba(7, 17, 31, 0.62); }
.chat-fwd__panel {
  display: flex; flex-direction: column; gap: 12px;
  width: min(440px, 100%); max-height: min(560px, calc(86vh * var(--vu)));
  padding: 16px; background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop);
  opacity: 0; transform: translateY(12px) scale(0.98);
  transition: opacity .18s var(--ease), transform .2s var(--ease-out);
}
.chat-fwd.is-open .chat-fwd__panel { opacity: 1; transform: none; }
.chat-fwd__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.chat-fwd__title { font-size: 15px; font-weight: 800; }
.chat-fwd__close { width: 28px; height: 28px; color: var(--color-muted); }
.chat-fwd__close svg { width: 16px; height: 16px; }
.chat-fwd__search { position: relative; display: block; }
.chat-fwd__search svg {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--color-faint); pointer-events: none;
}
.chat-fwd__search .input { width: 100%; padding-left: 34px; }
.chat-fwd__list {
  flex: 1 1 auto; min-height: 120px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
}
.chat-fwd__group { display: flex; flex-direction: column; gap: 2px; }
.chat-fwd__group[hidden] { display: none; }
.chat-fwd__group-title {
  padding: 8px 10px 4px; font-size: 11.5px; font-weight: 750;
  letter-spacing: .06em; text-transform: uppercase; color: var(--color-muted);
}
.chat-fwd__item[hidden] { display: none; }
.chat-fwd__contact {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: var(--radius-sm);
  background: transparent; text-align: left; cursor: pointer;
  transition: background-color .12s var(--ease);
}
.chat-fwd__contact:hover { background: var(--color-primary-soft); }
.chat-fwd__avatar { flex: 0 0 auto; }
.chat-fwd__contact-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.chat-fwd__contact-main strong {
  font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-fwd__none { padding: 10px; }
/* Кружок-галочка справа: чаты в пикере отмечаются пачкой, как сообщения в
   ленте (заказчик, 28.08.2026). margin-left: auto прижимает его к краю строки —
   имя и подпись остаются слева и по-прежнему обрезаются многоточием. */
.chat-fwd__check {
  flex: 0 0 auto; margin-left: auto; display: grid; place-items: center;
  width: 20px; height: 20px; border-radius: 50%;
  border: 1.5px solid var(--color-border); background: transparent;
  color: transparent; transition: background-color .12s var(--ease), border-color .12s var(--ease);
}
.chat-fwd__check svg { width: 12px; height: 12px; }
.chat-fwd__contact.is-picked { background: var(--color-primary-soft); }
.chat-fwd__contact.is-picked .chat-fwd__check {
  background: var(--color-primary); border-color: var(--color-primary);
  color: var(--color-on-primary);
}
/* Подвал модалки: счётчик отмеченных чатов и кнопки. Появляется вместе с
   первой галочкой — пустой полосы под списком в обычном состоянии нет. */
.chat-fwd__foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding-top: 10px; border-top: 1px solid var(--color-border);
}
.chat-fwd__foot[hidden] { display: none; }
.chat-fwd__picked { margin-right: auto; font-size: 12.5px; font-weight: 700; }
.chat-fwd__foot .btn[hidden] { display: none; }
/* Почему в пикере нет группы «Сотрудники ПВЗ» (сообщение собственника). */
.chat-fwd__note {
  margin: 6px 2px 0; padding: 9px 11px; font-size: 12.5px; line-height: 1.45;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-surface-soft);
}

/* Модалка непрочитанных уведомлений после входа. z-index 130 — выше
   camera/guide-modal (120): пока не подтверждено прочтение, перекрывает всё. */
.login-notes-modal { position: fixed; inset: 0; z-index: 130; display: grid; place-items: center; padding: 24px; }
.login-notes-modal__backdrop { position: absolute; inset: 0; background: rgba(7,17,31,.78); backdrop-filter: blur(9px); }
.login-notes-modal__dialog {
  position: relative; display: flex; flex-direction: column;
  width: min(640px, 100%); max-height: calc(86vh * var(--vu));
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); overflow: hidden;
}
.login-notes-modal__head { padding: 18px 20px 12px; border-bottom: 1px solid var(--color-border); }
.login-notes-modal__title { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 850; }
.login-notes-modal__note { margin-top: 4px; color: var(--color-muted); font-size: 12.5px; line-height: 1.5; }
.login-notes-modal__body { flex: 1 1 auto; overflow-y: auto; padding: 12px 20px; display: flex; flex-direction: column; gap: 10px; }
.login-note {
  position: relative;
  display: flex; gap: 10px; align-items: flex-start;
  padding: 10px 12px; border: 1px solid var(--color-border);
  border-radius: var(--radius-sm); background: var(--color-surface-soft);
  transition: background-color .16s var(--ease), border-color .16s var(--ease);
}
.login-note .notif-drop__sev { margin-top: 6px; }
.login-note__content { min-width: 0; flex: 1 1 auto; }
/* Переход к источнику уведомления — клик по всей карточке. Кликом занимается
   прозрачная кнопка, растянутая поверх неё (почему именно так — в шапке
   login_notifications_modal.html). Кольцо фокуса рисует общее правило
   button:focus-visible, поэтому радиус наследуем от карточки. */
.login-note__go {
  position: absolute; inset: 0; z-index: 1; cursor: pointer;
  padding: 0; border: 0; background: transparent; border-radius: inherit;
}
/* Пока список не пролистан, карточка не кликается — и не притворяется. */
.login-note__go:disabled { cursor: default; }
.login-notes-modal--ready .login-note:hover {
  background: color-mix(in srgb, var(--color-primary) 7%, var(--color-surface-soft));
  border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-border));
}
.login-note__chevron {
  flex: 0 0 auto; align-self: center; margin-left: auto; color: var(--color-muted);
  transition: transform .16s var(--ease), color .16s var(--ease);
}
.login-note__chevron svg { width: 16px; height: 16px; display: block; }
.login-notes-modal:not(.login-notes-modal--ready) .login-note__chevron { opacity: .35; }
.login-notes-modal--ready .login-note:hover .login-note__chevron {
  color: var(--color-primary-dark); transform: translateX(2px);
}
.login-note__meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; color: var(--color-muted); font-size: 12px; }
.login-note__priority { color: var(--color-danger); font-weight: 700; }
.login-note__title { display: block; margin-top: 2px; font-size: 14px; }
/* «Откуда пришло» между заголовком и текстом: тише заголовка, но это
   единственное, чем одна карточка отличается от девяти таких же. */
.login-note__source { display: block; margin-top: 2px; font-size: 12px; color: var(--color-muted); overflow-wrap: anywhere; }
.login-note__text { margin-top: 2px; font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
.login-notes-modal__end { padding: 10px 0 4px; text-align: center; color: var(--color-muted); font-size: 12.5px; }
.login-notes-modal__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 20px; border-top: 1px solid var(--color-border);
}
.login-notes-modal__hint { color: var(--color-muted); font-size: 12.5px; }
.login-notes-modal--ready .login-notes-modal__hint { color: var(--color-success); font-weight: 650; }
.login-notes-open { overflow: hidden; }
@media (max-width: 720px) {
  .login-notes-modal { padding: 10px; }
  .login-notes-modal__dialog { max-height: calc(94vh * var(--vu)); }
}

/* Переключатель веток треда («Управляющий» / «Администрация» в кабинете,
   «С управляющими» / «Личный канал» у собственника). Тот же сегмент-контрол,
   что у .seg-toggle в расчётах, но на ссылках: ветка меняет и ленту, и адрес
   отправки, поэтому это переход, а не радиокнопка. Живёт ВНУТРИ .chat-card
   первой строкой — отдельной полосой над карточкой смотрелся пришитым сбоку. */
.chat-switch {
  display: inline-flex; gap: 4px; padding: 4px; align-self: flex-start;
  max-width: 100%; width: fit-content;
  border: 1px solid var(--color-border); border-radius: var(--radius-pill);
  background: var(--color-surface-soft);
}
.chat-switch__item {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 16px; border-radius: var(--radius-pill);
  font-size: 13.5px; font-weight: 650; color: var(--color-muted);
  text-decoration: none; white-space: nowrap;
  transition: background .15s var(--ease), color .15s var(--ease);
}
a.chat-switch__item:hover { background: var(--color-surface); color: var(--color-text); }
.chat-switch__item.is-active {
  background: var(--color-primary); color: var(--color-on-primary);
  box-shadow: var(--shadow-sm);
}
/* Счётчик непрочитанного на закрытой ветке: без него ответ в соседней вкладке
   не заметен — её просто не открывают. */
.chat-switch__count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--radius-pill); background: var(--color-danger);
  color: #fff; font-size: 11px; font-weight: 800; line-height: 1;
}

/* Вторая строка шапки кабинетного чата: точка сотрудника и адресаты одной
   приглушённой строкой. Это модификатор к .page__subtitle, а НЕ отдельный
   этаж: отдельным блоком «Кому вы пишете» вставало в общую строку с
   заголовком (жалоба заказчика, 03.09.2026). Цвет, кегль, nowrap и многоточие
   строка наследует от .page__subtitle выше. */
.chat-recipients__sep { padding: 0 4px; color: var(--color-faint); }
/* Подпись тише точки и адресата: она служебная, читается «по дороге». */
.chat-recipients__label { color: var(--color-faint); font-weight: 600; }
.chat-recipients__person {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--color-text); font-weight: 650;
}
/* Бейдж роли в подписи тише штатного: рядом с 12.5px текстом его обычные
   12px/800 читаются как второй заголовок и спорят с именем чата. */
.page__head--compact .chat-recipients .role-badge {
  padding: 2px 7px; font-size: 10.5px; font-weight: 750;
}

/* В фильтрах списка чатов осталось одно поле поиска — держать вокруг него
   полную «панель фильтров» незачем (заказчик, 20.08.2026: меньше пустоты). */
.page--chat-list .filters-bar { padding: 8px 10px; }
/* Поиск занимает строку целиком: в сетке auto-fit рядом с ним оказывается
   пустой <noscript>, и поле съёживалось до половины ширины списка. */
.page--chat-list .filters-bar__search { grid-column: 1 / -1; }

/* Список контактов чата (staff) — как в мессенджере: аватар, имя, превью
   последнего сообщения; справа время и счётчик. Строка = ссылка на чат +
   (для admin) кнопка тихого просмотра. */
.chat-thread-list { display: flex; flex-direction: column; }
/* Секции списка чатов: «Команда» (личные диалоги staff) и «Сотрудники ПВЗ». */
.chat-list-group { display: flex; flex-direction: column; }
.chat-list-group + .chat-list-group { margin-top: 12px; }
.chat-list-group__title {
  padding: 2px 10px 6px; font-size: 11.5px; font-weight: 750;
  letter-spacing: .06em; text-transform: uppercase; color: var(--color-muted);
}
.chat-thread {
  display: flex; align-items: stretch; gap: 4px;
  border-bottom: 1px solid var(--color-border); border-radius: var(--radius-sm);
  transition: background .15s var(--ease);
}
.chat-thread:last-child { border-bottom: 0; }
.chat-thread:hover { background: var(--color-surface-soft); }
.chat-thread__link {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 12px;
  padding: 9px 10px; color: inherit; text-decoration: none;
}
.chat-thread__avatar { flex: 0 0 auto; }
.chat-thread__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.chat-thread__name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Превью последнего сообщения: одна строка с многоточием, имя автора — темнее. */
.chat-thread__preview {
  font-size: 13px; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-thread__preview strong { color: var(--color-text); font-weight: 600; margin-right: 4px; }
.chat-thread__preview--empty { color: var(--color-faint); font-style: italic; }
.chat-thread--unread .chat-thread__preview { color: var(--color-text); font-weight: 600; }
/* Автор последнего сообщения в группе — акцентом, как в мессенджерах: в общем
   чате первый вопрос «кто написал», а в непрочитанной строке жирный текст
   уравнивался с именем и съедал ответ (заказчик, 20.08.2026). */
.chat-thread__preview--group .chat-thread__author,
.chat-thread--unread .chat-thread__preview--group .chat-thread__author {
  color: var(--color-primary-dark); font-weight: 750;
}
/* Чип «нет учётной записи»: контакт есть, но кабинета у сотрудника ещё нет. */
.chat-thread__no-account {
  padding: 2px 8px; border-radius: 999px;
  border: 1px dashed var(--color-border);
  color: var(--color-muted); font-size: 11px; font-weight: 650; white-space: nowrap;
}
.chat-thread__time--short { display: none; }
.chat-thread__aside {
  display: flex; flex-direction: column; align-items: flex-end; justify-content: center;
  gap: 4px; flex: 0 0 auto; font-size: 12.5px;
}
/* Глазок «тихого просмотра» — вспомогательное действие, а не вторая кнопка
   строки: рамку и подложку снимаем, оставляем тихий значок, который наливается
   цветом при наведении на строку (заказчик, 21.08.2026 — «выглядит некрасиво»).
   Круглая подсветка появляется только под курсором на самом значке. */
.chat-thread__observe {
  flex: 0 0 auto; align-self: center; margin-right: 4px;
  width: 30px; height: 30px; border-radius: 50%;
  border: 0; background: none; color: var(--color-faint); opacity: .55;
  transition: color .15s var(--ease), background .15s var(--ease), opacity .15s var(--ease);
}
.chat-thread__observe svg { width: 17px; height: 17px; }
.chat-thread:hover .chat-thread__observe { opacity: 1; color: var(--color-muted); }
.chat-thread__observe:hover,
.chat-thread__observe:focus-visible {
  opacity: 1; color: var(--color-primary-dark); background: var(--color-primary-soft);
  transform: none; /* .btn-icon:hover подпрыгивает — в строке списка это лишнее */
}

/* --- Чаты в два окна: список слева, переписка справа ----------------------
   Раздел «Чаты» на широком экране раньше был одной колонкой 1240px: четыре
   строки списка вверху, а ниже до конца окна пустота (заказчик, 21.08.2026).
   Теперь это мессенджер: слева поиск и список на всю высоту окна, справа —
   открытая переписка с композером. Разметка ОДНА на оба случая: на узком
   экране правое окно снято (display: none), строки остаются обычными
   ссылками и раздел выглядит ровно как прежде. Тот же порог 1100px проверяет
   initChatSplit — тем же медиазапросом через matchMedia, а не своей меркой
   ширины (фильтры триггеров htmx вида click[expr] здесь запрещены: CSP не
   даёт eval). Медиазапрос меряет реальные px, несмотря на html { zoom }. */
.chat-split__view { display: none; }
/* Ручка ширины списка — только в двух окнах (см. медиазапрос ниже). */
.chat-split__grip { display: none; }
/* Пока тянут — курсор «колонка» на всём документе и никакого выделения текста:
   иначе протяжка через ленту выделяет чужие сообщения. */
html.is-chat-resizing { cursor: col-resize; user-select: none; }
/* Компактная шапка колонки — только в двух окнах: на узком экране остаётся
   обычная шапка страницы (см. thread_list.html). */
.chat-split__head { display: none; }

@media (min-width: 1100px) {
  /* Страница занимает ровно высоту окна: прокручиваются список и лента
     внутри, а не документ. Формула та же, что у экрана переписки — vh
     умножается на --vu (обратный множитель к --ui-scale). */
  .page.page--chat-list {
    height: calc(calc(100vh * var(--vu)) - var(--topbar-h));
    height: calc(calc(100dvh * var(--vu)) - var(--topbar-h));
    display: flex; flex-direction: column; min-height: 0;
  }
  /* Колонка чтения (1240px) — правило для одной ленты; двум окнам она мешает:
     переписка ужалась бы обратно в треть экрана. */
  .page--chat-list.page--reading > * { max-width: none; }
  .chat-split {
    flex: 1 1 auto; min-height: 0; padding: 0; gap: 0; overflow: hidden;
    display: grid;
    /* Ширину списка человек тянет сам (--chat-list-w, initChatSplitResize).
       clamp — сторож на случай чужого/старого значения в localStorage: уже, чем
       280px, строка нечитаема, а шире половины окна переписка превращается в
       щель. Без сохранённого значения — 400px: с 03.09.2026 строка списка
       живёт по макету (аватарка 52, имя 17), и в прежних 340 имя обрезалось
       многоточием у половины контактов. 400 в файле = 340 на экране — ровно
       ширина колонки из макета (html{zoom: .85}). */
    grid-template-columns:
      clamp(280px, var(--chat-list-w, 400px), min(560px, 50%)) minmax(0, 1fr);
  }
  .chat-split__list {
    position: relative; /* якорь для ручки ширины */
    display: flex; flex-direction: column; min-height: 0;
    /* Колонка списка того же цвета, что и переписка (03.09.2026, макет
       заказчика): раздел читается одной страницей, а границу между окнами
       держит волосяная кромка. До этого колонка была залита
       --color-surface-soft, потому что иначе не было видно ховера строки —
       теперь ховер и есть эта заливка (см. ниже). */
    border-right: 1px solid var(--color-border);
    background: var(--color-surface);
  }
  /* Ручка стоит на кромке колонки и ловит курсор шире, чем видна (10px против
     3px): попасть в однопиксельную линию мышью невозможно. Смещена НАРУЖУ
     (4px внутрь списка, 6px в сторону переписки) — иначе она накрывала бы
     полосу прокрутки списка, и та переставала бы ловить мышь. */
  .chat-split__grip {
    display: block; position: absolute; top: 0; bottom: 0; right: -6px;
    width: 10px; z-index: 3; cursor: col-resize; touch-action: none;
  }
  .chat-split__grip::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: 3px; width: 3px;
    border-radius: 2px; background: transparent;
    transition: background-color .16s var(--ease);
  }
  .chat-split__grip:hover::before,
  .chat-split__grip:focus-visible::before,
  html.is-chat-resizing .chat-split__grip::before { background: var(--color-primary); }
  .chat-split__grip:focus-visible { outline: none; }
  /* Заголовок «Чаты» и три кнопки съедали верхнюю полосу окна, а раздел должен
     быть занят перепиской (заказчик, 21.08.2026). Шапка страницы снята, её
     действия стоят значками в колонке списка — над поиском. */
  .page--chat-list > .page__head { display: none; }
  .chat-split__head {
    display: flex; align-items: center; gap: 6px; flex: 0 0 auto;
    padding: 10px 10px 0;
  }
  .chat-split__title { font-size: 20px; font-weight: 800; letter-spacing: -.02em; }
  .chat-split__tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }
  .chat-tool { width: 32px; height: 32px; border-radius: var(--radius-sm); }
  .chat-tool svg { width: 17px; height: 17px; }
  /* «Создать группу» — главное действие раздела, поэтому значок акцентный. */
  .chat-tool--primary {
    background: var(--color-primary); border-color: var(--color-primary);
    color: var(--color-on-primary);
  }
  .chat-tool--primary:hover {
    background: var(--color-primary-dark); border-color: var(--color-primary-dark);
    color: var(--color-on-primary);
  }
  .chat-split__list .filters-bar { flex: 0 0 auto; padding: 10px; }
  /* Поиск — «таблетка», как в макете: у поля списка чатов нет соседей по
     строке фильтров, и прямоугольник рядом с круглыми аватарками выбивался. */
  .chat-split__list .filters-bar__search .input { border-radius: var(--radius-pill); }
  .chat-split #chat-thread-results { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0 8px 10px; }
  .chat-split .chat-list-group + .chat-list-group { margin-top: 14px; }
  /* Строка списка в узкой колонке — две строки, как в мессенджере: имя и время
     сверху, превью снизу. Бейдж роли и подпись ПВЗ отсюда сняты (заказчик,
     21.08.2026: «чат выглядит перегруженным») — в колонке 300–360px они всё
     равно обрезались многоточием и уводили превью на третий этаж. И роль, и
     точка целиком видны в шапке открытой переписки справа; то же решение уже
     принято для телефона (mobile.css). */
  .chat-split .chat-thread { position: relative; border-bottom: 0; border-radius: var(--radius-lg); }
  /* Волосяной разделитель между строками (макет 03.09.2026). Псевдоэлементом,
     а не border-bottom: строка скруглена и подсвечивается заливкой, и кромка
     на ней читалась бы как рамка таблетки. Слева отступ ровно на аватарку с
     зазором (14 + 52 + 12), справа — на поле строки: линия идёт под текстом,
     как в мессенджерах, а не через весь столбец. */
  .chat-split .chat-thread + .chat-thread::before {
    content: ""; position: absolute; left: 78px; right: 14px; top: 0; height: 1px;
    background: var(--color-divider);
  }
  /* Ховер строки — ступень ОТ колонки: та белая (см. .chat-split__list), и
     строка под курсором уходит на тон в сторону, как везде в продукте.
     Прежде было наоборот (колонка серая, строка светлела до белой) — это
     держалось на заливке колонки и вместе с ней и снято. */
  .chat-split .chat-thread:hover { background: var(--color-surface-soft); }
  /* Строка по макету 03.09.2026: крупная аватарка, имя строкой, превью в ДВЕ
     строки. Выравнивание по верхней кромке, а не по центру: у двухстрочного
     превью центр уводил имя вниз от аватарки. */
  .chat-split .chat-thread__link { align-items: flex-start; padding: 12px 14px; gap: 12px; }
  /* Кегли и аватарка пересчитаны из макета делением на 0.85 (html{zoom}):
     аватарка 44 экранных px → 52 в файле, имя 14.5 → 17, превью 12.5 → 14.5.
     Написать сюда числа макета как есть — самая частая ошибка в этом файле:
     на экране вышло бы на 15% мельче, чем на картинке заказчика. */
  .chat-split .chat-thread__avatar { width: 52px; height: 52px; font-size: 18px; }
  .chat-split .chat-thread__name { flex-wrap: nowrap; gap: 6px; min-width: 0; }
  .chat-split .chat-thread__name strong {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
    font-size: 17px;
  }
  /* Две строки с многоточием — это -webkit-line-clamp, а не white-space:
     nowrap: у последней реплики смысл нередко начинается со второй строки. */
  .chat-split .chat-thread__preview {
    display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
    -webkit-box-orient: vertical; overflow: hidden;
    white-space: normal; max-width: 100%; line-height: 1.38; font-size: 14.5px;
  }
  .chat-split .chat-thread__name .role-badge,
  .chat-split .chat-thread__main .cell-sub { display: none; }
  /* Время — ТРЕТЬЕЙ строкой под превью, со значком часов (макет 04.09.2026).
     Раскладка строки поэтому грид, а не флекс: аватарка занимает обе строки
     колонки, имя с превью — первую, время с бейджем непрочитанного — вторую.
     Побочно это вернуло имени всю ширину: рядом с ним больше нет столбца
     времени, и контакты вроде «Помогаев Пётр (помощник УА)» перестали
     обрезаться многоточием. */
  .chat-split .chat-thread__link {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "ava main" "ava aside";
    align-items: start; column-gap: 12px; row-gap: 4px;
  }
  .chat-split .chat-thread__avatar { grid-area: ava; align-self: start; }
  .chat-split .chat-thread__main { grid-area: main; }
  .chat-split .chat-thread__aside {
    grid-area: aside; flex-direction: row; align-items: center;
    justify-content: flex-start; gap: 8px; padding-top: 0; font-size: 11.5px;
  }
  /* Часы — маска на псевдоэлементе самой подписи, а не отдельный узел: список
     перерисовывается поллингом и живым поиском (innerHTML), вставленный в
     разметку значок пришлось бы восстанавливать после каждого свопа. Нет
     времени — нет и подписи, а значит и значка: отдельный сторож не нужен. */
  .chat-split .chat-thread__time--short::before {
    content: ""; width: 13px; height: 13px; flex: 0 0 auto; background: currentColor;
    -webkit-mask: var(--icon-clock) center / contain no-repeat;
    mask: var(--icon-clock) center / contain no-repeat;
  }
  /* Полная дата «21.08.2026 13:37» съедала треть узкой строки — в колонке
     показываем короткую (день и время), полная остаётся в широком списке.
     Порядок этих двух строк важен: у короткой даты ОБА класса, правила равны
     по специфичности, и `--short` обязан стоять ПОСЛЕ — иначе она скрыта
     вместе с полной. */
  .chat-split .chat-thread__time { display: none; }
  .chat-split .chat-thread__time--short { display: inline-flex; align-items: center; gap: 5px; }
  /* Глазок тихого просмотра — только под курсором (и по фокусу с клавиатуры):
     столбик одинаковых значков у каждой строки был первым, что бросалось в
     глаза в узкой колонке. */
  @media (hover: hover) {
    .chat-split .chat-thread__observe { opacity: 0; pointer-events: none; }
    .chat-split .chat-thread:hover .chat-thread__observe,
    .chat-split .chat-thread__observe:focus-visible {
      opacity: 1; pointer-events: auto;
    }
  }
  /* Открытая переписка: заливка сильнее, чем у непрочитанного (у того 6% и
     кромка слева) — иначе «где я сейчас» и «где новое» читаются одинаково. */
  .chat-split .chat-thread.is-active,
  .chat-split .chat-thread.is-active:hover { background: var(--color-primary-soft); }

  .chat-split__view { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
  .chat-pane__inner { display: flex; flex-direction: column; min-height: 0; height: 100%; }
  .chat-pane__head {
    display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
    padding: 12px 16px; border-bottom: 1px solid var(--color-border);
  }
  .chat-pane__titles { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .chat-pane__title {
    margin: 0; font-size: 16px; font-weight: 750;
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  }
  .chat-pane__sub {
    margin: 0; font-size: 12.5px; color: var(--color-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .chat-pane__actions { margin-left: auto; display: flex; gap: 8px; flex: 0 0 auto; }
  /* Карточка переписки внутри панели теряет рамку и тень: она уже в карточке.
     html[data-theme="dark"] .card — это (0,2,1), поэтому «голого» селектора
     панели не хватало: в тёмной теме карточка сохраняла тень и градиент.
     Перечисляем оба варианта темы вместо гонки специфичностей. */
  .chat-split__view .chat-card,
  html[data-theme="dark"] .chat-split__view .chat-card,
  html[data-theme="light"] .chat-split__view .chat-card {
    flex: 1 1 auto; min-height: 0;
    border: 0; border-radius: 0; box-shadow: none; background: transparent;
    padding: 12px 16px 14px;
  }
  /* Лента в панели — без рамки и подложки: пузыри лежат прямо на карточке,
     как в мессенджере. Рамка нужна на отдельной странице, где лента делит
     карточку с композером, а здесь она добавляла вторую коробку в коробке. */
  .chat-split__view .chat-box {
    /* Полотно переписки с 03.09.2026 РАВНО карточке (см. --chat-canvas):
       пузырь без рамки виден за счёт собственной заливки, а не за счёт
       ступени под ним. Присваивание оставлено — панель не должна зависеть от
       того, какой фон достался ей от карточки. */
    border: 0; background: var(--chat-canvas); padding: 4px 0 12px 2px;
    /* Лента шире карточки на её же правый отступ: полоса прокрутки встаёт на
       кромку панели, а пузыри остаются на месте — их держит padding-right
       у .chat-msg ниже. */
    margin-right: -16px;
    /* min-height: 300px из базового правила ленты на низком окне выдавливал
       композер за нижнюю кромку карточки (.chat-split { overflow: hidden }). */
    min-height: 0;
  }
  .chat-split__view .chat-box > .chat-msg { padding-right: 18px; }
  /* Плашка режима (тихий просмотр, выключенная переписка) в панели — тоньше:
     она пояснение, а не заголовок. */
  .chat-split__view .chat-observe-note { padding: 8px 12px; font-size: 12.5px; }
  .chat-pane__placeholder {
    margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px;
    padding: 24px; color: var(--color-faint); text-align: center;
  }
  .chat-pane__placeholder svg { width: 46px; height: 46px; }
  .chat-pane__placeholder p { margin: 0; }
}

/* Приложенная пересылка над композером (схема Telegram): что пересылаем и от
   кого, крестик — отменить. Живёт внутри формы, поэтому уходит вместе с
   отправкой (dropForward в app.js). */
.chat-fwd-bar {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  padding: 8px 12px; border-radius: var(--radius-md);
  border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
  background: var(--color-primary-soft);
}
.chat-fwd-bar svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--color-primary-dark); }
.chat-fwd-bar__main { display: flex; flex-direction: column; gap: 1px; min-width: 0; font-size: 13px; }
.chat-fwd-bar__main strong { color: var(--color-primary-dark); }
.chat-fwd-bar__main .cell-sub {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-fwd-bar__cancel {
  margin-left: auto; flex: 0 0 auto; width: 28px; height: 28px;
  border: 0; background: none; color: var(--color-muted);
}
.chat-fwd-bar__cancel:hover { color: var(--color-danger); background: none; transform: none; }
.chat-fwd-bar__cancel svg { width: 15px; height: 15px; color: currentColor; }

/* Приложенный ответ-цитата над композером — сестра плашки пересылки, но её
   наполняет JS из отмеченных пузырей (сервер тут не участвует). Гасится
   атрибутом hidden, поэтому display снимаем явно: класс задаёт flex, и без
   этого правила [hidden] не сработало бы. */
.chat-reply-bar {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  padding: 8px 12px; border-radius: var(--radius-md);
  border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
  background: var(--color-primary-soft);
}
.chat-reply-bar[hidden] { display: none; }
.chat-reply-bar > svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--color-primary-dark); }

/* Плашка режима тихого просмотра над лентой. */
.chat-observe-note {
  display: flex; align-items: flex-start; gap: 10px; margin-bottom: 12px;
  padding: 10px 14px; border-radius: var(--radius-lg);
  border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
  background: var(--color-primary-soft); color: var(--color-text);
  font-size: 13px; line-height: 1.45;
}
.chat-observe-note svg { width: 18px; height: 18px; flex: 0 0 auto; margin-top: 1px; color: var(--color-primary-dark); }
/* Та же плашка, но НА МЕСТЕ композера (архивный диалог): отступ сверху, а не снизу,
   и flex-shrink как у формы — иначе на телефоне её сжимает лента. */
.chat-observe-note--foot { margin: 12px 0 0; flex: 0 0 auto; }

/* --- Двухпанельный чат (диалоги сотрудника): список слева | лента справа --- */
.chat-monitor {
  display: grid; grid-template-columns: minmax(260px, 340px) 1fr;
  gap: 0; padding: 0; overflow: hidden;
  min-height: 560px; height: calc(calc(100vh * var(--vu)) - 220px);
}
.chat-monitor__list {
  display: flex; flex-direction: column; min-height: 0;
  border-right: 1px solid var(--color-border); background: var(--color-surface-soft);
}

.chat-monitor__item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px; border: 0; border-radius: var(--radius-md);
  background: transparent; text-align: left; cursor: pointer;
  transition: background .15s var(--ease);
}
.chat-monitor__item:hover { background: var(--color-surface); }
.chat-monitor__item.is-active { background: var(--color-primary-soft); }
.chat-monitor__avatar {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--color-primary); color: var(--color-on-primary);
  font-weight: 650; font-size: 15px;
}
/* Вариант с фото (includes/user_avatar.html): картинка вместо инициала. */
.chat-monitor__avatar--img { background: var(--color-surface-soft); overflow: hidden; }
.chat-monitor__avatar--img img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Аватар у чужого пузыря в ленте сообщений (у своих аватара нет). */
.chat-msg__avatar { width: 33px; height: 33px; font-size: 13px; align-self: flex-end; margin-right: 11px; }
/* Аватар-ссылка на кадровую карточку: тот же кружок, но с откликом на курсор.
   transform держим только на ссылке — у обычного аватара анимировать нечего. */
a.chat-msg__avatar { cursor: pointer; transition: transform .18s var(--ease), box-shadow .18s var(--ease); }
a.chat-msg__avatar:hover { transform: scale(1.08); box-shadow: var(--ring-soft); }
a.chat-msg__avatar:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.chat-monitor__item-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.chat-monitor__item-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.chat-monitor__item-name {
  font-size: 13.5px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-monitor__item-time { flex: 0 0 auto; font-size: 11.5px; color: var(--color-muted); }
.chat-monitor__item-sub {
  font-size: 12px; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Роль/должность — фиксированная нижняя строка пункта: высота у всех одинаковая. */
.chat-monitor__item-role { display: flex; align-items: center; gap: 6px; margin-top: 3px; }
.chat-monitor__empty { padding: 24px 14px; color: var(--color-muted); font-size: 13px; text-align: center; }

.chat-monitor__view { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.chat-monitor__placeholder {
  margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px;
  color: var(--color-faint); text-align: center; padding: 24px;
}
.chat-monitor__placeholder svg { width: 46px; height: 46px; }
.chat-monitor__conv { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.chat-monitor__head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid var(--color-border); flex: 0 0 auto;
}
.chat-monitor__head-main { min-width: 0; }
.chat-monitor__head-title { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0; }
.chat-monitor__head-sub { margin: 2px 0 0; font-size: 12.5px; color: var(--color-muted); }
.chat-monitor__note {
  display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
  padding: 8px 16px; font-size: 12.5px; color: var(--color-primary-dark);
  background: var(--color-primary-soft);
  border-bottom: 1px solid color-mix(in srgb, var(--color-primary) 20%, var(--color-border));
}
.chat-monitor__note svg { width: 15px; height: 15px; flex: 0 0 auto; }
.chat-box--monitor { flex: 1 1 auto; overflow-y: auto; padding: 16px; min-height: 0; }

@media (max-width: 720px) {
  /* На телефоне колонки складываются: список сверху, лента снизу. */
  .chat-monitor { grid-template-columns: 1fr; height: auto; }
  .chat-monitor__list { border-right: 0; border-bottom: 1px solid var(--color-border); }
  #dialog-partners { max-height: calc(40vh * var(--vu)); }
  .chat-box--monitor { max-height: calc(60vh * var(--vu)); }
}

/* --- Диалоги сотрудника (owner / tech_admin): выбор сотрудника карточками --- */
.dialog-pick-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px;
}
.dialog-pick {
  position: relative; display: flex; gap: 12px; align-items: flex-start;
  padding: 14px; border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface); color: inherit; text-decoration: none;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease),
              transform .15s var(--ease);
}
.dialog-pick:hover {
  border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
  box-shadow: var(--shadow-sm); transform: translateY(-1px);
}
.dialog-pick__body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
.dialog-pick__name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 14px; }
.dialog-pick__sub {
  font-size: 12.5px; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dialog-pick__meta {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  margin-top: 4px; font-size: 12px; color: var(--color-muted);
}
.dialog-pick__badge { position: absolute; top: 12px; right: 12px; }
/* Роль и должность — фиксированная нижняя строка карточки (ничего не прыгает). */
.dialog-pick__roles { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.dialog-pick__position { font-size: 12px; font-weight: 650; color: var(--color-muted); }
/* Карточка с непрочитанным заметна и до наведения. */
.dialog-pick--unread {
  border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-border));
  background: color-mix(in srgb, var(--color-primary) 5%, var(--color-surface));
}

/* Левая колонка страницы диалогов: заголовок секции + список собеседников. */
.dialog-partners__head {
  padding: 12px 16px 6px; flex: 0 0 auto;
  font-size: 11.5px; font-weight: 750; letter-spacing: .07em; text-transform: uppercase;
  color: var(--color-muted);
}
#dialog-partners { overflow-y: auto; flex: 1 1 auto; padding: 0 8px 8px; }

/* --- Непрочитанное заметнее: выделение строк списков, пульс бейджа ---------- */
/* Строка треда с новыми сообщениями: тонированный фон + акцентная кромка слева. */
.chat-thread--unread {
  background: color-mix(in srgb, var(--color-primary) 6%, transparent);
  box-shadow: inset 3px 0 0 var(--color-primary);
}
.chat-thread--unread:hover { background: color-mix(in srgb, var(--color-primary) 10%, transparent); }
.chat-thread--unread .chat-thread__name strong { font-weight: 800; }
.chat-thread--unread .chat-thread__aside .cell-muted { color: var(--color-text); font-weight: 650; }

/* Пульс бейджа непрочитанного в сайдбаре: новое сообщение видно периферией,
   а не только цифрой. Поллинг пересоздаёт бейдж — анимация продолжается. */
@keyframes chat-badge-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-danger) 45%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.chat-unread-badge:not([hidden]) { animation: chat-badge-pulse 2s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .chat-unread-badge:not([hidden]) { animation: none; }
}

/* Свободная смена уходит за минуты — бейдж биржи пульсирует так же, как
   непрочитанное в чате, но своим цветом: это возможность, а не тревога. */
@keyframes xchg-badge-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-warning) 45%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.xchg-free-badge:not([hidden]) { animation: xchg-badge-pulse 2s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .xchg-free-badge:not([hidden]) { animation: none; }
}

@media (max-width: 720px) {
  .chat-msg__bubble { max-width: 92%; }
  /* Композер на телефоне — колонкой: поле во всю ширину,
     под ним строка «эмодзи + Отправить». */
  .chat-form__row { flex-direction: column; align-items: stretch; }
  .chat-form__tools .btn[type="submit"] { flex: 1 1 auto; }
  .chat-emoji__pop { right: auto; left: 0; }
}

/* ---------------------------------------------------------------------------
   Калькулятор KPI (переработанный)
   --------------------------------------------------------------------------- */
.kpi-calc .card { display: flex; flex-direction: column; gap: 14px; }
/* Классы с собственным display (grid/flex) иначе перебивают атрибут hidden. */
.kpi-calc [hidden] { display: none !important; }
.seg-toggle {
  display: inline-flex; gap: 4px; padding: 4px;
  border: 1px solid var(--color-border); border-radius: var(--radius-pill);
  background: var(--color-surface-soft); width: fit-content;
}
.seg-toggle__item { position: relative; }
.seg-toggle__item input { position: absolute; opacity: 0; pointer-events: none; }
.seg-toggle__item span {
  display: inline-block; padding: 7px 16px; border-radius: var(--radius-pill);
  font-size: 13.5px; font-weight: 650; color: var(--color-muted);
  cursor: pointer; transition: background .15s var(--ease), color .15s var(--ease);
}
.seg-toggle__item input:checked + span {
  background: var(--color-primary); color: var(--color-on-primary); box-shadow: var(--shadow-sm);
}
.seg-toggle__item input:focus-visible + span { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.check-row { display: flex; align-items: flex-start; gap: 10px; font-size: 14px; cursor: pointer; }
.check-row input { margin-top: 3px; }
.kpi-batch-mini { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.kpi-batch-mini li {
  display: flex; flex-direction: column; gap: 2px;
  padding: 10px 0; border-bottom: 1px solid var(--color-border); font-size: 13.5px;
}
.kpi-batch-mini li:last-child { border-bottom: 0; }

/* ---------------------------------------------------------------------------
   Конструкторы формул: зарплата и KPI (+ живой симулятор)

   Stage 72: правила формулы раньше жили в таблицах на 6–7 числовых колонок
   внутри узкой колонки грида — таблица не помещалась (755px в 457px) и
   страница «съезжала» в горизонтальный скролл. Теперь правило — карточка с
   подписанными полями, которые переносятся по ширине, а симулятор стоит в
   липкой колонке справа и не уезжает при прокрутке длинной формы.
   --------------------------------------------------------------------------- */
.form-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Компактные чипы «что задано» в списке формул. */
.chips--compact { gap: 6px; }
.chips--compact .chip { padding: 4px 9px; font-size: 12px; font-weight: 600; }

/* ---- Панель выбора формулы (какую правим + применить / удалить) --------- */
.fbar {
  display: flex; align-items: flex-end; gap: 12px 18px; flex-wrap: wrap;
  margin-bottom: 18px; padding: 14px 16px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface);
}
.fbar__pick { flex: 1 1 260px; min-width: 0; }
.fbar__pick .form-field { gap: 6px; }
.fbar__pick .input { width: 100%; }
.fbar__actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }

/* ---- Каркас страницы конструктора --------------------------------------- */
.fbuild { display: grid; gap: 18px; align-items: start; grid-template-columns: minmax(0, 1fr); }
.fbuild__main { display: flex; flex-direction: column; gap: 18px; min-width: 0; max-width: none; }
.fbuild__side { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
@media (min-width: 928px) {
  .fbuild { grid-template-columns: minmax(0, 1fr) minmax(288px, 336px); }
  .fbuild__side { position: sticky; top: 12px; }
}
.fbuild .card { display: flex; flex-direction: column; gap: 14px; }
.fbuild [hidden] { display: none !important; }

/* ---- Нумерованный заголовок шага ---------------------------------------- */
.fstep { display: flex; align-items: flex-start; gap: 12px; }
.fstep__num {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-size: 12.5px; font-weight: 800; line-height: 1;
}
.fstep__titles { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.fstep__title { margin: 0; font-size: 15px; font-weight: 700; color: var(--color-heading); line-height: 1.25; }
.fstep__hint { font-size: 12.5px; color: var(--color-muted); line-height: 1.4; }

/* ---- Правило = карточка (вместо строки широкой таблицы) ------------------ */
.frules { display: flex; flex-direction: column; gap: 12px; }
.frule {
  display: flex; flex-direction: column; gap: 12px;
  padding: 14px; border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); background: var(--color-surface-soft);
  transition: border-color .18s var(--ease), opacity .18s var(--ease);
}
.frule--new { border-style: dashed; background: transparent; }
.frule:has(.fcheck--danger input:checked) { opacity: .5; border-color: var(--color-danger); }
.frule__top { display: flex; align-items: center; gap: 10px 14px; flex-wrap: wrap; }
.frule__name { flex: 1 1 220px; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.frule__name .input { width: 100%; font-weight: 650; }
.frule__flags { display: flex; align-items: center; gap: 14px; margin-left: auto; }
.frule__grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); }
.frule__grid .field__label { font-size: 12px; }
.frule__grid .input, .frule__grid select.input { width: 100%; min-width: 0; }
/* «Способ расчёта» — самая длинная подпись («За каждую отработанную смену»):
   на широком экране колонка растягивается, чтобы select не обрезал текст. */
@media (min-width: 820px) {
  .frule__grid--modes { grid-template-columns: 0.85fr 1.4fr 0.85fr 0.9fr; }
}
.frule__badge {
  flex: 0 0 auto; padding: 3px 9px; border-radius: var(--radius-pill);
  background: var(--color-surface); border: 1px solid var(--color-border);
  font-size: 11.5px; font-weight: 700; color: var(--color-muted); white-space: nowrap;
}

/* ---- Компактный чекбокс с подписью (вкл / удалить) ---------------------- */
.fcheck {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-size: 12.5px; font-weight: 600; color: var(--color-muted); white-space: nowrap;
}
.fcheck input { width: 16px; height: 16px; accent-color: var(--color-primary); cursor: pointer; margin: 0; }
.fcheck--danger { color: var(--color-danger); }
.fcheck--danger input { accent-color: var(--color-danger); }

/* ---- Метрика KPI (набор фиксирован, поля зависят от метрики) ------------ */
.fmetric__head { display: flex; align-items: flex-start; gap: 10px; }
.fmetric__titles { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.fmetric__name { font-size: 14px; font-weight: 700; color: var(--color-heading); }
.fmetric__hint { font-size: 12px; color: var(--color-muted); line-height: 1.4; }
.fmetric .fcheck { margin-top: 1px; }
/* Пара «штраф за каждые N ₽» — два поля в одной подписи, поэтому ячейка
   занимает две колонки грида: в одной инпуты схлопывались до «0,0(». */
.form-field--pair { grid-column: span 2; }
.fpair { display: flex; align-items: center; gap: 8px; }
.fpair .input { flex: 1 1 0; min-width: 64px; }
.fpair__sep { color: var(--color-muted); font-size: 12px; white-space: nowrap; }

.kpi-sim__result { display: flex; flex-direction: column; gap: 6px; }
.kpi-sim__score { font-size: 40px; font-weight: 800; letter-spacing: -0.02em; color: var(--color-heading); line-height: 1; }
.kpi-sim__scale { color: var(--color-muted); font-size: 12.5px; }
.kpi-sim__bar {
  height: 8px; border-radius: 999px; overflow: hidden;
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
}
.kpi-sim__bar span {
  display: block; height: 100%; width: 0;
  background: var(--gradient-primary); border-radius: inherit;
  transition: width .25s var(--ease);
}
.kpi-sim__bonus { font-weight: 650; font-size: 13.5px; color: var(--color-primary-dark); }
.form-grid--sim { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.form-grid--sim .field__label { font-size: 12px; }
.form-grid--sim .input { padding: 7px 9px; font-size: 13px; }
.kpi-sim__breakdown, .kpi-sim__formula { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.kpi-sim__breakdown li, .kpi-sim__formula li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 7px 0; border-bottom: 1px solid var(--color-border); font-size: 13px;
}
.kpi-sim__breakdown li:last-child, .kpi-sim__formula li:last-child { border-bottom: 0; }
.kpi-sim__breakdown li span, .kpi-sim__formula li span { color: var(--color-muted); }
.kpi-sim__breakdown li strong, .kpi-sim__formula li strong { text-align: right; white-space: nowrap; }
/* «Формула словами» живёт в узкой колонке: длинные пары «название → правило»
   в одну строку рвались на рваные столбцы, поэтому значение уходит под
   подпись. Цифровая расшифровка (breakdown) остаётся двухколоночной. */
.kpi-sim__formula li { flex-direction: column; align-items: flex-start; gap: 3px; }
.kpi-sim__formula li strong { text-align: left; white-space: normal; }

/* =========================================================================
   Employee cabinet — «Мои KPI» (stage 42)
   Мотивационная подача балла: кольцо с уровнем, лестница премий, зоны роста,
   динамика по периодам. Тон кольца/фона задаётся модификатором уровня
   (excellent/good/attention/risk) — те же пороги, что kpi_score_badge.
   ========================================================================= */
.kpi-cab-hero {
  position: relative; display: flex; align-items: center; gap: 28px; flex-wrap: wrap;
  padding: 28px; margin-bottom: 18px;
  border-radius: var(--radius-2xl); border: 1px solid var(--color-border);
  background: var(--color-surface); box-shadow: var(--shadow-card);
  --kpi-tone: var(--color-primary); --kpi-tone-soft: var(--color-primary-soft);
}
.kpi-cab-hero::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: radial-gradient(circle at 0% 0%, var(--kpi-tone-soft), transparent 52%);
  opacity: .8;
}
.kpi-cab-hero > * { position: relative; }
.kpi-cab-hero--excellent { --kpi-tone: var(--color-success); --kpi-tone-soft: var(--color-success-soft); }
.kpi-cab-hero--good { --kpi-tone: var(--color-primary); --kpi-tone-soft: var(--color-primary-soft); }
.kpi-cab-hero--attention { --kpi-tone: var(--color-warning); --kpi-tone-soft: var(--color-warning-soft); }
.kpi-cab-hero--risk { --kpi-tone: var(--color-danger); --kpi-tone-soft: var(--color-danger-soft); }

.kpi-cab-ring {
  width: 176px; height: 176px; flex: 0 0 176px; border-radius: 50%; position: relative;
  background: conic-gradient(var(--kpi-tone) calc(var(--val, 0) * 1%), var(--color-surface-soft) 0);
  /* Не donut-pop: та анимация заканчивается на rotate(-90deg) (для SVG-пончиков)
     и положила бы цифры в центре набок. */
  animation: kpi-ring-pop .7s var(--ease) both;
}
@keyframes kpi-ring-pop { from { opacity: 0; transform: scale(.88); } to { opacity: 1; transform: scale(1); } }
.kpi-cab-ring--sm { width: 140px; height: 140px; flex-basis: 140px; }
.kpi-cab-ring::after {
  content: ""; position: absolute; inset: 13px; border-radius: 50%;
  background: var(--color-surface); box-shadow: inset 0 0 0 1px var(--color-border);
}
.kpi-cab-ring__center {
  position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px; text-align: center;
}
.kpi-cab-ring__center strong { font-size: 42px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; color: var(--color-heading); }
.kpi-cab-ring--sm .kpi-cab-ring__center strong { font-size: 34px; }
.kpi-cab-ring__center span { font-size: 12px; color: var(--color-muted); }

.kpi-cab-hero__info { flex: 1; min-width: 240px; display: grid; gap: 8px; justify-items: start; }
.kpi-cab-hero__eyebrow {
  color: var(--color-primary-dark); font-size: 12px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em;
}
.kpi-cab-hero__title { font-size: 27px; line-height: 1.1; letter-spacing: -0.015em; }
.kpi-cab-hero__msg { color: var(--color-muted); max-width: 46ch; }
.kpi-cab-chips { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.kpi-cab-chip {
  display: inline-flex; align-items: center; gap: 5px; padding: 6px 11px;
  border-radius: var(--radius-pill); border: 1px solid var(--color-border);
  background: var(--color-surface-soft); font-size: 12.5px; color: var(--color-muted);
}
.kpi-cab-chip strong { color: var(--color-heading); }
.kpi-cab-chip--money { border-color: color-mix(in srgb, var(--kpi-tone) 40%, var(--color-border)); background: var(--kpi-tone-soft); color: var(--color-text); }

.kpi-cab-next { display: grid; gap: 7px; width: 100%; max-width: 460px; margin-top: 2px; }
.kpi-cab-next__label { font-size: 13px; color: var(--color-muted); }
.kpi-cab-next__label strong { color: var(--color-heading); }
.kpi-cab-next__label--top { font-weight: 650; color: var(--color-success); }
.kpi-cab-next__track {
  display: block; height: 8px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); border: 1px solid var(--color-border); overflow: hidden;
}
.kpi-cab-next__fill {
  display: block; height: 100%; width: var(--w, 0); border-radius: inherit;
  background: linear-gradient(90deg, var(--kpi-tone), color-mix(in srgb, var(--kpi-tone) 55%, #fff));
}
.kpi-cab-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }

.kpi-cab-rate { display: flex; align-items: baseline; gap: 8px; margin: 2px 0 10px; }
.kpi-cab-rate__amount { font-size: 32px; font-weight: 800; letter-spacing: -0.02em; color: var(--color-heading); }
.kpi-cab-rate__type { color: var(--color-muted); }

.kpi-ladder { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.kpi-ladder__item {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 13px; border-radius: var(--radius-lg);
  border: 1px solid var(--color-border); background: var(--color-surface-soft);
}
.kpi-ladder__item--passed { opacity: .68; }
.kpi-ladder__item--current {
  border-color: color-mix(in srgb, var(--color-primary) 55%, var(--color-border));
  background: var(--color-primary-soft); box-shadow: var(--ring-soft);
}
.kpi-ladder__item strong { display: block; font-size: 13.5px; }
.kpi-ladder__range { display: block; color: var(--color-muted); font-size: 12px; margin-top: 2px; }
.kpi-ladder__bonus { font-weight: 800; white-space: nowrap; text-align: right; }
.kpi-ladder__you {
  display: block; font-style: normal; font-weight: 800; font-size: 11px;
  color: var(--color-primary-dark); text-transform: uppercase; letter-spacing: .05em;
}

.kpi-zone { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 10px; }
.kpi-zone__item {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 13px; border-radius: var(--radius-lg);
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
}
.kpi-zone__item--loss { border-left: 4px solid var(--color-danger); }
.kpi-zone__item--loss > b { color: var(--color-danger); }
.kpi-zone__item--gain { border-left: 4px solid var(--color-success); }
.kpi-zone__item--gain > b { color: var(--color-success); }
.kpi-zone__item strong { font-size: 13.5px; }
.kpi-zone__item .cell-sub { display: block; margin-top: 2px; }

/* stretch (не flex-end): колонки обязаны занять всю высоту ряда, иначе
   процентная высота столбика считается от схлопнувшейся auto-высоты. */
/* Тонкая горизонтальная сетка — фоном контейнера, без правки разметки.
   background-position: 0 100% привязывает её к оси, а не к верхнему краю. */
.kpi-cab-bars {
  display: flex; align-items: stretch; gap: 8px; height: 176px;
  background: repeating-linear-gradient(180deg, var(--color-divider) 0 1px, transparent 1px 25%);
  background-position: 0 100%;
}
.kpi-cab-bars__bar {
  flex: 1; max-width: 52px; display: flex; flex-direction: column; justify-content: flex-end;
  align-items: stretch; gap: 5px; text-align: center; color: inherit;
}
.kpi-cab-bars__bar b { font-size: 12px; font-weight: 800; }
.kpi-cab-bars__bar i {
  display: block; height: max(var(--h, 0%), 4px); min-height: 4px;
  /* Скруглённый верх, прямой низ — как в референсе: столбик «растёт» из оси,
     а не висит скруглённой таблеткой. */
  border-radius: 8px 8px 0 0;
  background: var(--color-primary);
  transition: filter .18s var(--ease), background-color .18s var(--ease);
}
/* Раньше последний столбец красился зелёным просто потому, что он последний.
   Референс выделяет ВЫБРАННЫЙ, а остальные держит одним приглушённым тоном. */
.kpi-cab-bars__bar:not(.is-active) i { background: color-mix(in srgb, var(--color-primary) 38%, transparent); }
.kpi-cab-bars__bar.is-active i { background: var(--color-primary); }
.kpi-cab-bars__bar:hover i { filter: brightness(1.12); }
.kpi-cab-bars__bar span { font-size: 11.5px; color: var(--color-muted); }

/* Тизер на профиле-лендинге: балл + ставка в одну карточку. */
.kpi-teaser__row { display: flex; align-items: center; gap: 16px; margin-bottom: 12px; }
.kpi-teaser__ring {
  width: 76px; height: 76px; flex: 0 0 76px; border-radius: 50%; position: relative;
  display: flex; align-items: center; justify-content: center;
  background: conic-gradient(var(--color-primary) calc(var(--val, 0) * 1%), var(--color-surface-soft) 0);
}
.kpi-teaser--excellent .kpi-teaser__ring { background: conic-gradient(var(--color-success) calc(var(--val, 0) * 1%), var(--color-surface-soft) 0); }
.kpi-teaser--attention .kpi-teaser__ring { background: conic-gradient(var(--color-warning) calc(var(--val, 0) * 1%), var(--color-surface-soft) 0); }
.kpi-teaser--risk .kpi-teaser__ring { background: conic-gradient(var(--color-danger) calc(var(--val, 0) * 1%), var(--color-surface-soft) 0); }
.kpi-teaser__ring::after { content: ""; position: absolute; inset: 7px; border-radius: 50%; background: var(--color-surface); }
.kpi-teaser__ring strong { position: relative; z-index: 1; font-size: 19px; font-weight: 800; letter-spacing: -0.02em; }
.kpi-teaser__info { display: grid; gap: 3px; }
.kpi-teaser__level { font-size: 16px; }
.kpi-teaser__bonus { font-size: 13px; color: var(--color-muted); }
.kpi-teaser__bonus b { color: var(--color-heading); }

@media (max-width: 760px) {
  .kpi-cab-hero { padding: 20px; gap: 18px; justify-content: center; text-align: center; }
  .kpi-cab-hero__info { justify-items: center; }
  .kpi-cab-ring { width: 150px; height: 150px; flex-basis: 150px; }
  .kpi-cab-bars { gap: 6px; }
  .kpi-cab-bars__bar b { font-size: 10.5px; }
}

/* =========================================================================
   Employee cabinet — «Инструкции» (stage 44)
   Интерактивный гид: hero с прогрессом «изучено» (localStorage), правила
   09:00 / 15 мин, кликабельная лента дня и карточки-аккордеоны на <details>.
   Префикс ins- (instructions): guide-* уже занят модалкой камер.
   ========================================================================= */
.ins-hero {
  position: relative; display: flex; gap: 26px; flex-wrap: wrap; align-items: stretch;
  padding: 28px; margin-bottom: 18px; overflow: hidden;
  border-radius: var(--radius-2xl); border: 1px solid var(--color-border);
  background: var(--color-surface); box-shadow: var(--shadow-card);
}
.ins-hero::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background:
    radial-gradient(circle at 0% 0%, var(--color-primary-soft), transparent 52%),
    radial-gradient(circle at 100% 100%, var(--color-beige-soft), transparent 46%);
  opacity: .85;
}
.ins-hero > * { position: relative; }
.ins-hero__info { flex: 1 1 320px; min-width: 260px; display: grid; gap: 8px; align-content: start; }
.ins-hero__eyebrow {
  color: var(--color-primary-dark); font-size: 12px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em;
}
.ins-hero__title { font-size: 26px; line-height: 1.12; letter-spacing: -0.015em; margin: 0; }
.ins-hero__msg { color: var(--color-muted); max-width: 52ch; margin: 0; }
.ins-hero__progress { display: grid; gap: 7px; max-width: 420px; margin-top: 6px; }
.ins-hero__count { font-size: 13px; color: var(--color-muted); }
.ins-hero__count b { color: var(--color-heading); font-weight: 800; }
.ins-hero__track {
  display: block; height: 8px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); border: 1px solid var(--color-border); overflow: hidden;
}
.ins-hero__fill {
  display: block; height: 100%; width: 0; border-radius: inherit;
  background: linear-gradient(90deg, var(--color-primary), color-mix(in srgb, var(--color-primary) 55%, #fff));
  transition: width .45s var(--ease-out);
}
.ins-hero__alldone { font-size: 13px; font-weight: 700; color: var(--color-success); }

.ins-hero__rules { flex: 0 1 300px; min-width: 240px; display: grid; gap: 10px; align-content: center; }
.ins-rule {
  display: flex; align-items: center; gap: 14px; padding: 13px 16px;
  border-radius: var(--radius-xl); border: 1px solid color-mix(in srgb, var(--color-primary) 32%, var(--color-border));
  background: var(--color-primary-soft);
}
.ins-rule--timer {
  border-color: color-mix(in srgb, var(--color-warning) 38%, var(--color-border));
  background: var(--color-warning-soft);
}
.ins-rule__value {
  font-size: 24px; font-weight: 800; letter-spacing: -0.02em; white-space: nowrap;
  color: var(--color-primary-dark);
}
.ins-rule--timer .ins-rule__value { color: var(--color-warning); }
.ins-rule__text { font-size: 12.5px; line-height: 1.45; color: var(--color-text); }

/* Лента дня: горизонтальный степпер, на узких экранах — скролл со snap. */
.ins-day__track {
  list-style: none; margin: 4px 0 10px; padding: 4px 2px 8px;
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(150px, 1fr); gap: 8px;
  overflow-x: auto; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
}
.ins-stop { scroll-snap-align: start; display: grid; }
.ins-stop__btn {
  position: relative; display: grid; gap: 3px; justify-items: start; text-align: left;
  padding: 12px 14px 14px; border-radius: var(--radius-lg);
  border: 1px solid var(--color-border); background: var(--color-surface-soft);
  font: inherit; color: inherit; cursor: pointer;
  transition: border-color .15s var(--ease), transform .15s var(--ease), box-shadow .15s var(--ease);
}
.ins-stop__btn--static { cursor: default; }
.ins-stop__btn:not(.ins-stop__btn--static):hover {
  border-color: var(--color-primary); transform: translateY(-2px); box-shadow: var(--shadow-sm);
}
.ins-stop__btn:focus-visible { outline: none; box-shadow: var(--ring); }
.ins-stop__time {
  font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em;
  color: var(--color-primary-dark);
}
/* Точка + соединительная линия к следующей остановке. */
.ins-stop__dot {
  width: 10px; height: 10px; border-radius: 50%; margin: 3px 0;
  background: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-soft);
}
.ins-stop { position: relative; }
.ins-stop:not(:last-child)::after {
  content: ""; position: absolute; top: 44px; right: -8px; width: 8px; height: 2px;
  background: var(--color-border-strong);
}
.ins-stop__title { font-size: 13.5px; color: var(--color-heading); }
.ins-stop__text { font-size: 12px; color: var(--color-muted); line-height: 1.4; }
.ins-day__hint { font-size: 12px; margin: 0; }

/* Карточки-гиды: <details> без маркера, плавное появление контента. */
.ins-list { display: grid; gap: 12px; margin-bottom: 18px; }
.ins-card {
  border-radius: var(--radius-xl); border: 1px solid var(--color-border);
  background: var(--color-surface); box-shadow: var(--shadow-card);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.ins-card[open] { border-color: color-mix(in srgb, var(--color-primary) 42%, var(--color-border)); box-shadow: var(--shadow-soft); }
.ins-card--flash { box-shadow: var(--ring-soft), var(--shadow-soft); }
.ins-card__head {
  display: flex; align-items: center; gap: 14px; padding: 15px 18px;
  cursor: pointer; list-style: none; border-radius: inherit; user-select: none;
}
.ins-card__head::-webkit-details-marker { display: none; }
.ins-card__head:focus-visible { outline: none; box-shadow: var(--ring); }
/* Без пастельной подложки: карточек-гидов в списке 10-20, и одинаковый
   сквиркл у каждой давал тот же орнамент, что и на странице отчётов. */
.ins-card__icon {
  flex: 0 0 30px; width: 30px; height: 30px; display: grid; place-items: center;
  color: var(--color-primary);
}
.ins-card__icon svg { width: 28px; height: 28px; }
.ins-card__heading { flex: 1; min-width: 0; display: grid; gap: 2px; }
.ins-card__title { font-size: 15px; color: var(--color-heading); letter-spacing: -0.01em; }
.ins-card__tagline { font-size: 12.5px; color: var(--color-muted); }
.ins-card__meta { display: flex; align-items: center; gap: 10px; }
.ins-card__count { font-size: 12px; color: var(--color-faint); white-space: nowrap; }
.ins-card__chev { display: grid; place-items: center; color: var(--color-faint); transition: transform .2s var(--ease); }
.ins-card__chev svg { width: 18px; height: 18px; }
.ins-card[open] .ins-card__chev { transform: rotate(180deg); }
.ins-card__check {
  display: none; width: 22px; height: 22px; border-radius: 50%;
  place-items: center; color: #fff; background: var(--color-success);
}
.ins-card__check svg { width: 13px; height: 13px; }
.ins-card.is-read .ins-card__check { display: grid; }
.ins-card.is-read .ins-card__icon { color: var(--color-success); background: var(--color-success-soft); }

.ins-card__body { padding: 2px 18px 18px; animation: ins-body-in .28s var(--ease-out) both; }
@keyframes ins-body-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }

.ins-callout {
  margin: 8px 0 14px; padding: 11px 14px; border-radius: var(--radius-lg);
  font-size: 13px; line-height: 1.5;
  border: 1px solid color-mix(in srgb, var(--color-danger) 34%, var(--color-border));
  background: var(--color-danger-soft);
}
.ins-callout--rule b { color: var(--color-danger); }
.ins-callout--tip {
  margin: 14px 0; border-color: color-mix(in srgb, var(--color-info) 32%, var(--color-border));
  background: var(--color-info-soft);
}
.ins-callout--tip b { color: var(--color-primary-dark); }

.ins-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.ins-step { position: relative; display: flex; gap: 14px; padding: 8px 2px; }
/* Вертикальная нить между номерами шагов. */
.ins-step:not(:last-child)::before {
  content: ""; position: absolute; left: 15px; top: 40px; bottom: -6px; width: 2px;
  background: var(--color-border);
}
.ins-step__num {
  flex: 0 0 28px; width: 28px; height: 28px; display: grid; place-items: center;
  border-radius: 50%; font-size: 13px; font-weight: 800;
  color: var(--color-primary-dark); background: var(--color-primary-soft);
  border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
}
.ins-step__content { display: grid; gap: 2px; padding-top: 3px; }
.ins-step__content strong { font-size: 13.5px; color: var(--color-heading); }
.ins-step__content p { margin: 0; font-size: 13px; color: var(--color-muted); line-height: 1.5; }

.ins-card__actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.ins-card.is-read [data-guide-read] {
  background: var(--color-success-soft); color: var(--color-success);
  border-color: color-mix(in srgb, var(--color-success) 40%, var(--color-border));
  box-shadow: none; background-image: none;
}

.ins-help { margin-bottom: 18px; }
.ins-help__row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.ins-help__title { display: block; font-size: 15px; margin-bottom: 3px; color: var(--color-heading); }
.ins-help__row .cell-muted { margin: 0; max-width: 60ch; }

/* Скриншоты в гидах (stage 46): «браузерная» рамка с точками, телефонная —
   для мобильных кадров кабинета. Клик открывает лайтбокс (initGuide). */
.ins-shots { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 14px; }
.ins-shot { margin: 0; flex: 1 1 380px; min-width: 260px; max-width: 560px; }
.ins-shot--phone { flex: 0 1 240px; min-width: 200px; }
.ins-shot__btn {
  display: block; width: 100%; padding: 0; border: 0; background: none;
  cursor: zoom-in; border-radius: 12px;
}
.ins-shot__btn:focus-visible { outline: none; box-shadow: var(--ring); }
.ins-shot__frame {
  display: block; overflow: hidden; border-radius: 12px;
  border: 1px solid var(--color-border); background: var(--color-surface);
  box-shadow: var(--shadow-soft);
  transition: transform .18s var(--ease), box-shadow .18s var(--ease);
}
.ins-shot__btn:hover .ins-shot__frame { transform: translateY(-2px); box-shadow: var(--shadow-pop, var(--shadow-soft)); }
.ins-shot__bar {
  display: flex; gap: 5px; padding: 8px 10px;
  border-bottom: 1px solid var(--color-border);
  background: color-mix(in srgb, var(--color-border) 26%, var(--color-surface));
}
.ins-shot__bar i { width: 8px; height: 8px; border-radius: 50%; background: var(--color-faint); opacity: .55; }
.ins-shot__img { display: block; width: 100%; height: auto; }
/* Скриншот следует активной теме: тёмный вариант показываем только при
   data-theme="dark" (тему ставит инлайн-скрипт base.html, не media query). */
.ins-shot__img--dark { display: none; }
[data-theme="dark"] .ins-shot__img--light { display: none; }
[data-theme="dark"] .ins-shot__img--dark { display: block; }
.ins-shot__caption { margin-top: 7px; font-size: 12.5px; color: var(--color-muted); line-height: 1.45; }

@media (max-width: 760px) {
  .ins-hero { padding: 20px; gap: 16px; }
  .ins-hero__title { font-size: 22px; }
  .ins-hero__rules { flex-basis: 100%; }
  .ins-day__track { grid-auto-columns: minmax(168px, 72%); }
  .ins-card__head { padding: 13px 14px; gap: 11px; }
  .ins-card__count { display: none; }
  .ins-card__body { padding: 0 14px 16px; }
}

/* === Зачёты (offsets): drag&drop загрузка PDF-заявлений =================== */
.offset-dropzone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; padding: 34px 22px; text-align: center; cursor: pointer;
  border: 2px dashed color-mix(in srgb, var(--color-primary) 38%, var(--color-border));
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-primary) 4%, var(--color-surface));
  transition: border-color .18s var(--ease), background .18s var(--ease), transform .18s var(--ease);
}
.offset-dropzone:hover,
.offset-dropzone:focus-visible { border-color: var(--color-primary); background: var(--color-primary-soft); }
.offset-dropzone.is-dragover {
  border-color: var(--color-primary); border-style: solid;
  background: var(--color-primary-soft); transform: scale(1.01);
}
.offset-dropzone.is-uploading { opacity: .65; pointer-events: none; }
.offset-dropzone__icon {
  width: 44px; height: 44px; border-radius: var(--radius-md);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
}
.offset-dropzone__icon svg { width: 22px; height: 22px; }
.offset-dropzone__title { font-weight: 700; }
.offset-dropzone__hint { font-size: 12.5px; color: var(--color-muted); }
.offset-dropzone input[type="file"] {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
/* Импорт JSON расширения Турбо-ПВЗ: реюз дропзоны + локальный предпросмотр */
.bundle-dropzone.is-ready {
  border-color: color-mix(in srgb, var(--color-success) 55%, var(--color-border));
  background: color-mix(in srgb, var(--color-success) 6%, var(--color-surface));
}
.bundle-dropzone.is-ready .offset-dropzone__icon {
  background: var(--color-success-soft); color: var(--color-success);
}
.bundle-dropzone.is-invalid {
  border-color: color-mix(in srgb, var(--color-danger) 55%, var(--color-border));
  background: color-mix(in srgb, var(--color-danger) 5%, var(--color-surface));
}
.bundle-dropzone.is-invalid .offset-dropzone__icon {
  background: var(--color-danger-soft); color: var(--color-danger);
}
/* display:flex на классах перебивает UA-правило [hidden] — возвращаем его явно. */
.bundle-check[hidden], .bundle-check__stats[hidden], .bundle-stat[hidden] { display: none; }
.bundle-check {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 16px; border-radius: var(--radius-md);
  border: 1px solid var(--color-border); background: var(--color-surface-alt, var(--color-surface));
}
.bundle-check--ok { border-color: color-mix(in srgb, var(--color-success) 35%, var(--color-border)); }
.bundle-check--error { border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border)); }
.bundle-check__file { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.bundle-check__name { font-weight: 700; word-break: break-all; }
.bundle-check__meta { font-size: 12.5px; }
.bundle-check__stats { display: flex; gap: 10px; flex-wrap: wrap; }
.bundle-stat {
  display: inline-flex; align-items: baseline; gap: 6px; padding: 7px 12px;
  border-radius: 999px; background: var(--color-primary-soft);
  color: var(--color-primary-dark); font-size: 13px;
}
.bundle-stat b { font-size: 15px; font-weight: 800; }
.bundle-stat--warn { background: var(--color-warning-soft, var(--color-coral-soft)); color: var(--color-warning, var(--color-coral)); }
.bundle-check__error { margin: 0; color: var(--color-danger); font-weight: 600; }

/* === Импорты: единая зона загрузки (JSON расширения + PDF OZON) ========== */
.dropbay { display: flex; flex-direction: column; gap: 12px; }
.dropbay__zone {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px; padding: 40px 24px; text-align: center; cursor: pointer;
  border: 2px dashed color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
  border-radius: var(--radius-lg);
  background:
    radial-gradient(120% 100% at 50% 0%, color-mix(in srgb, var(--color-primary) 7%, transparent), transparent 70%),
    var(--color-surface);
  transition: border-color .18s var(--ease), background .18s var(--ease), transform .18s var(--ease);
}
.dropbay__zone:hover,
.dropbay__zone:focus-visible { border-color: var(--color-primary); }
.dropbay__zone.is-dragover {
  border-color: var(--color-primary); border-style: solid; transform: scale(1.008);
  background: var(--color-primary-soft);
}
.dropbay__zone.is-ready {
  border-style: solid;
  border-color: color-mix(in srgb, var(--color-success) 50%, var(--color-border));
  background: color-mix(in srgb, var(--color-success) 5%, var(--color-surface));
}
.dropbay__zone.is-ready .dropbay__icon { background: var(--color-success-soft); color: var(--color-success); }
.dropbay__zone.is-invalid {
  border-style: solid;
  border-color: color-mix(in srgb, var(--color-danger) 50%, var(--color-border));
  background: color-mix(in srgb, var(--color-danger) 5%, var(--color-surface));
}
.dropbay__zone.is-invalid .dropbay__icon { background: var(--color-danger-soft); color: var(--color-danger); }
.dropbay__zone.is-uploading { opacity: .6; pointer-events: none; }
.dropbay__icon {
  width: 48px; height: 48px; border-radius: var(--radius-md);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  transition: background .18s var(--ease), color .18s var(--ease);
}
.dropbay__icon svg { width: 23px; height: 23px; }
/* overflow-wrap: anywhere, а не word-break: break-all — последний рвал ЛЮБОЕ
   слово по слогам, и на телефоне подпись читалась как «нажмите, что бы
   выбрать». Здесь перенос нужен только для длинных имён файлов (заголовок
   становится именем выбранного файла), а обычный текст должен переноситься по
   словам — это и делает anywhere. */
.dropbay__title { font-weight: 700; font-size: 15px; overflow-wrap: anywhere; }
.dropbay__hint { font-size: 12.5px; color: var(--color-muted); }
.dropbay__kinds { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 6px; }
.dropbay__kind {
  display: inline-flex; align-items: baseline; gap: 6px; padding: 5px 11px;
  border-radius: 999px; border: 1px solid var(--color-border);
  background: var(--color-surface-alt, var(--color-surface));
  font-size: 12px; color: var(--color-muted);
}
.dropbay__kind b { font-weight: 800; color: var(--color-heading); letter-spacing: .02em; }
.dropbay__zone input[type="file"] {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
/* display:flex на классах перебивает UA-правило [hidden] — возвращаем явно. */
.dropbay__stage[hidden], .dropbay__opt[hidden], .dropbay__error[hidden] { display: none; }
.dropbay__stage {
  display: flex; flex-direction: column; gap: 12px;
  padding: 14px 16px; border-radius: var(--radius-md);
  border: 1px solid var(--color-border); background: var(--color-surface);
}
.dropbay__files { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.dropbay__file { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: 13px; }
.dropbay__file.is-bad .dropbay__file-meta { color: var(--color-danger); font-weight: 600; }
.dropbay__badge {
  flex: none; padding: 3px 8px; border-radius: 6px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .05em;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
}
.dropbay__badge--pdf { background: var(--color-coral-soft, var(--color-warning-soft)); color: var(--color-coral, var(--color-warning)); }
.dropbay__file-name { font-weight: 600; word-break: break-all; }
.dropbay__file-meta { color: var(--color-muted); font-size: 12.5px; }
.dropbay__error { margin: 0; color: var(--color-danger); font-weight: 600; font-size: 13px; }
.dropbay__opt {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 13px; border-radius: var(--radius-md);
  background: var(--color-surface-alt, var(--color-primary-soft));
}
.dropbay__opt-label { font-size: 13px; font-weight: 600; }
.dropbay__opt-select {
  padding: 6px 10px; border-radius: 8px; font: inherit; font-size: 13px;
  border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-text);
}
.dropbay__opt-hint { flex-basis: 100%; font-size: 12px; color: var(--color-muted); }
.dropbay__go { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
@media (max-width: 640px) {
  .dropbay__zone { padding: 30px 16px; }
  .dropbay__kinds { flex-direction: column; align-items: center; }
  .dropbay__go { justify-content: stretch; }
  .dropbay__go .btn { flex: 1; }
}

/* === Импорт: выбор «что применить» (.impsel) =============================
   Разделы разобранного файла с галочками. Чекбоксы намеренно нативные (как в
   .kpi-check-col): кастомный .checkbox прячет input под opacity:0, а здесь
   галочек могут быть сотни — их надо видеть и попадать в них пальцем. */
.impsel { display: flex; flex-direction: column; gap: 10px; }
/* Сводка «что снято» — одной строкой: перенос по словам рвал её надвое. */
.impsel__total { flex: none; align-self: flex-start; white-space: nowrap; font-size: 13px; }
.impsel__group {
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface);
  transition: border-color .16s var(--ease), opacity .16s var(--ease);
}
/* Снятый раздел гасится целиком — сразу видно, что он не поедет в систему. */
.impsel__group.is-off { opacity: .55; border-style: dashed; }
.impsel__head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 11px 14px;
}
.impsel__toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; }
.impsel__toggle input[type="checkbox"] {
  width: 18px; height: 18px; accent-color: var(--color-primary); cursor: pointer;
}
.impsel__name { font-weight: 700; font-size: 14.5px; color: var(--color-heading); }
.impsel__meta {
  margin-left: auto; display: inline-flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--color-muted);
}
.impsel__rows { border-top: 1px solid var(--color-border); }
.impsel__rows > summary {
  padding: 9px 14px; cursor: pointer; font-size: 13px; color: var(--color-muted);
  user-select: none;
}
.impsel__rows > summary:hover { color: var(--color-primary); }
.impsel__bulk {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 0 14px 8px;
}
.impsel__count { font-size: 12.5px; color: var(--color-muted); margin-left: auto; }
.impsel__list {
  list-style: none; margin: 0; padding: 0 8px 8px;
  max-height: 380px; overflow-y: auto;
}
.impsel__row + .impsel__row { border-top: 1px solid var(--color-border-soft, var(--color-border)); }
.impsel__pick {
  display: flex; align-items: center; gap: 10px; padding: 7px 6px; cursor: pointer;
}
.impsel__pick input[type="checkbox"] {
  flex: none; width: 16px; height: 16px; accent-color: var(--color-primary); cursor: pointer;
}
.impsel__label { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.impsel__title { display: inline-flex; align-items: center; gap: 7px; font-size: 13.5px; }
.impsel__sub {
  font-size: 12.5px; color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.impsel__note { margin-left: auto; flex: none; font-size: 12.5px; color: var(--color-muted); }
.impsel__row.is-off .impsel__label { opacity: .5; text-decoration: line-through; }
.impsel__more, .impsel__hint { margin: 0; padding: 0 14px 12px; font-size: 12.5px; }
/* Фильтр периода претензий: одна строка «применить за <месяц>». Живёт и внутри
   раздела карточки, и отдельной формой на применённой загрузке. */
.impsel__period {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 14px; border-top: 1px solid var(--color-border);
}
.impsel__period-pick {
  display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 13px; color: var(--color-muted);
}
.impsel__period-pick .input { width: auto; min-width: 200px; min-height: var(--control-h-sm); }
.impsel__period-hint { flex-basis: 100%; margin: 0; font-size: 12.5px; }
/* Строку, не попавшую в выбранный месяц, прячем целиком: она всё равно уедет с
   формой (решение принимает сервер), но глазами её выбирать уже незачем. */
.impsel__row.is-out { display: none; }
@media (max-width: 640px) {
  .impsel__note { margin-left: 0; }
  .impsel__pick { flex-wrap: wrap; }
  .impsel__period-pick .input { width: 100%; min-width: 0; }
}

/* Интеграции: плашка «вышла новая сборка расширения» (висит до подтверждения).
   Оформление то же, что у плашки «укажите почту» (.mail-cta): это тоже дело,
   которое человек обязан сделать сам, — только не срочное, поэтому янтарь. */
.ext-update {
  display: grid; grid-template-columns: auto 1fr; gap: 12px 16px;
  background: color-mix(in srgb, var(--color-warning) 7%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-warning) 30%, var(--color-border));
  border-left: 4px solid var(--color-warning);
}
.ext-update__icon {
  align-self: start; margin-top: 2px;
  flex: none; width: 38px; height: 38px; border-radius: 11px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-warning) 18%, transparent);
  color: var(--color-warning);
}
.ext-update__icon svg { width: 20px; height: 20px; }
.ext-update__body {
  display: flex; flex-wrap: wrap; gap: 12px 18px;
  align-items: center; justify-content: space-between;
}
.ext-update__title { margin: 0 0 4px; font-size: 16px; font-weight: 800; color: var(--color-heading); }
.ext-update__text { margin: 0; font-size: 13px; color: var(--color-muted); max-width: 62ch; }
.ext-update__notes { margin: 8px 0 0; padding-left: 18px; display: grid; gap: 4px; }
.ext-update__notes li { font-size: 13px; color: var(--color-muted); }
.ext-update__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ext-update__actions form { margin: 0; }
.ext-version { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

/* Интеграции: пошаговая установка расширения Турбо-ПВЗ */
.ext-steps {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px; margin: 14px 0;
}
@media (max-width: 760px) { .ext-steps { grid-template-columns: 1fr; } }
.ext-step {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 13px 14px; border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: color-mix(in srgb, var(--color-primary) 3%, var(--color-surface));
}
.ext-step__num {
  flex: none; width: 28px; height: 28px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-weight: 800; font-size: 14px;
}
.ext-step__title { font-weight: 700; display: block; margin-bottom: 3px; }
.ext-step p { margin: 0; font-size: 13px; color: var(--color-muted); }
.ext-step code { font-size: 12.5px; }
.ext-step .btn-sm { margin-left: 6px; vertical-align: middle; }

/* Интеграции: адреса страницы расширений по браузерам */
.ext-browsers { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }
.ext-browser {
  padding: 11px 14px; border-radius: var(--radius-md);
  border: 1px solid var(--color-border); background: var(--color-surface);
}
.ext-browser__head {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: 6px 14px;
}
.ext-browser__name { font-weight: 700; }
.ext-browser__url { display: inline-flex; align-items: center; white-space: nowrap; }
.ext-browser__url code { font-size: 12.5px; }
.ext-browser__url .btn-sm { margin-left: 6px; }
.ext-browser__hint { margin: 4px 0 0; font-size: 13px; color: var(--color-muted); }

.ext-note {
  margin-top: 14px; padding: 12px 14px; border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: color-mix(in srgb, var(--color-primary) 4%, var(--color-surface));
}
.ext-note--muted { background: var(--color-surface-soft); }
.ext-note__title { display: block; font-weight: 700; margin-bottom: 6px; }
.ext-note ul { margin: 0; padding-left: 18px; display: grid; gap: 4px; }
.ext-note li,
.ext-note p { font-size: 13px; color: var(--color-muted); }
.ext-note p { margin: 8px 0 0; }
.ext-note code { font-size: 12.5px; }

/* Агент ПВЗ: карточка точки со статусом сигнала и кнопкой скачивания */
/* Чипы фильтра по установке (Все / Не установлено / Установлено). */
.agent-statefilter { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.agent-chip {
  display: inline-flex; align-items: center; gap: 7px;
  height: var(--control-h-sm); padding: 0 12px; border-radius: 999px;
  border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-text); font-weight: 650; font-size: 13px; cursor: pointer;
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.agent-chip:hover { border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-border)); }
.agent-chip.is-active {
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface));
  color: var(--color-primary-dark);
}
.agent-chip__n {
  min-width: 18px; padding: 0 5px; border-radius: 999px; text-align: center;
  font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, var(--color-muted) 16%, transparent);
  color: var(--color-muted);
}
.agent-chip.is-active .agent-chip__n {
  background: var(--color-primary); color: var(--color-on-primary);
}
/* «Не установлено: 0» — гасим счётчик, чтобы зелёная картина читалась сразу. */
.agent-chip.is-empty .agent-chip__n { background: var(--color-success-soft); color: var(--color-success); }
.agent-filter { margin-top: 14px; }
.agent-filter .input { width: 100%; max-width: 420px; }
/* Скрытая фильтром карточка обязана исчезать: grid-элементы игнорируют [hidden]
   без явного display:none (та же ловушка, что и в остальных сетках). */
.agent-card[hidden] { display: none; }
.agent-grid {
  list-style: none; margin: 14px 0 0; padding: 0;
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.agent-card {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px; border-radius: var(--radius-md);
  border: 1px solid var(--color-border); background: var(--color-surface);
}
/* Светофор установки — просьба заказчика «прям зелёная или красная рамка»:
   картина сети должна читаться с расстояния, без чтения бейджей. */
.agent-card--installed {
  border: 2px solid color-mix(in srgb, var(--color-success) 65%, transparent);
  background: color-mix(in srgb, var(--color-success) 4%, var(--color-surface));
}
.agent-card--missing {
  border: 2px solid color-mix(in srgb, var(--color-coral) 65%, transparent);
  background: color-mix(in srgb, var(--color-coral) 4%, var(--color-surface));
}
.agent-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.agent-card__code { display: block; font-weight: 700; }
.agent-card__name { display: block; font-size: 13px; color: var(--color-muted); }
.agent-card__meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0; }
.agent-card__meta dt { font-size: 12px; color: var(--color-muted); }
.agent-card__meta dd { margin: 2px 0 0; font-size: 13px; font-weight: 600; }
.agent-card__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: auto; }
.agent-card__actions form { margin: 0; }
.agent-card__hint { margin: 0; font-size: 12px; color: var(--color-muted); }

/* Пилюли типов строк в детальной */
.offset-kind { display: inline-flex; align-items: center; gap: 6px; font-weight: 650; }
.offset-kind--claim { color: var(--color-primary-dark); }
.offset-kind--fine { color: var(--color-coral); }
/* Детальная отчёта по оборотам: раскрывающиеся блоки ПВЗ на <details> */
.turnover-pvz { border-top: 1px solid var(--color-border); }
.turnover-pvz:first-of-type { border-top: 0; }
.turnover-pvz__head {
  display: flex; align-items: center; gap: 12px; padding: 12px 6px;
  cursor: pointer; list-style: none; border-radius: var(--radius-md);
}
.turnover-pvz__head::-webkit-details-marker { display: none; }
.turnover-pvz__head:hover { background: color-mix(in srgb, var(--color-primary) 5%, transparent); }
.turnover-pvz__chevron { color: var(--color-muted); flex: none; transition: transform .18s var(--ease); }
.turnover-pvz__chevron svg { width: 16px; height: 16px; display: block; }
.turnover-pvz[open] > .turnover-pvz__head .turnover-pvz__chevron { transform: rotate(90deg); }
.turnover-pvz__title { font-weight: 650; }
.turnover-pvz__address { font-size: 12.5px; color: var(--color-muted); }
.turnover-pvz__meta { margin-left: auto; text-align: right; white-space: nowrap; }
.turnover-pvz__total { font-weight: 700; }
.turnover-pvz__total--negative { color: var(--color-coral); }
.turnover-pvz__count { font-size: 12px; color: var(--color-muted); }
.turnover-pvz__body { padding: 0 6px 14px; }
.turnover-pvz__prev { margin: 0 0 10px; font-size: 12.5px; }
/* Мини-бар доли ПВЗ в вознаграждении сети (в правой колонке summary) */
.turnover-pvz__share {
  display: block; width: 96px; height: 4px; margin: 5px 0 0 auto;
  border-radius: 999px; background: color-mix(in srgb, var(--color-primary) 14%, transparent);
  overflow: hidden;
}
.turnover-pvz__share span { display: block; height: 100%; width: var(--w, 0%); border-radius: inherit; background: var(--color-primary); }

/* Аналитика отчёта по оборотам: бейджи роста, вердикт периода, сравнение ПВЗ */
.tw-period { color: var(--color-primary-dark); font-weight: 700; }
.delta-chip {
  display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px;
  font-size: 12px; font-weight: 700; line-height: 1.4; white-space: nowrap;
  font-variant-numeric: tabular-nums; vertical-align: middle;
}
.delta-chip--up { background: var(--color-success-soft); color: var(--color-success); }
.delta-chip--down { background: var(--color-coral-soft); color: var(--color-coral); }
.delta-chip--flat { background: color-mix(in srgb, var(--color-muted) 14%, transparent); color: var(--color-muted); }
.metric-card__valrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
/* Как и голое значение, ряд «значение + бейдж» прижат к низу карточки. */
.metric-card > .metric-card__valrow { margin-top: auto; }
.metric-card__valrow .metric-card__value { --metric-value-size: 21px; overflow-wrap: anywhere; }
.metric-card__value--pvz { --metric-value-size: 17px; }

.tw-verdict { border-left: 3px solid var(--color-primary); }
.tw-verdict--up { border-left-color: var(--color-success); }
.tw-verdict--down { border-left-color: var(--color-coral); }
.tw-verdict--flat { border-left-color: var(--color-muted); }
/* Отчёт за часть месяца: не рост и не падение — предупреждение о данных. */
.tw-verdict--partial { border-left-color: var(--color-warning); }
.tw-verdict__head { display: flex; align-items: center; gap: 14px; }
.tw-verdict__icon {
  width: 44px; height: 44px; flex: none; border-radius: var(--radius-md);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
}
.tw-verdict__icon svg { width: 22px; height: 22px; }
.tw-verdict--up .tw-verdict__icon { background: var(--color-success-soft); color: var(--color-success); }
.tw-verdict--down .tw-verdict__icon { background: var(--color-coral-soft); color: var(--color-coral); }
.tw-verdict--partial .tw-verdict__icon { background: var(--color-warning-soft); color: var(--color-warning); }
.tw-verdict__title { margin: 0; font-size: 17px; font-weight: 750; letter-spacing: -0.01em; }
.tw-verdict__sub { margin: 2px 0 0; font-size: 12.5px; color: var(--color-muted); }
.tw-verdict__insights { margin: 12px 0 0; padding: 0; list-style: none; display: grid; gap: 7px; }
.tw-verdict__insights li {
  position: relative; padding-left: 18px; font-size: 13.5px; line-height: 1.45;
}
.tw-verdict__insights li::before {
  content: ""; position: absolute; left: 4px; top: 8px; width: 6px; height: 6px;
  border-radius: 50%; background: color-mix(in srgb, var(--color-primary) 55%, transparent);
}

.tw-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 28px; }
.tw-compare__title { margin: 0 0 4px; font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--color-muted); }
.tw-compare__row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px dashed var(--color-border); }
.tw-compare__row:first-of-type { border-top: 0; }
.tw-compare__name { flex: none; max-width: 38%; font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tw-compare__bar { flex: 1; height: 6px; border-radius: 999px; background: color-mix(in srgb, var(--color-muted) 12%, transparent); overflow: hidden; }
.tw-compare__bar span { display: block; height: 100%; width: var(--w, 0%); border-radius: inherit; }
.tw-compare__col--up .tw-compare__bar span { background: var(--color-success); }
.tw-compare__col--down .tw-compare__bar span { background: var(--color-coral); }
.tw-compare__delta { font-size: 13px; font-weight: 650; white-space: nowrap; font-variant-numeric: tabular-nums; }
.tw-compare__col--down .tw-compare__delta { color: var(--color-coral); }
.tw-compare__col--up .tw-compare__delta { color: var(--color-success); }
.tw-compare__footer { margin: 12px 0 0; font-size: 12.5px; }
@media (max-width: 819px) {
  .tw-compare { grid-template-columns: 1fr; }
  .tw-compare__col--down .tw-compare__title { margin-top: 12px; }
}

/* Сверка отчёта OZON со справочником ПВЗ. «Нет в системе» — деньги, которые ни к
   чему не привязаны, поэтому у группы коралловая рамка; «нет в отчёте» —
   информационная, приглушённая. */
.tw-recon { border-left: 3px solid var(--color-warning, var(--color-coral)); }
.tw-recon__group { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--color-border); }
.tw-recon__group:first-of-type { margin-top: 4px; padding-top: 0; border-top: 0; }
.tw-recon__title { margin: 0 0 4px; font-size: 14px; font-weight: 650; }
.tw-recon__group--alert .tw-recon__title { color: var(--color-coral); }
.tw-recon__hint { margin: 0 0 10px; font-size: 12.5px; max-width: 72ch; }
.tw-recon__action { margin: 10px 0 0; }
.tw-recon__codes {
  display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none;
}
.tw-recon__codes li a {
  display: inline-block; padding: 4px 10px; border-radius: var(--radius-sm);
  background: var(--color-surface-alt); font-size: 12.5px; font-weight: 600;
  color: var(--color-text); text-decoration: none;
}
.tw-recon__codes li a:hover { background: var(--color-primary-soft); color: var(--color-primary-dark); }

/* =========================================================================
   Browser-bundle analytics (страница импорта JSON Турбо-ПВЗ)
   Hero-панель графитовая в обеих темах (как sidebar — фирменная подпись),
   всё остальное — на токенах, поэтому светлая/тёмная темы бесплатны.
   ========================================================================= */
.bundle-hero {
  position: relative; overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 320px);
  gap: 20px 36px; align-items: start;
  padding: 26px 30px; border-radius: var(--radius-2xl);
  background:
    radial-gradient(520px 260px at 12% -30%, rgba(0, 91, 255, 0.32), transparent 66%),
    radial-gradient(420px 240px at 96% 120%, rgba(241, 37, 107, 0.16), transparent 70%),
    linear-gradient(135deg, #111319 0%, #141823 58%, #101728 100%);
  border: 0;
  color: #C7CDDA;
  box-shadow: var(--shadow-soft);
}
/* В тёмной теме та же панель нейтральная: графитово-синяя плита из светлой
   темы посреди чёрного полотна читалась бы чужой вставкой. */
html[data-theme="dark"] .bundle-hero {
  background:
    radial-gradient(520px 260px at 12% -30%, rgba(25, 109, 255, 0.28), transparent 66%),
    radial-gradient(420px 240px at 96% 120%, rgba(79, 144, 255, 0.16), transparent 70%),
    linear-gradient(135deg, #141416 0%, #1B1C20 58%, #151518 100%);
  border-color: #2A2B31;
  color: #C3C4CA;
}
.bundle-hero__main { display: flex; flex-direction: column; gap: 12px; min-width: 0; align-self: center; }
.bundle-hero__eyebrow {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 12px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: #8F98AC;
}
.bundle-badge {
  display: inline-flex; align-items: center; padding: 3px 10px; border-radius: var(--radius-pill);
  font-size: 11.5px; font-weight: 700; letter-spacing: .02em; text-transform: none;
  background: rgba(255, 255, 255, 0.08); color: #B4BAC7; border: 1px solid rgba(255, 255, 255, 0.10);
}
.bundle-badge--success { background: rgba(14, 159, 93, 0.18); color: #5BD69C; border-color: rgba(14, 159, 93, 0.34); }
.bundle-badge--warning { background: rgba(199, 125, 10, 0.18); color: #E8B45C; border-color: rgba(199, 125, 10, 0.34); }
.bundle-hero__value {
  font-size: clamp(30px, calc(4vw * var(--vu)), 44px); font-weight: 800; letter-spacing: -0.03em;
  color: #FFFFFF; line-height: 1.05; font-variant-numeric: tabular-nums;
}
.bundle-hero__caption { margin: 0; font-size: 13.5px; color: #A6AFC2; }
.bundle-splitbar {
  display: flex; height: 14px; border-radius: var(--radius-pill); overflow: hidden;
  background: rgba(255, 255, 255, 0.08); margin-top: 4px;
}
.bundle-splitbar__seg { display: block; background: var(--seg); min-width: 3px; }
.bundle-splitbar__seg + .bundle-splitbar__seg { border-left: 2px solid rgba(10, 12, 18, 0.55); }
.bundle-hero__legend {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 12.5px; color: #A6AFC2;
}
.bundle-hero__legend li { display: inline-flex; align-items: center; gap: 7px; }
.bundle-hero__legend b { color: #E8EBF2; font-weight: 700; font-variant-numeric: tabular-nums; }
.bundle-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--seg, var(--color-faint)); flex: none; }
.bundle-hero__side { display: grid; gap: 10px; align-content: start; }
.bundle-stat {
  display: grid; gap: 2px; padding: 11px 14px; border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.08);
}
.bundle-stat__label { font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: #8F98AC; }
.bundle-stat__value { font-size: 16px; font-weight: 800; color: #FFFFFF; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.bundle-stat__hint { font-size: 12px; color: #A6AFC2; }
.bundle-stat__hint--alert { color: #FF7CA8; font-weight: 600; }

/* Светлая тема: графитовая плита смотрелась чужеродно (отзыв заказчика) —
   hero переходит на светлые токены, лёгкие фирменные отсветы остаются.
   Скоуп строго .bundle-hero: пилюли .bundle-stat на проверке импорта не трогаем. */
html[data-theme="light"] .bundle-hero {
  background:
    radial-gradient(520px 260px at 12% -30%, rgba(0, 91, 255, 0.08), transparent 66%),
    radial-gradient(420px 240px at 96% 120%, rgba(241, 37, 107, 0.05), transparent 70%),
    var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-text);
}
html[data-theme="light"] .bundle-hero .bundle-hero__eyebrow { color: var(--color-muted); }
html[data-theme="light"] .bundle-hero .bundle-badge { background: var(--color-surface-soft); color: var(--color-muted); border-color: var(--color-border); }
html[data-theme="light"] .bundle-hero .bundle-badge--success { background: var(--color-success-soft); color: var(--color-success); border-color: color-mix(in srgb, var(--color-success) 32%, transparent); }
html[data-theme="light"] .bundle-hero .bundle-badge--warning { background: var(--color-warning-soft); color: var(--color-warning); border-color: color-mix(in srgb, var(--color-warning) 32%, transparent); }
html[data-theme="light"] .bundle-hero__value { color: var(--color-heading); }
html[data-theme="light"] .bundle-hero__caption { color: var(--color-muted); }
html[data-theme="light"] .bundle-hero__legend { color: var(--color-muted); }
html[data-theme="light"] .bundle-hero__legend b { color: var(--color-heading); }
html[data-theme="light"] .bundle-hero .bundle-splitbar { background: var(--color-surface-soft); }
html[data-theme="light"] .bundle-hero .bundle-splitbar__seg + .bundle-splitbar__seg { border-left-color: var(--color-surface); }
html[data-theme="light"] .bundle-hero .bundle-stat { background: var(--color-surface-alt); border-color: var(--color-border); }
html[data-theme="light"] .bundle-hero .bundle-stat__label { color: var(--color-faint); }
html[data-theme="light"] .bundle-hero .bundle-stat__value { color: var(--color-heading); }
html[data-theme="light"] .bundle-hero .bundle-stat__hint { color: var(--color-muted); }
html[data-theme="light"] .bundle-hero .bundle-stat__hint--alert { color: var(--color-coral); }

/* Charts row inherits .dash-cards; only cap donut ring growth inside. */
.bundle-charts { margin-top: 2px; }
/* «Ровные» графики Расчётов OZON (просьба заказчика, 22.07.2026): в ряду
   донаты сидят на одной высоте. Два слагаемых разнобоя: (1) подзаголовки
   переносятся на разное число строк — резервируем две строки; (2) глобальное
   правило центрирует кольцо в остатке карточки (margin-block: auto), и при
   короткой легенде кольцо сползает вниз — прижимаем к верху. */
.bundle-charts .card__subtitle { min-height: 3em; }
.bundle-charts .dash-widget__body > .dash-donut { margin-block: 0; }

/* ---- Merged per-PVZ table ------------------------------------------------ */
.bundle-pvz__toolbar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 4px; }
.bundle-pvz__search { max-width: 300px; min-width: 190px; }
.bundle-pvz__toggle {
  display: inline-flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600;
  color: var(--color-muted); cursor: pointer; user-select: none;
}
.bundle-pvz__toggle input { accent-color: var(--color-primary); width: 15px; height: 15px; }
.bundle-pvz__count { margin-left: auto; font-size: 13px; color: var(--color-muted); font-variant-numeric: tabular-nums; }

.bundle-pvz__table th[data-bundle-sort] { cursor: pointer; user-select: none; }
.bundle-pvz__table th[data-bundle-sort]::after { content: "↕"; margin-left: 4px; opacity: .38; font-size: 11px; }
/* «Претензии по ПВЗ» — самая широкая таблица раздела (до 9 колонок, у трёх из
   них ещё и вторая строка-подпись). Заголовки пускаем в две строки, отступы и
   подписи поджимаем: с общими 260px на подпись таблица уезжала в горизонтальный
   скролл уже на ноутбуке. Подписи обрезаются многоточием, полный текст — в
   title, поэтому сужение ничего не теряет. */
.bundle-pvz__table thead th { padding: 9px 8px; white-space: normal; min-width: 0; }
.bundle-pvz__table tbody td { padding: 11px 8px; }
.bundle-pvz__table .bundle-pvz__sub { max-width: 135px; }
.bundle-pvz__table .bundle-pvz__money { min-width: 118px; }
.bundle-pvz__addr { max-width: 150px; }
/* Первая колонка липнет при горизонтальном скролле: на узком экране таблица всё
   же поедет вбок, и без этого непонятно, чья это строка. */
.bundle-pvz__table tbody .bundle-pvz__name,
.bundle-pvz__table thead th:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--color-surface);
}
.bundle-pvz__table thead th:first-child { z-index: 3; background: var(--color-surface-alt); }
.bundle-pvz__group.is-open .bundle-pvz__row .bundle-pvz__name { background: var(--color-surface-alt); }
.bundle-pvz__table th[data-bundle-sort].is-asc::after { content: "↑"; opacity: 1; color: var(--color-primary); }
.bundle-pvz__table th[data-bundle-sort].is-desc::after { content: "↓"; opacity: 1; color: var(--color-primary); }
.bundle-pvz__group[hidden] { display: none; }
.bundle-pvz__row { cursor: pointer; }
/* Ховер снят: единое правило .table tbody tr:hover > td (см. раздел таблиц). */
.bundle-pvz__row:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }
.bundle-pvz__name { min-width: 180px; }
.bundle-pvz__name strong { display: block; }
.bundle-pvz__sub {
  display: block; font-size: 12px; color: var(--color-muted); max-width: 260px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bundle-pvz__money { min-width: 150px; }
.bundle-pvz__amount { display: block; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
.bundle-pvz__track { display: block; margin-top: 4px; height: 5px; border-radius: var(--radius-pill); background: var(--color-surface-soft); overflow: hidden; max-width: 140px; }
.bundle-pvz__fill { display: block; height: 100%; width: var(--w, 0%); border-radius: inherit; background: linear-gradient(90deg, var(--color-primary), var(--color-coral)); transition: width .5s var(--ease); }
.bundle-pvz__chev { width: 34px; color: var(--color-faint); }
.bundle-pvz__chev svg { transition: transform .18s var(--ease); }
.bundle-pvz__group.is-open .bundle-pvz__chev svg { transform: rotate(180deg); }
.bundle-pvz__group.is-open .bundle-pvz__row td { background: var(--color-surface-alt); }

.bundle-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  border-radius: var(--radius-pill); font-size: 12px; font-weight: 700; white-space: nowrap;
  background: var(--color-surface-soft); color: var(--color-muted);
}
.bundle-chip b { font-variant-numeric: tabular-nums; }
.bundle-chip--clean { background: var(--color-success-soft); color: var(--color-success); }
.bundle-chip--low { background: var(--color-warning-soft); color: var(--color-warning); }
.bundle-chip--high { background: var(--color-danger-soft); color: var(--color-danger); }
.bundle-chip--claims { background: var(--color-primary-soft); color: var(--color-primary); }

/* Expanded detail row */
.bundle-pvz__details[hidden] { display: none !important; }
.bundle-pvz__details > td { background: var(--color-surface-alt); padding: 14px 16px 18px; }
.bundle-detail { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 8px 28px; }
.bundle-detail__title { margin: 0 0 10px; font-size: 12px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--color-muted); }
.bundle-cats { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.bundle-cat__head { display: flex; align-items: baseline; gap: 10px; font-size: 13px; min-width: 0; }
.bundle-cat__name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bundle-cat__section { font-size: 11.5px; color: var(--color-faint); white-space: nowrap; }
.bundle-cat__value { margin-left: auto; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
.bundle-cat__track { display: block; margin-top: 4px; height: 5px; border-radius: var(--radius-pill); background: color-mix(in srgb, var(--color-muted) 14%, transparent); overflow: hidden; }
.bundle-cat__fill { display: block; height: 100%; width: var(--w, 0%); border-radius: inherit; background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark)); }
.bundle-detail__facts { margin: 0; display: grid; gap: 8px; }
.bundle-detail__facts > div { display: grid; gap: 1px; }
.bundle-detail__facts dt { font-size: 11.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--color-faint); }
.bundle-detail__facts dd { margin: 0; font-size: 13.5px; font-weight: 600; }
.bundle-detail__chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }

/* Meta card as <details> */
.bundle-meta > summary { list-style: none; cursor: pointer; display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.bundle-meta > summary::-webkit-details-marker { display: none; }
.bundle-meta > summary::after { content: "＋"; margin-left: auto; color: var(--color-faint); font-weight: 700; }
.bundle-meta[open] > summary::after { content: "−"; }
.bundle-meta__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 0 28px; margin-top: 12px; }

@media (max-width: 819px) {
  .bundle-hero { grid-template-columns: 1fr; }
  .bundle-hero__side { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
}
@media (max-width: 640px) {
  .bundle-hero { padding: 20px; }
  .bundle-detail { grid-template-columns: 1fr; }
  .bundle-pvz__count { margin-left: 0; width: 100%; }
}

/* ---- Экономика сети (offsets): месяцы, доли, доп. тона чипов ------------ */
.eco-months { margin-bottom: 4px; }
.eco-months .chip { text-decoration: none; }
.eco-months__mark { color: var(--color-warning); font-size: 11px; }
/* На выбранном чипе фон синий — оранжевая метка читалась как случайная клякса. */
.eco-months .chip.is-active .eco-months__mark { color: currentColor; opacity: .75; }
.eco-sharebar { max-width: 460px; height: 10px; }
.eco-money { font-variant-numeric: tabular-nums; white-space: nowrap; }
.eco-share { font-weight: 800; font-variant-numeric: tabular-nums; }
.eco-share--ok { color: var(--color-success); }
.eco-share--warn { color: var(--color-warning); }
.eco-share--high { color: var(--color-danger); }
.bundle-chip--ok { background: var(--color-success-soft); color: var(--color-success); }
.bundle-chip--warn { background: var(--color-warning-soft); color: var(--color-warning); }
.bundle-chip--nodata { background: var(--color-surface-soft); color: var(--color-faint); }

/* ---- Экран собственника: «Экономика сети» v2 (stage 100) ---------------
   Первый экран собственника сети: крупный итог периода, KPI-карточки со своей
   динамикой, рейтинги точек. Анимации — прогрессивное улучшение: без JS всё
   отрисовано в конечном состоянии (см. .no-js-fallback ниже — класс is-in
   ставит initEcoMotion, а при prefers-reduced-motion переходы отключены). */
.eco-hero {
  position: relative; overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(260px, 1fr); gap: 28px;
  padding: 28px 30px; margin-bottom: 16px;
  border: 1px solid var(--color-border); border-radius: var(--radius-2xl);
  background:
    radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--color-primary) 14%, transparent), transparent 60%),
    var(--color-surface);
}
.eco-hero__glow {
  position: absolute; inset: auto -80px -120px auto; width: 320px; height: 320px; border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--color-primary) 26%, transparent), transparent 70%);
  filter: blur(10px); pointer-events: none; opacity: .5;
}
.eco-hero__main, .eco-hero__side { position: relative; display: grid; gap: 12px; min-width: 0; }
/* Левая колонка тянется по высоте правой — раскладываем содержимое по ней,
   иначе под полосой «остаётся/ушло» зияет пустой прямоугольник. */
.eco-hero__main { align-content: space-between; }
.eco-hero__side { align-content: start; }
.eco-hero__head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.eco-hero__period { font-size: 12.5px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--color-muted); }
.eco-hero__source { font-size: 12.5px; color: var(--color-faint); }
.eco-hero__value {
  font-size: clamp(34px, calc(4.4vw * var(--vu)), 56px); font-weight: 800; line-height: 1.02; letter-spacing: -.02em;
  color: var(--color-heading); font-variant-numeric: tabular-nums;
}
.eco-hero__caption { margin: 0; font-size: 13.5px; color: var(--color-muted); }
/* Помесячная разбивка вознаграждения в герое: месяц, полоса, сумма, дельта.
   Линейный график здесь отвергнут заказчиком дважды — цифры по месяцам
   читаются однозначно и ничего не «рвётся» при масштабировании. */
.eco-mrows { display: grid; gap: 8px; }
.eco-mrows__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.eco-mrows__title { font-size: 12px; font-weight: 700; color: var(--color-muted); }
.eco-mrows__hint { font-size: 11px; color: var(--color-faint); }
.eco-mrows__list { display: grid; gap: 6px; list-style: none; margin: 0; padding: 0; }
.eco-mrow {
  display: grid; grid-template-columns: 92px minmax(40px, 1fr) auto 62px;
  gap: 10px; align-items: center; padding: 6px 8px; border-radius: var(--radius-sm);
  font-size: 12.5px; transition: background .16s var(--ease);
}
.eco-mrow:hover { background: var(--color-surface-soft); }
.eco-mrow__month { color: var(--color-muted); white-space: nowrap; }
/* ◐ — месяц закрыт отчётом не целиком (сравнение по нему не считается). */
.eco-mrow__partial { margin-left: 4px; color: var(--color-warning); font-size: 11px; }
.eco-mrow__track { display: block; height: 8px; border-radius: 999px; background: var(--color-surface-soft); overflow: hidden; }
.eco-mrow__fill { display: block; height: 100%; border-radius: inherit; background: color-mix(in srgb, var(--color-primary) 55%, transparent); transform-origin: left center; }
.eco-mrow--best .eco-mrow__fill { background: var(--color-primary); }
.eco-mrow__value { font-weight: 800; color: var(--color-text); font-variant-numeric: tabular-nums; white-space: nowrap; }
.eco-mrow__delta { font-size: 12px; text-align: right; }

/* Полоса «сколько вознаграждения дошло до сети». */
.eco-flow { display: grid; gap: 10px; margin-top: 6px; }
.eco-flow__bar { display: flex; height: 14px; border-radius: 999px; overflow: hidden; background: var(--color-surface-soft); }
.eco-flow__seg { display: block; height: 100%; transform-origin: left center; }
.eco-flow__seg--success { background: var(--color-success); }
.eco-flow__seg--coral { background: var(--color-danger); }
.eco-flow__legend { display: flex; flex-wrap: wrap; gap: 8px 22px; list-style: none; margin: 0; padding: 0; font-size: 13px; color: var(--color-muted); }
.eco-flow__legend b { color: var(--color-text); font-variant-numeric: tabular-nums; }

.eco-facts { display: grid; gap: 8px; list-style: none; margin: 0; padding: 14px 0 0; border-top: 1px solid var(--color-border); }
.eco-facts li { display: flex; gap: 8px; align-items: baseline; justify-content: space-between; font-size: 13px; }
.eco-facts span { color: var(--color-muted); }
.eco-facts b { color: var(--color-text); font-variant-numeric: tabular-nums; text-align: right; }

/* KPI-карточки: значение + подпись + собственная мини-динамика.
   Колонок ровно 6 / 3 / 2 — карточек всегда шесть (_summary_cards), и при
   auto-fit на ноутбуке ряд разваливался на 5 + одинокую шестую «в подвале».
   Делители шести дают полные ряды на любой ширине. */
.eco-kpis { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
@media (max-width: 1200px) { .eco-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 560px) { .eco-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.eco-kpi {
  position: relative; overflow: hidden; display: grid; gap: 4px; align-content: start;
  padding: 16px 18px 10px; border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface); transition: transform .18s var(--ease), box-shadow .18s var(--ease), border-color .18s var(--ease);
}
.eco-kpi:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--color-border-strong); }
.eco-kpi::after { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--color-border-strong); }
.eco-kpi--primary::after { background: var(--color-primary); }
.eco-kpi--success::after { background: var(--color-success); }
.eco-kpi--warning::after { background: var(--color-warning); }
.eco-kpi--danger::after { background: var(--color-danger); }
.eco-kpi__label { font-size: 12px; font-weight: 700; color: var(--color-muted); }
.eco-kpi__value { font-size: 24px; font-weight: 800; line-height: 1.15; color: var(--color-heading); font-variant-numeric: tabular-nums; }
.eco-kpi__sub { font-size: 12px; color: var(--color-faint); min-height: 30px; }
/* Мини-динамика карточки столбиками: последний месяц ярче остальных. */
/* Ширина столбика фиксированная: при трёх месяцах растянутые на всю карточку
   прямоугольники выглядели как заплатки, а не как динамика. */
.eco-bars { display: flex; align-items: flex-end; gap: 3px; height: 34px; margin-top: 6px; }
.eco-bar { flex: 0 0 9px; border-radius: 2px 2px 0 0; background: var(--color-primary); opacity: .32; transform-origin: bottom center; }
.eco-bar--last { opacity: 1; }
.eco-kpi--danger .eco-bar { background: var(--color-danger); }
.eco-kpi--warning .eco-bar { background: var(--color-warning); }
.eco-kpi--success .eco-bar { background: var(--color-success); }

.eco-stages__bar { height: 14px; margin-bottom: 12px; }

/* Рейтинги точек: кто зарабатывает, кто теряет.
   Раскладка рядами, а не колонками: код и сумма — на одной строке, адрес под
   ними, бар — подчёркиванием во всю ширину карточки. В три колонки бар забирал
   себе фиксированные 150 px, и на ноутбуке названию оставалось ~130 px: адрес
   обрезался до «Россия, Псковская обл…», по которому точку не узнать, а сам
   бар висел в воздухе посреди карточки. */
.eco-rank { display: grid; gap: 14px; list-style: none; margin: 0; padding: 0; }
.eco-rank__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "code value" "sub sub" "bar bar";
  column-gap: 14px; row-gap: 5px; align-items: baseline;
}
.eco-rank__code { grid-area: code; min-width: 0; font-size: 13.5px; font-weight: 700; color: var(--color-heading); }
.eco-rank__code a { color: inherit; text-decoration: none; }
.eco-rank__code a:hover { color: var(--color-primary); }
.eco-rank__sub { grid-area: sub; font-size: 12px; font-weight: 500; color: var(--color-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.eco-rank__bar { grid-area: bar; align-self: center; display: block; margin-top: 2px; height: 6px; border-radius: 999px; background: var(--color-surface-soft); overflow: hidden; }
.eco-rank__fill { display: block; height: 100%; border-radius: inherit; background: var(--color-primary); transform-origin: left center; }
.eco-rank--alert .eco-rank__fill { background: var(--color-danger); }
.eco-rank__value { grid-area: value; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }

.eco-delta { font-weight: 800; font-variant-numeric: tabular-nums; }
.eco-delta--up { color: var(--color-success); }
.eco-delta--down { color: var(--color-danger); }
.eco-delta--flat { color: var(--color-faint); }

.eco-cols { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 16px; margin-bottom: 16px; align-items: start; }
.eco-cols--ranks { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.eco-cols > .card { margin-bottom: 0; }
.eco-alerts { display: grid; gap: 8px; list-style: none; margin: 0; padding: 0; }
.eco-alert { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 12px 14px; border-radius: var(--radius-md); background: var(--color-surface-soft); transition: background .16s var(--ease); }
.eco-alert:hover { background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface-soft)); }
.eco-alert__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-border-strong); }
.eco-alert--danger .eco-alert__dot { background: var(--color-danger); }
.eco-alert--warning .eco-alert__dot { background: var(--color-warning); }
.eco-alert__body { display: grid; gap: 2px; min-width: 0; }
.eco-alert__title { font-size: 13.5px; color: var(--color-heading); }
.eco-alert__sub { font-size: 12.5px; color: var(--color-muted); }
.eco-doclist { display: grid; gap: 10px; list-style: none; margin: 0 0 12px; padding: 0; }
.eco-doc { display: grid; gap: 2px; }
.eco-doc__title { font-size: 13.5px; font-weight: 700; color: var(--color-heading); text-decoration: none; }
.eco-doc__title:hover { color: var(--color-primary); }
.eco-doc__sub { font-size: 12.5px; color: var(--color-muted); }
.eco-docs__foot { font-size: 12.5px; color: var(--color-faint); margin: 0 0 12px; }
/* Обёртка [data-eco-page] — якорь для JS-анимации, интервалов она не давала.
   Верхние блоки несли собственный margin-bottom, а «Штрафы по месяцам»,
   «Разрезы за период» и «Претензии по ПВЗ» стояли встык и читались одним
   слипшимся блоком — тонкая полоска «Разрезов» между двумя большими
   карточками вообще выглядела швом, а не отдельным блоком. Раскладываем
   колонкой с общим интервалом и снимаем ручные отступы у детей. */
[data-eco-page] { display: flex; flex-direction: column; gap: 16px; }
[data-eco-page] > * { margin-block: 0; }

.eco-more { padding: 16px 18px; }
/* Сводка — сам по себе блок-кнопка: стрелка вместо системного маркера и
   ховер, иначе строку между карточками не опознать как раскрывающуюся. */
.eco-more__summary {
  display: flex; align-items: center; gap: 9px; cursor: pointer;
  font-weight: 700; color: var(--color-muted); list-style: none; user-select: none;
}
.eco-more__summary::-webkit-details-marker { display: none; }
.eco-more__summary:hover { color: var(--color-heading); }
.eco-more__summary svg { width: 16px; height: 16px; flex: none; transition: transform .18s var(--ease); }
.eco-more[open] .eco-more__summary { margin-bottom: 14px; color: var(--color-heading); }
.eco-more[open] .eco-more__summary svg { transform: rotate(180deg); }
.eco-more__count { color: var(--color-faint); font-weight: 600; }
.eco-more .dash-cards { margin-bottom: 0; }

/* Появление блоков и рост полос — только когда JS проставил data-eco-motion.
   Без скрипта класс не появится, и страница остаётся статичной, но полной. */
[data-eco-motion] [data-eco-reveal] { opacity: 0; transform: translateY(14px); }
[data-eco-motion] [data-eco-reveal].is-in {
  opacity: 1; transform: none;
  transition: opacity .5s var(--ease) calc(var(--i, 0) * 60ms), transform .5s var(--ease) calc(var(--i, 0) * 60ms);
}
[data-eco-motion] .eco-flow__seg,
[data-eco-motion] .eco-rank__fill,
[data-eco-motion] .eco-mrow__fill,
[data-eco-motion] .eco-stages__bar .bundle-splitbar__seg { transform: scaleX(0); }
[data-eco-motion] .is-in .eco-flow__seg,
[data-eco-motion] .is-in .eco-rank__fill,
[data-eco-motion] .is-in .eco-mrow__fill,
[data-eco-motion] .is-in.eco-stages .bundle-splitbar__seg { transform: scaleX(1); transition: transform .8s var(--ease); }
[data-eco-motion] .eco-bar { transform: scaleY(0); }
[data-eco-motion] .is-in .eco-bar { transform: scaleY(1); transition: transform .55s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  [data-eco-motion] [data-eco-reveal],
  [data-eco-motion] .eco-flow__seg,
  [data-eco-motion] .eco-rank__fill,
  [data-eco-motion] .eco-mrow__fill,
  [data-eco-motion] .eco-stages__bar .bundle-splitbar__seg { opacity: 1; transform: none; transition: none; }
  [data-eco-motion] .eco-bar { transform: none; }
}

@media (max-width: 864px) {
  .eco-hero { grid-template-columns: 1fr; padding: 22px; }
  .eco-cols, .eco-cols--ranks { grid-template-columns: 1fr; }
}

/* ---- Вкладки раздела «Расчёты OZON» + кнопки удаления документов --------
   Здесь была лишняя закрывающая скобка (файл в целом сходился только за счёт
   неё). Из-за ошибки разбора браузер отбрасывал СЛЕДУЮЩЕЕ правило целиком —
   контейнер .eco-tabs / .hub-tabs терял flex, серую пилюлю и padding, а вкладки
   хабов рассыпались в столбик. Заметно это стало на телефоне, но ломалось везде. */
/* max-width + скролл — обязательны обоим: вкладок бывает четыре, и на телефоне
   ряд шире экрана уводил в горизонтальный скролл ВСЮ страницу. */
.eco-tabs, .hub-tabs { display: flex; gap: 4px; padding: 4px; background: var(--color-surface-soft); border-radius: var(--radius-lg); width: fit-content; max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.eco-tabs::-webkit-scrollbar, .hub-tabs::-webkit-scrollbar { display: none; }
.eco-tabs__tab, .hub-tabs__tab { flex: none; }
.eco-tabs__tab, .hub-tabs__tab {
  padding: 8px 18px; border-radius: var(--radius-md); font-size: 13.5px; font-weight: 700;
  color: var(--color-muted); text-decoration: none; transition: all .16s var(--ease); white-space: nowrap;
}
.eco-tabs__tab:hover, .hub-tabs__tab:hover { color: var(--color-text); }
.eco-tabs__tab.is-active, .hub-tabs__tab.is-active { background: var(--color-surface); color: var(--color-heading); box-shadow: var(--shadow-sm); }
/* Хаб-вкладки укрупнённого меню (07.2026): та же пилюля, но со своим нижним
   отступом и горизонтальным скроллом на узких экранах (мобильный кабинет). */
.hub-tabs { margin-bottom: 16px; max-width: 100%; overflow-x: auto; }
.doc-delete { width: 1%; white-space: nowrap; text-align: right; }
.doc-delete form { display: inline; }
.doc-delete__btn { color: var(--color-danger); }
.doc-delete__btn:hover { background: var(--color-danger-soft); color: var(--color-danger); }

/* === Импорт: карточка удаления загрузки (.impdel) ========================
   Состав удаления с числами вместо голого confirm(). У каждого типа данных своя
   кнопка справа от строки: видно, что именно она удалит, и промахнуться мимо
   соседнего потока нечем. */
.impdel__list { list-style: none; margin: 0 0 4px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.impdel__item {
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface);
  display: flex; align-items: flex-start; gap: 12px; padding: 10px 14px;
}
.impdel__label { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.impdel__act { flex: none; }
.impdel__name { font-weight: 700; font-size: 14px; color: var(--color-heading); }
.impdel__hint { font-size: 12.5px; color: var(--color-muted); }
/* Строка, которую принесла прошлая загрузка: повторная заливка ЭТОГО файла её
   не вернёт — предупреждение должно быть заметно до клика, а не в message. */
.impdel__warn { font-size: 12.5px; color: var(--color-danger); }
.impdel__none { margin: 10px 0 0; font-size: 12.5px; color: var(--color-muted); }
.impdel__keep { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.impdel__keep-line { margin: 0; font-size: 12.5px; color: var(--color-muted); }
.impdel__go { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.impdel__go-hint { font-size: 12.5px; color: var(--color-muted); }
@media (max-width: 640px) {
  .impdel__item { flex-direction: column; align-items: stretch; }
  .impdel__act .btn { width: 100%; }
}
.bundle-cta .card__head { align-items: center; }

/* ---- Кликабельные строки реестров (data-row-href) ----------------------- */
.row-link { cursor: pointer; }
.row-link:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }
tr.row-link { transition: background .14s var(--ease); }
/* Ховер снят: единое правило .table tbody tr:hover > td (см. раздел таблиц). */
tr.row-link td:first-child a { position: relative; z-index: 1; }

/* ---- Ячейка «ПВЗ» в реестрах (includes/pvz_cell.html) ------------------- */
/* Адрес — главная строка (по нему точку и узнают), код — техническая подпись. */
.pvz-cell { display: block; min-width: 0; }
.pvz-cell__address { display: block; font-weight: 700; }
.pvz-cell__code { display: block; margin-top: 2px; font-size: 12px; color: var(--color-muted); }
.table-cell-pvz { max-width: 260px; min-width: 160px; }

/* ---- Выбор периода по месяцам (Расчёты OZON) ---------------------------- */
.eco-months-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px 24px; flex-wrap: wrap; margin-bottom: 4px; }
.eco-months-bar .eco-months { margin-bottom: 0; }
/* Чипы в один ряд с тихим горизонтальным скроллом — длинная история не ломает
   сетку и не заворачивает ряд в «лесенку» (в шаблоне чипов максимум 6). */
.eco-months { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.eco-months::-webkit-scrollbar { display: none; }
.eco-months .chip { flex: none; }
/* Форма периода — единая пилюля: подпись, два месяца, кнопка. */
.eco-period {
  display: flex; align-items: center; gap: 6px;
  padding: 5px 6px 5px 14px; border-radius: var(--radius-pill);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-xs);
}
.eco-period__label { font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--color-faint); }
.eco-period__dash { color: var(--color-faint); }
/* background-color, а не сокращение background: шорткат затирал у select.input
   треугольник (он нарисован background-image), и месяцы выглядели статичной
   подписью, а не выпадающим списком. Правый padding — под ту же стрелку. */
.eco-period .input {
  width: auto; min-width: 118px; padding: 7px 32px 7px 13px; font-size: 13px; font-weight: 600;
  border: 0; background-color: var(--color-surface-soft); border-radius: var(--radius-pill);
}
.eco-period .btn { border-radius: var(--radius-pill); }
/* Пилюля периода — 430px в одну строку: на телефоне она уводила страницу в
   горизонтальный скролл, а кнопка «Показать» оказывалась за краем экрана.
   Ниже 640px тот же набор раскладывается в два ряда и тянется по ширине. */
@media (max-width: 640px) {
  .eco-months-bar { align-items: stretch; }
  .eco-period {
    flex-wrap: wrap; width: 100%; padding: 10px 12px;
    border-radius: var(--radius-lg); row-gap: 8px;
  }
  .eco-period__label { flex: 1 0 100%; }
  .eco-period .input { flex: 1 1 40%; min-width: 0; }
  .eco-period .btn { flex: 1 0 100%; }
}


/* ---- Хаб «Зарплата и KPI» (/formulas/) ---------------------------------- */
.formula-hero {
  position: relative; overflow: hidden; display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(300px, .85fr); gap: clamp(20px, calc(3vw * var(--vu)), 40px);
  margin-bottom: 20px; padding: clamp(22px, calc(4vw * var(--vu)), 38px);
  border: 1px solid var(--color-border); border-radius: var(--radius-2xl);
  background: var(--color-surface); box-shadow: var(--shadow-soft);
}
.formula-hero::before {
  content: ""; position: absolute; width: 480px; height: 480px; right: -240px; top: -280px;
  border-radius: 50%; background: var(--color-primary-soft); pointer-events: none;
}
.formula-hero > * { position: relative; }
.formula-hero__content { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }
.formula-hero__eyebrow {
  margin-bottom: 10px; color: var(--color-primary-dark);
  font-size: 11px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase;
}
.formula-hero__title { max-width: 640px; font-size: clamp(26px, calc(3.4vw * var(--vu)), 38px); line-height: 1.12; letter-spacing: -.03em; }
.formula-hero__text { max-width: 600px; margin-top: 12px; color: var(--color-muted); font-size: 14px; line-height: 1.55; }
.formula-hero__actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 22px; }
.formula-hero__aside {
  display: flex; flex-direction: column; gap: 14px; align-self: center;
  padding: 20px; border: 1px solid var(--color-border); border-radius: var(--radius-xl);
  background: color-mix(in srgb, var(--color-surface-alt) 82%, transparent);
  backdrop-filter: blur(4px);
}
.formula-hero__aside-title { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--color-muted); }
.formula-hero__steps { display: flex; flex-direction: column; gap: 13px; margin: 0; padding: 0; list-style: none; }
.formula-hero__step { display: flex; gap: 11px; }
.formula-hero__step-num {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 26px; height: 26px; margin-top: 1px; border-radius: 50%;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-size: 12.5px; font-weight: 800;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
.formula-hero__step-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.formula-hero__step-body strong { font-size: 13px; }
.formula-hero__step-body span { color: var(--color-muted); font-size: 12px; line-height: 1.45; }
.formula-hero__step-body a { color: var(--color-primary-dark); font-weight: 700; }
.formula-hero__step-body a:hover { text-decoration: underline; }

.formula-section { margin-top: 26px; }
.formula-section__head {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 14px 24px;
  margin-bottom: 15px;
}
.formula-section__head > div { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.formula-section__eyebrow { color: var(--color-muted); font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.formula-section__title { font-size: 21px; letter-spacing: -.02em; }
.formula-section__text { max-width: 720px; margin-top: 3px; color: var(--color-muted); font-size: 12.5px; line-height: 1.5; }

/* Активные профили: две карточки с цветным корешком ₽ / KPI */
.formula-active-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; }
.formula-active-card {
  position: relative; overflow: hidden; display: flex; flex-direction: column; align-items: flex-start; gap: 13px;
  padding: 20px 20px 20px 24px; border: 1px solid var(--color-border); border-radius: var(--radius-xl);
  background: var(--color-surface); box-shadow: var(--shadow-card);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease), transform .18s var(--ease);
}
.formula-active-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-soft); }
.formula-active-card::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; }
.formula-active-card--payroll::before { background: linear-gradient(180deg, var(--color-primary), color-mix(in srgb, var(--color-primary) 45%, transparent)); }
.formula-active-card--kpi::before { background: linear-gradient(180deg, var(--color-success), color-mix(in srgb, var(--color-success) 45%, transparent)); }
.formula-active-card__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; }
.formula-active-card__icon {
  display: inline-flex; align-items: center; justify-content: center; min-width: 42px; height: 42px;
  padding: 0 10px; border-radius: 13px; font-size: 15px; font-weight: 850;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
}
.formula-active-card--kpi .formula-active-card__icon { background: var(--color-success-soft); color: var(--color-success); font-size: 12.5px; }
.formula-active-card__kind { color: var(--color-muted); font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; }
.formula-active-card__title { margin-top: 3px; font-size: 18px; letter-spacing: -.015em; }
.formula-active-card__text { margin-top: 6px; color: var(--color-muted); font-size: 12.5px; line-height: 1.5; }
.formula-active-card__facts { display: flex; gap: 7px; flex-wrap: wrap; margin-top: auto; }
.formula-active-card__facts span {
  padding: 6px 11px; border: 1px solid var(--color-border); border-radius: var(--radius-pill);
  background: var(--color-surface-soft); color: var(--color-muted); font-size: 11.5px;
}
.formula-active-card__facts b { color: var(--color-text); font-weight: 750; }

/* Галерея готовых сценариев */
.formula-template-block { margin-top: 18px; }
.formula-template-block:first-of-type { margin-top: 0; }
.formula-template-block__head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 11px; }
.formula-template-block__head h3 { font-size: 15px; letter-spacing: -.01em; }
.formula-template-block__head span { color: var(--color-muted); font-size: 12px; }
.formula-template-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 13px; }
.formula-template-card {
  display: flex; flex-direction: column; gap: 9px; padding: 17px;
  border: 1px solid var(--color-border); border-radius: var(--radius-xl);
  background: var(--color-surface); box-shadow: var(--shadow-card);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease), transform .18s var(--ease);
}
.formula-template-card:hover {
  transform: translateY(-2px); box-shadow: var(--shadow-soft);
  border-color: color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
}
.formula-template-card--kpi:hover { border-color: color-mix(in srgb, var(--color-success) 38%, var(--color-border)); }
.formula-template-card__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.formula-template-card__badge {
  padding: 4px 10px; border-radius: var(--radius-pill);
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
}
.formula-template-card--kpi .formula-template-card__badge { background: var(--color-success-soft); color: var(--color-success); }
.formula-template-card__mark { color: var(--color-faint); font-size: 13px; font-weight: 850; }
.formula-template-card h3 { font-size: 15.5px; letter-spacing: -.01em; }
.formula-template-card > p { color: var(--color-muted); font-size: 12px; line-height: 1.5; }
.formula-template-card__facts { display: flex; flex-direction: column; gap: 5px; margin: 2px 0 0; padding: 0; list-style: none; }
.formula-template-card__facts li { position: relative; padding-left: 16px; color: var(--color-muted); font-size: 11.5px; }
.formula-template-card__facts li::before {
  content: "✓"; position: absolute; left: 0; top: 0;
  color: var(--color-success); font-size: 11px; font-weight: 800;
}
.formula-template-card__action { display: flex; flex-direction: column; gap: 7px; margin-top: auto; padding-top: 8px; }

/* Библиотека версий */
.formula-library { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; align-items: start; }
.formula-library__group {
  overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-xl);
  background: var(--color-surface); box-shadow: var(--shadow-card);
}
.formula-library__head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 17px; border-bottom: 1px solid var(--color-border); background: var(--color-surface-alt);
}
.formula-library__head > div { display: flex; align-items: center; gap: 10px; }
.formula-library__head h3 { font-size: 14.5px; }
.formula-library__icon {
  display: inline-flex; align-items: center; justify-content: center; min-width: 32px; height: 32px;
  padding: 0 8px; border-radius: 10px; font-size: 13px; font-weight: 850;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
}
.formula-library__icon--kpi { background: var(--color-success-soft); color: var(--color-success); font-size: 10.5px; }
.formula-library__count { color: var(--color-muted); font-size: 11.5px; font-weight: 650; }
.formula-profile-list { display: flex; flex-direction: column; }
.formula-profile-card {
  display: flex; align-items: center; justify-content: space-between; gap: 12px 18px;
  padding: 14px 17px; border-bottom: 1px solid var(--color-border);
  transition: background .16s var(--ease);
}
.formula-profile-card:last-child { border-bottom: 0; }
.formula-profile-card:hover { background: var(--color-surface-alt); }
.formula-profile-card--active { background: color-mix(in srgb, var(--color-primary-soft) 45%, transparent); }
.formula-profile-card--active:hover { background: color-mix(in srgb, var(--color-primary-soft) 65%, transparent); }
.formula-profile-card__body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.formula-profile-card__title-row { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.formula-profile-card__title { color: var(--color-heading); font-size: 13.5px; font-weight: 750; }
.formula-profile-card__title:hover { color: var(--color-primary-dark); }
.formula-profile-card__body > p { color: var(--color-muted); font-size: 11.5px; line-height: 1.45; }
.formula-profile-card__meta { display: flex; gap: 5px 12px; flex-wrap: wrap; color: var(--color-faint); font-size: 11px; }
.formula-profile-card__actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; }
.formula-profile-card__archive { color: var(--color-danger); }
.formula-empty-inline { padding: 18px 17px; color: var(--color-muted); font-size: 12.5px; }

/* ---- Конструкторы ------------------------------------------------------- */
.formula-builder-head {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 20px;
  margin-bottom: 16px;
}
.formula-builder-head__main { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.formula-builder-head__back { align-self: flex-start; color: var(--color-muted); font-size: 12.5px; font-weight: 650; }
.formula-builder-head__back:hover { color: var(--color-primary-dark); }
.formula-builder-head__title-row { display: flex; align-items: center; gap: 11px; flex-wrap: wrap; }
.formula-builder-head__title-row .page__title { overflow-wrap: anywhere; }
.formula-builder-head__mark { width: 46px; height: 46px; border-radius: 14px; }
.formula-builder-head__title-row > div { min-width: 0; }
.formula-version-bar { align-items: center; box-shadow: var(--shadow-card); }
.formula-version-bar__note { display: flex; flex: 1 1 280px; flex-direction: column; gap: 2px; min-width: 0; }
.formula-version-bar__note strong { font-size: 12.5px; }
.formula-version-bar__note span { color: var(--color-muted); font-size: 11.5px; }
.formula-step-card { padding: 20px; }
.formula-step-card .fstep { padding-bottom: 2px; }
.formula-step-card .fstep__num { width: 30px; height: 30px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 15%, transparent); }
.formula-step-card .fstep__title { font-size: 17px; }
.formula-step-card .fstep__hint { max-width: 850px; }
.formula-profile-fields { display: grid; grid-template-columns: minmax(210px, .8fr) minmax(280px, 1.2fr); gap: 14px; }
.formula-profile-fields textarea.input { min-height: 78px; resize: vertical; }
.formula-toggle-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.formula-toggle-card {
  display: flex; align-items: flex-start; gap: 11px; padding: 14px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-soft); cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.formula-toggle-card:hover { border-color: color-mix(in srgb, var(--color-primary) 42%, var(--color-border)); }
.formula-toggle-card:has(input:checked) { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); background: var(--color-primary-soft); }
.formula-toggle-card__control input { width: 18px; height: 18px; margin: 1px 0 0; accent-color: var(--color-primary); }
.formula-toggle-card__body { display: flex; flex-direction: column; gap: 3px; }
.formula-toggle-card__body strong { font-size: 13.5px; }
.formula-toggle-card__body span { color: var(--color-muted); font-size: 12px; line-height: 1.4; }
.formula-inline-note { padding: 10px 12px; border-radius: var(--radius-md); background: var(--color-info-soft); color: var(--color-muted); font-size: 12px; }
.formula-inline-note a { color: var(--color-primary-dark); font-weight: 700; }
.formula-input-suffix { position: relative; display: flex; align-items: stretch; }
.formula-input-suffix .input { width: 100%; padding-right: 44px; }
.formula-input-suffix > span { position: absolute; right: 13px; top: 50%; transform: translateY(-50%); color: var(--color-muted); font-size: 12px; font-weight: 700; pointer-events: none; }
.formula-save-bar { border-color: color-mix(in srgb, var(--color-primary) 20%, var(--color-border)); box-shadow: 0 -8px 26px color-mix(in srgb, var(--color-bg-deep) 65%, transparent); }

@media (min-width: 928px) {
  .fbuild { grid-template-columns: minmax(0, 1fr) minmax(320px, 365px); }
}
.formula-preview { gap: 13px !important; overflow: hidden; }
.formula-preview__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.formula-preview__head > div { display: flex; flex-direction: column; gap: 3px; }
.formula-preview__intro { color: var(--color-muted); font-size: 12px; line-height: 1.45; }
.formula-preview__result {
  margin-inline: -2px; padding: 15px; border: 1px solid color-mix(in srgb, var(--color-primary) 22%, var(--color-border));
  border-radius: var(--radius-lg); background: linear-gradient(135deg, var(--color-primary-soft), var(--color-surface-alt));
}
.formula-preview__result .kpi-sim__score { font-size: 36px; }
.formula-preview__label { color: var(--color-muted); font-size: 10.5px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; }
.formula-preview-check {
  display: flex; align-items: flex-start; gap: 9px; padding: 10px;
  border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface-soft); cursor: pointer;
}
.formula-preview-check input { width: 17px; height: 17px; margin: 1px 0 0; accent-color: var(--color-primary); }
.formula-preview-check > span { display: flex; flex-direction: column; gap: 2px; }
.formula-preview-check strong { font-size: 12px; }
.formula-preview-check small { color: var(--color-muted); font-size: 10.5px; line-height: 1.35; }
.formula-preview__breakdown { display: flex; flex-direction: column; gap: 4px; }
.formula-preview__disclaimer {
  padding: 10px; border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface-soft); color: var(--color-muted); font-size: 10.5px; line-height: 1.45;
}
.formula-plain-card { gap: 10px !important; }
.formula-score-grid { padding: 14px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface-soft); }
.formula-metric-list .frule { background: var(--color-surface-alt); }
.formula-metric-switch { position: relative; display: inline-flex; margin-top: 1px; cursor: pointer; }
.formula-metric-switch input { position: absolute; opacity: 0; pointer-events: none; }
.formula-metric-switch span {
  position: relative; width: 36px; height: 21px; border-radius: var(--radius-pill);
  background: var(--color-border-strong); transition: background .18s var(--ease);
}
.formula-metric-switch span::after {
  content: ""; position: absolute; left: 3px; top: 3px; width: 15px; height: 15px;
  border-radius: 50%; background: var(--color-surface); box-shadow: var(--shadow-xs);
  transition: transform .18s var(--ease);
}
.formula-metric-switch input:checked + span { background: var(--color-primary); }
.formula-metric-switch input:checked + span::after { transform: translateX(15px); }
.formula-metric-switch input:focus-visible + span { box-shadow: var(--ring); }

/* ---- Простая настройка KPI (конструктор формулы) ------------------------ */
/* Прогрессивное раскрытие: сверху «насколько строго» одной кнопкой, ниже —
   переключатели показателей, точные числа спрятаны под «Точные значения».
   Поля формы те же, меняется только подача. */
.kstep { gap: 16px !important; }
.kstep .fstep { align-items: center; }
.kstep__expand { margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
.kstep__note { color: var(--color-muted); font-size: 12.5px; line-height: 1.45; }
@media (max-width: 700px) {
  /* Кнопка «показать все числа» не должна сжимать заголовок шага. */
  .kstep .fstep { flex-wrap: wrap; }
  .kstep__expand { order: 3; width: 100%; margin-left: 0; justify-content: flex-start; }
}

.kpreset { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(212px, 1fr)); }
.kpreset__card {
  position: relative; display: flex; flex-direction: column; gap: 5px;
  padding: 17px 16px 16px; text-align: left; cursor: pointer;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-soft); color: inherit;
  transition: border-color .18s var(--ease), background .18s var(--ease), transform .18s var(--ease);
}
.kpreset__card:hover { transform: translateY(-2px); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); }
.kpreset__card:focus-visible { outline: none; box-shadow: var(--ring); }
.kpreset__card.is-active {
  border-color: var(--color-primary); background: var(--color-primary-soft);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 40%, transparent);
}
.kpreset__card.is-active::after {
  content: "✓"; position: absolute; top: 12px; right: 13px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--color-primary); color: var(--color-on-primary); font-size: 11px; font-weight: 800;
}
.kpreset__badge {
  align-self: flex-start; margin-bottom: 2px; padding: 3px 8px; border-radius: var(--radius-pill);
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-size: 9.5px; font-weight: 850; letter-spacing: .05em; text-transform: uppercase;
}
/* Знак пресета — залитый глиф из общего реестра. Раньше тут стоял
   настоящий эмодзи (🙂 ⚖️ 🎯) кеглем 22px: он рисовался шрифтом системы,
   то есть выглядел по-разному на каждой ОС и не подчинялся ни теме, ни
   акценту. */
.kpreset__mark { display: grid; place-items: center; width: 26px; height: 26px; color: var(--color-primary); }
.kpreset__mark svg { width: 24px; height: 24px; }
.kpreset__name { font-size: 15px; font-weight: 750; color: var(--color-heading); }
.kpreset__text { color: var(--color-muted); font-size: 12px; line-height: 1.45; }

.kgroup { display: flex; flex-direction: column; gap: 9px; }
.kgroup__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.kgroup__title {
  margin: 0; font-size: 10.5px; font-weight: 850; letter-spacing: .07em;
  text-transform: uppercase; color: var(--color-primary-dark);
}
.kgroup__hint { color: var(--color-muted); font-size: 11.5px; }
.kgroup__list { display: flex; flex-direction: column; gap: 8px; }

.kmet {
  display: flex; flex-direction: column; gap: 9px; padding: 13px 15px;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-alt); transition: border-color .18s var(--ease), opacity .18s var(--ease);
}
.kmet:hover { border-color: color-mix(in srgb, var(--color-primary) 30%, var(--color-border)); }
.kmet--off { opacity: .58; }
.kmet--off .kmet__level { visibility: hidden; }
.kmet__main { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.kmet__text { flex: 1 1 220px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kmet__name { font-size: 14px; font-weight: 700; color: var(--color-heading); }
.kmet__hint { color: var(--color-muted); font-size: 11.5px; line-height: 1.4; }

.kmet__switch { position: relative; display: inline-flex; margin-top: 2px; flex: 0 0 auto; cursor: pointer; }
.kmet__switch input { position: absolute; opacity: 0; pointer-events: none; }
.kmet__switch > span[aria-hidden] {
  position: relative; width: 36px; height: 21px; border-radius: var(--radius-pill);
  background: var(--color-border-strong); transition: background .18s var(--ease);
}
.kmet__switch > span[aria-hidden]::after {
  content: ""; position: absolute; left: 3px; top: 3px; width: 15px; height: 15px;
  border-radius: 50%; background: var(--color-surface); box-shadow: var(--shadow-xs);
  transition: transform .18s var(--ease);
}
.kmet__switch input:checked + span[aria-hidden] { background: var(--color-primary); }
.kmet__switch input:checked + span[aria-hidden]::after { transform: translateX(15px); }
.kmet__switch input:focus-visible + span[aria-hidden] { box-shadow: var(--ring); }

.kmet__level {
  display: inline-flex; align-items: center; gap: 2px; flex: 0 0 auto; padding: 3px;
  border: 1px solid var(--color-border); border-radius: var(--radius-pill);
  background: var(--color-surface);
}
.kmet__lvl {
  padding: 5px 12px; border: 0; border-radius: var(--radius-pill); background: transparent;
  color: var(--color-muted); font-size: 11.5px; font-weight: 700; cursor: pointer;
  transition: background .16s var(--ease), color .16s var(--ease);
}
.kmet__lvl:hover { color: var(--color-heading); background: var(--color-surface-soft); }
.kmet__lvl:focus-visible { outline: none; box-shadow: var(--ring); }
.kmet__lvl.is-active { background: var(--color-primary); color: var(--color-on-primary); }
.kmet__lvl--own { cursor: default; }
.kmet__lvl--own.is-active { background: var(--color-warning-soft); color: var(--color-warning); }
.kmet__lvl--own:hover { background: var(--color-warning-soft); color: var(--color-warning); }

.kmet__foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.kmet__sentence { flex: 1 1 200px; color: var(--color-muted); font-size: 12px; line-height: 1.4; }
.kmet__more {
  flex: 0 0 auto; padding: 0; border: 0; background: transparent;
  color: var(--color-primary-dark); font-size: 11.5px; font-weight: 700; cursor: pointer;
}
.kmet__more:hover { text-decoration: underline; }
.kmet__more:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--radius-sm, 6px); }
.kmet__adv {
  padding-top: 11px; border-top: 1px dashed var(--color-border);
}

.kbonus { display: flex; flex-direction: column; gap: 8px; }
/* Две строки: «название + флаги», под ними «диапазон балла + премия».
   В одну строку четыре блока не помещаются — числовые поля схлопывались
   до «95,» и «10(» в узкой колонке конструктора. */
.kbonus__row {
  display: grid; gap: 11px 14px; padding: 13px 15px;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas: "name flags" "range pay";
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface-alt);
}
.kbonus__row--new { border-style: dashed; background: transparent; }
.kbonus__row:has(.fcheck--danger input:checked) { opacity: .5; border-color: var(--color-danger); }
.kbonus__name { grid-area: name; }
.kbonus__flags { grid-area: flags; }
.kbonus__range { grid-area: range; }
.kbonus__pay { grid-area: pay; }
.kbonus__row > div { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.kbonus__row .input { width: 100%; min-width: 0; }
.kbonus__row .fpair .input { flex: 1 1 0; min-width: 74px; text-align: center; }
.kbonus__row .field__label { font-size: 11.5px; }
.kbonus__flags { flex-direction: row !important; align-items: center; justify-content: flex-end; gap: 14px !important; }
@media (max-width: 640px) {
  .kbonus__row { grid-template-columns: minmax(0, 1fr); grid-template-areas: "name" "flags" "range" "pay"; }
  .kbonus__flags { justify-content: flex-start; }
}

.kextra { border-radius: var(--radius-lg); }
.kextra__summary {
  display: flex; flex-direction: column; gap: 2px; padding: 3px 0; cursor: pointer; list-style: none;
}
.kextra__summary::-webkit-details-marker { display: none; }
.kextra__title { font-size: 15px; font-weight: 700; color: var(--color-heading); }
.kextra__title::after { content: " ▾"; color: var(--color-muted); font-size: 12px; }
.kextra[open] .kextra__title::after { content: " ▴"; }
.kextra__hint { color: var(--color-muted); font-size: 12.5px; }
.kextra__body { display: flex; flex-direction: column; gap: 14px; padding-top: 14px; }

.ksim__summary {
  padding: 3px 0; color: var(--color-primary-dark); font-size: 12px; font-weight: 700;
  cursor: pointer; list-style: none;
}
.ksim__summary::-webkit-details-marker { display: none; }
.ksim__summary::after { content: " ▾"; }
.ksim[open] .ksim__summary::after { content: " ▴"; }
.ksim__grid { padding-top: 10px; }

/* ---- Кабинет сотрудника: начисления ------------------------------------ */
.payroll-cab-hero {
  position: relative; overflow: hidden; display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(250px, .65fr);
  gap: 24px; margin-bottom: 28px; padding: clamp(22px, calc(4vw * var(--vu)), 38px);
  border: 1px solid var(--color-border); border-radius: var(--radius-2xl); box-shadow: var(--shadow-soft);
  background: var(--color-surface);
}
.payroll-cab-hero::before {
  content: ""; position: absolute; width: 420px; height: 420px; left: -230px; top: -250px;
  border-radius: 50%; background: var(--color-primary-soft); pointer-events: none;
}
.payroll-cab-hero--published { border-color: color-mix(in srgb, var(--color-success) 24%, var(--color-border)); }
.payroll-cab-hero--published::before { background: var(--color-success-soft); }
.payroll-cab-hero > * { position: relative; }
.payroll-cab-hero__main { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }
.payroll-cab-hero__top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 7px; }
.payroll-cab-hero__period { margin-bottom: 22px; font-size: 18px; }
.payroll-cab-hero__label { color: var(--color-muted); font-size: 12.5px; font-weight: 650; }
.payroll-cab-hero__amount { margin-top: 2px; font-size: clamp(40px, calc(7vw * var(--vu)), 62px); line-height: 1; color: var(--color-heading); letter-spacing: -.045em; }
.payroll-cab-hero__amount small { font-size: .45em; font-weight: 750; color: var(--color-muted); }
.payroll-cab-hero__facts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 22px; }
.payroll-cab-hero__facts span { display: flex; flex-direction: column; gap: 2px; min-width: 115px; padding: 9px 12px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: color-mix(in srgb, var(--color-surface) 78%, transparent); }
.payroll-cab-hero__facts small { color: var(--color-muted); font-size: 10.5px; }
.payroll-cab-hero__facts b { font-size: 13px; }
.payroll-cab-hero__pending-title { margin-top: 5px; font-size: clamp(24px, calc(4vw * var(--vu)), 34px); line-height: 1.14; }
.payroll-cab-hero__pending-text { max-width: 580px; margin-top: 10px; color: var(--color-muted); }
.payroll-cab-hero__side {
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 13px;
  padding: 20px; border: 1px solid var(--color-border); border-radius: var(--radius-xl); background: var(--color-surface-alt);
}
.payroll-cab-hero__side-icon { display: inline-flex; align-items: center; justify-content: center; width: 45px; height: 45px; border-radius: 14px; background: var(--color-primary-soft); color: var(--color-primary-dark); font-size: 19px; font-weight: 850; }
.payroll-cab-hero__side p { color: var(--color-muted); font-size: 12.5px; line-height: 1.45; }
.payroll-cab-history__count { color: var(--color-muted); font-size: 12px; }
.payroll-cab-list { overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-xl); background: var(--color-surface); box-shadow: var(--shadow-card); }
.payroll-cab-row {
  display: grid; grid-template-columns: minmax(145px, .45fr) minmax(250px, 1fr) auto;
  gap: 18px; align-items: center; padding: 17px 18px; border-bottom: 1px solid var(--color-border);
  transition: background .18s var(--ease), padding-left .18s var(--ease);
}
.payroll-cab-row:last-child { border-bottom: 0; }
.payroll-cab-row:hover { padding-left: 21px; background: var(--color-surface-alt); }
.payroll-cab-row__date,
.payroll-cab-row__summary { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.payroll-cab-row__date small,
.payroll-cab-row__summary small { color: var(--color-muted); font-size: 11.5px; }
.payroll-cab-row__summary strong { font-size: 16px; }
.payroll-cab-row__status { display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
.payroll-cab-row__arrow { color: var(--color-faint); font-size: 18px; }
.payroll-cab-empty { margin-top: 18px; }

/* ---- Кабинет: рассрочки удержаний по претензиям ------------------------- */
.cab-inst { margin-bottom: 26px; }
.cab-inst__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; }
.cab-inst__card {
  display: flex; flex-direction: column; gap: 12px; padding: 18px;
  border: 1px solid color-mix(in srgb, var(--color-warning) 26%, var(--color-border));
  border-radius: var(--radius-xl); background: var(--color-surface); box-shadow: var(--shadow-card);
}
.cab-inst__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.cab-inst__badge {
  padding: 4px 10px; border-radius: var(--radius-pill);
  background: var(--color-warning-soft); color: var(--color-warning-strong, var(--color-warning));
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
}
.cab-inst__months { color: var(--color-muted); font-size: 11.5px; font-weight: 650; }
.cab-inst__amounts { display: flex; flex-direction: column; gap: 2px; }
.cab-inst__amounts strong { font-size: 28px; line-height: 1.1; letter-spacing: -.03em; color: var(--color-heading); }
.cab-inst__amounts strong small { font-size: .55em; color: var(--color-muted); font-weight: 750; }
.cab-inst__amounts > span { color: var(--color-muted); font-size: 12px; }
.cab-inst__bar {
  position: relative; height: 7px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); overflow: hidden;
}
.cab-inst__bar span {
  position: absolute; inset: 0 auto 0 0; border-radius: inherit;
  background: linear-gradient(90deg, var(--color-success), color-mix(in srgb, var(--color-success) 65%, var(--color-primary)));
  transition: width .3s var(--ease);
}
.cab-inst__steps { display: flex; gap: 6px; flex-wrap: wrap; margin: 0; padding: 0; list-style: none; }
.cab-inst__step {
  display: flex; flex-direction: column; gap: 1px; min-width: 84px; flex: 1 1 0;
  padding: 8px 10px; border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface-soft);
}
.cab-inst__step b { font-size: 12.5px; }
.cab-inst__step small { color: var(--color-faint); font-size: 10.5px; }
.cab-inst__step--past { border-color: color-mix(in srgb, var(--color-success) 34%, var(--color-border)); background: var(--color-success-soft); }
.cab-inst__step--past b { color: var(--color-success); }
.cab-inst__step--current { border-color: color-mix(in srgb, var(--color-primary) 42%, var(--color-border)); background: var(--color-primary-soft); }
.cab-inst__step--current b { color: var(--color-primary-dark); }
/* «Из них N ₽ не удержали» — предупреждение, а не сноска: человек должен
   заметить, что часть долга ещё висит, даже если месяцы уже прошли. */
.cab-inst__debt {
  margin: 0; padding: 8px 10px; border-radius: var(--radius-md);
  background: var(--color-warning-soft); color: var(--color-warning);
  font-size: 12.5px; line-height: 1.45;
}
.cab-inst__debt b { color: inherit; }
.cab-inst__next { color: var(--color-muted); font-size: 12px; }
.cab-inst__next b { color: var(--color-heading); }

/* ---- Детальный расчётный лист ------------------------------------------ */
.payroll-detail-head { align-items: flex-end; }
.payroll-detail-head > div { display: flex; flex-direction: column; gap: 4px; }
.payroll-detail-hero {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: clamp(22px, calc(4vw * var(--vu)), 34px); margin-bottom: 16px;
  border: 0;
  border-radius: var(--radius-2xl); background: linear-gradient(135deg, var(--color-success-soft), var(--color-surface) 55%); box-shadow: var(--shadow-card);
}
.payroll-detail-hero__main { display: flex; flex-direction: column; align-items: flex-start; }
.payroll-detail-hero__label { margin-top: 13px; color: var(--color-muted); font-size: 12px; }
.payroll-detail-hero__amount { font-size: clamp(42px, calc(7vw * var(--vu)), 62px); line-height: 1.05; letter-spacing: -.045em; }
.payroll-detail-hero__amount small { font-size: .45em; color: var(--color-muted); }
.payroll-detail-hero__main p { max-width: 570px; margin-top: 10px; color: var(--color-muted); font-size: 12.5px; }
.payroll-detail-hero__work { display: flex; gap: 10px; }
.payroll-detail-hero__work span { display: flex; flex-direction: column; gap: 1px; min-width: 90px; padding: 14px; text-align: center; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: color-mix(in srgb, var(--color-surface) 80%, transparent); }
.payroll-detail-hero__work small { color: var(--color-muted); font-size: 11px; }
.payroll-detail-hero__work strong { font-size: 20px; }
.payroll-detail-summary {
  display: grid; grid-template-columns: minmax(135px, 1fr) auto minmax(135px, 1fr) auto minmax(135px, 1fr) auto minmax(135px, 1fr) auto minmax(135px, 1fr);
  gap: 8px; align-items: stretch; margin-bottom: 16px;
}
.payroll-detail-summary__card { display: flex; flex-direction: column; gap: 4px; padding: 14px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-card); }
.payroll-detail-summary__card > span { color: var(--color-muted); font-size: 11px; }
.payroll-detail-summary__card strong { margin-top: auto; font-size: 17px; }
.payroll-detail-summary__card small { min-height: 2.8em; color: var(--color-faint); font-size: 9.5px; line-height: 1.35; }
.payroll-detail-summary__card--bonus { border-color: color-mix(in srgb, var(--color-success) 24%, var(--color-border)); }
.payroll-detail-summary__card--deduction { border-color: color-mix(in srgb, var(--color-danger) 24%, var(--color-border)); }
.payroll-detail-summary__card--deduction strong { color: var(--color-danger); }
.payroll-detail-summary__card--total { border-color: color-mix(in srgb, var(--color-primary) 36%, var(--color-border)); background: var(--color-primary-soft); }
.payroll-detail-summary__card--total strong { color: var(--color-primary-dark); }
.payroll-detail-summary__operator { align-self: center; color: var(--color-muted); font-size: 18px; font-weight: 800; }
.payroll-detail-breakdown { margin-bottom: 16px; }
.payroll-detail-pending {
  display: flex; align-items: center; gap: 18px; margin-bottom: 16px; padding: clamp(22px, calc(4vw * var(--vu)), 34px);
  border: 1px solid color-mix(in srgb, var(--color-warning) 28%, var(--color-border));
  border-radius: var(--radius-2xl); background: linear-gradient(135deg, var(--color-warning-soft), var(--color-surface) 58%); box-shadow: var(--shadow-card);
}
.payroll-detail-pending__icon { display: inline-flex; align-items: center; justify-content: center; width: 56px; height: 56px; flex: 0 0 auto; border-radius: 17px; background: var(--color-warning-soft); font-size: 25px; }
.payroll-detail-pending > div { flex: 1 1 auto; }
.payroll-detail-pending h2 { margin-top: 4px; font-size: 24px; }
.payroll-detail-pending p { max-width: 650px; margin-top: 7px; color: var(--color-muted); }
.payroll-comment-card { gap: 14px; }
.payroll-comment-card__message { padding: 13px 15px; border-left: 3px solid var(--color-primary); border-radius: 0 var(--radius-md) var(--radius-md) 0; background: var(--color-primary-soft); }
.payroll-comment-card__message > span { color: var(--color-muted); font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
.payroll-comment-card__message p { margin-top: 4px; }
.payroll-comment-form { display: flex; align-items: flex-end; gap: 10px; }
.payroll-comment-form .input { flex: 1 1 auto; min-height: 72px; resize: vertical; }

@media (max-width: 880px) {
  .formula-hero { grid-template-columns: minmax(0, 1fr) minmax(245px, .62fr); }
  .formula-library { grid-template-columns: 1fr; }
  .payroll-detail-summary { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .payroll-detail-summary__operator { display: none; }
}

@media (max-width: 819px) {
  .formula-hero,
  .payroll-cab-hero { grid-template-columns: 1fr; }
  .formula-active-grid { grid-template-columns: 1fr; }
  .formula-builder-head { align-items: flex-start; flex-direction: column; }
  .formula-profile-fields,
  .formula-toggle-grid { grid-template-columns: 1fr; }
  .formula-version-bar__note { order: 3; flex-basis: 100%; }
  .payroll-cab-hero__side { align-items: stretch; }
  .payroll-cab-row { grid-template-columns: minmax(120px, .4fr) minmax(0, 1fr); }
  .payroll-cab-row__status { grid-column: 1 / -1; justify-content: space-between; }
  .payroll-detail-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .payroll-detail-summary__card--total { grid-column: 1 / -1; }
}

@media (max-width: 620px) {
  .formula-hero { padding: 22px; }
  .formula-hero__actions,
  .formula-hero__actions .btn { width: 100%; }
  .formula-section__head { align-items: flex-start; flex-direction: column; }
  .formula-profile-card { align-items: stretch; flex-direction: column; }
  .formula-profile-card__actions { justify-content: flex-start; }
  .formula-template-block__head { align-items: flex-start; flex-direction: column; gap: 2px; }
  .formula-template-grid { grid-template-columns: 1fr; }
  .formula-version-bar { align-items: stretch; }
  .formula-version-bar .fbar__actions,
  .formula-version-bar .fbar__actions form,
  .formula-version-bar .fbar__actions .btn { width: 100%; }
  .formula-step-card { padding: 16px; }
  .formula-score-grid,
  .form-grid--3 { grid-template-columns: 1fr; }
  .form-field--pair { grid-column: auto; }
  .fpair { align-items: stretch; flex-direction: column; }
  .fpair__sep { align-self: flex-start; }
  .payroll-cab-hero { padding: 20px; }
  .payroll-cab-hero__facts { display: grid; grid-template-columns: 1fr; width: 100%; }
  .payroll-cab-row { grid-template-columns: 1fr; gap: 8px; }
  .payroll-cab-row__status { grid-column: auto; }
  .payroll-detail-hero,
  .payroll-detail-pending { align-items: flex-start; flex-direction: column; }
  .payroll-detail-hero__work { width: 100%; }
  .payroll-detail-hero__work span { flex: 1 1 0; }
  .payroll-detail-summary { grid-template-columns: 1fr; }
  .payroll-detail-summary__card--total { grid-column: auto; }
  .payroll-comment-form { align-items: stretch; flex-direction: column; }
  .payroll-comment-form .btn { width: 100%; }
}

@media (max-width: 420px) {
  .form-grid--sim { grid-template-columns: 1fr; }
  .formula-preview-check { grid-column: auto; }
  .formula-builder-head__mark { width: 40px; height: 40px; }
  .formula-builder-head__title-row .page__title { font-size: 24px; }
  .payroll-cab-hero__amount,
  .payroll-detail-hero__amount { font-size: 38px; }
}

/* =========================================================================
   Карточка претензии (stage 94)
   Шапка «ключевые факты» + панель обработки, где раскрыт один шаг. Раньше
   правая колонка была стопкой из шести открытых форм и уезжала за экран.
   ========================================================================= */
.grid--claim {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}

/* --- Шапка --------------------------------------------------------------- */
.claim-hero {
  display: grid;
  grid-template-columns: minmax(240px, 0.9fr) minmax(0, 2fr);
  gap: 24px;
  align-items: center;
  padding: 26px 28px;
  margin-bottom: 16px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-2xl);
  background: var(--gradient-surface);
  box-shadow: var(--shadow-card);
}
.claim-hero__main { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.claim-hero__badges { display: flex; flex-wrap: wrap; gap: 6px; }
.claim-hero__amount {
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--color-heading);
  font-variant-numeric: tabular-nums;
}
.claim-hero__title { color: var(--color-muted); font-size: 13.5px; font-weight: 600; }
.claim-hero__facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  margin: 0;
}
.claim-hero__facts > div { padding: 0 18px; border-left: 1px solid var(--color-border); min-width: 0; }
.claim-hero__facts > div:first-child { border-left: 0; padding-left: 0; }
.claim-hero__facts dt {
  color: var(--color-faint);
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 5px;
}
.claim-hero__facts dd {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--color-text);
  overflow-wrap: anywhere;
}
.claim-hero__sub {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  font-weight: 500;
  color: var(--color-muted);
}
.claim-hero__empty { color: var(--color-warning); }

/* --- Панель обработки ---------------------------------------------------- */
.claim-panel { gap: 12px; }
.claim-panel__state {
  padding: 10px 14px;
  border-radius: var(--radius-md);
  font-size: 13px;
  font-weight: 600;
  background: var(--color-surface-soft);
  color: var(--color-muted);
}
.claim-panel__state--success { background: var(--color-success-soft); color: var(--color-success); }
.claim-panel__state--warning { background: var(--color-warning-soft); color: var(--color-warning); }

.claim-acts { display: grid; gap: 8px; }
.claim-act {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  overflow: hidden;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.claim-act[open] { border-color: var(--color-border-strong); box-shadow: var(--shadow-xs); }
.claim-act__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.claim-act__head::-webkit-details-marker { display: none; }
.claim-act__head::after {
  content: "";
  margin-left: auto;
  width: 8px;
  height: 8px;
  flex: none;
  border-right: 2px solid var(--color-faint);
  border-bottom: 2px solid var(--color-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s var(--ease);
}
.claim-act[open] > .claim-act__head::after { transform: rotate(225deg) translate(-2px, -2px); }
.claim-act__head:hover { background: var(--color-surface-soft); }
.claim-act__title { font-size: 13.5px; font-weight: 700; color: var(--color-text); }
.claim-act__hint { font-size: 12px; color: var(--color-muted); }
.claim-act--danger .claim-act__title { color: var(--color-danger); }
.claim-act__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 4px 14px 14px;
  border-top: 1px solid var(--color-border);
}
.claim-act__foot { display: flex; gap: 10px; flex-wrap: wrap; }
.claim-act__foot .btn { min-width: 180px; }
.claim-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
/* Тег в селекторе — чтобы перебить общий textarea.input (min-height: 118px):
   в свёрнутой панели шесть высоких полей снова растянули бы колонку. */
.claim-act__body textarea.input { min-height: 64px; }

/* --- Детали -------------------------------------------------------------- */
.claim-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; margin: 0; }
/* Подпись над значением, а не слева: значения тут разной длины (ФИО против
   «Нет»), и в паре «слева-справа» длинное ФИО рвалось на три строки. */
.claim-facts > div {
  display: grid;
  gap: 3px;
  padding: 9px 0;
  border-bottom: 1px solid var(--color-border);
  min-width: 0;
}
.claim-facts dt {
  color: var(--color-faint);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.claim-facts dd { margin: 0; font-size: 13px; font-weight: 650; min-width: 0; overflow-wrap: anywhere; }
.claim-texts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 14px; }
.claim-text {
  padding: 10px 12px;
  border-radius: var(--radius-md);
  background: var(--color-surface-soft);
  min-width: 0;
}
.claim-text__label {
  display: block;
  color: var(--color-faint);
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 4px;
}
.claim-text__body { margin: 0; font-size: 13px; color: var(--color-text); overflow-wrap: anywhere; }

@media (max-width: 944px) {
  .claim-hero { grid-template-columns: 1fr; gap: 18px; }
  .claim-hero__facts > div { padding-inline: 14px; }
}
@media (max-width: 819px) {
  .grid--claim { grid-template-columns: 1fr; }
  .claim-hero__facts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 0; }
  .claim-hero__facts > div:nth-child(odd) { border-left: 0; padding-left: 0; }
  .claim-fields,
  .claim-facts,
  .claim-texts { grid-template-columns: 1fr; }
  .claim-act__foot .btn { width: 100%; }
}
@media (max-width: 520px) {
  .claim-hero { padding: 18px; }
  .claim-hero__amount { font-size: 28px; }
  .claim-hero__facts { grid-template-columns: 1fr; }
  .claim-hero__facts > div { border-left: 0; padding-left: 0; }
}

/* =========================================================================
   Алерты ПВЗ (stage 94) — строка на точку, внутри чипы по правилам.
   ========================================================================= */
/* Список смен, с которыми нахлёст, в плашке подтверждения пересечения.
   Внутри .alert, поэтому цвет наследуется — задаём только раскладку. */
.overlap-conflicts { margin: 6px 0 10px; padding-left: 18px; display: grid; gap: 4px; font-weight: 500; }
.overlap-conflicts li { line-height: 1.45; }
.overlap-conflicts .badge { margin-left: 6px; }

.alert-rows { display: grid; gap: 10px; }
.alert-row {
  padding: 14px 16px;
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-warning);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
}
.alert-row--danger { border-left-color: var(--color-danger); }
.alert-row__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}
.alert-row__pvz { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.alert-row__code { font-size: 15px; font-weight: 800; color: var(--color-heading); }
.alert-row__code:hover { color: var(--color-primary); }
.alert-row__name { color: var(--color-muted); font-size: 13px; }
.alert-chips { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; margin: 0; padding: 0; }
.alert-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px 8px 10px;
  border-radius: var(--radius-md);
  background: var(--color-surface-soft);
  border: 1px solid transparent;
  transition: border-color .18s var(--ease), transform .18s var(--ease);
}
.alert-chip:hover { border-color: var(--color-border-strong); transform: translateY(-1px); }
.alert-chip__count {
  display: grid;
  place-items: center;
  min-width: 30px;
  height: 30px;
  padding: 0 6px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: var(--color-warning-soft);
  color: var(--color-warning);
}
.alert-chip--danger .alert-chip__count { background: var(--color-danger-soft); color: var(--color-danger); }
.alert-chip__body { display: grid; gap: 1px; min-width: 0; }
.alert-chip__title { font-size: 13px; font-weight: 700; color: var(--color-text); }
.alert-chip__text { font-size: 11.5px; color: var(--color-muted); }

@media (max-width: 640px) {
  .alert-row__head { align-items: flex-start; flex-direction: column; gap: 6px; }
  .alert-chip { width: 100%; }
}

/* =========================================================================
   Панель «Отчёты OZON» (stage 94, редизайн — stage 102).
   Когда придёт следующий отчёт и что его нужно скачать — отрывными листками.
   ========================================================================= */
.repcal {
  position: relative;
  overflow: hidden;
  padding: 18px 20px;
  margin-bottom: 18px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-2xl);
  background: var(--gradient-surface);
  box-shadow: var(--shadow-card);
}
/* Цветная «заря» слева — тон меняется вместе с состоянием сроков. */
.repcal::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 360px;
  pointer-events: none;
  background: radial-gradient(120% 100% at 0% 0%, var(--color-primary-soft), transparent 72%);
}
.repcal--due::before { background: radial-gradient(120% 100% at 0% 0%, var(--color-warning-soft), transparent 72%); }
.repcal--done::before { background: radial-gradient(120% 100% at 0% 0%, var(--color-success-soft), transparent 72%); }
.repcal--due { border-color: color-mix(in srgb, var(--color-warning) 40%, var(--color-border)); }

.repcal__head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 14px;
}
.repcal__intro { min-width: 0; display: grid; gap: 3px; }
.repcal__title {
  margin: 0;
  font-size: 16px;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--color-heading);
}
.repcal__summary { margin: 0; max-width: 68ch; font-size: 12.5px; line-height: 1.45; color: var(--color-muted); }
.repcal--due .repcal__summary { color: var(--color-warning); font-weight: 650; }
.repcal__cta { flex: none; }

.repcal__slots {
  position: relative;
  display: grid;
  /* Сроков стало три (31.07.2026: + рейтинг ПВЗ каждую среду), и жёсткие две
     колонки оставляли третий листок висеть в одиночестве на полширины.
     auto-fit раскладывает столько, сколько влезает, не ломая узкие экраны. */
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 12px;
}
/* Четвёртый срок — аренда (08.2026). Вчетвером в ряд на текст листка
   оставалось 110px, и обрезалось всё вплоть до «+ ещё 3» — ровно та часть,
   которая говорит, что сроков в месяце несколько. Четыре листка встают 2×2:
   правило адресовано ИМЕННО четырём (`:nth-child(4)` у последнего), чтобы
   раскладка трёх сроков осталась ровно такой, какой была. */
@media (min-width: 881px) {
  .repcal__slots:has(> .repslot:nth-child(4):last-child) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
.repslot {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease), transform .18s var(--ease);
}
.repslot:hover { transform: translateY(-1px); box-shadow: var(--shadow-soft); }
.repslot--done { border-color: color-mix(in srgb, var(--color-success) 32%, var(--color-border)); }
.repslot--due { border-color: color-mix(in srgb, var(--color-warning) 55%, var(--color-border)); }

/* Отрывной листок календаря: цветная шапка с месяцем + крупное число. */
.repslot__leaf {
  flex: none;
  display: grid;
  grid-template-rows: 18px 1fr;
  width: 52px;
  height: 58px;
  overflow: hidden;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  box-shadow: var(--shadow-soft);
}
.repslot__month {
  position: relative;
  display: grid;
  place-items: center;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-on-primary);
  background: var(--color-primary);
}
.repslot__day {
  display: grid;
  place-items: center;
  font-size: 25px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--color-heading);
}
/* Перфорация отрывного листка — две «дырки» под пружину. */
.repslot__month::before,
.repslot__month::after {
  content: "";
  position: absolute;
  top: 6px;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .55);
}
.repslot__month::before { left: 7px; }
.repslot__month::after { right: 7px; }
.repslot--done .repslot__month { background: var(--color-success); }
.repslot--due .repslot__month { background: var(--color-warning); }
.repslot--due .repslot__day { color: var(--color-warning); }

.repslot__body { display: grid; gap: 2px; min-width: 0; flex: 1 1 auto; }
.repslot__title { font-size: 14px; font-weight: 700; color: var(--color-heading); }
.repslot__sub {
  font-size: 12px;
  color: var(--color-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Факт по сроку: галочка — документ есть, иначе точка-напоминание. */
.repslot__fact {
  margin-top: 3px;
  font-size: 12px;
  font-weight: 650;
  color: var(--color-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.repslot__fact::before {
  content: "✓";
  margin-right: 5px;
  font-weight: 800;
  color: var(--color-success);
}
.repslot--due .repslot__fact { color: var(--color-warning); }
.repslot--due .repslot__fact::before { content: "•"; color: var(--color-warning); }

.repslot__side { flex: none; display: grid; justify-items: end; gap: 7px; }
.repslot__when {
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: var(--color-surface-soft);
  font-size: 11.5px;
  font-weight: 700;
  white-space: nowrap;
  color: var(--color-muted);
}
.repslot--due .repslot__when { background: var(--color-warning-soft); color: var(--color-warning); }
.repslot__action { flex: none; }

@media (max-width: 880px) {
  .repcal__slots { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
  .repcal { padding: 14px 16px; }
  .repcal__head { flex-direction: column; align-items: stretch; gap: 12px; }
  .repcal__cta { width: 100%; }
  .repslot { flex-wrap: wrap; }
  .repslot__side { flex: 1 0 100%; grid-auto-flow: column; justify-items: stretch; align-items: center; }
  .repslot__when { text-align: center; }
}

/* =========================================================================
   Динамика тарифа (stage 94) — список переломов ставки по датам.
   ========================================================================= */
.tariff-changes { display: grid; gap: 2px; list-style: none; margin: 0; padding: 0; }
.tariff-change {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  font-size: 13px;
}
.tariff-change:nth-child(odd) { background: var(--color-surface-soft); }
.tariff-change__date { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--color-text); }
.tariff-change__move { color: var(--color-muted); }
.tariff-change__move b { color: var(--color-text); }
.tariff-change__points { color: var(--color-faint); font-size: 12px; }
.tariff-change__delta {
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: var(--color-success-soft);
  color: var(--color-success);
}
.tariff-change--down .tariff-change__delta { background: var(--color-danger-soft); color: var(--color-danger); }

/* Динамика ставки в строке точки: ступенчатый спарклайн + дельта «было → стало». */
.rate-move { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap; }
.rate-spark { width: 96px; height: 22px; overflow: visible; }
.rate-spark polyline { fill: none; stroke: var(--color-success); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.rate-move--down .rate-spark polyline { stroke: var(--color-danger); }
.rate-move--flat .rate-spark polyline { stroke: var(--color-border-strong); }
.rate-move__delta { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--color-success); }
.rate-move--down .rate-move__delta { color: var(--color-danger); }
.rate-move--flat .rate-move__delta { color: var(--color-faint); font-weight: 500; }

/* Таблица тарифов: всё по центру и строка целиком раскрывает историю периодов
   (просьба заказчика, 29.07.2026). Спарклайн и «было → стало» центрируются
   вместе с колонками, иначе ряд читался как три разных выравнивания. */
/* `.table thead th` задаёт left с большей специфичностью — повторяем её. */
.table--center thead th,
.table--center td { text-align: center; vertical-align: middle; }
.rate-table .rate-move { justify-content: center; }
.rate-row--expandable { cursor: pointer; }
.rate-row--expandable:hover,
.rate-row--expandable:focus-visible { background: var(--color-surface-soft); outline: none; }
.rate-row--expandable:focus-visible td:first-child { box-shadow: inset 3px 0 0 var(--color-primary); }
.rate-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--color-muted); font-size: 13px; white-space: nowrap;
}
.rate-toggle svg { width: 15px; height: 15px; transition: transform .18s var(--ease); }
.rate-group.is-open .rate-toggle { color: var(--color-text); }
.rate-group.is-open .rate-toggle svg { transform: rotate(180deg); }
.rate-group.is-open .rate-row { background: var(--color-surface-soft); }
.rate-details > td { background: var(--color-surface-soft); padding-top: 4px; }
.rate-periods { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; justify-items: center; }
.rate-periods li {
  display: inline-flex; align-items: baseline; gap: 10px; flex-wrap: wrap; justify-content: center;
  font-size: 13px;
}
.rate-periods__span { font-variant-numeric: tabular-nums; color: var(--color-muted); }
.rate-periods__rate { font-weight: 700; font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
  .tariff-change { grid-template-columns: 1fr auto; row-gap: 4px; }
  .tariff-change__move { grid-column: 1 / -1; order: 3; }
  .tariff-change__points { order: 4; }
}

/* Нижний ряд карточки претензии: три блока в строку (stage 94, 27.07.2026).
   Раньше это были два ряда по две карточки — блоки разной высоты оставляли
   под собой пустую половину экрана. */
.grid--claim-bottom {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}
@media (max-width: 760px) {
  .grid--claim-bottom { grid-template-columns: 1fr; }
}
/* Правая колонка карточки претензии: детали + файлы одной стопкой. */
.claim-side { display: grid; gap: 16px; align-content: start; min-width: 0; }

/* Светофор контроля приёмки (stage 95) удалён вместе с таблицей «Точки»
   (29.07.2026) — стилей .accept-dot* здесь больше нет. */

/* Полоска «сколько счётчика съедено» в деталке приёмки по точке.
   Ширина заливки приходит строкой из бэкенда: ru-локаль в шаблоне превращает
   float в «87,5 %» и ломает style. */
.usage-bar { display: block; width: 64px; height: 6px; border-radius: 999px; background: var(--color-surface-soft); overflow: hidden; }
.usage-bar__fill { display: block; height: 100%; border-radius: inherit; background: var(--color-success); }
.usage-bar--warning .usage-bar__fill { background: var(--color-warning); }
.usage-bar--danger .usage-bar__fill { background: var(--color-danger); }
.usage-bar--muted .usage-bar__fill { background: var(--color-border-strong); }
/* Чип-подпись (период окна) — не ссылка, кликать нечего. */
.chip--static { cursor: default; color: var(--color-muted); }

/* Разделитель дня в таблице приёмки: одна лента перевозок читается кучей,
   а разбор идёт днями («что было 24-го»). Строка с colspan заодно выключает
   клиентскую сортировку таблицы (initTableSort не трогает сгруппированные) —
   это осознанно: сортировка по столбцу перемешала бы дни. */
.table-daybreak td {
  padding: 9px 14px; background: var(--color-surface-alt);
  border-top: 1px solid var(--color-border);
}
.table tbody tr.table-daybreak:hover { background: transparent; }
.table tbody tr.table-daybreak:hover td { background: var(--color-surface-alt); }
.table-daybreak__day {
  font-weight: 800; font-size: 12.5px; color: var(--color-heading);
  letter-spacing: -0.01em;
}
.table-daybreak__meta { margin-left: 10px; font-size: 12px; color: var(--color-muted); }
.table-daybreak--danger td { box-shadow: inset 3px 0 0 var(--color-danger); }

/* Расшифровка счётчика по дням: скрытая строка под перевозкой.
   [hidden] обязан стоять ПЕРЕД display — иначе display:table-row перебивает
   правило браузера и «скрытая» строка видна (наступали на это трижды). */
.accept-steps[hidden] { display: none !important; }
.accept-row { cursor: pointer; }
/* Ховер снят: единое правило .table tbody tr:hover > td (см. раздел таблиц). */
.accept-row.is-open td { background: var(--color-surface-soft); }
.accept-steps td { background: var(--color-surface-alt); }
.accept-steps__head { font-size: 12.5px; color: var(--color-muted); margin-bottom: 10px; }
.accept-steps__list { display: grid; gap: 4px; list-style: none; margin: 0; padding: 0; }
.accept-step {
  display: grid; grid-template-columns: 88px 168px minmax(0, 1fr) 96px 108px;
  gap: 12px; align-items: baseline; padding: 6px 10px; border-radius: var(--radius-sm);
  font-size: 12.5px; font-variant-numeric: tabular-nums; background: var(--color-surface);
}
.accept-step__day { font-weight: 700; color: var(--color-heading); }
.accept-step__window, .accept-step__range { color: var(--color-muted); }
.accept-step__gained { font-weight: 700; color: var(--color-success); }
.accept-step__running { color: var(--color-text); }
.accept-step--idle { background: transparent; }
.accept-step--idle .accept-step__gained { color: var(--color-faint); font-weight: 500; }
@media (max-width: 819px) {
  .accept-step { grid-template-columns: 1fr 1fr; }
}

/* ==========================================================================
   Платформенная консоль (/platform/) и обратная связь
   --------------------------------------------------------------------------
   Консоль — интерфейс владельца продукта над всеми организациями. Она
   переиспользует токены и компоненты системы (.card/.table/.badge), но имеет свою
   оболочку: смотреть на данные клиентов и работать внутри одной сети — разные
   режимы, и путать их нельзя. Префикс .pc- — platform console, .fb- — feedback.
   ========================================================================== */

.pc-shell { min-height: 100vh; background: var(--color-bg); display: flex; flex-direction: column; }

.pc-topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 20px;
  padding: 0 22px; min-height: 60px;
  background: var(--sidebar-bg); border-bottom: 1px solid var(--sidebar-border);
}
/* «Назад» в консоли: шапка здесь графитовая в обеих темах, поэтому кнопка
   берёт цвета рельса, а не полотна — иначе в светлой теме получалась светлая
   плашка на тёмном фоне. */
.pc-topbar .topbar__back {
  flex-shrink: 0; margin-right: -8px;
  border-color: transparent; background: rgba(255, 255, 255, .06); color: var(--sidebar-text);
}
.pc-topbar .topbar__back:hover { background: rgba(255, 255, 255, .12); color: var(--sidebar-heading); }
.pc-brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; flex-shrink: 0; }
.pc-brand__mark {
  width: 32px; height: 32px; border-radius: var(--radius-md);
  display: grid; place-items: center;
  background: var(--gradient-brand); color: #fff;
  font-weight: 800; font-size: 16px; letter-spacing: -0.02em;
}
.pc-brand__text { display: flex; flex-direction: column; line-height: 1.15; }
.pc-brand__text strong { color: var(--sidebar-heading); font-size: 14.5px; font-weight: 750; }
.pc-brand__text em { color: var(--sidebar-text); font-size: 11px; font-style: normal; opacity: .8; }

.pc-nav { display: flex; align-items: center; gap: 2px; margin-left: 12px; overflow-x: auto; scrollbar-width: none; }
.pc-nav::-webkit-scrollbar { display: none; }
.pc-nav__item {
  position: relative; white-space: nowrap;
  padding: 8px 13px; border-radius: var(--radius-pill);
  color: var(--sidebar-text); text-decoration: none;
  font-size: 13.5px; font-weight: 600;
  transition: background-color .16s var(--ease), color .16s var(--ease);
}
.pc-nav__item:hover { background: rgba(255, 255, 255, .07); color: var(--sidebar-heading); }
.pc-nav__item.is-active { background: var(--sidebar-active); color: #fff; }
.pc-nav__badge {
  margin-left: 6px; padding: 1px 7px; border-radius: var(--radius-pill);
  background: var(--color-coral); color: #fff; font-size: 11px; font-weight: 800;
}
.pc-nav__item.is-active .pc-nav__badge { background: rgba(255, 255, 255, .25); }

.pc-topbar__side { margin-left: auto; display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.pc-user {
  width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(255, 255, 255, .1); color: var(--sidebar-heading);
  font-size: 12px; font-weight: 750;
}
.pc-exit { color: var(--sidebar-text); text-decoration: none; font-size: 13px; font-weight: 600; }
.pc-exit:hover { color: var(--sidebar-heading); }

.pc-main {
  flex: 1; width: 100%; max-width: 1360px; margin-inline: auto;
  padding: 24px 22px 60px; display: flex; flex-direction: column; gap: 20px;
  animation: fade-in .3s var(--ease) both;
}

/* ---- Крупные цифры ------------------------------------------------------ */
.pc-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; }
.pc-stat {
  display: flex; flex-direction: column; gap: 5px;
  padding: 17px 18px; border-radius: var(--radius-lg);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
  animation: pop-in .34s var(--ease) both; animation-delay: calc(var(--i, 0) * 45ms);
  transition: transform .2s var(--ease), box-shadow .2s var(--ease);
}
.pc-stat:hover { transform: translateY(-2px); box-shadow: var(--shadow-soft); }
.pc-stat__label { font-size: 12.5px; font-weight: 650; color: var(--color-muted); }
.pc-stat__value {
  font-size: 30px; font-weight: 800; line-height: 1.05;
  color: var(--color-heading); letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.pc-stat__value--cold { color: var(--color-warning); }
.pc-stat__hint { font-size: 12px; color: var(--color-faint); display: flex; align-items: center; gap: 6px; }

.pc-pulse {
  width: 7px; height: 7px; border-radius: 50%; background: var(--color-success);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-success) 60%, transparent);
  animation: pc-pulse 2s ease-out infinite;
}
@keyframes pc-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-success) 55%, transparent); }
  70%  { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.pc-online {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11.5px; font-weight: 700; color: var(--color-success);
}

/* ---- Панели ------------------------------------------------------------- */
.pc-panel { display: flex; flex-direction: column; gap: 14px; }
.pc-panel__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.pc-panel__title { font-size: 15px; font-weight: 750; color: var(--color-heading); margin: 0; }
.pc-panel__hint { font-size: 12px; color: var(--color-faint); }
.pc-panel__link { font-size: 12.5px; font-weight: 650; color: var(--color-primary); text-decoration: none; }
.pc-panel__link:hover { text-decoration: underline; }
.pc-panel--cold { border-color: color-mix(in srgb, var(--color-warning) 35%, var(--color-border)); }
.pc-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 20px; align-items: start; }
.pc-columns--aside { grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); }
.pc-aside { display: flex; flex-direction: column; gap: 20px; }
.pc-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pc-muted { color: var(--color-muted); font-size: 13px; }
.pc-muted--sm { font-size: 12px; color: var(--color-faint); }
.pc-slug {
  display: inline-block; margin-left: 6px;
  font-size: 11.5px; color: var(--color-faint); font-weight: 500;
}

/* ---- График активности --------------------------------------------------
   Высота столбца приходит целым процентом из Python (--pc-h): дробь в шаблоне
   отрендерилась бы по русской локали с запятой и сломала бы CSS. */
.pc-chart { display: flex; flex-direction: column; gap: 8px; }
.pc-chart__plot { display: flex; align-items: flex-end; gap: 3px; height: 150px; }
.pc-chart__col {
  position: relative; flex: 1; height: 100%;
  display: flex; align-items: flex-end; justify-content: center;
  border-radius: var(--radius-sm); cursor: default;
}
.pc-chart__col:hover, .pc-chart__col:focus-visible { background: var(--color-surface-soft); outline: none; }
.pc-chart__bar {
  width: 100%; max-width: 26px; min-height: 3px; height: max(3px, var(--pc-h, 0%));
  border-radius: 4px 4px 2px 2px;
  background: linear-gradient(180deg, var(--color-primary), color-mix(in srgb, var(--color-primary) 55%, transparent));
  transform-origin: bottom;
  animation: bar-grow .5s var(--ease) both; animation-delay: calc(var(--i, 0) * 14ms);
}
.pc-chart__col.is-today .pc-chart__bar { background: linear-gradient(180deg, var(--color-coral), color-mix(in srgb, var(--color-coral) 55%, transparent)); }
.pc-chart__tip {
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(4px);
  display: flex; flex-direction: column; gap: 2px; white-space: nowrap;
  padding: 7px 10px; border-radius: var(--radius-md);
  background: var(--color-heading); color: #fff; font-size: 11.5px;
  opacity: 0; pointer-events: none; z-index: 5;
  transition: opacity .16s var(--ease), transform .16s var(--ease);
}
.pc-chart__tip em { font-style: normal; opacity: .7; }
.pc-chart__col:hover .pc-chart__tip, .pc-chart__col:focus-visible .pc-chart__tip { opacity: 1; transform: translateX(-50%) translateY(0); }
.pc-chart__axis { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--color-faint); }

/* ---- Полосы освоения ---------------------------------------------------- */
.pc-bars { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.pc-bar { display: grid; grid-template-columns: minmax(96px, 1.1fr) minmax(0, 2fr) auto; align-items: center; gap: 11px; font-size: 13px; }
.pc-bar__label { color: var(--color-text); font-weight: 600; }
.pc-bar__track, .pc-cell-bar__track { height: 8px; border-radius: var(--radius-pill); background: var(--color-surface-soft); overflow: hidden; }
.pc-bar__fill, .pc-cell-bar__fill {
  display: block; height: 100%; width: var(--pc-share, 0%);
  border-radius: var(--radius-pill); background: var(--gradient-brand);
  animation: fill-grow .6s var(--ease) both;
}
.pc-bar__value { color: var(--color-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.pc-cell-bar { display: flex; flex-direction: column; gap: 5px; min-width: 150px; }
.pc-cell-bar__label { font-weight: 600; }
.pc-row--cold td { opacity: .62; }

/* ---- Чипы, роли, лента -------------------------------------------------- */
.pc-chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.pc-chip {
  padding: 6px 12px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); color: var(--color-text);
  font-size: 12.5px; font-weight: 600;
}
.pc-chip span { color: var(--color-faint); font-weight: 500; margin-left: 5px; font-size: 11px; }
.pc-chip--cold { background: var(--color-warning-soft); color: var(--color-warning); }

.pc-roles { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.pc-role {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 12px; border-radius: var(--radius-md); background: var(--color-surface-soft);
  font-size: 13px;
}
.pc-role.is-empty { opacity: .5; }
.pc-role__count { font-weight: 800; font-variant-numeric: tabular-nums; }

.pc-feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.pc-feed__item {
  display: grid; grid-template-columns: 108px minmax(0, 1fr); gap: 12px;
  padding: 10px 0; border-bottom: 1px solid var(--color-border); font-size: 13px;
}
.pc-feed__item:last-child { border-bottom: 0; }
.pc-feed__time { color: var(--color-faint); font-size: 12px; font-variant-numeric: tabular-nums; }
.pc-feed__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pc-feed__body strong { font-weight: 700; }
.pc-feed__meta { font-size: 11.5px; color: var(--color-faint); }
.pc-feed__item--critical .pc-feed__body strong { color: var(--color-danger); }
.pc-feed__item--warning .pc-feed__body strong { color: var(--color-warning); }

/* ---- Обращения в консоли ------------------------------------------------ */
.pc-tickets { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.pc-ticket { display: flex; flex-direction: column; gap: 4px; }
.pc-ticket__subject { font-weight: 700; color: var(--color-heading); text-decoration: none; }
.pc-ticket__subject:hover { color: var(--color-primary); }
.pc-ticket__excerpt { margin: 0; color: var(--color-muted); font-size: 13px; }
.pc-ticket__meta { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 11.5px; color: var(--color-faint); }
.pc-ticket--card {
  padding: 14px 16px; border-radius: var(--radius-lg);
  background: var(--color-surface-alt); border: 1px solid var(--color-border);
  transition: transform .18s var(--ease), box-shadow .18s var(--ease);
}
.pc-ticket--card:hover { transform: translateY(-2px); box-shadow: var(--shadow-sm); }
.pc-ticket--card.is-unread { border-left: 3px solid var(--color-primary); }

/* ---- Фильтры, диапазоны, пагинация -------------------------------------- */
.pc-filters { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.pc-filters .input { max-width: 260px; }
.pc-range { display: inline-flex; gap: 4px; padding: 4px; border-radius: var(--radius-pill); background: var(--color-surface-soft); flex-wrap: wrap; }
.pc-range__item {
  padding: 6px 13px; border-radius: var(--radius-pill);
  color: var(--color-muted); text-decoration: none; font-size: 12.5px; font-weight: 650;
  transition: background-color .16s var(--ease), color .16s var(--ease);
}
.pc-range__item:hover { color: var(--color-text); }
.pc-range__item.is-active { background: var(--color-surface); color: var(--color-heading); box-shadow: var(--shadow-xs); }
.pc-range__count { opacity: .65; font-size: 11px; }
.pc-pager { display: flex; align-items: center; justify-content: center; gap: 12px; padding-top: 6px; }
.pc-pager__state { font-size: 12.5px; color: var(--color-muted); }

/* ---- Форма создания тенанта и выданные доступы -------------------------- */
.pc-form { padding: 22px; gap: 20px; }
.pc-form__section { display: flex; flex-direction: column; gap: 13px; }
.pc-form__legend { font-size: 13px; font-weight: 750; color: var(--color-muted); text-transform: uppercase; letter-spacing: .04em; margin: 0; }

.pc-credentials { padding: 22px; display: flex; flex-direction: column; gap: 18px; animation: pop-in .35s var(--ease) both; }
.pc-credentials__head { display: flex; gap: 14px; align-items: flex-start; }
.pc-credentials__icon { font-size: 26px; line-height: 1; }
.pc-credentials__title { margin: 0 0 3px; font-size: 16px; font-weight: 750; }
.pc-credentials__note { margin: 0; font-size: 13px; color: var(--color-muted); }
.pc-credentials__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin: 0; }
.pc-credentials__grid dt { font-size: 12px; color: var(--color-faint); font-weight: 650; margin-bottom: 4px; }
.pc-credentials__grid dd { margin: 0; font-size: 14px; font-weight: 600; }
.pc-credentials__hint { margin: 0; font-size: 12.5px; color: var(--color-muted); }
.pc-copy {
  display: inline-block; padding: 6px 11px; border-radius: var(--radius-sm);
  background: var(--color-surface-soft); border: 1px dashed var(--color-border-strong);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13.5px;
  cursor: pointer; transition: border-color .16s var(--ease), background-color .16s var(--ease);
}
.pc-copy:hover { border-color: var(--color-primary); background: var(--color-primary-soft); }
.pc-copy.is-copied { border-style: solid; border-color: var(--color-success); background: var(--color-success-soft); color: var(--color-success); }
.pc-copy--secret { letter-spacing: .06em; font-weight: 700; }

.pc-meta { margin: 0; display: flex; flex-direction: column; gap: 11px; }
.pc-meta dt { font-size: 11.5px; color: var(--color-faint); font-weight: 650; }
.pc-meta dd { margin: 0; font-size: 13px; }
.pc-meta__ua { word-break: break-word; color: var(--color-muted); font-size: 11.5px; }
.pc-status-form { display: flex; gap: 9px; align-items: center; flex-wrap: wrap; }

@media (max-width: 960px) {
  .pc-columns, .pc-columns--aside { grid-template-columns: 1fr; }
  .pc-main { padding: 18px 14px 48px; }
  .pc-topbar { flex-wrap: wrap; padding: 10px 14px; gap: 10px; }
  .pc-nav { order: 3; width: 100%; margin-left: 0; }
  .pc-feed__item { grid-template-columns: 1fr; gap: 3px; }
}

/* ==========================================================================
   Обратная связь: плавающая кнопка, модалка, переписка
   ========================================================================== */
.fb-fab {
  position: fixed; right: 22px; bottom: 22px; z-index: 60;
  display: inline-flex; align-items: center; gap: 0;
  height: 50px; padding: 0 15px; border: 0; border-radius: var(--radius-pill);
  background: var(--gradient-primary); color: var(--color-on-primary);
  box-shadow: var(--shadow-primary); cursor: pointer;
  font: inherit; font-weight: 700; font-size: 13.5px;
  transition: transform .22s var(--ease), box-shadow .22s var(--ease), gap .22s var(--ease);
}
.fb-fab svg { width: 21px; height: 21px; flex-shrink: 0; }
.fb-fab__label { max-width: 0; overflow: hidden; white-space: nowrap; opacity: 0; transition: max-width .28s var(--ease), opacity .2s var(--ease); }
.fb-fab:hover { transform: translateY(-2px); box-shadow: 0 10px 26px color-mix(in srgb, var(--color-primary) 34%, transparent); gap: 9px; }
.fb-fab:hover .fb-fab__label { max-width: 190px; opacity: 1; }
.fb-fab:focus-visible { outline: none; box-shadow: var(--ring), var(--shadow-primary); }
.fb-fab:focus-visible .fb-fab__label { max-width: 190px; opacity: 1; }
/* На экране чата свободного угла нет: страница занимает вьюпорт целиком, и
   кнопка садится на «Отправить» — на телефоне, на планшете и на любом
   невысоком окне десктопа (1280x720 проверен). Правило общее для всех
   ширин, а не мобильное: перекрытие зависит от высоты окна, не от телефона.
   Обратная связь никуда не девается — она пунктом меню в сайдбаре. */
body:has(.chat-card) .fb-fab { display: none; }

.fb-modal { position: fixed; inset: 0; z-index: 120; display: flex; align-items: center; justify-content: center; padding: 20px; }
.fb-modal[hidden] { display: none; }
.fb-modal__backdrop { position: absolute; inset: 0; background: rgba(10, 12, 16, .55); backdrop-filter: blur(3px); animation: fade-in .2s var(--ease) both; }
.fb-modal__card {
  position: relative; z-index: 1;
  width: min(640px, 100%); max-height: min(88vh, 860px); overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px;
  padding: 22px; border-radius: var(--radius-xl);
  background: var(--color-surface); box-shadow: var(--shadow-pop);
  animation: fb-modal-in .3s var(--ease) both;
}
@keyframes fb-modal-in { from { opacity: 0; transform: translateY(20px) scale(.97); } to { opacity: 1; transform: none; } }
.fb-modal.is-closing .fb-modal__card { animation: fb-modal-out .18s var(--ease) both; }
.fb-modal.is-closing .fb-modal__backdrop { animation: fb-fade-out .18s var(--ease) both; }
@keyframes fb-modal-out { to { opacity: 0; transform: translateY(12px) scale(.98); } }
@keyframes fb-fade-out { to { opacity: 0; } }

.fb-modal__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.fb-modal__title { margin: 0 0 3px; font-size: 18px; font-weight: 800; color: var(--color-heading); }
.fb-modal__sub { margin: 0; font-size: 13px; color: var(--color-muted); }
.fb-modal__close {
  width: 34px; height: 34px; flex-shrink: 0;
  display: grid; place-items: center; border: 0; border-radius: var(--radius-md);
  background: transparent; color: var(--color-muted); cursor: pointer;
  transition: background-color .16s var(--ease), color .16s var(--ease);
}
.fb-modal__close svg { width: 18px; height: 18px; }
.fb-modal__close:hover { background: var(--color-surface-soft); color: var(--color-text); }

.fb-form { display: flex; flex-direction: column; gap: 15px; }
.fb-kinds { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.fb-kinds .field__label { padding: 0; }
.fb-kind { display: inline-flex; }
.fb-kinds { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; }
.fb-kinds legend { width: 100%; margin-bottom: 4px; }
.fb-kind input { position: absolute; opacity: 0; width: 0; height: 0; }
.fb-kind__box {
  padding: 8px 14px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); border: 1px solid transparent;
  font-size: 13px; font-weight: 650; color: var(--color-muted); cursor: pointer;
  transition: background-color .16s var(--ease), color .16s var(--ease), border-color .16s var(--ease), transform .16s var(--ease);
}
.fb-kind__box:hover { color: var(--color-text); }
.fb-kind input:checked + .fb-kind__box { background: var(--color-primary-soft); border-color: var(--color-primary); color: var(--color-primary-dark); transform: translateY(-1px); }
.fb-kind input:focus-visible + .fb-kind__box { box-shadow: var(--ring-soft); }

.fb-drop {
  display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
  padding: 20px 16px; border-radius: var(--radius-lg);
  border: 1.5px dashed var(--color-border-strong); background: var(--color-surface-alt);
  cursor: pointer; transition: border-color .18s var(--ease), background-color .18s var(--ease);
}
.fb-drop svg { width: 26px; height: 26px; color: var(--color-muted); }
.fb-drop__title { font-size: 13.5px; font-weight: 650; }
.fb-drop__or { color: var(--color-faint); font-weight: 500; }
.fb-drop__hint { font-size: 12px; color: var(--color-faint); }
.fb-drop:hover, .fb-drop.is-dragover { border-color: var(--color-primary); background: var(--color-primary-soft); }
.fb-drop.is-filled { border-style: solid; border-color: var(--color-success); background: var(--color-success-soft); }
.fb-drop__file { font-size: 12.5px; font-weight: 650; color: var(--color-success); }

/* Выбранные файлы: миниатюра + крестик. Список живёт вне ярлыка .fb-drop,
   иначе клик по крестику открывал бы диалог выбора файлов. */
.fb-picked { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.fb-picked[hidden] { display: none; }
.fb-picked__item {
  position: relative; width: 74px;
  border-radius: var(--radius-md); overflow: hidden;
  border: 1px solid var(--color-border); background: var(--color-surface-alt);
  animation: fade-in .2s var(--ease) both;
}
.fb-picked__thumb { display: block; width: 100%; height: 56px; object-fit: cover; }
.fb-picked__name {
  display: block; padding: 3px 5px; font-size: 10.5px; color: var(--color-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fb-picked__drop {
  position: absolute; top: 3px; right: 3px;
  width: 20px; height: 20px; display: grid; place-items: center;
  border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(10, 12, 16, .62); color: #fff; line-height: 1;
  font-size: 13px; font-weight: 700;
}
.fb-picked__drop:hover { background: var(--color-danger); }
.fb-picked__drop:focus-visible { box-shadow: var(--ring-soft); }

.fb-form__foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.fb-form__note { font-size: 11.5px; color: var(--color-faint); max-width: 260px; }
.fb-form.is-sending { opacity: .6; pointer-events: none; }

/* ---- Переписка по обращению --------------------------------------------- */
.fb-thread { display: flex; flex-direction: column; gap: 12px; }
.fb-thread__pending { margin: 0; font-size: 13px; color: var(--color-muted); font-style: italic; }
.fb-bubble {
  max-width: 82%; padding: 13px 15px; border-radius: var(--radius-lg);
  background: var(--color-surface-soft);
  animation: fade-in .3s var(--ease) both;
}
.fb-bubble--out { align-self: flex-end; background: var(--color-primary-soft); }
.fb-bubble--in { align-self: flex-start; }
.fb-bubble__head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 5px; font-size: 12px; }
.fb-bubble__head strong { font-weight: 750; }
.fb-bubble__head span { color: var(--color-faint); font-size: 11.5px; }
.fb-bubble__text { margin: 0; font-size: 13.5px; line-height: 1.5; word-break: break-word; }
/* Галерея: одна картинка занимает прежнюю ширину, несколько выстраиваются в
   ряд с переносом — читать переписку сверху вниз важнее, чем разглядывать
   каждый скриншот в полный размер (для этого есть клик). */
.fb-shots { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.fb-shot { display: block; border-radius: var(--radius-md); overflow: hidden; max-width: 320px; }
.fb-shots .fb-shot { flex: 0 1 auto; max-width: 152px; }
.fb-shots:has(> .fb-shot:only-child) .fb-shot { max-width: 320px; }
.fb-shot img { display: block; width: 100%; height: auto; }
.fb-reply { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--color-border); display: flex; flex-direction: column; gap: 10px; }

.fb-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.fb-list__item { border-bottom: 1px solid var(--color-border); }
.fb-list__item:last-child { border-bottom: 0; }
.fb-list__link {
  display: flex; flex-direction: column; gap: 5px;
  padding: 14px 4px; text-decoration: none; color: inherit;
  transition: background-color .16s var(--ease);
}
.fb-list__link:hover { background: var(--color-surface-soft); }
.fb-list__top { display: flex; align-items: center; gap: 10px; justify-content: space-between; }
.fb-list__subject { font-size: 14px; font-weight: 700; color: var(--color-heading); }
.fb-list__excerpt { font-size: 13px; color: var(--color-muted); }
.fb-list__meta { font-size: 11.5px; color: var(--color-faint); }

@media (max-width: 600px) {
  .fb-fab { right: 14px; bottom: 14px; height: 46px; }
  .fb-bubble { max-width: 100%; }
  .fb-modal { padding: 0; align-items: flex-end; }
  .fb-modal__card { width: 100%; max-height: 92vh; border-radius: var(--radius-xl) var(--radius-xl) 0 0; }
}

/* ---- Состояние сервера ---------------------------------------------------
   Один язык тонов на весь экран: ok / warning / danger / unknown. Цвет здесь
   несёт смысл, поэтому он всегда продублирован словом — по одному кружку
   нельзя понять, что именно кончается, и дальтоник не должен угадывать. */
.pc-verdict {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 16px 18px; border-radius: var(--radius-lg);
  border: 1px solid var(--color-border); background: var(--color-surface);
  box-shadow: var(--shadow-card);
}
.pc-verdict__dot {
  width: 12px; height: 12px; border-radius: 50%; flex: none;
  background: var(--color-muted);
}
.pc-verdict__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pc-verdict__title { font-size: 16px; font-weight: 800; color: var(--color-heading); }
.pc-verdict__hint { font-size: 12.5px; color: var(--color-muted); }
.pc-verdict__time { margin-left: auto; font-size: 11.5px; color: var(--color-faint); }
.pc-verdict--ok { border-color: color-mix(in srgb, var(--color-success) 45%, var(--color-border)); background: var(--color-success-soft); }
.pc-verdict--ok .pc-verdict__dot {
  background: var(--color-success);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-success) 60%, transparent);
  animation: pc-pulse 2.4s ease-out infinite;
}
.pc-verdict--warning { border-color: color-mix(in srgb, var(--color-warning) 45%, var(--color-border)); background: var(--color-warning-soft); }
.pc-verdict--warning .pc-verdict__dot { background: var(--color-warning); }
.pc-verdict--danger { border-color: color-mix(in srgb, var(--color-danger) 50%, var(--color-border)); background: var(--color-danger-soft); }
.pc-verdict--danger .pc-verdict__dot { background: var(--color-danger); }

.pc-meters { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 14px; }
.pc-meter {
  display: flex; flex-direction: column; gap: 9px;
  padding: 16px 18px; border-radius: var(--radius-lg);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
  animation: pop-in .34s var(--ease) both; animation-delay: calc(var(--i, 0) * 45ms);
}
.pc-meter__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.pc-meter__label { font-size: 12.5px; font-weight: 650; color: var(--color-muted); }
.pc-meter__value {
  font-size: 24px; font-weight: 800; line-height: 1;
  color: var(--color-heading); font-variant-numeric: tabular-nums;
}
.pc-meter__track {
  height: 9px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); overflow: hidden;
}
.pc-meter__fill {
  display: block; height: 100%; width: var(--pc-share, 0%);
  border-radius: var(--radius-pill); background: var(--gradient-brand);
  animation: fill-grow .6s var(--ease) both;
}
.pc-meter__hint { font-size: 11.5px; color: var(--color-faint); line-height: 1.45; }
.pc-meter--warning { border-color: color-mix(in srgb, var(--color-warning) 40%, var(--color-border)); }
.pc-meter--warning .pc-meter__fill { background: var(--color-warning); }
.pc-meter--warning .pc-meter__value { color: var(--color-warning); }
.pc-meter--danger { border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border)); }
.pc-meter--danger .pc-meter__fill { background: var(--color-danger); }
.pc-meter--danger .pc-meter__value { color: var(--color-danger); }
.pc-meter--unknown .pc-meter__value { color: var(--color-faint); }

.pc-facts { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
.pc-facts__row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 8px 0; border-bottom: 1px solid var(--color-border);
  font-size: 13px;
}
.pc-facts__row:last-child { border-bottom: 0; }
.pc-facts__row dt, .pc-facts__row > span:first-child { color: var(--color-muted); }
.pc-facts__row dd { margin: 0; }
.pc-facts__value { font-weight: 650; color: var(--color-heading); text-align: right; font-variant-numeric: tabular-nums; }
.pc-facts__value--warning { color: var(--color-warning); }
.pc-facts__value--danger { color: var(--color-danger); }
.pc-facts__value--unknown { color: var(--color-faint); }
.pc-facts--compact .pc-facts__row { padding: 5px 0; font-size: 12.5px; }
.pc-subtitle { margin: 6px 0 0; font-size: 12px; font-weight: 700; color: var(--color-faint); text-transform: uppercase; letter-spacing: .04em; }
.pc-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; word-break: break-all; }

.pc-badge {
  font-size: 11.5px; font-weight: 700; padding: 3px 9px;
  border-radius: var(--radius-pill); background: var(--color-surface-soft); color: var(--color-muted);
}
.pc-badge--ok { background: var(--color-success-soft); color: var(--color-success); }
.pc-badge--warning { background: var(--color-warning-soft); color: var(--color-warning); }
.pc-badge--danger { background: var(--color-danger-soft); color: var(--color-danger); }

.pc-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.pc-chip {
  font-size: 11.5px; font-weight: 650; padding: 3px 9px;
  border-radius: var(--radius-pill); background: var(--color-surface-soft); color: var(--color-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.pc-chip--ok { background: var(--color-success-soft); color: var(--color-success); }

/* ---- Период аналитики --------------------------------------------------- */
.pc-periods { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.pc-period {
  padding: 6px 14px; border-radius: var(--radius-pill);
  font-size: 12.5px; font-weight: 650; text-decoration: none;
  color: var(--color-muted); background: var(--color-surface);
  border: 1px solid var(--color-border);
  transition: color .16s var(--ease), background-color .16s var(--ease), border-color .16s var(--ease);
}
.pc-period:hover { color: var(--color-heading); border-color: var(--color-border-strong); }
.pc-period.is-active {
  color: var(--color-on-primary); background: var(--color-primary); border-color: var(--color-primary);
}
.pc-periods__hint { margin-left: auto; font-size: 11.5px; color: var(--color-faint); }

/* ---- Выводы истории ----------------------------------------------------- */
.pc-insights {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 10px;
}
.pc-insight {
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 14px; border-radius: var(--radius-md);
  background: var(--color-surface-soft); border-left: 3px solid var(--color-border-strong);
}
.pc-insight__title { font-size: 13.5px; font-weight: 700; color: var(--color-heading); }
.pc-insight__hint { font-size: 12px; color: var(--color-muted); line-height: 1.45; }
.pc-insight--ok { border-left-color: var(--color-success); background: var(--color-success-soft); }
.pc-insight--warning { border-left-color: var(--color-warning); background: var(--color-warning-soft); }
.pc-insight--danger { border-left-color: var(--color-danger); background: var(--color-danger-soft); }

/* ---- Графики истории ----------------------------------------------------
   SVG рисуется по целочисленным координатам из Python: дробь ушла бы в
   разметку с русской запятой и график молча исчез бы. preserveAspectRatio
   отключён — холст растягивается по ширине карточки, высоту держит CSS. */
.pc-graphs { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 18px; }
.pc-graph { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.pc-graph__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.pc-graph__title { margin: 0; font-size: 13px; font-weight: 700; color: var(--color-heading); }
.pc-graph__facts { display: flex; align-items: baseline; gap: 9px; font-variant-numeric: tabular-nums; }
.pc-graph__now { font-size: 15px; font-weight: 800; color: var(--color-heading); }
.pc-graph__peak { font-size: 11.5px; color: var(--color-faint); }
.pc-graph__plot { position: relative; }
.pc-graph__svg { display: block; width: 100%; height: 120px; overflow: visible; }
.pc-graph__grid { stroke: var(--color-border); stroke-width: 1; vector-effect: non-scaling-stroke; }
.pc-graph__area { fill: var(--color-primary); opacity: .12; }
.pc-graph__line {
  fill: none; stroke: var(--color-primary); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
  /* Без этого горизонтальное растяжение viewBox раздавило бы линию в нить. */
  vector-effect: non-scaling-stroke;
}
.pc-graph__dot { fill: var(--color-primary); }
.pc-graph__scale {
  position: absolute; inset: 0 auto 0 0; display: flex; flex-direction: column;
  justify-content: space-between; pointer-events: none;
  font-size: 10px; color: var(--color-faint); font-variant-numeric: tabular-nums;
}
.pc-graph__scale span { background: var(--color-surface); padding-right: 4px; line-height: 1; }
.pc-graph__foot {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 10.5px; color: var(--color-faint);
}
.pc-graph__avg { color: var(--color-muted); font-weight: 650; }
.pc-graph__blank { margin: 0; font-size: 12.5px; color: var(--color-faint); padding: 26px 0; text-align: center; }
.pc-graph.is-empty { opacity: .65; }

/* ---- Нагрузка по часам --------------------------------------------------- */
.pc-hours { display: flex; align-items: flex-end; gap: 3px; height: 120px; }
.pc-hours__col {
  position: relative; flex: 1; height: 100%;
  display: flex; align-items: flex-end; border-radius: var(--radius-sm); cursor: default;
}
.pc-hours__col:hover, .pc-hours__col:focus-visible { background: var(--color-surface-soft); outline: none; }
.pc-hours__bar {
  width: 100%; min-height: 3px; height: max(3px, var(--pc-h, 0%));
  border-radius: 3px 3px 1px 1px;
  background: linear-gradient(180deg, var(--color-primary), color-mix(in srgb, var(--color-primary) 45%, transparent));
  animation: bar-grow .5s var(--ease) both; animation-delay: calc(var(--i, 0) * 12ms);
}
.pc-hours__tip {
  position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  display: none; flex-direction: column; gap: 1px; white-space: nowrap; z-index: 3;
  padding: 6px 9px; border-radius: var(--radius-sm);
  background: var(--color-heading); color: var(--color-surface); font-size: 11px;
}
.pc-hours__tip em { font-style: normal; opacity: .7; }
.pc-hours__col:hover .pc-hours__tip, .pc-hours__col:focus-visible .pc-hours__tip { display: flex; }
.pc-hours__axis {
  display: flex; justify-content: space-between;
  font-size: 10.5px; color: var(--color-faint); margin-top: 2px;
}

.pc-meter__value--sm { font-size: 17px; }

/* Без анимаций: движение — украшение, оно не должно мешать тем, кто его
   отключил. Состояния (цвет, размер, фокус) остаются на месте. */
@media (prefers-reduced-motion: reduce) {
  .pc-stat, .pc-chart__bar, .pc-bar__fill, .pc-cell-bar__fill,
  .pc-main, .pc-credentials, .fb-bubble, .fb-modal__card, .fb-modal__backdrop,
  .pc-meter, .pc-meter__fill, .pc-hours__bar {
    animation: none !important;
  }
  .pc-pulse, .pc-verdict--ok .pc-verdict__dot { animation: none; }
  .fb-fab, .pc-stat, .pc-ticket--card { transition: none; }
}

/* ---- Настройки уведомлений: состояние канала и блок сводки -------------- */
.notif-channel {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;
  padding: 14px 16px; border-radius: var(--radius-lg);
  background: var(--color-surface-alt); border: 1px solid var(--color-border);
}
.notif-channel__item { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.notif-channel__label { font-size: 12px; color: var(--color-faint); font-weight: 650; }
.notif-channel__value { font-size: 13.5px; word-break: break-word; }
.notif-channel__value--ok { color: var(--color-success); }
.notif-channel__value--bad { color: var(--color-danger); }
.notif-channel__value--warn { color: var(--color-warning); }
/* Состояние уведомлений браузера пишет JS: до его ответа место под текст
   должно быть занято, иначе карточка дёргается на каждой загрузке. */
.push-browser__status { display: block; min-height: 20px; }
.push-browser__hint { display: block; margin-top: 8px; font-size: 13px; color: var(--color-faint); }
.notif-digest { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; }
/* Галочки каналов выравниваем по центру ячейки, а не по базовой линии текста:
   в пустом лейбле строка всё равно резервирует место под нижние выносные
   элементы, и квадратик уезжает на пару пикселей вверх. */
.notif-table td .checkbox { vertical-align: middle; }
/* Подпись «всегда» под галочкой сдвигала саму галочку вверх: ячейка с подписью
   центрируется по блоку «галочка + подпись», а соседние — по одной галочке.
   Пустая строка сверху ровно той же высоты делает блок симметричным, и галочка
   встаёт в один ряд с соседними по строке. */
.notif-always { display: flex; flex-direction: column; align-items: center; }
.notif-always::before { content: ""; height: 16px; }
.notif-always__note {
  margin-top: 2px; font-size: 10.5px; line-height: 14px; color: var(--color-faint);
}

/* ---- Двухфакторка консоли ----------------------------------------------- */
.pc-qr {
  display: inline-flex; padding: 16px; border-radius: var(--radius-lg);
  background: #fff; border: 1px solid var(--color-border);
  /* Панель-родитель растягивает детей на всю ширину (align-items: stretch) —
     белое поле расползалось, а QR прижимался к левому краю. */
  align-self: flex-start;
}
/* Размер задаёт CSS (SVG с viewBox масштабируется без потерь): 264px —
   заметно крупнее прежних 185, камере проще зацепиться за модули. */
.pc-qr svg { display: block; width: 264px; height: 264px; }

.pc-codes {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px;
}
.pc-codes code {
  display: block; padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--color-surface-soft); border: 1px dashed var(--color-border-strong);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 14px; letter-spacing: .08em; text-align: center;
}

/* Поле для шестизначного кода: крупно и моноширинно — цифры набирают вслепую,
   поглядывая на телефон, мелкий шрифт здесь стоит ошибок. */
.pc-code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 26px; letter-spacing: .34em; text-align: center; padding: 14px 12px;
}

/* Экран-«шлагбаум»: кроме ввода кода на нём делать нечего. */
.pc-gate { display: grid; place-items: center; min-height: 60vh; }
.pc-gate__card { width: min(420px, 100%); text-align: center; align-items: center; }
.pc-gate__icon { font-size: 34px; line-height: 1; }
.pc-gate__title { margin: 6px 0 2px; font-size: 20px; font-weight: 800; color: var(--color-heading); }
.pc-gate__sub { margin: 0 0 6px; font-size: 13.5px; color: var(--color-muted); }
.pc-gate__form { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.pc-gate__hint { margin: 0; font-size: 12px; color: var(--color-faint); }

/* =========================================================================
   Сезонное оформление : зима (единственный сезон продукта)
   Настройка продукта (PlatformSettings.seasonal_theme) -> data-season на <html>.
   Слой строго декоративный: ни один токен читаемости (текст, границы,
   primary) здесь не переопределяется — сезон добавляет только свои
   --season-*, поэтому контраст AA дизайн-системы остаётся нетронутым.
   Весна/лето/осень убраны по решению заказчика (02.08.2026); структура
   осталась посезонной, чтобы вернуть сезон = добавить один блок.
   ========================================================================= */
html[data-season] {
  /* Значения по умолчанию; сезон ниже переопределяет их целиком. */
  --season-accent: var(--color-primary);
  --season-wash: transparent;
  --season-particle: var(--color-primary);
  --season-particle-2: var(--color-primary);
  --season-alpha: .5;
}

/* Светлая тема: снежинки должны читаться НА светлом полотне, поэтому они
   здесь голубые, а не белые (белое на #EEF0F4 не видно вовсе). */
html[data-season="winter"] {
  --season-accent: #6BA5FF;
  --season-wash: rgba(107, 165, 255, .12);
  --season-particle: #7FB0FF;
  --season-particle-2: #C3DAFF;
  --season-alpha: .66;
}

/* Тёмная тема: полотно #12141C — снежинки светлее и чуть прозрачнее. */
html[data-theme="dark"][data-season="winter"] {
  --season-accent: #9CC4FF;
  --season-wash: rgba(120, 170, 255, .10);
  --season-particle: #E6F0FF;
  --season-particle-2: #B9D5FF;
  --season-alpha: .46;
}

/* ---- Тихие акценты (работают и с выключенной анимацией) ----------------- */
/* Хайрлайн под шапкой — самый спокойный носитель сезона: он не спорит с
   контентом и виден на каждой странице. */
html[data-season] .topbar::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  pointer-events: none;
  background: linear-gradient(90deg,
    transparent, var(--season-accent) 24%, var(--season-accent) 76%, transparent);
  opacity: .7;
}
/* Подсветка полотна сверху. Именно background-image на .app-main, а не
   псевдоэлемент: .app-main — flex-контейнер, лишний ::before стал бы его
   flex-элементом и сдвинул шапку. */
html[data-season] .app-main {
  background-image: radial-gradient(120% 40% at 50% 0, var(--season-wash), transparent 70%);
  background-repeat: no-repeat;
}

/* ---- Слой частиц -------------------------------------------------------- */
/* z-index 26: выше карточек страницы, но ниже шапки (30), дропдаунов (40+) и
   модалок (120+) — снег идёт «за стеклом» интерфейса, а не поверх диалога. */
.season-layer {
  position: fixed; inset: 0; z-index: 26;
  pointer-events: none; overflow: hidden;
}

/* Три рисунка снежинки. Это маски (важен только альфа-канал, цвет берётся из
   --season-particle), поэтому одна и та же картинка красится под обе темы.
   Шестилучевые: узор повторяется каждые 60°, за счёт этого вращение можно
   зациклить без видимого рывка (см. --spin в app.js). Data-URI разрешён
   политикой CSP: img-src 'self' data: (app/common/csp.py). */
html[data-season="winter"] {
  /* Ажурная — самая крупная и подробная. */
  --flake-1: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M12 1.5v21M2.9 6.75l18.2 10.5M2.9 17.25l18.2-10.5'/%3E%3Cpath d='M12 5.6 9.4 3M12 5.6l2.6-2.6M12 18.4 9.4 21M12 18.4l2.6 2.6'/%3E%3Cpath d='M6.7 8.6 3.1 8.5M6.7 8.6 4.8 11.7M17.3 15.4l3.6.1M17.3 15.4l1.9-3.1'/%3E%3Cpath d='M6.7 15.4 3.1 15.5M6.7 15.4 4.8 12.3M17.3 8.6l3.6-.1M17.3 8.6l1.9 3.1'/%3E%3C/g%3E%3C/svg%3E");
  /* Простая шестилучевая — для средних частиц. */
  --flake-2: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round'%3E%3Cpath d='M12 2.5v19M4 7.25l16 9.5M4 16.75l16-9.5'/%3E%3Cpath d='M12 7.4 9.9 5.2M12 7.4l2.1-2.2M12 16.6l-2.1 2.2M12 16.6l2.1 2.2'/%3E%3C/g%3E%3C/svg%3E");
  /* Три толстых луча — мелкие частицы дальнего плана: тонкий узор на 7 пикселях
     всё равно превращается в кашу, а жирная звёздочка читается снежинкой. */
  --flake-3: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23fff' stroke-width='3.4' stroke-linecap='round'%3E%3Cpath d='M12 4v16M5.1 8 18.9 16M5.1 16 18.9 8'/%3E%3C/g%3E%3C/svg%3E");
}

/* Одна частица — это контейнер, который падает, и ::before, который сам по
   себе качается вбок и вращается. Разделение принципиально: падение,
   покачивание и вращение — три независимых движения, а склеить их в один
   transform нельзя (последняя анимация перетёрла бы предыдущие). Контейнер
   двигает transform, снежинка внутри — отдельные свойства translate и rotate,
   поэтому они не конфликтуют. */
.season-particle {
  position: absolute;
  top: calc(-12vh * var(--vu));
  left: calc(var(--x) * 1vw * var(--vu));
  width: calc(var(--size) * 15px); height: calc(var(--size) * 15px);
  opacity: 0;
  will-change: transform;
  animation: season-fall var(--dur) linear var(--delay) infinite;
}

/* Рисунок частицы привязан к сезону: без своего блока сезон нарисует пустоту,
   а не голый квадрат без маски. */
html[data-season="winter"] .season-particle::before {
  content: "";
  display: block; width: 100%; height: 100%;
  background: linear-gradient(150deg, var(--season-particle-2), var(--season-particle));
  -webkit-mask: var(--flake-1) center / contain no-repeat;
  mask: var(--flake-1) center / contain no-repeat;
  /* Мелкие снежинки размываем сильнее: получается глубина кадра, дальний
     план не спорит с данными. max() — страховка от отрицательного blur. */
  filter: blur(max(0px, calc((1.15 - var(--size)) * .7px)));
  animation:
    season-sway var(--sway-dur) ease-in-out var(--delay) infinite alternate,
    season-turn var(--turn-dur) linear var(--delay) infinite;
}
html[data-season="winter"] .season-particle[data-flake="2"]::before {
  -webkit-mask-image: var(--flake-2);
  mask-image: var(--flake-2);
}
html[data-season="winter"] .season-particle[data-flake="3"]::before {
  -webkit-mask-image: var(--flake-3);
  mask-image: var(--flake-3);
}

@keyframes season-fall {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  8%   { opacity: var(--season-alpha); }
  88%  { opacity: var(--season-alpha); }
  100% {
    transform: translate3d(calc(var(--drift) * 1vw * var(--vu)), calc(126vh * var(--vu)), 0);
    opacity: 0;
  }
}
/* Покачивание влево-вправо: alternate замыкает цикл без рывка. */
@keyframes season-sway {
  from { translate: calc(var(--sway) * -1px) 0; }
  to   { translate: calc(var(--sway) * 1px) 0; }
}
/* Вращение всегда кратно 60° — на шестилучевом узоре стык кадров незаметен. */
@keyframes season-turn {
  from { rotate: 0deg; }
  to   { rotate: var(--spin); }
}

/* Движение выключено системно — слой остаётся, но пустым: initSeasonDecor не
   создаёт ни одной частицы. Правило ниже страхует случай, когда настройку
   поменяли уже после отрисовки. */
@media (prefers-reduced-motion: reduce) {
  .season-particle, .season-particle::before { animation: none; opacity: 0; }
}

/* ---- Выбор сезона на странице настроек ---------------------------------- */
.season-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
}
.season-pick { position: relative; display: block; cursor: pointer; }
/* Радио скрыт визуально, но остаётся фокусируемым: display:none выключил бы
   клавиатуру (та же ловушка, что со скрытым .checkbox input). */
.season-pick input {
  position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0;
}
.season-pick__body {
  display: flex; flex-direction: column; gap: 8px; height: 100%;
  padding: 12px; border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface);
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease), transform .16s var(--ease);
}
.season-pick:hover .season-pick__body { transform: translateY(-2px); box-shadow: var(--shadow-sm); }
.season-pick input:checked + .season-pick__body {
  border-color: var(--color-primary); box-shadow: var(--ring-soft);
}
.season-pick input:focus-visible + .season-pick__body { box-shadow: var(--ring); }
.season-pick__swatch {
  height: 58px; border-radius: var(--radius-md); position: relative; overflow: hidden;
  background: var(--color-surface-soft);
}
.season-pick__name { font-weight: 700; font-size: 14px; color: var(--color-heading); }
.season-pick__hint { font-size: 12px; color: var(--color-muted); line-height: 1.35; }

/* Превью варианта: заливка + сами снежинки картинкой (тот же рисунок, что
   падает по экрану), без единого лишнего узла. Здесь это background-image, а
   не маска, поэтому в SVG зашит белый цвет — он и нужен на ледяной заливке. */
.season-pick__swatch {
  --flake-1: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M12 1.5v21M2.9 6.75l18.2 10.5M2.9 17.25l18.2-10.5'/%3E%3Cpath d='M12 5.6 9.4 3M12 5.6l2.6-2.6M12 18.4 9.4 21M12 18.4l2.6 2.6'/%3E%3Cpath d='M6.7 8.6 3.1 8.5M6.7 8.6 4.8 11.7M17.3 15.4l3.6.1M17.3 15.4l1.9-3.1'/%3E%3Cpath d='M6.7 15.4 3.1 15.5M6.7 15.4 4.8 12.3M17.3 8.6l3.6-.1M17.3 8.6l1.9 3.1'/%3E%3C/g%3E%3C/svg%3E");
  --flake-2: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round'%3E%3Cpath d='M12 2.5v19M4 7.25l16 9.5M4 16.75l16-9.5'/%3E%3Cpath d='M12 7.4 9.9 5.2M12 7.4l2.1-2.2M12 16.6l-2.1 2.2M12 16.6l2.1 2.2'/%3E%3C/g%3E%3C/svg%3E");
}
.season-pick__swatch[data-swatch="off"] {
  background:
    repeating-linear-gradient(135deg,
      var(--color-surface-soft) 0 8px, var(--color-surface-alt) 8px 16px);
}
/* «Авто» — половина ледяная, половина строгая: зимой снег, в остальное время нет. */
.season-pick__swatch[data-swatch="auto"] {
  background:
    var(--flake-2) 20% 34% / 15px no-repeat,
    var(--flake-1) 36% 74% / 12px no-repeat,
    linear-gradient(102deg, #6BA5FF 0 52%, var(--color-surface-alt) 52%);
}
.season-pick__swatch[data-swatch="winter"] {
  background:
    var(--flake-1) 24% 32% / 17px no-repeat,
    var(--flake-2) 62% 22% / 12px no-repeat,
    var(--flake-1) 76% 70% / 13px no-repeat,
    linear-gradient(160deg, #A9CCFF, #6BA5FF);
}

/* ---- Баннер «Вы смотрите как …» (режим поддержки) ------------------------ */
.imp-banner {
  position: sticky; top: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center; gap: 14px;
  padding: 9px 16px; flex-wrap: wrap;
  background: repeating-linear-gradient(
    -45deg,
    var(--color-warning-soft), var(--color-warning-soft) 14px,
    color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft)) 14px,
    color-mix(in srgb, var(--color-warning) 16%, var(--color-warning-soft)) 28px
  );
  border-bottom: 1px solid var(--color-warning);
  color: var(--color-warning); font-size: 13px; font-weight: 650;
}
.imp-banner__btn {
  padding: 5px 12px; border: 1px solid var(--color-warning);
  border-radius: var(--radius-pill); background: var(--color-surface);
  color: var(--color-warning); font: inherit; font-weight: 700; cursor: pointer;
}
.imp-banner__btn:hover { background: var(--color-warning); color: #fff; }

.pc-row-actions { display: flex; gap: 6px; flex-wrap: wrap; }

/* ---- «Требует внимания» на обзоре консоли -------------------------------- */
.pc-attn { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.pc-attn__item {
  display: flex; flex-direction: column; gap: 2px;
  padding: 10px 13px; border-radius: var(--radius-md);
  border-left: 3px solid var(--color-warning);
  background: var(--color-warning-soft);
}
.pc-attn__item--danger { border-left-color: var(--color-danger); background: var(--color-danger-soft); }
.pc-attn__title { font-size: 13.5px; font-weight: 700; color: var(--color-heading); text-decoration: none; }
.pc-attn__title:hover { text-decoration: underline; }
.pc-attn__hint { font-size: 12px; color: var(--color-muted); }

/* =========================================================================
   Оживление сводной панели (stage 110)

   Всё «начальное скрытие» живёт ТОЛЬКО под [data-dash-motion] — атрибут
   ставит initDashMotion в app.js. Без JS (и при prefers-reduced-motion, где
   модуль просто не включается) разметка остаётся полностью видимой: это
   прогрессивное улучшение, а не условие отрисовки.

   fill-mode у появлений — backwards, не both/forwards: завершившаяся
   filling-анимация opacity навсегда оставляет stacking context, и fixed-
   оверлеи внутри .page рисуются под sidebar/topbar (известная ловушка,
   см. комментарий к .fade-in выше). Поэтому «до показа» элемент прячет
   отдельное правило :not(.is-in), а после анимации он возвращается к
   обычному состоянию без всякой заливки.
   ========================================================================= */
@keyframes dash-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
/* Иконка метрики: короткий «щелчок» вместо простого появления. */
@keyframes dash-icon-in {
  from { opacity: 0; transform: scale(.55) rotate(-14deg); }
  62%  { opacity: 1; transform: scale(1.09) rotate(3deg); }
  to   { opacity: 1; transform: none; }
}
/* Отрисовка линейного графика: двигается только правая грань. */
@keyframes dash-wipe {
  from { clip-path: inset(-30% 100% -30% -8%); }
  to   { clip-path: inset(-30% -8% -30% -8%); }
}
/* Точка конца серии на линейном графике позиционируется через translate(-50%,-50%),
   поэтому сдвиг обязан остаться внутри кадров — иначе маркер уедет с линии. */
@keyframes dash-dot-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(.2); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
/* Два круга внимания на критичной карточке — не бесконечная пульсация:
   в рабочем инструменте вечно мигающий блок быстро становится шумом. */
@keyframes dash-attn {
  0%, 100% { box-shadow: var(--shadow-card), 0 0 0 0 color-mix(in srgb, var(--color-danger) 40%, transparent); }
  50%      { box-shadow: var(--shadow-card), 0 0 0 7px color-mix(in srgb, var(--color-danger) 0%, transparent); }
}
/* Подсветка виджета, который только что приехал живым обновлением. */
@keyframes dash-flash {
  from { box-shadow: var(--shadow-card), inset 0 0 0 100px color-mix(in srgb, var(--color-primary) 12%, transparent); }
  to   { box-shadow: var(--shadow-card), inset 0 0 0 100px transparent; }
}

/* ---- Каскадное появление карточек -------------------------------------- */
[data-dash-motion] [data-dash-reveal]:not(.is-in) { opacity: 0; }
[data-dash-motion] [data-dash-reveal].is-in {
  animation: dash-rise .52s var(--ease) var(--d, 0ms) backwards;
}
/* Календарь отчётов собирается отдельным сценарием — см. блок «Сборка панели
   отчётов OZON» ниже. Здесь только ветка на случай месячной сетки: разметка
   блока однажды менялась, и правило безвредно, если таких ячеек нет. */
[data-dash-motion] .repcal.is-in .repday--mark {
  animation: dash-icon-in .42s var(--ease) 280ms backwards;
}
[data-dash-motion] .dash-metric.is-in .metric-card__icon {
  animation: dash-icon-in .5s var(--ease) calc(var(--d, 0ms) + 120ms) backwards;
}

/* ---- Графики оживают вместе со своей карточкой -------------------------- */
/* Полосы: ширину держим на нуле, пока карточка не показана, дальше её
   доводит уже существующий transition width .5s (плюс каскад по строкам). */
[data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-chart__fill { width: 0; }
[data-dash-motion] .dash-chart__fill { transition-delay: var(--rd, 0ms); }

/* Линия и её заливка проявляются слева направо — как будто график чертят по
   ходу времени. Двигается только правая грань clip-path; остальные грани
   сразу вынесены за края, иначе обрезался бы толстый штрих у верхней и
   нижней точек серии. */
[data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-linechart__series {
  clip-path: inset(-30% 100% -30% -8%);
}
[data-dash-motion] [data-dash-reveal].is-in .dash-linechart__series {
  animation: dash-wipe 1.05s var(--ease) calc(var(--d, 0ms) + 80ms) backwards;
}
[data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-linechart__dot--end { opacity: 0; }
[data-dash-motion] [data-dash-reveal].is-in .dash-linechart__dot--end {
  animation: dash-dot-in .34s var(--ease) calc(var(--d, 0ms) + 1060ms) backwards;
}

/* Кольцо доната анимирует JS через Web Animations API (см. drawDonut в
   app.js): длина сегмента живёт в инлайновом stroke-dasharray, а собственный
   transition этого правила перехватил бы «сжатие в ноль». */

/* ---- Сборка панели «Отчёты OZON» ----------------------------------------
   Панель — единственная цель появления (initDashMotion берёт .repcal целиком),
   всё внутри едет от неё фиксированными задержками. Отдельно прятать
   внутренности не нужно: пока панель не показана, её не видно вообще, а все
   анимации ниже держат своё начальное состояние через fill: backwards.

   Метафора блока — отрывной календарь, поэтому главный акцент один: число
   срока перекидывается, как страница, а шапка с месяцем въезжает сверху.
   Сам листок при этом никуда не летит — иначе движений в мелком элементе
   становится больше, чем смысла.
   ------------------------------------------------------------------------ */
@keyframes repcal-glow {
  from { opacity: 0; transform: translateX(-28px); }
  to   { opacity: 1; transform: none; }
}
@keyframes repcal-line-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes repcal-pop-in {
  from { opacity: 0; transform: scale(.86); }
  to   { opacity: 1; transform: none; }
}
@keyframes repcal-month-in {
  from { transform: translateY(-100%); }
  to   { transform: none; }
}
/* Перекидывание страницы: разворот вокруг верхнего края листка. */
@keyframes repcal-flip {
  from { opacity: 0; transform: rotateX(-88deg); }
  to   { opacity: 1; transform: none; }
}
/* Срок открыт — кнопка загрузки один раз просит внимания (два круга, не
   бесконечная пульсация: вечно мигающая кнопка в рабочем экране — шум). */
@keyframes repcal-cta-attn {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-warning) 46%, transparent); }
  50%      { box-shadow: 0 0 0 9px color-mix(in srgb, var(--color-warning) 0%, transparent); }
}

/* Цветная «заря» слева проявляется со сдвигом — панель как будто наливается
   своим состоянием (синее / жёлтое при открытом сроке / зелёное). */
[data-dash-motion] .repcal.is-in::before {
  animation: repcal-glow .8s var(--ease) 120ms backwards;
}
[data-dash-motion] .repcal.is-in .repcal__title { animation: repcal-line-in .42s var(--ease) 90ms backwards; }
[data-dash-motion] .repcal.is-in .repcal__summary { animation: repcal-line-in .42s var(--ease) 150ms backwards; }
[data-dash-motion] .repcal.is-in .repcal__cta { animation: repcal-pop-in .4s var(--ease) 210ms backwards; }

/* Шаг каскада по срокам. Сроков всегда единицы (отчёт по оборотам + заявления
   о зачёте), поэтому хватает четырёх ступеней, остальные делят первую. */
[data-dash-motion] .repcal.is-in .repslot { --slot-d: 250ms; }
[data-dash-motion] .repcal.is-in .repslot:nth-child(2) { --slot-d: 330ms; }
[data-dash-motion] .repcal.is-in .repslot:nth-child(3) { --slot-d: 410ms; }
[data-dash-motion] .repcal.is-in .repslot:nth-child(4) { --slot-d: 490ms; }

[data-dash-motion] .repcal.is-in .repslot {
  animation: repcal-line-in .46s var(--ease) var(--slot-d) backwards;
}
[data-dash-motion] .repcal.is-in .repslot__month {
  animation: repcal-month-in .42s var(--ease) calc(var(--slot-d) + 110ms) backwards;
}
[data-dash-motion] .repcal.is-in .repslot__day {
  animation: repcal-flip .5s var(--ease) calc(var(--slot-d) + 170ms) backwards;
}
[data-dash-motion] .repcal.is-in .repslot__fact {
  animation: repcal-line-in .4s var(--ease) calc(var(--slot-d) + 210ms) backwards;
}
[data-dash-motion] .repcal.is-in .repslot__when {
  animation: repcal-pop-in .36s var(--ease) calc(var(--slot-d) + 250ms) backwards;
}
[data-dash-motion] .repcal.is-in .repslot__action {
  animation: repcal-line-in .36s var(--ease) calc(var(--slot-d) + 300ms) backwards;
}
/* Кнопка просит внимания только когда срок реально открыт и загрузка нужна. */
[data-dash-motion] .repcal--due.is-in .repcal__cta {
  animation: repcal-pop-in .4s var(--ease) 210ms backwards,
             repcal-cta-attn 1.5s var(--ease) 1100ms 2 backwards;
}

/* Перспектива нужна для перекидывания числа; на статике не влияет ни на что. */
.repslot__leaf { perspective: 320px; }
.repslot__day { transform-origin: top center; }
.repslot__leaf { transition: transform .22s var(--ease); }
.repslot:hover .repslot__leaf { transform: translateY(-2px) rotate(-2.5deg); }

/* ---- Живое обновление и критичные карточки ------------------------------ */
.dash-widget.is-fresh, .dash-metric.is-fresh { animation: dash-flash 1.1s var(--ease) backwards; }
[data-dash-motion] .dash-widget--critical.is-in,
[data-dash-motion] .dash-metric--critical.is-in {
  animation: dash-rise .52s var(--ease) var(--d, 0ms) backwards,
             dash-attn 1.5s var(--ease) calc(var(--d, 0ms) + 620ms) 2 backwards;
}

/* ---- Мелкая моторика: наведение ----------------------------------------- */
.metric-card__icon { transition: transform .22s var(--ease); }
.metric-card:hover .metric-card__icon { transform: scale(1.07) rotate(-3deg); }
.dash-list__item { transition: background-color .16s var(--ease); border-radius: var(--radius-md); }
/* Плитка и без наведения лежит на мягком фоне, поэтому ховер подкрашивается
   синим — иначе строка под курсором никак не отзывалась. Подсветка только у
   плиток со ссылкой: у справочной строки она обещала клик, которого нет. */
.dash-list__item[data-row-href]:hover { background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface-soft)); }
.sidebar-item__icon { transition: transform .18s var(--ease), opacity .18s var(--ease); }
.sidebar-item:hover .sidebar-item__icon { transform: translateX(2px); }

@media (prefers-reduced-motion: reduce) {
  /* Модуль и так не включается, но страхуемся: ни одно правило выше не
     должно оставить контент невидимым, если атрибут всё-таки проставлен. */
  [data-dash-motion] [data-dash-reveal]:not(.is-in) { opacity: 1; }
  [data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-chart__fill { width: var(--w, 0); }
  [data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-linechart__series { clip-path: none; }
  [data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-linechart__dot--end { opacity: 1; }
  [data-dash-motion] [data-dash-reveal],
  [data-dash-motion] [data-dash-reveal].is-in,
  [data-dash-motion] [data-dash-reveal].is-in .dash-linechart__series,
  [data-dash-motion] [data-dash-reveal].is-in .dash-linechart__dot--end,
  [data-dash-motion] .repcal.is-in .repday--mark,
  [data-dash-motion] .repcal.is-in::before,
  [data-dash-motion] .repcal.is-in .repcal__title,
  [data-dash-motion] .repcal.is-in .repcal__summary,
  [data-dash-motion] .repcal.is-in .repcal__cta,
  [data-dash-motion] .repcal--due.is-in .repcal__cta,
  [data-dash-motion] .repcal.is-in .repslot,
  [data-dash-motion] .repcal.is-in .repslot__month,
  [data-dash-motion] .repcal.is-in .repslot__day,
  [data-dash-motion] .repcal.is-in .repslot__fact,
  [data-dash-motion] .repcal.is-in .repslot__when,
  [data-dash-motion] .repcal.is-in .repslot__action,
  [data-dash-motion] .dash-metric.is-in .metric-card__icon,
  .dash-widget.is-fresh, .dash-metric.is-fresh { animation: none; }
  .metric-card__icon, .sidebar-item__icon, .repslot__leaf { transition: none; }
  .metric-card:hover .metric-card__icon,
  .repslot:hover .repslot__leaf { transform: none; }
}

/* Печать: до карточки могли не доскроллить, и на бумаге она осталась бы
   белым пятном. Появления на печати не нужны — показываем всё разом. */
@media print {
  [data-dash-motion] [data-dash-reveal]:not(.is-in) { opacity: 1; }
  [data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-chart__fill { width: var(--w, 0); }
  [data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-linechart__series { clip-path: none; }
  [data-dash-motion] [data-dash-reveal]:not(.is-in) .dash-linechart__dot--end { opacity: 1; }
}

/* =========================================================================
   Stage 110 — PWA: установка на телефон, нижняя таб-панель, safe-area

   Задача этапа — чтобы система, поставленная на домашний экран, не выглядела
   «сайтом без адресной строки»: контент обязан уходить под чёлку, а не
   заканчиваться белой полосой; главное — под большим пальцем; системная
   строка статуса — цвета полотна.
   ========================================================================= */

/* ---- Безопасные зоны ----------------------------------------------------
   env(safe-area-inset-*) даёт нули везде, где вырезов нет (и во всех
   десктопных браузерах), поэтому правила безусловны: отдельная ветка под
   standalone только плодила бы расхождение между вкладкой и приложением.
   Работают они лишь в паре с viewport-fit=cover в base.html.

   Боковой отступ прописан через переменную, а не константой в max(): у .page и
   .topbar выше по файлу есть мобильный брейкпоинт (16px / 0 14px), и правило
   с зашитым десктопным числом молча раздуло бы поля на телефоне — там, где
   каждый пиксель ширины на счету.
   ------------------------------------------------------------------------ */
.topbar {
  --edge-pad: 22px;
  padding-top: env(safe-area-inset-top);
  height: calc(var(--topbar-h) + env(safe-area-inset-top));
  padding-left: max(var(--edge-pad), env(safe-area-inset-left));
  padding-right: max(var(--edge-pad), env(safe-area-inset-right));
}
.sidebar { padding-top: env(safe-area-inset-top); }
.page {
  --edge-pad: 28px;
  padding-left: max(var(--edge-pad), env(safe-area-inset-left));
  padding-right: max(var(--edge-pad), env(safe-area-inset-right));
}
@media (max-width: 720px) {
  .page { --edge-pad: 16px; }
  .topbar { --edge-pad: 14px; }
}

/* Шапка тянется во всю ширину полотна (фон и нижняя кромка обязаны доходить
   до краёв), но её содержимое выравнивается по колонке страницы — иначе на
   широком мониторе поиск и аватар разъезжаются к краям экрана, а карточки
   стоят посередине. Отступ = поле до колонки + собственный отступ .page.
   min() со стороной колонки не нужен: пока полотно уже --page-max, разность
   отрицательна и max() возвращает базовые 22px. */
@media (min-width: 820px) {
  .topbar { --edge-pad: max(22px, calc((100% - var(--page-max)) / 2 + 28px)); }
}

/* Установленное приложение не должно «оттягиваться» как страница: жест
   pull-to-refresh поверх HTMX-интерфейса перезагружает экран ровно тогда,
   когда человек хотел проскроллить список смен вверх. */
html[data-display-mode="standalone"],
html[data-display-mode="standalone"] body {
  overscroll-behavior-y: none;
}

/* ---- Нижняя таб-панель --------------------------------------------------- */
.tabbar {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  background: color-mix(in srgb, var(--color-bg) 86%, transparent);
  backdrop-filter: saturate(160%) blur(18px);
  -webkit-backdrop-filter: saturate(160%) blur(18px);
  border-top: 1px solid var(--color-border);
  /* Полоска «домой» на iPhone: без этого отступа нижний ряд иконок оказывается
     ровно под ней и не нажимается. */
  padding-bottom: env(safe-area-inset-bottom);
}
.tabbar__item {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; padding: 7px 2px 6px; min-height: 54px;
  background: none; border: 0; color: var(--color-muted);
  font: inherit; font-size: 10px; font-weight: 600; text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.tabbar__icon-wrap {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 26px; border-radius: var(--radius-pill);
  transition: background-color .2s var(--ease);
}
/* Подсветка активной вкладки — «таблетка» под иконкой, как в нативных
   приложениях. Отдельный слой, а не фон самой ссылки: иначе подсветка
   растягивалась бы на подпись и ряд выглядел бы полосатым. */
.tabbar__item.is-active .tabbar__icon-wrap { background: var(--color-primary-soft); }
.tabbar__icon { width: 21px; height: 21px; transition: transform .2s var(--ease); }
.tabbar__item.is-active { color: var(--color-primary-dark); }
.tabbar__item.is-active .tabbar__icon { transform: translateY(-1px); }
.tabbar__item:active .tabbar__icon { transform: scale(.88); }
.tabbar__label {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  letter-spacing: .01em;
}
/* Бейдж непрочитанного приходит из общих include'ов и по умолчанию встроен в
   строку — в таб-панели он обязан висеть углом над иконкой. */
.tabbar__icon-wrap .badge {
  position: absolute; top: -3px; right: 4px; min-width: 16px; height: 16px;
  padding: 0 4px; font-size: 9.5px; line-height: 16px; border-radius: var(--radius-pill);
  pointer-events: none;
}

@media (max-width: 819px) {
  .tabbar { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; }
  /* Место под панель: без этого последняя карточка списка прячется под ней,
     а «Сохранить» внизу формы становится ненажимаемым. */
  .page { padding-bottom: calc(74px + env(safe-area-inset-bottom)); }
  /* Плавающие элементы поднимаем над панелью. */
  .fb-fab { bottom: calc(84px + env(safe-area-inset-bottom)); }
  .toast-stack { bottom: calc(84px + env(safe-area-inset-bottom)); }
  /* Drawer полного меню выезжает поверх панели. */
  .sidebar { z-index: 45; }
  .sidebar-scrim { z-index: 44; }
}

/* ---- Шторка «поставьте приложение» -------------------------------------- */
.pwa-sheet { position: fixed; inset: 0; z-index: 130; }
.pwa-sheet[hidden] { display: none; }
.pwa-sheet__scrim {
  position: absolute; inset: 0; border: 0; padding: 0;
  background: rgba(6, 10, 20, .55); backdrop-filter: blur(2px);
  opacity: 0; transition: opacity .26s var(--ease);
}
.pwa-sheet.is-open .pwa-sheet__scrim { opacity: 1; }
.pwa-sheet__panel {
  position: absolute; left: 0; right: 0; bottom: 0;
  width: min(460px, 100%); margin-inline: auto;
  max-height: calc(94vh * var(--vu)); max-height: 94dvh; overflow-y: auto;
  padding: 8px 22px calc(18px + env(safe-area-inset-bottom));
  background: var(--color-surface);
  border: 1px solid var(--color-border); border-bottom: 0;
  border-radius: 26px 26px 0 0;
  box-shadow: 0 -18px 60px rgba(4, 8, 18, .38);
  transform: translateY(102%);
  transition: transform .32s var(--ease-out);
}
.pwa-sheet.is-open .pwa-sheet__panel { transform: none; }
/* На десктопе шторка вызывается только вручную из меню профиля — там она
   читается как карточка, а не как приклеенная к краю панель. */
@media (min-width: 820px) {
  .pwa-sheet__panel { bottom: 26px; border-radius: 26px; border-bottom: 1px solid var(--color-border); }
}

.pwa-sheet__grab {
  display: block; width: 42px; height: 4px; margin: 6px auto 14px;
  border: 0; padding: 0; border-radius: var(--radius-pill);
  background: var(--color-border-strong); cursor: pointer;
}
.pwa-sheet__head { display: flex; align-items: center; gap: 14px; }
.pwa-sheet__icon {
  width: 60px; height: 60px; flex: 0 0 auto;
  /* Ровно та же доля, что у самого PNG (CORNER в scripts генерации иконок).
     Фиксированные 16px на 60px — это 27%, то есть CSS обрезал бы уже
     скруглённую картинку ВТОРОЙ раз, срезая углы подложки полумесяцем. */
  border-radius: 22%;
  box-shadow: 0 10px 26px color-mix(in srgb, var(--color-primary) 28%, transparent);
}
.pwa-sheet__title { font-size: 19px; font-weight: 800; }
.pwa-sheet__subtitle { margin-top: 3px; font-size: 13px; color: var(--color-muted); }

.pwa-perks { list-style: none; margin: 18px 0 4px; padding: 0; display: grid; gap: 12px; }
.pwa-perks__item { display: flex; align-items: flex-start; gap: 11px; font-size: 13px; color: var(--color-muted); }
.pwa-perks__item svg { width: 19px; height: 19px; flex: 0 0 auto; margin-top: 1px; color: var(--color-primary); }
.pwa-perks__item strong { color: var(--color-text); font-weight: 700; }

/* Отбивка перед шагами: без неё «зачем» и «как» сливаются в один поток
   одинаковых строк, и глазу не за что зацепиться. */
.pwa-steps {
  list-style: none; margin: 20px 0 0; padding: 20px 0 0; display: grid; gap: 13px;
  border-top: 1px solid var(--color-border);
}
.pwa-steps__item { display: flex; align-items: flex-start; gap: 12px; }
.pwa-steps__num {
  flex: 0 0 auto; width: 25px; height: 25px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--color-primary); color: var(--color-on-primary);
  font-size: 12.5px; font-weight: 800;
}
.pwa-steps__text { font-size: 13.5px; line-height: 1.55; padding-top: 2px; }
/* Клавиша-подсказка: рисуем кнопку из интерфейса телефона прямо в тексте —
   словами «значок поделиться» человек её на экране не находит. */
.pwa-key {
  display: inline-flex; align-items: center; gap: 5px; vertical-align: -4px;
  padding: 3px 7px; margin: 0 2px; border-radius: 8px;
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  color: var(--color-primary-dark); font-size: 12.5px; font-weight: 700;
}
.pwa-key svg { width: 15px; height: 15px; }
.pwa-key--wide { color: var(--color-text); }

.pwa-sheet__cta { width: 100%; margin-top: 18px; justify-content: center; }
.pwa-sheet__note { margin-top: 10px; font-size: 12px; color: var(--color-faint); text-align: center; }
.pwa-sheet__note--warn { color: var(--color-warning); text-align: left; }
.pwa-sheet__later {
  display: block; width: 100%; margin-top: 12px; padding: 10px;
  background: none; border: 0; color: var(--color-faint);
  font: inherit; font-size: 13px; font-weight: 600;
}
.pwa-sheet__later:hover { color: var(--color-text); }
/* display в правиле выше сильнее браузерного [hidden] { display: none } —
   без этой строки «Позже» остаётся на экране в ветках с инструкцией. */
.pwa-sheet__later[hidden] { display: none; }

/* ---- Мягкий запрос разрешения на уведомления ----------------------------- */
.pwa-nudge {
  position: fixed; z-index: 125; left: 14px; right: 14px;
  bottom: calc(84px + env(safe-area-inset-bottom));
  width: min(420px, calc(100% - 28px)); margin-inline: auto;
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 15px; border-radius: var(--radius-lg);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-pop);
  opacity: 0; transform: translateY(14px);
  transition: opacity .24s var(--ease), transform .24s var(--ease-out);
}
.pwa-nudge[hidden] { display: none; }
.pwa-nudge.is-open { opacity: 1; transform: none; }
.pwa-nudge__icon { width: 22px; height: 22px; flex: 0 0 auto; color: var(--color-primary); margin-top: 2px; }
.pwa-nudge__body { flex: 1; min-width: 0; display: grid; gap: 3px; font-size: 12.5px; color: var(--color-muted); }
.pwa-nudge__body strong { font-size: 13.5px; color: var(--color-text); }
.pwa-nudge__actions { display: flex; flex-direction: column; gap: 6px; flex: 0 0 auto; }
@media (max-width: 480px) {
  .pwa-nudge { flex-wrap: wrap; }
  .pwa-nudge__actions { flex-direction: row; width: 100%; }
  .pwa-nudge__actions .btn { flex: 1; justify-content: center; }
}
@media (min-width: 820px) {
  /* Выше кнопки обратной связи (.fb-fab: bottom 22px, высота 50): карточка
     шире её и с бо́льшим z-index, поэтому на 26px она просто накрывала бы
     кнопку, пока человек не ответит на вопрос про уведомления. */
  .pwa-nudge { bottom: 86px; left: auto; right: 26px; margin-inline: 0; }
}

/* Пока открыта шторка, фон не скроллим: иначе «Позже» уезжает из-под пальца. */
body.is-sheet-open { overflow: hidden; }

/* ---- Офлайн-заглушка ----------------------------------------------------- */
.offline {
  min-height: calc(100vh * var(--vu)); min-height: 100dvh;
  display: grid; place-items: center; padding: 24px;
}
.offline__stage { width: min(400px, 100%); }

/* Сцена с маскотом. Птица целиком над карточкой, а не заходит на неё: у позы
   «грусть» хвост опущен вниз, и любое перекрытие читается как обрезанная
   картинка, а не как «Литти сидит на карточке». Высота фиксирована — иначе
   карточка прыгает, пока грузится webp. */
.offline__scene {
  position: relative; height: 192px;
  display: grid; place-items: center; pointer-events: none;
}
.offline__mascot {
  position: relative; z-index: 1; width: 176px; height: auto;
  margin-bottom: 6px;
  filter: drop-shadow(0 16px 22px rgba(9, 11, 18, 0.26));
  animation: offlineFloat 5.4s ease-in-out infinite;
}
/* Мягкий свет за спиной — карточка внизу светлая, и без него птица «висит»
   в пустоте. */
.offline__glow {
  position: absolute; width: 230px; height: 230px; border-radius: 50%;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--color-primary) 20%, transparent) 0%, transparent 68%);
}
/* Дуга «сигнала», который не доходит: рисуется границей круга, из которой
   видна только верхняя часть. Пульсирует — экран выглядит живым, пока телефон
   ждёт сеть. */
.offline__wave {
  position: absolute; top: 6px; width: 168px; height: 168px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--color-primary) 42%, transparent);
  -webkit-mask-image: linear-gradient(180deg, #000 0 34%, transparent 52%);
  mask-image: linear-gradient(180deg, #000 0 34%, transparent 52%);
  animation: offlineWave 2.8s ease-out infinite;
}
.offline__shadow {
  position: absolute; bottom: 0; width: 108px; height: 12px; border-radius: 50%;
  background: radial-gradient(ellipse, rgba(9, 11, 18, 0.2), transparent 70%);
  animation: offlineShadow 5.4s ease-in-out infinite;
}
@keyframes offlineFloat {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50%      { transform: translateY(-9px) rotate(1.5deg); }
}
@keyframes offlineShadow {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(0.88); opacity: 0.7; }
}
@keyframes offlineWave {
  0%   { transform: scale(0.72); opacity: 0; }
  35%  { opacity: 0.9; }
  100% { transform: scale(1.25); opacity: 0; }
}

.offline__card {
  position: relative; width: 100%; text-align: center;
  padding: 28px 26px; border-radius: var(--radius-xl);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-pop);
}
.offline__title { font-size: 22px; font-weight: 800; letter-spacing: -0.01em; }
.offline__text { margin-top: 10px; font-size: 13.5px; line-height: 1.6; color: var(--color-muted); }
.offline__retry { width: 100%; margin-top: 20px; justify-content: center; }
.offline__hint {
  margin-top: 12px; font-size: 12px; color: var(--color-faint);
  display: flex; align-items: baseline; justify-content: center; gap: 1px;
}
/* Три точки «ждём» — единственный индикатор того, что страница не замерла. */
.offline__dots { display: inline-flex; align-items: baseline; gap: 3px; margin-left: 4px; }
.offline__dots i {
  width: 3px; height: 3px; border-radius: 50%; background: currentColor;
  animation: offlineDot 1.4s ease-in-out infinite;
}
.offline__dots i:nth-child(2) { animation-delay: 0.18s; }
.offline__dots i:nth-child(3) { animation-delay: 0.36s; }
@keyframes offlineDot {
  0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
  30%           { opacity: 1; transform: translateY(-2px); }
}
/* Почему экран пустой, а не «как вчера» — мелким шрифтом под чертой. */
.offline__note {
  margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--color-border);
  font-size: 11.5px; line-height: 1.55; color: var(--color-faint);
}

@media (prefers-reduced-motion: reduce) {
  .offline__mascot, .offline__shadow, .offline__wave, .offline__dots i { animation: none; }
  .offline__wave { opacity: 0.55; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .pwa-sheet__panel, .pwa-sheet__scrim, .pwa-nudge,
  .tabbar__icon, .tabbar__icon-wrap { transition: none; }
}

/* ---- Список чатов на телефоне (правка вместе со stage 110) ---------------
   На 375px строка не помещалась: дата «23.07.2026 10:30» держала ~105px в одну
   строку, колонка с именем ужималась до 64px, а role-badge (white-space:nowrap)
   не умеет сжиматься — он вылезал из своей колонки и наезжал прямо на дату.
   Лечим обе причины: дате разрешаем перенос на две строки, бейджу — обрезку.
   ------------------------------------------------------------------------ */
@media (max-width: 560px) {
  .chat-thread__link { gap: 10px; padding: 12px 8px; }
  .chat-thread__aside { max-width: 88px; }
  .chat-thread__aside .cell-muted { white-space: normal; text-align: right; line-height: 1.35; }
  .chat-thread__name .role-badge {
    /* inline-block, а не inline-flex: у флекс-контейнера text-overflow не
       работает, и «Бухгалтер / расчётчик» обрезался бы наотмашь, без
       многоточия — это читается как поломка вёрстки, а не как сокращение. */
    display: inline-block;
    min-width: 0; max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
}

/* =========================================================================
   Мастер настройки зарплаты и KPI (stage 111) — .qhero / .qstep / .qcard /
   .qpreview. Один экран вместо трёх страниц: слева четыре шага, справа
   липкий предпросмотр «сколько получит сотрудник». Все анимации выключаются
   в prefers-reduced-motion — блок в конце файла.
   ========================================================================= */
.qhero {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: 28px; margin-bottom: 16px;
  border-radius: var(--radius-2xl);
  border: 1px solid var(--color-border);
  background:
    radial-gradient(120% 160% at 100% 0%, color-mix(in srgb, var(--color-primary) 22%, transparent) 0%, transparent 62%),
    var(--color-surface);
  box-shadow: var(--shadow-card);
}
.qhero__eyebrow {
  display: inline-block; margin-bottom: 8px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-primary);
}
.qhero__title { font-size: 27px; font-weight: 800; line-height: 1.18; color: var(--color-heading); }
.qhero__text { margin-top: 10px; max-width: 62ch; font-size: 13.5px; line-height: 1.6; color: var(--color-muted); }
.qhero__facts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.qhero__fact {
  padding: 6px 12px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  font-size: 12.5px; color: var(--color-muted);
}
.qhero__fact b { color: var(--color-text); font-weight: 700; }
.qhero__fact--auto { background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface)); }

/* Три круга «пульса» — намёк на то, что система работает сама. */
.qhero__ring { position: relative; flex: 0 0 96px; width: 96px; height: 96px; }
.qhero__ring-dot {
  position: absolute; inset: 0; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
  animation: qpulse 3.4s ease-out infinite;
}
.qhero__ring-dot:nth-child(2) { animation-delay: 1.1s; }
.qhero__ring-dot:nth-child(3) { animation-delay: 2.2s; }
@keyframes qpulse {
  0% { transform: scale(.45); opacity: 0; }
  25% { opacity: .9; }
  100% { transform: scale(1); opacity: 0; }
}

.qhero--applied { border-color: color-mix(in srgb, var(--color-success) 55%, var(--color-border)); }
.qhero__done {
  position: absolute; right: 22px; bottom: 18px;
  display: flex; align-items: center; gap: 10px;
  padding: 8px 14px 8px 10px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-success) 16%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-success) 45%, var(--color-border));
  font-size: 13px; font-weight: 600; color: var(--color-text);
}
.qhero__check { width: 26px; height: 26px; stroke: var(--color-success); stroke-width: 4; }
.qhero__check-circle { stroke-dasharray: 152; stroke-dashoffset: 152; animation: qdraw .5s ease-out forwards; }
.qhero__check-mark {
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 48; stroke-dashoffset: 48;
  animation: qdraw .35s .4s ease-out forwards;
}
@keyframes qdraw { to { stroke-dashoffset: 0; } }

/* ---- Каркас: шаги + липкий предпросмотр --------------------------------- */
.qsetup { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin-top: 16px; }
@media (min-width: 1180px) {
  .qsetup { grid-template-columns: minmax(0, 1fr) 372px; align-items: start; }
  .qside { position: sticky; top: 16px; }
}
/* minmax(0, 1fr), а не просто min-width: 0 у контейнера: трек «auto» считается
   по min-content содержимого, и крупное поле ставки (26px, size по умолчанию)
   распирало колонку шире экрана — на 360px страница уезжала вбок. */
.qsetup__main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; min-width: 0; }
.qside { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; min-width: 0; }
.qrate__input { min-width: 0; }

.qstep {
  padding: 22px; border-radius: var(--radius-xl);
  background: var(--color-surface); border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
}
/* Появление шага — CSS, а не JS: rAF в скрытой вкладке не срабатывает, и
   форма осталась бы прозрачной. fill-mode both держит стартовое состояние. */
.qstep { animation: qstepin .45s ease var(--qstep-delay, 0ms) both; }
@keyframes qstepin { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.qstep__head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 16px; }
.qstep__num {
  flex: 0 0 32px; width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--color-primary); color: var(--color-on-primary);
  font-size: 14px; font-weight: 800;
}
.qstep__titles { min-width: 0; }
.qstep__title { font-size: 17px; font-weight: 700; color: var(--color-heading); }
.qstep__hint { margin-top: 5px; font-size: 12.5px; line-height: 1.5; color: var(--color-muted); }
.qstep__flag {
  margin-left: auto; flex: 0 0 auto; align-self: center;
  padding: 4px 10px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); border: 1px solid var(--color-border);
  font-size: 11.5px; color: var(--color-muted); white-space: nowrap;
}
.qstep__note { margin-top: 12px; font-size: 12.5px; line-height: 1.5; color: var(--color-muted); }

/* ---- Шаг 1: ставка ------------------------------------------------------ */
.qrate { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 16px; }
.qrate__amount { display: grid; gap: 6px; }
.qrate__label { font-size: 12px; font-weight: 600; color: var(--color-muted); }
.qrate__input-wrap { position: relative; display: inline-flex; align-items: center; }
.qrate__input {
  width: 210px; padding: 10px 44px 10px 16px;
  border-radius: var(--radius-lg); border: 1px solid var(--color-border-strong);
  background: var(--color-surface-alt); color: var(--color-heading);
  font-size: 30px; font-weight: 800; font-variant-numeric: tabular-nums;
  transition: border-color .18s ease, box-shadow .18s ease;
}
.qrate__input:focus { outline: none; border-color: var(--color-primary); box-shadow: var(--ring); }
.qrate__input::-webkit-outer-spin-button,
.qrate__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qrate__input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.qrate__currency {
  position: absolute; right: 16px; pointer-events: none;
  font-size: 20px; font-weight: 700; color: var(--color-faint);
}
.qrate__from { margin-top: 16px; display: grid; gap: 8px; max-width: 340px; }
.qrate__missing { font-size: 12px; line-height: 1.5; color: var(--color-faint); }

.qseg { display: inline-flex; padding: 4px; gap: 4px; border-radius: var(--radius-pill); background: var(--color-surface-soft); }
.qseg__item { position: relative; }
.qseg__item input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.qseg__item span {
  display: block; padding: 0 16px; border-radius: var(--radius-pill);
  min-height: var(--control-h-sm); display: grid; place-items: center;
  font-size: 13px; font-weight: 600; color: var(--color-muted);
  cursor: pointer; transition: background .18s ease, color .18s ease;
}
.qseg__item input:checked + span { background: var(--color-surface); color: var(--color-primary); box-shadow: var(--shadow-xs); }
.qseg__item input:focus-visible + span { box-shadow: var(--ring); }

.qwho { margin-top: 18px; }
.qwho__label { display: block; margin-bottom: 8px; font-size: 12px; font-weight: 600; color: var(--color-muted); }
.qwho__list { display: grid; gap: 8px; }
.qwho__opt { position: relative; display: block; cursor: pointer; }
.qwho__opt input { position: absolute; opacity: 0; }
.qwho__body {
  display: grid; gap: 2px; padding: 11px 14px 11px 40px;
  border-radius: var(--radius-lg); border: 1px solid var(--color-border);
  background: var(--color-surface-alt);
  transition: border-color .18s ease, background .18s ease;
}
.qwho__body::before {
  content: ""; position: absolute; left: 14px; top: 15px;
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--color-border-strong); background: var(--color-surface);
  transition: border-color .18s ease, box-shadow .18s ease;
}
.qwho__opt input:checked + .qwho__body {
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
}
.qwho__opt input:checked + .qwho__body::before {
  border-color: var(--color-primary);
  box-shadow: inset 0 0 0 3px var(--color-surface), inset 0 0 0 9px var(--color-primary);
}
.qwho__opt input:focus-visible + .qwho__body { box-shadow: var(--ring); }
.qwho__body b { font-size: 13.5px; font-weight: 600; color: var(--color-text); }
.qwho__body small { font-size: 12px; line-height: 1.45; color: var(--color-muted); }

/* ---- Шаги 2–3: карточки-пресеты ----------------------------------------- */
.qcards { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(196px, 1fr)); }
.qcard { position: relative; display: block; cursor: pointer; }
.qcard input { position: absolute; opacity: 0; }
.qcard__inner {
  position: relative; display: grid; gap: 6px; height: 100%;
  padding: 16px 16px 18px; border-radius: var(--radius-lg);
  border: 1px solid var(--color-border); background: var(--color-surface-alt);
  transition: transform .18s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease;
}
.qcard:hover .qcard__inner { transform: translateY(-2px); box-shadow: var(--shadow-sm); }
.qcard input:focus-visible + .qcard__inner { box-shadow: var(--ring); }
.qcard__badge {
  position: absolute; top: -9px; left: 14px;
  padding: 3px 9px; border-radius: var(--radius-pill);
  background: var(--color-primary); color: var(--color-on-primary);
  font-size: 10.5px; font-weight: 700; letter-spacing: .03em;
}
.qcard__mark { font-size: 22px; line-height: 1; }
.qcard__name { font-size: 15px; font-weight: 700; color: var(--color-heading); }
.qcard__tagline { font-size: 12.5px; font-weight: 600; color: var(--color-primary); }
.qcard__text { font-size: 12px; line-height: 1.5; color: var(--color-muted); }
.qcard__tick {
  position: absolute; top: 12px; right: 12px;
  width: 20px; height: 20px; border-radius: 50%;
  border: 1px solid var(--color-border-strong); background: var(--color-surface);
  transform: scale(.7); opacity: 0; transition: transform .2s ease, opacity .2s ease;
}
.qcard__tick::after {
  content: ""; position: absolute; left: 6px; top: 3px;
  width: 5px; height: 10px; border: solid var(--color-on-primary);
  border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.qcard input:checked + .qcard__inner {
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 9%, var(--color-surface));
}
.qcard input:checked + .qcard__inner .qcard__tick {
  opacity: 1; transform: scale(1);
  background: var(--color-primary); border-color: var(--color-primary);
}

/* Лестница премии: полоски растут при смене пресета. */
.qladder { margin-top: 14px; display: grid; gap: 6px; }
.qladder__row {
  display: grid; grid-template-columns: 78px minmax(0, 1fr) 84px; align-items: center; gap: 10px;
  font-size: 12.5px; color: var(--color-muted);
  animation: qslide .32s ease var(--qladder-delay, 0ms) both;
}
.qladder__row.is-zero { opacity: .55; }
.qladder__range { font-variant-numeric: tabular-nums; }
.qladder__bar { height: 8px; border-radius: var(--radius-pill); background: var(--color-surface-soft); overflow: hidden; }
.qladder__bar i {
  display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--color-primary), color-mix(in srgb, var(--color-primary) 55%, var(--color-coral)));
  transition: width .4s cubic-bezier(.2, .7, .3, 1);
}
.qladder__percent { text-align: right; font-weight: 700; color: var(--color-text); font-variant-numeric: tabular-nums; }
@keyframes qslide { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }

/* ---- Тумблеры и денежные поля ------------------------------------------- */
.qextra, .qauto { margin-top: 16px; display: grid; gap: 10px; }
.qtoggle { display: grid; grid-template-columns: 44px minmax(0, 1fr); align-items: start; gap: 12px; cursor: pointer; }
.qtoggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.qtoggle__track {
  position: relative; width: 44px; height: 26px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); border: 1px solid var(--color-border-strong);
  transition: background .2s ease, border-color .2s ease;
}
.qtoggle__track::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--color-surface); box-shadow: var(--shadow-xs);
  transition: transform .22s cubic-bezier(.2, .7, .3, 1);
}
.qtoggle input:checked + .qtoggle__track { background: var(--color-primary); border-color: var(--color-primary); }
.qtoggle input:checked + .qtoggle__track::after { transform: translateX(18px); }
.qtoggle input:focus-visible + .qtoggle__track { box-shadow: var(--ring); }
.qtoggle__body { display: grid; gap: 3px; padding-top: 2px; }
.qtoggle__body b { font-size: 13.5px; font-weight: 600; color: var(--color-text); }
.qtoggle__body small { font-size: 12px; line-height: 1.45; color: var(--color-muted); }
.qmoney { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; padding-left: 56px; }
.qmoney[hidden] { display: none !important; }

.qwhen { margin-top: 16px; display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; }
.qwhen__field { display: grid; gap: 6px; min-width: 132px; }
.qwhen__sentence {
  flex: 1 1 240px; min-width: 200px;
  padding: 10px 14px; border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
  font-size: 12.5px; line-height: 1.5; color: var(--color-text);
}
.qwhen__sentence.is-off {
  background: var(--color-surface-soft);
  border-color: var(--color-border); color: var(--color-muted);
}

/* ---- Предпросмотр ------------------------------------------------------- */
.qpreview {
  padding: 22px; border-radius: var(--radius-xl);
  border: 1px solid var(--color-border);
  background:
    radial-gradient(90% 120% at 0% 0%, color-mix(in srgb, var(--color-primary) 14%, transparent) 0%, transparent 60%),
    var(--color-surface);
  box-shadow: var(--shadow-card);
}
.qpreview__eyebrow {
  font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--color-primary);
}
.qpreview__title { margin-top: 6px; font-size: 15px; font-weight: 700; color: var(--color-heading); }
.qpreview__total { display: flex; align-items: baseline; gap: 6px; margin-top: 12px; }
.qpreview__total b {
  font-size: 38px; font-weight: 800; line-height: 1;
  color: var(--color-heading); font-variant-numeric: tabular-nums;
}
.qpreview__total span { font-size: 20px; font-weight: 700; color: var(--color-faint); }
.qpreview__sub { margin-top: 6px; font-size: 12.5px; line-height: 1.5; color: var(--color-muted); }

.qbars { margin-top: 16px; display: grid; gap: 10px; }
.qbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 10px; font-size: 12px; }
.qbar__label { color: var(--color-muted); }
.qbar__value { text-align: right; font-weight: 700; color: var(--color-text); font-variant-numeric: tabular-nums; }
.qbar__track {
  grid-column: 1 / -1; height: 9px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); overflow: hidden;
}
.qbar__fill {
  display: block; height: 100%; width: 0; border-radius: inherit;
  background: var(--color-primary);
  transition: width .45s cubic-bezier(.2, .7, .3, 1);
}
.qbar--bonus .qbar__fill { background: linear-gradient(90deg, var(--color-coral), var(--color-primary)); }
.qbar.is-empty .qbar__fill { background: var(--color-border-strong); }

.qscenario { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 6px; }
.qscenario__btn {
  padding: 6px 12px; border-radius: var(--radius-pill);
  border: 1px solid var(--color-border); background: var(--color-surface-alt);
  font-size: 12px; font-weight: 600; color: var(--color-muted); cursor: pointer;
  transition: border-color .18s ease, color .18s ease, background .18s ease;
}
.qscenario__btn:hover { border-color: var(--color-border-strong); color: var(--color-text); }
.qscenario__btn.is-active {
  border-color: var(--color-primary); color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface));
}

.qshifts { display: grid; gap: 6px; margin-top: 14px; }
.qshifts__label { font-size: 12px; color: var(--color-muted); }
.qshifts__label b { color: var(--color-text); font-variant-numeric: tabular-nums; }
.qshifts input[type="range"] { width: 100%; accent-color: var(--color-primary); }

.qpreview__lines { margin-top: 14px; display: grid; gap: 7px; }
.qpreview__lines li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding-top: 7px; border-top: 1px solid var(--color-border);
  font-size: 12.5px; color: var(--color-muted);
}
.qpreview__lines li:first-child { border-top: none; padding-top: 0; }
.qpreview__lines b { color: var(--color-text); font-weight: 700; font-variant-numeric: tabular-nums; }
.qpreview__note { margin-top: 14px; font-size: 11.5px; line-height: 1.5; color: var(--color-faint); }

.qapply {
  padding: 16px; border-radius: var(--radius-xl);
  border: 1px solid var(--color-border); background: var(--color-surface);
  box-shadow: var(--shadow-card); display: grid; gap: 10px;
}
.qapply__btn { position: relative; justify-content: center; }
.qapply__btn.is-busy { pointer-events: none; opacity: .75; }
.qapply__btn.is-busy::after {
  content: ""; position: absolute; right: 16px; top: 50%; margin-top: -8px;
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--color-on-primary) 35%, transparent);
  border-top-color: var(--color-on-primary);
  animation: qspin .7s linear infinite;
}
@keyframes qspin { to { transform: rotate(360deg); } }
.qapply__note { font-size: 12px; line-height: 1.5; color: var(--color-muted); }
.qapply__link { font-size: 12.5px; font-weight: 600; color: var(--color-primary); }

@media (max-width: 720px) {
  .qhero { flex-direction: column; align-items: flex-start; padding: 20px; }
  /* «Фиксированная за период» не влезает в 375px одной строкой — сегмент
     занимает всю ширину и переносится, вместо того чтобы обрезаться. */
  .qseg { display: flex; flex-wrap: wrap; width: 100%; }
  .qseg__item { flex: 1 1 auto; }
  .qseg__item span { padding: 0 10px; font-size: 12.5px; text-align: center; }
  .qhero__ring { display: none; }
  .qhero__done { position: static; margin-top: 14px; }
  .qhero__title { font-size: 22px; }
  .qrate__input { width: 100%; font-size: 26px; }
  .qrate__amount { flex: 1 1 100%; }
  .qmoney { padding-left: 0; }
  .qladder__row { grid-template-columns: 68px minmax(0, 1fr) 74px; }
}

@media (prefers-reduced-motion: reduce) {
  .qhero__ring-dot, .qhero__check-circle, .qhero__check-mark,
  .qladder__row, .qapply__btn.is-busy::after { animation: none; }
  .qhero__check-circle, .qhero__check-mark { stroke-dashoffset: 0; }
  .qstep { animation: none; }
  .qcard__inner, .qcard__tick, .qbar__fill,
  .qladder__bar i, .qtoggle__track, .qtoggle__track::after { transition: none; }
}

/* «Тонкая настройка» на хабе формул (stage 111): шаблоны, версии и черновики
   свёрнуты — новичку их видеть незачем, но путь никуда не делся. */
.fadv {
  margin-top: 16px; border-radius: var(--radius-xl);
  border: 1px solid var(--color-border); background: var(--color-surface);
  box-shadow: var(--shadow-card);
}
.fadv__summary {
  display: grid; gap: 3px; padding: 16px 20px 16px 44px;
  position: relative; cursor: pointer; list-style: none;
}
.fadv__summary::-webkit-details-marker { display: none; }
.fadv__summary::before {
  content: ""; position: absolute; left: 20px; top: 22px;
  width: 8px; height: 8px; border: solid var(--color-faint);
  border-width: 0 2px 2px 0; transform: rotate(-45deg);
  transition: transform .2s ease;
}
.fadv[open] .fadv__summary::before { transform: rotate(45deg); top: 20px; }
.fadv__summary:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--radius-xl); }
.fadv__title { font-size: 15px; font-weight: 700; color: var(--color-heading); }
.fadv__hint { font-size: 12.5px; line-height: 1.5; color: var(--color-muted); }
.fadv__body { padding: 0 20px 20px; }
.fadv__body > .formula-section:first-child { margin-top: 0; }

@media (prefers-reduced-motion: reduce) {
  .fadv__summary::before { transition: none; }
}

/* ---------------------------------------------------------------------------
   Рейтинг ПВЗ (31.07.2026): подсветка уровня, распределение сети, история.

   Цвет здесь несёт смысл, а не украшение: рейтинг OZON живёт в диапазоне
   4,7–5,0, разницу в сотые глазом не поймать, и уровень читается только
   заливкой. Уровни те же, что в app/pvz/ratings.py::RATING_LEVELS —
   top / good / watch / risk (+ none, когда снимка ещё нет).
--------------------------------------------------------------------------- */
.rating-pill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 9px; border-radius: var(--radius-pill);
  font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums;
  border: 1px solid transparent; white-space: nowrap;
}
.rating-pill--top { background: var(--color-success-soft); color: var(--color-success); border-color: color-mix(in srgb, var(--color-success) 32%, transparent); }
.rating-pill--good { background: var(--color-info-soft); color: var(--color-info); border-color: color-mix(in srgb, var(--color-info) 32%, transparent); }
.rating-pill--watch { background: var(--color-warning-soft); color: var(--color-warning); border-color: color-mix(in srgb, var(--color-warning) 34%, transparent); }
.rating-pill--risk { background: var(--color-danger-soft); color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 36%, transparent); }
.rating-pill--none { background: var(--color-surface-soft); color: var(--color-muted); border-color: var(--color-border); }

/* Крупное значение (плитки сводки, карточка точки). */
.rating-value { font-variant-numeric: tabular-nums; }
.rating-value--top { color: var(--color-success); }
.rating-value--good { color: var(--color-info); }
.rating-value--watch { color: var(--color-warning); }
.rating-value--risk { color: var(--color-danger); }
.rating-value--none { color: var(--color-muted); }

.rating-delta {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.rating-delta--up { color: var(--color-success); }
.rating-delta--down { color: var(--color-danger); }
.rating-delta--flat { color: var(--color-muted); font-weight: 600; }
.rating-delta--up::before { content: "▲"; font-size: 9px; }
.rating-delta--down::before { content: "▼"; font-size: 9px; }

/* Мини-история точки. Линия нормируется по своему диапазону (см. _spark),
   поэтому non-scaling-stroke обязателен: без него растянутый по X viewBox
   раздувает толщину линии. */
.rating-spark { display: block; width: 92px; height: 24px; }
.rating-spark--top { color: var(--color-success); }
.rating-spark--good { color: var(--color-info); }
.rating-spark--watch { color: var(--color-warning); }
.rating-spark--risk { color: var(--color-danger); }

/* Распределение сети по уровням: одна полоса, сегмент = уровень. */
.rating-bar {
  display: flex; gap: 3px; margin: 4px 0 14px;
  height: 34px; border-radius: var(--radius-md); overflow: hidden;
}
.rating-bar__seg {
  display: flex; align-items: center; justify-content: center;
  min-width: 28px; font-size: 12.5px; font-weight: 700; color: #fff;
}
.rating-bar__seg--top { background: var(--color-success); }
.rating-bar__seg--good { background: var(--color-info); }
.rating-bar__seg--watch { background: var(--color-warning); }
.rating-bar__seg--risk { background: var(--color-danger); }

.rating-legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.rating-legend__item {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--color-text);
}
.rating-legend__title { font-weight: 650; }
.rating-legend__hint { color: var(--color-muted); font-size: 12.5px; }
.rating-legend__count { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; }
.rating-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.rating-dot--top { background: var(--color-success); }
.rating-dot--good { background: var(--color-info); }
.rating-dot--watch { background: var(--color-warning); }
.rating-dot--risk { background: var(--color-danger); }

/* Плашка «данные за прошлую неделю»: рейтинг обновляется по средам, и снимок
   до среды — это прошлая неделя, о чём страница обязана сказать прямо. */
.rating-stale {
  margin-bottom: 16px; padding: 12px 16px;
  border: 1px solid color-mix(in srgb, var(--color-warning) 45%, var(--color-border));
  border-left-width: 3px; border-radius: var(--radius-md);
  background: var(--color-warning-soft); color: var(--color-warning);
  font-size: 13px; line-height: 1.5;
}
.rating-note { margin: 12px 0 0; font-size: 12.5px; color: var(--color-muted); }

/* Карточка рейтинга в детальной точке. */
.rating-card__body {
  display: flex; flex-wrap: wrap; align-items: center; gap: 20px 32px;
}
.rating-card__now { display: flex; align-items: center; gap: 10px; }
.rating-card__now .rating-value { font-size: 38px; font-weight: 800; line-height: 1; }
.rating-card__facts { display: flex; flex-wrap: wrap; gap: 20px 32px; }
.rating-fact { display: grid; gap: 2px; }
.rating-fact__label {
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--color-faint);
}
.rating-fact__value { display: grid; gap: 1px; font-size: 14px; font-weight: 650; }
.rating-card__spark { display: grid; gap: 3px; margin-left: auto; }
.rating-card__spark .rating-spark { width: 140px; height: 36px; }

.rating-history {
  list-style: none; margin: 14px 0 0; padding: 12px 0 0; display: flex;
  flex-wrap: wrap; gap: 6px; border-top: 1px solid var(--color-border);
}
.rating-history__item {
  display: grid; gap: 1px; justify-items: center;
  padding: 5px 10px; border-radius: var(--radius-md);
  background: var(--color-surface-soft); min-width: 62px;
}
.rating-history__date { font-size: 11px; color: var(--color-muted); }
.rating-history__value { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
  .rating-card__spark { margin-left: 0; }
  .rating-card__spark .rating-spark { width: 100%; }
}

/* Движение рейтинга во времени («Расчёты OZON» → Аналитика Турбо-ПВЗ). */
.rating-moves { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.rating-move {
  display: grid; gap: 3px;
  padding: 10px 13px; border-radius: var(--radius-md);
  background: var(--color-surface-soft);
}
.rating-move__label {
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--color-faint);
}
.rating-move__hint { font-size: 12.5px; color: var(--color-muted); }
.rating-move .rating-delta { font-size: 20px; }

/* Строки «кто просел / кто вырос»: точка — значения — дельта. */
.rating-moves--rows { gap: 6px; }
.rating-move--row {
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: 12px; padding: 8px 12px;
}
.rating-move--row .rating-delta { font-size: 14px; justify-self: end; min-width: 62px; }
.rating-move__pvz { display: grid; gap: 1px; min-width: 0; color: inherit; text-decoration: none; }
.rating-move__address {
  font-size: 13.5px; font-weight: 650; color: var(--color-heading);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rating-move__pvz:hover .rating-move__address { color: var(--color-primary); }
.rating-move__values {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--color-muted); white-space: nowrap;
}

@media (max-width: 640px) {
  .rating-move--row { grid-template-columns: minmax(0, 1fr) auto; }
  .rating-move--row .rating-move__values { grid-column: 1 / -1; }
}

/* =========================================================================
   Карточка смены (stage 126)
   Было: четыре секции grid--detail подряд. Верхняя пара «параметры + действия»
   выглядела нормально, а три нижние карточки занимали ЛЕВУЮ половину сетки —
   правая половина оставалась пустой во всю высоту экрана, и страница обрывалась
   дырой. Стало: одна сетка — слева стопка «что за смена», справа липкая панель
   действий, история — во всю ширину под ними.
   ========================================================================= */
.shiftpage__main,
.shiftpage__side { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
/* Панель действий выше остальной стопки — держим её у верха при прокрутке. */
.shiftpage__side { position: sticky; top: calc(var(--topbar-h) + 16px); align-self: start; }
.shiftpage__history { grid-column: 1 / -1; }

/* Пара компактных карточек («Замены» + «Открытие») под параметрами: занимают ту
   самую высоту, что раньше пустовала. Одна карточка (замены выключены флагом)
   растягивается на всю ширину колонки сама — auto-fit. */
.shiftpage__mini { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 18px; align-items: stretch; }
.card--mini { padding: 16px 18px; gap: 11px; }
.card--mini .card__title { font-size: 15px; }
/* В узкой карточке колонка ярлыков в 170px не помещается — ярлык слева,
   значение прижато вправо. */
.card--mini .detail-list > div { grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 8px 0; }
.card--mini .detail-list dd { text-align: right; overflow-wrap: anywhere; }

/* История: в одну колонку 20 событий вытянулись бы в узкий столбец, поэтому
   раскладываем плитками по ширине карточки. */
.timeline.timeline--grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: 12px 26px;
}

@media (max-width: 880px) {
  .shiftpage__side { position: static; }
}

/* --- Список претензий: подсветка, причина, выбор ПВЗ ---------------------- */
/* Подсветка отвечает на один вопрос — «что требует моего действия», поэтому
   ведущий признак не статус, а отсутствие виновного (правило — в модели,
   Claim.list_accent). Цветом красим ЛЕВУЮ КРОМКУ и лёгкую заливку, а не весь
   фон: строка остаётся читаемой, и подсветка не спорит с бейджами статуса,
   которые уже цветные. */
.claim-row td { position: relative; }
.claim-row--danger td:first-child::before,
.claim-row--warning td:first-child::before,
.claim-row--done td:first-child::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
}
.claim-row--danger td:first-child::before { background: var(--color-danger); }
.claim-row--warning td:first-child::before { background: var(--color-warning); }
.claim-row--done td:first-child::before { background: var(--color-success); }
.claim-row--danger td { background: color-mix(in srgb, var(--color-danger) 6%, transparent); }
.claim-row--warning td { background: color-mix(in srgb, var(--color-warning) 8%, transparent); }
/* Закрытые и уже учтённые не должны спорить за внимание с рабочей очередью. */
.claim-row--muted td { opacity: .62; }
/* Ховер обязан перебивать заливку строки — иначе под курсором ничего не
   меняется и таблица кажется неживой. Тег в селекторе поднимает вес над
   .claim-row--*, которые тоже красят td. */
/* Ховер снят: единое правило .table tbody tr:hover > td (см. раздел таблиц). */

/* Причина — самая длинная колонка; режем её, а не всю таблицу. */
.claim-row__reason { max-width: 320px; }
.claim-row__reason-text {
  display: block; font-weight: 650;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.claim-row__reason .badge { margin-top: 3px; }
.table--claims .btn-sm { white-space: nowrap; }
/* Действия — одной строкой: две кнопки в столбик делали строку вдвое выше
   остальных, и таблица «прыгала» на каждой неразобранной претензии. */
.table--claims td:last-child { white-space: nowrap; }
.table--claims td:last-child .btn + .btn { margin-left: 6px; }

.claim-legend { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-bottom: 12px; }
.claim-legend__item {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--color-muted);
}
.claim-legend__item::before {
  content: ""; width: 10px; height: 3px; border-radius: 2px; flex: none;
}
.claim-legend__item--danger::before { background: var(--color-danger); }
.claim-legend__item--warning::before { background: var(--color-warning); }
.claim-legend__item--done::before { background: var(--color-success); }
.claim-legend__item--muted::before { background: var(--color-border); }

.claim-empty-hint { text-align: center; margin-top: -8px; }

/* Разбор по выборке — свёрнут: рабочий режим страницы «найти и разобрать». */
.claim-breakdown { padding: 0; }
.claim-breakdown__toggle {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  padding: 14px 18px; font-weight: 750; font-size: 13.5px;
}
.claim-breakdown__toggle::-webkit-details-marker { display: none; }
.claim-breakdown__toggle svg { width: 15px; height: 15px; transition: transform .16s var(--ease); }
.claim-breakdown[open] .claim-breakdown__toggle svg { transform: rotate(180deg); }
.claim-breakdown__sum { margin-left: auto; color: var(--color-muted); font-weight: 600; }
.claim-breakdown__body { padding: 0 18px 18px; }

/* Панель назначения виновного: факты претензии одной строкой над формой. */
.claim-assign__facts {
  display: flex; flex-wrap: wrap; gap: 10px 22px; margin-top: 14px;
  padding: 12px 14px; border-radius: var(--radius-md);
  background: var(--color-surface-soft);
}
.claim-assign__fact { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.claim-assign__fact em { font-style: normal; font-size: 11.5px; color: var(--color-muted); font-weight: 700; }
.claim-assign__fact b { font-size: 13.5px; font-weight: 800; }
.claim-assign__count { margin-top: 12px; }
/* Тот же приём, что в попапе смены: общий textarea.input тянет 118px, и панель
   вырастает выше экрана. */
.cell-modal__dialog textarea.input { min-height: 60px; }

/* Выбор ПВЗ с поиском. Нативный select остаётся в DOM носителем значения (без
   JS он и есть контрол), поэтому прячем его, а не удаляем. */
.pvz-pick { position: relative; display: block; min-width: 0; }
.pvz-pick__native { position: absolute; opacity: 0; pointer-events: none; width: 100%; }
/* Якоря плиток: переход по «#…» ставит блок ровно под липкую шапку, а не под
   неё. Без отступа заголовок раздела уезжал ЗА шапку, и человек попадал на
   середину таблицы, не увидев, куда именно его привели. */
[id="turbo-tariff"],
[id="turbo-tariff-trend"] { scroll-margin-top: calc(var(--topbar-h) + 12px); }

/* Вспышка голого списка до сборки виджета (заказчик, 01.09.2026).
   Множественный выбор приезжает как <select multiple size="6"> — браузер рисует
   его шестистрочным списком адресов во всю высоту, и панель фильтров подскакивает
   на сотню пикселей, пока до неё не дойдёт defer-скрипт. На таблице графика
   разметка самая тяжёлая в продукте, и вспышку видно глазом.
   Не прячем совсем, а ужимаем до роста обычного поля: коробка держит место под
   будущую кнопку, и в момент подмены соседи не прыгают. Текст пунктов гасим —
   иначе в коробке мелькала бы первая строка списка.
   Заперто в html.has-js (класс ставит инлайновый скрипт в <head>): без JS
   виджета не будет вовсе, и нативный список обязан остаться рабочим. */
html.has-js select[data-pvz-pick][multiple]:not(.pvz-pick__native) {
  height: var(--pick-collapsed-h, var(--control-h)); overflow: hidden; color: transparent;
}
.pvz-pick__button {
  width: 100%; text-align: left; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--color-muted);
}
.pvz-pick.is-set .pvz-pick__button { color: var(--color-text); font-weight: 650; }
.pvz-pick.is-open .pvz-pick__button { border-color: var(--color-primary); }
.pvz-pick__pop {
  position: absolute; z-index: 40; top: calc(100% + 4px); left: 0;
  width: max(100%, 280px); padding: 8px;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-md); box-shadow: var(--shadow-pop);
}
/* [hidden] перед правилом display — иначе оно перебивает браузерное
   [hidden]{display:none} и список висит раскрытым (ловушка проекта). */
.pvz-pick__pop[hidden] { display: none; }
.pvz-pick__list { max-height: 260px; overflow: auto; margin: 8px 0 0; padding: 0; list-style: none; }
.pvz-pick__list li[hidden] { display: none; }
.pvz-pick__option {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 7px 9px; border: 0; border-radius: var(--radius-sm);
  background: transparent; color: inherit; font: inherit; font-size: 13px;
}
.pvz-pick__option:hover { background: var(--color-surface-soft); }
/* Приписка справа в строке (data-note): у сотрудника — код его точки. Тише
   имени и не переносится: строка списка обязана остаться в одну высоту. */
.pvz-pick__option--noted { display: flex; align-items: baseline; gap: 8px; }
.pvz-pick__note {
  margin-left: auto; flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 11.5px; color: var(--color-muted);
}
.pvz-pick__empty { margin: 8px 4px 4px; font-size: 12.5px; color: var(--color-muted); }
/* Заголовок группы (город, «Мои точки») — из optgroup исходного select. */
.pvz-pick__group {
  padding: 8px 9px 3px; font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; color: var(--color-muted);
}
/* Обязательное поле формы: подсказку браузера в прозрачном select не видно,
   поэтому «незаполнено» показываем на кнопке. */
.pvz-pick.is-invalid .pvz-pick__button { border-color: var(--color-danger); }

/* Множественный выбор ([data-pvz-pick] на <select multiple>): пункт не
   закрывает список, а ставит галку — «покажи вот ЭТИ точки, а не одну или
   все». Квадратик состояния повторяет .checkbox__box один в один: это одна и
   та же галка продукта, и выглядеть она обязана одинаково. */
.pvz-pick--multi .pvz-pick__option {
  display: flex; align-items: center; gap: 9px; position: relative;
}
.pvz-pick--multi .pvz-pick__option::before {
  content: ""; flex: 0 0 auto; width: 17px; height: 17px;
  border-radius: 5px; border: 1.5px solid var(--color-border);
  background: var(--color-surface); transition: all .18s var(--ease);
}
/* Сама галочка — поверх квадратика: он flex-элемент строки и своих детей не
   имеет, поэтому «птичку» рисует второй псевдоэлемент, привязанный к строке.
   Координаты считаются от padding строки (9px) и ширины квадратика (17px). */
.pvz-pick--multi .pvz-pick__option::after {
  content: ""; position: absolute; left: 15px; top: 50%; margin-top: -6px;
  width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0); transition: transform .18s var(--ease);
}
.pvz-pick--multi .pvz-pick__option.is-on::before {
  background: var(--color-primary); border-color: var(--color-primary);
}
.pvz-pick--multi .pvz-pick__option.is-on::after { transform: rotate(45deg) scale(1); }
.pvz-pick--multi .pvz-pick__option.is-on { color: var(--color-text); font-weight: 600; }
/* «Все ПВЗ» в множественном режиме — не пункт списка, а сброс: снятые галки
   и есть «все». Поэтому строка стоит над списком и отделена чертой. */
.pvz-pick__reset {
  display: block; width: 100%; margin-top: 8px; padding: 7px 9px;
  text-align: left; cursor: pointer; font: inherit; font-size: 13px; font-weight: 600;
  border: 0; border-bottom: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: transparent; color: var(--color-muted);
}
.pvz-pick__reset:hover { background: var(--color-surface-soft); color: var(--color-text); }
.pvz-pick__reset.is-on { color: var(--color-text); }

@media (max-width: 620px) {
  .claim-row__reason { max-width: 200px; }
  .claim-breakdown__sum { margin-left: 0; flex-basis: 100%; }
}

/* --- Штрафы OZON: рабочая таблица раздела «Претензии OZON» ---------------- */
.fines-search { margin: 0 0 12px; }
.fines-act { display: inline; }
.table--fines td:last-child { white-space: nowrap; }
.table--fines .claim-row__reason-text { max-width: 260px; }
/* Ховер обязан перебивать заливку строки — тег в селекторе поднимает вес над
   .claim-row--*, которые тоже красят td (та же связка, что в списке претензий). */
/* Ховер снят: единое правило .table tbody tr:hover > td (см. раздел таблиц). */
@media (max-width: 620px) {
  .table--fines .claim-row__reason-text { max-width: 160px; }
}

/* --- Ставка за смену: столбец справочника ПВЗ ---------------------------- */
/* Справочник должен остаться справочником: сумма стоит как обычное значение
   таблицы, а «поле ввода» проявляется рамкой только на наведении и в фокусе.
   Шесть десятков рамок с кнопками превращали список точек в анкету. */
.pvz-rate-cell__form {
  display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end;
}
.pvz-rate-cell__field { position: relative; display: inline-flex; align-items: center; }
.pvz-rate-cell__input {
  width: 92px;
  padding: 5px 20px 5px 8px;
  font: inherit; font-weight: 700; font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--color-heading);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  transition: border-color .15s var(--ease), background-color .15s var(--ease);
  appearance: textfield; -moz-appearance: textfield;
}
/* Стрелки-спиннеры в узкой ячейке только мешают: шаг задаётся с клавиатуры. */
.pvz-rate-cell__input::-webkit-outer-spin-button,
.pvz-rate-cell__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.pvz-rate-cell__input::placeholder { font-weight: 400; color: var(--color-faint); }
.table tbody tr:hover .pvz-rate-cell__input { border-color: var(--color-border); background: var(--color-surface); }
.pvz-rate-cell__input:focus {
  border-color: var(--color-primary); background: var(--color-surface); outline: none;
}
.pvz-rate-cell__currency {
  position: absolute; right: 7px;
  color: var(--color-muted); font-size: 12px; pointer-events: none;
}
/* Кнопка держит своё место (visibility, а не display): столбец не должен
   дёргаться по ширине, когда курсор идёт по списку сверху вниз. order: -1 —
   она стоит СЛЕВА от суммы, хотя в разметке идёт после поля (порядок обхода
   табом остаётся «поле → кнопка»). Иначе зарезервированное под неё место
   отжимало сумму от правого края, и число не попадало под свой заголовок. */
.pvz-rate-cell__save { order: -1; visibility: hidden; }
.pvz-rate-cell__form:focus-within .pvz-rate-cell__save,
.table tbody tr:hover .pvz-rate-cell__save { visibility: visible; }
@media (hover: none) {
  /* На тач-экране «наведения» не бывает — кнопка нужна всегда. */
  .pvz-rate-cell__save { visibility: visible; }
}
.pvz-rate-cell__since,
.pvz-rate-cell__ok,
.pvz-rate-cell__warn { display: block; margin-top: 3px; font-size: 11.5px; }
/* Правка ставки задним числом (23.08.2026): по строке, где уже отработаны
   смены, первое сохранение не пишет, а объясняет и открывает галочку. Она
   встаёт второй строкой формы (flex-basis: 100%), поэтому столбец не
   раздвигается по ширине, а кнопка «Сохранить» перестаёт прятаться — иначе
   подтвердить было бы нечем, пока курсор не на строке. */
.pvz-rate-cell__form--retro { flex-wrap: wrap; }
.pvz-rate-cell__form--retro .pvz-rate-cell__save { visibility: visible; }
.pvz-rate-cell__retro {
  flex-basis: 100%;
  display: inline-flex; align-items: center; gap: 5px; justify-content: flex-end;
  margin-top: 4px;
  font-size: 11.5px; font-weight: 600; color: var(--color-warning, var(--color-muted));
  /* Переносится, а не вылезает: на телефоне ячейка ставки узкая (поле 84px),
     и nowrap увёл бы подпись за край столбца. */
  text-align: right; cursor: pointer;
}
.pvz-rate-cell__ok { color: var(--color-success); }
.pvz-rate-cell__warn { color: var(--color-warning, var(--color-muted)); }
/* Подсветка результата: сохранённая ячейка помечается зелёным, ошибочная —
   красным, чтобы при проходе списка сверху вниз было видно, что уже сделано. */
.pvz-rate-cell--saved { box-shadow: inset 0 -2px 0 var(--color-success); }
.pvz-rate-cell--error { box-shadow: inset 0 -2px 0 var(--color-danger); }
/* Ждёт подтверждения — не ошибка и не успех: жёлтая черта, как у остальных
   предупреждений системы. */
.pvz-rate-cell--retro { box-shadow: inset 0 -2px 0 var(--color-warning, var(--color-muted)); }
/* Объяснение — три строки текста в ячейке справочника: без потолка по ширине
   столбец «Ставка за смену» растягивался бы на всю фразу (на телефоне таблица
   не ограничена окном и уезжала в 1000 px), а рядом стоящие числа — вбок. */
.pvz-rate-cell--retro .pvz-rate-cell__warn,
.pvz-employee-rate--retro .pvz-rate-cell__warn { max-width: 260px; margin-left: auto; }
.pvz-rate-count {
  display: block; font-weight: 400; text-transform: none; letter-spacing: 0;
  color: var(--color-muted);
}
/* Панель фильтров ПВЗ — грид с колонками от 170px. Поле даты с подписью в
   одну колонку не влезает (у input[type=date] своя минимальная ширина), и без
   span 2 оно наезжало на кнопку «Применить». min-width: 0 обязателен: без него
   грид-элемент не даёт полю сжиматься и снова вылезает за свою колонку. */
.filters-bar__rate-date {
  grid-column: span 2;
  display: flex; align-items: center; gap: 8px; min-width: 0;
}
.filters-bar__rate-date > span {
  flex: none; color: var(--color-muted); font-size: 13px; white-space: nowrap;
}
.filters-bar__rate-date .input { min-width: 0; flex: 1 1 auto; }
@media (max-width: 720px) {
  /* Узкая панель: дате отдаём всю строку — в половине она обрезала год
     («01.08 📅» вместо «01.08.2026»). */
  .filters-bar__rate-date { grid-column: 1 / -1; }
}
@media (max-width: 620px) {
  .pvz-rate-cell__input { width: 84px; }
  .pvz-rate-cell__form { gap: 4px; }
}

/* --- Ставки сотрудников: панель в карточке ПВЗ --------------------------- */
/* Поле ввода здесь то же самое (.pvz-rate-cell__*), меняется только обвязка:
   строка таблицы вместо ячейки справочника. */
.pvz-employee-rates__date { display: flex; align-items: flex-end; gap: 8px; flex: none; }
.pvz-employee-rates__date label { display: flex; align-items: center; gap: 8px; min-width: 0; }
.pvz-employee-rates__date label > span {
  flex: none; color: var(--color-muted); font-size: 13px; white-space: nowrap;
}
.pvz-employee-rates__date .input { min-width: 0; }
.pvz-employee-rates__hint { margin: 10px 0 0; font-size: 12.5px; }
.table--employee-rates td { vertical-align: top; }
/* Подсветка результата — как в справочнике: видно, что уже сделано, при
   проходе списка сверху вниз. */
.pvz-employee-rate--saved td { box-shadow: inset 0 -2px 0 var(--color-success); }
.pvz-employee-rate--error td { box-shadow: inset 0 -2px 0 var(--color-danger); }
.pvz-employee-rate--retro td {
  box-shadow: inset 0 -2px 0 var(--color-warning, var(--color-muted));
}
@media (max-width: 620px) {
  .pvz-employee-rates__date { width: 100%; }
  .pvz-employee-rates__date label { flex: 1 1 auto; }
}

/* --- Оплата труда: панель в карточке сотрудника -------------------------- */
/* Панель — элемент сетки, а у него min-width: auto = ширина содержимого:
   таблица «что получит за смену» распирала колонку и на телефоне уводила всю
   страницу вбок. С нулём прокручивается .table-wrap, а не страница. */
.pay-card { min-width: 0; container-type: inline-size; }
/* Вид оплаты и ставка — одно решение, поэтому одна форма с одной кнопкой.
   Поля стоят в ряд на широком экране и в столбец на узком: подпись режима
   длинная, и в половине ширины она рвалась бы на пять строк. */
.pay-card__form {
  display: grid; gap: 14px;
  grid-template-columns: minmax(220px, 1fr) minmax(160px, 220px);
  align-items: start;
}
.pay-card__actions {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.pay-card__ok { color: var(--color-success); font-size: 12.5px; }
.pay-card__warn { color: var(--color-warning, var(--color-muted)); font-size: 12.5px; }
/* Подтверждение правки задним числом: панель — грид в две колонки, поэтому
   галочка занимает всю строку, иначе она встала бы рядом с полем суммы и
   читалась бы как часть подписи к нему. */
.pay-card__retro {
  grid-column: 1 / -1;
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 12.5px; font-weight: 600; color: var(--color-warning, var(--color-muted));
  cursor: pointer;
}
.pay-card__subtitle {
  margin: 18px 0 8px; font-size: 13px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .04em; color: var(--color-muted);
}
.pay-card__note { margin-top: 14px; }
/* Проверка «что получит за смену»: у подмены точек до двенадцати, и по две
   строки на каждую (код + адрес) панель уезжала на треть экрана ниже соседней
   колонки. Адрес встаёт РЯДОМ с кодом — строка одна, а на узкой карточке
   переносится сам. */
.table--pay-preview td.pay-preview__pvz {
  display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 8px; row-gap: 2px;
}
.table--pay-preview tbody td.pay-preview__pvz > .cell-sub { display: inline; margin-top: 0; }
/* Подсветка результата — как у ставок: видно, что сохранение прошло. */
.pay-card--saved { box-shadow: inset 0 -2px 0 var(--color-success); }
.pay-card--error { box-shadow: inset 0 -2px 0 var(--color-danger); }
/* Контейнерный запрос, а не медиа: карточку сужает не окно, а вторая колонка
   grid--detail. В окне на половину экрана панель получала ~310px при минимуме
   сетки 220 + 160 + зазор = 394 — и форма вылезала за рамку карточки, хотя по
   медиа-запросу «широкий экран». */
@container (max-width: 430px) {
  .pay-card__form { grid-template-columns: minmax(0, 1fr); }
}

/* --- Ставки в графике: строки ручного вида оплаты ------------------------ */
/* Незаполненная ставка — не «пусто», а причина, по которой смена не
   рассчитается: строка помечена кантом, чтобы её было видно при прокрутке. */
.manual-rate--missing td:first-child { box-shadow: inset 3px 0 0 var(--color-danger); }

/* =========================================================================
   Статистика сотрудников по нарушениям (08.2026)
   Топ («Контроль → Сотрудники») и разбор одного человека. Две фигуры на всё:
   горизонтальная полоска доли (в таблице топа и в разрезах карточки) и
   помесячный столбик. Полоска мерится не от суммы, а от МАКСИМУМА среза:
   вопрос экрана — «кто/что выделяется», а доля от суммы у сорока строк
   превращается в сорок одинаковых огрызков.
   ========================================================================= */
.vstat-bar {
  display: block; min-width: 64px; height: 7px; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); overflow: hidden;
}
.vstat-bar__fill {
  display: block; width: var(--w, 0); height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
}
.vstat-bar__fill--coral {
  background: linear-gradient(90deg, var(--color-coral), color-mix(in srgb, var(--color-coral) 60%, #fff));
}

/* Разрезы карточки: карточки одного веса встают рядом и не тянут за собой
   пустую половину экрана, как это делала бы двухколоночная grid--detail. */
.vstat-grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); align-items: start; }

.vstat-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.vstat-list__item {
  display: grid; grid-template-columns: minmax(0, 1fr) 96px 40px;
  gap: 12px; align-items: center; font-size: 13.5px;
}
.vstat-list__label { color: var(--color-muted); min-width: 0; overflow-wrap: anywhere; }
.vstat-list__value { font-weight: 800; text-align: right; font-variant-numeric: tabular-nums; }

/* Помесячная динамика: пустой месяц — тоже месяц, поэтому у него остаётся
   колонка и подпись, просто без столбика. */
.vstat-months {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: flex-end; gap: 8px; overflow-x: auto;
}
.vstat-months__item {
  flex: 1 0 42px; display: flex; flex-direction: column; align-items: center; gap: 6px;
}
.vstat-months__count { font-size: 12px; font-weight: 800; font-variant-numeric: tabular-nums; }
.vstat-months__track {
  width: 100%; height: 90px; display: flex; align-items: flex-end;
  background: linear-gradient(var(--color-surface-soft), var(--color-surface-soft)) no-repeat bottom / 100% 2px;
}
.vstat-months__fill {
  display: block; width: 100%; min-height: 2px; height: var(--h, 0);
  border-radius: 5px 5px 2px 2px;
  background: linear-gradient(180deg, var(--color-primary), var(--color-primary-dark));
}
.vstat-months__label { font-size: 11px; color: var(--color-muted); white-space: nowrap; }

@media (max-width: 720px) {
  .vstat-list__item { grid-template-columns: minmax(0, 1fr) 64px 34px; }
  .vstat-months__track { height: 64px; }
}

/* ---------------------------------------------------------------------------
   Обновления продукта: окно «Что нового», лента истории и экран консоли.
   z-index 129 — на ступень ниже модалки непрочитанных уведомлений (130):
   одновременно они не показываются, но порядок должен быть задан явно.

   Окно намеренно тихое: один лист, одна кромка, ни градиентов, ни свечения.
   Прежняя версия несла синюю шапку с подсветкой, знак продукта в градиентном
   квадрате, надзаголовок капсом и карточки-плитки в утопленном колодце — пять
   украшений на два абзаца текста, и глаз шёл по ним, а не по строкам. Здесь
   работает только типографика: размер, вес, воздух.
--------------------------------------------------------------------------- */
.upd-modal { position: fixed; inset: 0; z-index: 129; display: grid; place-items: center; padding: 28px; }
.upd-modal__backdrop {
  position: absolute; inset: 0;
  background: rgba(7,17,31,.5); backdrop-filter: blur(3px);
}
.upd-modal__dialog {
  position: relative; display: flex; flex-direction: column;
  /* 620 px — строка в 70–75 знаков, мера чтения. 880 давали 110 знаков (вдвое
     больше комфортного), 760 всё ещё растягивали абзац поперёк экрана. */
  width: min(620px, 100%); max-height: calc(86vh * var(--vu));
  /* Окно — ОДИН лист: шапка, лента и подвал на общем surface, отбиты только
     кромками. Прежние три слоя (bg-полотно, surface-шапка, карточки на
     surface) в тёмной теме различались на четыре единицы яркости — стопка
     новостей всё равно читалась сплошным серым, зато ступеней стало три.
     Второй слой тени — --shadow-pop; прежде здесь стоял var(--shadow-lg), а
     такого токена в системе нет: неизвестная переменная делает НЕВАЛИДНЫМ всё
     свойство целиком, и окно оставалось вообще без тени. */
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-xl); overflow: hidden;
  box-shadow: 0 28px 70px -26px rgba(7,17,31,.5), var(--shadow-pop);
  animation: upd-modal-in .22s var(--ease) both;
}
@keyframes upd-modal-in {
  from { opacity: 0; transform: translateY(10px) scale(.99); }
  to { opacity: 1; transform: none; }
}

/* Шапка — только название окна, счётчик и крестик. Кромка снизу появляется,
   когда ленту сдвинули: на короткой новости, которая видна целиком, линия
   делила бы сплошной лист на пустые отсеки. */
.upd-modal__head {
  position: relative; z-index: 1; flex: 0 0 auto;
  display: flex; align-items: center; gap: 10px;
  padding: 20px 22px 14px; background: var(--color-surface);
  border-bottom: 1px solid transparent;
  transition: border-color .18s var(--ease);
}
.upd-modal__dialog.is-scrolled .upd-modal__head { border-bottom-color: var(--color-border); }
.upd-modal__title {
  flex: 1 1 auto; min-width: 0; margin: 0;
  font-size: 18px; font-weight: 700; line-height: 1.25;
  letter-spacing: -.01em; color: var(--color-heading);
}
/* Счётчик серый, а не синий: это справка «записей три», а не тревога. */
.upd-modal__count {
  flex: 0 0 auto; min-width: 22px; height: 21px; padding: 0 7px;
  display: inline-grid; place-items: center; border-radius: var(--radius-pill);
  background: var(--color-surface-soft); color: var(--color-muted);
  font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.upd-modal__close {
  position: relative; flex: 0 0 auto;
  display: grid; place-items: center; width: 32px; height: 32px;
  margin: -4px -6px -4px 0;
  border: 0; border-radius: var(--radius-md);
  background: none; color: var(--color-faint); cursor: pointer;
  transition: background .16s var(--ease), color .16s var(--ease);
}
.upd-modal__close svg { width: 17px; height: 17px; }
.upd-modal__close:hover { background: var(--color-surface-soft); color: var(--color-heading); }

.upd-modal__body {
  position: relative; flex: 1 1 auto; overflow-y: auto;
  padding: 2px 22px 22px; background: var(--color-surface);
  scrollbar-width: thin; scrollbar-color: var(--color-border-strong) transparent;
}
.upd-modal__body::-webkit-scrollbar { width: 10px; }
.upd-modal__body::-webkit-scrollbar-thumb {
  background: var(--color-border-strong); border-radius: var(--radius-pill);
  border: 3px solid var(--color-surface);
}
.upd-modal__all { flex: 1 1 auto; margin: 0; font-size: 12.5px; color: var(--color-faint); }
.upd-modal__all a { color: var(--color-primary); font-weight: 600; }
.upd-modal__foot {
  position: relative; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: flex-end; gap: 16px;
  padding: 13px 22px; border-top: 1px solid var(--color-border);
  background: var(--color-surface);
}
/* Дымка над подвалом: обрезанная на сгибе новость должна выглядеть как «ниже
   есть ещё», а не как обрубленная вёрстка. Появляется только пока ленте есть
   куда мотать (класс вешает скрипт окна) — на домотанной до конца ленте она
   сама выглядела бы обрывом. */
.upd-modal__foot::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 28px;
  background: linear-gradient(transparent, var(--color-surface));
  opacity: 0; transition: opacity .2s var(--ease); pointer-events: none;
}
.upd-modal__body.has-more ~ .upd-modal__foot::before { opacity: 1; }
.upd-modal__actions { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
.upd-modal-open { overflow: hidden; }

/* Карточка обновления — одна и та же в окне, в ленте и в предпросмотре консоли. */
.upd-card__meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; color: var(--color-faint); font-size: 12.5px; }
/* Дата — обычная серая строчка. Плашка с точкой была третьим акцентом подряд
   (после шапки и плитки) там, где нужна просто подпись «когда». */
.upd-card__date { font-size: 12.5px; font-weight: 500; color: var(--color-faint); }
.upd-card__title { margin: 6px 0 0; font-size: 17px; font-weight: 700; line-height: 1.3; color: var(--color-heading); }

/* В окне новости разделены линией, а не оправлены каждая в свою плитку:
   плитка на плитке — это ещё одна рамка вокруг того, что и так стоит в рамке
   окна. Важное отмечено красной плашкой «Важно» у даты, и только ею. */
.upd-modal .upd-card { padding: 18px 0 0; }
.upd-modal .upd-card + .upd-card { margin-top: 20px; border-top: 1px solid var(--color-border); }

/* Лента истории: те же тексты, но карточками страницы. */
.upd-card--page { padding: 22px 24px; }
.upd-card--page.is-unread { box-shadow: inset 3px 0 0 var(--color-primary); }

.upd-text { margin-top: 9px; font-size: 14.5px; line-height: 1.65; color: var(--color-text); overflow-wrap: anywhere; }
.upd-text p { margin: 0 0 10px; }
.upd-text p:last-child { margin-bottom: 0; }
/* Подзаголовок внутри новости на полступени крупнее текста и не жирнее 700:
   при 15.5/800 он спорил с собственным заголовком новости, и абзац под ним
   выглядел подписью к подзаголовку, а не продолжением новости. */
.upd-text__head { margin: 16px 0 6px; font-size: 14.5px; font-weight: 700; color: var(--color-heading); }
.upd-text__list { margin: 0 0 10px; padding-left: 20px; }
.upd-text__list li { margin: 3px 0; }
.upd-text__list li::marker { color: var(--color-border-strong); }
.upd-text a { color: var(--color-primary); font-weight: 600; }
.upd-text strong { font-weight: 650; color: var(--color-heading); }

.upd-feed { display: flex; flex-direction: column; gap: 14px; }

/* Экран консоли: список обновлений и редактор рядом с предпросмотром. */
.pc-hint { font-size: 12.5px; color: var(--color-muted); }
.pc-updates { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.pc-update {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 14px 0; border-top: 1px solid var(--color-border);
}
.pc-update:first-child { border-top: 0; }
.pc-update.is-draft { opacity: .82; }
.pc-update__main { min-width: 0; }
.pc-update__title {
  display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 15px; font-weight: 750; color: var(--color-heading); text-decoration: none;
}
.pc-update__title:hover { color: var(--color-primary); }
.pc-update__excerpt { margin: 4px 0 0; font-size: 13px; color: var(--color-muted); overflow-wrap: anywhere; }
.pc-update__meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 6px; font-size: 12px; color: var(--color-faint); }
.pc-update__side { flex: 0 0 auto; text-align: right; }
.pc-update__reads { display: block; font-size: 20px; font-weight: 850; font-variant-numeric: tabular-nums; }
.pc-update__reads-label { font-size: 11.5px; color: var(--color-muted); }

.pc-update-editor { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 18px; align-items: start; }
.pc-roles { display: flex; flex-direction: column; gap: 8px; }
.pc-danger-row { margin-top: 16px; }

/* Заготовки: четыре кнопки-карточки, с которых начинается текст. Подпись под
   названием отвечает на «а это про что» — без неё «Важное объявление» и
   «Плановые работы» различаются только на вкус. */
.upd-presets { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 8px; }
.upd-preset {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: 10px 12px; border: 1px solid var(--color-border);
  border-radius: var(--radius-md); background: var(--color-surface-alt);
  cursor: pointer; transition: border-color .16s var(--ease), background .16s var(--ease), transform .16s var(--ease);
}
.upd-preset:hover {
  border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
  background: var(--color-surface); transform: translateY(-1px);
}
.upd-preset__label { font-size: 13.5px; font-weight: 650; color: var(--color-heading); }
.upd-preset__hint { font-size: 12px; color: var(--color-muted); }

/* Панель разметки приклеена к полю: она его продолжение, а не отдельный блок,
   поэтому нижние углы у неё прямые, а верхние у textarea снимаются ниже. */
.upd-toolbar {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: 7px 8px; border: 1px solid var(--color-border); border-bottom: 0;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  background: var(--color-surface-alt);
}
.upd-toolbar__btn {
  padding: 5px 10px; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--color-muted); cursor: pointer;
  font-size: 12.5px; font-weight: 600; line-height: 1.3;
  transition: background .14s var(--ease), color .14s var(--ease), border-color .14s var(--ease);
}
.upd-toolbar__btn b { font-weight: 800; }
.upd-toolbar__btn:hover { background: var(--color-surface); color: var(--color-heading); border-color: var(--color-border); }
.upd-toolbar__count {
  margin-left: auto; padding-right: 4px;
  font-size: 11.5px; color: var(--color-faint); font-variant-numeric: tabular-nums;
}
.upd-toolbar__count.is-over { color: var(--color-danger); font-weight: 700; }
.upd-toolbar + .form-control { border-radius: 0 0 var(--radius-md) var(--radius-md); }

.upd-audience { display: flex; flex-wrap: wrap; gap: 6px; }
.upd-audience__chip { border: 0; cursor: pointer; font-family: inherit; }
.upd-audience__chip:hover { background: var(--color-surface-soft); color: var(--color-heading); }
.upd-audience__chip.is-active,
.upd-audience__chip.is-active:hover { background: var(--color-primary); color: var(--color-on-primary); }

/* Кнопки публикации держатся у нижней кромки экрана: решение принимается в
   тот момент, когда дочитал предпросмотр справа, а не когда домотал форму. */
.upd-actions {
  position: sticky; bottom: 0; z-index: 2;
  margin: 2px -22px -22px; padding: 14px 22px;
  border-top: 1px solid var(--color-border);
  border-radius: 0 0 var(--radius-xl) var(--radius-xl);
  background: var(--color-surface);
}

/* Правая колонка: предпросмотр едет вместе со страницей — иначе на длинном
   тексте он уезжает вверх ровно тогда, когда на него и надо смотреть. */
.pc-update-preview { position: sticky; top: 18px; display: flex; flex-direction: column; gap: 10px; }
.pc-update-preview__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pc-update-preview__live { font-size: 11.5px; color: var(--color-faint); }
.pc-update-preview__live::before {
  content: ""; display: inline-block; vertical-align: middle;
  width: 6px; height: 6px; margin-right: 6px; border-radius: 50%;
  background: var(--color-success);
}

/* Макет окна: те же классы .upd-modal__*, что и у настоящего окна (см.
   platform/includes/update_preview.html), — поэтому здесь только то, что
   отличает макет от живого окна: он не летит, не висит и не в полный рост. */
.upd-preview__frame {
  display: flex; flex-direction: column;
  border: 1px solid var(--color-border); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-card);
  overflow: hidden;
}
.upd-preview .upd-modal__head { padding: 15px 16px 11px; border-bottom-color: var(--color-border); }
.upd-preview .upd-modal__title { font-size: 15px; }
.upd-preview .upd-modal__close { cursor: default; }
.upd-preview .upd-modal__body { max-height: 46vh; padding: 2px 16px 16px; }
.upd-preview .upd-modal__foot { padding: 10px 16px; }
.upd-preview .upd-card { padding-top: 14px; }
.upd-preview .upd-card + .upd-card { margin-top: 14px; border-top: 1px solid var(--color-border); }
/* Подсказки на месте ещё не написанного: макет пустого окна иначе выглядит
   сломанным, а не пустым. */
.upd-preview .is-empty { color: var(--color-faint); }
.upd-preview .upd-card__title.is-empty { font-weight: 600; }
.upd-preview__audience { margin: 0; }

@media (max-width: 1100px) {
  .pc-update-editor { grid-template-columns: minmax(0, 1fr); }
  .pc-update-preview { position: static; }
  .upd-preview .upd-modal__body { max-height: none; }
}
@media (max-width: 720px) {
  /* На телефоне окно — не «карточка посреди экрана», а шторка снизу: так его
     видно целиком, кнопки остаются под большим пальцем, и не остаётся полей,
     которые на 375px съедают четверть текста. */
  .upd-modal { padding: 0; place-items: end stretch; }
  .upd-modal__dialog {
    width: 100%; max-height: calc(92vh * var(--vu));
    border-radius: var(--radius-2xl) var(--radius-2xl) 0 0; border-bottom: 0;
    animation-name: upd-sheet-in;
  }
  .upd-modal__head { padding: 17px 16px 12px; }
  .upd-modal__title { font-size: 17px; }
  .upd-modal__body { padding: 2px 16px 18px; }
  .upd-modal .upd-card { padding-top: 15px; }
  .upd-modal .upd-card + .upd-card { margin-top: 17px; }
  /* Подвал столбиком, но НЕ column-reverse: ссылка на раздел объясняет кнопки
     и должна стоять над ними, а не под большим пальцем. */
  .upd-modal__foot { flex-direction: column; align-items: stretch; gap: 12px; padding: 13px 16px; }
  .upd-modal__all { text-align: center; }
  .upd-modal__actions { display: grid; grid-template-columns: 1fr 1fr; }
  .upd-modal__actions form { display: contents; }
  .upd-modal__actions .btn { width: 100%; }
  .upd-card--page { padding: 18px; }
  .upd-card__title { font-size: 16.5px; }
  .pc-update { flex-direction: column; gap: 8px; }
  .pc-update__side { text-align: left; }
}
@keyframes upd-sheet-in {
  from { opacity: 0; transform: translateY(28px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .upd-modal__dialog { animation: none; }
}

/* =========================================================================
   СВОДНАЯ ПАНЕЛЬ СОБСТВЕННИКА — редизайн 08.2026
   -------------------------------------------------------------------------
   Задача формулировалась так: собственник смотрит с телефона, 30 секунд,
   между делом; ему нужно понять «всё ли ок» и «куда ткнуть, если не ок».
   Прежняя панель на это не отвечала — замер живой страницы давал 13 карточек,
   77 текстовых узлов, 25 разных пар «кегль/начертание» и 6 акцентных оттенков
   в одном экране, а на телефоне первые 811 px занимали два элемента, после
   которых не оставалось ни одной цифры о бизнесе.

   Отсюда четыре правила, которым подчинён весь блок ниже:

   1. ЧЕТЫРЕ УРОВНЯ ТЕКСТА, не двадцать пять.
        герой (цифра) 34/800 → заголовок раздела 21/800 →
        название блока 13/650 muted → тихая подпись 12/500 faint.
      Всё остальное обязано лечь на один из них.
   2. ГРАНИЦА РАЗДЕЛА — ВОЗДУХ, не линейка. 48 px между разделами против
      14 px внутри: отношение 1:3,5 читается само. Раньше было 22 против 18.
   3. ЦВЕТ = СОСТОЯНИЕ, и только оно. Норма цвета НЕ ПОЛУЧАЕТ — спокойный
      экран сам по себе означает «всё в порядке». Зелёный с панели убран:
      если хорошее тоже красится, отсутствие цвета перестаёт быть сигналом.
      Синий остаётся только интерактивом (ссылки, кнопки, фокус).
   4. ГЛУБИНА МЯГКАЯ. Тени — существующие токены, никаких свечений.
   ========================================================================= */

/* ---- Строка состояния ---------------------------------------------------
   Единственный элемент, отвечающий на «всё ли ок». Не настраивается: до
   08.2026 это была плитка, которую можно было выключить вместе с ответом. */
.dstat {
  position: relative;
  display: flex; flex-direction: column; gap: 10px;
  padding: 16px 18px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
  margin-bottom: 16px;
}
/* Тон — левой кромкой, а не заливкой всей карточки: заливка сделала бы строку
   вторым громким пятном рядом с «Данными OZON», и оба перекричали бы цифры. */
.dstat::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 3px;
  border-radius: var(--radius-xl) 0 0 var(--radius-xl);
  background: var(--color-border-strong);
}
.dstat--danger::before { background: var(--color-danger); }
.dstat--warning::before { background: var(--color-warning); }
.dstat--success::before { background: color-mix(in srgb, var(--color-success) 55%, transparent); }

.dstat__head { display: flex; align-items: center; gap: 9px; }
.dstat__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-border-strong); flex: none; }
.dstat--danger .dstat__dot { background: var(--color-danger); }
.dstat--warning .dstat__dot { background: var(--color-warning); }
.dstat--success .dstat__dot { background: var(--color-success); }
.dstat__headline {
  font-size: 15px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--color-heading);
}
.dstat--danger .dstat__headline,
.dstat--warning .dstat__headline { color: var(--color-heading); }
.dstat__calm { font-size: 13px; color: var(--color-muted); }

.dstat__list { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; margin: 0; padding: 0; }
.dstat__item { min-width: 0; }
.dstat__link {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 8px 12px; min-height: 38px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-alt);
  font: inherit; color: inherit; text-align: left;
  transition: border-color .18s var(--ease), background .18s var(--ease), transform .18s var(--ease);
}
.dstat__link:hover { border-color: var(--color-border-strong); transform: translateY(-1px); }
.dstat__link:focus-visible { outline: none; box-shadow: var(--ring); }
.dstat__value { font-size: 17px; font-weight: 800; font-variant-numeric: tabular-nums; }
.dstat__item--danger .dstat__value { color: var(--color-danger); }
.dstat__item--warning .dstat__value { color: var(--color-warning); }
.dstat__item--info .dstat__value { color: var(--color-muted); }
.dstat__label {
  font-size: 12.5px; color: var(--color-muted);
  /* Потолок ширины — страховка от «вылезет»: длинная подпись обрезается по
     своей ячейке, а не растягивает строку и не выдавливает соседей. */
  max-width: 30ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- Раздел -------------------------------------------------------------
   Единица восприятия. Заголовок КРУПНЕЕ любого текста внутри блока — раньше
   было наоборот (15/800 у раздела против 15.5–16/800 у карточек), и обложка
   группы читалась как ещё одна подпись. */
.dsec { display: flex; flex-direction: column; gap: 12px; }
.dsec + .dsec,
.dsec + .dsec-more { margin-top: 32px; }
.dsec__head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.dsec__intro { min-width: 0; display: grid; gap: 2px; }
.dsec__title {
  font-size: 21px; font-weight: 800; letter-spacing: -0.025em;
  color: var(--color-heading);
}
.dsec__question { font-size: 13px; color: var(--color-faint); }
/* Заголовок раздела и есть переход в него. Стрелка бледная и подъезжает при
   наведении: заголовок обязан выдавать, что он кликабельный, но не выглядеть
   кнопкой — он прежде всего обложка группы. */
.dsec__link {
  display: inline-flex; align-items: center; gap: 6px;
  color: inherit; text-decoration: none;
  /* Отрицательный внешний отступ гасит внутренний: тач-цель растёт до ~44 px,
     а заголовок остаётся ровно там, где стоял. */
  padding: 7px 4px; margin: -7px -4px;
}
.dsec__go {
  flex: none; color: var(--color-faint);
  opacity: .55; transform: translateX(-3px);
  transition: opacity .18s var(--ease), transform .18s var(--ease), color .18s var(--ease);
}
.dsec__link:hover { color: var(--color-primary); }
.dsec__link:hover .dsec__go { opacity: 1; transform: none; color: var(--color-primary); }
.dsec__link:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--radius-sm); }
@media (hover: none) {
  /* На тач-экране наведения не бывает — стрелка видна сразу. */
  .dsec__go { opacity: .8; transform: none; }
}
.dsec__grid {
  display: grid; grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--gap-cards); align-items: start;
}
.dsec__silent {
  grid-column: 1 / -1;
  font-size: 13px; color: var(--color-faint);
  padding: 18px 2px;
}
@media (max-width: 1000px) {
  .dsec__grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .dsec__grid { grid-template-columns: 1fr; gap: 12px; }
  .dsec + .dsec,
  .dsec + .dsec-more { margin-top: 24px; }
  .dsec__title { font-size: 19px; }
  /* Заголовок и «Открыть раздел» одной строкой: в столбик шапка раздела
     занимала 100 px и выталкивала первую плитку за край экрана. */
  .dsec__head { align-items: baseline; flex-direction: row; gap: 10px; }
  .dsec__question { font-size: 12.5px; }
}

/* ---- Пустая панель («выключил всё») ------------------------------------ */
.dempty {
  display: grid; justify-items: center; gap: 12px; text-align: center;
  padding: 56px 24px;
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-2xl);
  background: color-mix(in srgb, var(--color-surface) 55%, transparent);
}
.dempty__icon {
  width: 52px; height: 52px; border-radius: var(--radius-lg);
  display: grid; place-items: center;
  background: var(--color-surface-soft); color: var(--color-faint);
}
.dempty__icon svg { width: 26px; height: 26px; }
.dempty__title { font-size: 19px; font-weight: 800; color: var(--color-heading); }
.dempty__text { font-size: 13px; color: var(--color-muted); max-width: 46ch; }

.dsec-more {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 12px 16px; min-height: 44px;
  border: 1px dashed var(--color-border-strong); border-radius: var(--radius-lg);
  background: none; color: var(--color-muted);
  font-size: 13px; font-weight: 650;
  transition: border-color .18s var(--ease), color .18s var(--ease);
}
.dsec-more:hover { border-color: var(--color-primary); color: var(--color-primary); }
.dsec-more:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---- Плитка с числом: герой + две тихие строки -------------------------- */
.dsec .metric-card { padding: 22px; gap: 8px; justify-content: flex-end; }
.dsec .metric-card__body { gap: 4px; }
.dsec .metric-card__label {
  font-size: 13px; font-weight: 650; color: var(--color-muted);
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
}
.dsec .metric-card__value {
  /* Только ЖЕЛАЕМЫЙ размер: фактический считает базовое правило по ширине
     карточки и длине значения (см. .metric-card__value). */
  --metric-value-size: 38px;
  font-weight: 800; letter-spacing: -0.03em;
  color: var(--color-heading); font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
/* Пилюля из базового правила здесь ГАСИТСЯ: на сводной цвет достаётся только
   вниманию и проблеме (решение заказчика 10.08.2026), а дельте ниже задана
   фиксированная высота в две строки — пилюля с ней выглядела бы сломанной. */
.dsec .metric-card__delta {
  display: block; background: transparent; padding: 0; border-radius: 0;
  font-size: 12px; font-weight: 500; color: var(--color-faint);
}
/* Цвет достаётся ТОЛЬКО состоянию «внимание» и «проблема». Норма и «успех»
   остаются нейтральными: спокойный экран и есть сообщение «всё хорошо». */
.dsec .metric-card__delta--warning { color: var(--color-warning); font-weight: 650; }
.dsec .metric-card__delta--coral { color: var(--color-danger); font-weight: 650; }
.dsec .metric-card__delta--success,
.dsec .metric-card__delta--primary { color: var(--color-faint); font-weight: 500; }
/* Число остаётся нейтральным и в жёлтом состоянии — см. комментарий у
   .metric-card__value--warning. Красное держим: проблема должна кричать. */
.dsec .dash-metric--coral .metric-card__value,
.dsec .dash-metric--critical .metric-card__value { color: var(--color-danger); }
@media (max-width: 640px) {
  .dsec .metric-card { min-height: 104px; }
  .dsec .metric-card__value { --metric-value-size: 33px; }
}

/* Карточка-блок: заголовок тише, чем заголовок раздела. */
.dsec .card__title { font-size: 13px; font-weight: 650; color: var(--color-muted); letter-spacing: 0; }
.dsec .card__subtitle { font-size: 12px; color: var(--color-faint); margin-top: 2px; }
.dsec .card__head-side { display: inline-flex; align-items: center; gap: 8px; flex: none; }

/* ---- Каталог настройки -------------------------------------------------- */
.dcat {
  position: fixed; inset: 0 0 0 auto; z-index: 60;
  width: min(440px, 100vw);
  display: flex; flex-direction: column;
  background: var(--color-surface);
  border-left: 1px solid var(--color-border);
  box-shadow: var(--shadow-pop);
}
.dcat[hidden] { display: none; }
.dcat__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 18px 20px; border-bottom: 1px solid var(--color-border);
}
.dcat__title { font-size: 17px; font-weight: 800; color: var(--color-heading); }
.dcat__sub { font-size: 12px; color: var(--color-faint); margin-top: 3px; }
.dcat__close {
  flex: none; width: 34px; height: 34px; display: grid; place-items: center;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: none; color: var(--color-muted);
}
.dcat__close:hover { border-color: var(--color-border-strong); color: var(--color-text); }
.dcat__close:focus-visible { outline: none; box-shadow: var(--ring); }
/* Без отступа сверху. Padding-top у скролл-контейнера — это полоса, сквозь
   которую содержимое проезжает ВЫШЕ липкой шапки раздела: липкий элемент
   прилипает к границе content box, а не padding box. Под «Настроить главную»
   это читалось как зазор, в котором мелькали обрезанные тумблеры. Дыхание
   перед первым разделом даёт padding-top самой .dcat__section-head. */
.dcat__body { flex: 1; overflow-y: auto; padding: 0 20px 20px; overscroll-behavior: contain; }
.dcat__section + .dcat__section { margin-top: 26px; }
.dcat__section-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 14px 0 8px; position: sticky; top: 0; z-index: 1;
  background: var(--color-surface);
}
.dcat__section-title { font-size: 15px; font-weight: 800; color: var(--color-heading); }
.dcat__section-question { font-size: 11.5px; color: var(--color-faint); margin-top: 1px; }
.dcat__count {
  flex: none; font-size: 11px; font-weight: 700; color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}
.dcat__count.is-off { color: var(--color-faint); }
.dcat__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.dcat__row {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 10px 10px 10px 8px; margin: 0 -8px;
  border-radius: var(--radius-md); cursor: pointer;
  transition: background .16s var(--ease);
}
.dcat__row:hover { background: var(--color-surface-soft); }
.dcat__input { position: absolute; opacity: 0; width: 0; height: 0; }
/* Тумблер: пружина на ручке, мгновенная заливка дорожки. Тач-цель — вся
   строка (48 px по высоте), сам переключатель только показывает состояние. */
.dcat__switch {
  flex: none; margin-top: 2px;
  width: 36px; height: 21px; border-radius: var(--radius-pill);
  background: var(--color-border-strong);
  transition: background .2s var(--ease);
  display: block; position: relative;
}
.dcat__knob {
  position: absolute; top: 2.5px; left: 2.5px;
  width: 16px; height: 16px; border-radius: 50%;
  background: #fff; box-shadow: var(--shadow-sm);
  transition: transform .26s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dcat__input:checked + .dcat__switch { background: var(--color-primary); }
.dcat__input:checked + .dcat__switch .dcat__knob { transform: translateX(15px); }
.dcat__input:focus-visible + .dcat__switch { box-shadow: var(--ring); }
.dcat__text { display: grid; gap: 2px; min-width: 0; }
.dcat__name {
  font-size: 13px; font-weight: 650; color: var(--color-text);
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.dcat__desc { font-size: 11.5px; color: var(--color-faint); line-height: 1.4; }
.dcat__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 20px; border-top: 1px solid var(--color-border);
}
.dcat-scrim {
  position: fixed; inset: 0; z-index: 59;
  background: rgba(8, 10, 16, .42); backdrop-filter: blur(2px);
}
.dcat-scrim[hidden] { display: none; }
@media (max-width: 640px) {
  .dcat { inset: auto 0 0 0; width: 100%; height: min(86vh, 720px); border-left: none;
          border-top: 1px solid var(--color-border);
          border-radius: var(--radius-2xl) var(--radius-2xl) 0 0; }
}


/* =========================================================================
   Оживление сводной панели — редизайн 08.2026
   -------------------------------------------------------------------------
   Тот же приём, что и у прежнего модуля (stage 110), и та же страховка:
   ВСЁ начальное скрытие живёт под [data-dash-motion], а атрибут ставит сам
   JS-модуль. Без JS, при prefers-reduced-motion, в фоновой вкладке и без
   IntersectionObserver атрибута нет — страница остаётся обычной статикой,
   а не пустым экраном. Анимация никогда не задерживает доступ к данным.

   Анимируем только opacity и transform: ни одно правило здесь не вызывает
   пересчёт раскладки. Схлопывание блока — исключение, и оно сделано через
   grid-template-rows/max-width на самой ячейке сетки, то есть перетекание
   соседей и есть цель, а не побочный эффект.
   ========================================================================= */

/* ---- Появление разделов: НЕТ ---------------------------------------------
   Каскад «карточки выезжают одна за другой» был убран по замечанию заказчика
   (10.08.2026): на панели, куда заходят на тридцать секунд, ожидание проезда
   раздражает — данные должны стоять на месте с первого кадра.

   Модуль оживления при этом остался: он по-прежнему отсчитывает числа и
   растит полосы графиков. Начального скрытия у разделов нет вовсе, поэтому
   ждать нечего — счётчик стартует на уже видимой карточке.

   Правило «анимируем только то, что не вызывает пересчёт раскладки» от этого
   не пострадало: движение осталось там, где оно ОТВЕТ на действие человека
   (тумблер, наведение), а не встречает его на входе. */

/* ---- Схлопывание блока --------------------------------------------------
   Соседи перетекают на освободившееся место, а не прыгают. Ячейка сжимается
   по ширине колонок сетки — grid сам доводит соседей до новых позиций. */
.dblock.is-collapsing {
  animation: dblock-collapse .34s var(--ease) forwards;
  pointer-events: none;
}
@keyframes dblock-collapse {
  0% { opacity: 1; transform: none; }
  45% { opacity: 0; transform: scale(.94); }
  100% { opacity: 0; transform: scale(.94); grid-column: span 0; max-height: 0;
         margin: 0; padding: 0; }
}
/* Раздел схлопывается целиком — заголовок уходит ПОСЛЕДНИМ: он опора взгляда,
   и если убрать его первым, оставшиеся карточки на миг повисают без хозяина. */
.dsec.is-collapsing { animation: dsec-fade .42s var(--ease) forwards; overflow: hidden; }
.dsec.is-collapsing .dblock { animation: dsec-drop .2s var(--ease) forwards; }
.dsec.is-collapsing .dsec__head { animation: dsec-drop .22s var(--ease) 160ms forwards; }
@keyframes dsec-drop {
  to { opacity: 0; transform: translateY(-6px); }
}
@keyframes dsec-fade {
  0% { max-height: 900px; opacity: 1; }
  70% { max-height: 900px; }
  100% { max-height: 0; opacity: 0; margin-top: 0; }
}
/* Возврат раздела: он прилетает на СВОЁ место в порядке (место выбирает
   бэкенд, ``layout.toggle_block``), поэтому анимация — прилёт, а не появление
   в конце списка. */
.dsec.is-returned {
  animation: dsec-land .58s cubic-bezier(0.22, 1.2, 0.36, 1) both;
}
@keyframes dsec-land {
  from { opacity: 0; transform: translateY(22px) scale(.97); }
  to { opacity: 1; transform: none; }
}

/* ---- Скелетон -----------------------------------------------------------
   Мягкий перелив, затем кросс-фейд в данные. Спиннера по центру экрана нет
   нигде: он сообщает «жди», а не «вот столько сейчас приедет». */
.dskel {
  border-radius: var(--radius-lg);
  background: linear-gradient(90deg,
    var(--color-surface-soft) 0%,
    color-mix(in srgb, var(--color-surface-soft) 55%, var(--color-surface)) 48%,
    var(--color-surface-soft) 100%);
  background-size: 240% 100%;
  animation: dskel-shimmer 1.5s linear infinite;
}
@keyframes dskel-shimmer {
  from { background-position: 120% 0; }
  to { background-position: -120% 0; }
}
.dblock__body.is-swapping > * { animation: dsec-crossfade .28s var(--ease) both; }
@keyframes dsec-crossfade {
  from { opacity: .35; }
  to { opacity: 1; }
}

/* ---- Микровзаимодействия ------------------------------------------------ */
.dsec .metric-card,
.dsec .card {
  transition: transform .18s var(--ease), box-shadow .18s var(--ease),
              border-color .18s var(--ease);
}
.dsec .metric-card:active,
.dsec .card:active { transform: scale(.995); }

/* Каталог: выезд панели и затемнение. */
.dcat.is-open { animation: dcat-in .3s var(--ease-out) both; }
.dcat-scrim.is-open { animation: dsec-crossfade .22s var(--ease) both; }
@keyframes dcat-in {
  from { transform: translateX(28px); opacity: 0; }
  to { transform: none; opacity: 1; }
}
@media (max-width: 640px) {
  @keyframes dcat-in {
    from { transform: translateY(32px); opacity: 0; }
    to { transform: none; opacity: 1; }
  }
}

/* ---- Уважение к системной настройке ------------------------------------
   Обязательное правило, а не пожелание: при «уменьшить движение» остаются
   мгновенные переходы и ничего больше. Дублируется здесь на случай, если
   элемент получил класс не от модуля (HTMX-своп, возврат раздела). */
@media (prefers-reduced-motion: reduce) {
  .dsec.is-returned,
  .dsec.is-collapsing,
  .dsec.is-collapsing .dblock,
  .dsec.is-collapsing .dsec__head,
  .dblock.is-collapsing,
  .dblock__body.is-swapping > *,
  .dcat.is-open,
  .dcat-scrim.is-open,
  .dskel { animation: none !important; }
  .dblock.is-collapsing,
  .dsec.is-collapsing { display: none; }
  .dcat__knob { transition: none; }
}
@media print {
  [data-dash-motion] .dsec .dblock,
  [data-dash-motion] .dstat { opacity: 1 !important; animation: none !important; }
}

/* ---- Правки после первого прогона в браузере (08.2026) ------------------ */
/* [hidden] на flex/grid-контейнере не работает: display из класса перебивает
   его. Ловушка стоила видимого «и ещё N» — свёрнутый список показывался. */
.dstat__list[hidden],
.dcat[hidden],
.dcat-scrim[hidden] { display: none !important; }

/* Строка состояния плотнее: она обязана читаться одним взглядом, а не
   раскладываться на два ряда карточек. */
.dstat { padding: 13px 16px; gap: 8px; }
.dstat__link { padding: 6px 11px; min-height: 34px; }
.dstat__value { font-size: 15.5px; }

/* Плитка с числом — горизонтальная полоса, а не высокая карточка. На 1180 px
   контента блок в half-ширины (≈580 px) с одной цифрой в левом верхнем углу
   выглядел пустым: пропорция 580×118 не заполняется одним числом. Цифра и
   подпись встают на одну базовую линию, карточка становится ниже и честно
   занимает ширину. */
.dsec .metric-card { padding: 22px; }
.dsec .metric-card__body {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline; column-gap: 14px; row-gap: 4px;
}
.dsec .metric-card__label { grid-column: 1 / -1; }
.dsec .metric-card__value { grid-column: 1; }
.dsec .metric-card__delta { grid-column: 2; text-align: right; }
@media (max-width: 640px) {
  .dsec .metric-card__body { grid-template-columns: 1fr; }
  .dsec .metric-card__delta { grid-column: 1; text-align: left; }
}

/* Кольцо не должно распирать раздел вдвое выше соседней плитки. */
.dsec .dash-gauge__ring { width: 124px; height: 124px; }
.dsec .dash-gauge__center strong { font-size: 24px; }
.dsec .dash-gauge__center--md strong { font-size: 19px; }
.dsec .dash-gauge__center--sm strong { font-size: 16px; }
.dsec .dash-donut__ring { width: 148px; height: 148px; }
.dsec .card { min-height: 96px; }

/* Дуга шкалы в норме — фирменная синяя, а не серая. Серую заказчик прочитал
   как «блок не работает» (10.08.2026), и он прав: у кольца серый цвет читается
   не как «всё хорошо», а как «данные не пришли» — ровно так же выглядит его
   фон. Правило «норма цвета не получает» остаётся в силе для ЧИСЕЛ: там
   отсутствие цвета сравнивается с красным и жёлтым соседями и работает.
   Внимание и проблема свои цвета сохраняют. */
.dsec .dash-gauge__arc.tone--success { stroke: var(--color-primary); }


/* Шкала внутри раздела — горизонтальная полоса вровень с плиткой-соседкой.
   Кольцо, центрированное в высокой карточке, делало ряд «плитка + шкала»
   разновысоким вдвое: 96 против 230 px, и раздел терял ритм. Кольцо уходит
   влево и уменьшается, подпись встаёт рядом — карточка становится такой же
   полосой, как и плитка с числом. */
/* Ряд раздела — единая полоса: карточки тянутся до общей высоты. Без этого
   пара «плитка 77 px + шкала 169 px» читалась как рваный ряд, а не как ритм.
   Цифра в плитке прижата к низу (существующее правило metric-card), поэтому
   растянутая карточка не выглядит полупустой. */
.dsec__grid { align-items: stretch; }
.dsec .dblock--metric { align-self: stretch; }
/* ...но только пока ряд из одних чисел. С 21.08.2026 порядок в разделе
   свободный, и число может встать рядом с графиком — там растяжка давала белую
   карточку с цифрой у нижнего края и пустотой сверху на пол-экрана (та самая
   «лесенка», ради которой 10.08.2026 заводили полосы). Смешанный ряд отмечает
   сервер: только он знает, как блоки лягут строками (``blocks.fill_rows``).
   Графики в таком ряду продолжают тянуться друг под друга — ровняться им есть
   по чему. */
.dsec .dblock--tile.dblock--loose { align-self: start; }
.dsec .dash-widget:has(.dash-gauge) { padding: 14px 18px; gap: 6px; }
.dsec .dash-widget:has(.dash-gauge) .card__subtitle { font-size: 11.5px; margin-top: 0; }
.dsec .dash-widget__body:has(> .dash-gauge) { justify-content: center; flex: 1; }
.dsec .dash-gauge { justify-content: flex-start; padding: 0; }
.dsec .dash-gauge__ring { width: 76px; height: 76px; }
.dsec .dash-gauge__center { padding: 0 14px; }
.dsec .dash-gauge__center strong { font-size: 17px; }
.dsec .dash-gauge__center--md strong { font-size: 15px; }
.dsec .dash-gauge__center--sm strong,
.dsec .dash-gauge__center--xs strong { font-size: 13px; }
.dsec .dash-gauge__center span { display: none; }
.dsec .dash-gauge__bg,
.dsec .dash-gauge__arc { stroke-width: 8.5; }
/* Кольца на сводной мельче общих, поэтому толщина у них своя. Но 8,5 при
   новом r=16 давали 21% размера — меньше, чем у общего кольца, и ровно та
   «тонкая дуга в пустой карточке», на которую жаловался заказчик. 10 даёт
   24,4% и всё ещё оставляет дырку под денежную строку (её пересчитывает
   fitDonutCenter, читая толщину и радиус прямо из разметки). */
.dsec .dash-donut__bg,
.dsec .dash-donut__seg { stroke-width: 10; }
.dsec .dash-donut__seg.is-active { stroke-width: 11.4; }
/* Подпись кольца переезжает из центра наружу — там она читается, а в кольце
   диаметром 92 px ломалась на три строки. */
.dsec .dash-gauge::after {
  content: attr(data-caption);
  font-size: 12.5px; color: var(--color-muted);
  align-self: center; margin-left: 16px; min-width: 0;
}

/* ---- Телефон: главная за один экран ------------------------------------
   Здесь и решается задача «30 секунд с телефона». До редизайна первые 811 px
   занимали плашка почты (292) и панель OZON (405) — собственник открывал
   главную и не видел ни одного числа о своём бизнесе. Ниже — бюджет высот, из
   которого следует, что в 844 px помещаются оба неприкосновенных элемента,
   строка состояния И первый раздел с цифрами. */
@media (max-width: 640px) {
  /* Шапка: заголовок, подпись и одна строка управляющих чипов. */
  .page__head { gap: 10px; }
  .page__actions { gap: 6px; flex-wrap: wrap; }
  .page__actions .chip { min-height: 34px; padding: 6px 10px; font-size: 12px; }

  /* Строка состояния: проблемы в столбик, но плотно. */
  .dstat { padding: 11px 13px; gap: 7px; margin-bottom: 12px; }
  .dstat__list { gap: 6px; }
  .dstat__item { width: 100%; }
  .dstat__link { width: 100%; min-height: 36px; padding: 7px 11px; }
  .dstat__value { font-size: 15px; }
  .dstat__label { font-size: 12.5px; }

}

/* Бюджет первого экрана телефона — вторая итерация по замерам.
   Было: почта 164 + шапка 142 + состояние 223 + OZON 256 = 785 px до первой
   цифры (при экране 844). Стало: 92 + 96 + 165 + 150 = 503, и в первый экран
   попадает раздел «Сегодня» с обеими плитками. */
@media (max-width: 640px) {
  /* Переключатель сборок с телефона убран: это вторичный выбор («тот же взгляд
     на те же данные»), а «Настроить» покрывает главную нужду. Он остаётся на
     планшете и десктопе. */
  .page__actions .lswitch { display: none; }
  /* Одной строкой держится ряд ЧИПОВ главной («Настроить» + период), ради него
     правило и писалось. Без :has() оно доставалось всем разделам подряд, и в
     кадровой карточке четыре кнопки шапки отказывались переноситься: строка
     распирала страницу до 456px на экране в 375 и уезжала за правый край
     вместе с содержимым. Кнопкам перенос нужен, чипам — нет. */
  .page__actions:has(> .chip) { flex-wrap: nowrap; }
  .page__actions .chip { font-size: 11.5px; padding: 6px 9px; min-height: 32px; }
  .page__actions .chip b { font-weight: 700; }
  .dash-period b { max-width: 15ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  .dstat__link { min-height: 34px; padding: 6px 10px; }

}

@media (max-width: 640px) {
  .dstat { padding: 10px 12px; gap: 6px; }
}

/* Третья итерация по замерам первого экрана телефона (846 → ~770 px до первой
   цифры). Каждый пункт снимает дублирование, а не информацию. */
@media (max-width: 640px) {
  /* Подпись «Сегодня, 10 августа» слово в слово повторяет бейдж периода,
     который стоит на 20 px ниже. */
  .page--dash .page__subtitle { display: none; }
}

/* Полоса «Настроить главную» под холстом. Узел есть всегда (цель oob-свопа),
   но пустой ничего не занимает. */
.dsetup:empty { display: none; }
.dsetup { margin-top: 32px; }
@media (max-width: 640px) { .dsetup { margin-top: 24px; } }

/* Строки списков внутри раздела ложатся на те же четыре уровня, что и всё
   остальное на панели. Без этого «Дыры в графике» приносили в раздел свои
   13.5/700 и 15/800 — ещё две ступени кегля рядом с четырьмя имеющимися, и
   иерархия снова размывалась на «много текста примерно одного размера». */
.dsec .dash-list__label { font-size: 13px; font-weight: 650; color: var(--color-text); }
.dsec .dash-list__sub { font-size: 12px; font-weight: 400; color: var(--color-faint); }
.dsec .dash-list__value { font-size: 14px; font-weight: 800; }
.dsec .dash-list__value--info { color: var(--color-muted); }
/* «Успех» цвета не получает и здесь — правило одно на всю панель. */
.dsec .dash-list__value--success { color: var(--color-muted); }
.dsec .card__head .btn { font-size: 12.5px; font-weight: 650; }

@media (max-width: 640px) {
  /* Ссылка-заголовок и так крупная тач-цель; чипы состояния добираем до 34. */
  .dsec .card__head .btn { min-height: 32px; }
}

/* Скелетон раздела, который сейчас приедет из каталога. Появляется только
   тогда, когда раздела на панели ещё нет и ответ придётся ждать целый
   round-trip: подменять существующий раздел успевает быстрее, чем глаз
   заметит. Размеры повторяют настоящий раздел, чтобы приезд данных не сдвигал
   страницу. */
.dsec--skeleton { pointer-events: none; }
.dskel--title { display: block; width: 180px; height: 26px; }
.dskel--card { grid-column: span 6; height: 96px; display: block; }
@media (max-width: 1000px) { .dskel--card { grid-column: span 3; } }
@media (max-width: 640px) { .dskel--card { grid-column: 1 / -1; height: 86px; } }

/* Иконка плитки скрыта ТОЛЬКО на сводной собственника: в ряду из четырёх
   плиток четыре разноцветных кружка спорили с цифрами за внимание. На панелях
   остальных ролей разметка и вид прежние — их в этой работе не трогали. */
.dsec .metric-card__icon { display: none; }

/* ---- Раскладка сводной на широком экране ---------------------------------
   С 1280 px разделы встают по два в ряд: ширина уходит в дело, панель
   становится вдвое короче. Сетка, а НЕ независимые колонки: колонки пакуют
   разделы встык и не оставляют дыр, но соседи начинаются на разной высоте, и
   панель читается вразнобой (заказчик, 10.08.2026). Ряд выравнивает верхние
   края — «уровень» виден с одного взгляда, а разница высот внутри ряда
   ограничена содержимым двух разделов, а не всей колонки.

   Ниже 1280 px разделы идут сплошняком, как на телефоне. */
@media (min-width: 1280px) {
  /* Правка идёт в ТОЙ ЖЕ раскладке, что и панель: две колонки, внутри раздела
     шесть колонок. Иначе конструктор врал бы — человек тянул бы блок в
     двенадцатиколоночной сетке во всю ширину, а на панели тот же блок
     оказывался бы в половине холста. Ширин ровно две (половина/строка), и
     ``dashboard_builder.js`` считает их по тому же правилу, что и бэкенд. */
  .dcanvas {
    display: grid;
    /* minmax(0, 1fr), а не 1fr: голый 1fr берёт за минимум ШИРИНУ СОДЕРЖИМОГО,
       и колонка с длинными подписями и легендой доната раздувалась вдвое
       против соседней. Колонки обязаны быть равными. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
    gap: 32px 28px;
  }
  /* Отступ между разделами задаёт сетка — собственный margin убираем, иначе
     верхние разделы в строке съезжают относительно нижних. */
  .dsec + .dsec,
  .dsec + .dsec-more { margin-top: 0; }
  /* Полоса настройки и пустое состояние — во всю ширину, они не разделы. */
  .dsetup, .dempty { grid-column: 1 / -1; margin-top: 0; }

  /* Сетка раздела — ДВЕНАДЦАТЬ колонок и своя ширина блока (--span), даже
     когда раздел стоит в половине холста.

     Была шестиколоночная сетка с планшетной шириной (--span-sm), и это стоило
     конструктору всей вариативности: `tablet_span` знает ровно две ступени
     («половина» и «вся строка»), поэтому на десктопе десять ширин из двенадцати
     выглядели одинаково, и тянуть было нечего (заказчик, 10.08.2026: «нужно
     больше вариативности, длинные блоки можно подужать»).

     Опасение, из-за которого шесть колонок и вводили, — «четверть строки
     превращается в 107 px» — снято замером: половина холста это 824 px на
     1920 и ~600 px на 1280, то есть четверть строки — 206 и 150 px. Для
     плитки с числом это рабочая ширина. Планшетная пара (--span-sm) осталась
     там, где она и нужна, — в медиазапросе до 1000 px. */
  .dsec__grid { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  .dsec .dblock { grid-column: span var(--span, 3); }

  /* Раздел, которому не досталось пары, занимает ОБЕ колонки.
     Это самая большая дыра панели, и видна она была не сразу: при нечётном
     числе разделов последний вставал в левую колонку, а вся правая рядом с ним
     оставалась пустой — на «Утре» это 664×404, четверть холста (заказчик,
     10.08.2026: «минимум пустого пространства»).

     Замер на той же сборке: пустого места 43 % → 28 %, и панель при этом стала
     КОРОЧЕ (1149 → 1097 px) — блоки одинокого раздела раскладываются в
     двенадцать колонок вместо шести и занимают меньше строк.

     Пробовали и более радикальное — все разделы во всю ширину: на «Утре»
     пустоты 21 %, но высота 1149 → 1645, а на «Стандартной» выигрыша нет вовсе
     (28 % против 29 %) при вдвое более длинной панели. Отвергнуто: платить
     двойной прокруткой за процент пустоты не стоит. */
  .dsec:last-of-type:nth-of-type(odd) { grid-column: 1 / -1; }
}

/* Узкая плитка: подпись переезжает ПОД цифру, а не жмётся справа от неё.
   В двухколоночной раскладке карточка сужается до ~280 px, и подпись вроде
   «июнь 2026 · −85,5 % к прошлому периоду» рвалась на четыре строки, вытягивая
   карточку вдвое выше соседей. Контейнерный запрос, а не медиа: ширину задаёт
   сетка раздела, а не вьюпорт (.metric-card уже объявлена container). */
@container (max-width: 360px) {
  .dsec .metric-card__body { grid-template-columns: minmax(0, 1fr); }
  .dsec .metric-card__value,
  .dsec .metric-card__delta { grid-column: 1; text-align: left; }
}

/* Длинный список внутри раздела показывает три строки, остальное — за «ещё N».
   Обещание из плана («свёртка вместо потери данных») стало обязательным, когда
   разделы встали по два в ряд: «Дыры в графике» на шести точках вытягивали
   раздел «Люди» вчетверо выше соседнего. Кнопку добавляет JS (initDashLists) —
   без него видны все строки, то есть деградация без потери данных. */
.dsec .dash-list:not(.is-open) > .dash-list__item:nth-child(n + 4) { display: none; }
.dsec .dash-list__more {
  align-self: flex-start;
  margin-top: 2px; padding: 6px 2px; min-height: 30px;
  background: none; border: none;
  font: inherit; font-size: 12.5px; font-weight: 650; color: var(--color-primary);
}
.dsec .dash-list__more:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--radius-sm); }

/* ---- Кольца в разделе: одна высота на всю строку -------------------------
   Замечание заказчика (10.08.2026): «графики отличаются по высоте». В узкой
   колонке кольцо (176 px) и легенда вставали друг под другом, и высота
   карточки зависела от числа строк легенды: у «Точек сети» один сегмент, у
   «Программы на ПК» — два, разница в шестьдесят пикселей на соседних блоках.

   Лечим не подгонкой, а причиной: кольцо меньше, легенда компактнее и
   ЗАФИКСИРОВАННОЙ высоты на три строки. Три — потолок осмысленного доната на
   этой панели (открыто/опоздали/не открылись, установлена/ждут), больше
   сегментов читаются уже не кольцом, а списком. */
/* 03.09.2026, заход второй и по другой причине. Прошлое решение лечило разрыв
   «подпись … значение» ампутацией ширины: легенду сузили до 320 px, пару
   отцентрировали, и пустота ушла в ПОЛЯ по краям карточки. Заказчик увидел её
   там же: «у круговых очень много пустого пространства». Замер подтвердил —
   на блоке во всю строку кольцо занимало 22% ширины, а справа от легенды
   оставалось 167-292 px чистого поля.
   Теперь ширина не отнимается, а ЗАНИМАЕТСЯ: пара разложена в две колонки
   (кольцо по содержимому, легенда — весь остаток), а разрыв внутри строки
   легенды закрывает трек доли. Ради этого правило переехало с flex на grid:
   у .dash-donut ровно два потомка, и они ложатся в колонки сами, без правки
   разметки.

   ВАЖНО: растяжение включается только с 560 px ширины КАРТОЧКИ (@container
   ниже). В узком блоке отдавать легенде «весь остаток» нельзя: остаток там
   меньше, чем нужно строке, и «220 744,37 ₽ · 39%» вылезала за карточку на
   68 px. Пустоты в узком блоке и нет — она была ровно на широких. */
.dsec .dash-donut {
  display: flex;
  gap: 14px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.dsec .dash-donut__ring { width: 132px; height: 132px; }
.dsec .dash-donut__center { padding: 0 26px; }
.dsec .dash-donut__center strong { font-size: 19px; }
.dsec .dash-donut__center--md strong { font-size: 16px; }
.dsec .dash-donut__center--sm strong,
.dsec .dash-donut__center--xs strong { font-size: 13px; }
.dsec .dash-donut .chart-legend {
  gap: 2px;
  /* Три строки — потолок осмысленного кольца на этой панели; резервируем их
     всегда, и соседние кольца встают вровень независимо от числа сегментов. */
  min-height: 66px;
  align-content: center;
  /* В узкой карточке всё как было: легенда по содержимому, пара по центру.
     Потолок ширины снимается только на широкой (@container 560px), где его и
     ставили ради несуществующей теперь проблемы. */
  flex: 0 1 auto; min-width: 150px; max-width: 320px;
}
/* Строка легенды доната = строка графика: подпись — трек доли — значение.
   Тот же приём, что у горизонтальных полос .dash-chart в этом же шаблоне;
   раньше две ветки одного файла не знали друг о друге, и кольцо решало
   задачу кружком, а полосы — треком. Трек показан только там, где есть
   свободная ширина: в узкой карточке он схлопывается до нуля (min-width: 0)
   и строка выглядит как прежде. */
.dsec [data-donut-legend] .chart-legend__track {
  flex: 1 1 auto;
  min-width: 0;
  height: 6px;
  margin: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--chart-track);
  overflow: hidden;
}
.dsec [data-donut-legend] .chart-legend__fill {
  display: block;
  width: var(--pct, 0%);
  height: 100%;
  border-radius: inherit;
  background: var(--seg, var(--color-primary));
  transition: width .5s var(--ease);
}
/* Цвет заливки берётся из тона СЕГМЕНТА, а он объявлен на точке (.tone--*),
   не на строке. Поднимаем его на строку через саму точку: соседний селектор
   дешевле, чем дублировать класс тона в разметке. */
.dsec [data-donut-legend] li:has(.tone--primary) .chart-legend__fill { background: var(--color-primary); }
.dsec [data-donut-legend] li:has(.tone--success) .chart-legend__fill { background: var(--color-success); }
.dsec [data-donut-legend] li:has(.tone--warning) .chart-legend__fill { background: var(--chart-amber, var(--color-warning)); }
.dsec [data-donut-legend] li:has(.tone--danger) .chart-legend__fill { background: var(--color-danger); }
.dsec [data-donut-legend] li:has(.tone--coral) .chart-legend__fill { background: var(--color-coral); }
.dsec [data-donut-legend] li:has(.tone--beige) .chart-legend__fill { background: var(--chart-sand, var(--color-beige)); }
.dsec [data-donut-legend] li:has(.tone--violet) .chart-legend__fill { background: var(--color-violet); }
.dsec [data-donut-legend] li:has(.tone--muted) .chart-legend__fill { background: var(--color-faint); }
.dsec .dash-donut .chart-legend li { font-size: 12px; padding: 2px 7px; }
/* Значение возвращается в ОДНУ строку с названием: перенос вниз (правило для
   широкой карточки) удваивал высоту каждой строки легенды, а вместе с ней и
   разброс высот между соседними кольцами. В колонке места хватает. */
.dsec [data-donut-legend] .chart-legend__val {
  margin-left: auto; flex-basis: auto; text-align: right;
}
.dsec [data-donut-legend] li { flex-wrap: nowrap; }
.dsec [data-donut-legend] li > span:not(.chart-legend__dot):not(.chart-legend__val):not(.chart-legend__track),
.dsec [data-donut-legend] li > a {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Кольцо растёт вместе с карточкой. 132 px были рассчитаны на узкую колонку, но
   блок во всю строку раздела — это 1030 px на широком мониторе, и кольцо в нём
   выглядело значком в углу пустого поля (заказчик, 10.08.2026: «слишком
   огромное, много пустого пространства»). Контейнерный запрос, а не медиа:
   ширину карточки задаёт раскладка человека, а не размер окна — тот же блок в
   половину строки обязан остаться прежним. */
.dsec .dash-widget:has(.dash-donut) { container-type: inline-size; container-name: donutcard; }
/* Ступень «есть лишняя ширина» — 440 px. С неё пара раскладывается в две
   колонки: кольцо по содержимому, легенда — весь остаток, а разрыв внутри
   строки занимает трек доли. Порог отдельный от «крупного кольца» (560 ниже)
   нарочно: растянуть легенду можно раньше, чем увеличивать кольцо, и блок
   в половину строки (486 px) перестаёт отдавать 117 px в поле.
   Ниже 440 остаётся прежний вид: там лишней ширины нет, а «220 744,37 ₽ · 39%»
   в растянутой строке просто не помещается. */
@container donutcard (min-width: 440px) {
  .dsec .dash-donut {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    justify-content: stretch;
  }
  .dsec .dash-donut .chart-legend { min-width: 0; max-width: none; }
  .dsec [data-donut-legend] .chart-legend__track { display: block; }
  /* Подпись перестаёт тянуться: свободную ширину забирает трек, а не
     многоточие. Ограничение живёт ТОЛЬКО здесь — в узкой карточке трека нет,
     и резать подпись там незачем. */
  .dsec [data-donut-legend] li > span:not(.chart-legend__dot):not(.chart-legend__val):not(.chart-legend__track),
  .dsec [data-donut-legend] li > a { flex: 0 1 auto; max-width: 46%; }
}
@container donutcard (min-width: 560px) {
  .dsec .dash-donut { gap: 28px; }
  .dsec .dash-donut__ring { width: 172px; height: 172px; }
  .dsec .dash-donut__center { padding: 0 34px; }
  .dsec .dash-donut__center strong { font-size: 24px; }
  .dsec .dash-donut__center--md strong { font-size: 20px; }
  .dsec .dash-donut .chart-legend li { font-size: 13px; }
}
/* Вторая ступень. Выше 560 px кольцо застревало на 172 навсегда, а блок во всю
   строку на широком мониторе — это 900-1400 px: кольцо занимало 12-19% ширины
   карточки. Ступень обязана стоять ПОСЛЕ предыдущего @container: у правил
   одинаковая специфичность, побеждает нижнее. */
@container donutcard (min-width: 900px) {
  .dsec .dash-donut { gap: 36px; }
  .dsec .dash-donut__ring { width: 208px; height: 208px; }
  .dsec .dash-donut__center { padding: 0 42px; }
  .dsec .dash-donut__center strong { font-size: 28px; }
  .dsec .dash-donut__center--md strong { font-size: 23px; }
  .dsec .dash-donut__center--sm strong { font-size: 18px; }
  .dsec .dash-donut__center--xs strong { font-size: 15px; }
  .dsec .dash-donut .chart-legend { gap: 6px; }
  .dsec .dash-donut .chart-legend li { font-size: 13.5px; padding: 4px 8px; }
  .dsec [data-donut-legend] .chart-legend__track { height: 8px; margin: 0 10px; }
}

/* Все кольца раздела — одной высоты, в какой бы строке ни стояли.
   Замер показал: кольцо и легенда у всех одинаковые (106 px), шапка тоже
   (27 px), а расходились карточки ровно на строку ПОДПИСИ — у одного блока
   она есть, у других нет. Поэтому не подгоняем общую высоту числом (оно
   поехало бы от каждого нового текста), а резервируем недостающую строку там,
   где подписи нет. Браузер без :has() просто оставит прежнюю разницу — это
   косметика, а не поломка. */
.dsec .dash-widget:has(.dash-donut) .dash-widget__body { justify-content: center; flex: 1; }
.dsec .dash-widget:has(.dash-donut):not(:has(.card__subtitle)) .card__head {
  margin-bottom: calc(1.45em + 12px);
}

/* Полоса плиток и полоса графиков разделены собственным зазором: между ними
   он тот же, что между карточками, — полосы читаются как одна группа. */
.dsec__grid + .dsec__grid { margin-top: 14px; }
.dsec__grid:empty { display: none; }
@media (max-width: 640px) { .dsec__grid + .dsec__grid { margin-top: 12px; } }

/* Шкала стоит в ряду с плитками — значит и высотой должна быть плиткой:
   кольцо слева, число внутри, подпись рядом. Заголовок и подпись карточки
   ужимаются до уровня плитки, чтобы ряд читался одной строкой.

   Правила висят на БЛОКЕ (.dblock--tile), а не на сетке: раздел — одна сетка в
   свободном порядке (21.08.2026), и вид плитки не может зависеть от того, где
   она в этом порядке оказалась. Признак «плитка» ставит сервер (meta.tile),
   поэтому панель и конструктор читают одно и то же. */
.dsec .dblock--tile .dash-widget { padding: 12px 16px; gap: 2px; min-height: 0; }
.dsec .dblock--tile .card__title { font-size: 13px; font-weight: 650; }
.dsec .dblock--tile .card__subtitle { font-size: 12px; margin-top: 0; }
.dsec .dblock--tile .dash-gauge__center strong { font-size: 20px; }
.dsec .dblock--tile .dash-gauge::after { font-size: 12px; margin-left: 12px; }
.dsec .dblock--tile .dash-widget__body { flex: none; }
/* Кольцо шкалы — 80 px, а не 64. При включённой плотности (html{zoom:.8}) 64 px
   превращались в 51 экранный, и «11%» внутри читалось хуже подписи под ним:
   заказчик (10.08.2026) назвал шкалу «слишком мелкой». Плитка высотой 129 px
   держит 80 px кольца без роста строки. */
.dsec .dblock--tile .dash-gauge__ring { width: 80px; height: 80px; }

/* Подвал каталога: «Ширина и порядок» — видимая кнопка, а не бледная ссылка в
   углу, которую вдобавок перекрывала плавающая кнопка обратной связи. */
.dcat__foot { flex-direction: column; align-items: stretch; gap: 8px; }
.dcat__advanced {
  display: flex; align-items: center; gap: 11px;
  padding: 11px 13px; border-radius: var(--radius-md);
  border: 1px solid var(--color-border); background: var(--color-surface-alt);
  color: var(--color-text);
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.dcat__advanced:hover { border-color: var(--color-primary); background: var(--color-primary-soft); }
.dcat__advanced:focus-visible { outline: none; box-shadow: var(--ring); }
.dcat__advanced > svg:first-child { flex: none; color: var(--color-primary); }
.dcat__advanced-text { display: grid; gap: 1px; min-width: 0; flex: 1; }
.dcat__advanced-text b { font-size: 13px; font-weight: 700; }
.dcat__advanced-text span { font-size: 11.5px; color: var(--color-faint); }
.dcat__advanced-go { flex: none; color: var(--color-faint); }
.dcat__reset { align-self: flex-start; }
/* Плавающая кнопка обратной связи живёт в том же углу и на том же z-index,
   что и подвал каталога, — на время настройки её убираем. */
body:has(.dcat:not([hidden])) .fb-fab { display: none; }

/* =========================================================================
   Живые шапки разделов (stage 144)

   Промо-шапка — единственный крупный блок над сгибом, и статичной она
   читается как картинка из презентации. Оживляем ФОН и акценты, а не текст:
   подсветка медленно плывёт и дышит, по главной кнопке изредка проходит блик,
   номера шагов подсвечиваются по очереди. Заголовок, абзац и цифры не
   двигаются — каскадные появления контента заказчик отклонял уже дважды
   (см. .fade-in и снятый каскад карточек сводной), повторять их здесь нельзя.

   Разметке нужен один класс `hero-live` на корне шапки. Пятно берётся из
   собственного ::before шапки, если оно там есть, — правило безопасно и для
   шапок без него: псевдоэлемента без content просто не существует. Шапкам,
   у которых своего пятна нет, класс рисует его сам. Всё движение глушится
   общим правилом prefers-reduced-motion в конце секции анимаций.
   ========================================================================= */
@keyframes hero-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
/* Пятно живёт своей жизнью: плывёт и меняет яркость, период длинный —
   на глаз это не «анимация», а ощущение, что блок дышит. */
@keyframes hero-aura {
  from { transform: translate3d(0, 0, 0) scale(1); opacity: .72; }
  to   { transform: translate3d(-5%, 6%, 0) scale(1.14); opacity: 1; }
}
/* Подсветку, растянутую на весь блок (inset: 0), сдвигать нельзя — при
   увеличении она вылезет за скруглённый угол. Ей остаётся дыхание яркостью. */
@keyframes hero-aura-still { from { opacity: .5; } to { opacity: .95; } }
@keyframes hero-sheen {
  0%        { transform: translateX(-140%) skewX(-14deg); }
  42%, 100% { transform: translateX(560%) skewX(-14deg); }
}
/* Номер шага загорается и гаснет; соседние сдвинуты по фазе, поэтому по
   списку 1-2-3 сверху вниз пробегает волна — «мастер идёт по шагам». */
@keyframes hero-step-glow {
  0%, 58%, 100% {
    background: var(--color-primary-soft);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
  }
  16%, 30% {
    background: color-mix(in srgb, var(--color-primary) 20%, var(--color-primary-soft));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 42%, transparent),
                0 0 0 5px color-mix(in srgb, var(--color-primary) 13%, transparent);
  }
}

/* fill-mode backwards (не both) — завершившаяся filling-анимация opacity
   навсегда оставляет stacking context, см. комментарий у .fade-in. */
.hero-live { animation: hero-rise .55s var(--ease-out) backwards; }
.hero-live::before { animation: hero-aura 19s ease-in-out infinite alternate; }
.kpi-cab-hero.hero-live::before,
.ins-hero.hero-live::before { animation-name: hero-aura-still; }
.eco-hero.hero-live .eco-hero__glow { animation: hero-aura 19s ease-in-out infinite alternate; }
/* Экономика открывает шапку своим наблюдателем прокрутки (data-eco-reveal):
   вторая анимация входа дала бы двойное движение одного блока. */
.eco-hero.hero-live { animation-name: none; }

/* Шапкам без собственного пятна класс рисует своё. Пятно уходит под контент
   через отрицательный z-index, поэтому корню нужен свой stacking context
   (isolation) — иначе оно спрячется под фон блока. Детей не трогаем: у
   .qhero__done позиция absolute, и общее правило `> *` её бы сломало. */
.qhero.hero-live,
.bundle-hero.hero-live,
.claim-hero.hero-live,
.payroll-detail-hero.hero-live { position: relative; overflow: hidden; isolation: isolate; }
.qhero.hero-live::after,
.bundle-hero.hero-live::after,
.claim-hero.hero-live::after,
.payroll-detail-hero.hero-live::after {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  right: -180px; top: -230px; width: 460px; height: 460px; border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--hero-tone) var(--hero-tone-mix), transparent), transparent 68%);
  animation: hero-aura 19s ease-in-out infinite alternate;
}
.hero-live { --hero-tone: var(--color-primary); --hero-tone-mix: 15%; }
.payroll-detail-hero.hero-live { --hero-tone: var(--color-success); }
/* Шапка «Турбо-ПВЗ» тёмная в любой теме — ей плотность нужна всегда. */
.bundle-hero.hero-live { --hero-tone-mix: 26%; }
/* На тёмном фоне те же 15 % не видно вовсе — пятно приходится делать плотнее,
   иначе движение есть только в отладчике. */
html[data-theme="dark"] .hero-live { --hero-tone-mix: 26%; }

/* Главная кнопка шапки изредка ловит блик — взгляд возвращается к действию,
   а не к тексту. Пауза между проходами длинная: непрерывное мерцание
   читается как реклама, а не как интерфейс. */
.hero-live .btn-primary { position: relative; overflow: hidden; }
.hero-live .btn-primary::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 22%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .34), transparent);
  animation: hero-sheen 8s var(--ease) 1.4s infinite;
  pointer-events: none;
}

.formula-hero.hero-live .formula-hero__step-num { animation: hero-step-glow 5.4s var(--ease) infinite; }
.formula-hero.hero-live .formula-hero__step:nth-child(2) .formula-hero__step-num { animation-delay: .55s; }
.formula-hero.hero-live .formula-hero__step:nth-child(3) .formula-hero__step-num { animation-delay: 1.1s; }
.formula-hero.hero-live .formula-hero__step:nth-child(4) .formula-hero__step-num { animation-delay: 1.65s; }

/* =========================================================================
   Телефон: неприкосновенное сжимается, десктоп не меняется (10.08.2026)
   -------------------------------------------------------------------------
   Плашка почты и панель «Данные OZON» возвращены к прежнему виду по просьбе
   заказчика — и на десктопе он остаётся ровно таким. Но на экране 390×844 эти
   два блока занимают 697 px из 844, и первая цифра о бизнесе оказывается на
   1162-м пикселе: главная перестаёт отвечать на свой вопрос за тридцать секунд.

   Поэтому сжимаем их ТОЛЬКО ниже 640 px и только за счёт повторов: заголовок
   плашки уже содержит и суть, и последствие («иначе пароль не восстановить»),
   а у каждого срока OZON подпись дублирует то, что написано на его же листке.
   Ни одного действия и ни одной цифры при этом не теряется.
   ========================================================================= */
@media (max-width: 640px) {
  /* --- Плашка почты: 292 px → ~110 --- */
  .mail-cta { padding: 12px 14px; gap: 8px 12px; margin-bottom: 12px; }
  .mail-cta__icon { width: 30px; height: 30px; border-radius: 9px; margin-top: 0; }
  .mail-cta__icon svg { width: 17px; height: 17px; }
  /* Надзаголовок «ПОЧТА НЕ УКАЗАНА» пересказывает заголовок, который идёт
     строкой ниже, — на узком экране это лишняя строка. */
  .mail-cta__eyebrow { display: none; }
  .mail-cta__title { font-size: 14px; line-height: 1.3; margin-top: 0; }
  /* Абзац объяснения ужимается до двух строк: первая фраза несёт то, ради чего
     он написан («забудете пароль — придёт ссылка»), остальное — повтор
     заголовка другими словами. Полный текст остаётся на странице почты. */
  .mail-cta__text {
    font-size: 12px; margin-top: 4px;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .mail-cta__form { flex-wrap: nowrap; gap: 6px; }
  .mail-cta__input { flex: 1 1 auto; min-width: 0; height: 40px; font-size: 13px; }
  .mail-cta__btn { min-height: 40px; padding-inline: 14px; }

  /* --- «Данные OZON»: 405 px → ~200 --- */
  .repcal { padding: 12px 13px; margin-bottom: 12px; }
  .repcal__head { flex-direction: column; align-items: stretch; gap: 8px; margin-bottom: 9px; }
  .repcal__title { font-size: 14px; }
  .repcal__summary { font-size: 12px; }
  .repcal__slots { gap: 6px; }
  .repslot { padding: 8px 10px; gap: 10px; align-items: center; }
  .repslot__leaf { width: 38px; }
  .repslot__month { font-size: 8.5px; }
  .repslot__day { font-size: 19px; }
  .repslot__title { font-size: 13px; }
  /* Подпись срока и «факт» пересказывают то, что уже написано на листке и в
     строке справа: дату и состояние. Оставляем «факт» — он единственный несёт
     новое (что именно загружено и когда). */
  .repslot__sub { display: none; }
  .repslot__fact { font-size: 11.5px; }
  .repslot__side { gap: 4px; }
  .repslot__when { font-size: 10.5px; padding: 2px 6px; }
  .repslot__action { min-height: 30px; padding-inline: 11px; font-size: 12px; }
}

@media (max-width: 640px) {
  /* Фраза панели и подпись просроченного срока говорят одно и то же
     («рейтинг обновился — соберите»), а кнопка стоит рядом со сроком. На узком
     экране остаётся срок: он точнее и ведёт к действию. */
  .repcal--due .repcal__summary { display: none; }
  .repcal--due .repcal__head { margin-bottom: 6px; }
  /* Строка состояния: чипы плотнее — их число растёт вместе с проблемами,
     и каждый лишний пиксель множится. */
  .dstat { padding: 11px 12px; gap: 7px; }
  .dstat__list { gap: 5px; }
  .dstat__link { min-height: 34px; padding: 5px 10px; }
  .dstat__value { font-size: 15px; }
  .dstat__label { font-size: 12.5px; max-width: none; }
}

/* ---- Единая высота плитки ------------------------------------------------
   Замечание заказчика (10.08.2026): «разнобой, всё по разным уровням». Плитки
   в разных разделах выходили 93 px и 135 px — разницу давала подпись: где-то
   одна строка, где-то три. Ряд с рядом ещё совпадал (карточки тянутся друг за
   другом), а две колонки между собой — уже нет, и панель читалась ступеньками.

   Лечим причиной: подписи резервируют ДВЕ строки всегда. Короткая подпись
   добирает пустой строкой, длинная в две укладывается — и любая плитка на
   панели, в любом разделе и любой колонке, одной высоты. */
/* 129 CSS-px = 103 экранных при html{zoom:.8} — ровно та высота, которую
   набирает плитка с двухстрочной подписью. Пол нужен, чтобы одинокая плитка в
   ряду (тянуться не за кем) не оказалась ниже остальных. */
/* Пол высоты — ОДИН на всю сводную. Раньше их было три (118, 96 и 129), и
   фактически работал только последний; после роста полей 16 → 22 и кегля
   значения 34 → 44 честный пол это 152. */
.dsec .metric-card,
.dsec .dblock--tile .dash-widget { min-height: 144px; }
/* Подпись — РОВНО две строки: не min-height, а фиксированная высота. С
   min-height подпись из двух длинных строк давала лишние 4 px, и ряды снова
   расходились на пиксели. */
.dsec .metric-card__delta,
.dsec .dblock--tile .card__subtitle {
  height: 2.5em; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Карточка-шкала в полосе плиток строится КАК плитка, а не как график:
   кольцо слева, заголовок и подпись справа. Пока она оставалась «карточкой с
   графиком», её высота складывалась иначе (заголовок + подпись + кольцо
   столбиком) — выходило 134 px против 106 у соседей, и ряд с ней тянул за
   собой всю строку. */
.dsec .dblock--tile .dash-widget:has(.dash-gauge) {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "ring head" "ring sub";
  align-items: center;
  column-gap: 14px; row-gap: 2px;
}
.dsec .dblock--tile .dash-widget:has(.dash-gauge) .card__head { grid-area: head; align-self: end; }
.dsec .dblock--tile .dash-widget:has(.dash-gauge) .card__subtitle { grid-area: sub; align-self: start; }
.dsec .dblock--tile .dash-widget:has(.dash-gauge) .dash-widget__body { grid-area: ring; }
.dsec .dblock--tile .dash-gauge::after { display: none; }

@media (max-width: 640px) {
  .dsec .metric-card,
  .dsec .dblock--tile .dash-widget { min-height: 104px; }
}

/* Появления у карточек раздела нет — как и у всей панели (заказчик,
   10.08.2026). Общий `.stagger-item` в системе объявлен с fill-mode backwards,
   то есть до старта анимации держит opacity: 0. Пока анимация идёт — это
   незаметные 0,18 с, но она НЕ идёт в фоновой вкладке (браузер замораживает
   кадры) и в паре других случаев: карточки просто остаются невидимыми, а
   раздел выглядит пустым при живом содержимом. На панели, где появление и так
   отключено, правило только вредит. */
.dsec .stagger-item { animation: none; opacity: 1; }

/* =========================================================================
   Аренда точек (08.2026)
   Вкладка «ПВЗ → Аренда» и блок аренды в карточке точки. Цвет здесь несёт
   состояние срока, а не украшение: просрочено — danger, скоро — warning,
   оплачено — success. Токены общие с остальной системой, своих цветов нет.
   ========================================================================= */
.rent-month { display: flex; align-items: center; gap: 8px; }
.rent-month__label { font-size: 13px; color: var(--color-muted); white-space: nowrap; }
/* Селект фильтра — по содержимому. У .input ширина 100 %, и на «Коммуналке»
   с двумя парами «подпись + поле» каждое поле требовало всю строку: пара
   переносилась под пару, шапка вырастала со строки до 118 px, а между
   заголовком и вкладками зияла пустота. */
.rent-month .input { width: auto; min-width: 132px; max-width: 220px; }

/* Воздух между блоками доски. Сама доска — один <div> (её целиком отдаёт
   HTMX-ответ смены срока), и её содержимое выпадало из flex-раскладки
   страницы: у .page есть gap 20px, а внутри обёртки его нет вовсе. Карточки
   стояли кромка в кромку — «Новый счёт» читался как продолжение
   предупреждения над ним, а не как отдельная карточка. Отдельные margin'ы
   вроде .rating-stale тут гасим: ритм задаёт одна величина, а не сумма
   случайных отступов. */
.money-board { display: flex; flex-direction: column; gap: 22px; }
.money-board > .rating-stale,
.money-board > .bulk-bar { margin-bottom: 0; }
/* На телефоне ритм страницы плотнее (mobile.css: .page gap 14px) — доска
   держит тот же шаг, иначе она одна разъезжается на фоне остальных разделов. */
@media (max-width: 819px) {
  .money-board { gap: 14px; }
}
/* Больше воздуха внутри блоков — только на большом экране. На телефоне
   плотность задаёт mobile.css одноклассовыми селекторами (.card 14px,
   td 9/10px), а правила ниже специфичнее и перебили бы их молча: та же
   ловушка, что с плиткой метрик. */
@media (min-width: 820px) {
  .money-board > .card { padding: 22px; }
  .money-board > .rating-stale { padding: 14px 18px; }
  /* Строки обеих таблиц просторнее общей плотности: это списки денег, их
     читают сверху вниз, а не сканируют как справочник. */
  .money-board .table tbody td { padding: 14px; }
}

/* Итоги месяца занимают строку целиком. Общая .metric-grid — сетка на семь
   колонок под плотные панели; здесь карточек три-четыре, и они вставали в
   левой половине экрана, а справа зияла пустая половина: читалось как
   «остальное не догрузилось», а подписи при этом ломались на две строки. */
.metric-grid--money { grid-template-columns: repeat(auto-fit, minmax(222px, 1fr)); }
/* Карточек в ряду три-четыре, и каждая шире обычной — цифра в ней должна
   держать масштаб, иначе сумма месяца теряется в углу пустой плитки. Только на
   большом экране: на телефоне плитки стоят 2×N и живут по правилам mobile.css
   (padding 11px, значение 19–22px), а этот селектор специфичнее и молча
   перебил бы их, раздув плитку на пол-экрана. */
@media (min-width: 820px) {
  .metric-grid--money .metric-card { padding: 17px 18px; }
  .metric-grid--money .metric-card__value { --metric-value-size: 33px; }
}

/* Доля закрытого — полоской под числом: «оплачено 1 из 6» видно с полосы
   раньше, чем прочитан текст подписи. Цвет один, success: полоса отвечает на
   вопрос «сколько уже закрыто», а не подсвечивает беду. */
.metric-meter {
  display: block; height: 5px; margin-top: -3px;
  border-radius: var(--radius-pill);
  background: var(--color-surface-soft);
  overflow: hidden;
}
.metric-meter__fill {
  display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--color-success),
                              color-mix(in srgb, var(--color-success) 58%, #FFFFFF));
}

.rent-table td { vertical-align: top; }
.rent-table__actions { white-space: nowrap; }
/* Ширины колонок заданы явно. На авто-раскладке пять коротких колонок делили
   остаток поровну: между суммой и сроком висел провал в сотню пикселей, а
   «Оплата» уезжала от своей же строки к правому краю таблицы.
   Классами, а не :nth-child: колонка выбора появляется и исчезает вместе с
   правом на массовую отметку, и позиционная раскладка сдвинулась бы вся. */
.pick-col { width: 38px; }
.pick-col input[type="checkbox"] { display: block; margin: 2px 0 0; }
.rent-col-point { width: 34%; }
.rent-col-money { width: 13%; }
.rent-col-due { width: 16%; }
.rent-col-state { width: 14%; }
.rent-col-act { width: 21%; }
/* Деньги — колонкой цифр: одинаковая ширина знака и правый край, иначе
   «7 000 ₽» и «72 000 ₽» взглядом не сравнить. */
.rent-money { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Просроченная строка помечается полосой слева, а не заливкой: заливка на
   половину таблицы читается как «всё сломалось», полоса — как «сюда». */
.rent-row--overdue td:first-child { box-shadow: inset 3px 0 0 var(--color-danger); }
.rent-row--due td:first-child { box-shadow: inset 3px 0 0 var(--color-warning); }
.rent-row--paid { opacity: 0.72; }

/* ---- Срок: календарь прямо в строке ------------------------------------
   Дата остаётся значением таблицы, а не кнопкой: рамка проявляется на
   наведении, постоянно видна только иконка календаря — шесть десятков кнопок
   «5 августа» превратили бы список в панель управления. */
.rent-due__pick {
  display: inline-flex; align-items: center; gap: 6px;
  margin: -3px -7px; padding: 3px 7px;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  color: inherit; text-decoration: none; font: inherit; cursor: pointer;
  transition: background-color .15s var(--ease), border-color .15s var(--ease),
              color .15s var(--ease);
}
.rent-due__icon { width: 14px; height: 14px; flex: none; color: var(--color-faint); }
.rent-due__label { font-variant-numeric: tabular-nums; }
.rent-due__pick:hover { background: var(--color-surface-soft); border-color: var(--color-border-strong); }
.rent-due__pick:hover .rent-due__icon { color: var(--color-primary); }
.rent-due__pick:focus-visible { outline: none; border-color: var(--color-primary); box-shadow: var(--ring-soft); }
.rent-due__pick[aria-expanded="true"] {
  background: var(--color-primary-soft); border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
.rent-due__pick[aria-expanded="true"] .rent-due__icon { color: var(--color-primary); }

/* Поповер выбора числа. Каркас — общий с справочным календарём периода
   (.periodcal): фиксированное позиционирование в <body>, потому что
   .table-wrap скроллится и обрезал бы поповер внутри ячейки. */
.rentcal { transform-origin: top left; }
.rentcal__main { width: 288px; }
.rentcal__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.rentcal__code { font-size: 14px; font-weight: 800; color: var(--color-heading); letter-spacing: -0.01em; }
.rentcal__month { font-size: 12.5px; font-weight: 600; color: var(--color-muted); }
.rentcal .periodcal__day.is-blank { visibility: hidden; }
.rentcal__day {
  border-color: var(--color-border); background: var(--color-surface);
  cursor: pointer; font: inherit; font-size: 13px; font-weight: 600;
  color: var(--color-text); font-variant-numeric: tabular-nums;
  transition: background-color .12s var(--ease), border-color .12s var(--ease),
              color .12s var(--ease);
}
.rentcal__day:hover { background: var(--color-primary-soft); border-color: var(--color-primary); color: var(--color-primary-dark); }
.rentcal__day:focus-visible { outline: none; box-shadow: var(--ring); }
/* «Сегодня» — обводка, «выбрано» — заливка: это два разных ответа, и одинаковый
   синий квадрат путал бы их. Отсюда же переопределение .periodcal__day.is-today
   (там сегодня залито): в выбирающем календаре заливка занята выбором. */
.rentcal .periodcal__day.is-today {
  background: var(--color-surface); border-color: var(--color-primary);
  color: var(--color-primary-dark); font-weight: 800;
}
.rentcal .periodcal__day.is-picked {
  background: var(--color-primary); border-color: var(--color-primary);
  color: var(--color-on-primary); font-weight: 800;
}
.rentcal .periodcal__day.is-picked:hover { background: var(--color-primary-strong); color: var(--color-on-primary); }

/* Числа, которых в этом месяце нет: договор «оплата до 31-го» существует, и
   короткий месяц не должен делать такой срок невыбираемым. */
.rentcal__tail {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border);
}
.rentcal__tail-label {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-faint);
}
.rentcal__chip {
  min-width: 30px; height: 26px; padding: 0 8px;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-surface); color: var(--color-muted);
  font: inherit; font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.rentcal__chip:hover { background: var(--color-primary-soft); border-color: var(--color-primary); color: var(--color-primary-dark); }
.rentcal__chip.is-picked { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-on-primary); }
.rentcal__hint { margin: 10px 0 0; font-size: 12px; line-height: 1.4; color: var(--color-muted); }

.rent-pay { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* «Другая сумма или дата» — необязательный сценарий, поэтому свёрнут в
   <details>: два поля в каждой строке забирали 40 % ширины таблицы (данные
   ужимались до 100 px) и читались как форма, которую забыли заполнить.
   Открытая деталь занимает свою строку в ячейке, а не толкает кнопку. */
/* Переключатель необязательных полей — значок «…» ростом с кнопку рядом, а не
   подпись. Подписью («другая сумма или дата») он не помещался в колонку и
   падал вторым этажом под кнопку: в каждой строке получалось по два яруса, и
   таблица читалась как наваленные друг на друга блоки. Значок держит строку
   одной высоты при любой ширине окна, а смысл несут title и aria-label. */
.rent-pay__more > summary {
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--color-faint); cursor: pointer;
  list-style: none; user-select: none;
  transition: background-color .14s var(--ease), color .14s var(--ease),
              border-color .14s var(--ease);
}
.rent-pay__more > summary svg { width: 16px; height: 16px; }
.rent-pay__more > summary::-webkit-details-marker { display: none; }
.rent-pay__more > summary:hover {
  background: var(--color-surface-soft); border-color: var(--color-border-strong);
  color: var(--color-primary);
}
.rent-pay__more > summary:focus-visible { outline: none; box-shadow: var(--ring); color: var(--color-primary); }
.rent-pay__more[open] > summary {
  background: var(--color-primary-soft); border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
/* Раскрытые поля занимают свою строку в ячейке — но только у той строки, где
   их открыли, а не у всех сразу. */
.rent-pay__more[open] { flex: 1 0 100%; }
.rent-pay__fields { display: grid; gap: 6px; }
.rent-pay__amount, .rent-pay__date { width: 100%; }
.rent-pay__fields { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 6px; }

.rent-card__actions { display: flex; gap: 8px; flex-wrap: wrap; }
.rent-card__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 18px; }
.rent-card__amount { display: flex; flex-direction: column; gap: 2px; }
.rent-card__value { font-size: 26px; font-weight: 650; letter-spacing: -0.01em; }
.rent-card__caption { font-size: 13px; color: var(--color-muted); }
.rent-card__note { margin: 0; font-size: 13px; color: var(--color-muted); }
.rent-card__empty {
  margin: 0;
  padding: 12px 14px;
  border-radius: var(--radius-md);
  background: var(--color-warning-soft);
  color: var(--color-warning);
  font-size: 13px;
}

.rent-history { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 4px; }
.rent-history__row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--color-surface-alt);
  font-size: 13px;
}
.rent-history__month { color: var(--color-muted); }
.rent-history__state { font-size: 12px; color: var(--color-muted); }
.rent-history__amount { font-variant-numeric: tabular-nums; }
.rent-history__row--paid .rent-history__state { color: var(--color-success); }
.rent-history__row--missed .rent-history__state { color: var(--color-danger); }

@media (max-width: 640px) {
  .rent-pay { width: 100%; }
}

/* Коммуналка (17.08.2026): вкладка «ПВЗ → Коммуналка» и блок в карточке точки.
   Наследует классы аренды (rent-table / rent-row / rent-history) — обе вкладки
   про деньги помещения и обязаны читаться одинаково; своё здесь только форма
   нового счёта и фильтры в шапке. */
.util-filters { flex-wrap: wrap; row-gap: 8px; column-gap: 12px; }

/* «Новый счёт» свёрнут, пока за месяц есть счета: форма из семи полей — самый
   крупный объект экрана, и раскрытой она забирала первый экран целиком,
   отодвигая под сгиб то, ради чего страницу открыли, — список того, что надо
   заплатить. Раскрыта по умолчанию только когда счетов нет: тогда завести
   первый и есть единственное дело страницы.
   display/padding перебиваем через .card.util-new — у .card свои flex и
   padding, и одноклассовый модификатор ниже по файлу читается как случайность. */
.card.util-new { display: block; padding: 0; }
.util-new__summary {
  display: flex; align-items: center; gap: 12px;
  padding: 18px 20px; border-radius: var(--radius-xl);
  cursor: pointer; list-style: none; user-select: none;
  transition: background-color .14s var(--ease);
}
.util-new__summary::-webkit-details-marker { display: none; }
.util-new__summary:hover { background: var(--color-surface-soft); }
.util-new__summary:focus-visible { outline: none; box-shadow: var(--ring); }
.util-new__plus {
  width: 32px; height: 32px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-md);
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  transition: transform .2s var(--ease);
}
.util-new__plus svg { width: 17px; height: 17px; }
/* Плюс поворачивается в крестик — то же действие в обратную сторону. */
.util-new[open] .util-new__plus { transform: rotate(45deg); }
.util-new__title { font-size: 15.5px; font-weight: 700; color: var(--color-heading); letter-spacing: -0.01em; }
.util-new__hint { display: block; margin-top: 2px; font-size: 13px; color: var(--color-muted); }
.util-form { padding: 0 20px 20px; }
@media (prefers-reduced-motion: reduce) {
  .util-new__plus { transition: none; }
}

.util-form__grid {
  display: grid;
  /* Шире 200 px — потому что при minmax(160px, …) на широком экране колонок
     набиралось восемь, а полей шесть: строка обрывалась на две трети, и справа
     висела дыра в 360 px. */
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px 14px;
}
/* На широком экране ширины полей осмысленные, а не поровну: адрес точки и
   название счёта длинные, сумма и даты короткие. */
@media (min-width: 1100px) {
  .util-form__grid { grid-template-columns: 1.5fr 1fr 1.6fr .8fr 1fr 1fr; }
}
.util-form__comment { grid-column: 1 / -1; }
.util-form__actions { display: flex; justify-content: flex-end; margin-top: 12px; }
/* Кнопки строки стоят в один ряд: перенос делал из ячейки два этажа и рвал
   ритм таблицы. На узком экране (таблица там и так уезжает в горизонтальный
   скролл) перенос возвращается. */
.util-actions { display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; }
.util-actions > form { flex: none; }
.util-remove__btn {
  width: 30px; height: 30px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--color-faint); cursor: pointer;
  transition: background-color .14s var(--ease), color .14s var(--ease),
              border-color .14s var(--ease);
}
.util-remove__btn svg { width: 15px; height: 15px; }
.util-remove__btn:hover {
  background: var(--color-danger-soft); border-color: var(--color-danger);
  color: var(--color-danger);
}
.util-remove__btn:focus-visible { outline: none; box-shadow: var(--ring); color: var(--color-danger); }
.util-points td { vertical-align: top; }
.util-points th:first-child { width: 30%; }

/* Вид счёта — значком. В столбце из «Электроэнергия / Вода и канализация /
   Интернет и связь» слова начинаются по-разному и читаются целиком; значок
   разбирается периферийным зрением, и список сканируется, а не вычитывается.
   Цветов три, не девять: тепло (свет, отопление), вода/связь, чистота и
   охрана — остальное нейтральное. Больше оттенков превратили бы столбец в
   радугу и перебили бы цвет состояния, который здесь единственный значимый. */
.util-kind { display: flex; align-items: center; gap: 10px; }
.util-kind__text { display: grid; gap: 2px; min-width: 0; }
.util-kind__text .cell-sub { display: block; }
.util-kind__icon {
  width: 28px; height: 28px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--color-surface-soft); color: var(--color-muted);
}
.util-kind__icon svg { width: 15px; height: 15px; }
.util-kind__icon--warm { background: var(--color-warning-soft); color: var(--color-warning); }
.util-kind__icon--cool { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.util-kind__icon--fresh { background: var(--color-success-soft); color: var(--color-success); }

/* Своя раскладка колонок: у коммуналки их шесть, и «Счёт» с названием длиннее
   всех остальных вместе взятых. Последней колонке отдано больше, чем кажется
   нужным: в ней две кнопки подряд («Снять отметку» и «Убрать»), и на узкой
   колонке они вставали друг под друга — строка вырастала вдвое. */
.util-col-point { width: 17%; }
.util-col-bill { width: 27%; }
.util-col-money { width: 10%; }
.util-col-due { width: 12%; }
.util-col-state { width: 13%; }
.util-col-act { width: 21%; }

/* «Повторить прошлый месяц» — в шапке списка справа и главным действием на
   пустом месяце; форма не должна ломать flex-раскладку шапки карточки. */
.util-repeat { display: flex; flex: none; }
.util-repeat .btn { gap: 7px; }
.util-repeat svg { width: 15px; height: 15px; flex: none; }
@media (max-width: 640px) {
  .util-form__grid { grid-template-columns: 1fr; }
  .util-new__summary { padding: 14px 16px; }
  .util-form { padding: 0 16px 16px; }
  .util-actions { flex-wrap: wrap; }
}

/* Подсказка «кто есть кто» у селекта роли (17.08.2026) — попап «Создать
   пользователя», форма учётки и кадровая форма. Карточка роли: чем человек
   будет заниматься, что увидит и чего нет, живые скриншоты его экранов из
   инструкций. Анимация — смысловая: смена роли = смена карточки (уходит вверх,
   приезжает снизу), скриншоты сами листаются кросс-фейдом с лёгким наездом и
   замирают на hover. Под prefers-reduced-motion всё статично. */
.qadd__dialog:has(.rguide) { width: min(900px, 100%); }
.rguide { position: relative; margin-top: 4px; }
.rguide.is-empty { display: none; }
.rguide__card {
  position: relative; overflow: hidden;
  padding: 16px 18px 14px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--color-primary-soft) 55%, transparent), transparent 46%),
    var(--color-surface);
  transform-origin: 50% 0;
}
.rguide__card.is-entering { animation: rguide-in .42s var(--ease, cubic-bezier(.2,.7,.2,1)) both; }
/* Смена роли: старая карточка исчезает сразу (наложение двух текстов читалось
   как «двоится»), контейнер плавно едет по высоте к новой карточке, новая
   входит снизу. */
.rguide.is-resizing { overflow: hidden; transition: height .28s var(--ease, cubic-bezier(.2,.7,.2,1)); }
@keyframes rguide-in {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  60% { opacity: 1; }
  to { opacity: 1; transform: none; }
}
/* Мягкое пятно света в углу — тот же приём, что hero-live: дышит, но контент
   не двигается. */
.rguide__glow {
  position: absolute; inset: -40% auto auto -20%; width: 60%; aspect-ratio: 1;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--color-primary) 22%, transparent), transparent 70%);
  filter: blur(18px); opacity: .55;
  animation: rguide-breathe 7s ease-in-out infinite;
}
@keyframes rguide-breathe {
  0%, 100% { transform: translate3d(0,0,0) scale(1); opacity: .45; }
  50% { transform: translate3d(6%, 4%, 0) scale(1.08); opacity: .7; }
}
.rguide__head { position: relative; display: grid; gap: 4px; }
.rguide__head .role-badge { justify-self: start; }
.rguide__title { margin: 4px 0 0; font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.rguide__who { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--color-muted); }
.rguide__scope {
  display: inline-flex; align-items: center; gap: 6px; justify-self: start; margin-top: 4px;
  padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 700;
  background: var(--color-surface-alt); color: var(--color-text);
}
.rguide__scope i { width: 7px; height: 7px; border-radius: 50%; background: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-soft); }
.rguide__body {
  position: relative; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  gap: 16px 20px; margin-top: 14px; align-items: start;
}
.rguide__facts { display: grid; gap: 10px; }
.rguide__list-title { display: block; font-size: 11.5px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 4px; }
.rguide__list ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.rguide__list li {
  padding: 4px 9px; border-radius: 999px; font-size: 12.5px; line-height: 1.3;
  background: var(--color-success-soft); color: var(--color-success);
}
.rguide__list--hidden li { background: var(--color-surface-alt); color: var(--color-muted); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--color-muted) 55%, transparent); }
.rguide__chat { margin: 2px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--color-muted); }
.rguide__chat b { color: var(--color-text); }
.rguide__money { margin: 0; display: inline-flex; align-items: center; gap: 6px; justify-self: start; font-size: 12.5px; font-weight: 700; padding: 4px 9px; border-radius: 8px; }
.rguide__money::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.rguide__money--yes { background: var(--color-warning-soft); color: var(--color-warning); }
.rguide__money--no { background: var(--color-success-soft); color: var(--color-success); }

.rguide__shots { display: grid; gap: 8px; }
.rguide__stage {
  position: relative; aspect-ratio: 16 / 10; overflow: hidden; cursor: pointer;
  border-radius: 10px; border: 1px solid var(--color-border);
  background: var(--color-surface-alt); box-shadow: var(--shadow-soft);
}
.rguide__shots--phone .rguide__stage { aspect-ratio: 9 / 12; width: min(100%, 240px); justify-self: center; }
.rguide__shot {
  position: absolute; inset: 0; margin: 0; opacity: 0;
  transition: opacity .55s ease;
}
.rguide__shot.is-shown { opacity: 1; z-index: 1; }
/* Пока кадр не пришёл — шиммер на сцене (картинки ленивые, JS греет их
   заранее, но первый показ иногда опережает сеть); пришёл — проявляется. */
.rguide__stage::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(100deg, transparent 20%, color-mix(in srgb, var(--color-surface) 65%, transparent) 50%, transparent 80%);
  background-size: 220% 100%;
  animation: rguide-shimmer 1.4s linear infinite;
}
@keyframes rguide-shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }
.rguide__img {
  display: block; width: 100%; height: 100%; object-fit: cover; object-position: top center;
  transform: scale(1); opacity: 0; transition: opacity .35s ease;
}
.rguide__img.is-loaded { opacity: 1; }
.rguide__shot.is-shown .rguide__img.is-loaded { animation: rguide-kenburns 6s ease-out both; }
@keyframes rguide-kenburns { from { transform: scale(1); } to { transform: scale(1.035); } }
.rguide__img--dark { display: none; }
[data-theme="dark"] .rguide__img--light { display: none; }
[data-theme="dark"] .rguide__img--dark { display: block; }
.rguide__caption { font-size: 12px; line-height: 1.4; color: var(--color-muted); min-height: 1.4em; text-align: center; }
.rguide__dots { display: flex; justify-content: center; gap: 6px; }
.rguide__dot {
  width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px; cursor: pointer;
  background: var(--color-border); transition: width .25s var(--ease, ease), background .25s;
}
.rguide__dot.is-active { width: 22px; background: var(--color-primary); }
.rguide__dot:focus-visible { outline: none; box-shadow: var(--ring); }
@media (max-width: 640px) {
  .rguide__body { grid-template-columns: 1fr; }
  .rguide__card { padding: 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .rguide__card.is-entering, .rguide__glow, .rguide__shot.is-shown .rguide__img,
  .rguide__stage::before { animation: none; }
  .rguide__shot, .rguide__img, .rguide.is-resizing { transition: none; }
}

/* Аренда в экономике сети (08.2026): третий сегмент полосы «куда ушли деньги»
   и своя KPI-карточка. Тон violet — единственный свободный из палитры полосы:
   success занят остатком, coral — штрафами. */
.eco-flow__seg--violet { background: var(--color-violet); }
.eco-kpi--violet::after { background: var(--color-violet); }
.eco-kpi--violet .eco-bar { background: var(--color-violet); }
.eco-kpi--muted::after { background: var(--color-border-strong); }
.eco-flow__note { margin: 6px 0 0; font-size: 12px; color: var(--color-faint); }

/* Карточка «Помещение» в форме ПВЗ (08.2026).
   Две зоны в ряд: слева выбор помещения, справа условия аренды. Обычная
   .form-grid тут не годится — у точки в собственности справа не остаётся
   ничего, и половина карточки стоит пустой; поэтому правая зона своя и
   целиком подменяется строкой-объяснением. */
.rent-grid {
  display: grid;
  grid-template-columns: minmax(190px, 260px) minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
/* Условия аренды: сумма и число оплаты в ряд, примечание — во всю ширину.
   auto-fit, а не жёсткие две колонки: карточка живёт и в широкой форме, и в
   узкой колонке двухколоночной раскладки. */
.rent-grid__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 16px;
  min-width: 0;
}
.rent-grid__note { grid-column: 1 / -1; }
/* Строка-объяснение занимает место полей аренды и выравнивается по первому
   ряду: под подписью «Помещение», а не под самим селектом. */
.rent-grid__owned {
  margin: 0;
  align-self: center;
  padding: 12px 14px;
  border-radius: var(--radius-md);
  background: var(--color-surface-alt);
  color: var(--color-muted);
  font-size: 13px;
  line-height: 1.45;
}
.rent-grid [hidden] { display: none; }

@media (max-width: 819px) {
  .rent-grid { grid-template-columns: 1fr; }
}

/* Карточки формы колонками, а не сеткой (08.2026).
   `.form-shell:has(> .card ~ .card)` раскладывает карточки CSS-гридом, а грид
   равняет строки по самой высокой карточке: под «Точкой» оставалась дыра
   ровно в ту разницу, на которую «Режим работы» длиннее из-за семи дней
   недели. Многоколоночная раскладка укладывает карточки встык — каждая своей
   высоты, дыр не остаётся вовсе.
   Обёртка нужна для того, чтобы sticky-панель «Сохранить» и общие ошибки
   остались снаружи: position: sticky внутри многоколоночного контекста не
   работает. */
.form-columns { display: flex; flex-direction: column; gap: 18px; }

@media (min-width: 1200px) {
  /* Обёртка отменяет `:has(> .card ~ .card)` (прямых карточек у формы больше
     нет), поэтому потолок ширины снимается здесь явно. */
  .form-shell:has(> .form-columns),
  .pvz-form:has(> .form-columns) { max-width: none; }
  .form-columns {
    display: block;
    columns: 2;
    column-gap: 18px;
  }
  /* Карточка не должна разрываться между колонками, а margin-bottom вместо
     gap — в многоколоночной раскладке gap между элементами не действует. */
  .form-columns > .card {
    break-inside: avoid;
    margin-bottom: 18px;
  }
  /* Широкая карточка (карта, таблица) занимает обе колонки. */
  .form-columns > .card--wide { column-span: all; }
}

/* Листок и заголовок срока — ссылки в раздел (08.2026). Кнопка справа ведёт
   туда, где срок закрывают; клик по самой карточке должен просто открывать
   раздел. Подчёркивания нет: листок — картинка, а не текстовая ссылка. */
a.repslot__leaf { text-decoration: none; cursor: pointer; }
a.repslot__leaf:hover { border-color: var(--color-border-strong); }
a.repslot__leaf:focus-visible,
a.repslot__title:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
a.repslot__title { text-decoration: none; color: var(--color-heading); justify-self: start; }
a.repslot__title:hover { color: var(--color-primary); }

/* =========================================================================
   Кадровая форма: плотность (08.2026)
   Форма из четырёх коротких карточек занимала семь тысяч пикселей. Причины
   были разные, и лечатся они тоже по-разному:
   * «Точки управляющего» — вся сеть плиткой — стояли в половину ширины
     колонкой в 5000px (карточка ушла в .card--wide, см. шаблон);
   * карточки лежали гридом, и ряд равнялся по самой высокой (шаблон,
     .form-columns);
   * поля стояли по два в ряд при ширине карточки под 900px.

   Своей прокрутки у списка точек нет намеренно. Ограничить ему высоту
   напрашивается (сеть в 70 ПВЗ — это восемь рядов плиток), но обрезанный
   список на macOS выглядит законченным: полосы прокрутки там всплывающие, и
   человек видит сорок точек из семидесяти, не зная об остальных. Для контрола,
   который раздаёт доступ, это хуже длинной страницы; докручивать до кнопок не
   надо — панель «Сохранить» липкая.
   ========================================================================= */

/* «Работа» — единственная карточка с тремя короткими полями в ряду (тип
   занятости и две даты). Общий шаг в 300px оставлял «Дату увольнения» одну в
   ряду с дырой во всю вторую половину карточки. Ставить три жёсткие колонки
   нельзя: в узком окне их не растащить — поэтому auto-fit, он же сам сложит
   поля в два ряда или в столбик. В «Основном» шаг остаётся общим: полей там
   два, и от третьей колонки появилась бы дыра уже у «Статуса». */
.emp-form .form-grid--work { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

@media (min-width: 1200px) {
  /* Плитки точек: во всю ширину их влезает шесть в ряд, и вся сеть видна
     сразу — ради этого карточка и делалась широкой. */
  .emp-form .scope-pick__grid { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
}

/* =========================================================================
   Меню «Ещё» в шапке страницы (08.2026)
   Редкие переходы и необратимые действия убраны с глаз в выпадающий список —
   на общем механизме [data-dropdown], как меню профиля в шапке.
   ========================================================================= */

.actions-menu__toggle { gap: 7px; }
.actions-menu__caret { transition: transform .18s var(--ease); }
.dropdown.is-open .actions-menu__caret { transform: rotate(180deg); }

/* right/width переопределяются классом поверх .dropdown-menu намеренно: на
   узком экране общее правило уводит меню на 52px вправо (это подгонка под
   колокольчик в шапке, у которой кнопка прижата к краю окна). Здесь кнопка
   стоит в ряду действий страницы, и тот же сдвиг вывесил бы меню за экран. */
.dropdown-menu.actions-menu {
  right: 0;
  width: min(268px, calc(calc(100vw * var(--vu)) - 32px));
}
.actions-menu a { white-space: nowrap; }
.actions-menu .badge { margin-left: auto; }
.actions-menu__sep { height: 1px; margin: 5px 4px; background: var(--color-border); }
/* Красный у «Уволить» и «Удалить навсегда» держится и под курсором: общее
   правило .dropdown-menu a:hover красит пункт в цвет ссылки, и необратимое
   действие подсвечивалось ровно как безобидный переход. */
.actions-menu .actions-menu__item--danger { color: var(--color-danger); }
.actions-menu .actions-menu__item--danger:hover {
  color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 12%, transparent);
}

/* =========================================================================
   Расходники — реестр заявок (08.2026)
   У раздела не было ни одного своего правила: он жил на базовых классах,
   рассчитанных на другие размеры, и оформление разъезжалось.
   ========================================================================= */

/* Плиток четыре, а базовая .metric-grid — жёсткие семь колонок: карточки
   занимали половину строки, справа висела пустая треть. auto-fit сам
   растягивает четыре плитки на всю ширину (как у нарушений и претензий). */
.metric-grid--supplies { grid-template-columns: repeat(auto-fit, minmax(162px, 1fr)); }

/* Три фильтра и кнопка. В общей auto-fit сетке «Применить» получала колонку
   в треть экрана и выглядела как пустое поле — ширину кнопке даёт содержимое
   (тот же приём, что в .filters-bar--cabinet). */
.filters-bar--supplies { grid-template-columns: repeat(3, minmax(160px, 1fr)) auto; }

/* Действия строки: поле ответа над кнопками, всё прижато вправо. До этого
   форма стояла на .inline-form (display: inline), а .input внутри —
   width: 100%, и комментарий растягивал колонку «Действия» на четверть
   таблицы, сбрасывая кнопки под себя вразнобой. В одну строку связку не
   поставить: поле плюс три кнопки просят под 400px — это половина таблицы. */
.supply-actions { display: grid; justify-items: end; gap: 8px; }
.supply-actions .input { width: 100%; max-width: 240px; }
.supply-actions__buttons { display: flex; gap: 6px; justify-content: flex-end; }
.supply-actions .btn { white-space: nowrap; }

/* Ниже 720px строку фильтров ведёт mobile.css (поля по двое, кнопка во всю
   ширину) — он грузится после app.css, поэтому своего правила для телефона
   здесь нет: оно всё равно не сработало бы. */
@media (max-width: 900px) {
  .filters-bar--supplies { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .metric-grid--supplies { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .supply-actions .input { max-width: none; }
}

/* =========================================================================
   Расчёт начислений (/payroll/calculate/)
   Страница-запуск: три шага слева, справка справа, оценка перед стартом.
   ========================================================================= */

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

/* Полотно: шаги шире справки. Общая .grid--detail делит экран почти поровну
   (1.05fr / .95fr), а справа лежит справочный материал — половина экрана ему
   не нужна, форме её не хватало. */
.pay-calc-layout {
  display: grid; gap: 18px; align-items: start;
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 1120px) {
  .pay-calc-layout { grid-template-columns: minmax(0, 1.55fr) minmax(280px, .72fr); }
}

.pay-calc { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.pay-step .card__head { align-items: center; gap: 12px; flex-wrap: wrap; }
.pay-step .card__title { display: inline-flex; align-items: center; }
.pay-step__num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 23px; height: 23px; margin-right: 9px; border-radius: 50%;
  background: var(--color-primary-soft); color: var(--color-primary-dark);
  font-size: 12.5px; font-weight: 800; flex: none;
}

.pay-presets { margin-top: 12px; }
.chip--action {
  border: 1px solid var(--color-border); background: var(--color-surface);
  cursor: pointer; font: inherit; font-size: 12px; font-weight: 600;
}
.chip--action:hover {
  border-color: color-mix(in srgb, var(--color-primary) 55%, var(--color-border));
  color: var(--color-primary-dark); background: var(--color-surface-alt);
}
.chip--action:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

/* Готовые границы периода: «Июль 2026» само по себе не показывало, какие
   именно смены уйдут в расчёт. */
.pay-resolved {
  margin: 14px 0 0; padding: 10px 13px; border-radius: var(--radius-md);
  background: var(--color-surface-soft); color: var(--color-muted);
  font-size: 13px; font-weight: 600; line-height: 1.45;
}
.pay-resolved b { color: var(--color-text); font-variant-numeric: tabular-nums; }
.pay-resolved.is-bad {
  background: var(--color-danger-soft); color: var(--color-danger);
}
.pay-resolved.is-bad b { color: inherit; }

.pay-scope { margin: 14px 0 0; font-size: 13px; color: var(--color-muted); line-height: 1.5; }
.pay-scope b { color: var(--color-text); font-weight: 700; }

.pay-option { display: flex; flex-direction: column; gap: 5px; }
.pay-option + .pay-option { margin-top: 14px; }
.pay-option .field__hint { margin-left: 26px; }

.pay-actions__hint { color: var(--color-muted); font-size: 12.5px; margin-left: auto; }
/* Форма ушла на сервер: соседние кнопки гасим, чтобы вторым кликом не начать
   ещё один пакет (сама нажатая уже держит .btn.is-loading). */
.pay-calc.is-submitting .pay-actions .btn { pointer-events: none; }
.pay-calc.is-submitting .pay-actions .btn:not(.is-loading) { opacity: .55; }

/* ---- Ориентировочный расчёт -------------------------------------------- */
.pay-estimate-slot { display: block; transition: opacity .15s var(--ease); }
.pay-estimate-slot.htmx-request { opacity: .45; pointer-events: none; }
/* Плашка ожидания живёт СНАРУЖИ подменяемого блока: внутри её затёр бы тот же
   ответ, ради которого она и показана. */
.pay-estimate-wait { display: none; }
.pay-estimate-slot.htmx-request + .pay-estimate-wait {
  display: flex; align-items: center; gap: 12px;
  color: var(--color-muted); font-size: 13.5px;
}

.pay-estimate { border-color: color-mix(in srgb, var(--color-primary) 30%, var(--color-border)); }
.pay-estimate__hero {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px;
  padding: 14px 16px; border-radius: var(--radius-lg);
  background: var(--color-primary-soft);
}
.pay-estimate__label { width: 100%; font-size: 12.5px; font-weight: 700; color: var(--color-muted); }
.pay-estimate__sum {
  font-size: 31px; font-weight: 800; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.pay-estimate__meta { color: var(--color-muted); font-size: 13px; font-weight: 600; }

/* Сумма показана цепочкой, а не четырьмя равноправными числами: видно, из
   чего она собралась и на чём потеряла. */
.pay-sum { list-style: none; margin: 16px 0 0; padding: 0; }
.pay-sum li {
  display: grid; grid-template-columns: 16px minmax(0, 1fr) auto;
  align-items: baseline; gap: 10px; padding: 9px 2px; font-size: 14px;
  border-bottom: 1px dashed var(--color-border);
}
.pay-sum li:last-child { border-bottom: 0; }
.pay-sum__op { color: var(--color-muted); font-weight: 800; text-align: center; }
.pay-sum__val { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.pay-sum .is-plus .pay-sum__op, .pay-sum .is-plus .pay-sum__val { color: var(--color-success); }
.pay-sum .is-minus .pay-sum__op, .pay-sum .is-minus .pay-sum__val { color: var(--color-danger); }
.pay-sum .is-total {
  border-bottom: 0; border-top: 1px solid var(--color-border);
  margin-top: 3px; padding-top: 11px; font-weight: 700;
}
.pay-sum .is-total:last-child .pay-sum__val { font-size: 16px; }

.pay-estimate__problems { margin: 8px 0 0; padding-left: 18px; font-size: 13px; line-height: 1.55; }
.pay-estimate__problems li + li { margin-top: 5px; }
.pay-estimate__count { white-space: nowrap; opacity: .8; font-size: 12px; }
.pay-estimate__more { margin: 8px 0 0; font-size: 12.5px; opacity: .85; line-height: 1.5; }
.pay-estimate__more .link { color: inherit; text-decoration: underline; }
.pay-estimate__note { margin: 14px 0 0; font-size: 12.5px; color: var(--color-muted); line-height: 1.5; }

.pay-batch-err { color: var(--color-danger); font-weight: 700; }
.pay-batch-err::before { content: "· "; color: var(--color-muted); font-weight: 400; }

@media (max-width: 640px) {
  .pay-estimate__sum { font-size: 26px; }
  .pay-actions__hint { margin-left: 0; width: 100%; }
}

/* ==== Док свёрнутых чатов (24.08.2026) =====================================
   «Свернуть в уголок»: переписка живёт плавающим окном у правого нижнего
   угла на любой странице и перетаскивается за шапку (позицию пишет JS в
   left/top; по умолчанию — угол из right/bottom ниже). Всё видимое здесь
   существует только на широком экране — том же пороге 1100px, что у чатов в
   два окна (см. initChatDock): на телефоне переписка и так занимает весь
   экран. Окно прижато к углу и на время показа накрывает кнопку обращений
   (.fb-fab), как любой чат-виджет. z-index 110 — над дропдаунами (60) и
   поповерами (80), под модалками (120) и тостами (200). Промежуточных
   чипов-таблеток нет (сняты 24.08.2026): ответ chat:dock_chips приезжает в
   скрытый контейнер данных и как UI не рисуется. */
.chat-dock[hidden] { display: none; }
/* Кнопка «Свернуть в уголок» в шапке переписки: показывает её только живой
   док (html.has-chat-dock ставит initChatDock) — без JS окно нечем открыть. */
.chat-dock-min { display: none; }
/* Экран сузили ПОСЛЕ загрузки: правила ниже живут в медиа >=1100px, и без
   этой страховки уже открытое окно теряло бы fixed-позицию и падало
   неоформленным потоком в конец страницы. */
@media (max-width: 1099px) {
  .chat-dock { display: none; }
}

@media (min-width: 1100px) {
  html.has-chat-dock .chat-dock-min { display: inline-flex; padding-inline: 9px; }
  html.has-chat-dock .chat-dock-min svg { width: 16px; height: 16px; }

  .chat-dock__win {
    position: fixed; right: 22px; bottom: 22px; z-index: 110;
    width: 400px;
    /* Не выше окна браузера: vh под html { zoom } умножается на --vu. */
    height: min(600px, calc(calc(100dvh * var(--vu)) - var(--topbar-h) - 30px));
    display: flex; flex-direction: column;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-pop);
    overflow: hidden;
    /* Четыре ручки движения: их крутит пружинка из initChatDock, собирает
       transform — CSS (см. .is-moving ниже). */
    --dock-dx: 0px; --dock-dy: 0px; --dock-tilt: 0deg; --dock-lift: 1;
    transition: box-shadow .22s var(--ease);
    /* Появление: окно поднимается из угла, а не возникает встык. Без
       fill-mode — доиграв, анимация отпускает transform совсем. */
    animation: chat-dock-pop .28s var(--ease-out);
  }
  .chat-dock__win[hidden] { display: none; }
  /* transform живёт ТОЛЬКО на время жеста: постоянный сделал бы окно
     containing block'ом для fixed-потомков (меню сообщения, лайтбокс), а
     пока окно едет, ничего этого открытым быть не может. */
  .chat-dock__win.is-moving {
    transform: translate3d(var(--dock-dx), var(--dock-dy), 0)
               rotate(var(--dock-tilt)) scale(var(--dock-lift));
    will-change: transform;
  }
  /* Взяли в руку: окно приподнимается тенью, как карточка над столом. */
  .chat-dock__win.is-dragging {
    box-shadow: 0 34px 74px -22px rgba(12, 14, 18, 0.38),
                0 12px 26px -14px rgba(12, 14, 18, 0.24);
  }
  html[data-theme="dark"] .chat-dock__win.is-dragging {
    box-shadow: 0 34px 84px rgba(0, 0, 0, 0.78), 0 12px 28px rgba(0, 0, 0, 0.62);
  }
  .chat-dock__win.is-dragging .chat-dock__bar { cursor: grabbing; }
  @keyframes chat-dock-pop {
    from { opacity: 0; transform: translate3d(0, 16px, 0) scale(0.965); }
    to { opacity: 1; transform: none; }
  }
  /* Просили меньше движения — окно просто появляется и просто едет за
     курсором (пружинку и наклон глушит сам initChatDock). */
  @media (prefers-reduced-motion: reduce) {
    .chat-dock__win { animation: none; transition: none; }
  }
  /* Шапка окна: аватар — имя — кнопки. Лёгкая подложка отделяет «раму» окна
     от переписки, как в мессенджерах (26.08.2026). */
  .chat-dock__bar {
    display: flex; align-items: center; gap: 9px; flex: 0 0 auto;
    padding: 8px 9px 8px 10px;
    background: var(--color-surface-soft);
    border-bottom: 1px solid var(--color-border);
    /* За шапку окно таскают: без user-select протяжка выделяла бы заголовок. */
    cursor: grab; user-select: none; touch-action: none;
  }
  .chat-dock__bar:active { cursor: grabbing; }
  /* Аватар клонирует в этот слот initChatDock; пока чат не приехал, слот
     пустой и места не занимает. */
  .chat-dock__ava { display: flex; flex: 0 0 auto; }
  .chat-dock__ava:empty { display: none; }
  .chat-dock__bar .chat-dock__avatar {
    width: 30px; height: 30px; font-size: 13px;
    /* Кружок на подложке шапки: тонкая кромка цвета окна отделяет фото. */
    box-shadow: 0 0 0 1px var(--color-surface);
  }
  .chat-dock__who { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
  .chat-dock__title {
    flex: 0 1 auto; min-width: 0;
    font-size: 14px; font-weight: 700; color: var(--color-heading);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* Глазок «тихого просмотра» — тот же смысл, что в списке чатов: открытая
     этим ключом переписка не ставит отметок о прочтении. */
  .chat-dock__eye { width: 14px; height: 14px; flex: 0 0 auto; color: var(--color-muted); }
  /* Правило display выше перебило бы [hidden] — гасим явно (без чата глазка нет). */
  .chat-dock__eye[hidden] { display: none; }
  .chat-dock__tools { display: flex; gap: 4px; flex: 0 0 auto; }
  .chat-dock__btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0;
    border: 0; border-radius: var(--radius-sm);
    background: transparent; color: var(--color-muted); cursor: pointer;
  }
  .chat-dock__btn:hover { background: var(--color-surface); color: var(--color-heading); }
  .chat-dock__btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
  /* Закрыть — единственное необратимое действие шапки, и под курсором оно
     называет себя цветом. */
  .chat-dock__btn[data-dock-remove]:hover { color: var(--color-danger); }
  .chat-dock__btn svg { width: 15px; height: 15px; }

  /* Перевоз pane-фрагмента в окно: те же сбросы, что у правой панели раздела
     чатов (.chat-split__view) — карточка без рамки и тени, лента с
     min-height: 0 (базовые 300px выталкивали бы композер за край окна). */
  .chat-dock__body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  .chat-dock__body:empty { align-items: center; justify-content: center; }
  .chat-dock__body:empty::before {
    content: "Открываем переписку…"; color: var(--color-muted); font-size: 13px;
  }
  .chat-dock__body .chat-pane__inner {
    display: flex; flex-direction: column; min-height: 0; height: 100%;
  }
  /* Шапка фрагмента в окне спрятана: имя уже в баре окна, а её кнопки
     («Карточка сотрудника», «Свернуть в уголок») мини-окну не нужны —
     заодно не задваивается и сама кнопка сворачивания. */
  .chat-dock__body .chat-pane__head { display: none; }
  /* Явные тёмная/светлая вариации — чтобы перебить html[data-theme="dark"]
     .card (глянец и рамка карточки), как это делает .chat-split__view. */
  .chat-dock__body .chat-card,
  html[data-theme="dark"] .chat-dock__body .chat-card,
  html[data-theme="light"] .chat-dock__body .chat-card {
    flex: 1 1 auto; min-height: 0; border: 0; border-radius: 0;
    box-shadow: none; background: transparent; padding: 10px 12px 12px;
  }
  .chat-dock__body .chat-box { border: 0; min-height: 0; padding: 4px 2px 10px 8px; }
  .chat-dock__body .chat-observe-note { padding: 8px 12px; font-size: 12.5px; }
}

/* =========================================================================
   Единый календарь выбора даты (25.08.2026)

   Заказчик: «может везде сделать такой же календарь, как у нас на сводной
   панели?». Каркас поэтому общий с .periodcal — тот же поповер, та же ширина,
   та же сетка; разница в том, что дни здесь кнопки, а снизу есть «Сегодня» и
   «Очистить». Поведение — в initDatePicker (app.js).
   ========================================================================= */

/* Значок календаря в поле остаётся нативным (он уже красится по color-scheme
   и не требует своей иконки), но перестаёт быть кнопкой: клик по нему
   проваливается в само поле, а поле открывает наш календарь. Без этого
   поверх нашего поповера вылезал бы системный. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="month"]::-webkit-calendar-picker-indicator { pointer-events: none; }
input[type="date"].input,
input[type="date"].input-sm,
input[type="date"].form-control,
input[type="month"].input,
input[type="month"].input-sm,
input[type="month"].form-control { cursor: pointer; }

/* Поле месяца открывает тот же поповер, но сразу сеткой месяцев: дней в нём
   нет вовсе, поэтому заголовок показывает год и перестаёт быть переключателем,
   а «Сегодня» в подвале становится «Этот месяц» (initDatePicker). */
.datepick--month .datepick__months { padding: 2px 0 4px; }
/* Заголовок там выключен (переключать нечего) — он не должен подсвечиваться
   под курсором и обещать нажатие, которого не будет. */
.datepick__title:disabled { cursor: default; }
.datepick__title:disabled:hover { background: none; color: inherit; }

/* Выше попапов (.cell-modal и прочие живут на 120–150): поля даты есть и
   внутри них — «повторять до» в назначении смен, даты в карточках. */
.datepick { z-index: 160; transform-origin: top left; }
/* Ловушка «display перебивает [hidden]»: у .periodcal__week/__days есть свой
   display: grid, и атрибут hidden их не убирал — переключение «дни ↔ месяцы»
   рисовало обе сетки разом, одну под другой. */
.datepick [hidden] { display: none; }

.datepick__title {
  border: 0; background: none; font: inherit; cursor: pointer;
  font-size: 14px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--color-heading); padding: 4px 10px; border-radius: var(--radius-sm);
  transition: background-color .12s var(--ease), color .12s var(--ease);
}
.datepick__title:hover { background: var(--color-surface-soft); color: var(--color-primary-dark); }
.datepick__title:focus-visible { outline: none; box-shadow: var(--ring); }

/* День-кнопка. Рамки в спокойном состоянии нет намеренно: 42 обведённые
   клетки читаются как таблица, а не как календарь (см. сводную панель). */
.datepick__day {
  cursor: pointer; font: inherit; background: none;
  font-size: 13px; font-weight: 700; color: var(--color-text);
  font-variant-numeric: tabular-nums;
  transition: background-color .12s var(--ease), border-color .12s var(--ease),
              color .12s var(--ease);
}
.datepick__day:hover:not(:disabled) {
  background: var(--color-primary-soft); border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
.datepick__day:focus-visible { outline: none; box-shadow: var(--ring); }
.datepick__day:disabled { cursor: not-allowed; opacity: .3; }

/* «Сегодня» — обводка, «выбрано» — заливка: та же развязка, что в календаре
   срока аренды. В выбирающем календаре заливка занята выбором, и одинаковый
   синий квадрат путал бы два разных ответа. */
.datepick .periodcal__day.is-today {
  background: none; border-color: var(--color-primary);
  color: var(--color-primary-dark); font-weight: 800;
}
.datepick .periodcal__day.is-picked {
  background: var(--color-primary); border-color: var(--color-primary);
  color: var(--color-on-primary); font-weight: 800;
}
.datepick .periodcal__day.is-picked:hover {
  background: var(--color-primary-strong); color: var(--color-on-primary);
}

/* Выбор месяца: клик по заголовку. Нужен полям вроде «Дата приёма», где до
   цели двадцать нажатий стрелкой. Стрелки в этом режиме листают годы. */
.datepick__months { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.datepick__mon {
  height: 44px; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; cursor: pointer; font: inherit;
  font-size: 13px; font-weight: 700; color: var(--color-text);
  transition: background-color .12s var(--ease), border-color .12s var(--ease),
              color .12s var(--ease);
}
.datepick__mon:hover {
  background: var(--color-primary-soft); border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
.datepick__mon:focus-visible { outline: none; box-shadow: var(--ring); }
.datepick__mon.is-today { border-color: var(--color-primary); color: var(--color-primary-dark); font-weight: 800; }
.datepick__mon.is-picked {
  background: var(--color-primary); border-color: var(--color-primary);
  color: var(--color-on-primary); font-weight: 800;
}

.datepick__foot {
  display: flex; gap: 8px; margin-top: 11px; padding-top: 10px;
  border-top: 1px solid var(--color-border);
}
.datepick__act {
  flex: 1 1 0; min-height: 32px; padding: 0 10px;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-surface); color: var(--color-muted);
  font: inherit; font-size: 12.5px; font-weight: 700; cursor: pointer;
  transition: background-color .12s var(--ease), border-color .12s var(--ease),
              color .12s var(--ease);
}
.datepick__act:hover {
  background: var(--color-primary-soft); border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
.datepick__act:focus-visible { outline: none; box-shadow: var(--ring); }
.datepick__act--clear:hover {
  background: var(--color-danger-soft);
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border));
  color: var(--color-danger);
}

/* На телефоне календарь — главное на экране: клетки крупнее пальца, а ширину
   держит общий потолок .periodcal (не вылезти за экран). */
@media (max-width: 560px) {
  .datepick__main { width: 320px; }
  .datepick .periodcal__day { height: 42px; }
  .datepick__day { font-size: 14px; }
  .datepick__act { min-height: 38px; font-size: 13px; }
}

/* =========================================================================
   Выбор времени (.timepick) — 27.08.2026, переделан 02.09.2026
   -------------------------------------------------------------------------
   Родной поповер времени в Chrome — два барабана во весь экран: открытый над
   списком «Кроме» он накрывал сами точки, ради которых его и открывали.
   Вместо него своя панель (initTimePicker в app.js).

   Что было и почему не годилось (заказчик 02.09.2026: «это просто ужасно
   выглядит, давай проще — как на десктопе»). Первая версия давала три сетки
   подряд — часы 12×2, минуты пятиминутками и ряд «Точнее +1…+4», — и 09:07 в
   ней набиралось СЛОЖЕНИЕМ: «05» плюс «+2». Оба слагаемых закрывали панель,
   поэтому за одно открытие 09:07 было недостижимо в принципе, а по дороге в
   поле реально уходило 09:05 — с input и change, которые видели htmx и живые
   счётчики. Крупное табло сверху существовало только затем, чтобы объяснить
   «+2». Двадцать четыре клетки через 2 px читались лентой цифр
   («000102…2223»), три подписи-капса подряд — серой штриховкой. Синяя плашка
   под «09:03» на скриншоте заказчика — это ::selection: user-select у панели
   не было вовсе.

   Что стало. Две строки и один объект: тихая строка с именем поля и командами
   («Сейчас», «Очистить») — и крупное «09 : 07» в колодце, который выглядит и
   ведёт себя как .input (в него ПЕЧАТАЮТ, стрелки у правого края шагают ±1).
   Строки готовых значений (09:00 · 09:30 · 10:00) нет намеренно — заказчик
   02.09.2026: «убери эти доп цифры»; время сюда набирают, а не выбирают из
   чужой догадки. Одна подпись-капс на всю панель вместо трёх. Тач-цель — сама
   половина значения (72×44, на телефоне 80×50) вместо клетки 24 px. Высота
   98 px против 278: панель снова держит обещание «ничего не заслонять» — в
   подвале попапа «Кроме» она накрывает одну строку списка вместо шести.

   Силуэт в тёмной теме держат три вещи, потому что там --shadow-pop чёрный по
   почти чёрному: рамка --color-border-strong, внутренняя кромка от
   --color-heading (в тёмной она светлая, в светлой тёмная — одно объявление
   на обе темы, правил html[data-theme] у компонента нет) и колодец значения
   на ступень глубже поверхности.

   Поле остаётся <input type="time"> со значением «HH:MM»: форма, сервер и
   htmx о надстройке не знают. Опт-аут — data-timepick="off" (приёмка считает
   секунды, округление до минуты сдвигало бы сам факт).
   ========================================================================= */

/* Значок часов в поле остаётся нативным (он красится по color-scheme), но
   перестаёт быть кнопкой: иначе поверх нашего поповера вылезал бы системный.
   Опт-аут общий с JS — data-timepick="off" (приёмка считает секунды). */
input[type="time"]:not([data-timepick="off"])::-webkit-calendar-picker-indicator {
  pointer-events: none;
}
input[type="time"]:not([data-timepick="off"]) { cursor: pointer; }

/* Пока панель открыта, поле-источник подсвечено: в форме смены «Начало» и
   «Окончание» стоят рядом, и по одному числу не понять, какое из них правишь.
   Связью это объясняется дешевле и честнее, чем подписью. */
input[type="time"][data-timepick-open] {
  border-color: var(--color-primary); box-shadow: var(--ring-soft);
}

/* Выше попапов (.cell-modal и прочие живут на 120–150) — как и у календаря:
   поля времени есть и внутри них (назначение смены, список «Кроме»). */
.timepick {
  position: fixed; z-index: 160; transform-origin: top left;
  display: flex; flex-direction: column; gap: 8px;
  /* Ширина + потолок в процентах, как у .periodcal__main: min() с 100vw
     схлопывал поповер в столбик там, где ширина окна ещё не известна. */
  width: 320px; max-width: calc(100% - 24px); padding: 12px;
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-xl);
  background: var(--color-surface);
  box-shadow: var(--shadow-pop),
              inset 0 0 0 1px color-mix(in srgb, var(--color-heading) 6%, transparent);
  /* Без этого ::selection красит крупные цифры синим — это и есть «плашка» со
     скриншота заказчика, — а на телефоне долгий тап поднимает системные
     маркеры выделения и лупу поверх панели. Полю ввода выделение возвращаем
     точечно, ниже. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  opacity: 0; transform: translateY(-8px) scale(.96);
  transition: opacity .18s var(--ease-out), transform .26s var(--ease-out);
}
.timepick.is-open { opacity: 1; transform: none; }
/* Откинутая вверх или вбок панель обязана раскрываться ОТ поля: иначе анимация
   показывает, что она приехала из чужого угла. */
.timepick--up { transform-origin: bottom left; transform: translateY(8px) scale(.96); }
.timepick--side { transform-origin: center; transform: scale(.96); }
/* Пока панель тает (hidden ставится с задержкой, иначе уход обрывается на
   первом кадре), клики сквозь неё проваливаться не должны. */
.timepick:not(.is-open) { pointer-events: none; }
/* [hidden] после display — у поповера свой flex, и атрибут его не убирал бы
   (та же ловушка, что у .datepick). */
.timepick[hidden] { display: none; }
/* Та же ловушка у детей: у шапки и у чипов свой display. */
.timepick [hidden] { display: none; }

/* Шапка: имя поля слева, команды справа — одной строкой. Отдельного подвала
   с кнопками нет: он стоил панели 46 px и разделительной черты, а «Сейчас»
   и «Очистить» — редкие команды, которым хватает тихой строки наверху. */
.timepick__top {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 18px;
}
/* Имя поля. Единственный капс на всю панель. */
.timepick__head {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--color-faint);
}
.timepick__acts { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
/* Команда, а не кнопка: без заливки и рамки. Заливку в панели имеет право
   носить только само значение. */
.timepick__act {
  padding: 2px 6px; border: 0; border-radius: var(--radius-sm);
  background: none; color: var(--color-muted);
  font: inherit; font-size: 11.5px; font-weight: 700; cursor: pointer;
  transition: background-color .12s var(--ease), color .12s var(--ease);
}
.timepick__act:hover { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.timepick__act:focus-visible { outline: none; box-shadow: var(--ring); }
.timepick__act--clear:hover { background: var(--color-danger-soft); color: var(--color-danger); }

/* Колодец значения: выглядит как .input и работает как .input — это не табло,
   а место, куда набирают. Он же даёт панели глубину, которой в тёмной теме не
   даёт ни тень, ни рамка.

   padding-right под обойму стрелок — не украшение: стрелки лежат в колодце
   абсолютом, и без поправки цифры центрировались бы по всей его ширине, то
   есть заезжали бы стрелкам под ноги, а слева зияла бы дыра. */
.timepick__val {
  position: relative; height: 52px; padding: 0 34px 0 4px;
  display: flex; align-items: center; justify-content: center;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-md);
  cursor: text;
  transition: background-color .12s var(--ease), border-color .12s var(--ease),
              box-shadow .12s var(--ease);
}
.timepick__val:focus-within {
  background: var(--color-surface); border-color: var(--color-primary);
  box-shadow: var(--ring-soft);
}

.timepick__seg {
  width: 72px; height: 44px; padding: 0; margin: 0; border: 0; background: none;
  font: inherit; font-size: 32px; font-weight: 800; letter-spacing: -0.02em;
  text-align: center; color: var(--color-heading);
  /* На честный tnum шрифта не полагаемся: половины центрируются в клетке
     фиксированной ширины, поэтому «11:44» и «08:39» стоят на месте. */
  font-variant-numeric: tabular-nums; border-radius: 0;
  caret-color: var(--color-primary);
  /* Панели выделение запрещено, полю ввода оно нужно: набранная половина
     выделяется, и следующая цифра её заменяет, а не дописывает. */
  user-select: text; -webkit-user-select: text;
  transition: color .12s var(--ease), box-shadow .12s var(--ease);
}
/* Выделение внутри половины прячем: подчёркивание уже показывает, куда
   печатают, а вторая, более тёмная полоса поверх выглядела браком — ровно та
   «синяя плашка», с которой началась переделка. Само выделение остаётся
   рабочим: следующая цифра заменяет половину. */
/* Цвет здесь обязателен, а не «inherit»: глобальное правило ::selection
   (одно на весь продукт) красит выделенный текст в --color-text, и половина,
   которую мы только что покрасили синим, снова становилась белой. Проверено
   замером: computed color половины — синий, а на экране она белая. */
.timepick__seg::selection { background: transparent; color: var(--color-primary); }
/* Активная половина — цветом и подчёркиванием, а не заливкой: заливка на одной
   из двух половин делала их разными объектами, и колодец переставал читаться
   как одно значение. */
/* box-shadow: none обязателен: глобальное правило input:focus-visible (app.css
   ~562) вешает кольцо на КАЖДЫЙ input, и вокруг половины появлялась вторая
   рамка внутри колодца, у которого кольцо уже есть на :focus-within. */
.timepick__seg:focus { outline: none; box-shadow: none; color: var(--color-primary); }
/* Цифры перекатываются в сторону шага: без движения удержание стрелки
   выглядит как мигание числа, и непонятно, в какую сторону едет время.
   Половина не участвует в раскладке колодца (фиксированная ширина), поэтому
   transform на ней ничего не двигает вокруг. */
@keyframes timepick-up {
  from { transform: translateY(9px); opacity: .2; }
  to { transform: none; opacity: 1; }
}
@keyframes timepick-down {
  from { transform: translateY(-9px); opacity: .2; }
  to { transform: none; opacity: 1; }
}
/* «Сейчас» меняет обе половины разом — это не шаг, поэтому не перекат, а
   короткий наплыв. */
@keyframes timepick-pop {
  from { transform: scale(.86); opacity: .35; }
  to { transform: none; opacity: 1; }
}
.timepick__seg.is-up { animation: timepick-up .2s var(--ease-out); }
.timepick__seg.is-down { animation: timepick-down .2s var(--ease-out); }
.timepick__seg.is-pop { animation: timepick-pop .22s var(--ease-out); }

/* Бегунок активной половины. Он ЕЗДИТ между часами и минутами — одно движение
   вместо двух вспышек. Пока половина не выбрана (открыли пальцем, клавиатуру
   не поднимали), бегунка нет вовсе.
   Переходы объявлены только у .is-on: иначе первое появление панели тянуло бы
   бегунок из положения, оставшегося с прошлого открытия. */
.timepick__bar {
  position: absolute; left: 0; bottom: 5px; height: 3px; width: 0;
  border-radius: 3px; background: var(--color-primary);
  opacity: 0; pointer-events: none;
  transition: opacity .12s var(--ease-out);
}
.timepick__bar.is-on {
  opacity: 1;
  transition: transform .24s var(--ease-out), width .24s var(--ease-out),
              opacity .12s var(--ease-out);
}
.timepick__colon {
  width: 14px; text-align: center; transform: translateY(-2px);
  font-size: 26px; font-weight: 700; color: var(--color-faint);
}

/* Стрелки шага живут ВНУТРИ колодца и без своей рамки: отдельная обойма справа
   читалась бы вторым контролом и старила бы панель. Они — запасной путь для
   мыши; с клавиатуры то же делают ↑/↓, мышью — колесо над половиной. */
.timepick__spin {
  position: absolute; right: 4px; top: 4px; bottom: 4px; width: 26px;
  display: grid; grid-template-rows: 1fr 1fr; gap: 2px;
}
.timepick__step {
  display: grid; place-items: center; padding: 0; border: 0;
  background: none; color: var(--color-faint);
  cursor: pointer; touch-action: manipulation;
  border-radius: var(--radius-sm);
  transition: background-color .12s var(--ease), color .12s var(--ease),
              transform .12s var(--ease-out);
}
.timepick__step:hover { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.timepick__step:active,
.timepick__step.is-held {
  background: var(--color-primary); color: var(--color-on-primary);
  transform: scale(.86);
}
.timepick__step:focus-visible { outline: none; box-shadow: var(--ring); }
.timepick__step svg { width: 13px; height: 13px; }

/* Палец крупнее курсора: половина значения 84×46 — целая тач-цель, чипы 38.
   Стрелки остаются вспомогательными: главный мобильный путь — чип или
   цифровая клавиатура по тапу в цифры. */
@media (max-width: 560px) {
  .timepick { max-width: calc(100% - 16px); }
  .timepick__val { height: 60px; padding-right: 42px; }
  .timepick__seg { width: 80px; height: 50px; font-size: 34px; }
  .timepick__colon { width: 16px; font-size: 28px; }
  .timepick__spin { width: 34px; }
  .timepick__act { padding: 6px 8px; font-size: 12.5px; }
}

@media (prefers-reduced-motion: reduce) {
  .timepick, .timepick__bar, .timepick__bar.is-on, .timepick__step { transition: none; }
  .timepick__seg.is-up,
  .timepick__seg.is-down,
  .timepick__seg.is-pop { animation: none; }
}

/* =========================================================================
   Подсказка «i» (26.08.2026)
   -------------------------------------------------------------------------
   Хаб настроек нёс под каждым из двенадцати заголовков абзац объяснения —
   стена текста, которую читают ровно один раз. Объяснение осталось, но
   спряталось за круглым значком в правом нижнем углу карточки: наведение
   мышью или нажатие пальцем — и текст выходит подсказкой.

   Узел подсказки один на страницу и лежит в <body> (position: fixed): у
   карточек свои тени, скруглённые углы и будущие overflow — ::after у значка
   резался бы о них, а .card-grid к тому же прокручивается вместе со страницей.
   Координаты ставит JS (initInfoTips) — они экранные, поэтому делятся на
   uiScale(), см. html { zoom }.
   Слой 95 — выше шапки (34) и рельса (40), ниже модалок.
   ========================================================================= */
.infodot {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--color-border); border-radius: 50%;
  background: var(--color-surface); color: var(--color-faint);
  font: inherit; font-size: 13px; font-weight: 800; line-height: 1;
  font-style: italic; cursor: help;
  transition: background-color .14s var(--ease), border-color .14s var(--ease),
              color .14s var(--ease);
}
.infodot:hover,
.infodot[aria-expanded="true"] {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  color: var(--color-primary-dark);
}
.infodot:focus-visible { outline: none; box-shadow: var(--ring); }
/* Палец в кружок 26 px не попадает: невидимый запас вокруг значка доводит
   область нажатия до 42 px, ничего не сдвигая на экране. */
.infodot::after { content: ""; position: absolute; inset: -8px; border-radius: 50%; }

.infotip {
  position: fixed; left: 0; top: 0; z-index: 95;
  width: max-content; max-width: min(320px, calc(100vw - 24px));
  padding: 11px 13px;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-surface); color: var(--color-text);
  font-size: 13px; line-height: 1.45;
  box-shadow: var(--shadow-pop);
  pointer-events: none;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity .14s var(--ease), transform .14s var(--ease),
              visibility 0s linear .14s;
}
/* Подсказка над значком приезжает снизу вверх — иначе движение спорит с
   направлением, в котором она открылась. Ниже .infotip.is-open (0,2,0) этого
   правила (0,1,0), так что открытое состояние всё равно за ним. */
.infotip--up { transform: translateY(4px); }
.infotip.is-open {
  opacity: 1; visibility: visible; transform: none; transition-delay: 0s;
}

/* Уголок ходит за значком: подсказку прижало к краю экрана — она не отрывается
   от того, что объясняет. Смещение кладёт JS в --tip-arrow. */
.infotip__arrow {
  position: absolute; left: var(--tip-arrow, 22px); top: -6px;
  width: 11px; height: 11px; margin-left: -6px;
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  border-left: 1px solid var(--color-border);
  border-top-left-radius: 3px;
  transform: rotate(45deg);
}
.infotip--up .infotip__arrow {
  top: auto; bottom: -6px;
  border-top: 0; border-left: 0;
  border-right: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
  border-radius: 0 0 3px 0;
}

/* ---- Карточка настроек: ссылка + значок рядом --------------------------
   Кнопка не может лежать внутри <a>, поэтому она сосед, положенный поверх
   карточки. Подъём при наведении переезжает на обёртку: оставь его на ссылке —
   и значок при наведении оставался бы на месте, пока карточка уходит вверх.
   ------------------------------------------------------------------------ */
.card-grid--compact { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }

.set-card {
  position: relative; display: flex;
  transition: transform 0.18s var(--ease);
}
.set-card > .report-card {
  flex: 1 1 auto; min-width: 0; min-height: 104px;
}
/* Значок стоит в одной строке с «Открыть →», а не рядом с заголовком, поэтому
   колодец нужен только подвалу: заголовку оставляем всю ширину карточки. */
.set-card .report-card__foot { padding-right: 34px; }
.set-card:hover { transform: translateY(-3px); }
.set-card:hover > .report-card,
.set-card:focus-within > .report-card {
  transform: none;
  box-shadow: var(--shadow-pop);
  border-color: var(--color-primary);
}
.set-card > .report-card:hover { transform: none; }
.set-card .infodot { position: absolute; right: 14px; bottom: 15px; z-index: 2; }

@media (prefers-reduced-motion: reduce) {
  .set-card, .set-card:hover { transform: none; transition: none; }
}

/* === Биржа смен (09.2026) ==================================================
   Карточки свободных смен в кабинете (телефон — приоритет), выбор режима в
   форме публикации, список откликов на карточке предложения. Токены —
   общие; своих цветов нет. */
.offer-section { display: grid; gap: 12px; }
/* Воздух между секциями биржи: «Можно взять сейчас» и «Мои отклики» — разные
   разговоры, и слипшимися они читались одним списком (заказчик, 04.09.2026). */
.offer-section + .offer-section { margin-top: 26px; }
.offer-section__head { display: grid; gap: 4px; }
.offer-section__title { font-size: 17px; font-weight: 800; display: flex; align-items: center; gap: 8px; }
.offer-cards { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.offer-card {
  position: relative; display: grid; gap: 10px; padding: 14px 16px;
  border-radius: var(--radius-lg); background: var(--color-surface); border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-primary); transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.offer-card:hover { border-color: var(--color-primary); box-shadow: var(--shadow-soft); }
.offer-card--linked { border-left-color: var(--color-warning); }
.offer-card--muted { opacity: .72; border-left-color: var(--color-border); }
.offer-card--muted:hover { box-shadow: none; }
/* Вся карточка — ссылка на подробности; кнопки лежат поверх (z-index). */
.offer-card__link { position: absolute; inset: 0; border-radius: inherit; z-index: 0; }
.offer-card__head, .offer-card__where, .offer-card__note, .offer-card__meta, .offer-card__actions { position: relative; z-index: 1; pointer-events: none; }
.offer-card__actions { pointer-events: auto; }
.offer-card__head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.offer-card__day { display: block; font-size: 18px; font-weight: 800; letter-spacing: -0.01em; color: var(--color-heading); }
.offer-card__time { display: block; font-size: 14px; font-weight: 700; color: var(--color-primary-dark); margin-top: 2px; font-variant-numeric: tabular-nums; }
.offer-card__type { flex: none; padding: 4px 10px; border-radius: var(--radius-pill); background: var(--color-surface-soft); color: var(--color-muted); font-size: 12px; font-weight: 700; }
.offer-card__where strong { display: block; font-size: 14px; }
.offer-card__where span { display: block; color: var(--color-muted); font-size: 12px; margin-top: 2px; }
.offer-card__note { margin: 0; font-size: 13px; color: var(--color-text); }
.offer-card__meta { display: flex; flex-wrap: wrap; gap: 6px 12px; color: var(--color-muted); font-size: 12px; }
.offer-card__actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 2px; }
.offer-card__reason { font-size: 12.5px; color: var(--color-muted); font-weight: 600; }
.offer-blocked > summary { cursor: pointer; font-weight: 700; color: var(--color-muted); padding: 6px 0; display: flex; align-items: center; gap: 8px; }
.offer-blocked[open] > summary { margin-bottom: 10px; }
.offer-history .shift-list__link { opacity: .8; }
.offer-detail__note { padding: 10px 12px; border-radius: var(--radius-md); background: var(--color-surface-soft); font-size: 13.5px; }

/* Форма публикации свободной смены: разделы с заголовками вместо одной длинной
   простыни полей. Сетка раздела — шесть колонок; поле занимает 6 (во всю
   ширину), 3 (половина) или 2 (треть) и схлопывается на узких экранах. Один ряд
   из шести полей ломал подписи на две строки, а подсказка под «сколько человек»
   разъезжала сетку по высоте. */
/* Потолок уже общего (.card--form на широком экране — 1180px): полей мало и
   они короткие, а поле «сколько человек» шириной в пол-экрана читается как
   ошибка вёрстки. */
.offer-form { display: block; max-width: 940px; }
.offer-form__alert { margin-bottom: 4px; }
.offer-form__section { display: grid; gap: 12px; padding: 16px 0; }
/* Разделитель — каждому разделу, кроме первого. Именно ``~``, а не ``+``:
   между разделами лежат скрытые поля (csrf, audience), и соседство рвётся. */
.offer-form__section ~ .offer-form__section { border-top: 1px solid var(--color-border); }
.offer-form__section:first-of-type { padding-top: 0; }
.offer-form input[type="number"] { max-width: 140px; }
.offer-form__title { font-size: 13px; font-weight: 800; color: var(--color-text); padding: 0; }
.offer-form__note { color: var(--color-muted); font-size: 12.5px; }
.offer-form__grid {
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 14px; align-items: start;
}
.offer-form__col--full { grid-column: span 6; }
.offer-form__col--half { grid-column: span 3; }
.offer-form__col--third { grid-column: span 2; }
.offer-form__foot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding-top: 16px; margin-top: 16px; border-top: 1px solid var(--color-border);
}
/* «Дополнительно»: всё, у чего есть разумное умолчание, свёрнуто (заказчик,
   04.09.2026 — «в пару кликов кинуть заявку, что требуется сотрудник»). Поля
   остаются в DOM и уходят на сервер: <details> ничего не отключает. */
.offer-form__more { padding-top: 16px; margin-top: 16px; border-top: 1px solid var(--color-border); }
.offer-form__more-summary {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  cursor: pointer; list-style: none;
  font-size: 13px; font-weight: 800; color: var(--color-text);
}
.offer-form__more-summary::-webkit-details-marker { display: none; }
/* Стрелка своя: маркер браузера не красится и в тёмной теме светится белым. */
.offer-form__more-summary::before {
  content: ""; flex: 0 0 auto; width: 7px; height: 7px;
  border-right: 2px solid var(--color-muted); border-bottom: 2px solid var(--color-muted);
  transform: rotate(-45deg); transition: transform .15s ease;
}
.offer-form__more[open] > .offer-form__more-summary::before { transform: rotate(45deg); }
.offer-form__more-hint { font-size: 12.5px; font-weight: 500; color: var(--color-muted); }
/* Первый раздел внутри «Дополнительно» не должен рисовать вторую линию под
   самим summary — рельса уже есть у details. */
.offer-form__more > .offer-form__section:first-of-type { border-top: 0; }
@media (max-width: 760px) {
  .offer-form__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .offer-form__col--full, .offer-form__col--half { grid-column: 1 / -1; }
  .offer-form__col--third { grid-column: span 1; }
}
@media (max-width: 460px) {
  .offer-form__grid { grid-template-columns: 1fr; }
  .offer-form__col--third { grid-column: 1 / -1; }
  .offer-form__foot > .btn { flex: 1 1 100%; }
}

/* Выбор режима/адресации — радио карточкой с подписью и подсказкой; на широком
   экране два варианта встают рядом, иначе строка в 800px несла бы шесть слов. */
.offer-choice { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.offer-choice__item,
.offer-toggle {
  display: flex; gap: 10px; align-items: flex-start; padding: 11px 13px;
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  cursor: pointer; background: var(--color-surface); transition: border-color .15s var(--ease), background .15s var(--ease);
}
.offer-choice__item:hover, .offer-toggle:hover { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); }
.offer-choice__item:has(input:checked),
.offer-toggle:has(input:checked) { border-color: var(--color-primary); background: var(--color-primary-soft); }
.offer-choice__item:has(input:focus-visible),
.offer-toggle:has(input:focus-visible) { box-shadow: var(--ring); }
.offer-choice__item input { margin: 2px 0 0; accent-color: var(--color-primary); flex: none; }
.offer-choice__body, .offer-toggle__body { display: grid; gap: 3px; min-width: 0; }
.offer-choice__title, .offer-toggle__title { font-weight: 700; font-size: 13.5px; color: var(--color-heading); }
.offer-choice__hint, .offer-toggle__hint { color: var(--color-muted); font-size: 12px; line-height: 1.45; }
/* Галочка «разовая замена» — та же карточка, что у радио, с фирменным квадратом
   (.checkbox__box), а не голым браузерным чекбоксом сбоку от селекта. */
/* Карточка тянется на всю высоту ряда и центрует содержимое: рядом с ней
   стоит селект с подписью и подсказкой, и прижатая к верху галочка вставала
   вровень с подписью, а не с полем. */
.offer-toggle { align-self: stretch; align-items: center; position: relative; }
.offer-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.offer-toggle .checkbox__box { flex: none; margin-top: 1px; }
/* Правила состояния у ``.checkbox`` привязаны к своему предку — повторяем их
   для карточки-галочки, иначе квадрат не закрашивается при отметке. */
.offer-toggle input:checked + .checkbox__box { background: var(--color-primary); border-color: var(--color-primary); }
.offer-toggle input:checked + .checkbox__box::after { transform: rotate(45deg) scale(1); }

/* Смена по заявке на замену: параметры показываем, но не даём править. */
.offer-linked { display: grid; gap: 10px; padding: 13px 15px; border-radius: var(--radius-md); background: var(--color-surface-soft); border: 1px solid var(--color-border); }
.offer-linked__grid { display: grid; gap: 10px 18px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.offer-linked__grid span { display: block; color: var(--color-muted); font-size: 12px; margin-bottom: 3px; }
.offer-linked__grid strong { display: block; font-size: 13.5px; }
.offer-linked__note { color: var(--color-muted); font-size: 12.5px; line-height: 1.5; }

/* Отклики на карточке предложения (staff). */
.offer-claims { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.offer-claim { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; padding: 10px 12px; border-radius: var(--radius-md); background: var(--color-surface-soft); border: 1px solid var(--color-border); }
.offer-claim--pending { border-color: color-mix(in srgb, var(--color-warning) 45%, var(--color-border)); }
.offer-claim--accepted { border-color: color-mix(in srgb, var(--color-success) 45%, var(--color-border)); }
.offer-claim__who { display: grid; gap: 2px; min-width: 0; }
.offer-claim__who span { font-size: 12px; }
.offer-claim__note { font-size: 13px; color: var(--color-text); }
.offer-claim__side { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.offer-assign { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.hub-tabs__badge { margin-left: 4px; padding: 1px 7px; font-size: 11px; vertical-align: middle; }
@media (max-width: 720px) {
  .offer-claim { flex-direction: column; }
  .offer-claim__side { justify-content: flex-start; }
}
/* Маркер открытого предложения биржи в клетке таблицы графика. */
/* В МЕСЯЧНОМ виде плашка биржи нейтральна: синий фон + пунктир + янтарный «!»
   были четвёртой цветовой системой в клетке 88px, причём синий на этом экране
   уже занят навигацией (колонка «сегодня», ховер строки, «+»), а янтарный —
   заменой. Плюс z-index: 2 ставил её ВЫШЕ фамилий. В дневном виде
   (.mx-offer--day) всё остаётся: там клетка крупная и конкурентов нет. */
.mx-offer { display: inline-flex; align-items: center; gap: 3px; margin: 2px 0 3px; padding: 1px 7px; border-radius: var(--radius-pill); font-size: 10.5px; font-weight: 700; line-height: 1.5; text-decoration: none; background: var(--color-primary-soft); color: var(--color-primary-dark); border: 1px dashed color-mix(in srgb, var(--color-primary) 45%, transparent); position: relative; z-index: 2; }
.sched-matrix:not(.sched-matrix--day) .mx-offer {
  background: transparent; color: var(--color-muted);
  border: 1px dashed var(--color-border); z-index: 1;
}
.sched-matrix:not(.sched-matrix--day) .mx-offer b { color: var(--color-primary-dark); }
.mx-offer:hover { border-style: solid; }
.mx-offer b { color: var(--color-warning); }
.mx-offer--day { font-size: 12px; padding: 4px 10px; margin: 0 0 6px; }

/* ---- Выдача паролей пачкой (.creds-*, .cred-card) --------------------------
   Действующие пароли выгрузить нельзя — в базе только хеш Argon2, — поэтому
   «выгрузка логинов и паролей» на списке учёток означает выдачу НОВЫХ: отметил
   строки, получил таблицу, забрал файлом или карточками на печать. */

/* Столбец отметок в таблице пользователей. Галочка вне .checkbox намеренно:
   там input скрыт (opacity: 0) под кастомную отрисовку, а в строке списка
   нужен обычный нативный чекбокс — как в колонке утверждения KPI. */
.creds-pick-col { width: 36px; }
.creds-pick-col input { display: block; width: 16px; height: 16px; accent-color: var(--color-primary); cursor: pointer; }

/* Панель действий появляется только с первой отметкой, поэтому обязана уметь
   прятаться: у .bulk-bar стоит display: flex, а он перебивает браузерное
   правило [hidden] { display: none } — без этой строки панель висела бы всегда. */
.creds-bar[hidden] { display: none; }
.creds-bar__note { font-size: 12.5px; }

/* Логин и пароль переписывают от руки: в пропорциональном начертании «l», «1»
   и «I» не различить, поэтому обе колонки моноширинные. */
.creds-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.creds-pass { font-weight: 700; letter-spacing: 0.02em; white-space: nowrap; }

.creds-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }

/* Карточки на печать: две колонки, чтобы на лист А4 вставало восемь штук. */
.creds-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.cred-card {
  display: flex; flex-direction: column; gap: 8px;
  padding: 14px 16px; border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); background: var(--color-surface);
  /* Разрыв страницы не должен рассекать карточку: половина логина на одном
     листе, половина пароля на другом — это уже не доступ, а головоломка. */
  break-inside: avoid; page-break-inside: avoid;
}
.cred-card__brand { margin: 0; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-muted); }
.cred-card__name { margin: 0; font-size: 15.5px; font-weight: 800; color: var(--color-heading); }
.cred-card__pvz { margin: 0; font-size: 12.5px; color: var(--color-muted); }
.cred-card__rows { margin: 0; display: grid; gap: 6px; }
.cred-card__row { display: flex; align-items: baseline; gap: 10px; }
.cred-card__row dt { flex: 0 0 62px; font-size: 12px; color: var(--color-muted); }
.cred-card__row dd { margin: 0; min-width: 0; font-size: 13.5px; word-break: break-all; }
.cred-card__note { margin: 0; font-size: 11.5px; color: var(--color-muted); }
@media (max-width: 720px) {
  .creds-cards { grid-template-columns: minmax(0, 1fr); }
}

/* Печать карточек. Правила заперты на .creds-print-page: у остальных страниц
   системы печать остаётся прежней. */
@media print {
  @page { margin: 12mm; }
  /* Токены переопределяем на самом <body>: значение, объявленное на элементе,
     всегда бьёт унаследованное от html[data-theme="dark"], сколько бы
     специфичности ни было у того селектора. Иначе тёмная тема отдала бы белый
     текст (#E7E7EA) на белой бумаге — фон-то браузер не печатает. */
  body.creds-print-page {
    --color-bg: #FFFFFF;
    --color-bg-deep: #FFFFFF;
    --color-surface: #FFFFFF;
    --color-surface-soft: #FFFFFF;
    --color-surface-alt: #FFFFFF;
    --color-border: #9AA0AA;
    --color-border-strong: #6B7280;
    /* Пятая точка объявления палитры — и её легко забыть. Разделители таблиц
       переведены на --color-divider (color-mix от --color-text), а здесь текст
       чёрный: 9% чёрного на бумаге это почти невидимый #E8E8E8. Задаём тем же
       тоном, что и рамку, иначе границы таблиц в карточках доступа исчезнут
       при печати — и никто не заметит, пока не распечатает. */
    --color-divider: #9AA0AA;
    --color-text: #000000;
    --color-heading: #000000;
    --color-muted: #333333;
    --color-faint: #333333;
    background: #FFFFFF !important;
    background-image: none !important;
    color: #000000 !important;
  }
  /* Оболочка на бумаге не нужна вся целиком: шапка, боковое меню, таб-панель,
     плавающие виджеты и любые кнопки. Псевдоэлементы .app-shell — полосы
     overscroll, они fixed и напечатались бы на каждом листе. */
  body.creds-print-page .topbar,
  body.creds-print-page .sidebar,
  body.creds-print-page .tabbar,
  body.creds-print-page .imp-banner,
  body.creds-print-page .messages,
  body.creds-print-page .railtip,
  body.creds-print-page .season-layer,
  body.creds-print-page .fb-fab,
  body.creds-print-page .fb-modal,
  body.creds-print-page .pwa-sheet,
  body.creds-print-page .chat-dock,
  body.creds-print-page .creds-noprint,
  body.creds-print-page .btn { display: none !important; }
  body.creds-print-page .app-shell::before,
  body.creds-print-page .app-shell::after { content: none !important; display: none !important; }
  body.creds-print-page .app-shell,
  body.creds-print-page .app-main { display: block !important; }
  body.creds-print-page .page {
    display: block !important; padding: 0 !important; gap: 0 !important;
    max-width: none !important; margin: 0 !important; animation: none !important;
  }
  body.creds-print-page .creds-cards { gap: 6mm; }
  body.creds-print-page .cred-card {
    border: 1px solid #444444 !important;
    background: #FFFFFF !important;
    color: #000000 !important;
  }
  body.creds-print-page .cred-card__brand,
  body.creds-print-page .cred-card__pvz,
  body.creds-print-page .cred-card__note,
  body.creds-print-page .cred-card__row dt { color: #333333 !important; }
  body.creds-print-page .cred-card__name { color: #000000 !important; }
}
