    :root {
      /* Тёмная схема объявлена явно. Без этого браузер рисует СВОЙ фокусный
         обод для светлого фона — тёмно-синий, и на обсидиане его не видно;
         той же причины касаются календарь у input[type=date], выпадающий
         список <select> и полосы прокрутки. Светлая тема переключает схему
         обратно — см. [data-theme="light"]. */
      color-scheme: dark;
      /* Obsidian & Luxury Tech palette */
      --bg-base: #050811;
      --bg-mid: #0b1120;
      --bg-deep: #10172a;
      --bg-gradient: linear-gradient(160deg, var(--bg-base) 0%, var(--bg-mid) 50%, var(--bg-deep) 100%);
      --card-bg: rgba(16, 23, 42, 0.78);
      --card-border: rgba(255, 255, 255, 0.08);
      --glass-bg: rgba(16, 23, 42, 0.78);
      --glass-border: rgba(255, 255, 255, 0.08);
      --glass-hover: rgba(22, 30, 50, 0.9);
      --accent-gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%);
      --accent-gradient-emerald: linear-gradient(135deg, #10b981 0%, #06b6d4 100%);
      --accent-gradient-gold: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
      /* Горизонтальный вариант фирменного градиента для узких полос прогресса
         (столбики расхода токенов): под 135deg тонкая полоса высотой 14px
         показывает почти один цвет, и шкала перестаёт читаться. */
      --accent-gradient-primary-h: linear-gradient(90deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%);
      --accent-purple: #8b5cf6;
      --accent-indigo: #6366f1;
      /* Осветлённый индиго для заголовков разделов: на обсидиане --accent-indigo
         уходит в фон, поэтому подписи секций набраны отдельным оттенком.
         В светлой теме он переопределён на насыщенный — см. [data-theme="light"]. */
      --accent-indigo-soft: #a5b4fc;
      --accent-cyan: #06b6d4;
      --accent-green: #10b981;
      /* Алиас под имя из docs/DESIGN_SYSTEM.md: блок диагностики был свёрстан
         на --accent-emerald, которого в этой палитре не существовало, и все
         «успешные» индикаторы отрисовывались дефолтным цветом текста. */
      --accent-emerald: var(--accent-green);
      --accent-teal: #06b6d4;
      --accent-amber: #f59e0b;
      /* Имя из docs/DESIGN_SYSTEM.md для «золотого» смысла (VIP, ипотека,
         премиум-объекты). Отдельный токен, а не новый цвет: янтарь и золото —
         один и тот же оттенок, но читаются в разметке по-разному. */
      --accent-gold: var(--accent-amber);
      /* Осветлённые оттенки акцентов для ТЕКСТА на тёмной подложке: чистый
         --accent-purple/-green/-gold/-rose на обсидиане не добирает контраста,
         поэтому бейджи и подписи набраны на ступень светлее. Раньше эти
         значения были рассыпаны по файлу хардкодом. */
      --accent-purple-soft: #c084fc;
      --accent-emerald-soft: #34d399;
      --accent-gold-soft: #fbbf24;
      --accent-rose-soft: #fda4af;
      /* Зеркальная пара к -soft: затемнённые оттенки той же ветки палитры для
         ТЕКСТА на жемчужном фоне светлой темы. Чистый янтарь на своей же
         подложке даёт 1.9:1 — подпись плашки на свету не читалась. */
      --accent-amber-deep: #92400e;
      --accent-purple-deep: #6d28d9;
      --accent-rose: #f43f5e;
      --text-main: #fafafa;
      --text-body: #cbd5e1;
      --text-muted: #808ea3;
      --gradient-indigo-cyan: var(--accent-gradient-primary);
      --gradient-emerald-teal: var(--accent-gradient-emerald);
      --font-display: 'Outfit', system-ui, sans-serif;
      --font-body: 'Plus Jakarta Sans', -apple-system, sans-serif;
      /* Моноширинный набор для технических значений (hub.verify_token, id и т.п.):
         в подсказках важно различать O/0 и l/1, иначе секрет вводят с опечаткой. */
      --font-mono: 'SF Mono', 'JetBrains Mono', Consolas, monospace;
      --radius-sm: 8px;
      --radius-md: 12px;
      --radius-lg: 16px;
      --radius-pill: 999px;
      --blur-glass: blur(16px);
      --blur-modal-deep: blur(20px);
      --shadow-glow-indigo: 0 8px 30px rgba(99, 102, 241, 0.35);
      --shadow-glow-emerald: 0 8px 30px rgba(16, 185, 129, 0.3);
      --shadow-glow-gold: 0 8px 30px rgba(245, 158, 11, 0.3);
      --shadow-card: 0 10px 30px rgba(0, 0, 0, 0.45);
      /* Высота липкой шапки. Её знают сайдбар (sticky top) и scroll-margin
         кнопок разделов: без общей величины панель уезжала под шапку, а
         прокрутка к активному разделу оставляла его наполовину закрытым. */
      --header-h: 74px;
      /* Подложка «на полтона светлее фона» для вложенных панелей, строк списков
         и чипсов. Была рассыпана по файлу как rgba(255,255,255,0.02…0.09) —
         на жемчужном фоне светлой темы такие подложки давали белое на белом,
         и панель переставала отделяться от карточки. Токен разводит тему:
         на обсидиане светлеем, на жемчуге — темнеем. */
      --surface-tint-weak: rgba(255, 255, 255, 0.02);
      --surface-tint: rgba(255, 255, 255, 0.03);
      --surface-tint-mid: rgba(255, 255, 255, 0.04);
      --surface-tint-chip: rgba(255, 255, 255, 0.05);
      --surface-tint-strong: rgba(255, 255, 255, 0.08);
      --surface-tint-stronger: rgba(255, 255, 255, 0.1);
      /* Ступеней пять, а не две (докрутка ревью 17.08.2026): в файле
         использовались значения 0.02, 0.03, 0.04, 0.08 и 0.1, и сведение их к
         двум токенам МЕНЯЛО тёмную тему в тринадцати местах — вложенные панели
         становились вдвое светлее, а чипсы фильтров, наоборот, приглушались.
         Правка задумывалась как чисто светлая, поэтому каждая ступень
         сохраняет своё прежнее значение. */
      /* Разделитель строк таблиц и внутренних блоков. Был хардкодом
         rgba(255,255,255,0.05): на жемчужном фоне светлой темы это белое по
         белому, и строки во ВСЕХ таблицах кабинета сливались в сплошное поле. */
      --row-border: rgba(255, 255, 255, 0.05);
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      font-family: var(--font-body);
    }

    html, body {
      overflow-x: hidden;
      max-width: 100vw;
      scrollbar-width: thin;
      scrollbar-color: rgba(255, 255, 255, 0.16) transparent;
    }

    /* Custom Dark Scrollbars */
    ::-webkit-scrollbar {
      width: 6px;
      height: 6px;
    }
    ::-webkit-scrollbar-track {
      background: transparent;
    }
    ::-webkit-scrollbar-thumb {
      background: rgba(255, 255, 255, 0.16);
      border-radius: 999px;
    }
    ::-webkit-scrollbar-thumb:hover {
      background: rgba(255, 255, 255, 0.3);
    }

    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
    }

    /* ══════════════════════════════════════════════════════════════════════
       ОБОД ФОКУСА ПО УМОЛЧАНИЮ
       ----------------------------------------------------------------------
       Обход кабинета клавиатурой раньше опирался на обод браузера везде, где
       у компонента не было своего правила: у кнопок-иконок таблиц, вкладок
       застройщиков, ссылок внутри карточек. Браузерный обод берёт цвет из
       схемы страницы и на обсидиане оказывается тёмно-синим на тёмном — тот
       же дефект, каким на виджете было белое кольцо на белом.
       Селектор завёрнут в :where(): его вес — ноль, поэтому ЛЮБОЕ правило
       компонента ниже по файлу по-прежнему перебивает этот обод, а элементы
       без собственного правила перестают терять фокус.
       ══════════════════════════════════════════════════════════════════════ */
    :where(a[href], area, button, input, select, textarea, summary,
           [tabindex]:not([tabindex="-1"]), [contenteditable="true"]):focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }

    body {
      background: var(--bg-gradient);
      color: var(--text-body);
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      position: relative;
    }

    /* Background Mesh Glow */
    body::before {
      content: '';
      position: fixed;
      top: 0; left: 50%;
      transform: translateX(-50%);
      width: 1000px; height: 400px;
      background: radial-gradient(ellipse at top, rgba(99, 102, 241, 0.12), transparent 70%);
      pointer-events: none;
      z-index: 0;
    }

    header, .app-container {
      position: relative;
      z-index: 1;
    }

    h1, h3, h4 { font-family: var(--font-display); }

    /* Navbar Header */
    header {
      background: rgba(11, 17, 32, 0.94);
      backdrop-filter: var(--blur-glass);
      border-bottom: 1px solid var(--glass-border);
      padding: 16px 32px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      /* Шапка состоит из ТРЁХ цельных блоков: бренд с индикатором, переключатель
         застройщика (только у платформы) и блок действий. Перенос разрешён
         только между ними — поэтому на узком экране шапка складывается ровными
         этажами, а не рвётся посередине группы кнопок. */
      flex-wrap: wrap;
      row-gap: 10px;
      position: sticky;
      top: 0;
      z-index: 100;
    }

    /* Бренд — единственный участок шапки, которому можно сжиматься: заголовок и
       подзаголовок ужимаются многоточием, а лимиты, пользователь и кнопки —
       нет. Без min-width:0 текст держал бы 600px, шапка не влезала в строку и
       блок действий уезжал вторым этажом (жалоба владельца 16.08.2026). */
    .brand {
      display: flex;
      align-items: center;
      gap: 12px;
      /* База 0, а не auto: перенос в flex решается по ЖЕЛАЕМОЙ ширине блоков, а
         не по фактической. С базой auto длинный заголовок «хотел» 530px и
         выталкивал блок действий на второй этаж там, где места хватало с
         запасом. С базой 0 бренд забирает лишь остаток строки и обрезается
         многоточием, а перенос случается ровно тогда, когда действительно не
         помещаются сами кнопки. */
      flex: 1 1 0;
      /* Пол в 220px — чтобы бренд с индикатором не схлопнулся в ноль: у
         платформенного аккаунта переключатель застройщика занимал всю строку и
         логотип с бейджем исчезали из шапки совсем. */
      min-width: 220px;
      /* Страховка от наложения: у платформенного аккаунта в шапке живут ещё и
         переключатель застройщика, ключ токена и авто-тесты. Без overflow
         содержимое бренда вылезало из своей коробки и переключатель наезжал на
         бейдж состояния (поймано прогоном «худшего случая» 16.08.2026). */
      overflow: hidden;
    }
    /* min-content, а не 0: заголовок обрезается многоточием сколько угодно, но
       подпись версии остаётся читаемой целиком — ради неё в кабинет и заходят
       после выкладки. Ниже этой границы бренд уже не сжимается. */
    .brand-title {
      /* База 0 — чтобы место в бренде первым отдавал ЗАГОЛОВОК, а не бейдж
         состояния: при равных базах сжимались оба, и «Система в норме»
         превращалась в «Систем…» при целом заголовке — а состояние тут важнее
         названия кабинета.
         Нижней границы по умолчанию нет: заголовок набран nowrap, и его
         min-content равен всей строке — с ним бренд не сжимался вовсе и место
         отдавал бейдж состояния. */
      flex: 1 1 0;
      min-width: 0;
    }
    /* Пол в 230px появляется ТОЛЬКО когда подпись версии реально показана: она
       не сжимается и не обрезается — ради неё в кабинет и заходят после
       выкладки. Пока сервер версию не прислал, пола нет и бренд сжимается до
       логотипа с бейджем. Браузер без :has() просто вернётся к обрезке подписи
       в редком узком случае — раскладка при этом не ломается. */
    .brand-title:has(.build-stamp:not([hidden])) { min-width: 230px; }

    .brand-logo {
      width: 40px;
      height: 40px;
      background: var(--gradient-indigo-cyan);
      color: white;
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 20px;
      box-shadow: var(--shadow-glow-indigo);
    }

    /* Версия сборки в шапке: подпись, а не элемент управления. Приглушена
       намеренно — читают её редко, но найти нужно без кликов. */
    .build-stamp {
      display: inline-flex;
      align-items: center;
      padding: 3px 8px;
      border: 1px solid var(--glass-border);
      border-radius: 8px;
      font-size: 0.7rem;
      font-variant-numeric: tabular-nums;
      color: var(--text-muted);
      white-space: nowrap;
      cursor: help;
      line-height: 1.1;
      max-width: 100%;
      /* Сжимается подзаголовок, а не подпись: см. .brand-title p. */
      flex-shrink: 0;
      transition: all 0.2s ease;
    }

    .build-stamp:hover { color: var(--text-body); border-color: var(--accent-indigo); }

    /* Кликабельна подпись ТОЛЬКО у администратора платформы: журнал изменений
       закрыт `architecture:read` (решение владельца 16.08.2026). По `guides:read`
       гейтить нельзя — это право есть и у владельца застройщика, и подпись вела
       бы его на 403. Без права это по-прежнему просто подпись: ни курсора-руки,
       ни фокуса, ни клика — кабинет не показывает вход, которого нет. */
    .build-stamp.is-linked { cursor: pointer; }
    .build-stamp.is-linked:hover,
    .build-stamp.is-linked:focus-visible {
      color: var(--text-main);
      border-color: var(--accent-indigo);
      background: rgba(99, 102, 241, 0.14);
    }
    .build-stamp.is-linked:focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }

    /* Своё `display` перебивает браузерное `[hidden] { display: none }` — без
       этой строки скрытая подпись остаётся в шапке пустой рамкой: и до первого
       ответа /health, и когда версии нет вовсе (находка ревью 16.08.2026). Та
       же ловушка уже ловилась на .catalog-pager и .property-inherited. */
    .build-stamp[hidden] { display: none; }

    /* Счётчик коммитов после тега («+7»): код новее последнего помеченного
       релиза. Это НЕ тревога — поэтому спокойный индиго из палитры, а не
       предупреждающий цвет. Значок нейтрально-информационный и не должен
       выглядеть как ошибка. */
    .build-stamp-ahead {
      margin-left: 4px;
      padding: 0 5px;
      border-radius: 6px;
      background: rgba(99, 102, 241, 0.16);
      color: var(--accent-indigo);
      font-size: 0.92em;
      font-weight: 600;
      line-height: 1.4;
    }

    /* В светлой теме индиго на светлой подложке теряет контраст — берём
       насыщенный фиолетовый из палитры и чуть плотнее фон. */
    [data-theme="light"] .build-stamp-ahead {
      background: rgba(139, 92, 246, 0.14);
      color: var(--accent-purple);
    }

    /* Сервер не смог определить, откуда взялась версия: показываем, но не
       выдаём за проверенный факт. */
    .build-stamp.is-unverified { opacity: 0.72; border-style: dashed; }

    .health-badge {
      display: inline-flex;
      align-items: center;
      gap: 7px;
      background: rgba(16, 185, 129, 0.12);
      color: var(--accent-green);
      border: 1px solid rgba(16, 185, 129, 0.3);
      padding: 6px 12px;
      border-radius: var(--radius-pill);
      font-size: 0.75rem;
      font-weight: 600;
      margin-left: 8px;
      /* Бейдж не переносит свой текст и не сжимается: при переносе «Система в
         норме» вставала в две строки, бейдж вырастал до 44px и тянул за собой
         высоту всей шапки, а кружок-индикатор съезжал относительно текста
         (жалоба владельца 16.08.2026). Место экономит подзаголовок бренда. */
      white-space: nowrap;
      /* Сжиматься можно, переносить текст — нельзя: в крайнем случае подпись
         обрежется многоточием (цветной кружок и title остаются), но бейдж
         никогда не станет двухстрочным и не вырастит шапку. */
      flex: 0 1 auto;
      min-width: 0;
    }
    /* Кружок-индикатор не сжимается никогда — именно он и «съезжал», когда
       текст рядом уходил на вторую строку. */
    #healthBadgeText {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      min-width: 0;
    }
    .health-badge {
      cursor: pointer;
      font-family: inherit;
      transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
    }
    .health-badge:hover {
      background: rgba(16, 185, 129, 0.22);
      border-color: rgba(16, 185, 129, 0.55);
      transform: translateY(-1px);
    }
    /* Связь с сервером потеряна — плашка краснеет, точка перестает пульсировать */
    .health-badge.is-offline {
      background: rgba(244, 63, 94, 0.14);
      color: var(--accent-rose);
      border-color: rgba(244, 63, 94, 0.4);
    }
    .health-badge.is-offline:hover {
      background: rgba(244, 63, 94, 0.24);
      border-color: rgba(244, 63, 94, 0.6);
    }
    .health-badge.is-offline .health-dot {
      background: var(--accent-rose);
      box-shadow: 0 0 8px var(--accent-rose);
      animation: none;
    }
    /* Частичная деградация (LLM-предохранитель): база жива, поэтому не красный,
       но и не зелёный — иначе «LLM Fallback» читался бы как норма. */
    .health-badge.is-degraded {
      background: rgba(245, 158, 11, 0.14);
      color: var(--accent-amber);
      border-color: rgba(245, 158, 11, 0.4);
    }
    .health-badge.is-degraded:hover {
      background: rgba(245, 158, 11, 0.24);
      border-color: rgba(245, 158, 11, 0.6);
    }
    .health-badge.is-degraded .health-dot {
      background: var(--accent-amber);
      box-shadow: 0 0 8px var(--accent-amber);
    }
    /* Подробная диагностика — рычаг платформы. У остальных бейдж остаётся
       индикатором: без курсора-руки и без подъёма на наведении, чтобы он не
       обещал клик, которого не будет. */
    .health-badge.is-static {
      cursor: default;
    }
    .health-badge.is-static:hover {
      transform: none;
      background: rgba(16, 185, 129, 0.12);
      border-color: rgba(16, 185, 129, 0.3);
    }
    .health-badge.is-static.is-offline:hover {
      background: rgba(244, 63, 94, 0.14);
      border-color: rgba(244, 63, 94, 0.4);
    }
    .health-badge.is-static.is-degraded:hover {
      background: rgba(245, 158, 11, 0.14);
      border-color: rgba(245, 158, 11, 0.4);
    }

    .health-dot {
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: var(--accent-green);
      box-shadow: 0 0 8px var(--accent-green);
      animation: pulseGlow 2.4s infinite;
      flex: 0 0 auto;
    }

    /* Конструктор роли, тона и правил поведения ИИ */
    .behavior-editor {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-top: 16px;
      padding: 16px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: rgba(255, 255, 255, 0.03);
    }
    [data-theme="light"] .behavior-editor { background: rgba(241, 245, 249, 0.6); }
    .behavior-counter {
      font-size: 0.72rem;
      color: var(--text-muted);
      font-weight: 600;
    }

    /* === Вкладка «Настройки ИИ»: карточки по значимости ====================
       Плоская стопка textarea не давала подсказки, что важнее: главный промпт
       роли выглядел так же, как скрипт возражений. Номер + сдержанный акцент
       карточки расставляют приоритет ещё до чтения подписей. Подсветка здесь
       намеренно тише, чем была: раздел выбивался из остальных вкладок, где
       карточки различаются 3px-полосой слева, а не сплошным свечением. */
    .ai-setting-card {
      margin-top: 18px;
      padding: 18px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }
    .ai-setting-card:hover {
      border-color: rgba(99, 102, 241, 0.28);
      box-shadow: var(--shadow-card);
    }
    /* Цветных полос у карточек раздела больше нет: они спорили с общей сеткой
       админки, где карточки одинаковы, а смысл несут заголовок и номер блока. */

    .ai-setting-head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 14px;
      flex-wrap: wrap;
      /* Шапка блока — отдельная полоса внутри карточки: 16px внутреннего
         отступа читаются как намеренная врезка, а не как «уехал текст». */
      padding: 16px;
      margin-bottom: 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: rgba(99, 102, 241, 0.05);
    }
    .ai-setting-titles { display: flex; align-items: flex-start; gap: 12px; min-width: 0; }
    .ai-setting-meta {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
      flex: 0 0 auto;
    }
    /* Номер = порядок применения блока в системном промпте (роль → тон →
       запреты → возражения → Co-Pilot). Оформлен как ровный числовой бейдж —
       так же, как порядковые значки в остальных вкладках кабинета. */
    .ai-setting-rank {
      flex: 0 0 auto;
      width: 26px;
      height: 26px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: rgba(99, 102, 241, 0.12);
      font-family: var(--font-display);
      font-size: 0.86rem;
      font-weight: 700;
      line-height: 1;
      color: var(--text-body);
    }
    .ai-setting-title {
      margin: 0 0 4px;
      font-family: var(--font-display);
      font-size: 1.02rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .ai-setting-card--primary .ai-setting-title { font-size: 1.14rem; }
    .ai-setting-desc {
      margin: 0;
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-muted);
      max-width: 74ch;
    }
    .ai-setting-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
    /* Подпись нужна скринридеру и `for=`, но визуально дублирует заголовок карточки */
    .sr-only-label {
      position: absolute;
      width: 1px; height: 1px;
      margin: -1px; padding: 0;
      overflow: hidden;
      clip: rect(0 0 0 0);
      white-space: nowrap;
      border: 0;
    }

    /* Ряд с заготовками: выбор подставляет текст, но не сохраняет — поэтому
       рядом всегда виден бейдж «не сохранено» в шапке карточки. */
    .ai-template-row {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
      margin-bottom: 10px;
    }
    .ai-template-label {
      font-size: 0.74rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: var(--text-muted);
      margin: 0;
    }
    /* Ряд занимает ровно ту же ширину, что и селектор тона (#behavior-tone,
       .form-select во всю ширину .form-group): раньше короткий селект с хвостом
       пустоты читался как чужеродный элемент раздела. */
    .ai-template-row { width: 100%; }
    .ai-template-select { flex: 1 1 200px; min-width: 0; }
    .ai-reset-btn { font-size: 0.8rem; padding: 6px 12px; }
    .ai-reset-btn[disabled] { opacity: 0.45; cursor: not-allowed; }
    /* Отметка «Сейчас: …» — какая заготовка реально действует. Свой текст
       подсвечен иначе: «Свой текст» — это не поломка, а честный факт. */
    .ai-template-current {
      flex: 0 0 auto;
      padding: 4px 10px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(99, 102, 241, 0.35);
      background: rgba(99, 102, 241, 0.12);
      font-size: 0.74rem;
      font-weight: 600;
      color: var(--text-body);
      white-space: nowrap;
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .ai-template-current.is-custom {
      border-color: rgba(245, 158, 11, 0.4);
      background: rgba(245, 158, 11, 0.12);
    }

    /* Обёртки .ai-tone-directive-block здесь больше нет: ряд заготовок тона —
       такой же .ai-template-row, как в остальных блоках, и рамка вокруг него
       делала из общего механизма частный случай. Пресет голоса от текста
       директивы разводит подпись .ai-subfield-label — как в других карточках. */

    /* Пометка «скрыт у застройщика»: рычаг доступа выключен, блок остаётся
       в кабинете владельца платформы и полностью редактируем — он наполняет
       его за застройщика. Тот же янтарь, что у замков: «управляет платформа». */
    .ai-platform-only-badge {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 4px 10px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(245, 158, 11, 0.35);
      background: rgba(245, 158, 11, 0.12);
      color: var(--accent-amber);
      font-size: 0.74rem;
      font-weight: 600;
      white-space: nowrap;
    }
    .ai-platform-only-badge svg { flex: 0 0 auto; }

    /* === Рычаги доступа застройщика (владелец платформы; панели во вкладках
       «Настройки ИИ» и «Интеллект ИИ» — разметка и стили общие)
       Формулировка — разрешение, а не запрет: ВКЛ = застройщик редактирует.
       Закрытый блок подсвечен янтарём так же, как заперые строки в панели
       замков — один цвет означает «этим управляет платформа». */
    .ai-access-panel {
      margin-top: 18px;
      /* Панель рычага — соседний блок, а не подложка: без собственного нижнего
         отступа следующий .faq-toolbar (у него только margin-bottom) вставал
         вплотную и визуально наезжал на рамку панели. Тот же ритм 20px, что и
         у самого тулбара, — и на ≤768px, где тулбар становится колонкой. */
      margin-bottom: 20px;
      padding: 16px;
      border: 1px solid rgba(99, 102, 241, 0.28);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
    }
    .ai-access-head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      flex-wrap: wrap;
      margin-bottom: 12px;
    }
    .ai-access-heading {
      margin: 0 0 4px;
      font-family: var(--font-display);
      font-size: 0.98rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .ai-access-sub {
      margin: 0;
      font-size: 0.8rem;
      color: var(--text-muted);
    }
    .ai-access-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 14px;
      flex-wrap: wrap;
      padding: 12px 14px;
      margin-bottom: 8px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--surface-tint-weak);
      transition: all 0.2s ease;
    }
    .ai-access-row:hover {
      border-color: rgba(99, 102, 241, 0.35);
      transform: translateY(-1px);
      box-shadow: var(--shadow-card);
    }
    .ai-access-row.is-closed {
      border-color: rgba(245, 158, 11, 0.4);
      background: rgba(245, 158, 11, 0.06);
    }
    .ai-access-texts { flex: 1 1 320px; min-width: 0; }
    .ai-access-title {
      display: block;
      font-weight: 600;
      font-size: 0.9rem;
      color: var(--text-main);
    }
    .ai-access-hint {
      margin: 4px 0 0;
      font-size: 0.78rem;
      line-height: 1.45;
      color: var(--text-muted);
      max-width: 74ch;
    }
    .ai-access-state {
      display: flex;
      align-items: center;
      gap: 10px;
      flex: 0 0 auto;
      font-size: 0.78rem;
      color: var(--text-muted);
      cursor: pointer;
    }
    .ai-access-state-text { white-space: nowrap; }
    .ai-access-empty {
      margin: 0;
      padding: 10px 2px;
      font-size: 0.8rem;
      color: var(--text-muted);
    }
    .ai-access-error { color: var(--accent-rose); }

    /* Итог импорта психотипов. Тост живёт три секунды — этого мало, когда импорт
       СНЁС существующие профили: оператор должен видеть их список столько,
       сколько нужно, чтобы сверить с бэкапом. Строка остаётся в разделе до
       ручного закрытия или следующего импорта. */
    .brain-import-report,
    .faq-import-report {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin-top: 12px;
      padding: 12px 14px;
      border: 1px solid var(--card-border);
      border-left: 3px solid var(--text-muted);
      border-radius: 12px;
      background: var(--card-bg);
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-body);
      animation: brainImportReportIn 0.2s ease;
    }
    .brain-import-report.is-warning,
    .faq-import-report.is-warning {
      border-left-color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.08);
    }
    .brain-import-report.is-success,
    .faq-import-report.is-success {
      border-left-color: var(--accent-emerald);
    }
    .brain-import-report-body,
    .faq-import-report-body { flex: 1 1 auto; min-width: 0; }
    .brain-import-report-title,
    .faq-import-report-title {
      display: block;
      font-weight: 600;
      color: var(--text-main);
      margin-bottom: 4px;
    }
    /* Подпись над перечнем: реестров в отчёте четыре (психотипы, сценарии,
       стратегии Co-Pilot, заготовки промптов), и без подписи оператор читает
       все списки как один. Подписи длинные — переносим их, а не режем. */
    .brain-import-report-caption {
      display: block;
      overflow-wrap: anywhere;
      margin-top: 10px;
      font-weight: 600;
      font-size: 0.78rem;
      letter-spacing: 0.02em;
      color: var(--text-muted);
    }
    .brain-import-report-list,
    .faq-import-report-list {
      margin: 6px 0 0;
      padding: 0;
      list-style: none;
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
    }
    .brain-import-report-list li,
    .faq-import-report-list li {
      font-family: var(--font-mono);
      font-size: 0.76rem;
      padding: 2px 8px;
      border-radius: 6px;
      background: rgba(245, 158, 11, 0.16);
      color: var(--accent-amber);
      word-break: break-all;
    }
    .brain-import-report-close,
    .faq-import-report-close {
      flex: 0 0 auto;
      background: none;
      border: none;
      cursor: pointer;
      color: var(--text-muted);
      font-size: 1rem;
      line-height: 1;
      padding: 2px 4px;
      border-radius: 6px;
      transition: all 0.2s ease;
    }
    .brain-import-report-close:hover,
    .faq-import-report-close:hover { color: var(--text-main); background: var(--glass-hover); }
    @keyframes brainImportReportIn {
      from { opacity: 0; transform: translateY(-4px); }
      to { opacity: 1; transform: translateY(0); }
    }

    /* Режим замены базы вопросов. Рычаг живёт в панели рядом с кнопкой импорта:
       переключатель разрушающего режима, стоящий отдельно от действия, читают
       уже после того, как файл выбран. Подпись — про последствие. */
    .faq-import-mode {
      display: inline-flex;
      align-items: center;
      gap: 10px;
      padding: 6px 12px;
      border: 1px solid var(--card-border);
      border-radius: var(--radius-pill);
      background: var(--card-bg);
      cursor: pointer;
      user-select: none;
      transition: all 0.2s ease;
    }
    .faq-import-mode:hover { border-color: var(--accent-amber); background: var(--glass-hover); }
    .faq-import-mode:focus-within { border-color: var(--accent-purple); }
    .faq-import-mode-text {
      display: flex;
      flex-direction: column;
      font-size: 0.78rem;
      font-weight: 600;
      line-height: 1.25;
      color: var(--text-body);
    }
    .faq-import-mode-text small {
      font-size: 0.7rem;
      font-weight: 500;
      color: var(--text-muted);
    }
    /* Включённый режим виден без чтения подписи: рамка и текст предупреждения. */
    .faq-import-mode:has(input:checked) {
      border-color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.1);
    }
    .faq-import-mode:has(input:checked) .faq-import-mode-text { color: var(--accent-amber); }
    .faq-import-mode:has(input:checked) .faq-import-mode-text small { color: var(--accent-amber); }

    @media (max-width: 768px) {
      .brain-import-report,
      .faq-import-report { padding: 12px; font-size: 0.8rem; }
      .ai-access-panel { padding: 14px; }
      .ai-access-row { flex-direction: column; align-items: stretch; }
      .ai-access-state { justify-content: space-between; }
      .ai-template-current { white-space: normal; }
      .ai-platform-only-badge { white-space: normal; }
    }

    .ai-subfield-label {
      display: block;
      margin-top: 14px;
      font-size: 0.82rem;
      font-weight: 600;
      color: var(--text-body);
    }
    .ai-field-hint {
      margin: 6px 0 8px;
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }

    /* === Авто-касание уснувших покупателей ================================
       Блок объясняет, из чего бот соберёт сообщение, ДО того как владелец
       выставит сроки, — поэтому объяснение стоит над полями и оформлено как
       врезка-подсказка, а не как ещё одна форма. Цвета — только токены. */
    .reengage-explain {
      padding: 14px 16px;
      margin-bottom: 16px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-indigo);
      border-radius: var(--radius-md);
      background: rgba(99, 102, 241, 0.06);
    }
    .reengage-explain-title {
      margin: 0 0 10px;
      font-family: var(--font-display);
      font-size: 0.92rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .reengage-fields-title { margin-top: 4px; }
    .reengage-explain-list {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
    .reengage-explain-list li {
      position: relative;
      padding-left: 20px;
      font-size: 0.82rem;
      line-height: 1.55;
      color: var(--text-body);
      max-width: 82ch;
    }
    /* Маркер — не эмодзи: он часть хрома списка, а не данных (см. §9 дизайн-системы). */
    .reengage-explain-list li::before {
      content: '';
      position: absolute;
      left: 4px;
      top: 0.6em;
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--accent-indigo);
    }
    .reengage-explain-lead {
      font-weight: 600;
      color: var(--text-main);
    }
    .reengage-explain-list b {
      font-weight: 600;
      color: var(--text-main);
      white-space: nowrap;
    }
    .reengage-sample-lead { margin: 14px 0 8px; }

    /* Образец сообщения. Оформлен карточкой мессенджера с явной пометкой
       «Образец»: иначе такой блок читается как лог отправленного. */
    .reengage-sample {
      margin: 0;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
    }
    .reengage-sample-cap {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
      margin-bottom: 10px;
      font-size: 0.74rem;
      color: var(--text-muted);
    }
    .reengage-sample-tag {
      flex: 0 0 auto;
      padding: 3px 9px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(245, 158, 11, 0.35);
      background: rgba(245, 158, 11, 0.12);
      color: var(--accent-amber);
      font-size: 0.68rem;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }
    .reengage-sample-body {
      margin: 0;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      border-top-left-radius: var(--radius-sm);
      background: rgba(99, 102, 241, 0.1);
      font-size: 0.86rem;
      line-height: 1.6;
      color: var(--text-body);
      max-width: 62ch;
    }
    .reengage-sample-body p { margin: 0 0 10px; }
    .reengage-sample-body p:last-child { margin-bottom: 0; }
    /* Строка отписки визуально отделена: её дописывает система, а не модель. */
    .reengage-sample-stop {
      padding-top: 8px;
      border-top: 1px dashed var(--glass-border);
      font-size: 0.8rem;
      color: var(--text-muted);
    }
    .reengage-sample-note {
      margin: 10px 0 0;
      font-size: 0.74rem;
      line-height: 1.5;
      color: var(--text-muted);
    }

    /* Сроки: ровно две колонки на десктопе (2×2 — четыре поля читаются как одна
       группа), одна на телефоне. auto-fit здесь оставлял бы четвёртое поле
       висеть одиноко в новой строке из трёх. */
    .reengage-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 4px 18px;
    }
    .reengage-field { margin-bottom: 10px; }
    .reengage-field .ai-field-hint { margin-bottom: 0; }
    .reengage-lock-note {
      margin: 10px 0 0;
      font-size: 0.8rem;
      line-height: 1.5;
      color: var(--text-muted);
      max-width: 82ch;
    }
    /* Переход с карточки модуля во «Модулях»: тумблер и объяснение живут в
       разных вкладках, кнопка связывает их. */
    .module-card-link {
      margin-top: 10px;
      font-size: 0.78rem;
      padding: 5px 11px;
    }
    /* Заблокированная карточка гасит свои кнопки (.setting-locked), но эта —
       не правка, а переход к объяснению. Клиенту без модуля в тарифе он нужен
       ровно так же: он читает, что покупает. */
    .faq-card.setting-locked .module-card-link {
      pointer-events: auto;
      cursor: pointer;
      opacity: 1;
    }
    /* Подсветка после перехода — тот же приём, что у блока лимитов в «Профиле»:
       только рамка и тень, размеры не меняются. */
    #reengagement-card.is-highlighted {
      border-color: var(--accent-indigo);
      box-shadow: var(--shadow-glow-indigo);
      animation: profileHighlight 2.2s ease-out;
    }
    @media (prefers-reduced-motion: reduce) {
      #reengagement-card.is-highlighted { animation-duration: 0.01ms; }
    }
    @media (max-width: 768px) {
      .reengage-explain { padding: 12px 13px; }
      .reengage-grid { grid-template-columns: 1fr; gap: 0 0; }
      .reengage-sample-body { max-width: 100%; }
      .reengage-explain-list b { white-space: normal; }
      #reengagement-actions .btn { flex: 1 1 100%; justify-content: center; }
    }
    @media (max-width: 480px) {
      .reengage-sample { padding: 10px 11px; }
      .reengage-sample-body { padding: 10px 11px; font-size: 0.84rem; }
      .reengage-explain-list li { padding-left: 16px; }
      .reengage-explain-list li::before { left: 2px; }
    }

    /* Плашки группового замка здесь больше нет: запертый блок не показывается
       вовсе (applyBehaviorSectionLocks), объяснять нечего. */

    .ai-prompt-preview { margin-top: 14px; }
    /* Итоговый промпт относится ко всему разделу, поэтому стоит отдельной
       панелью над карточками — раньше он прятался внутри карточки тона. */
    .ai-prompt-preview--section {
      margin-top: 16px;
      padding: 12px 16px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
    }
    .ai-prompt-preview summary {
      cursor: pointer;
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text-body);
      transition: color 0.2s ease;
    }
    .ai-prompt-preview summary:hover { color: var(--text-main); }
    .ai-prompt-preview-hint {
      margin: 8px 0 0;
      font-size: 0.78rem;
      color: var(--text-muted);
    }
    .ai-prompt-preview-body {
      margin: 10px 0 0;
      padding: 12px;
      max-height: 260px;
      overflow: auto;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: rgba(255, 255, 255, 0.03);
      font-family: var(--font-mono);
      font-size: 0.76rem;
      line-height: 1.55;
      color: var(--text-body);
      white-space: pre-wrap;
      word-break: break-word;
    }
    [data-theme="light"] .ai-prompt-preview-body { background: rgba(241, 245, 249, 0.7); }

    /* === Карточки стратегий Co-Pilot ======================================
       Раньше переиспользовали .faq-card из другой вкладки и выбивались из
       раздела. Теперь — та же стеклянная подложка, что у .ai-setting-card. */
    .copilot-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 12px;
    }
    .copilot-card {
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: rgba(255, 255, 255, 0.03);
      transition: border-color 0.2s ease, transform 0.2s ease;
    }
    [data-theme="light"] .copilot-card { background: rgba(241, 245, 249, 0.6); }
    .copilot-card:hover { border-color: rgba(99, 102, 241, 0.28); transform: translateY(-2px); }
    .copilot-card.is-off { border-style: dashed; }
    .copilot-card-head { display: flex; align-items: center; gap: 10px; }
    .copilot-card-icon { font-size: 1.15rem; line-height: 1; flex: 0 0 auto; }
    .copilot-card-titles { flex: 1; min-width: 0; }
    .copilot-card-title {
      font-family: var(--font-display);
      font-size: 0.98rem;
      font-weight: 600;
      color: var(--text-main);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .copilot-card-key {
      font-family: var(--font-mono);
      font-size: 0.7rem;
      color: var(--text-muted);
      margin-top: 2px;
    }
    .copilot-card-prompt {
      margin: 0;
      font-size: 0.82rem;
      line-height: 1.45;
      color: var(--text-muted);
      flex: 1;
    }
    .copilot-card-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
    .copilot-card-actions .btn { font-size: 0.8rem; padding: 4px 10px; }


    /* === Настройка, запертая администратором платформы =====================
       Заготовка под флаг `is_locked`: элемент виден (застройщик должен понимать,
       что настройка существует), но правке не поддаётся и объясняет почему. */
    .setting-locked {
      position: relative;
      opacity: 0.62;
      filter: saturate(0.6);
    }
    .setting-locked input,
    .setting-locked textarea,
    .setting-locked select,
    .setting-locked button {
      cursor: not-allowed;
      pointer-events: none;
    }
    .setting-lock-badge {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      margin: 8px 0 0;
      padding: 4px 10px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(245, 158, 11, 0.35);
      background: rgba(245, 158, 11, 0.12);
      color: var(--accent-amber);
      font-size: 0.74rem;
      font-weight: 600;
      opacity: 1;
      filter: none;
    }
    .setting-lock-badge svg { flex: 0 0 auto; }

    /* Три причины недоступности модуля различаются цветом бейджа и не кричат:
       янтарь — замок платформы (по умолчанию, см. выше), индиго — вопрос тарифа
       (это предложение, а не запрет), серый — у роли нет полномочия вовсе. */
    .setting-lock-badge.is-plan {
      border-color: rgba(99, 102, 241, 0.35);
      background: rgba(99, 102, 241, 0.12);
      color: var(--accent-indigo);
    }
    .setting-lock-badge.is-role {
      border-color: var(--glass-border);
      background: rgba(128, 142, 163, 0.12);
      color: var(--text-muted);
    }

    /* Карточка модуля под замком: тумблер видно (модуль существует), но он
       не кликается — застройщик понимает, что решение принято платформой. */
    .faq-card.setting-locked { opacity: 0.72; }
    .faq-card.setting-locked .module-toggle-switch { cursor: not-allowed; }
    /* Вне тарифа карточка не гаснет так же сильно, как запертая платформой:
       модуль остаётся предложением, его должно быть видно и хотеться. */
    .faq-card.module-blocked-plan { opacity: 0.86; filter: saturate(0.85); }
    .faq-card.module-blocked-plan .setting-lock-badge { opacity: 1; }
    /* Замок платформы — это запрет, а не предложение: карточка гаснет заметнее
       тарифной и получает янтарную отбивку слева в тон бейджу, чтобы состояние
       читалось до чтения подписи. */
    .faq-card.module-blocked-platform {
      opacity: 0.74;
      filter: saturate(0.68);
      border-color: rgba(245, 158, 11, 0.28);
      box-shadow: inset 3px 0 0 0 rgba(245, 158, 11, 0.5);
    }
    .faq-card.module-blocked-platform .setting-lock-badge { opacity: 1; }
    /* Роли не положено — самое приглушённое состояние: клиент тут ничего не
       решает, поэтому ни цвета, ни отбивки, только серая тень модуля. */
    .faq-card.module-blocked-role {
      opacity: 0.55;
      filter: saturate(0.35);
      border-color: var(--glass-border);
    }
    .faq-card.module-blocked-role .module-card-title { color: var(--text-muted); }
    .faq-card.module-blocked-role .setting-lock-badge { opacity: 0.9; }

    /* === Панель замков платформы (вкладка «Застройщики») ==================== */
    .platform-locks { margin-top: 22px; }
    .platform-locks-group { margin-top: 18px; }
    .platform-locks-group h4 {
      margin: 0 0 10px;
      font-family: var(--font-display);
      font-size: 0.95rem;
      color: var(--text-main);
    }
    .platform-lock-row {
      display: grid;
      grid-template-columns: minmax(0, 1.4fr) auto minmax(0, 1fr);
      gap: 12px;
      align-items: center;
      padding: 12px 14px;
      margin-bottom: 8px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .platform-lock-row:hover {
      border-color: rgba(99, 102, 241, 0.35);
      transform: translateY(-1px);
      box-shadow: var(--shadow-card);
    }
    .platform-lock-row.is-locked {
      border-color: rgba(245, 158, 11, 0.4);
      background: rgba(245, 158, 11, 0.06);
    }
    .platform-lock-title {
      font-weight: 600;
      font-size: 0.9rem;
      color: var(--text-main);
    }
    .platform-lock-key {
      display: block;
      margin-top: 3px;
      font-family: var(--font-mono);
      font-size: 0.72rem;
      color: var(--text-muted);
      word-break: break-all;
    }
    /* Строка модуля несёт два независимых рычага: слева состояние у застройщика,
       справа право менять его самому. Название — посередине, чтобы тумблеры не
       слипались в один орган управления. */
    .platform-lock-row--feature {
      grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 210px);
    }
    /* Принудительное значение сильнее всего остального — строка помечена
       заметнее обычного замка, а тумблер состояния показан замороженным. */
    .platform-lock-row--feature.is-forced {
      border-color: rgba(139, 92, 246, 0.45);
      background: rgba(139, 92, 246, 0.07);
    }
    .platform-lock-power.is-frozen { color: var(--text-muted); }
    .platform-lock-power.is-frozen .module-toggle-switch {
      cursor: not-allowed;
      opacity: 0.55;
    }
    .platform-lock-name { min-width: 0; }
    .platform-lock-desc {
      display: block;
      margin-top: 3px;
      font-size: 0.76rem;
      line-height: 1.45;
      color: var(--text-muted);
    }
    .platform-lock-power {
      color: var(--text-main);
      font-weight: 600;
    }
    .platform-lock-state {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.78rem;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .platform-lock-forced {
      display: flex;
      gap: 8px;
      align-items: center;
    }
    .platform-lock-forced .form-input,
    .platform-lock-forced .form-select { font-size: 0.82rem; padding: 6px 10px; }
    /* Компактный вариант поля: место под собственную стрелку списка сохраняем */
    .platform-lock-forced .form-select { padding-right: 32px; }
    .platform-lock-forced .btn { padding: 6px 12px; font-size: 0.78rem; }
    .platform-lock-forced[hidden] { display: none; }
    /* Последствие замка раздела нужно прочитать ДО клика: у области `section`
       принудительного значения нет, вместо поля ввода — что именно исчезнет
       у застройщика. */
    .platform-lock-hint {
      margin: 0;
      font-size: 0.76rem;
      line-height: 1.45;
      color: var(--text-muted);
    }

    @media (max-width: 768px) {
      .platform-lock-row { grid-template-columns: 1fr; gap: 10px; }
      /* На узком экране порядок читаемости другой: сначала о каком модуле речь,
         потом состояние, потом право застройщика. */
      .platform-lock-row--feature { grid-template-columns: 1fr; }
      .platform-lock-row--feature .platform-lock-name { order: -1; }
      .platform-lock-row--feature .platform-lock-state { white-space: normal; }
      .platform-lock-row--feature .platform-lock-forced .form-select { flex: 1 1 100%; }
      .platform-lock-forced { flex-wrap: wrap; }
      .platform-lock-forced .form-input { flex: 1 1 100%; }
      .platform-lock-forced .btn { width: 100%; justify-content: center; }
    }

    @media (max-width: 768px) {
      .ai-setting-card { padding: 14px; margin-top: 14px; }
      .ai-setting-head { flex-direction: column; align-items: stretch; gap: 10px; }
      .ai-setting-head > .btn { width: 100%; justify-content: center; }
      .ai-setting-card--primary .ai-setting-title { font-size: 1.04rem; }
      .ai-setting-actions .btn { width: 100%; justify-content: center; }
      .ai-prompt-preview-body { max-height: 200px; font-size: 0.72rem; }
      .ai-template-row { align-items: stretch; }
      .ai-template-select { flex: 1 1 100%; }
      .ai-reset-btn { width: 100%; justify-content: center; }
      .copilot-grid { grid-template-columns: 1fr; }
    }
    @media (max-width: 480px) {
      .ai-setting-titles { gap: 9px; }
      .ai-setting-rank { width: 22px; height: 22px; font-size: 0.78rem; }
      .ai-setting-desc { font-size: 0.78rem; }
      .ai-prompt-preview--section { padding: 12px; }
      .copilot-card-actions .btn { flex: 1 1 100%; justify-content: center; }
    }

    /* Структурированный блок контактов в карточке клиента */
    .contacts-card {
      background: rgba(15, 23, 42, 0.6);
      border: 1px solid var(--glass-border);
      border-radius: 10px;
      padding: 14px 16px;
    }
    [data-theme="light"] .contacts-card { background: rgba(241, 245, 249, 0.75); }
    .contacts-card-title {
      font-size: 0.82rem;
      font-weight: 700;
      color: var(--accent-purple-soft);
      margin-bottom: 10px;
    }
    .contacts-grid {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .contacts-row {
      display: grid;
      grid-template-columns: 190px minmax(0, 1fr);
      gap: 10px;
      align-items: center;
    }
    @media (max-width: 620px) {
      .contacts-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
    }
    .contacts-label {
      font-size: 0.78rem;
      color: var(--text-muted);
      font-weight: 600;
    }
    .contacts-value {
      font-weight: 700;
      font-size: 0.95rem;
      color: var(--text-main);
      overflow-wrap: anywhere;
    }
    .contacts-value-name { color: var(--accent-purple-soft); font-size: 1.05rem; }
    .contacts-value-phone { color: var(--accent-emerald-soft); font-size: 1.05rem; }
    .contacts-row .keywords-list { margin: 0; }

    /* Плитки в окне диагностики системы */
    .health-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 12px;
    }
    .health-tile {
      display: flex;
      flex-direction: column;
      gap: 4px;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: rgba(255, 255, 255, 0.04);
      min-width: 0;
    }
    [data-theme="light"] .health-tile { background: rgba(241, 245, 249, 0.7); }
    .health-tile-label {
      font-size: 0.72rem;
      color: var(--text-muted);
      font-weight: 600;
    }
    .health-tile-value {
      font-size: 1.1rem;
      font-weight: 700;
      font-family: var(--font-display);
      color: var(--text-main);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .health-tile.is-error .health-tile-value { color: var(--accent-rose); }
    /* Строка предохранителя («CLOSED · сбоев 0 из 5») в узкую плитку не влезает
       и обрезалась троеточием — ровно та часть, ради которой окно и открывают. */
    .health-tile.is-wide { grid-column: span 2; }
    .health-tile.is-wide .health-tile-value {
      white-space: normal;
      overflow: visible;
      font-size: 0.95rem;
      line-height: 1.3;
    }
    /* Пояснение под плитками: срез, который прислал сервер, зависит от прав, и
       короткая сетка без единого слова читалась бы как поломка окна. */
    .health-note {
      grid-column: 1 / -1;
      margin: 0;
      padding: 10px 14px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      font-size: 0.82rem;
      line-height: 1.45;
      color: var(--text-muted);
      background: rgba(255, 255, 255, 0.02);
    }
    [data-theme="light"] .health-note { background: rgba(241, 245, 249, 0.55); }
    @media (max-width: 540px) {
      .health-tile.is-wide { grid-column: 1 / -1; }
      .health-note { font-size: 0.78rem; padding: 10px 12px; }
    }
    @keyframes pulseGlow {
      0%, 100% { opacity: 1; transform: scale(1); }
      50% { opacity: 0.5; transform: scale(1.3); }
    }

    .brand-title h1 {
      font-size: 1.25rem;
      font-weight: 700;
      /* Название кабинета обрезается многоточием, а не переносится: перенос
         удваивал высоту шапки на планшете. */
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      background: linear-gradient(to right, #ffffff, var(--accent-cyan));
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
    }

    /* Строка подзаголовка держит и подпись версии. `min-height` тут не украшение:
       подпись приходит после ответа /health, и без зарезервированной высоты
       шапка подпрыгивала бы через полсекунды после загрузки.
       `nowrap` и запас в высоте — находка ревью 16.08.2026: при `wrap` в полосе
       ~768-1050px (текст подзаголовка ещё виден, он прячется только с 767px)
       подпись не помещалась рядом и уезжала на вторую строку, увеличивая шапку
       на ~25px — исходная жалоба «всё пляшет» воспроизводилась в этой полосе.
       Резерв 20px тоже был впритык: сама подпись ≈20.3px с рамкой и padding. */
    .brand-title p {
      font-size: 0.75rem;
      color: var(--text-muted);
      display: flex;
      align-items: center;
      flex-wrap: nowrap;
      gap: 8px;
      min-height: 24px;
      /* Длинному подзаголовку сжиматься, подписи — нет: она короткая, и её
         обрезка сделала бы версию нечитаемой ради лишних 20px текста. */
      overflow: hidden;
    }
    .brand-subtitle-text {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      min-width: 0;
    }

    /* ── Правая часть шапки ─────────────────────────────────────────────────
       Блок действий не переносится ВНУТРИ себя никогда (nowrap на всех трёх
       уровнях) и не сжимается: пока он влезает — стоит в одну строку с брендом,
       а как только перестаёт — целиком уезжает следующей строкой, прижатый
       вправо. Именно попытка переносить его по одному ребёнку и давала одинокую
       иконочную кнопку во втором этаже. */
    .header-actions {
      display: flex;
      flex-wrap: nowrap;
      align-items: center;
      justify-content: flex-end;
      gap: 8px;
      flex: 0 0 auto;
      /* Перенесённый на свою строку блок действий прижимается вправо, а не
         повисает слева: space-between у шапки работает только внутри строки. */
      margin-left: auto;
    }
    .header-status-group,
    .header-tools {
      display: flex;
      flex-wrap: nowrap;
      align-items: center;
      gap: 8px;
      flex: 0 0 auto;
    }

    /* Ступени сжатия шапки. Порядок продуман: сначала уходит самое
       необязательное (подпись меню, декоративный подзаголовок), и только на
       телефоне шапка становится двухэтажной. Так на КАЖДОЙ ширине она выглядит
       намеренно, а не «сломавшейся». */
    @media (max-width: 1200px) {
      /* Меню инструментов превращается в иконку: подпись занимала 120px, а
         открывают меню редко. */
      .dropdown-toggle-label { display: none; }
      #demoDropdownToggle { padding-left: 10px; padding-right: 10px; }
      /* Переключатель застройщика есть только у платформенного аккаунта, и он
         самый широкий элемент шапки. С ноутбука он получает СВОЮ строку целиком
         — это единственный способ уместить его, бренд, состояние и инструменты
         не наезжая друг на друга. У застройщика переключателя нет (display:none),
         поэтому его шапка остаётся однострочной. */
    }
    @media (max-width: 1023px) {
      /* Декоративный подзаголовок уходит раньше телефона: он единственный, чьё
         исчезновение никто не заметит, а освобождает он ~250px. Строка
         подзаголовка остаётся — в ней живёт подпись версии с зарезервированной
         высотой, и шапка по-прежнему не дёргается после ответа /health. */
      .brand-subtitle-text { display: none; }
      header { padding: 14px 20px; }
    }
    @media (max-width: 880px) {
      /* Логин показываем, роль — нет: под кем вошёл, видно и без неё, а
         вертикальную высоту чипа роль не меняет. */
      .user-chip .user-chip-role { display: none; }
      .brand-logo { width: 34px; height: 34px; }
    }

    /* ── Выпадающее меню шапки ───────────────────────────────────────────────
       Демо-страницы и Swagger открывают редко, а места в шапке они занимали
       больше, чем ежедневные действия. Прячем их за одну кнопку. */
    .dropdown { position: relative; display: inline-block; }
    .dropdown-arrow { font-size: 0.7rem; opacity: 0.8; }
    .dropdown-menu {
      position: absolute;
      top: calc(100% + 6px);
      right: 0;
      min-width: 210px;
      background: rgba(15, 23, 42, 0.96);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
      padding: 6px;
      display: none;
      z-index: 1000;
      backdrop-filter: blur(12px);
    }
    .dropdown-menu.show { display: block; }
    .dropdown-item {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px 12px;
      color: var(--text-main);
      text-decoration: none;
      font-size: 0.88rem;
      white-space: nowrap;
      border-radius: var(--radius-sm);
      transition: background 0.15s ease;
    }
    .dropdown-item:hover { background: rgba(99, 102, 241, 0.15); }
    .dropdown-divider { height: 1px; background: var(--glass-border); margin: 4px 0; }
    [data-theme="light"] .dropdown-menu {
      background: rgba(255, 255, 255, 0.98);
      border-color: rgba(15, 23, 42, 0.12);
      box-shadow: 0 10px 25px rgba(15, 23, 42, 0.15);
    }
    /* На узком экране меню упирается правым краем в кнопку и не вылезает
       за viewport, но длинные пункты всё равно надо ограничить по ширине. */
    @media (max-width: 768px) {
      .dropdown-menu { min-width: 0; max-width: min(280px, calc(100vw - 32px)); }
    }

    .btn {
      padding: 10px 18px;
      border-radius: var(--radius-sm);
      font-size: 0.875rem;
      font-weight: 600;
      cursor: pointer;
      border: 1px solid transparent;
      transition: all 0.2s ease;
      display: inline-flex;
      align-items: center;
      gap: 8px;
      text-decoration: none;
    }
    .btn:focus-visible, .tab-btn:focus-visible, .search-input:focus-visible,
    .chip:focus-visible, .funnel-step:focus-visible, .form-select:focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }

    .btn-primary {
      background: var(--gradient-indigo-cyan);
      color: white;
      box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
    }

    .btn-primary:hover {
      transform: translateY(-1px);
      box-shadow: 0 6px 18px rgba(99, 102, 241, 0.5);
    }

    .btn-secondary {
      background: rgba(51, 65, 85, 0.6);
      color: var(--text-main);
      border-color: var(--glass-border);
    }

    .btn-secondary:hover {
      background: rgba(71, 85, 105, 0.8);
    }

    .btn-outline {
      background: transparent;
      border-color: var(--accent-purple);
      color: var(--accent-purple-soft);
    }

    .btn-outline:hover {
      background: rgba(139, 92, 246, 0.15);
    }

    .btn-danger {
      background: rgba(244, 63, 94, 0.2);
      color: var(--accent-rose-soft);
      border-color: rgba(244, 63, 94, 0.4);
    }

    /* :not(.is-disabled) — выключенная кнопка (например, удаление
       забронированного объекта) не должна подсвечиваться под курсором. */
    .btn-danger:not(.is-disabled):hover {
      background: rgba(244, 63, 94, 0.4);
    }

    /* Container & Layout */
    /* Сетка кабинета: слева — сайдбар разделов, справа — активная вкладка.
       KPI-карточки и кнопка мобильного меню тянутся во всю ширину. */
    .app-container {
      flex: 1;
      max-width: 1560px;
      width: 100%;
      margin: 0 auto;
      padding: 20px 28px;
      display: grid;
      /* Строки заданы явно: при переключении разделов автоплейсмент мог
         поставить контент в новый ряд и оставить пустую полосу под KPI. */
      grid-template-columns: 236px minmax(0, 1fr);
      /* Три ряда: надвкладочный стек, KPI, рабочая область. Ряд стека добавлен
         после жалобы владельца — без него сводка автоплейсментом уезжала вниз
         страницы и в узкую колонку сайдбара. */
      grid-template-rows: auto auto auto;
      align-items: start;
      /* Контейнер тянется на всю высоту окна (flex: 1), а строки auto по
         умолчанию делят свободное место между собой — из-за этого под KPI
         зияла полоса пустоты, и её размер прыгал при переключении разделов.
         align-content: start прижимает строки к верху, остаток остаётся внизу. */
      align-content: start;
      gap: 20px;
    }
    /* Надвкладочный стек: всегда во всю ширину и всегда первым рядом. */
    .app-container > .app-topstack {
      grid-column: 1 / -1;
      grid-row: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 14px;
    }
    /* Обе карточки скрыты (роль без доступа к расходу, лимит в норме) — стек
       не должен оставлять зазор над KPI. Без поддержки :has() остаётся ровно
       один gap контейнера: направление ошибки безопасное. */
    .app-container > .app-topstack:not(:has(> :not([style*="display: none"]))) { display: none; }
    .app-container > .dashboard-board,
    .app-container > .sidebar-toggle,
    .app-container > .sidebar-scrim { grid-column: 1 / -1; grid-row: 2; }
    .app-container > .tab-content { grid-column: 2; grid-row: 3; min-width: 0; }

    /* Navigation Sidebar */
    .sidebar-nav {
      grid-column: 1;
      grid-row: 3;
      position: sticky;
      /* Шапка липкая и перекрывает первый экран: без её высоты в отступе
         верхние разделы прятались под ней. */
      top: calc(var(--header-h) + 12px);
      align-self: start;
    }

    .tabs-nav {
      display: flex;
      flex-direction: column;
      gap: 4px;
      /* backdrop-filter без базовой заливки не имеет фолбэка: в браузерах без
         поддержки блюра панель осталась бы прозрачной поверх таблиц. */
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      -webkit-backdrop-filter: var(--blur-glass);
      padding: 10px;
      border-radius: var(--radius-lg);
      border: 1px solid var(--glass-border);
      box-shadow: var(--shadow-card);
      max-height: calc(100vh - var(--header-h) - 24px);
      overflow-y: auto;
      overscroll-behavior: contain;
    }

    .pill-tabs {
      position: relative;
    }

    .nav-category + .nav-category { margin-top: 14px; }

    .nav-category-title {
      font-family: var(--font-display);
      font-size: 0.68rem;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text-muted);
      opacity: 0.75;
      padding: 6px 10px;
    }

    .pill-indicator {
      position: absolute;
      /* Координаты целиком задаёт JS через transform: translate(offsetLeft, offsetTop)
         — эти значения уже включают паддинг контейнера, поэтому любое статическое
         смещение здесь сдвинуло бы плашку мимо кнопки. */
      top: 0;
      left: 0;
      height: 0;
      border-radius: var(--radius-md);
      background: linear-gradient(135deg, rgba(99, 102, 241, 0.4), rgba(217, 70, 239, 0.22));
      border: 1px solid rgba(139, 92, 246, 0.45);
      box-shadow: 0 0 16px rgba(139, 92, 246, 0.35);
      transition: transform 0.25s ease, width 0.25s ease, height 0.25s ease;
      z-index: 0;
      pointer-events: none;
    }

    .tab-btn {
      position: relative;
      z-index: 1;
      width: 100%;
      padding: 10px 12px;
      border-radius: var(--radius-md);
      font-weight: 500;
      font-size: 0.875rem;
      text-align: left;
      color: var(--text-body);
      background: transparent;
      border: none;
      cursor: pointer;
      transition: color 0.2s ease, background 0.2s ease, transform 0.2s ease;
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .tab-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      color: var(--text-muted);
      transition: color 0.2s ease, transform 0.2s ease;
    }
    .tab-label {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .tab-btn:hover {
      color: var(--text-main);
      background: var(--glass-hover);
    }
    .tab-btn:hover .tab-icon { color: var(--accent-purple); transform: translateX(1px); }

    .tab-btn.active {
      color: #e0e7ff;
      font-weight: 600;
      background: transparent;
    }
    .tab-btn.active .tab-icon { color: #e0e7ff; }

    /* Мобильная навигация: сайдбар уезжает в выдвижную панель */
    .sidebar-toggle {
      display: none;
      align-items: center;
      gap: 8px;
      padding: 10px 14px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      color: var(--text-main);
      font-size: 0.875rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .sidebar-toggle:hover { background: var(--glass-hover); }
    .sidebar-scrim { display: none; }

    /* Drag-and-Drop Upload Zone */
    .dropzone {
      display: flex;
      align-items: center;
      gap: 16px;
      padding: 24px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      background: rgba(15, 23, 42, 0.4);
      color: var(--text-muted);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    /* Поле выбора файла внутри зоны спрятано, но остаётся в обходе табом —
       см. .file-input-hidden. Обод рисуем на самой зоне: фокус на точке
       размером 1px пользователь не увидит. */
    .dropzone:focus-visible,
    .dropzone:focus-within {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }
    .dropzone:hover, .dropzone.dragover {
      border-color: var(--accent-indigo);
      background: rgba(99, 102, 241, 0.08);
      color: var(--text-main);
    }
    .dropzone-text strong { color: var(--text-main); font-family: var(--font-display); }
    .dropzone-hint { font-size: 0.75rem; margin-top: 2px; }

    /* Tab Content Areas */
    .tab-content {
      display: none;
      animation: fadeIn 0.3s ease;
    }

    .tab-content.active {
      display: block;
    }

    /* Только прозрачность: сдвиг по Y при появлении раздела читался как
       «подпрыгивание» колонки контента относительно сайдбара. */
    @keyframes fadeIn {
      from { opacity: 0; }
      to { opacity: 1; }
    }

    /* Top Stats Bar */
    /* 4 KPI-карточки: фиксированная сетка вместо auto-fit — иначе последняя карточка
       растягивалась на всю строку и блок «плавал» при смене ширины окна. */
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 16px;
      align-items: stretch;
    }
    /* .stats-grid остался ровно у телеметрии (#telemetry-cards): верхний ряд
       KPI-плиток кабинета заменён доской действий (.dashboard-board). */

    .stat-card {
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-lg);
      padding: 16px 18px;
      min-height: 124px;
      min-width: 0;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      gap: 8px;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.05);
      transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
      position: relative;
      overflow: hidden;
    }
    .stat-card::before {
      content: '';
      position: absolute;
      top: 0; left: 0; right: 0;
      height: 3px;
      background: linear-gradient(90deg, #6366f1 0%, #d946ef 50%, #06b6d4 100%);
      opacity: 0.85;
    }
    .stat-card:hover {
      transform: translateY(-3px);
      border-color: rgba(99, 102, 241, 0.45);
      box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4), 0 0 20px rgba(99, 102, 241, 0.18);
    }

    .stat-header {
      display: flex;
      /* Перенос вместо обрезки: при узкой карточке (600px и 1280px в замере)
         «Горячие Лиды» превращалось в «Горячие …», хотя рядом было место
         строкой ниже. Бейдж уходит на вторую строку, заголовок остаётся целым. */
      flex-wrap: wrap;
      justify-content: space-between;
      align-items: center;
      gap: 4px;
      color: var(--text-muted);
      font-size: 0.85rem;
      min-height: 22px;
    }
    /* Заголовок сжимается и обрезается, бейдж — никогда: иначе «Квалификация ≥ 80%» уезжал за карточку */
    .stat-header > span:not(.stat-icon):not(.stat-badge) {
      flex: 1 1 auto;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-weight: 600;
    }
    .stat-header .stat-badge { flex: 0 0 auto; }

    /* Иконка не сжимается и не съезжает: заголовок рядом с ней резиновый, и без
       flex: 0 0 auto длинная подпись сплющивала бы svg. */
    .stat-icon {
      display: inline-flex;
      align-items: center;
      flex: 0 0 auto;
      line-height: 0;
      color: var(--accent-cyan);
      margin-right: 4px;
    }

    /* Значение из числа И слова («~950 диалогов») в общий кегль не помещается
       и упирается в многоточие: у соседних карточек значение — одна короткая
       величина, у запаса ресурса единица измерения стоит в самой цифре.
       Уменьшать общий .stat-value нельзя — там суммы, ради которых он крупный. */
    .stat-value.stat-value-phrase {
      font-size: 1.05rem;
    }

    .stat-value {
      font-size: 1.6rem;
      font-weight: 700;
      font-family: var(--font-display);
      color: var(--text-main);
      line-height: 1.2;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .stat-sub {
      font-size: 0.78rem;
      line-height: 1.35;
      color: var(--text-muted);
      margin-top: 2px;
      /* Без обрезки в две строки: подпись карточки затрат длиннее, и обрезался
         как раз хвост с токенами. Высота карточек держится min-height. */
      word-break: break-word;
    }

    /* Подложка карточки. Раньше рамка рисовалась через border-box-градиент, и
       он спорил с ::before и overflow: hidden — края размывались, скругление
       ломалось. Теперь обычная рамка плюс градиентная заливка. */
    .gradient-border-card {
      background: linear-gradient(145deg, rgba(15, 23, 42, 0.8) 0%, rgba(30, 41, 59, 0.5) 100%);
    }
    [data-theme="light"] .gradient-border-card {
      background: linear-gradient(145deg, rgba(255, 255, 255, 0.9) 0%, rgba(241, 245, 249, 0.8) 100%);
      border-color: rgba(0, 0, 0, 0.08);
    }

    /* Icon Button */
    .icon-btn {
      padding: 6px 8px !important;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }

    .stat-badge {
      font-size: 0.75rem;
      padding: 4px 8px;
      border-radius: var(--radius-pill);
      font-weight: 600;
      /* Бейджи вида «загружено 120, есть ещё» не должны рваться на две строки */
      white-space: nowrap;
    }

    .badge-purple { background: rgba(139, 92, 246, 0.2); color: var(--accent-purple-soft); }
    .badge-green { background: rgba(16, 185, 129, 0.2); color: var(--accent-emerald-soft); }
    .badge-amber { background: rgba(245, 158, 11, 0.2); color: var(--accent-gold-soft); }
    .badge-rose { background: rgba(244, 63, 94, 0.2); color: var(--accent-rose-soft); }
    .badge-gray { background: rgba(148, 163, 184, 0.18); color: var(--text-muted); }

    /* Fast-Path FAQ Section */
    .faq-toolbar {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 16px;
      margin-bottom: 20px;
      flex-wrap: wrap;
    }

    /* Группа кнопок тулбара. Перенос включён НА ВСЕХ ширинах, а не только на
       телефоне: без него flex сжимает сами кнопки, и подписи рвутся на
       несколько строк раньше, чем строка успевает перенестись. Кнопки при
       этом не сжимаем и не переносим их текст — подпись кнопки либо целая,
       либо кнопка уезжает на следующую строку. */
    .faq-toolbar-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
    }
    .faq-toolbar-actions .btn {
      flex: 0 0 auto;
      white-space: nowrap;
    }

    .search-box {
      position: relative;
      flex: 1;
      min-width: 280px;
    }

    .search-input {
      width: 100%;
      padding: 12px 16px 12px 40px;
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      color: var(--text-main);
      font-size: 0.9rem;
      outline: none;
    }

    .search-input:focus {
      border-color: var(--accent-purple);
      box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
    }

    .search-icon {
      position: absolute;
      left: 14px;
      top: 50%;
      transform: translateY(-50%);
      color: var(--text-muted);
    }

    .faq-cards-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
      gap: 20px;
    }

    .faq-card {
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: 14px;
      padding: 20px;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      gap: 16px;
      transition: all 0.2s ease;
      position: relative;
    }

    .faq-card:hover {
      border-color: rgba(99, 102, 241, 0.5);
      transform: translateY(-2px);
      box-shadow: var(--shadow-card);
    }

    .faq-card.compare-selected {
      border-color: var(--accent-indigo);
      box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.35);
    }

    /* Карточка каталога: строки данных соседей должны стоять на одной линии.
       Раньше карточка растягивалась по justify-content: space-between, и весь
       запас высоты растекался по промежуткам между блоками — у объекта без
       заметок цена и кнопки уезжали вниз относительно соседа с двумя заметками.
       Теперь запас забирает ровно один блок — .property-card-body (данные +
       заметки), а цена, кнопки и чекбокс прижаты к низу и совпадают по всему
       ряду грида. Карточки в ряду и так одной высоты: align-items: stretch. */
    .faq-card.property-card { justify-content: flex-start; }
    .property-card-body {
      flex: 1 1 auto;
      min-height: 0;
    }

    /* Строка действий карточки каталога: 3D-тур, чертёж, правка, КП, удаление.
       Кнопки переносятся на новую строку — на узком экране их до пяти. */
    .property-card-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      font-size: 0.8rem;
      margin-top: 10px;
    }

    /* Удаление недоступно (объект забронирован или продан): кнопка остаётся на
       месте, но гасится и не притворяется рабочей. Атрибут disabled НЕ ставим
       намеренно — иначе на тач-экране, где нет ховера и подсказки-title,
       причину отказа узнать было бы неоткуда; клик показывает её тостом. */
    .property-delete-btn.is-disabled {
      opacity: 0.45;
      cursor: not-allowed;
      filter: grayscale(0.6);
    }
    .property-delete-btn.is-disabled:hover {
      transform: none;
      box-shadow: none;
    }

    /* ======================================================================
       Материалы в карточке каталога: миниатюра планировки + значки того, что
       можно отправить клиенту.

       Полоса рисуется у КАЖДОЙ карточки, в том числе у квартиры без единой
       картинки, и высота у неё фиксированная. Иначе соседи по ряду сетки
       разъезжались бы по вертикали на высоту превью, и цена с кнопками стояли
       бы на разной высоте по всему экрану.
       ====================================================================== */
    .property-media-strip {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin-top: 10px;
    }
    .property-media-thumb {
      flex: 0 0 auto;
      position: relative;
      width: 74px;
      height: 74px;
      padding: 0;
      overflow: hidden;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .property-media-thumb img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }
    button.property-media-thumb:hover {
      border-color: var(--accent-indigo);
      box-shadow: var(--shadow-glow-indigo);
    }
    button.property-media-thumb:active { transform: scale(0.97); }
    button.property-media-thumb:focus-visible {
      outline: 2px solid var(--accent-indigo);
      outline-offset: 2px;
    }
    /* Планировки нет либо ссылка застройщика мертва: место остаётся занятым
       ровно тем же размером, а рамка пунктиром честно говорит, что кадра нет. */
    .property-media-thumb.is-empty,
    .property-media-thumb.is-broken {
      color: var(--text-muted);
      border-style: dashed;
      background: transparent;
    }
    .property-media-thumb.is-empty { cursor: default; }
    /* Битое превью кнопку НЕ выключает: у квартиры обычно несколько планировок,
       и одна мёртвая ссылка не повод закрыть доступ к остальным. */
    .property-media-thumb.is-broken::after {
      content: '🚫';
      font-size: 1.1rem;
      opacity: 0.7;
    }
    .property-media-thumb.is-broken .property-media-more { display: none; }
    .property-media-more {
      position: absolute;
      right: 3px;
      bottom: 3px;
      padding: 1px 5px;
      font-family: var(--font-display);
      font-size: 0.68rem;
      font-weight: 700;
      color: var(--text-main);
      /* Стеклянная плашка, а не своя заливка: планировки бывают и на белом, и на
         тёмном фоне, а токен карточки уже согласован с цветом текста в обеих
         темах — фиксированный цвет читался бы только в одной из них. */
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      border-radius: var(--radius-pill);
    }

    .property-media-flags {
      flex: 1 1 auto;
      min-width: 0;
      display: flex;
      flex-wrap: wrap;
      align-content: flex-start;
      gap: 4px;
    }
    .property-media-flag {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      padding: 2px 8px;
      font-size: 0.7rem;
      line-height: 1.5;
      color: var(--text-body);
      background: var(--glass-hover);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-pill);
      white-space: nowrap;
    }
    .property-media-flag.is-empty {
      color: var(--text-muted);
      background: transparent;
      border-style: dashed;
    }

    /* ======================================================================
       Цена со скидкой застройщика. Блока нет вовсе, когда акции нет: ноль или
       прочерк владелец прочитал бы как «акция есть, но пустая».
       ====================================================================== */
    .property-price-block { margin-top: 8px; }
    .property-price-row {
      display: flex;
      align-items: baseline;
      flex-wrap: wrap;
      gap: 8px;
    }
    .property-price-old {
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-muted);
      text-decoration: line-through;
      text-decoration-thickness: 1px;
    }
    .property-price-new {
      font-size: 1.3rem;
      font-weight: 800;
      color: var(--accent-green);
    }
    /* Условие акции — золотом, как остальные акции кабинета, и рядом с суммой:
       «-5%» без оговорки «при 100% оплате» это несуществующее предложение. */
    .property-discount-note {
      margin-top: 4px;
      padding: 3px 8px;
      display: inline-block;
      font-size: 0.72rem;
      line-height: 1.4;
      color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.12);
      border: 1px solid rgba(245, 158, 11, 0.3);
      border-radius: var(--radius-sm);
    }

    /* Обложка жилого комплекса в навигации по каталогу. Своя СТРОКА под узлом
       объекта, а не значок внутри строки: панель структуры узкая (~270px), и в
       одном ряду со счётчиком и кнопками правки обложка срезала название ЖК до
       одной буквы. Ширину здесь не занимает никто, кроме самой обложки.

       Стили привязаны к обложке, а не к разметке дерева: навигация переезжает
       на крошки с выпадающими списками, и кадр должен пережить смену разметки —
       новой навигации хватит вставить тот же элемент. */
    .catalog-project-cover {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      max-width: 100%;
      margin: 2px 0 4px 34px;
      padding: 3px 10px 3px 3px;
      cursor: pointer;
      color: var(--text-muted);
      background: transparent;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-pill);
      transition: all 0.2s ease;
    }
    .catalog-project-cover img {
      flex: 0 0 auto;
      width: 22px;
      height: 22px;
      object-fit: cover;
      display: block;
      border-radius: var(--radius-pill);
    }
    .catalog-project-cover-text {
      font-size: 0.72rem;
      font-weight: 600;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .catalog-project-cover:hover {
      color: var(--text-main);
      border-color: var(--accent-indigo);
      background: var(--glass-hover);
    }
    .catalog-project-cover:active { transform: scale(0.97); }
    .catalog-project-cover:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 1px; }
    /* Мёртвая ссылка на CDN застройщика не должна оставлять пустой квадрат:
       кадр исчезает, подпись со счётчиком остаётся — галерея-то работает. */
    .catalog-project-cover.is-broken img { display: none; }
    .catalog-project-cover.is-broken { padding-left: 10px; }

    /* Та же обложка — но в карточке объекта, а не в узкой панели дерева.
       Места хватает, поэтому кадр крупнее и без отступа под уголок дерева. */
    .catalog-node-media .catalog-project-cover {
      margin: 0;
      padding: 6px 14px 6px 6px;
    }
    .catalog-node-media .catalog-project-cover img {
      width: 40px;
      height: 40px;
      border-radius: var(--radius-sm);
    }
    .catalog-node-media .catalog-project-cover-text { font-size: 0.82rem; }
    /* Пустая строка законна ТОЛЬКО здесь: карточку открывают для правки
       конкретного объекта, а не для навигации по каталогу (см. комментарий
       у renderCatalogNodeMedia в admin.js). */
    .catalog-node-media-empty {
      margin: 0;
      color: var(--text-muted);
      font-size: 0.85rem;
    }

    /* Привязка ЖК к наш.дом.рф (Parser): строка статуса + кнопка под узлом
       объекта, той же ширины и с тем же отступом, что и обложка выше. Тон
       левой полоски повторяет словарь feed-source-card (is-ok/is-warn/is-error/
       is-idle), но своя разметка — карточка источника рассчитана на грид с
       тремя колонками, а тут одна узкая строка внутри дерева. */
    .catalog-ndrf-row {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 8px 10px;
      margin: 2px 0 2px 34px;
      padding: 4px 10px;
      border-left: 3px solid var(--text-muted);
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      font-size: 0.76rem;
    }
    .catalog-ndrf-row.is-ok { border-left-color: var(--accent-emerald); }
    .catalog-ndrf-row.is-warn { border-left-color: var(--accent-amber); }
    .catalog-ndrf-row.is-error { border-left-color: var(--accent-rose); }
    .catalog-ndrf-row.is-idle { border-left-color: var(--accent-indigo); }
    .catalog-ndrf-status { flex: 1 1 auto; min-width: 120px; color: var(--text-body); overflow-wrap: anywhere; }
    .catalog-ndrf-row.is-error .catalog-ndrf-status { color: var(--accent-rose); }
    .catalog-ndrf-busy {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .catalog-ndrf-btn {
      padding: 4px 10px;
      font-size: 0.74rem;
      white-space: nowrap;
    }
    .catalog-ndrf-candidates {
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: 6px;
      margin: 2px 0 6px 34px;
      padding: 0;
    }
    .catalog-ndrf-candidate {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      padding: 6px 10px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .catalog-ndrf-candidate:hover { background: var(--glass-hover); border-color: var(--accent-indigo); }
    .catalog-ndrf-candidate-main { min-width: 0; }
    .catalog-ndrf-candidate-name { font-weight: 600; color: var(--text-main); font-size: 0.8rem; }
    .catalog-ndrf-candidate-meta { color: var(--text-muted); font-size: 0.74rem; margin-top: 1px; }
    /* Предупреждение «это очередь, а не весь ЖК» — предупреждающим тоном, тем
       же токеном, что и остальные оговорки в этом блоке. */
    .catalog-ndrf-candidate-note { color: var(--accent-amber); font-size: 0.72rem; margin-top: 3px; line-height: 1.35; }
    .catalog-ndrf-candidate-link { color: var(--accent-cyan); font-size: 0.74rem; text-decoration: none; }
    .catalog-ndrf-candidate-link:hover { text-decoration: underline; }
    .catalog-ndrf-select { flex: 0 0 auto; padding: 4px 12px; font-size: 0.74rem; }

    @media (prefers-reduced-motion: reduce) {
      .catalog-ndrf-row, .catalog-ndrf-candidate, .catalog-ndrf-btn { transition-duration: 0.01ms; }
    }

    /* ======================================================================
       Галерея материалов (#property-media-modal): крупный кадр, счётчик,
       миниатюры. Одно окно и на квартиру, и на жилой комплекс.
       ====================================================================== */
    /* Шире обычной модалки: чертёж на 580px нечитаем. Отступы между блоками
       задаёт gap самой карточки (.modal-card — flex-колонка), поэтому у
       внутренних блоков margin обнулён — иначе к 20px gap прибавлялись бы
       собственные отступы и окно не влезало бы в 85vh. */
    /* Селектор двойной (.modal-card.media-gallery-card) намеренно: базовые
       .modal-card { max-width: 580px; gap: 20px } объявлены НИЖЕ по файлу, и при
       равной специфичности выигрывали бы они — окно осталось бы узким. */
    .modal-card.media-gallery-card {
      max-width: 860px;
      gap: 12px;
    }
    .media-gallery-subtitle {
      margin: -6px 0 0;
      font-size: 0.8rem;
      line-height: 1.4;
      color: var(--text-muted);
    }
    .media-gallery-frame {
      position: relative;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 260px;
      max-height: 56vh;
      padding: 10px;
      overflow: hidden;
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
    }
    .media-gallery-frame img {
      max-width: 100%;
      max-height: 52vh;
      object-fit: contain;
      border-radius: var(--radius-sm);
    }
    /* Битую ссылку объясняем словами и оставляем возможность её проверить —
       пустой прямоугольник читался бы как поломка кабинета. */
    .media-gallery-broken { display: none; }
    .media-gallery-frame.is-broken .media-gallery-broken {
      display: block;
      max-width: 340px;
      text-align: center;
      font-size: 0.85rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .media-gallery-frame.is-broken .media-gallery-broken a {
      display: block;
      margin-top: 6px;
      color: var(--accent-cyan);
    }
    .media-gallery-caption {
      margin-top: 8px;
      font-size: 0.82rem;
      color: var(--text-body);
    }
    /* PDF и 3D-тур — ссылки, а не картинки: карточкой, а не битым <img>. */
    .media-gallery-linkcard {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 8px;
      max-width: 100%;
      text-align: center;
      color: var(--text-body);
    }
    .media-gallery-linkicon { font-size: 2rem; }
    .media-gallery-linkurl {
      max-width: 100%;
      font-family: var(--font-mono);
      font-size: 0.72rem;
      color: var(--text-muted);
      overflow-wrap: anywhere;
    }
    .media-gallery-empty {
      padding: 40px 20px;
      text-align: center;
      color: var(--text-muted);
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
    }

    .media-gallery-bar {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 8px;
    }
    .media-gallery-counter {
      font-family: var(--font-display);
      font-size: 0.82rem;
      font-weight: 600;
      color: var(--text-body);
      min-width: 68px;
      text-align: center;
    }
    .media-gallery-nav:disabled { opacity: 0.4; cursor: not-allowed; }
    .media-gallery-original { text-decoration: none; padding: 6px 12px; font-size: 0.8rem; }

    .media-gallery-thumbs {
      display: flex;
      flex-wrap: nowrap;
      gap: 6px;
      overflow-x: auto;
      overscroll-behavior-x: contain;
      padding-bottom: 4px;
      flex: 0 0 auto;
    }
    .media-gallery-thumb {
      flex: 0 0 auto;
      width: 62px;
      height: 62px;
      padding: 0;
      overflow: hidden;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      color: var(--text-muted);
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .media-gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .media-gallery-thumb:hover { border-color: var(--accent-indigo); }
    .media-gallery-thumb:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 1px; }
    .media-gallery-thumb.is-active {
      border-color: var(--accent-indigo);
      box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.35);
    }
    .media-gallery-thumb.is-broken::after { content: '🚫'; font-size: 0.9rem; opacity: 0.7; }
    .media-gallery-thumbicon { font-size: 1.4rem; }

    /* Мобильный экран: кадр ниже, миниатюры мельче, полоса материалов в карточке
       остаётся строкой — превью там и так 74px, в одну колонку оно влезает. */
    @media (max-width: 768px) {
      .media-gallery-frame { min-height: 200px; max-height: 44vh; }
      .media-gallery-frame img { max-height: 40vh; }
      .media-gallery-thumb { width: 52px; height: 52px; }
    }
    @media (max-width: 480px) {
      .property-media-thumb { width: 62px; height: 62px; }
      .property-media-flag { font-size: 0.66rem; padding: 2px 6px; }
      .media-gallery-frame { min-height: 170px; }
      .media-gallery-counter { min-width: 58px; font-size: 0.76rem; }
      /* «Открыть оригинал» уезжает на свою строку — иначе на 360px она вытесняет
         стрелки листания за край. */
      .media-gallery-original { flex: 1 1 100%; text-align: center; }
      .media-gallery-thumb { width: 46px; height: 46px; }
    }

    .compare-checkbox-row {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.8rem;
      color: var(--text-muted);
      margin-top: 8px;
    }
    .compare-checkbox-row input[type="checkbox"] {
      width: 16px;
      height: 16px;
      accent-color: var(--accent-indigo);
      cursor: pointer;
    }

    /* === Постраничный вывод каталога =====================================
       Панель навигации по экранам каталога. Стоит дважды — над сеткой и под
       ней, поэтому она сознательно тихая: стеклянная подложка, без градиентов
       и без акцентного цвета. Заметной на этой вкладке остаётся красная панель
       удаления, а не листалка.
       ==================================================================== */
    .catalog-pager {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 10px 16px;
      margin-top: 16px;
      padding: 10px 14px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
    }
    .catalog-pager[hidden] { display: none; }
    .catalog-pager-bottom { margin-bottom: 8px; }
    .catalog-pager-info {
      font-size: 0.85rem;
      color: var(--text-muted);
      min-width: 0;
    }
    .catalog-pager-info strong { color: var(--text-main); font-weight: 600; }
    .catalog-pager-controls {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }
    .catalog-pager-page {
      font-size: 0.85rem;
      color: var(--text-body);
      padding: 0 4px;
      white-space: nowrap;
    }
    .catalog-pager-page strong { color: var(--text-main); font-family: var(--font-display); }
    .catalog-pager-btn {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 6px 12px;
      font-size: 0.82rem;
      transition: all 0.2s ease;
    }
    .catalog-pager-btn.is-icon { padding: 6px 9px; }
    .catalog-pager-btn:disabled {
      opacity: 0.45;
      cursor: not-allowed;
      transform: none;
    }
    /* Сколько карточек на экране — личное предпочтение владельца, а не предел
       сервера: селектор стоит вплотную к листалке, потому что управляет ровно
       ею. Пачки удаления режутся по своему потолку и на этот выбор не смотрят. */
    .catalog-pager-size {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.82rem;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .catalog-pager-size select {
      padding: 5px 8px;
      font-size: 0.82rem;
      color: var(--text-body);
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .catalog-pager-size select:hover { border-color: var(--accent-indigo); color: var(--text-main); }
    .catalog-pager-size select:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 1px; }

    @media (max-width: 768px) {
      .catalog-pager { flex-direction: column; align-items: stretch; }
      .catalog-pager-info { text-align: center; }
      .catalog-pager-controls { justify-content: center; }
      .catalog-pager-size { justify-content: center; }
    }
    @media (max-width: 480px) {
      /* Порядок на узком экране: «Назад» и «Дальше» по краям, номер экрана
         посередине, прыжки к началу и концу — по бокам. Ряд остаётся одной
         строкой: в столбик пять кнопок заняли бы пол-экрана. */
      .catalog-pager-controls { gap: 6px; flex-wrap: nowrap; }
      .catalog-pager-btn { padding: 6px 8px; font-size: 0.76rem; }
      .catalog-pager-page { font-size: 0.76rem; padding: 0; }
    }

    /* === Структура каталога: город → объект → корпус =====================
       Дерево живёт в накладке (#catalog-structure-modal), а не колонкой слева
       от карточек. Колонка стоила 300px на любой ширине окна — ровно один
       отнятый столбец карточек — при трёх-четырёх видимых узлах. Постоянная
       навигация переехала на строку крошек (.catalog-crumbs ниже).
       ==================================================================== */
    /* Окно структуры шире обычной модалки: имя ЖК плюс срок сдачи плюс кнопки
       правки в 580px не помещаются, и название резалось до одной буквы. */
    .modal-card.catalog-structure-card {
      max-width: 720px;
      gap: 14px;
    }

    .catalog-tree {
      display: flex;
      flex-direction: column;
      min-height: 0;
      padding: 14px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      box-shadow: var(--shadow-card);
    }
    .catalog-tree-head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 10px;
    }
    .catalog-tree-head-text { min-width: 0; }
    .catalog-tree-title {
      margin: 0;
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .catalog-tree-summary {
      margin: 4px 0 0;
      font-size: 0.78rem;
      line-height: 1.35;
      color: var(--text-muted);
    }
    .catalog-tree-body {
      margin-top: 12px;
      max-height: 62vh;
      overflow-y: auto;
      overscroll-behavior: contain;
    }
    .catalog-tree-list { list-style: none; margin: 0; padding: 0; }
    /* Свёрнутый уровень остаётся в DOM и лишь прячется: раскрытие узла не
       перерисовывает дерево, поэтому прокрутка и фокус остаются на месте. */
    .catalog-tree-list[hidden] { display: none; }
    .catalog-tree-list .catalog-tree-list { padding-left: 12px; }
    /* Тонкая направляющая вместо отступа в пустоту: по ней видно, что корпус
       принадлежит объекту, а объект — городу. */
    .catalog-tree-list .catalog-tree-list { border-left: 1px solid var(--glass-border); margin-left: 8px; }

    .catalog-tree-row {
      display: flex;
      align-items: center;
      gap: 2px;
      margin: 2px 0;
    }
    .catalog-tree-twist {
      flex: 0 0 auto;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 22px;
      height: 26px;
      padding: 0;
      cursor: pointer;
      color: var(--text-muted);
      background: transparent;
      border: none;
      border-radius: var(--radius-sm);
      transition: all 0.2s ease;
    }
    .catalog-tree-twist:hover { color: var(--text-main); background: var(--glass-hover); }
    .catalog-tree-twist svg { transition: transform 0.2s ease; transform: rotate(-90deg); }
    .catalog-tree-twist[aria-expanded="true"] svg { transform: rotate(0deg); }
    .catalog-tree-twist.is-leaf { cursor: default; opacity: 0; pointer-events: none; }

    .catalog-tree-node {
      flex: 1 1 auto;
      min-width: 0;
      display: flex;
      align-items: baseline;
      gap: 8px;
      padding: 6px 10px;
      cursor: pointer;
      text-align: left;
      color: var(--text-body);
      background: transparent;
      border: 1px solid transparent;
      border-radius: var(--radius-sm);
      font-family: var(--font-body);
      font-size: 0.85rem;
      transition: all 0.2s ease;
    }
    .catalog-tree-node:hover { background: var(--glass-hover); color: var(--text-main); }
    .catalog-tree-node:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 1px; }
    .catalog-tree-node.is-selected {
      color: var(--text-main);
      border-color: var(--accent-indigo);
      background: rgba(99, 102, 241, 0.14);
    }
    .catalog-tree-node-main { min-width: 0; flex: 1 1 auto; }
    .catalog-tree-node-name {
      display: block;
      font-weight: 600;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .catalog-tree-node-meta {
      display: block;
      margin-top: 2px;
      font-size: 0.72rem;
      line-height: 1.3;
      color: var(--text-muted);
    }
    .catalog-tree-count {
      flex: 0 0 auto;
      font-family: var(--font-display);
      font-size: 0.75rem;
      font-weight: 600;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .catalog-tree-node.is-selected .catalog-tree-count { color: var(--text-body); }
    .catalog-tree-free { color: var(--accent-emerald); }

    /* Правка и удаление узла. На мыши кнопки проявляются при наведении на строку
       (иначе два значка у каждой из сорока строк превращают дерево в панель
       инструментов), но появляются мгновенно и не двигают текст: место под них
       зарезервировано всегда. Клавиатуре они видны по фокусу — :focus-within. */
    .catalog-tree-actions {
      flex: 0 0 auto;
      display: inline-flex;
      align-items: center;
      gap: 2px;
      margin-left: 2px;
      opacity: 0;
      transition: opacity 0.2s ease;
    }
    .catalog-tree-row:hover .catalog-tree-actions,
    .catalog-tree-row:focus-within .catalog-tree-actions { opacity: 1; }
    .catalog-tree-act {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 24px;
      height: 24px;
      padding: 0;
      cursor: pointer;
      color: var(--text-muted);
      background: transparent;
      border: 1px solid transparent;
      border-radius: var(--radius-sm);
      transition: all 0.2s ease;
    }
    .catalog-tree-act:hover {
      color: var(--text-main);
      background: var(--glass-hover);
      border-color: var(--glass-border);
    }
    .catalog-tree-act.is-danger:hover { color: var(--accent-rose); border-color: var(--accent-rose); }
    .catalog-tree-act:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 1px; }

    /* Уровни различаются размером подписи, а не цветом: город крупнее объекта,
       объект крупнее корпуса. */
    .catalog-tree-node.level-root .catalog-tree-node-name { font-size: 0.9rem; }
    /* Регион — уровень над городом: тот же шрифт заголовков, что и у города,
       но чуть крупнее, чтобы верх дерева читался как верх. */
    .catalog-tree-node.level-region .catalog-tree-node-name {
      font-family: var(--font-display);
      font-size: 0.88rem;
    }
    .catalog-tree-node.level-city .catalog-tree-node-name { font-family: var(--font-display); }
    .catalog-tree-node.level-building .catalog-tree-node-name { font-weight: 500; }
    /* «Без объекта» — часть каталога, выпавшая из иерархии. Янтарная метка
       говорит «сюда стоит заглянуть», но узел остаётся обычным и кликабельным. */
    .catalog-tree-node.is-orphan .catalog-tree-node-name { color: var(--accent-amber); }

    .catalog-tree-empty {
      padding: 18px 12px;
      text-align: center;
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .catalog-tree-empty strong { display: block; margin-bottom: 6px; color: var(--text-body); }

    /* === Крошки каталога ================================================
       «Весь каталог › Москва ▾ › ЖК Северный ▾ › Корпус 2 ▾» — постоянная
       навигация вместо панели дерева. Занимает одну строку и не отнимает
       ширину у сетки карточек.

       Высота ЗАФИКСИРОВАНА (min-height): до прихода структуры внутри стоит
       скелетон той же высоты, поэтому сетка ниже не сдвигается ни разу за
       загрузку — прежняя панель дерева прыгала именно на этом.
       ==================================================================== */
    .catalog-crumbs {
      position: relative;   /* система координат выпадающего списка */
      /* Слой строки крошек целиком. Нужен именно здесь, а не у меню:
         backdrop-filter ниже создаёт собственный stacking context, поэтому
         z-index:60 у .catalog-crumbs-menu действует ТОЛЬКО внутри этой строки,
         а сама строка без z-index красится как z-index:0 и уезжает под
         карточки (.faq-card, position:relative) и панели .toolbar-filters,
         лежащие ниже по DOM.
         Число 40 выбрано между ними и шапкой: сиблинги каталога позиционированы
         без z-index (эффективный 0) — 40 их перекрывает; sticky header имеет
         z-index:100, и открытое меню при скролле уходит ПОД шапку, а не поверх
         неё, что и требуется. Модалки проекта живут выше 100 и тоже не задеты. */
      z-index: 40;
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 8px 14px;
      min-height: 54px;
      margin-top: 16px;
      padding: 8px 12px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-indigo);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      font-size: 0.85rem;
      color: var(--text-body);
    }
    .catalog-crumbs-path {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 2px;
      min-width: 0;
      flex: 1 1 auto;
    }
    .catalog-crumbs-side {
      display: flex;
      align-items: center;
      gap: 8px;
      flex: 0 0 auto;
    }
    .catalog-crumbs-counts {
      font-size: 0.78rem;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .catalog-crumbs-counts strong { color: var(--text-main); font-weight: 600; }
    .catalog-crumbs-reset {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 30px;
      height: 30px;
      padding: 0;
      cursor: pointer;
      color: var(--text-muted);
      background: transparent;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      transition: all 0.2s ease;
    }
    .catalog-crumbs-reset:hover { color: var(--accent-rose); border-color: var(--accent-rose); }
    .catalog-crumbs-reset[hidden] { display: none; }

    /* Сегмент крошки. Имени отдана вся свободная ширина: счётчики уехали
       вправо в общую подпись строки и в подсказки пунктов списка — в прежнем
       дереве счётчик «доступно 972» съедал 86px и резал «Новаторию» до «Нов…». */
    .catalog-crumb {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      max-width: 100%;
      padding: 5px 10px;
      cursor: pointer;
      font-family: var(--font-body);
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text-body);
      background: transparent;
      border: 1px solid transparent;
      border-radius: var(--radius-sm);
      transition: all 0.2s ease;
    }
    .catalog-crumb:hover { color: var(--text-main); background: var(--glass-hover); border-color: var(--glass-border); }
    .catalog-crumb:active { transform: scale(0.97); }
    .catalog-crumb:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 1px; }
    .catalog-crumb.is-current { color: var(--text-main); border-color: var(--glass-border); background: var(--glass-hover); }
    /* Приглашение спуститься на уровень ниже («Все объекты», «Все корпуса») —
       тише выбранных узлов: это не то, что показано, а то, что можно выбрать. */
    .catalog-crumb.is-placeholder { color: var(--text-muted); font-weight: 500; }
    .catalog-crumb-name {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .catalog-crumb-caret { flex: 0 0 auto; opacity: 0.7; transition: transform 0.2s ease; }
    .catalog-crumb[aria-expanded="true"] .catalog-crumb-caret { transform: rotate(180deg); }
    .catalog-crumbs-sep {
      flex: 0 0 auto;
      color: var(--text-muted);
      opacity: 0.6;
      user-select: none;
    }
    /* Скелетон постоянной высоты: 30px — ровно высота сегмента крошки. */
    .catalog-crumb-skeleton {
      display: inline-block;
      height: 30px;
      border-radius: var(--radius-sm);
    }

    /* Выпадающий список братьев уровня. Полупрозрачным его делать нельзя:
       под ним лежат карточки каталога, и текст читался бы поверх фотографий. */
    .catalog-crumbs-menu {
      position: absolute;
      top: calc(100% - 4px);
      left: 12px;
      z-index: 60;
      width: min(420px, calc(100vw - 48px));
      max-height: 60vh;
      overflow-y: auto;
      overscroll-behavior: contain;
      padding: 6px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--bg-deep);
      box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
      animation: fadeInUp 0.18s ease;
    }
    .catalog-crumbs-menu[hidden] { display: none; }
    .catalog-crumb-item {
      display: flex;
      align-items: center;
      gap: 2px;
      border-radius: var(--radius-sm);
    }
    .catalog-crumb-item:hover { background: var(--glass-hover); }
    .catalog-crumb-pick {
      flex: 1 1 auto;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 2px;
      padding: 8px 10px;
      text-align: left;
      cursor: pointer;
      color: var(--text-body);
      background: transparent;
      border: none;
      border-radius: var(--radius-sm);
      transition: all 0.2s ease;
    }
    .catalog-crumb-pick:hover { color: var(--text-main); }
    .catalog-crumb-pick:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: -2px; }
    .catalog-crumb-pick-name {
      font-size: 0.86rem;
      font-weight: 600;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    /* Подпись переносится, а не обрезается многоточием: в ней стоит счётчик
       «доступно N», и обрезанное «досту…» не отвечает ни на один вопрос. */
    .catalog-crumb-pick-meta {
      font-size: 0.74rem;
      line-height: 1.35;
      color: var(--text-muted);
      overflow-wrap: anywhere;
    }
    .catalog-crumb-item.is-selected { background: var(--glass-hover); }
    .catalog-crumb-item.is-selected .catalog-crumb-pick-name { color: var(--accent-indigo); }
    .catalog-crumb-item.is-orphan .catalog-crumb-pick-name { color: var(--accent-amber); }
    .catalog-crumb-free { color: var(--accent-emerald); }
    /* Обложка ЖК — своей строкой под пунктом, как и в дереве: в один ряд с
       именем она отняла бы у него ширину. Отступ дерева (34px) здесь лишний. */
    .catalog-crumb-empty {
      padding: 14px 12px;
      font-size: 0.8rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    /* Кнопки правки и удаления в списке видны всегда: список открывают ради
       выбора, и появление кнопок только по наведению их бы спрятало. */
    .catalog-crumb-item .catalog-tree-actions { opacity: 1; padding-right: 6px; }

    /* Форма правки узла: два поля в ряд на широком экране, одно — на узком. */
    .catalog-node-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 0 12px;
    }
    /* Значение из чужой выгрузки в выпадающем списке: его владелец не выбирал,
       и выглядеть как равноправный вариант оно не должно. Часть браузеров
       оформление <option> игнорирует — смысл несёт подпись самого пункта
       («Как в выгрузке: …») и заметка ниже, цвет только помогает. */
    .catalog-node-foreign-option { color: var(--accent-amber); font-style: italic; }
    .catalog-node-foreign-note {
      margin: 0 0 12px;
      padding: 8px 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-amber);
      background: var(--glass-hover);
      font-size: 0.8rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .catalog-node-foreign-note[hidden] { display: none; }

    /* Прежние имена узла (docs/plans/PLAN_CATALOG_NODE_ALIASES.md): их узнаёт
       повторная загрузка старого файла, поэтому список не спрятан — владелец
       должен видеть, по каким написаниям узел ещё найдётся, и снять любое, если
       переименование было ошибкой. */
    .catalog-node-aliases-group[hidden] { display: none; }
    .catalog-node-aliases-list {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin: 4px 0 0;
    }
    .catalog-node-alias-pill {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 6px 8px 6px 12px;
      border-radius: var(--radius-pill);
      border: 1px solid var(--glass-border);
      background: var(--surface-tint-chip);
      color: var(--text-body);
      font-size: 0.8rem;
      line-height: 1.3;
      max-width: 100%;
    }
    .catalog-node-alias-pill span {
      overflow-wrap: anywhere;
    }
    .catalog-node-alias-remove {
      flex: 0 0 auto;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 18px;
      height: 18px;
      padding: 0;
      border: none;
      border-radius: 999px;
      background: transparent;
      color: var(--text-muted);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .catalog-node-alias-remove:hover {
      background: rgba(244, 63, 94, 0.18);
      color: var(--accent-rose);
    }
    .catalog-node-alias-remove:focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 1px;
    }
    .catalog-node-aliases-empty {
      margin: 4px 0 0;
      font-size: 0.8rem;
      color: var(--text-muted);
      font-style: italic;
    }

    /* Подсказки под полями «до метро» не должны прилипать к следующей строке
       сетки: без отступа подпись «Станция метро» садилась на хвост района. */
    .catalog-node-grid .form-group .form-hint { margin: 4px 0 0; }
    .catalog-node-metro-fields { margin-top: 8px; }

    /* Предпросмотр «до метро»: владелец видит ГОТОВУЮ фразу сервера, а не два
       поля, из которых надо мысленно собрать реплику бота. Полоса слева
       изумрудная, когда данные есть, и приглушённая, когда поля пусты, —
       пустота здесь не ошибка, а честное «данных нет». */
    .catalog-node-metro-preview {
      margin: 0 0 12px;
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-emerald);
      background: var(--glass-hover);
      transition: border-color 0.2s ease;
    }
    .catalog-node-metro-preview.is-empty { border-left-color: var(--text-muted); }
    .catalog-node-metro-preview-title {
      display: block;
      font-family: var(--font-display);
      font-size: 0.68rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text-muted);
      margin-bottom: 4px;
    }
    .catalog-node-metro-preview-text {
      margin: 0;
      font-size: 0.88rem;
      line-height: 1.5;
      color: var(--text-body);
      overflow-wrap: anywhere;
    }
    .catalog-node-metro-preview.is-empty .catalog-node-metro-preview-text {
      color: var(--text-muted);
    }
    .catalog-node-metro-preview-note {
      margin: 6px 0 0;
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--accent-amber);
    }
    .catalog-node-metro-preview-note[hidden] { display: none; }
    /* Отказ сервера внутри формы правки узла (409 на имя-двойник). Оформлен как
       текст отказа при удалении — розовая полоса слева, — потому что это одна и
       та же речь сервера, только показанная в другом месте. Текст показывается
       дословно и не обрезается: в нём сказано, что делать дальше. */
    .catalog-node-error,
    .catalog-node-blocked-text {
      margin: 0 0 4px;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-rose);
      background: var(--glass-hover);
      font-size: 0.88rem;
      line-height: 1.55;
      color: var(--text-body);
      /* Длинное название ЖК из выгрузки не должно распирать окно. */
      overflow-wrap: anywhere;
    }
    .catalog-node-error {
      margin: 0 0 12px;
      animation: fadeInUp 0.2s ease;
    }
    .catalog-node-error[hidden] { display: none; }

    @media (max-width: 1024px) {
      .catalog-tree-body { max-height: 52vh; }
    }
    /* Пальцем не наводят: там, где наведения нет, кнопки узла видны всегда и
       крупнее — иначе правка и удаление были бы недоступны с телефона вовсе. */
    @media (hover: none) {
      .catalog-tree-actions { opacity: 1; }
      .catalog-tree-act { width: 32px; height: 32px; border-color: var(--glass-border); }
    }

    @media (max-width: 768px) {
      /* Крошки в две строки: путь сверху, счётчики и кнопки под ним. Высота
         остаётся постоянной — её держит min-height самой строки. */
      .catalog-crumbs { min-height: 86px; align-items: stretch; }
      .catalog-crumbs-side { justify-content: space-between; width: 100%; }
      .catalog-crumbs-menu { left: 8px; right: 8px; width: auto; }
    }

    @media (max-width: 480px) {
      .catalog-tree { padding: 12px; }
      .catalog-tree-node { padding: 8px; font-size: 0.82rem; }
      .catalog-tree-list .catalog-tree-list { padding-left: 8px; margin-left: 6px; }
      .catalog-node-grid { grid-template-columns: 1fr; }
      /* Имя узла на телефоне важнее счётчиков: они уходят под путь строкой ниже. */
      .catalog-crumbs { min-height: 104px; }
      .catalog-crumb { padding: 5px 8px; font-size: 0.82rem; }
      .catalog-crumbs-counts { white-space: normal; }
      .catalog-crumbs-side { flex-wrap: wrap; gap: 6px; }
    }

    /* === Офисы продаж =====================================================
       Раздел «Офисы продаж» и форма офиса (СЕКЦИЯ E плана PLAN_SALES_OFFICES).
       Цвета берутся только токенами: признак назначения — индиго (показ) и
       изумруд (сделка), выключенный офис — приглушённый текст.
       ==================================================================== */
    .offices-empty {
      margin-top: 16px;
      padding: 28px 24px;
      text-align: center;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      animation: fadeInUp 0.35s ease;
    }
    .offices-empty[hidden] { display: none; }
    .offices-empty-icon { color: var(--accent-indigo); display: inline-flex; }
    .offices-empty h4 {
      margin: 8px 0 10px;
      font-family: var(--font-display);
      font-size: 1.05rem;
      color: var(--text-main);
    }
    .offices-empty p {
      margin: 0 auto 10px;
      max-width: 560px;
      font-size: 0.9rem;
      line-height: 1.6;
      color: var(--text-body);
    }
    .offices-empty-note { color: var(--text-muted); font-size: 0.85rem; }
    .offices-empty .btn { margin-top: 6px; }

    /* Признаки назначения в таблице: два независимых значка, а не один «тип». */
    .office-purpose-tags { display: flex; flex-wrap: wrap; gap: 6px; }
    .office-tag {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      padding: 3px 9px;
      border-radius: var(--radius-pill);
      font-size: 0.74rem;
      border: 1px solid var(--glass-border);
      color: var(--text-body);
      background: var(--glass-hover);
      white-space: nowrap;
    }
    .office-tag.office-tag-viewing { border-color: var(--accent-indigo); color: var(--accent-indigo); }
    .office-tag.office-tag-deal { border-color: var(--accent-emerald); color: var(--accent-emerald); }
    .office-row-off td { opacity: 0.62; }
    .office-projects-cell { font-size: 0.82rem; line-height: 1.5; color: var(--text-body); }
    .office-projects-cell .office-projects-none { color: var(--text-muted); }
    /* Привязка неизвестна — это НЕ «ЖК нет». Отдельный цвет и текст: сказать
       «ни одного» там, где мы просто не знаем, значит соврать владельцу. */
    .office-projects-cell .office-projects-unknown { color: var(--accent-amber); }
    /* Пока перечень едет — нейтральное «уточняем», чтобы мигание янтарным не
       читалось как предупреждение о том, чего ещё не произошло. */
    .office-projects-cell .office-projects-pending {
      color: var(--text-muted);
      opacity: 0.75;
      animation: office-projects-pulse 1.4s ease-in-out infinite;
    }
    @keyframes office-projects-pulse {
      0%, 100% { opacity: 0.45; }
      50% { opacity: 0.9; }
    }
    @media (prefers-reduced-motion: reduce) {
      .office-projects-cell .office-projects-pending { animation: none; }
    }
    .office-tag-unknown { border-color: var(--accent-amber); color: var(--accent-amber); }
    .office-tag-pending { color: var(--text-muted); }

    /* === Форма офиса ====================================================== */
    .office-form-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: 0 14px;
    }
    .office-form-grid .form-group .form-hint { margin: 4px 0 0; }
    /* Отказ сервера внутри формы: та же розовая полоса, что у правки узла
       каталога — это одна и та же речь сервера, показанная у поля. */
    .office-form-error {
      margin: 0 0 12px;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-rose);
      background: var(--glass-hover);
      font-size: 0.88rem;
      line-height: 1.55;
      color: var(--text-body);
      overflow-wrap: anywhere;
      animation: fadeInUp 0.2s ease;
    }
    .office-form-error[hidden] { display: none; }

    .office-purpose {
      margin: 6px 0 14px;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--glass-hover);
    }
    .office-purpose legend {
      padding: 0 6px;
      font-family: var(--font-display);
      font-size: 0.86rem;
      color: var(--text-main);
    }
    .office-purpose-item {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      padding: 8px 6px;
      border-radius: var(--radius-sm);
      cursor: pointer;
      transition: background 0.2s ease;
    }
    .office-purpose-item:hover { background: var(--card-bg); }
    .office-purpose-item input { margin-top: 3px; accent-color: var(--accent-indigo); }
    .office-purpose-text { font-size: 0.9rem; color: var(--text-body); line-height: 1.4; }
    .office-purpose-text small { display: block; color: var(--text-muted); font-size: 0.78rem; margin-top: 2px; }
    .office-purpose-hint {
      margin: 6px 0 0;
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .office-purpose-hint.office-purpose-warn { color: var(--accent-amber); }

    .office-active-toggle {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 8px 0 0;
      cursor: pointer;
    }
    .office-active-text { font-size: 0.82rem; color: var(--text-muted); line-height: 1.4; }

    .office-projects {
      margin: 4px 0 14px;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
    }
    .office-projects[hidden] { display: none; }
    .office-projects-title {
      display: block;
      font-family: var(--font-display);
      font-size: 0.82rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: var(--text-muted);
      margin-bottom: 8px;
    }
    .office-projects-list { display: flex; flex-wrap: wrap; gap: 6px; }
    .office-projects-list .office-tag { cursor: default; }
    .office-projects-note { margin: 8px 0 0; font-size: 0.78rem; color: var(--text-muted); line-height: 1.5; }

    /* ЖК офиса — галочки прямо в форме. Список ограничен по высоте: у
       застройщика бывает под сотню ЖК, и без потолка форма уезжала бы за экран,
       унося кнопку «Сохранить». */
    .office-projects-list {
      flex-direction: column;
      flex-wrap: nowrap;
      max-height: 240px;
      overflow-y: auto;
    }
    .office-projects-status {
      margin: 0 0 8px;
      font-size: 0.8rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    .office-projects-status.is-warn { color: var(--accent-amber); }
    .office-projects-pending { color: var(--text-muted); }

    .office-project-pick {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      padding: 8px 10px;
      border: 1px solid transparent;
      border-radius: var(--radius-sm);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .office-project-pick:hover { background: var(--glass-hover); }
    .office-project-pick.is-picked {
      border-color: var(--accent-indigo);
      background: var(--glass-hover);
    }
    .office-project-pick input { margin-top: 3px; accent-color: var(--accent-purple); }
    .office-project-text { display: flex; flex-direction: column; gap: 2px; }
    .office-project-name { font-size: 0.88rem; color: var(--text-main); }
    .office-project-city { font-size: 0.76rem; color: var(--text-muted); }
    .office-project-owner { font-size: 0.76rem; color: var(--accent-amber); }
    .office-project-owner.is-orphan { color: var(--accent-rose); }

    /* Подсказка вместо селектора офиса в карточке ЖК, когда офисов нет вовсе. */
    .catalog-node-office-empty {
      margin: 4px 0 0;
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    .catalog-node-office-empty[hidden] { display: none; }
    .catalog-node-office-empty .link-btn {
      background: none;
      border: none;
      padding: 0;
      font: inherit;
      color: var(--accent-purple);
      cursor: pointer;
      text-decoration: underline;
      transition: all 0.2s ease;
    }
    .catalog-node-office-empty .link-btn:hover { color: var(--accent-indigo-soft); }

    /* Список офисов не приехал — тревожный, но не аварийный тон: данные целы. */
    .catalog-node-office-error {
      margin: 4px 0 0;
      padding: 8px 10px;
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    .catalog-node-office-error[hidden] { display: none; }
    .catalog-node-office-error .link-btn,
    .project-cards-notice .link-btn {
      background: none;
      border: none;
      padding: 0;
      font: inherit;
      color: var(--accent-purple);
      cursor: pointer;
      text-decoration: underline;
      transition: all 0.2s ease;
    }
    .catalog-node-office-error .link-btn:hover,
    .project-cards-notice .link-btn:hover { color: var(--accent-indigo-soft); }
    .catalog-node-office-error .link-btn:disabled,
    .project-cards-notice .link-btn:disabled { opacity: 0.6; cursor: default; }

    /* Подсказки под селектором объекта реестра в карточке корпуса — те же
       тона, что у офиса продаж: пусто ≠ ошибка загрузки. */
    .catalog-node-eishs-empty {
      margin: 4px 0 0;
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    .catalog-node-eishs-empty[hidden] { display: none; }
    .catalog-node-eishs-error {
      margin: 4px 0 0;
      padding: 8px 10px;
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    .catalog-node-eishs-error[hidden] { display: none; }
    #catalog-node-eishs-manual-toggle {
      margin-top: 6px;
      background: none;
      border: none;
      padding: 0;
      font: inherit;
      font-size: 0.8rem;
      color: var(--accent-purple);
      cursor: pointer;
      text-decoration: underline;
      transition: all 0.2s ease;
    }
    #catalog-node-eishs-manual-toggle:hover { color: var(--accent-indigo-soft); }

    /* Полоса-предупреждение над сеткой карточек ЖК: тянется на всю ширину, иначе
       встала бы одной колонкой и читалась как ещё одна карточка. */
    .project-cards-notice {
      grid-column: 1 / -1;
      margin: 0 0 4px;
      padding: 10px 12px;
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      font-size: 0.84rem;
      line-height: 1.5;
      color: var(--text-body);
    }

    /* КАРТОЧКИ ЖК ---------------------------------------------------------- */
    .project-cards-summary {
      margin: 0 0 12px;
      font-size: 0.82rem;
      color: var(--text-muted);
    }
    .project-cards-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
      gap: 12px;
      overflow-y: auto;
    }
    .project-card {
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .project-card:hover {
      border-color: var(--accent-indigo);
      transform: translateY(-2px);
    }
    .project-card-head { display: flex; flex-direction: column; gap: 4px; }
    .project-card-name {
      margin: 0;
      font-family: var(--font-display);
      font-size: 1rem;
      color: var(--text-main);
    }
    .project-card-count { font-size: 0.76rem; color: var(--text-muted); }
    .project-card-body { display: flex; flex-direction: column; gap: 6px; }
    .project-card-row {
      display: flex;
      justify-content: space-between;
      gap: 10px;
      font-size: 0.82rem;
    }
    .project-card-key { color: var(--text-muted); }
    .project-card-val { color: var(--text-body); text-align: right; }
    .project-card-row.is-empty .project-card-val { color: var(--accent-amber); }
    /* Сайт объекта: строка рисуется только когда ссылка есть (см. websiteRow
       в renderProjectCards), поэтому отдельного «пустого» состояния нет. */
    .project-card-link {
      color: var(--accent-cyan);
      text-decoration: none;
      font-weight: 600;
      transition: color 0.2s ease;
    }
    .project-card-link:hover,
    .project-card-link:focus-visible { color: var(--accent-indigo); text-decoration: underline; }
    .project-card-actions { display: flex; }
    .project-card-actions .btn { width: 100%; justify-content: center; }

    /* Бейдж «есть предложения синхронизации наш.дом.рф» на карточке ЖК:
       amber — обычный диф (PROPOSED, ждёт клика «Применить»/«Отклонить»),
       rose — скрапер сломан на этом объекте (NEEDS_ATTENTION, чинить конфиг/
       код, а не подтверждать поле). Разные цвета — разные действия. */
    .project-card-sync-badge {
      align-self: flex-start;
      cursor: pointer;
      border: none;
    }
    .project-card-sync-badge.is-attention {
      background: rgba(244, 63, 94, 0.2);
      color: var(--accent-rose-soft);
    }

    /* Кнопка «Синхронизация наш.дом.рф» в шапке каталога: счётчик виден только
       когда есть что показать — молчание не должно выглядеть как «нечего
       синхронизировать», это делает renderNashdomrfBadges(). */
    .nashdomrf-proposals-count { margin-left: 6px; }

    /* Заголовки двух разделов окна «Синхронизация с наш.дом.рф»: привязка ЖК
       к реестру и сверка сроков сдачи корпусов — теперь оба раздела в одном
       окне, разделяем их подписью, а не только пустотой между блоками. */
    .nashdomrf-section-title { margin: 2px 0 6px; }
    .nashdomrf-section-title-second { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--glass-border); }

    /* СПИСОК ЖК С ПРИВЯЗКОЙ К РЕЕСТРУ (переехал из дерева каталога) --------- */
    .nashdomrf-link-list {
      display: flex;
      flex-direction: column;
      gap: 10px;
      overflow-y: auto;
      margin-bottom: 4px;
    }
    .nashdomrf-link-card {
      border: 1px solid var(--glass-border);
      border-radius: 12px;
      padding: 10px 12px;
      background: var(--glass-bg);
    }
    .nashdomrf-link-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
    .nashdomrf-link-name { font-weight: 600; color: var(--text-main); font-size: 0.86rem; }
    .nashdomrf-link-city { color: var(--text-muted); font-size: 0.78rem; }
    .nashdomrf-link-developer { color: var(--text-muted); font-size: 0.76rem; margin-top: 2px; overflow-wrap: anywhere; }
    .nashdomrf-link-disabled { color: var(--text-muted); font-size: 0.78rem; margin-top: 6px; }
    .nashdomrf-link-empty { color: var(--text-muted); font-size: 0.82rem; padding: 8px 2px; }

    /* РУЧНАЯ ПРИВЯЗКА ЖК: у 11 из 13 ЖК на бою needs_manual приходит с ПУСТЫМ
       списком кандидатов — без этого блока привязать их из кабинета нечем.
       Тот же левый отступ (34px), что у .catalog-ndrf-row/-candidates выше в
       этой же карточке — блок продолжает их вертикальный ряд, а не начинает
       новый. */
    .nashdomrf-link-manual {
      margin: 8px 0 2px 34px;
      padding: 8px 10px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--card-bg);
    }
    .nashdomrf-link-manual-label { display: block; margin-bottom: 4px; font-weight: 600; }
    .nashdomrf-link-manual-row { display: flex; gap: 8px; flex-wrap: wrap; }
    .nashdomrf-link-manual-input {
      flex: 1 1 220px;
      min-width: 160px;
      width: auto;
      padding: 6px 10px;
      font-size: 0.78rem;
    }
    .nashdomrf-link-manual-btn { flex: 0 0 auto; padding: 6px 14px; font-size: 0.78rem; white-space: nowrap; }
    .nashdomrf-link-manual > .form-hint { margin: 4px 0 0; }

    @media (max-width: 480px) {
      .nashdomrf-link-manual { margin-left: 0; }
      .nashdomrf-link-manual-row { flex-direction: column; }
      .nashdomrf-link-manual-btn { width: 100%; }
    }

    /* СПИСОК ПРЕДЛОЖЕНИЙ СИНХРОНИЗАЦИИ -------------------------------------- */
    .nashdomrf-proposals-list {
      display: flex;
      flex-direction: column;
      gap: 10px;
      overflow-y: auto;
    }
    .nashdomrf-proposal-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .nashdomrf-proposal-card.is-attention {
      border-left: 3px solid var(--accent-rose);
    }
    .nashdomrf-proposal-card.is-proposed {
      border-left: 3px solid var(--accent-amber);
    }
    .nashdomrf-proposal-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      flex-wrap: wrap;
    }
    .nashdomrf-proposal-title {
      margin: 0;
      font-family: var(--font-display);
      font-size: 0.92rem;
      color: var(--text-main);
    }
    .nashdomrf-proposal-field {
      font-size: 0.78rem;
      color: var(--text-muted);
    }
    .nashdomrf-proposal-diff {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.86rem;
      flex-wrap: wrap;
    }
    .nashdomrf-proposal-old { color: var(--text-muted); text-decoration: line-through; }
    .nashdomrf-proposal-new { color: var(--text-main); font-weight: 600; }
    .nashdomrf-proposal-arrow { color: var(--text-muted); }
    .nashdomrf-proposal-error {
      font-size: 0.82rem;
      color: var(--accent-rose-soft);
      line-height: 1.5;
    }
    .nashdomrf-proposal-source {
      font-size: 0.76rem;
    }
    .nashdomrf-proposal-source a { color: var(--accent-cyan); }
    .nashdomrf-proposal-actions {
      display: flex;
      gap: 8px;
      justify-content: flex-end;
    }
    .nashdomrf-proposals-empty {
      padding: 24px 12px;
      text-align: center;
      color: var(--text-muted);
      font-size: 0.86rem;
    }

    /* СПИСОК ПРЕДЛОЖЕНИЙ FAQ (Shadow Mode, тот же стиль карточек, что у
       наш.дом.рф выше) --------------------------------------------------- */
    .faq-proposals-list {
      display: flex;
      flex-direction: column;
      gap: 10px;
      overflow-y: auto;
    }
    .faq-proposal-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 14px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .faq-proposal-card .form-group { margin: 0; }
    .faq-proposal-excerpt {
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      background: var(--bg-deep);
      border: 1px dashed var(--glass-border);
      color: var(--text-muted);
      font-size: 0.82rem;
      line-height: 1.5;
      white-space: pre-wrap;
    }

    @media (max-width: 480px) {
      .project-cards-grid { grid-template-columns: 1fr; }
      .office-projects-list { max-height: 200px; }
      .project-card-row { flex-direction: column; gap: 2px; }
      .project-card-val { text-align: left; }
      .nashdomrf-proposal-actions { flex-direction: column; }
      .nashdomrf-proposal-actions .btn { width: 100%; justify-content: center; }
    }

    @media (max-width: 768px) {
      .offices-empty { padding: 22px 16px; }
    }
    @media (max-width: 480px) {
      .office-form-grid { grid-template-columns: 1fr; }
      .offices-empty h4 { font-size: 0.98rem; }
      .offices-empty p { font-size: 0.86rem; }
      .office-purpose { padding: 10px; }
      .office-purpose-item { padding: 8px 2px; }
    }

    /* === Удаление подборкой ==============================================
       Отдельный режим выделения. Визуально он ничем не похож на выбор для
       сравнения: там индиго-обводка и нейтральная подпись, здесь — розовая
       рамка, розовая галочка и красная панель над каталогом. Перепутать
       «отметил для сравнения» и «отметил для удаления» нельзя ещё и потому,
       что галочки взаимоисключающие: в режиме удаления карточка показывает
       только его чекбокс.
       ==================================================================== */
    .bulk-delete-bar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 12px;
      margin-top: 16px;
      padding: 12px 16px;
      border-radius: var(--radius-md);
      border: 1px solid var(--accent-rose);
      /* Фолбэк для браузеров без color-mix: панель остаётся стеклянной,
         опасность считывается рамкой и красной кнопкой. */
      background: var(--card-bg);
      background: color-mix(in srgb, var(--accent-rose) 12%, var(--card-bg));
      backdrop-filter: var(--blur-glass);
      animation: fadeInUp 0.25s ease;
    }
    .bulk-delete-bar[hidden] { display: none; }
    .bulk-delete-bar-text {
      font-size: 0.85rem;
      line-height: 1.5;
      color: var(--text-body);
      min-width: 0;
    }
    .bulk-delete-bar-text strong { color: var(--text-main); }
    .bulk-delete-bar-hint {
      display: block;
      font-size: 0.78rem;
      color: var(--accent-rose);
      margin-top: 2px;
    }
    .bulk-delete-bar-hint:empty { display: none; }
    .bulk-delete-bar-actions {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }
    .bulk-delete-bar-actions .btn { padding: 6px 12px; font-size: 0.82rem; }
    /* Кнопка удаления без единой отметки не просто неактивна, а явно тусклая:
       раньше она выглядела рабочей, и человек нажимал её вхолостую. */
    .bulk-delete-bar-actions .btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }

    /* Кнопка «В корзину» у арендатора по умолчанию заперта намеренно, и это
       должно быть ВИДНО. Находка ревью 16.08.2026: без этих строк она выглядела
       обычной, даже приподнималась на наведении, а клик не проходил — читается
       как «кабинет завис», а не «так нельзя». Общего правила `.btn:disabled` в
       кабинете нет, оно задаётся точечно (см. соседей выше). */
    .btn-tenant-trash:disabled { opacity: 0.45; cursor: not-allowed; }
    /* Специфичность и порядок здесь важны: `.btn-secondary:hover` меняет фон, а
       его светлая версия объявлена НИЖЕ по файлу и с той же силой — без
       `:disabled` в обоих селекторах и без сброса фона заблокированная кнопка
       продолжала подсвечиваться как рабочая (докрутка ревью 16.08.2026). */
    .btn-tenant-trash:disabled:hover,
    [data-theme="light"] .btn-tenant-trash:disabled:hover {
      transform: none;
      box-shadow: none;
      border-color: var(--glass-border);
      background: var(--card-bg);
    }

    /* Та же находка (докрутка ревью 05.09.2026), тот же приём — восемь мест
       вкладки «Документы (RAG)», закрытых правом documents:write:
       docsWriteLockReason()/applyDocsWriteLock() и три соседних замка
       (applyDocProjectWriteLock/applyDocPriorityWriteLock/
       applyDocTextWriteLock) гасят их через `disabled`, но без строк ниже
       кнопка выглядела рабочей и приподнимала фон/иконку на наведении, а
       `title` на disabled-элементе браузер не показывает вовсе (disabled не
       получает mouse-события) — объяснение до человека не доходило. Явное
       пояснение теперь плашкой .docs-write-lock рядом (или общей над
       таблицей — .rag-actions-expanded), а здесь только визуальная метка
       «не трогать». Общего .btn:disabled в кабинете по-прежнему нет —
       список поимённый. */
    .rag-actions-expanded .btn:disabled,
    /* Вложенная навигация Super Admin по моделям: использует те же кнопки и
       цветовые токены, что остальные панели, без отдельного фреймворка. */
    .model-control-nav {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
      margin: 14px 0 4px;
    }
    .model-control-nav .is-active {
      background: var(--accent, #6d5dfc);
      border-color: var(--accent, #6d5dfc);
      color: #fff;
    }
    .model-control-section[hidden] { display: none; }
    .model-control-section { padding-top: 2px; }
    .model-stat-filters {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
      margin-top: 8px;
    }
    .model-stat-filters .form-select { width: auto; min-width: 150px; }

    #rag-benchmark-run-btn:disabled,
    #bench-questions-save-btn:disabled,
    #bench-questions-reset-btn:disabled,
    #doc-project-save-btn:disabled,
    #doc-priority-save-btn:disabled,
    .doc-prio-preset:disabled {
      opacity: 0.45;
      cursor: not-allowed;
    }
    /* .btn-primary:hover — единственный вариант с подъёмом и свечением
       (transform + box-shadow); .btn-secondary/.btn-danger фон не поднимают,
       только меняют заливку — та сбрасывается селекторами ниже. */
    #rag-benchmark-run-btn:disabled:hover,
    #bench-questions-save-btn:disabled:hover,
    #doc-project-save-btn:disabled:hover,
    #doc-priority-save-btn:disabled:hover {
      transform: none;
      box-shadow: none;
    }
    .rag-actions-expanded .btn-secondary:disabled:hover,
    #bench-questions-reset-btn:disabled:hover {
      background: rgba(51, 65, 85, 0.6);
      border-color: var(--glass-border);
    }
    [data-theme="light"] .rag-actions-expanded .btn-secondary:disabled:hover,
    [data-theme="light"] #bench-questions-reset-btn:disabled:hover {
      background: rgba(226, 232, 240, 0.7);
      border-color: var(--glass-border);
    }
    .rag-actions-expanded .btn-danger:disabled:hover {
      background: rgba(244, 63, 94, 0.2);
      border-color: rgba(244, 63, 94, 0.4);
    }
    [data-theme="light"] .rag-actions-expanded .btn-danger:disabled:hover {
      background: rgba(244, 63, 94, 0.12);
      border-color: rgba(244, 63, 94, 0.35);
    }
    /* Значок «Переиндекс» (.btn-secondary) наследует общий .btn:hover >
       .btn-icon — фиолетовый цвет и сдвиг на пиксель — и он же матчится на
       disabled-кнопке под курсором. У .btn-primary/.btn-danger это уже
       нейтрализовано общим правилом (.btn-primary:hover > .btn-icon и
       соседним), .btn-secondary своего сброса не имел. */
    .rag-actions-expanded .btn-secondary:disabled:hover > .btn-icon,
    #bench-questions-reset-btn:disabled:hover > .btn-icon {
      opacity: 0.85;
      color: inherit;
      transform: none;
    }
    /* Готовые ранги важности (.doc-prio-preset) — своя раскладка, не .btn:
       hover обычно поднимает и красит текст, disabled должен остаться на
       месте и приглушённым. */
    .doc-prio-preset:disabled:hover {
      color: var(--text-body);
      border-color: var(--card-border);
      transform: none;
    }

    /* Те же три места, что закрыты выше, но не кнопки, а поля ввода
       (#bench-questions-text/keywords, #doc-text-input) и селектор
       (#doc-project-select): своего `color`/`background` у .form-input/
       .form-textarea/.form-select достаточно, чтобы перебить браузерный
       дефолт disabled-поля — оно выглядело редактируемым и молча не
       принимало ввод. Приём — тот же приглушённый фон, что и у
       .form-input.is-readonly выше по файлу (там для другого случая —
       поле остаётся выделяемым текстом, здесь обычный disabled достаточен). */
    #bench-questions-text:disabled,
    #bench-questions-keywords:disabled,
    #doc-text-input:disabled,
    #doc-priority-value:disabled,
    #doc-priority-date:disabled {
      background: rgba(15, 23, 42, 0.35);
      color: var(--text-body);
      cursor: not-allowed;
    }
    [data-theme="light"] #bench-questions-text:disabled,
    [data-theme="light"] #bench-questions-keywords:disabled,
    [data-theme="light"] #doc-text-input:disabled,
    [data-theme="light"] #doc-priority-value:disabled,
    [data-theme="light"] #doc-priority-date:disabled {
      background-color: rgba(226, 232, 240, 0.55);
    }
    /* Селекторы (#doc-project-select, #doc-priority-pair — окно «Важность»,
       соперник документа): background тут ещё и рисует стрелку через
       background-image, поэтому фон трогать нельзя — только приглушение. */
    #doc-project-select:disabled,
    #doc-priority-pair:disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }

    /* Кнопка удаления во время запроса: спиннер и текст хода работы в строку.
       Ответ на тысячах объектов не мгновенный (сервер удаляет пакетами с
       паузами), и неподвижная кнопка читалась бы как зависший кабинет. */
    #bulkDeleteRunBtn { display: inline-flex; align-items: center; gap: 8px; }
    #bulkDeleteRunBtn .diag-spinner {
      width: 14px;
      height: 14px;
      border-width: 2px;
      /* currentColor, а не белый: в светлой теме подпись кнопки тёмная, и
         белое кольцо спиннера на её розовой заливке было не видно. */
      border-color: color-mix(in srgb, currentColor 35%, transparent);
      border-top-color: currentColor;
    }

    /* Три числа режима подборки. Крупно и подписаны словами: функции отметки и
       счётчики существовали и раньше, но набранные мелким текстом в строке
       пояснения читались как их отсутствие. */
    .bulk-delete-stats {
      display: flex;
      align-items: stretch;
      gap: 10px;
      flex-wrap: wrap;
      flex: 0 0 auto;
    }
    .bulk-delete-stat {
      display: flex;
      flex-direction: column;
      gap: 2px;
      min-width: 92px;
      padding: 8px 12px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
    }
    .bulk-delete-stat.is-primary {
      border-color: var(--accent-rose);
      background: var(--card-bg);
      background: color-mix(in srgb, var(--accent-rose) 16%, var(--card-bg));
    }
    .bulk-delete-stat-num {
      font-family: var(--font-display);
      font-size: 1.5rem;
      font-weight: 700;
      line-height: 1.1;
      color: var(--text-main);
    }
    .bulk-delete-stat.is-primary .bulk-delete-stat-num { color: var(--accent-rose); }
    .bulk-delete-stat-label {
      font-size: 0.72rem;
      line-height: 1.3;
      color: var(--text-muted);
    }

    /* Кнопка-переключатель режима во включённом состоянии */
    #bulkDeleteModeBtn.is-active {
      border-color: var(--accent-rose);
      color: var(--accent-rose);
    }

    .bulk-delete-checkbox-row {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.8rem;
      font-weight: 600;
      color: var(--accent-rose);
      margin-top: 8px;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .bulk-delete-checkbox-row input[type="checkbox"] {
      width: 16px;
      height: 16px;
      accent-color: var(--accent-rose);
      cursor: pointer;
    }
    .faq-card.bulk-delete-selected {
      border-color: var(--accent-rose);
      box-shadow: 0 0 0 2px var(--accent-rose);
      box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-rose) 35%, transparent);
    }

    /* Отчёт об удалении подборкой */
    .bulk-result-summary {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 14px;
    }
    .bulk-result-group { margin-bottom: 14px; }
    .bulk-result-group-title {
      font-family: var(--font-display);
      font-size: 0.88rem;
      font-weight: 600;
      color: var(--text-main);
      margin-bottom: 6px;
    }
    .bulk-result-reason {
      font-size: 0.8rem;
      color: var(--text-muted);
      margin: 0 0 6px;
      line-height: 1.5;
    }
    /* Список отказов бывает на десятки строк: прокручиваем его внутри окна,
       чтобы кнопка «Закрыть» не уезжала за нижний край экрана. */
    .bulk-result-list {
      list-style: none;
      margin: 0;
      padding: 8px 10px;
      max-height: 190px;
      overflow-y: auto;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--card-bg);
    }
    .bulk-result-list li {
      font-size: 0.82rem;
      color: var(--text-body);
      padding: 3px 0;
      border-bottom: 1px solid var(--glass-border);
      word-break: break-word;
    }
    .bulk-result-list li:last-child { border-bottom: none; }
    .bulk-result-list.is-muted li { color: var(--text-muted); font-family: var(--font-mono); font-size: 0.76rem; }

    @media (max-width: 768px) {
      .bulk-delete-bar { flex-direction: column; align-items: stretch; }
      .bulk-delete-bar-actions .btn { flex: 1 1 auto; justify-content: center; }
      .bulk-delete-stats { justify-content: space-between; }
      .bulk-delete-stat { flex: 1 1 30%; min-width: 0; }
    }
    @media (max-width: 480px) {
      .bulk-delete-bar-actions { flex-direction: column; align-items: stretch; }
      .bulk-delete-bar-actions .btn { width: 100%; }
      .bulk-result-list { max-height: 46vh; }
      /* Главное число остаётся крупным, два справочных — вполовину тише. */
      .bulk-delete-stats { flex-direction: column; }
      .bulk-delete-stat { flex-direction: row; align-items: baseline; gap: 8px; padding: 6px 10px; }
      .bulk-delete-stat-num { font-size: 1.2rem; }
      .bulk-delete-stat.is-primary .bulk-delete-stat-num { font-size: 1.6rem; }
    }

    /* Feature Flag Toggle Switch */
    .module-toggle-switch {
      position: relative;
      display: inline-block;
      width: 42px;
      height: 24px;
      flex-shrink: 0;
    }
    .module-toggle-switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }
    .module-toggle-slider {
      position: absolute;
      cursor: pointer;
      top: 0; left: 0; right: 0; bottom: 0;
      background: rgba(148, 163, 184, 0.3);
      border-radius: var(--radius-pill);
      transition: background 0.2s ease;
    }
    .module-toggle-slider::before {
      position: absolute;
      content: "";
      height: 18px;
      width: 18px;
      left: 3px;
      bottom: 3px;
      background: #fff;
      border-radius: 50%;
      transition: transform 0.2s ease;
    }
    /* Сам флажок в переключателе не виден (0×0, opacity 0), но фокус
       получает именно он. Без этого правила рычаг модуля при обходе табом
       не подсвечивался ничем: пользователь нажимал пробел вслепую. */
    .module-toggle-switch input:focus-visible + .module-toggle-slider {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 3px;
    }

    .module-toggle-switch input:checked + .module-toggle-slider {
      background: var(--gradient-indigo-cyan);
    }
    .module-toggle-switch input:checked + .module-toggle-slider::before {
      transform: translateX(18px);
    }

    /* Уменьшенный тумблер для подчинённой функции внутри карточки модуля:
       он обязан читаться как «часть блока», а не как равный переключатель. */
    .module-toggle-switch.module-toggle-sm {
      width: 34px;
      height: 20px;
    }
    .module-toggle-sm .module-toggle-slider::before {
      height: 14px;
      width: 14px;
    }
    .module-toggle-sm input:checked + .module-toggle-slider::before {
      transform: translateX(14px);
    }

    /* ======================================================================
       Функции модуля — вложенные переключатели внутри карточки родителя.
       Приём заявок на ипотеку не отдельный товар, а функция калькулятора:
       отступ слева, вертикальная линия-связка и приглушённый фон показывают
       подчинённость, а выключенный родитель гасит блок целиком (.is-blocked).
       ====================================================================== */
    .module-subflags {
      margin-top: 14px;
      padding-left: 12px;
      border-left: 2px solid rgba(139, 92, 246, 0.35);
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .module-subflags-caption {
      font-size: 0.7rem;
      text-transform: uppercase;
      letter-spacing: 0.6px;
      font-weight: 700;
      color: var(--accent-indigo-soft);
      opacity: 0.85;
    }
    .module-subflag {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      background: rgba(99, 102, 241, 0.07);
      border: 1px solid var(--glass-border);
      transition: all 0.2s ease;
    }
    .module-subflag:hover {
      background: rgba(99, 102, 241, 0.12);
      border-color: rgba(99, 102, 241, 0.3);
    }
    .module-subflag.is-blocked {
      opacity: 0.62;
      background: rgba(148, 163, 184, 0.07);
    }
    .module-subflag.is-blocked .module-toggle-switch { cursor: not-allowed; }
    .module-subflag-text { min-width: 0; }
    .module-subflag-title {
      font-size: 0.9rem;
      font-weight: 600;
      color: var(--text-main);
      line-height: 1.3;
      overflow-wrap: anywhere;
    }
    .module-subflag-desc {
      margin-top: 4px;
      font-size: 0.78rem;
      color: var(--text-muted);
      line-height: 1.45;
    }
    .module-subflag .setting-lock-badge { margin-top: 8px; }

    @media (max-width: 480px) {
      .module-subflags { padding-left: 10px; }
      .module-subflag {
        flex-direction: column;
        align-items: stretch;
        gap: 10px;
      }
      .module-subflag .module-toggle-switch { align-self: flex-end; }
    }

    /* ------------------------------------------------------------------
       Разрешение внутри карточки интеграции: рычаг стоит рядом с адресом
       приёмника, которым управляет, и оформлен как часть того же блока.

       Включённое состояние подсвечивается ЯНТАРНЫМ (--accent-amber —
       «предупреждение»), а не изумрудным «успехом»: включённый режим
       отправляет в живую воронку владельца выдуманные заявки, и это стоит
       заметить с одного взгляда. Розовый (--accent-rose) здесь был бы
       неправдой: ничего не сломано, режим штатный.
       ------------------------------------------------------------------ */
    .integration-permission {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 14px;
      margin-top: 18px;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      background: rgba(99, 102, 241, 0.07);
      border: 1px solid var(--glass-border);
      transition: all 0.2s ease;
    }
    .integration-permission:hover {
      background: rgba(99, 102, 241, 0.12);
      border-color: rgba(99, 102, 241, 0.3);
    }
    .integration-permission.is-on {
      background: rgba(245, 158, 11, 0.1);
      border-color: rgba(245, 158, 11, 0.45);
    }
    .integration-permission.is-on:hover {
      background: rgba(245, 158, 11, 0.16);
      border-color: rgba(245, 158, 11, 0.6);
    }
    .integration-permission.is-on .integration-permission-title { color: var(--accent-amber); }
    .integration-permission.is-blocked {
      opacity: 0.62;
      background: rgba(148, 163, 184, 0.07);
      border-color: var(--glass-border);
    }
    .integration-permission.is-blocked .module-toggle-switch,
    .integration-permission.is-blocked .module-toggle-slider { cursor: not-allowed; }
    .integration-permission-text { min-width: 0; }
    .integration-permission-title {
      font-size: 0.92rem;
      font-weight: 600;
      color: var(--text-main);
      line-height: 1.3;
      overflow-wrap: anywhere;
    }
    .integration-permission-desc {
      margin-top: 5px;
      font-size: 0.8rem;
      color: var(--text-muted);
      line-height: 1.5;
    }
    .integration-permission-desc code {
      font-family: var(--font-mono);
      font-size: 0.76rem;
      padding: 1px 5px;
      border-radius: var(--radius-sm);
      background: rgba(245, 158, 11, 0.16);
      color: var(--accent-amber);
    }
    .integration-permission-note {
      margin-top: 8px;
      font-size: 0.78rem;
      line-height: 1.45;
      color: var(--text-body);
    }
    .integration-permission-note:empty { display: none; }
    .integration-permission.is-on .integration-permission-note { color: var(--accent-amber); }

    /* Тумблер в момент запроса: состояние ещё не подтверждено сервером, и
       второе нажатие отправило бы встречную команду. */
    .integration-permission.is-busy { opacity: 0.7; }
    .integration-permission.is-busy .module-toggle-switch { cursor: progress; }

    @media (max-width: 480px) {
      .integration-permission {
        flex-direction: column;
        align-items: stretch;
        gap: 10px;
      }
      .integration-permission .module-toggle-switch { align-self: flex-end; }
    }

    /* ------------------------------------------------------------------
       Выбор CRM застройщика: три карточки-переключателя вместо выпадающего
       списка. Список прячет варианты за кликом, а здесь вариантов ровно три
       и у каждого своя цена ошибки — их видно сразу.

       Выбранная карточка подсвечена ФИРМЕННЫМ индиго (--accent-indigo):
       это выбор, а не успех и не предупреждение.
       ------------------------------------------------------------------ */
    .crm-provider-choice {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 10px;
      margin-top: 6px;
    }
    .crm-provider-option {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .crm-provider-option:hover {
      border-color: rgba(99, 102, 241, 0.45);
      background: rgba(99, 102, 241, 0.08);
      transform: translateY(-1px);
    }
    .crm-provider-option input[type="radio"] {
      margin-top: 3px;
      flex-shrink: 0;
      accent-color: var(--accent-indigo);
      cursor: inherit;
    }
    .crm-provider-body { min-width: 0; }
    .crm-provider-name {
      display: block;
      font-size: 0.92rem;
      font-weight: 600;
      color: var(--text-main);
      line-height: 1.3;
    }
    .crm-provider-desc {
      display: block;
      margin-top: 4px;
      font-size: 0.78rem;
      color: var(--text-muted);
      line-height: 1.45;
    }
    .crm-provider-option.is-selected {
      border-color: rgba(99, 102, 241, 0.6);
      background: rgba(99, 102, 241, 0.12);
      box-shadow: var(--shadow-glow-indigo);
    }
    .crm-provider-option.is-selected .crm-provider-name { color: var(--accent-indigo-soft); }
    /* Правка закрыта (замок платформы или роль без прав) — карточки остаются
       на экране, но перестают притворяться кликабельными. */
    .crm-provider-option.is-locked {
      opacity: 0.62;
      cursor: not-allowed;
      transform: none;
    }
    .crm-provider-option.is-locked:hover {
      transform: none;
      border-color: var(--glass-border);
      background: var(--card-bg);
    }
    .crm-provider-option.is-locked.is-selected:hover {
      border-color: rgba(99, 102, 241, 0.6);
      background: rgba(99, 102, 241, 0.12);
    }

    /* ------------------------------------------------------------------
       Условие сделки на три положения (рассрочка). Три карточки-переключателя
       в один ряд: вариантов ровно три, и у каждого своя цена ошибки — прятать
       их за выпадающим списком значит заставить владельца открывать список,
       чтобы узнать, что вообще бывает. Форма повторяет выбор CRM намеренно:
       один и тот же жест — один и тот же вид.
       ------------------------------------------------------------------ */
    .deal-policy-choice {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
      gap: 10px;
      margin-top: 4px;
    }
    .deal-policy-option {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .deal-policy-option:hover {
      border-color: rgba(99, 102, 241, 0.45);
      background: rgba(99, 102, 241, 0.08);
      transform: translateY(-1px);
    }
    .deal-policy-option:active { transform: scale(0.985); }
    .deal-policy-option input[type="radio"] {
      margin-top: 3px;
      flex-shrink: 0;
      accent-color: var(--accent-indigo);
      cursor: inherit;
    }
    .deal-policy-body { min-width: 0; }
    .deal-policy-name {
      display: block;
      font-size: 0.92rem;
      font-weight: 600;
      color: var(--text-main);
      line-height: 1.3;
    }
    .deal-policy-desc {
      display: block;
      margin-top: 4px;
      font-size: 0.78rem;
      color: var(--text-muted);
      line-height: 1.45;
    }
    .deal-policy-option.is-selected {
      border-color: rgba(99, 102, 241, 0.6);
      background: rgba(99, 102, 241, 0.12);
      box-shadow: var(--shadow-glow-indigo);
    }
    .deal-policy-option.is-selected .deal-policy-name { color: var(--accent-indigo-soft); }
    .deal-policy-option.is-saving { opacity: 0.7; cursor: progress; }
    /* Правка закрыта (замок платформы или роль без прав): карточки остаются на
       экране — владелец обязан видеть, что ему выставили, — но перестают
       притворяться кликабельными. */
    .deal-policy-option.is-locked {
      opacity: 0.62;
      cursor: not-allowed;
      transform: none;
    }
    .deal-policy-option.is-locked:hover {
      transform: none;
      border-color: var(--glass-border);
      background: var(--card-bg);
    }
    .deal-policy-option.is-locked.is-selected:hover {
      border-color: rgba(99, 102, 241, 0.6);
      background: rgba(99, 102, 241, 0.12);
    }
    .deal-policy-echo { margin-top: 10px; }
    /* Предупреждение о расхождении с материалами застройщика. Янтарь, а не
       красный: настройка сохранена, ошибки не произошло — владельцу нужно
       перечитать свои буклеты, а не чинить сбой. */
    .deal-policy-conflict {
      margin-top: 12px;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(245, 158, 11, 0.35);
      border-left: 3px solid var(--accent-amber);
      background: rgba(245, 158, 11, 0.1);
      animation: dealPolicyConflictIn 0.22s ease;
    }
    .deal-policy-conflict[hidden] { display: none; }
    @keyframes dealPolicyConflictIn {
      from { opacity: 0; transform: translateY(-4px); }
      to { opacity: 1; transform: translateY(0); }
    }
    .deal-policy-conflict-head {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .deal-policy-conflict-icon {
      display: inline-flex;
      flex-shrink: 0;
      color: var(--accent-gold-soft);
    }
    .deal-policy-conflict-icon svg {
      fill: none;
      stroke: currentColor;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    .deal-policy-conflict-title {
      font-size: 0.88rem;
      font-weight: 600;
      color: var(--accent-gold-soft);
      line-height: 1.3;
    }
    .deal-policy-conflict-close {
      margin-left: auto;
      flex-shrink: 0;
      width: 26px;
      height: 26px;
      border: none;
      border-radius: var(--radius-sm);
      background: transparent;
      color: var(--text-muted);
      font-size: 1.05rem;
      line-height: 1;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .deal-policy-conflict-close:hover {
      background: rgba(245, 158, 11, 0.18);
      color: var(--accent-gold-soft);
    }
    .deal-policy-conflict-close:active { transform: scale(0.92); }
    /* Текст приходит с сервера маркированным списком цитат: переносы строк —
       часть смысла, схлопывать их в абзац нельзя. */
    .deal-policy-conflict-text {
      margin: 8px 0 0;
      font-size: 0.82rem;
      line-height: 1.55;
      color: var(--text-main);
      white-space: pre-wrap;
      overflow-wrap: anywhere;
    }
    @media (max-width: 480px) {
      .deal-policy-choice { grid-template-columns: 1fr; }
      .deal-policy-option { padding: 11px 12px; }
      .deal-policy-conflict { padding: 11px 12px; }
      .deal-policy-conflict-text { font-size: 0.8rem; }
    }

    /* Поддомен amoCRM: суффикс домена — часть подписи, а не часть ввода.
       Владелец вводит «elbandito237» и видит, во что это развернётся. */
    .crm-subdomain-field {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }
    .crm-subdomain-field .form-input { flex: 1 1 220px; min-width: 0; }
    .crm-subdomain-suffix {
      font-family: var(--font-mono);
      font-size: 0.85rem;
      color: var(--text-muted);
      white-space: nowrap;
    }
    /* Токен — строка в несколько сотен символов: однострочное поле прячет
       её конец и мешает убедиться, что вставилось целиком. */
    .crm-token-input {
      font-family: var(--font-mono);
      font-size: 0.78rem;
      line-height: 1.5;
      resize: vertical;
      word-break: break-all;
    }

    /* Срок жизни токена. Спокойное состояние — обычная подпись; близкое
       истечение — ЯНТАРНОЕ предупреждение (--accent-amber), истёкший токен —
       РОЗОВОЕ (--accent-rose): заявки уже не уходят, это поломка. */
    .crm-token-expiry {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin-top: 12px;
      padding: 11px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: rgba(148, 163, 184, 0.08);
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-body);
      transition: all 0.2s ease;
    }
    .crm-token-expiry-icon { flex-shrink: 0; line-height: 1.4; }
    .crm-token-expiry-text { min-width: 0; overflow-wrap: anywhere; }
    .crm-token-expiry.is-warning {
      background: rgba(245, 158, 11, 0.12);
      border-color: rgba(245, 158, 11, 0.45);
      color: var(--accent-amber);
    }
    .crm-token-expiry.is-critical {
      background: rgba(244, 63, 94, 0.12);
      border-color: rgba(244, 63, 94, 0.5);
      color: var(--accent-rose);
    }

    /* Отказ сервера (422) при сохранении карты контуров Домопланера — та же
       розовая полоса, что у формы офиса и источника фида: одна и та же речь
       сервера, показанная у поля, а не растворённая в тосте. */
    .domoplaner-map-error {
      margin: 10px 0 0;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-rose);
      background: var(--glass-hover);
      font-size: 0.85rem;
      line-height: 1.5;
      color: var(--text-body);
      overflow-wrap: anywhere;
    }

    @media (max-width: 480px) {
      .crm-provider-choice { grid-template-columns: 1fr; }
      .crm-subdomain-field { flex-direction: column; align-items: stretch; }
      /* Тот же дефект базиса: `flex: 1 1 220px` выше — это ширина, пока ось
         горизонтальна, и ВЫСОТА, как только направление стало колонкой. Поле
         адреса аккаунта вытягивалось в прямоугольник на четверть экрана. */
      .crm-subdomain-field .form-input { flex: 0 1 auto; }
      .crm-subdomain-suffix { align-self: flex-start; }
    }

    /* ------------------------------------------------------------------
       Маршрутизация сделок amoCRM: строка «Все сделки» плюс список правил.

       Строка правила — не таблица намеренно. Столбцы с шапкой держат ширину
       шести колонок и на ноутбуке уезжают в горизонтальную прокрутку, а
       читать правило приходится целиком: ключ + три поля. Здесь каждая
       строка складывается сама (grid auto-fit), а подпись поля живёт РЯДОМ
       с полем, а не в далёкой шапке.
       ------------------------------------------------------------------ */
    #crm-amocrm-routing {
      margin-top: 20px;
      padding-top: 18px;
      border-top: 1px solid var(--glass-border);
    }
    .amo-routing-head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      flex-wrap: wrap;
    }
    .amo-routing-head-text { min-width: 0; flex: 1 1 320px; }
    .amo-routing-title {
      margin: 0;
      font-family: var(--font-display);
      font-size: 0.98rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .amo-routing-subtitle { margin: 6px 0 0; font-size: 0.8rem; line-height: 1.5; }
    .amo-routing-head .btn { flex-shrink: 0; }

    /* Состояние справочника. Молчание здесь опаснее лишней строки: настроенная
       маршрутизация продолжает работать и с непрочитанным списком, но владелец
       обязан знать, что видит СНИМОК имён, а не сегодняшнюю amoCRM. */
    .amo-routing-note {
      margin-top: 10px;
      font-size: 0.8rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .amo-routing-note:empty { display: none; }
    .amo-routing-note.is-warning { color: var(--accent-amber); }

    .amo-routing-row {
      display: grid;
      grid-template-columns: minmax(200px, 260px) minmax(0, 1fr) auto;
      align-items: start;
      gap: 12px;
      margin-top: 12px;
      padding: 12px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .amo-routing-row:hover { border-color: rgba(99, 102, 241, 0.35); }
    /* «Все сделки» — основа настройки, а не запасной вариант: отсюда индиго. */
    .amo-routing-row.is-default {
      border-color: rgba(99, 102, 241, 0.4);
      background: rgba(99, 102, 241, 0.08);
    }
    .amo-routing-key { min-width: 0; display: flex; flex-direction: column; gap: 6px; }
    .amo-routing-key-title {
      font-size: 0.9rem;
      font-weight: 600;
      color: var(--text-main);
      line-height: 1.3;
    }
    .amo-routing-key-desc {
      font-size: 0.76rem;
      color: var(--text-muted);
      line-height: 1.45;
      overflow-wrap: anywhere;
    }
    .amo-routing-key .form-select { width: 100%; margin: 0; }

    /* Место строки в очереди разбора. Пилюля, а не абзац: очерёдность надо
       считывать взглядом сверху вниз, не вчитываясь. */
    .amo-routing-order {
      align-self: flex-start;
      padding: 2px 9px;
      border-radius: var(--radius-pill);
      border: 1px solid var(--glass-border);
      background: rgba(148, 163, 184, 0.12);
      font-size: 0.7rem;
      font-weight: 600;
      letter-spacing: 0.02em;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .amo-routing-row.is-default .amo-routing-order {
      border-color: rgba(99, 102, 241, 0.4);
      background: rgba(99, 102, 241, 0.14);
      color: var(--accent-indigo-soft);
    }

    /* Строка, которую перехватывает правило выше. Она НЕ ошибка ввода и не
       мешает сохранению — она просто никогда не выстрелит, и выглядеть должна
       предупреждением (янтарь), а не поломкой (роза). */
    .amo-routing-row.is-shadowed {
      border-color: rgba(245, 158, 11, 0.5);
      background: rgba(245, 158, 11, 0.08);
    }
    .amo-routing-shadow {
      font-size: 0.74rem;
      line-height: 1.45;
      color: var(--accent-amber);
    }

    .amo-routing-fields {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
      gap: 10px;
      min-width: 0;
    }
    .amo-routing-field { min-width: 0; }
    .amo-routing-field label {
      display: block;
      margin-bottom: 4px;
      font-size: 0.74rem;
      font-weight: 600;
      letter-spacing: 0.02em;
      color: var(--text-muted);
    }
    .amo-routing-field .form-select { width: 100%; margin: 0; }

    .amo-routing-remove { flex-shrink: 0; align-self: center; }

    /* Ссылка на пропавший в amoCRM объект. Строка остаётся видимой и
       редактируемой: спрятать её значило бы стереть настройку владельца за
       него, а отправку она не ломает — поле просто не уходит. */
    .amo-routing-field.is-missing .form-select {
      border-color: rgba(244, 63, 94, 0.5);
      color: var(--accent-rose);
    }
    .amo-routing-missing {
      display: block;
      margin-top: 4px;
      font-size: 0.72rem;
      line-height: 1.4;
      color: var(--accent-rose);
    }

    /* Этап существует, но выбрать его нельзя (служебные «Успешно реализовано» и
       «Закрыто и не реализовано»). ЯНТАРЬ, А НЕ РОЗА, и это не оформительская
       мелочь: розовым кабинет говорит «сломано, объекта нет» — здесь объект на
       месте, настройка цела, но работает не так, как выглядит. Цвет обязан
       отличать «почини, иначе потеряешь» от «уточни, иначе получится не то». */
    .amo-routing-field.is-unselectable .form-select {
      border-color: rgba(245, 158, 11, 0.5);
      color: var(--accent-amber);
    }
    .amo-routing-unselectable {
      display: block;
      margin-top: 4px;
      font-size: 0.72rem;
      line-height: 1.4;
      color: var(--accent-amber);
    }

    .amo-routing-empty { margin: 12px 0 0; font-size: 0.8rem; line-height: 1.5; }
    .amo-routing-order-note {
      margin: 14px 0 0;
      padding-left: 11px;
      border-left: 2px solid rgba(99, 102, 241, 0.45);
      font-size: 0.78rem;
      line-height: 1.5;
    }
    .amo-routing-actions { margin-top: 14px; }

    @media (max-width: 900px) {
      .amo-routing-row {
        grid-template-columns: minmax(0, 1fr) auto;
      }
      .amo-routing-fields { grid-column: 1 / -1; }
    }
    @media (max-width: 480px) {
      .amo-routing-head { flex-direction: column; align-items: stretch; }
      /* Базис 320px задавался под строку, где он означает ШИРИНУ. В колонке та же
         цифра становится ВЫСОТОЙ — и между пояснением и кнопкой зияла пустая
         полоса в четверть экрана. Найдено живым просмотром на 420px. */
      .amo-routing-head-text { flex: 0 1 auto; }
      .amo-routing-head .btn { width: 100%; justify-content: center; }
      .amo-routing-row { grid-template-columns: minmax(0, 1fr); padding: 12px; }
      .amo-routing-fields { grid-template-columns: 1fr; }
      .amo-routing-remove { align-self: stretch; }
      .amo-routing-remove .btn { width: 100%; justify-content: center; }
      .amo-routing-actions { flex-direction: column; align-items: stretch; }
      .amo-routing-actions .btn { width: 100%; justify-content: center; }
    }

    /* Светлая тема: полупрозрачный индиго на жемчужном фоне читается бледнее,
       чем на обсидиане, — подложку строки «Все сделки» усиливаем. */
    [data-theme="light"] .amo-routing-row.is-default {
      background: rgba(99, 102, 241, 0.1);
      border-color: rgba(99, 102, 241, 0.32);
    }
    [data-theme="light"] .amo-routing-row {
      background: rgba(255, 255, 255, 0.7);
    }

    /* Property Comparison Matrix */
    .comparison-matrix-table {
      width: 100%;
      border-collapse: collapse;
      font-size: 0.85rem;
      background: rgba(15, 23, 42, 0.4);
      border-radius: var(--radius-sm);
      overflow: hidden;
      margin-bottom: 0;
    }
    /* Отступ снизу переехал на контейнер прокрутки, иначе полоса прокрутки
       рисовалась внутри отступа и отрывалась от нижнего края таблицы. */
    .comparison-matrix-scroll {
      margin-bottom: 12px;
      background: transparent;
    }
    .comparison-matrix-table th {
      background: rgba(99, 102, 241, 0.18);
      color: var(--text-main);
      padding: 10px 14px;
      text-align: left;
      white-space: nowrap;
    }
    .comparison-matrix-table td {
      padding: 10px 14px;
      border-top: 1px solid var(--glass-border);
      color: var(--text-body);
    }
    .comparison-matrix-table td.rowlabel {
      color: var(--text-muted);
      font-weight: 600;
      white-space: nowrap;
    }
    .comparison-matrix-table td.winner-cell {
      color: var(--accent-green);
      font-weight: 700;
    }
    .comparison-summary-box {
      background: rgba(16, 185, 129, 0.08);
      border-left: 3px solid var(--accent-green);
      border-radius: var(--radius-sm);
      padding: 12px 16px;
      font-size: 0.9rem;
      color: var(--text-main);
      line-height: 1.5;
    }

    .faq-card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    /* Название модуля стоит в шапке слева от тумблера. Длинный заголовок
       переносится по словам и не выдавливает тумблер (у него flex-shrink: 0). */
    .module-card-title {
      font-family: var(--font-display);
      font-weight: 700;
      font-size: 1.05rem;
      line-height: 1.3;
      color: var(--text-main);
      min-width: 0;
      padding-right: 12px;
      overflow-wrap: anywhere;
      word-break: break-word;
    }

    /* Технический ключ модуля — служебная подпись для поддержки, а не заголовок. */
    .module-card-key {
      margin-top: 10px;
      font-family: var(--font-mono);
      font-size: 0.72rem;
      color: var(--text-muted);
      opacity: 0.75;
      word-break: break-all;
    }

    .category-tag {
      font-size: 0.75rem;
      text-transform: uppercase;
      font-weight: 700;
      letter-spacing: 0.5px;
      padding: 4px 10px;
      border-radius: 6px;
      background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(34, 211, 238, 0.15));
      color: var(--accent-indigo-soft);
      border: 1px solid rgba(99, 102, 241, 0.3);
    }

    .keywords-list {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: 6px;
    }

    .keyword-chip {
      font-size: 0.8rem;
      background: rgba(51, 65, 85, 0.5);
      color: var(--text-body);
      padding: 3px 8px;
      border-radius: 4px;
      border: 1px solid var(--glass-border);
    }

    /* Заметки в карточке квартиры: 📝 для ИИ (видит клиент) и 🔒 служебная для менеджера */
    .property-note {
      margin-top: 8px;
      padding: 6px 9px;
      border-radius: 8px;
      font-size: 0.8rem;
      line-height: 1.4;
      border: 1px solid var(--glass-border);
      color: var(--text-body, #cbd5e1);
    }
    .property-note-ai {
      background: rgba(52, 211, 153, 0.10);
      border-color: rgba(52, 211, 153, 0.28);
    }
    .property-note-internal {
      background: rgba(245, 158, 11, 0.10);
      border-color: rgba(245, 158, 11, 0.30);
    }
    .property-note-label { font-weight: 600; color: var(--text-main); }

    /* Длинная заметка живёт в карточке свёрнутой: видны ровно две строки, полный
       текст раскрывается по клику здесь же. Ограничение именно по СТРОКАМ, а не
       по числу символов: обрезка по символам давала соседним карточкам разную
       высоту блока (короткие слова укладывались в меньшее число строк).
       Класс is-clamped ставит JS только тем заметкам, которые реально не влезли
       в две строки, — у остальных кнопка «Показать полностью» не нужна. */
    .property-note-body {
      white-space: pre-wrap;
      overflow-wrap: anywhere;
    }
    .property-note.is-clamped:not(.is-expanded) .property-note-body {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      overflow: hidden;
    }
    .property-note:not(.is-clamped) .property-note-toggle { display: none; }

    .property-note-toggle {
      display: block;
      margin-top: 6px;
      padding: 0;
      background: none;
      border: none;
      font: inherit;
      font-size: 0.75rem;
      font-weight: 600;
      color: var(--accent-indigo);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .property-note-toggle:hover { color: var(--accent-cyan); }
    .property-note-toggle:focus-visible {
      outline: 2px solid var(--accent-indigo);
      outline-offset: 2px;
      border-radius: 4px;
    }

    /* Характеристики квартиры в карточке каталога: кухня, жилая, лоджии, секция,
       вид из окна. Строка из пар «подпись — значение», которая переносится по
       ширине карточки. Незаполненного значения здесь нет вовсе: пустая площадь
       не показывается ни нулём, ни прочерком — её называет форма правки. */
    .property-specs {
      display: flex;
      flex-wrap: wrap;
      gap: 4px 12px;
      margin-top: 6px;
      font-size: 0.85rem;
      color: var(--text-muted);
    }
    .property-specs strong { color: var(--text-body); font-weight: 600; }
    /* Срок сдачи и состояние стройки — данные дома, а не квартиры. Отдельная
       строка со своей подписью: иначе владелец правил бы их в карточке и не
       понимал, почему правка возвращается после следующей правки дома. */
    .property-inherited-line {
      margin-top: 6px;
      font-size: 0.85rem;
      color: var(--text-muted);
    }
    .property-inherited-line strong { color: var(--text-body); font-weight: 600; }
    .property-inherited-line .property-inherited-src {
      font-size: 0.75rem;
      opacity: 0.85;
    }

    /* Блок характеристик в форме квартиры */
    .property-specs-block {
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--surface-tint-weak);
      padding: 12px 14px;
      margin-bottom: 12px;
    }
    .property-specs-title,
    .property-inherited-title {
      font-size: 0.8rem;
      font-weight: 700;
      letter-spacing: 0.02em;
      text-transform: uppercase;
      color: var(--text-muted);
      margin-bottom: 10px;
    }
    .property-specs-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 0 12px;
    }
    .property-specs-wide { grid-column: 1 / -1; }
    /* Честное предупреждение про синхронизацию — спокойной янтарной полосой, как
       заметка о значениях из выгрузки в форме правки дома: это та же речь про
       тот же фид. */
    .property-feed-note {
      margin: 6px 0 0;
      padding: 8px 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-amber);
      background: var(--glass-hover);
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .property-feed-note[hidden] { display: none; }

    /* Данные дома в форме квартиры: показ + переход к правке дома */
    .property-inherited {
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-indigo);
      border-radius: var(--radius-sm);
      background: var(--surface-tint-weak);
      padding: 12px 14px;
      margin-bottom: 12px;
    }
    .property-inherited[hidden] { display: none; }
    .property-inherited-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
    }
    .property-inherited-head .property-inherited-title { margin-bottom: 0; }
    .property-inherited-edit { padding: 6px 12px; font-size: 0.8rem; }
    /* .btn задаёт display: inline-flex и перебивает браузерное [hidden] —
       без этой строки кнопка осталась бы видна и роли без catalog:write. */
    .property-inherited-edit[hidden] { display: none; }
    .property-inherited-rows {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 6px 12px;
      margin: 10px 0 8px;
    }
    .property-inherited-row {
      display: flex;
      flex-direction: column;
      gap: 2px;
      font-size: 0.9rem;
      color: var(--text-body);
    }
    .property-inherited-label {
      font-size: 0.75rem;
      color: var(--text-muted);
    }
    @media (max-width: 480px) {
      .property-specs-grid,
      .property-inherited-rows { grid-template-columns: minmax(0, 1fr); }
      .property-inherited-head { flex-direction: column; align-items: flex-start; gap: 8px; }
      .property-inherited-edit { width: 100%; }
    }

    /* Контрол 2D-чертежа (PDF) в форме объекта: выбор файла и действия над уже
       загруженным чертежом стоят одной строкой, на узком экране — колонкой. */
    .plan-pdf-row {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }
    .plan-pdf-row .form-input[type="file"] {
      flex: 1 1 240px;
      min-width: 0;
      padding: 8px 10px;
      font-size: 0.85rem;
      cursor: pointer;
    }
    .plan-pdf-row .form-input[type="file"]::file-selector-button {
      margin-right: 10px;
      padding: 5px 12px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      color: var(--text-body);
      font: inherit;
      font-size: 0.8rem;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .plan-pdf-row .form-input[type="file"]:hover::file-selector-button {
      border-color: var(--accent-indigo);
      color: var(--text-main);
    }
    .plan-pdf-row .btn { flex: 0 0 auto; white-space: nowrap; }

    @media (max-width: 480px) {
      .plan-pdf-row { flex-direction: column; align-items: stretch; }
      /* БАЗИС СЧИТАЕТСЯ ПО ГЛАВНОЙ ОСИ, а она здесь уже вертикальная: `240px`
         из строки выше задавал бы полю не ширину, а ВЫСОТУ — поле выбора файла
         занимало бы пол-экрана телефона. Тот же дефект был найден 18.08.2026 в
         шапке маршрутизации amoCRM и в поле адреса аккаунта. */
      .plan-pdf-row .form-input[type="file"] { flex: 0 1 auto; }
      .plan-pdf-row .btn { justify-content: center; width: 100%; }

      /* Фильтров каталога стало семь (ЖК, регион, город, статус, тип помещения,
         отделка, поиск):
         на узком экране они идут колонкой во всю ширину, иначе селекторы с
         min-width: 150px вылезают за край панели. Правило ограничено вкладкой
         каталога, чтобы не трогать остальные панели фильтров. */
      #tab-properties .toolbar-filters .form-group { flex: 1 1 100%; }
      #tab-properties .toolbar-filters .form-select,
      #tab-properties .toolbar-filters .form-input { width: 100%; min-width: 0; }
    }

    .faq-response-preview {
      font-size: 0.9rem;
      line-height: 1.5;
      color: var(--text-main);
      background: rgba(15, 23, 42, 0.5);
      padding: 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-purple);
      white-space: pre-wrap;
      max-height: 140px;
      overflow-y: auto;
    }

    .faq-actions {
      display: flex;
      justify-content: flex-end;
      gap: 8px;
      border-top: 1px solid var(--row-border);
      padding-top: 12px;
    }

    /* Modal Dialog */
    .modal-overlay {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(15, 23, 42, 0.8);
      backdrop-filter: var(--blur-modal-deep);
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 1000;
      opacity: 0;
      pointer-events: none;
      /* visibility, а не только opacity: прозрачное окно остаётся в обходе
         клавиатурой. В кабинете 35 таких окон, и Tab по любому разделу
         проваливался в их поля и кнопки — фокус уезжал в никуда, экран при
         этом не менялся. Скрытие задерживаем на длительность затухания,
         чтобы окно не исчезало рывком. */
      visibility: hidden;
      transition: opacity 0.2s ease, backdrop-filter 0.2s ease, visibility 0s linear 0.2s;
      padding: 16px;
    }

    .modal-overlay.active {
      opacity: 1;
      pointer-events: auto;
      visibility: visible;
      transition: opacity 0.2s ease, backdrop-filter 0.2s ease, visibility 0s;
    }

    .modal-card {
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-lg);
      width: 100%;
      max-width: 580px;
      max-height: 85vh;
      overflow-y: auto;
      padding: 28px;
      display: flex;
      flex-direction: column;
      gap: 20px;
      box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
      transform: scale(0.95);
      transition: all 0.2s ease;
    }

    .modal-overlay.active .modal-card {
      transform: scale(1);
    }

    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .modal-header h3 {
      font-size: 1.2rem;
      font-weight: 700;
    }

    /* Кнопки управления окном (развернуть + закрыть) держатся вместе справа */
    .modal-header-actions {
      display: flex;
      gap: 6px;
      align-items: center;
      flex: 0 0 auto;
    }

    .form-group {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .form-group label {
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text-muted);
    }

    .form-input, .form-textarea, .form-select {
      width: 100%;
      padding: 12px;
      background: rgba(15, 23, 42, 0.7);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      color: var(--text-main);
      font-size: 0.9rem;
      outline: none;
    }

    .form-input:focus, .form-textarea:focus, .form-select:focus {
      border-color: var(--accent-purple);
      box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
    }

    /* Мышью поле видно и без обода — курсор стоит в нём. Клавиатурой видно
       только подсветку, а она держалась на рамке 1px и полупрозрачном ореоле
       (0.2 альфы даёт около 1.3:1 к фону карточки — на глаз это ничто).
       `outline: none` в базовом правиле весит больше обода по умолчанию, так
       что нулевой :where() сюда не достаёт: правило нужно именное. */
    .form-input:focus-visible,
    .form-textarea:focus-visible,
    .form-select:focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }

    .form-textarea {
      resize: vertical;
      min-height: 110px;
    }

    /* Поле только для чтения (просмотр штатного сценария тренажёра, замороженный
       код сценария при правке). Не `disabled`: текст должен оставаться
       выделяемым и копируемым — ради чтения просмотр и открывают. Приглушённая
       заливка и курсор `default` при этом говорят, что править нечего. */
    .form-input.is-readonly,
    .form-textarea.is-readonly {
      background: rgba(15, 23, 42, 0.35);
      color: var(--text-body);
      cursor: default;
      transition: all 0.2s ease;
    }

    .form-input.is-readonly:focus,
    .form-textarea.is-readonly:focus {
      border-color: var(--glass-border);
      box-shadow: none;
    }

    /* Приглушение выше говорит «править нечего», но оно снимало и рамку, и
       ореол разом: поле остаётся в обходе табом (текст читают и копируют), а
       где стоит каретка — не видно ничем. Обод возвращаем, подсветку рамки —
       нет: поле по-прежнему не выглядит редактируемым. */
    .form-input.is-readonly:focus-visible,
    .form-textarea.is-readonly:focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }

    [data-theme="light"] .form-input.is-readonly,
    [data-theme="light"] .form-textarea.is-readonly {
      background: var(--bg-deep);
    }

    .modal-actions {
      display: flex;
      justify-content: flex-end;
      gap: 12px;
      margin-top: 8px;
    }

    /* --- Переиспользуемые диалоги (#app-confirm-modal / #app-prompt-modal) --- */
    /* Уже, чем обычная модалка: диалог несёт один вопрос, и широкая карточка
       растягивала бы две строки текста на всю ширину экрана. */
    .modal-card.app-dialog-card {
      max-width: 440px;
      gap: 16px;
    }

    /* pre-line сохраняет пустую строку между сутью действия и его последствием —
       так же, как это делал перенос \n\n в нативном confirm(). */
    .app-dialog-message {
      white-space: pre-line;
      font-size: 0.9rem;
      line-height: 1.55;
      color: var(--text-body);
      margin: 0;
    }

    .app-dialog-error {
      font-size: 0.82rem;
      color: var(--accent-rose);
    }

    /* --- Предпросмотр документа перед загрузкой (#doc-preview-modal) ---
       Шире обычного диалога: главное в окне — образец извлечённого текста,
       который человек должен прочитать, а не угадать по обрывкам строк. */
    .modal-card.doc-preview-card {
      max-width: 620px;
      gap: 16px;
      /* Отступ карточки продублирован переменной: липкая полоса кнопок ниже
         должна вылезать ровно на него, а он меняется на телефоне (18px). */
      --doc-preview-pad: 28px;
    }
    /* Кнопки решения не должны уезжать за нижний край: на телефоне разбор
         буклета выше экрана, и «Загрузить» пришлось бы искать прокруткой
         внутри карточки. Полоса липнет ко дну собственного скролла карточки. */
    .modal-card.doc-preview-card .modal-actions {
      position: sticky;
      bottom: calc(var(--doc-preview-pad) * -1);
      margin: 0 calc(var(--doc-preview-pad) * -1) calc(var(--doc-preview-pad) * -1);
      padding: 12px var(--doc-preview-pad) var(--doc-preview-pad);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      -webkit-backdrop-filter: var(--blur-glass);
    }
    /* Кнопки этого окна НЕ наследуют `transition: all` от .btn.
       Причина найдена глазами: `all` анимирует и `visibility`, а окно включает
       кнопку загрузки уже после ответа сервера — то есть посреди собственного
       появления оверлея. Переход по visibility залипал, и главная кнопка
       («Загрузить в базу знаний») не отрисовывалась вовсе: владелец видел
       разбор файла и одну «Отмену». Перечисляем анимируемое явно. */
    .modal-card.doc-preview-card .modal-actions .btn {
      /* opacity сюда НЕ входит намеренно: кнопка гаснет и зажигается по
         готовности разбора, и переход по прозрачности застаёт её в
         полупрозрачном виде — кнопка выглядит нерабочей, хотя нажимается. */
      transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
    }
    .modal-card.doc-preview-card .modal-actions .btn:disabled {
      opacity: 0.45;
      cursor: not-allowed;
      transform: none;
    }
    .doc-preview-loading {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 22px 4px;
      font-size: 0.9rem;
      color: var(--text-muted);
    }
    .doc-preview-body {
      display: flex;
      flex-direction: column;
      gap: 14px;
    }
    /* «Сервер занят» — не поломка и не запрет: янтарный, а не розовый, иначе
       владелец прочитает это как «файл не годится» и пойдёт искать другой. */
    .doc-preview-busy {
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 16px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: color-mix(in srgb, var(--accent-amber) 7%, var(--card-bg));
    }
    .doc-preview-busy-head {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .doc-preview-busy-icon { line-height: 1; }
    .doc-preview-busy-title {
      font-family: var(--font-display);
      font-weight: 600;
      color: var(--text-main);
    }
    .doc-preview-busy-text {
      margin: 0;
      font-size: 0.92rem;
      line-height: 1.55;
      color: var(--text-body);
    }
    .doc-preview-busy-hint {
      margin: 0;
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .doc-preview-busy-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-top: 2px;
    }
    .doc-preview-busy-actions .btn {
      transition: all 0.2s ease;
    }
    .doc-preview-busy-actions .btn:disabled {
      opacity: 0.45;
      cursor: not-allowed;
      transform: none;
    }
    @media (max-width: 480px) {
      .doc-preview-busy-actions { flex-direction: column; align-items: stretch; }
      .doc-preview-busy-actions .btn { width: 100%; }
    }
    .doc-preview-file {
      display: flex;
      align-items: baseline;
      flex-wrap: wrap;
      gap: 4px 10px;
    }
    .doc-preview-filename {
      font-family: var(--font-display);
      font-weight: 600;
      color: var(--text-main);
      word-break: break-word;
    }
    .doc-preview-meta {
      font-size: 0.78rem;
      color: var(--text-muted);
    }
    /* Числа человеческим языком: «из 83 МБ извлеклось 5 400 знаков» — это
       основная фраза окна, поэтому она крупнее и контрастнее замечаний. */
    .doc-preview-summary {
      margin: 0;
      font-size: 0.92rem;
      line-height: 1.55;
      color: var(--text-body);
    }
    .doc-preview-notes {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .doc-preview-note {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      padding: 9px 12px;
      border: 1px solid var(--glass-border);
      border-left-width: 3px;
      border-radius: var(--radius-sm);
      background: var(--card-bg);
      font-size: 0.84rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    /* Тон замечания = его цена. Спокойный (починка разрывов — норма для
       буклетов), предупреждающий (мало текста) и блокирующий (файл не пройдёт). */
    .doc-preview-note.is-calm { border-left-color: var(--accent-cyan); }
    .doc-preview-note.is-warn { border-left-color: var(--accent-amber); }
    .doc-preview-note.is-block { border-left-color: var(--accent-rose); }
    /* «Посмотрите и решите»: личные контакты в документе. Не ошибка и не запрет —
       индиго, а не розовый, иначе владелец примет предупреждение за отказ. */
    .doc-preview-note.is-notice {
      border-left-color: var(--accent-indigo);
      background: color-mix(in srgb, var(--accent-indigo) 8%, var(--card-bg));
    }
    .doc-preview-note-icon {
      flex: 0 0 auto;
      line-height: 1;
      margin-top: 1px;
    }
    .doc-preview-note.is-calm .doc-preview-note-icon { color: var(--accent-cyan); }
    .doc-preview-note.is-warn .doc-preview-note-icon { color: var(--accent-amber); }
    .doc-preview-note.is-block .doc-preview-note-icon { color: var(--accent-rose); }
    .doc-preview-note.is-notice .doc-preview-note-icon {
      color: var(--accent-indigo);
      font-weight: 700;
      font-style: italic;
      font-family: var(--font-display);
    }

    .doc-preview-sample-label {
      font-size: 0.75rem;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      font-family: var(--font-display);
      color: var(--text-muted);
      margin-bottom: 6px;
    }
    /* Восемь-десять строк с прокруткой: показать надо достаточно, чтобы человек
       узнал свой документ, но не столько, чтобы кнопки уехали за экран. */
    .doc-preview-sample {
      max-height: 190px;
      overflow-y: auto;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: rgba(15, 23, 42, 0.4);
      font-size: 0.83rem;
      line-height: 1.6;
      color: var(--text-body);
      white-space: pre-wrap;
      word-break: break-word;
      transition: border-color 0.2s ease;
    }
    .doc-preview-sample:focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }
    .doc-preview-sample.is-empty {
      color: var(--text-muted);
      font-style: italic;
      white-space: normal;
    }
    [data-theme="light"] .doc-preview-sample { background: rgba(15, 23, 42, 0.04); }

    @media (max-width: 768px) {
      .modal-card.doc-preview-card { --doc-preview-pad: 18px; }
      .doc-preview-sample { max-height: 160px; }
    }
    @media (max-width: 480px) {
      .modal-card.doc-preview-card .modal-actions {
        flex-direction: column-reverse;
        gap: 8px;
      }
      .modal-card.doc-preview-card .modal-actions .btn {
        width: 100%;
        justify-content: center;
      }
      .doc-preview-sample { max-height: 140px; font-size: 0.8rem; }
    }

    /* --- Правка текста документа базы знаний (#doc-text-modal) ---------------
       Окно шире и выше предпросмотра: здесь не «образец на восемь строк», а
       рабочее место — владелец правит методичку по двадцать минут. Поле тянется
       по высоте окна, а полоса кнопок и предупреждение липнут ко дну, чтобы
       «Сохранить и переиндексировать» не приходилось искать прокруткой. */
    .modal-card.doc-text-card {
      max-width: 900px;
      width: 100%;
      gap: 14px;
      --doc-text-pad: 28px;
    }
    .doc-text-loading {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 22px 4px;
      font-size: 0.9rem;
      color: var(--text-muted);
    }
    .doc-text-body {
      display: flex;
      flex-direction: column;
      gap: 12px;
      min-height: 0;
    }
    .doc-text-head {
      display: flex;
      align-items: baseline;
      flex-wrap: wrap;
      gap: 4px 12px;
    }
    .doc-text-name {
      font-family: var(--font-display);
      font-weight: 600;
      font-size: 1rem;
      color: var(--text-main);
      word-break: break-word;
    }
    .doc-text-meta {
      font-size: 0.8rem;
      color: var(--text-muted);
      margin-left: auto;
      white-space: nowrap;
    }
    .doc-text-field { display: flex; flex-direction: column; gap: 6px; min-height: 0; }
    .doc-text-label {
      font-size: 0.75rem;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      font-family: var(--font-display);
      color: var(--text-muted);
    }
    .form-textarea.doc-text-area {
      min-height: 46vh;
      max-height: 58vh;
      font-size: 0.86rem;
      line-height: 1.6;
      color: var(--text-body);
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }
    .doc-text-field-head {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 8px 12px;
      flex-wrap: wrap;
    }
    .doc-text-counter {
      font-size: 0.78rem;
      color: var(--text-muted);
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }
    .doc-text-counter .is-changed { color: var(--accent-amber); font-weight: 600; }
    /* Предупреждение стоит вплотную к кнопке и липнет вместе с ней: человек
       читает его в момент решения, а не при открытии окна, когда до правки
       ещё двадцать минут. */
    /* Двойной класс намеренно: базовые правила .notice-box лежат в <style> самой
       страницы, то есть НИЖЕ этого файла в каскаде, и при равной специфичности
       выигрывали бы её полупрозрачный фон. */
    .notice-box.doc-text-warning {
      position: sticky;
      bottom: 0;
      margin: 0;
      /* Заливка непрозрачная намеренно: у .notice-box фон полупрозрачный, и на
         узком экране (проверено на 390px) сквозь липкую плашку просвечивал текст
         документа — предупреждение читалось поверх строк регламента. */
      background: var(--bg-deep);
    }
    .modal-card.doc-text-card .modal-actions {
      position: sticky;
      bottom: calc(var(--doc-text-pad) * -1);
      margin: 0 calc(var(--doc-text-pad) * -1) calc(var(--doc-text-pad) * -1);
      padding: 12px var(--doc-text-pad) var(--doc-text-pad);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      -webkit-backdrop-filter: var(--blur-glass);
    }
    /* Тот же разбор, что у предпросмотра: `transition: all` от .btn анимирует
       и visibility, а кнопка сохранения включается уже после ответа сервера. */
    .modal-card.doc-text-card .modal-actions .btn {
      transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
    }
    .modal-card.doc-text-card .modal-actions .btn:disabled {
      opacity: 0.45;
      cursor: not-allowed;
      transform: none;
    }
    @media (max-width: 768px) {
      .modal-card.doc-text-card { --doc-text-pad: 18px; }
      /* Ниже, чем на десктопе, намеренно: под полем идёт счётчик с отметкой
         «есть несохранённые правки», а липкая плашка предупреждения накрывала
         его — на 390px это единственный признак, что работа не сохранена. */
      .form-textarea.doc-text-area { min-height: 34vh; max-height: 46vh; }
      .doc-text-meta { margin-left: 0; white-space: normal; }
    }
    @media (max-width: 480px) {
      .form-textarea.doc-text-area { min-height: 32vh; max-height: 42vh; font-size: 0.83rem; }
      .modal-card.doc-text-card .modal-actions {
        flex-direction: column-reverse;
        gap: 8px;
      }
      .modal-card.doc-text-card .modal-actions .btn {
        width: 100%;
        justify-content: center;
      }
    }

    @media (max-width: 480px) {
      /* На узком экране кнопки диалога встают колонкой и растягиваются:
         подтверждение опасного действия не должно требовать точного попадания. */
      .modal-card.app-dialog-card .modal-actions {
        flex-direction: column-reverse;
        gap: 8px;
      }
      .modal-card.app-dialog-card .modal-actions .btn {
        width: 100%;
        justify-content: center;
      }
      /* Три кнопки в ряд (Отмена · Скопировать в свой · Сохранить) на 480px
         сжимаются до нечитаемых огрызков — разрешаем перенос. */
      .modal-actions {
        flex-wrap: wrap;
        gap: 8px;
      }
    }

    /* CRM Table */
    .table-container {
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: thin;
      scrollbar-color: rgba(139, 92, 246, 0.4) transparent;
    }
    .table-container::-webkit-scrollbar { height: 6px; }
    .table-container::-webkit-scrollbar-track { background: transparent; }
    .table-container::-webkit-scrollbar-thumb {
      background: rgba(139, 92, 246, 0.4);
      border-radius: var(--radius-pill);
    }
    .table-container::-webkit-scrollbar-thumb:hover { background: rgba(139, 92, 246, 0.65); }

    /* Постраничная листалка списков сделок и диалогов: общий бар под таблицей. */
    .leads-pagination {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 10px;
      margin-top: 12px;
      padding: 10px 14px;
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
    }
    .leads-pagination-info {
      font-size: 0.85rem;
      color: var(--text-muted);
    }
    .leads-pagination-controls {
      display: flex;
      gap: 8px;
    }
    .leads-pagination-controls button:disabled {
      opacity: 0.45;
      cursor: not-allowed;
    }
    @media (max-width: 768px) {
      .leads-pagination { flex-direction: column; align-items: stretch; }
      .leads-pagination-controls { justify-content: center; }
    }

    table {
      width: 100%;
      min-width: 640px;
      border-collapse: collapse;
      text-align: left;
      font-size: 0.9rem;
    }

    /* Колонка контента сузилась под сайдбар, поэтому шаг ячеек компактнее:
       16px по горизонтали уводили таблицы сделок и документов в перенос. */
    th {
      background: var(--bg-mid);
      padding: 12px 14px;
      font-weight: 600;
      color: var(--text-muted);
      border-bottom: 1px solid var(--glass-border);
      white-space: nowrap;
    }

    td {
      padding: 12px 14px;
      color: var(--text-body);
      border-bottom: 1px solid var(--row-border);
    }

    tr:hover td {
      background: rgba(99, 102, 241, 0.06);
    }

    /* Кнопки действий внутри ячейки: раскладку держит вложенный div, а не сам
       <td>. Инлайновый display:flex на ячейке ломает табличную геометрию —
       нижняя граница строки рвётся и уезжает. */
    .table-actions-cell {
      display: flex;
      gap: 6px;
      align-items: center;
      flex-wrap: wrap;
    }
    .table-actions-cell.stacked {
      flex-direction: column;
      align-items: stretch;
    }
    /* Единый размер кнопки действия в таблице: раньше каждая строка носила свои
       инлайновые font-size/padding, и колонки «Действия» разъезжались. */
    .btn-table-action {
      font-size: 0.8rem;
      padding: 4px 10px;
      line-height: 1;
    }
    /* Кнопка только с иконкой — квадратная, чтобы пара «карандаш / корзина»
       не прыгала по ширине. */
    .btn-table-action.icon-only {
      padding: 6px;
      min-width: 32px;
      justify-content: center;
    }

    /* Сортируемые заголовки таблицы сделок */
    th.sortable {
      cursor: pointer;
      user-select: none;
      transition: color 0.2s ease, background 0.2s ease;
    }
    th.sortable:hover { color: var(--text-main); background: var(--glass-hover); }
    th.sortable:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; }
    /* Стрелка — псевдоэлемент из data-атрибута: текст самого заголовка остаётся
       чистым, а сортировку рисует CSS. */
    th.sortable::after {
      content: attr(data-arrow);
      display: inline-block;
      margin-left: 6px;
      font-size: 0.7rem;
      opacity: 0.35;
    }
    th.sortable.sorted { color: var(--text-main); }
    th.sortable.sorted::after { opacity: 1; color: var(--accent-purple); }

    /* Шкала квалификации лида — адаптивный трек для темной и светлой тем */
    .bant-bar-track {
      width: 72px;
      height: 8px;
      border-radius: var(--radius-pill);
      background: rgba(148, 163, 184, 0.2);
      border: 1px solid var(--panel-border, var(--glass-border));
      overflow: hidden;
      display: inline-block;
      margin-right: 8px;
      vertical-align: middle;
    }
    .bant-bar-fill {
      display: block;
      height: 100%;
      border-radius: var(--radius-pill);
      background: linear-gradient(90deg, #6366f1, #a855f7, #22c55e);
      transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
    }

    /* Прогрев в строке «Сделок»: полоса и бейдж со словом стадии одним рядом.
       Раньше оба элемента лежали в ячейке инлайново, и подросший бейдж
       переносился по строчным правилам — с рваным вертикальным ритмом рядом с
       мини-воронкой. Flex с переносом делает поведение предсказуемым: не
       хватило ширины — бейдж уходит под полосу целиком, а не рвётся. */
    .lead-score-row {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 4px 0;
      margin-bottom: 6px;
    }
    /* Слово стадии внутри бейджа. Отдельный span, а не голый текст: во-первых,
       он не даёт разорвать подпись посередине («Горячая» / «сделка» на двух
       строках читается как две разные ступени), во-вторых, это единственная
       точка, где слово можно спрятать, если колонка когда-нибудь снова
       ужмётся, — значок и процент при этом останутся на месте.
       Прятать его сейчас НЕ нужно: таблица сделок горизонтально прокручиваемая
       (.table-container, min-width у #tab-leads table), ширина колонки от
       ширины экрана не зависит, и на 480px слово влезает так же, как на
       десктопе. Скрытие по @media отняло бы различие «горячая против
       квалифицированной» ровно там, где менеджер работает с телефона. */
    .lead-score-badge .lead-score-stage { white-space: nowrap; }
    @media (max-width: 768px) {
      /* Полоса уступает место словам — так же, как в строке диалога
         (.dialog-score-row). Процент и стадия читаются всегда, шкала поверх
         них — украшение. */
      .lead-score-row .bant-bar-track { width: 48px; margin-right: 6px; }
      .lead-score-badge { font-size: 0.72rem; padding: 4px 7px; }
    }

    /* Фиксация ширины колонок CRM-таблицы: без скачков верстки при загрузке данных */
    /* 1119px — сумма min-width всех восьми колонок (140+145+110+210+160+96+100+158),
       а не округление. При меньшем значении колонки «Сессий» и «Создана» не могут
       отдать ширину соседям: их доля 8% опускается ниже собственного min-width, и
       недостающие пиксели молча уходят в горизонтальную прокрутку контейнера. */
    #tab-leads table { table-layout: fixed; min-width: 1119px; }
    /* Заголовки не переносятся: таблица лежит в .table-container с
       overflow-x: auto, поэтому длинная шапка уезжает в скролл, а не налезает
       на соседнюю колонку. */
    #tab-leads th { white-space: nowrap; }
    #tab-leads th:nth-child(1) { width: 13%; min-width: 140px; }  /* Клиент */
    #tab-leads th:nth-child(2) { width: 14%; min-width: 145px; }  /* Телефон */
    #tab-leads th:nth-child(3) { width: 10%; min-width: 110px; }  /* Канал */
    /* Колонка 4 расширена со 168px: бейдж прогрева печатает не только процент,
       но и слово стадии («❄️ Первичный контакт 12%» — самая длинная подпись,
       ~169px). При прежней ширине td (168 − 28px паддингов = 140px) слово
       срезалось бы `overflow: hidden` ниже по файлу. Ширину добрали у «Сессий»
       и «Создана» — там содержимое короткое и фиксированное. */
    #tab-leads th:nth-child(4) { width: 18%; min-width: 210px; }  /* Квалификация & Этап */
    #tab-leads th:nth-child(5) { width: 15%; min-width: 160px; }  /* Статус Сделки */
    #tab-leads th:nth-child(6) { width: 8%;  min-width: 96px;  }  /* Сессии */
    #tab-leads th:nth-child(7) { width: 8%;  min-width: 100px; }  /* Создана */
    #tab-leads th:nth-child(8) { width: 15%; min-width: 158px; }  /* Действия */
    #tab-leads td { overflow: hidden; text-overflow: ellipsis; }
    /* Однострочные ячейки не должны рваться пополам: имя, телефон, канал,
       число сессий и дата обрезаются многоточием, а не переносятся. */
    /* Колонка «Клиент» многострочная: имя остаётся однострочным (обрезается
       внутри .lead-name-text), а плашки «ДЕМО» и «нужен ответ» встают под ним.
       Раньше ячейка целиком была nowrap + ellipsis — всё после имени срезалось
       многоточием, и обе плашки не отображались вовсе. */
    #tab-leads td:nth-child(2),
    #tab-leads td:nth-child(3),
    #tab-leads td:nth-child(6),
    #tab-leads td:nth-child(7) { white-space: nowrap; }
    /* Кнопки действий в «Сделках» НЕ ЗАПРЕЩАЮТ перенос (жалоба владельца
       16.08.2026: «вёрстка в сделках у одного поля в таблице, другие нормально»).
       С `nowrap` подпись «Карточка Клиента» вместе со значком требует 164px, а
       ячейка — 160px, и кнопки вылезали за правый край таблицы на 31px. Колонка
       и раньше была впритык, добавленный значок её добил.
       Перенос по словам плюс `min-width: 0` — кнопка становится в две строки,
       но остаётся внутри своей колонки и читается целиком; обрезать подпись
       многоточием здесь нельзя, «Карточка Кли…» ничего не сообщает. */
    #tab-leads .table-actions-cell.stacked .btn {
      white-space: normal;
      min-width: 0;
      text-align: left;
      line-height: 1.25;
      /* Владелец (сессия 03.09.2026): «иконку прижать влево, текст так же от
         иконок, а не центровать» — раньше justify-content:center гонял
         значок+текст по горизонтали от кнопки к кнопке (у переносящихся на
         две строки подписей центровка ещё и съезжала построчно). Теперь
         содержимое всегда прижато к левому краю: .btn-icon имеет
         flex-shrink:0 и одинаковую ширину (16px svg) у всех пяти кнопок
         ячейки, поэтому подпись каждой кнопки стартует на одной вертикали. */
      justify-content: flex-start;
    }
    /* Имя клиента: одна строка с многоточием, полное значение — в title. */
    #tab-leads td:nth-child(1) .lead-name-text {
      display: block;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    /* Название канала («TELEGRAM», «WHATSAPP») — самое длинное значение узкой
       колонки: чуть мельче шрифт и уже поля, чтобы метка не обрезалась. */
    #tab-leads td:nth-child(3) .category-tag { font-size: 0.7rem; padding: 3px 8px; letter-spacing: 0.2px; }

    /* ── Таблица застройщиков ─────────────────────────────────────────────
       Ширины фиксируем так же, как в CRM-таблице: ячейки здесь многострочные
       (лимиты, квоты, состояние), и без table-layout: fixed колонки тянулись
       за самым длинным словом, а строка «Не работает» распирала таблицу. */
    /* Колонок пять: расход и квоты сведены в одну ячейку «Лимиты и расход»,
       и ширина ушла ей — в ней пять строк с числами, а не один бейдж.
       Сумма min-width (900px) выбрана по самому узкому рабочему экрану: на
       1280px под таблицу остаётся ≈966px (1280 − 56 полей контейнера − 236
       сайдбара − 20 зазора), на 1440px ≈1128px. Горизонтального скролла нет ни
       там, ни там; на планшете таблица по-прежнему листается внутри
       .table-container. */
    #tab-tenants table { table-layout: fixed; min-width: 900px; }
    #tab-tenants th { white-space: nowrap; }
    #tab-tenants th:nth-child(1) { width: 19%; min-width: 180px; }  /* Застройщик */
    #tab-tenants th:nth-child(2) { width: 16%; min-width: 150px; }  /* Адреса */
    #tab-tenants th:nth-child(3) { width: 33%; min-width: 290px; }  /* Лимиты и расход */
    #tab-tenants th:nth-child(4) { width: 14%; min-width: 130px; }  /* Состояние */
    #tab-tenants th:nth-child(5) { width: 18%; min-width: 150px; }  /* Действия */
    #tab-tenants td { vertical-align: top; }
    #tab-tenants .table-actions-cell.stacked .btn { white-space: nowrap; }
    /* Подписи строк здесь короткие («Сутки», «Месяц»), и широкая колонка
       подписи отнимала бы место у самого значения. */
    #tab-tenants .usage-label { min-width: 58px; }
    /* Текущий арендатор подсвечен строкой, а не инлайновым стилем в JS. */
    #tab-tenants tr.is-current-tenant td { background: rgba(139, 92, 246, 0.08); }

    /* Переключатель вида реестра: «Рабочие» / «Корзина». Сегментированная
       группа, а не две отдельные кнопки, — вид один, значений два. */
    .tenants-view-switch {
      display: inline-flex;
      align-items: stretch;
      gap: 2px;
      padding: 2px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--glass-bg);
    }
    .tenants-view-btn {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 6px 12px;
      border: 0;
      border-radius: calc(var(--radius-md) - 3px);
      background: transparent;
      color: var(--text-muted);
      font-family: var(--font-body);
      font-size: 0.82rem;
      font-weight: 600;
      cursor: pointer;
      white-space: nowrap;
      transition: all 0.2s ease;
    }
    .tenants-view-btn:hover { color: var(--text-main); background: var(--glass-hover); }
    .tenants-view-btn:active { transform: translateY(1px); }
    .tenants-view-btn.is-active {
      color: var(--text-main);
      background: rgba(139, 92, 246, 0.18);
      box-shadow: inset 0 0 0 1px rgba(139, 92, 246, 0.35);
    }
    .tenants-view-btn .head-icon { display: inline-flex; }
    .tenants-view-count {
      min-width: 20px;
      padding: 1px 6px;
      border-radius: 999px;
      background: var(--surface-tint-strong);
      font-size: 0.72rem;
      font-variant-numeric: tabular-nums;
      text-align: center;
    }
    .tenants-trash-note {
      margin: 0 0 14px;
      padding: 10px 14px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: var(--glass-bg);
      color: var(--text-body);
      font-size: 0.82rem;
      line-height: 1.5;
    }
    .tenants-trash-note strong { color: var(--text-main); }
    /* Дата отправки в корзину под именем: она задаёт отсчёт срока хранения. */
    .tenant-trash-meta {
      font-size: 0.72rem;
      line-height: 1.35;
      color: var(--text-muted);
    }
    @media (max-width: 768px) {
      .tenants-view-switch { width: 100%; }
      .tenants-view-btn { flex: 1 1 0; justify-content: center; }
    }
    @media (max-width: 480px) {
      .tenants-view-btn { padding: 8px 6px; font-size: 0.76rem; }
      .tenants-view-btn .head-icon { display: none; }
      .tenants-trash-note { font-size: 0.78rem; }
    }

    .tenant-name-cell {
      display: flex;
      flex-direction: column;
      gap: 4px;
      min-width: 0;
    }
    .tenant-name-cell strong {
      color: var(--text-main);
      overflow-wrap: anywhere;
    }
    .tenant-name-badges {
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
    }
    .tenant-address {
      display: flex;
      flex-direction: column;
      gap: 3px;
      min-width: 0;
    }
    .tenant-address code {
      font-size: 0.78rem;
      overflow-wrap: anywhere;
    }
    .tenant-address span {
      font-size: 0.72rem;
      line-height: 1.35;
      color: var(--text-muted);
      overflow-wrap: anywhere;
    }

    /* Карточка клиента: просторное окно вместо тесного модального */
    .modal-card.client-card-modal {
      max-width: 1080px;
      width: 92vw;
      max-height: 92vh;
      display: flex;
      flex-direction: column;
    }
    .client-facts-box {
      padding: 14px 16px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: rgba(15, 23, 42, 0.35);
      min-height: 200px;
      max-height: 52vh;
      overflow-y: auto;
    }
    [data-theme="light"] .client-facts-box { background: rgba(241, 245, 249, 0.6); }

    /* Сводка по покупателю над редактируемым списком фактов. Приглушена
       намеренно: главное в блоке памяти — сами факты, которые правят, а сводка
       читается боковым зрением. pre-line — текст приходит с сервера строками
       («• [Категория]: факт»), и переносы в нём осмысленные. */
    .client-summary-row { margin-bottom: 10px; }
    .client-summary-title {
      font-family: var(--font-display);
      font-size: 0.72rem;
      font-weight: 700;
      letter-spacing: 0.03em;
      text-transform: uppercase;
      color: var(--text-muted);
      margin-bottom: 6px;
    }
    .client-summary-box {
      padding: 10px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--surface-tint);
      font-size: 0.84rem;
      line-height: 1.55;
      color: var(--text-body);
      white-space: pre-line;
      overflow-wrap: anywhere;
      max-height: 22vh;
      overflow-y: auto;
    }

    /* Разбивка BANT: итоговый процент той же полосой, что в таблице сделок, и
       оси словами под ним. Список пар «подпись → значение» переиспользует
       .lead-crm-route-details — тот же компонент, что у маршрута заявки. */
    .client-bant-box {
      padding: 12px 14px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--surface-tint);
    }
    .client-bant-score {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 10px;
    }
    .client-bant-score .bant-bar-track { width: 120px; margin-right: 0; }
    /* Подпись рядом с процентом — это НАЗВАНИЕ СТАДИИ с сервера («🎯
       Квалифицирован»), а не пояснение к цифре: читается наравне с числом,
       поэтому не приглушена. */
    .client-bant-score-caption {
      font-size: 0.8rem;
      font-weight: 600;
      color: var(--text-body);
    }
    .client-bant-details { margin-top: 0; }
    .client-bant-details .lead-crm-route-item dt { min-width: 132px; }
    .client-bant-empty { color: var(--text-muted); font-size: 0.82rem; }
    /* Оговорка под списком: чем экранное значение отличается от колонки в
       XLSX. Мельче строк списка — это сноска, а не ещё одна ось. */
    .client-bant-note {
      margin-top: 8px;
      padding-top: 8px;
      border-top: 1px solid var(--glass-border);
      font-size: 0.74rem;
      line-height: 1.45;
      color: var(--text-muted);
    }
    @media (max-width: 480px) {
      .client-bant-details .lead-crm-route-item dt { min-width: 0; }
      .client-bant-score .bant-bar-track { width: 88px; }
    }

    /* Группа фактов одной категории */
    .fact-group { margin-bottom: 16px; }
    .fact-group:last-child { margin-bottom: 0; }
    .fact-group-title {
      font-family: var(--font-display);
      font-size: 0.82rem;
      font-weight: 700;
      letter-spacing: 0.03em;
      text-transform: uppercase;
      color: var(--accent-purple);
      padding-bottom: 6px;
      margin-bottom: 8px;
      border-bottom: 1px solid var(--glass-border);
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .fact-group-count {
      font-size: 0.7rem;
      color: var(--text-muted);
      font-weight: 500;
      text-transform: none;
      letter-spacing: 0;
    }

    /* Редактируемая память клиента — просторные строки */
    .fact-row {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      padding: 8px 0;
    }
    .fact-row + .fact-row { border-top: 1px dashed var(--glass-border); }
    .fact-row .fact-edit-input {
      flex: 1;
      min-width: 0;
      padding: 9px 12px;
      font-size: 0.92rem;
      line-height: 1.5;
      min-height: 42px;
      resize: vertical;
      font-family: var(--font-body);
    }
    /* Дата факта и ссылка на диалог-источник — третьей строкой ячейки, во всю
       ширину под текстом (flex-basis: 100% в обёртке с переносом). Отдельная
       колонка здесь не нужна: это служебная приписка, а не второе содержание. */
    .fact-row { flex-wrap: wrap; }
    .fact-row-meta {
      flex: 1 0 100%;
      /* Ровно под текстом факта: 22px метки источника + её gap. */
      padding-left: 32px;
      /* Отступ сверху даёт row-gap самой .fact-row (10px) — свой не нужен. */
      margin-top: -2px;
      font-size: 0.72rem;
      color: var(--text-muted);
      display: flex;
      align-items: baseline;
      gap: 4px;
      flex-wrap: wrap;
    }
    .fact-row-meta .csat-review-session-link { font-size: 0.72rem; }
    @media (max-width: 480px) {
      .fact-row-meta { padding-left: 0; }
    }
    .fact-row-actions { display: flex; gap: 6px; padding-top: 3px; }
    .fact-row .icon-btn { flex: 0 0 auto; padding: 7px 10px; font-size: 0.9rem; }

    /* ── Происхождение факта ────────────────────────────────────────────────
       Метка «откуда это известно» стоит в узком поле слева от текста: главное
       в строке — сам факт, происхождение читается боковым зрением и не спорит
       с ним за внимание. Смысл несёт ФОРМА значка и тип рамки, цвет только
       дублирует — иначе метка пропадала бы при дальтонизме и в ч/б печати. */
    .fact-source {
      flex: 0 0 auto;
      width: 22px;
      height: 22px;
      margin-top: 10px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border: 1px solid currentColor;
      border-radius: var(--radius-pill);
      color: var(--text-muted);
      opacity: 0.72;
      transition: opacity 0.2s ease, transform 0.2s ease;
      cursor: help;
    }
    .fact-source svg { width: 12px; height: 12px; }
    .fact-row:hover .fact-source,
    .fact-source:hover { opacity: 1; transform: scale(1.06); }

    .fact-source-bot { color: var(--accent-indigo-soft); }
    /* Оба менеджерских источника — один цвет: оттенок говорит «это про
       менеджера», а различает их форма значка (карандаш — вписал руками,
       микрофон — ИИ разобрал живую речь). Разводить их ещё и цветом значило бы
       спорить с этой логикой и опираться на оттенок там, где он не читается. */
    .fact-source-manager,
    .fact-source-manager-speech { color: var(--accent-gold-soft); }
    .fact-source-crm { color: var(--accent-emerald-soft); }
    /* Происхождение неизвестно — это законное состояние старых фактов, а не
       ошибка: приглушённый серый и пунктирная рамка (второй, нецветовой
       признак «сведений нет»), никаких предупреждающих оттенков. */
    .fact-source-unknown {
      color: var(--text-muted);
      border-style: dashed;
    }

    /* Расшифровка значков: подсказка по наведению недоступна на тач-экранах
       и не печатается, поэтому источники подписаны словами один раз сверху. */
    .fact-source-legend {
      display: flex;
      flex-wrap: wrap;
      gap: 6px 14px;
      align-items: center;
    }
    .fact-source-legend:not(:empty) {
      margin-bottom: 12px;
      padding-bottom: 10px;
      border-bottom: 1px dashed var(--glass-border);
    }
    .fact-source-legend-item {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 0.72rem;
      color: var(--text-muted);
    }
    .fact-source-legend-item .fact-source {
      margin-top: 0;
      width: 18px;
      height: 18px;
      opacity: 1;
    }
    .fact-source-legend-item .fact-source svg { width: 10px; height: 10px; }

    /* На телефоне узкое поле метки сжимается, но не исчезает: без него строки
       снова становятся неразличимыми по происхождению. */
    @media (max-width: 480px) {
      .fact-row { gap: 8px; }
      .fact-source { width: 20px; height: 20px; margin-top: 9px; }
      .fact-source svg { width: 11px; height: 11px; }
      .fact-source-legend { gap: 4px 10px; }
      .fact-source-legend-item { font-size: 0.68rem; }
    }

    /* Форма добавления факта */
    .add-fact-row {
      display: flex;
      gap: 10px;
      align-items: flex-start;
      flex-wrap: wrap;
    }
    .add-fact-row .form-select { flex: 0 0 210px; }
    .add-fact-row .form-textarea {
      flex: 1;
      min-width: 260px;
      min-height: 46px;
      font-size: 0.92rem;
      resize: vertical;
    }

    /* Развернутый режим модального окна (диалог менеджера на весь экран) */
    .modal-card.modal-card-wide {
      max-width: 96vw !important;
      width: 96vw;
      height: 92vh;
      display: flex;
      flex-direction: column;
    }
    .modal-card.modal-card-wide .human-chat-feed {
      max-height: none;
      flex: 1;
    }
    .modal-card.modal-card-wide .human-chat-msg { max-width: 70%; }

    /* Реестр подсистем интеллекта бота (вкладка 8) */
    .brain-modules-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
      gap: 12px;
      margin-top: 16px;
    }

    .brain-module-card {
      background: var(--card-bg);
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      padding: 14px 16px;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .brain-module-card.brain-module-off { opacity: 0.6; }

    .brain-module-head {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 8px;
      font-size: 0.92rem;
    }

    .brain-module-desc {
      font-size: 0.82rem;
      color: var(--text-muted);
      line-height: 1.45;
    }

    .brain-metrics {
      display: flex;
      flex-direction: column;
      gap: 3px;
      font-size: 0.78rem;
    }
    .brain-metric {
      display: flex;
      justify-content: space-between;
      gap: 10px;
    }
    .brain-metric span { color: var(--text-muted); }
    .brain-metric strong { overflow-wrap: anywhere; text-align: right; }

    .brain-module-foot {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 6px;
      margin-top: auto;
    }
    .brain-source {
      font-size: 0.74rem;
      color: var(--text-muted);
    }

    /* Выбор сделки для КП по объекту каталога (#offer-deal-modal) */
    .offer-unit-banner {
      padding: 10px 12px;
      border-radius: 10px;
      font-size: 0.88rem;
      background: rgba(99, 102, 241, 0.12);
      border: 1px solid rgba(99, 102, 241, 0.3);
    }

    .offer-deal-list {
      max-height: 320px;
      min-height: 120px;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 6px;
      margin-top: 10px;
    }

    .offer-deal-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 10px;
      padding: 9px 12px;
      border-radius: 10px;
      cursor: pointer;
      background: var(--surface-tint-mid);
      border: 1px solid transparent;
      transition: background 0.15s ease, border-color 0.15s ease;
    }

    .offer-deal-row:hover { background: var(--surface-tint-strong); }

    .offer-deal-row-selected {
      background: rgba(99, 102, 241, 0.16);
      border-color: rgba(99, 102, 241, 0.55);
    }

    /* Предупреждение о пустом КП (#offer-unit-modal): жёлтое, потому что это не
       отказ, а цена решения — менеджер может продолжить, зная результат. */
    .offer-empty-warning {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      margin-top: 10px;
      padding: 10px 12px;
      border-radius: 10px;
      font-size: 0.82rem;
      line-height: 1.45;
      color: var(--text-body);
      background: rgba(245, 158, 11, 0.12);
      border: 1px solid rgba(245, 158, 11, 0.35);
    }

    .offer-unit-actions {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }

    /* Телефон: кнопки в подвале модалки не должны выдавливать друг друга
       в одну нечитаемую строку. */
    @media (max-width: 480px) {
      .offer-unit-actions {
        width: 100%;
        flex-direction: column-reverse;
      }
      .offer-unit-actions .btn { width: 100%; justify-content: center; }
      #offer-unit-modal .modal-actions { flex-direction: column; align-items: stretch; gap: 10px; }
    }

    /* Ссылка на КП для клиента (#offer-link-modal) ------------------------- */
    .offer-link-note {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      margin-top: 10px;
      padding: 10px 12px;
      border-radius: 10px;
      font-size: 0.82rem;
      line-height: 1.45;
      color: var(--text-body);
      background: rgba(99, 102, 241, 0.1);
      border: 1px solid rgba(99, 102, 241, 0.28);
    }

    .offer-link-list {
      display: flex;
      flex-direction: column;
      gap: 8px;
      margin-top: 12px;
      max-height: 340px;
      overflow-y: auto;
    }

    .offer-link-row {
      padding: 10px 12px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm, 10px);
      background: var(--surface-tint);
      transition: all 0.2s ease;
    }
    .offer-link-row:hover { border-color: rgba(99, 102, 241, 0.45); }

    .offer-link-row-title {
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text-body);
      margin-bottom: 6px;
    }
    .offer-link-row-title small {
      display: block;
      font-weight: 400;
      font-size: 0.74rem;
      color: var(--text-muted);
      margin-top: 2px;
    }

    .offer-link-row-controls {
      display: flex;
      gap: 6px;
      align-items: center;
    }
    .offer-link-row-controls .form-input {
      flex: 1;
      min-width: 0;
      font-family: 'JetBrains Mono', ui-monospace, monospace;
      font-size: 0.75rem;
      padding: 6px 8px;
    }
    .offer-link-row-controls .btn { padding: 6px 10px; font-size: 0.78rem; white-space: nowrap; }

    @media (max-width: 480px) {
      .offer-link-row-controls { flex-wrap: wrap; }
      .offer-link-row-controls .form-input { flex-basis: 100%; }
      .offer-link-row-controls .btn { flex: 1; justify-content: center; }
      #offer-link-modal .modal-actions { flex-direction: column; align-items: stretch; gap: 10px; }
    }

    /* Human Manager Direct Chat Modal */
    .human-chat-feed {
      max-height: 320px;
      min-height: 160px;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 6px;
      padding: 10px 12px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm, 10px);
      background: rgba(15, 23, 42, 0.35);
      /* Вертикальные отступы модалки задаёт gap у .modal-card — свой margin
         здесь только удваивал бы расстояние до шапки и до строки ввода. */
      margin: 0;
    }
    [data-theme="light"] .human-chat-feed { background: rgba(241, 245, 249, 0.6); }
    .human-chat-msg {
      margin: 0;
      max-width: 82%;
      padding: 8px 12px;
      border-radius: 12px;
      font-size: 0.86rem;
      line-height: 1.45;
      /* Пузырь НЕ pre-wrap: иначе перенос строки в HTML-шаблоне рендера
         превращался в пустую строку сверху сообщения. Переносы сохраняет
         только .msg-text, куда попадает исключительно текст клиента. */
      white-space: normal;
      word-break: break-word;
    }
    .human-chat-msg .msg-text {
      white-space: pre-wrap;
      word-break: break-word;
    }
    /* Подпись сессии в шапке: человекопонятная, технический ID — по клику */
    .human-chat-session-label {
      font-size: 0.78rem;
      font-weight: 500;
      color: var(--text-muted);
      cursor: pointer;
      transition: color 0.2s ease;
    }
    .human-chat-session-label:hover { color: var(--accent-purple); }
    .human-chat-msg.from-client {
      align-self: flex-start;
      background: rgba(99, 102, 241, 0.14);
      border: 1px solid rgba(99, 102, 241, 0.25);
    }
    .human-chat-msg.from-agent {
      align-self: flex-end;
      background: rgba(34, 197, 94, 0.12);
      border: 1px solid rgba(34, 197, 94, 0.28);
    }
    .human-chat-msg .msg-author {
      font-size: 0.7rem;
      color: var(--text-muted);
      margin-bottom: 2px;
    }
    /* Пометка авто-напоминания бота — НЕ реплика: центрированная полоса во всю
       ширину ленты, а не пузырь слева/справа, иначе выглядит как ответ на
       слова покупателя, которых не было (решение владельца 29.08.2026). */
    .reengagement-touch-row {
      align-self: center;
      width: 100%;
      max-width: 100%;
      margin: 2px 0;
      padding: 8px 12px;
      border-radius: var(--radius-sm, 10px);
      border: 1px dashed var(--glass-border);
      background: rgba(148, 163, 184, 0.08);
      font-size: 0.82rem;
      line-height: 1.4;
    }
    .reengagement-touch-row.is-sent {
      border-color: rgba(16, 185, 129, 0.35);
      background: rgba(16, 185, 129, 0.08);
    }
    .reengagement-touch-row.is-failed {
      border-color: rgba(244, 63, 94, 0.4);
      background: rgba(244, 63, 94, 0.08);
    }
    .reengagement-touch-head {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 6px 8px;
      font-size: 0.72rem;
      font-weight: 600;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.02em;
    }
    .reengagement-touch-row.is-sent .reengagement-touch-label { color: var(--accent-emerald); }
    .reengagement-touch-row.is-failed .reengagement-touch-label { color: var(--accent-rose); }
    .reengagement-touch-meta {
      margin-left: auto;
      font-weight: 500;
      text-transform: none;
      color: var(--text-muted);
    }
    .reengagement-touch-text {
      margin-top: 4px;
      color: var(--text-body, var(--text-main));
      white-space: pre-wrap;
      word-break: break-word;
      font-style: italic;
    }
    .reengagement-touch-reason {
      margin-top: 4px;
      color: var(--accent-rose);
      font-size: 0.78rem;
    }
    @media (max-width: 600px) {
      .reengagement-touch-meta { margin-left: 0; width: 100%; }
    }
    .human-chat-input-row {
      display: flex;
      gap: 10px;
      align-items: stretch;
      margin: 0;
    }
    /* Раскладку ряда держит CSS, а не инлайн-стили на кнопках: с
       `align-self: flex-end` в разметке медиазапрос ниже был бессилен —
       инлайн сильнее. */
    .human-chat-input-row .form-textarea { flex: 1 1 auto; min-width: 0; }
    .human-chat-input-row .btn { align-self: flex-end; }
    /* Поле ответа плюс «Подсказка ИИ» плюс «Отправить клиенту» в одну строку
       на телефоне не помещаются: ряд распирало до 411px в окне 380px, и
       «Отправить клиенту» уезжала за правый край окна диалога. Переносим
       кнопки под поле и растягиваем на всю ширину — обе остаются нажимаемыми. */
    @media (max-width: 600px) {
      .human-chat-input-row { flex-wrap: wrap; }
      .human-chat-input-row .form-textarea { flex: 1 1 100%; }
      .human-chat-input-row .btn { flex: 1 1 140px; justify-content: center; align-self: auto; }
    }

    /* Инлайн AI Co-Pilot внутри окна диалога менеджера */
    .copilot-box {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 12px;
      margin: 10px 0;
      border: 1px solid rgba(192, 132, 252, 0.35);
      border-radius: 12px;
      background: rgba(192, 132, 252, 0.07);
    }
    [data-theme="light"] .copilot-box { background: rgba(192, 132, 252, 0.1); }
    .copilot-box-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      font-size: 0.85rem;
      font-weight: 700;
      color: var(--accent-purple-soft);
    }
    .copilot-coverage {
      font-size: 0.76rem;
      font-weight: 600;
      color: var(--text-muted);
      text-align: right;
    }
    .copilot-box .chips-row { margin-bottom: 0; }
    .copilot-box-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      justify-content: flex-end;
    }

    /* Category Filter Chips */
    .chips-row {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 16px;
    }
    .chip {
      background: var(--surface-tint-chip);
      border: 1px solid var(--glass-border);
      color: var(--text-muted);
      padding: 7px 14px;
      border-radius: var(--radius-pill);
      font-size: 12px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
      white-space: nowrap;
    }
    .chip:hover { color: var(--text-main); background: var(--surface-tint-stronger); }
    .chip.active {
      background: var(--gradient-indigo-cyan);
      border-color: transparent;
      color: white;
    }

    /* Skeleton loading */
    .skeleton {
      background: linear-gradient(90deg, var(--surface-tint) 25%, var(--surface-tint-strong) 37%, var(--surface-tint) 63%);
      background-size: 400% 100%;
      animation: shimmer 1.4s ease infinite;
      border-radius: var(--radius-sm);
    }
    @keyframes shimmer {
      0% { background-position: 100% 50%; }
      100% { background-position: 0 50%; }
    }

    /* Toast Notifications */
    .toast-stack {
      position: fixed;
      bottom: 20px;
      right: 20px;
      display: flex;
      flex-direction: column;
      gap: 10px;
      z-index: 5000;
    }
    .toast {
      background: rgba(15, 23, 42, 0.95);
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-indigo);
      color: var(--text-main);
      padding: 12px 16px;
      border-radius: var(--radius-sm);
      font-size: 13px;
      box-shadow: var(--shadow-card);
      min-width: 240px;
      /* Длинные сообщения (например, причина отказа в удалении объекта) раньше
         растягивали тост в одну строку через весь экран — теперь переносятся. */
      max-width: min(420px, calc(100vw - 40px));
      line-height: 1.45;
      animation: toastIn 0.25s ease;
      backdrop-filter: var(--blur-glass);
    }
    .toast.success { border-left-color: var(--accent-green); }
    .toast.error { border-left-color: var(--accent-rose); }
    .toast.info { border-left-color: var(--accent-cyan); }
    /* Предупреждение — не ошибка: действие выполнено частично (например, токен
       стёрт локально, но сервер не ответил), и красный здесь читался бы как отказ. */
    .toast.warning { border-left-color: var(--accent-amber); }
    @keyframes toastIn {
      from { opacity: 0; transform: translateX(20px); }
      to { opacity: 1; transform: translateX(0); }
    }

    /* Responsive */
    /* Планшет — сетка 2х2, смартфон — одна колонка (брейкпоинты из плана) */
    @media (max-width: 1200px) {
      .app-container { grid-template-columns: 224px minmax(0, 1fr); gap: 18px; }
      .tab-btn { font-size: 0.82rem; padding: 9px 10px; }
    }

    @media (max-width: 1024px) {
      .app-container {
        padding: 20px;
        grid-template-columns: minmax(0, 1fr);
        /* Одна колонка: фиксированные ряды десктопа здесь наложили бы
           кнопку «Разделы» на KPI-карточки. */
        grid-template-rows: none;
      }
      .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .app-container > .app-topstack,
      .app-container > .dashboard-board,
      .app-container > .sidebar-toggle,
      .app-container > .sidebar-scrim { grid-row: auto; grid-column: 1 / -1; }
      .app-container > .tab-content { grid-column: 1; grid-row: auto; }

      /* Сайдбар превращается в выдвижную панель: на планшете и телефоне
         260px слева съедали бы половину таблицы сделок. */
      .sidebar-toggle { display: inline-flex; justify-self: start; }
      .sidebar-nav {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        width: min(280px, 86vw);
        z-index: 1200;
        padding: 16px;
        transform: translateX(-102%);
        /* visibility, а не только сдвиг: уехавшая за край панель без него
           остаётся в порядке табуляции, и Tab уводит фокус на невидимые
           кнопки разделов. */
        visibility: hidden;
        transition: transform 0.25s ease, visibility 0.25s ease;
        overflow-y: auto;
      }
      .sidebar-nav.open { transform: translateX(0); visibility: visible; }
      .tabs-nav { max-height: none; border-radius: var(--radius-md); }
      .sidebar-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 1100;
        background: rgba(2, 6, 23, 0.6);
        backdrop-filter: var(--blur-modal-deep);
        -webkit-backdrop-filter: var(--blur-modal-deep);
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.25s ease, visibility 0.25s ease;
      }
      .sidebar-scrim.open { opacity: 1; visibility: visible; }
    }

    @media (max-width: 550px) {
      .stats-grid { grid-template-columns: minmax(0, 1fr); }
      .stat-value { font-size: 1.45rem; }
    }

    @media (max-width: 767px) {
      header { padding: 12px 16px; flex-wrap: wrap; gap: 10px; }
      /* Телефон: шапка честно двухэтажная. Первый этаж — бренд, версия и
         состояние; второй — целиком блок действий, прижатый вправо. Это не
         «сломалось», а единственный перенос, который здесь и задуман: раньше
         дети переносились поодиночке и шапка вырастала до четырёх рядов. */
      /* Только здесь блоку действий разрешено переносить свои группы: у
         платформенного аккаунта на 380px «состояние» и «инструменты» в одну
         строку не влезают. Переносятся они целыми группами, поэтому этажей
         максимум два и оба выглядят намеренно. */
      .header-actions { flex: 1 1 100%; justify-content: flex-end; flex-wrap: wrap; }
      /* Обе группы влезают в строку даже на 380px (≈330px), но если подпись
         пользователя окажется длиннее — сжимается она, а не разрывается ряд. */
      /* Группы не сжимаются, а переносятся: сжатие обрезало логин до одной
         буквы там, где свободного места в строке было ещё больше сотни
         пикселей. Перенос честнее — пользователь остаётся читаемым. */
      .header-status-group, .header-tools { flex-shrink: 0; }
      /* Прячем ТЕКСТ подзаголовка, а не всю строку: строка держит подпись
         версии и её зарезервированную высоту. Скрыв строку целиком, мы бы
         вернули прыжок шапки при приходе ответа /health. */
      .brand-subtitle-text { display: none; }
      .app-container { padding: 14px; gap: 16px; }
      .faq-cards-grid { grid-template-columns: 1fr; }
      .faq-toolbar { flex-direction: column; align-items: stretch; }
      /* Кнопки импорта/экспорта на телефоне переносятся, рычаг замены встаёт
         отдельной строкой во всю ширину — иначе он ужимается до нечитаемого. */
      .faq-toolbar > div { flex-wrap: wrap; }
      .faq-import-mode { flex: 1 0 100%; justify-content: flex-start; border-radius: 12px; }
      .modal-card { padding: 18px; }
      /* Бейдж остаётся и на телефоне: он теперь единственный индикатор обоих
         контуров и единственный вход в диагностику. Прячем только подпись —
         цветная точка сообщает главное и занимает минимум места. */
      .health-badge { padding: 6px 9px; }
      .health-badge #healthBadgeText { display: none; }
      /* Подпись версии на телефоне не режем: ради неё сюда и заходят после
         выкладки. Она живёт в строке подзаголовка и на узком экране остаётся
         в ней одна — переносить .brand больше не нужно, именно этот перенос и
         заставлял шапку «плясать». */
      .build-stamp { padding: 2px 6px; font-size: 0.65rem; }
      /* Счётчик коммитов на телефоне ужимаем до минимума: строка тут и без него
         тесная, а само число читается и вплотную. */
      .build-stamp-ahead { margin-left: 3px; padding: 0 3px; }
      /* Подпись здесь мельче, поэтому и пол ниже — иначе он зря съедал бы
         ширину у бейджа состояния. */
      .brand-title:has(.build-stamp:not([hidden])) { min-width: 170px; }
      .dropzone { flex-direction: column; text-align: center; padding: 18px; }
    }

    @media (max-width: 600px) {
      .toast-stack {
        left: 16px;
        right: 16px;
        bottom: 16px;
        width: auto;
      }
      .toast { min-width: 0; width: 100%; }
    }

    .client-facts-box {
      background: rgba(15, 23, 42, 0.8);
      color: var(--text-body);
    }

    /* ============================================================
       Light Theme — Premium Pearl ([data-theme="light"])
       ============================================================ */
    [data-theme="light"] {
      /* Возврат к светлой схеме браузера: календарь, выпадающие списки и
         полосы прокрутки обязаны идти за темой кабинета, иначе на жемчужном
         фоне открывается чёрная панель выбора даты. */
      color-scheme: light;
      --bg-base: #f8fafc;
      --bg-mid: #f1f5f9;
      --bg-deep: #e2e8f0;
      --card-bg: rgba(255, 255, 255, 0.88);
      --card-border: rgba(0, 0, 0, 0.08);
      --glass-bg: rgba(255, 255, 255, 0.85);
      --glass-border: rgba(0, 0, 0, 0.08);
      --glass-hover: rgba(241, 245, 249, 0.95);
      --text-main: #0f172a;
      --text-body: #334155;
      --text-muted: #64748b;
      --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.06);
      /* Светлый индиго заголовков не читается на жемчужном фоне — на свету
         берём насыщенный оттенок той же ветки палитры. */
      --accent-indigo-soft: #4f46e5;
      /* ── Акценты на свету ────────────────────────────────────────────────
         Обсидиановая ветка палитры (#10b981, #f59e0b, #06b6d4, #f43f5e,
         #8b5cf6) на жемчужном фоне даёт 1.9–2.5:1 — цена объекта, телефон
         лида, подпись «Ждёт реакции» и условие акции читались как водяной
         знак. Замер по всем вкладкам нашёл больше двадцати таких мест, и
         точечные переопределения их не покрывали: тот же токен используют
         десятки правил. Поэтому насыщаем сами токены, а не правила —
         одна правка закрывает весь кабинет разом.
         Фоновые и рамочные применения этих же токенов (точки статуса,
         заливки шкал, левые полосы карточек) от затемнения только выигрывают:
         на белом они становятся заметнее, а белый текст поверх них — читаемее.
         Тёмная тема не затронута: переопределение живёт только здесь. */
      --accent-green: #047857;
      --accent-amber: #b45309;
      --accent-cyan: #0e7490;
      --accent-rose: #be123c;
      --accent-purple: #7c3aed;
      /* Осветлённая ветка (-soft) задумана как ТЕКСТ на обсидиане: на свету
         #fbbf24 даёт 1.6:1, #fda4af — 1.9:1. Зеркалим её в затемнённую. */
      --accent-purple-soft: #6d28d9;
      --accent-emerald-soft: #047857;
      --accent-gold-soft: #92400e;
      --accent-rose-soft: #be123c;
      --surface-tint-weak: rgba(15, 23, 42, 0.02);
      --surface-tint: rgba(15, 23, 42, 0.03);
      --surface-tint-mid: rgba(15, 23, 42, 0.04);
      --surface-tint-chip: rgba(15, 23, 42, 0.05);
      --surface-tint-strong: rgba(15, 23, 42, 0.07);
      --surface-tint-stronger: rgba(15, 23, 42, 0.09);
      --row-border: rgba(15, 23, 42, 0.08);
    }

    html[data-theme="light"],
    [data-theme="light"] body {
      scrollbar-color: rgba(15, 23, 42, 0.18) transparent;
    }
    [data-theme="light"] ::-webkit-scrollbar-thumb {
      background: rgba(15, 23, 42, 0.18);
    }
    [data-theme="light"] ::-webkit-scrollbar-thumb:hover {
      background: rgba(15, 23, 42, 0.32);
    }

    [data-theme="light"] header,
    [data-theme="light"] .tabs-nav,
    [data-theme="light"] th {
      background: rgba(255, 255, 255, 0.85);
    }
    [data-theme="light"] .btn-secondary {
      background: rgba(226, 232, 240, 0.7);
      color: var(--text-main);
    }
    /* Пастельный rose (--accent-rose-soft) задуман для обсидиана, на жемчужном
       фоне подпись опасного действия почти пропадала — на свету берём насыщенный
       оттенок той же ветки палитры, как и с индиго заголовков. */
    [data-theme="light"] .btn-danger {
      background: rgba(244, 63, 94, 0.12);
      color: var(--accent-rose);
      border-color: rgba(244, 63, 94, 0.35);
    }
    [data-theme="light"] .btn-danger:not(.is-disabled):hover {
      background: rgba(244, 63, 94, 0.22);
    }
    [data-theme="light"] .dropzone {
      background: rgba(241, 245, 249, 0.6);
    }
    [data-theme="light"] .keyword-chip {
      background: rgba(226, 232, 240, 0.7);
      color: var(--text-body);
    }
    [data-theme="light"] .faq-response-preview {
      background: rgba(241, 245, 249, 0.7);
      color: var(--text-main);
    }
    [data-theme="light"] .form-input,
    [data-theme="light"] .form-textarea,
    [data-theme="light"] .form-select {
      /* background-color, а не шорткат: шорткат сбрасывал background-image и
         вместе с ним собственную стрелку выпадающего списка (см. блок правил
         для элементов ввода в конце файла). */
      background-color: #ffffff;
    }
    [data-theme="light"] .toast {
      background: rgba(255, 255, 255, 0.97);
      color: var(--text-main);
    }
    [data-theme="light"] td {
      color: var(--text-body);
    }
    [data-theme="light"] tr:hover td {
      background: rgba(99, 102, 241, 0.05);
    }
    [data-theme="light"] .comparison-matrix-table {
      background: rgba(241, 245, 249, 0.7);
    }
    [data-theme="light"] .comparison-matrix-table td {
      color: var(--text-body);
    }

    /* High-contrast fixes (WCAG AAA pass) */
    [data-theme="light"] .brand-title h1 {
      background: linear-gradient(to right, #0f172a, #4f46e5);
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    [data-theme="light"] .badge-purple,
    [data-theme="light"] .stat-badge.badge-purple,
    [data-theme="light"] .category-tag {
      background: rgba(147, 51, 234, 0.12);
      color: #6b21a8;
      border-color: rgba(147, 51, 234, 0.25);
    }
    [data-theme="light"] .badge-green,
    [data-theme="light"] .stat-badge.badge-green {
      background: rgba(5, 150, 105, 0.12);
      color: #047857;
      border-color: rgba(5, 150, 105, 0.25);
    }
    [data-theme="light"] .badge-amber,
    [data-theme="light"] .stat-badge.badge-amber {
      background: rgba(217, 119, 6, 0.12);
      color: #b45309;
      border-color: rgba(217, 119, 6, 0.25);
    }
    [data-theme="light"] .badge-rose {
      background: rgba(225, 29, 72, 0.12);
      color: #be123c;
      border-color: rgba(225, 29, 72, 0.25);
    }
    [data-theme="light"] .badge-gray {
      background: rgba(100, 116, 139, 0.12);
      color: #475569;
    }
    [data-theme="light"] .stat-sub {
      color: #475569;
    }
    [data-theme="light"] .form-group label {
      color: #0f172a;
    }
    [data-theme="light"] .pill-indicator {
      background: linear-gradient(135deg, #4f46e5, #7c3aed);
      border-color: transparent;
      box-shadow: 0 4px 14px rgba(79, 70, 229, 0.3);
    }
    [data-theme="light"] .tab-btn.active,
    [data-theme="light"] .tab-btn.active .tab-icon {
      color: #ffffff;
    }
    [data-theme="light"] .tabs-nav,
    [data-theme="light"] .sidebar-toggle {
      background: rgba(255, 255, 255, 0.9);
    }
    [data-theme="light"] .tab-btn:hover {
      background: rgba(226, 232, 240, 0.75);
    }
    [data-theme="light"] .sidebar-scrim {
      background: rgba(15, 23, 42, 0.35);
    }
    [data-theme="light"] th.sortable:hover {
      background: rgba(226, 232, 240, 0.8);
    }
    [data-theme="light"] .preset-btn:hover,
    [data-theme="light"] .btn-secondary:hover {
      border-color: rgba(99, 102, 241, 0.4);
    }
    [data-theme="light"] .btn-secondary:hover {
      background: rgba(226, 232, 240, 0.9);
    }
    [data-theme="light"] .client-facts-box {
      background: rgba(241, 245, 249, 0.8);
      color: var(--text-main);
    }
    /* На жемчужном фоне осветлённые оттенки значков происхождения теряются —
       берём насыщенные базовые акценты. Смысл всё равно несёт форма значка. */
    [data-theme="light"] .fact-source { opacity: 0.9; }
    [data-theme="light"] .fact-source-bot { color: var(--accent-indigo); }
    [data-theme="light"] .fact-source-manager,
    [data-theme="light"] .fact-source-manager-speech { color: var(--accent-amber); }
    [data-theme="light"] .fact-source-crm { color: var(--accent-green); }

    /* ── Экран входа: переключатель способа авторизации ──────────────────────
       Ссылка, а не чекбокс: способов входа два, и они взаимоисключающие —
       чекбокс «войти по логину» рядом с полем токена читался как опция к нему. */
    .lockscreen-switch-wrap {
      text-align: center;
      margin-top: 2px;
      margin-bottom: 16px;
    }
    .lockscreen-switch-link {
      color: var(--accent-indigo, #818cf8);
      font-size: 0.85rem;
      text-decoration: none;
      transition: opacity 0.2s ease;
    }
    .lockscreen-switch-link:hover {
      text-decoration: underline;
      opacity: 0.9;
    }

    /* ── Группа кнопок в ячейке «Действия» ───────────────────────────────────
       Кнопки шли подряд без контейнера и слипались. Флекс с gap разводит их, а
       flex-wrap переносит на вторую строку вместо разрыва вёрстки таблицы. */
    .action-btn-group {
      display: flex;
      align-items: center;
      gap: 6px;
      flex-wrap: wrap;
    }
    .action-btn-group .btn {
      padding: 4px 8px;
      font-size: 0.8rem;
      white-space: nowrap;
    }

    /* ── База знаний RAG: компактные панели и фиксированная таблица ───────────
       Зона загрузки и селектор ЖК занимали два ряда на пол-экрана, а таблица без
       фиксации ширин ломала бейдж «🌐 Общий для всех ЖК» на три косые строки. */
    .rag-upload-bar {
      display: flex;
      gap: 12px;
      align-items: stretch;
      margin-bottom: 12px;
    }
    .rag-upload-bar .dropzone { flex: 1; padding: 14px 18px; gap: 12px; }
    .rag-upload-bar .dropzone-hint { margin-left: 6px; }
    .rag-upload-bar .form-select { flex: 0 0 230px; }
    .compact-rag-toolbar {
      display: flex;
      gap: 10px;
      align-items: center;
      flex-wrap: wrap;
      margin-top: 12px;
    }
    .compact-rag-toolbar #rag-search-query { flex: 1 1 260px; min-width: 220px; }
    .compact-rag-toolbar #rag-search-project { flex: 0 0 200px; }
    .compact-rag-toolbar #rag-search-topk { flex: 0 0 76px; }
    @media (max-width: 700px) {
      .rag-upload-bar { flex-direction: column; }
      .rag-upload-bar .form-select { flex: 1 1 auto; }
    }

    /* Минимальная ширина таблицы = сумма минимумов колонок. Когда она была
       меньше, колонки не помещались в собственную таблицу и шапка «Дата
       добавления» наезжала на «Действия». */
    #tab-docs table { table-layout: fixed; min-width: 1265px; }
    #tab-docs th:nth-child(1) { width: 17%; min-width: 165px; }  /* Название */
    #tab-docs th:nth-child(2) { width: 12%; min-width: 150px; }  /* Привязка к ЖК */
    #tab-docs th:nth-child(3) { width: 11%; min-width: 130px; }  /* Имя файла */
    #tab-docs th:nth-child(4) { width: 6%;  min-width: 78px;  }  /* Тип */
    #tab-docs th:nth-child(5) { width: 9%;  min-width: 110px; }  /* Категория */
    #tab-docs th:nth-child(6) { width: 9%;  min-width: 110px; }  /* Чанки */
    #tab-docs th:nth-child(7) { width: 9%;  min-width: 122px; }  /* Добавлен */
    #tab-docs th:nth-child(8) { width: 10%; min-width: 150px; }  /* Важность и актуальность */
    #tab-docs th:nth-child(9) { width: 17%; min-width: 250px; }  /* Действия */
    /* Заголовок не переносится и не наезжает на соседний: лишнее прячется. */
    #tab-docs th { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    #tab-docs td { overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
    /* Однострочные ячейки обрезаются многоточием вместо переноса: в узкой
       колонке контента название, файл, тип, категория и дата ломались на
       две-три строки и таблица становилась нечитаемой. */
    #tab-docs td:nth-child(1),
    #tab-docs td:nth-child(3),
    #tab-docs td:nth-child(4),
    #tab-docs td:nth-child(5),
    #tab-docs td:nth-child(6),
    #tab-docs td:nth-child(7) { white-space: nowrap; }
    /* Место под бейдж версии зарезервировано, иначе он уезжал за край ячейки. */
    #tab-docs td:nth-child(1) strong {
      display: inline-block;
      max-width: calc(100% - 52px);
      overflow: hidden;
      text-overflow: ellipsis;
      vertical-align: bottom;
    }
    /* Ячейка действий живёт по своим правилам: обрезать кнопки нельзя. */
    #tab-docs td:last-child { overflow: visible; }
    /* Бейдж переносится по словам, а не обрезается: колонке достаётся ~118px
       при нужных 153, и «Общий для всех ЖК» превращалось в «Общий для всех Ж…»
       на ЛЮБОЙ ширине окна, вплоть до 1440. Обрезанная область действия
       документа — это не подпись, а повод перепутать общий документ с
       привязанным к ЖК. Две строки в ячейке дешевле такой ошибки. */
    .project-badge {
      display: inline-flex;
      align-items: flex-start;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
      text-align: left;
      vertical-align: middle;
    }

    /* ── Панель тарифов и симулятор расходов (вкладка телеметрии) ─────────────
       Пересчёт идёт на клиенте по фактическим токенам: оператор двигает ползунок
       и сразу видит, во что обошёлся бы тот же трафик по другой цене. */
    .tariff-control-panel {
      /* Нижний отступ на самой панели, а не инлайном на соседних карточках:
         панель скрывается целиком у застройщика, и разрыв должен уходить
         вместе с ней. */
      margin: 16px 0;
      padding: 16px 18px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--surface-tint);
    }
    [data-theme="light"] .tariff-control-panel { background: rgba(241, 245, 249, 0.6); }
    .tariff-panel-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      flex-wrap: wrap;
      margin-bottom: 14px;
    }
    .tariff-panel-header h4 {
      font-size: 1rem;
      color: var(--text-main);
      font-family: var(--font-display);
    }
    .tariff-panel-actions { display: flex; align-items: center; gap: 8px; }
    .tariff-sliders-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
      gap: 14px 18px;
    }
    .slider-group { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
    .slider-group label {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      font-size: 0.8rem;
      color: var(--text-muted);
    }
    .slider-badge {
      font-weight: 700;
      color: var(--accent-cyan);
      font-family: var(--font-display);
      white-space: nowrap;
    }
    /* Подпись под ползунком: нужна там, где единица измерения не очевидна
       (например, ставка за минуту аудио рядом со ставками за токены). */
    .slider-hint {
      font-size: 0.72rem;
      line-height: 1.35;
      color: var(--text-muted);
    }
    .tariff-slider {
      width: 100%;
      accent-color: var(--accent-indigo);
      cursor: pointer;
    }
    .tariff-forecast {
      display: flex;
      align-items: baseline;
      gap: 10px;
      flex-wrap: wrap;
      margin-top: 16px;
      padding-top: 14px;
      border-top: 1px solid var(--glass-border);
    }
    .tariff-forecast-label { font-size: 0.85rem; color: var(--text-muted); }
    .tariff-forecast strong {
      font-size: 1.35rem;
      font-family: var(--font-display);
      color: var(--text-main);
    }
    .tariff-forecast-hint { font-size: 0.78rem; color: var(--text-muted); }

    /* ══════════════════════════════════════════════════════════════════════
       ЗАГОЛОВКИ РАЗДЕЛОВ И ОБЩИЕ ЭЛЕМЕНТЫ ВКЛАДОК
       Раньше размер и цвет каждого заголовка задавались инлайном прямо в
       разметке, и соседние вкладки расходились: где 1.1rem, где 1.05rem, где
       свой акцентный цвет. Два класса на всю админку.
       ══════════════════════════════════════════════════════════════════════ */
    .section-title {
      font-family: var(--font-display);
      font-size: 1.1rem;
      font-weight: 700;
      color: var(--text-main);
    }
    .subsection-title {
      font-family: var(--font-display);
      font-size: 1.02rem;
      font-weight: 600;
      color: var(--text-body);
    }
    /* ============================================================
       ЕДИНАЯ ТОЛЩИНА ЛИНИИ ЗНАЧКОВ. Эталон — боковое меню: viewBox 24,
       отрисовка 18px, stroke-width 1.8 — это 1.35 CSS-пикселя на экране.
       Значки кабинета рисуются в том же viewBox, но разными размерами
       (14px крестик модалки, 16px значок кнопки, 22px кнопка шапки), а
       толщина задавалась атрибутом в разметке — одна и та же «1.8» на
       16px давала 1.2px, и рядом с меню значки читались тоньше. Поэтому
       толщину задаём здесь, от размера отрисовки: 32.4 / размер.
       Правило одно на весь кабинет — все SVG здесь линейные значки
       24×24, других в admin.html нет. Атрибуты stroke-width в разметке
       перебиваются этим правилом намеренно: место одно и грепается. */
    svg[viewBox="0 0 24 24"] {
      fill: none;
      stroke: currentColor;
      stroke-width: 1.8;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    svg[viewBox="0 0 24 24"][width="12"] { stroke-width: 2.7; }
    svg[viewBox="0 0 24 24"][width="13"] { stroke-width: 2.5; }
    svg[viewBox="0 0 24 24"][width="14"] { stroke-width: 2.3; }
    svg[viewBox="0 0 24 24"][width="15"] { stroke-width: 2.15; }
    svg[viewBox="0 0 24 24"][width="16"] { stroke-width: 2; }
    svg[viewBox="0 0 24 24"][width="17"] { stroke-width: 1.9; }
    svg[viewBox="0 0 24 24"][width="18"] { stroke-width: 1.8; }
    svg[viewBox="0 0 24 24"][width="20"] { stroke-width: 1.6; }
    svg[viewBox="0 0 24 24"][width="22"] { stroke-width: 1.45; }
    svg[viewBox="0 0 24 24"][width="34"] { stroke-width: 0.95; }
    /* Значок заголовка раздела. Тот же язык, что и у бокового меню: линейный
       SVG, толщина 1.8, цвет — currentColor, никакого своего. Эмодзи давали
       разноцветные пятна в шрифте системы и на светлой теме читались иначе. */
    .head-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      vertical-align: -0.18em;
      /* Высота и line-height фиксированы: иначе строчный бокс значка добавляет
         к строке заголовка собственный интерлиньяж и заголовки со значком
         становятся на пару пикселей выше соседних. */
      height: 18px;
      line-height: 1;
      margin-right: 8px;
      color: var(--text-muted);
      flex-shrink: 0;
      transition: color 0.2s ease, transform 0.2s ease;
    }
    .head-icon svg { display: block; }
    /* Значок кнопки, подписи поля и вкладки. Тот же язык, что у .head-icon,
       только 16×16 — эмодзи в кнопках давали разноцветные пятна и разъезжались
       по базовой линии между шрифтами системы. Разметка значка — <use> на
       общий спрайт #ico-* в начале admin.html, поэтому обводка и заливка
       заданы здесь один раз, а не повторяются в каждой кнопке. */
    .btn-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      vertical-align: -0.2em;
      height: 16px;
      line-height: 1;
      margin-right: 6px;
      flex-shrink: 0;
      color: inherit;
      opacity: 0.85;
      transition: opacity 0.2s ease, color 0.2s ease, transform 0.2s ease;
    }
    /* Обводка, толщина и скругления заданы общим правилом svg[viewBox] выше —
       здесь только раскладка, иначе толщина снова расползётся по местам. */
    .btn-icon svg { display: block; }
    /* Крестик модалки и стрелки галереи стоят в .icon-btn голым <svg> без
       обёртки — плавность отклика задаём им отдельно, приём тот же. */
    .icon-btn > svg { transition: color 0.2s ease, transform 0.2s ease; }
    /* Подсветка при наведении — ровно та же, что у пунктов бокового меню
       (.tab-btn:hover .tab-icon): значок уходит в акцентный фиолетовый и
       сдвигается на пиксель. Раньше кнопки лишь добавляли непрозрачности, и
       отклик у одинаковых на вид значков был разным. */
    .btn:hover > .btn-icon,
    .icon-btn:hover > .btn-icon,
    .icon-btn:hover > svg {
      opacity: 1;
      color: var(--accent-purple);
      transform: translateX(1px);
    }
    /* Кнопка с заливкой (основное и опасное действие) отклик уже даёт фоном:
       фиолетовый значок на фиолетовой или красной кнопке пропадал. */
    .btn-primary:hover > .btn-icon,
    .btn-danger:hover > .btn-icon { color: inherit; transform: none; }
    /* Значок, стоящий в своей обёртке (карточка руководства, карточка
       диагностики, стратегия Co-Pilot, переключатель темы), ничего вправо не
       отодвигает: отступ там задаёт gap контейнера. */
    .docs-card-icon > .btn-icon,
    .diag-card-icon > .btn-icon,
    .copilot-card-icon > .btn-icon,
    .quality-empty-icon > .btn-icon,
    .media-gallery-thumbicon > .btn-icon,
    .theme-icon.btn-icon { margin-right: 0; }
    /* Карточки руководств крупнее кнопки: значок 18px читается как заголовок. */
    .docs-card-icon > .btn-icon { height: 18px; }
    .docs-card-icon > .btn-icon svg { width: 18px; height: 18px; }
    /* Значок в кнопке без подписи (.icon-btn) не отодвигает ничего вправо. */
    .icon-btn > .btn-icon { margin-right: 0; }
    /* Опасное действие: значок читается предупреждением, а не украшением. */
    .btn-danger > .btn-icon { opacity: 1; }
    @media (max-width: 480px) {
      .btn-icon { margin-right: 5px; }
    }
    /* Заголовок отзывается тем же приёмом, что и пункт бокового меню. */
    h2:hover > .head-icon,
    h3:hover > .head-icon { color: var(--accent-purple); transform: translateX(1px); }
    @media (max-width: 480px) {
      .head-icon { margin-right: 6px; height: 16px; }
      .head-icon svg { width: 16px; height: 16px; }
    }
    /* Разрыв между блоками одной вкладки — тоже общий, а не инлайновый. */
    .faq-toolbar.section-gap { margin-top: 24px; }
    /* Панель управления в шапке раздела: подпись + контрол + кнопка. */
    .toolbar-inline {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }
    .toolbar-inline-label { font-size: 0.85rem; color: var(--text-muted); }
    .toolbar-select { width: 150px; min-width: 120px; }
    /* Узкий числовой инпут (порог просрочки минут) — тот же язык фильтров,
       но без раздутой ширины текстового селекта. */
    .toolbar-select-narrow { width: 92px; min-width: 76px; }
    /* Пара «подпись + селект» внутри .toolbar-inline: своя группа нужна, чтобы
       на узком экране фильтр переносился целиком, а не отрывался от подписи. */
    .toolbar-inline-group {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
    }
    /* Заголовок раздела и бейдж его области рядом: бейдж нельзя вкладывать в сам
       заголовок — тот переписывается через textContent и стёр бы вложенный узел. */
    .toolbar-title-group {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
      min-width: 0;
    }
    /* Единая пустая/ошибочная строка таблицы вместо инлайновых цветов. */
    .table-empty-cell {
      text-align: center;
      padding: 24px;
      color: var(--text-muted);
    }
    .table-empty-cell.is-error { color: var(--accent-rose); }

    /* ── Вкладка телеметрии ───────────────────────────────────────────────
       Себестоимость в рублях — закупочная цена платформы у провайдера. Без
       права tariffs:manage вкладка получает класс no-cost-basis, и колонка
       исчезает целиком: и заголовок, и ячейки. Прятать только ячейки нельзя —
       шапка съедет относительно данных. */
    #tab-telemetry.no-cost-basis .cost-col { display: none; }
    /* Шесть показателей стоят двумя рядами по три, а не одной лентой: в ряд из
       пяти-шести карточек подписи («курс 95 ₽», «19 из 20 ответов») сжимаются до
       многоточия, а крупные цифры теряют кегль. Колонки фиксированные, не
       auto-fit: при скрытой себестоимости (право tariffs:manage) карточек пять,
       и auto-fit растягивал бы остаток ряда — сетка «дышала» на ровном месте.
       Правило адресное, по id: верхний ряд KPI над вкладками (`.stats-grid`)
       остаётся на четырёх колонках.
       Брейкпоинты повторены здесь же, а не в общем блоке responsive: тот идёт по
       файлу ВЫШЕ, и при равном весе селектора десктопные три колонки доживали бы
       до телефона. */
    #telemetry-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    @media (max-width: 1024px) {
      #telemetry-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    @media (max-width: 550px) {
      #telemetry-cards { grid-template-columns: minmax(0, 1fr); }
    }
    #tab-telemetry table { table-layout: fixed; min-width: 780px; }
    #tab-telemetry th { white-space: nowrap; }
    #tab-telemetry th:nth-child(1) { width: 26%; min-width: 180px; }
    #tab-telemetry th:nth-child(2) { width: 12%; min-width: 110px; }
    #tab-telemetry th:nth-child(3) { width: 16%; min-width: 130px; }
    #tab-telemetry th:nth-child(4) { width: 16%; min-width: 130px; }
    #tab-telemetry th:nth-child(5) { width: 15%; min-width: 130px; }
    #tab-telemetry th:nth-child(6) { width: 15%; min-width: 130px; }
    #tab-telemetry td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

    /* Шкалы динамики по дням и по часам: одна разметка на оба блока. */
    .telemetry-bars { margin-top: 10px; }
    .telemetry-bar-row {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-bottom: 6px;
      font-size: 0.85rem;
    }
    .telemetry-bar-label {
      flex: 0 0 92px;
      color: var(--text-muted);
    }
    .telemetry-bar-track {
      flex: 1 1 auto;
      height: 14px;
      min-width: 0;
      border-radius: var(--radius-sm);
      background: var(--glass-border);
      overflow: hidden;
    }
    .telemetry-bar-fill {
      height: 100%;
      background: var(--accent-gradient-primary-h);
      transition: width 0.3s ease;
    }
    .telemetry-bar-value {
      flex: 0 0 auto;
      min-width: 150px;
      text-align: right;
      color: var(--text-body);
    }
    .telemetry-bars-empty { font-size: 0.9rem; color: var(--text-muted); }
    @media (max-width: 767px) {
      .telemetry-bar-label { flex-basis: 64px; font-size: 0.78rem; }
      .telemetry-bar-value { min-width: 0; font-size: 0.78rem; }
      .toolbar-select { width: 100%; }
      /* На телефоне фильтры телеметрии становятся столбцом: селект застройщика
         тянется на всю ширину, подпись остаётся над ним. */
      .toolbar-inline-group { flex: 1 1 100%; }
    }

    /* ── Подраздел «Телеметрия движка» (#telemetry-engine-section) ─────────
       Стоит СРАЗУ ПОСЛЕ правил вкладки телеметрии намеренно: выше по файлу
       #tab-telemetry жёстко задаёт ширины колонок по порядковому номеру и режет
       содержимое ячеек многоточием. Для шестиколоночной таблицы расхода это
       верно, для семиколоночной таблицы отпечатков — нет, и переопределения
       обязаны идти ниже, иначе при равном весе селектора победят чужие ширины.
       Ни одного HEX: полупрозрачные подложки — rgba от тех же фирменных цветов,
       что и токены акцентов, поэтому обе темы наследуются сами. */
    .engine-section { margin-top: 8px; }

    /* Пояснение над срезом: короче вводного абзаца раздела, но той же природы. */
    .engine-note {
      margin: 0 0 12px;
      max-width: 96ch;
      font-size: 0.82rem;
      line-height: 1.6;
      color: var(--text-muted);
    }
    .engine-note strong { color: var(--text-body); font-weight: 600; }

    /* Две оговорки к разбивке задержки. Врезка, а не сноска: цифры без них
       читаются неверно, и прятать их мелким кеглем нельзя. */
    .engine-caveats {
      display: flex;
      flex-direction: column;
      gap: 8px;
      margin: 0 0 12px;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: rgba(245, 158, 11, 0.06);
    }
    .engine-caveat {
      margin: 0;
      max-width: 96ch;
      font-size: 0.82rem;
      line-height: 1.6;
      color: var(--text-body);
    }
    .engine-caveat strong { color: var(--text-main); font-weight: 600; }

    /* Зажжённое предупреждение по суткам. Отдельный вид от врезки-оговорки:
       оговорка объясняет, тревога требует разбора. */
    .engine-alert {
      margin: 0 0 12px;
      padding: 12px 14px;
      border: 1px solid rgba(244, 63, 94, 0.35);
      border-left: 3px solid var(--accent-rose);
      border-radius: var(--radius-sm);
      background: rgba(244, 63, 94, 0.08);
      font-size: 0.85rem;
      line-height: 1.6;
      color: var(--text-body);
    }
    .engine-alert strong { color: var(--text-main); font-weight: 600; }

    /* Крупные числа среза. Каждое обязано ехать со своим знаменателем — он
       набран отдельной строкой под значением, а не подсказкой при наведении:
       на телефоне подсказки нет вовсе, а доля без знаменателя врёт. */
    .engine-figures {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
      gap: 10px;
      margin: 0 0 12px;
    }
    .engine-figure {
      padding: 12px 14px;
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: border-color 0.2s ease, transform 0.2s ease;
    }
    .engine-figure:hover { border-color: var(--accent-indigo); transform: translateY(-2px); }
    .engine-figure-label {
      display: block;
      font-size: 0.74rem;
      letter-spacing: 0.04em;
      text-transform: uppercase;
      color: var(--text-muted);
    }
    .engine-figure-value {
      display: block;
      margin-top: 4px;
      font-family: var(--font-display);
      font-size: 1.35rem;
      font-weight: 700;
      color: var(--text-main);
      line-height: 1.2;
    }
    .engine-figure-value.is-warning { color: var(--accent-gold-soft); }
    .engine-figure-value.is-alarm { color: var(--accent-rose-soft); }
    /* Знаменатель доли. Не подсказка и не мелкая сноска — строка под цифрой. */
    .engine-figure-sub {
      display: block;
      margin-top: 4px;
      font-size: 0.76rem;
      line-height: 1.45;
      color: var(--text-muted);
    }

    /* Prompt-budget — часть инженерного отчёта, а не настройка. Таблицы делят
       общую геометрию engine-table; отдельный класс удерживает подпись блока
       читаемой, когда на телефоне таблица прокручивается горизонтально. */
    #tab-telemetry .engine-table-budget th:first-child,
    #tab-telemetry .engine-table-budget td:first-child { min-width: 210px; }

    /* Прочерк «измерения не было». Пунктирное подчёркивание отличает его от
       обычного дефиса в тексте и обещает пояснение при наведении; сам текст
       пояснения дублируется рядом обычной строкой, потому что на телефоне
       наведения не существует. */
    .engine-dash {
      color: var(--text-muted);
      border-bottom: 1px dashed var(--text-muted);
      cursor: help;
    }
    .engine-hint {
      display: block;
      margin-top: 2px;
      font-size: 0.74rem;
      line-height: 1.4;
      color: var(--text-muted);
    }

    /* Человеческий текст на месте пустой таблицы: пустая таблица читается как
       «всё хорошо», а это ровно та ложь, от которой уходим. */
    .engine-empty {
      margin: 0 0 12px;
      padding: 14px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-sm);
      font-size: 0.85rem;
      line-height: 1.6;
      color: var(--text-muted);
    }
    .engine-empty.is-error { color: var(--accent-rose); border-color: rgba(244, 63, 94, 0.35); }

    /* Таблицы среза. Ширины и переносы задаются здесь целиком: правила вкладки
       выше рассчитаны на другую таблицу. */
    #tab-telemetry .engine-table { table-layout: auto; min-width: 640px; }
    #tab-telemetry .engine-table th { white-space: nowrap; width: auto; min-width: 0; }
    #tab-telemetry .engine-table td {
      white-space: normal;
      overflow: visible;
      text-overflow: clip;
      vertical-align: top;
    }
    /* Числовые ячейки — моноширинные и правым краем: колонки сравнивают глазом. */
    #tab-telemetry .engine-table .engine-num {
      font-family: var(--font-mono);
      text-align: right;
      white-space: nowrap;
    }
    #tab-telemetry .engine-table th.engine-num { text-align: right; }
    /* Пояснение к прочерку внутри числовой ячейки переносится: nowrap колонки
       растянул бы таблицу на несколько экранов ради одной подписи. */
    #tab-telemetry .engine-table .engine-num .engine-hint { white-space: normal; }
    .engine-hash {
      font-family: var(--font-mono);
      font-size: 0.82rem;
      color: var(--accent-cyan);
      word-break: break-all;
    }
    .engine-when { font-family: var(--font-mono); font-size: 0.8rem; white-space: nowrap; }
    /* Строка вложенного слоя: отступ кодирует вложенность, о которой говорит
       врезка выше, — читать «rag внутри gather» приходится глазами. */
    .engine-layer-nested .engine-layer-name { padding-left: 18px; }
    .engine-layer-name-hint {
      display: block;
      margin-top: 2px;
      font-size: 0.74rem;
      line-height: 1.4;
      color: var(--text-muted);
      font-weight: 400;
    }
    .engine-row-total .engine-layer-name { font-weight: 700; color: var(--text-main); }
    .engine-row-total td { border-top: 1px solid var(--glass-border); }
    /* Заслон, который за окно не сработал ни разу. Строка дорисована по списку
       известных имён, а не пришла из журнала, — приглушаем её, чтобы отличать
       наблюдение от достроенного нуля, но не прячем: исчезнувший заслон
       неотличим от неподключённого. */
    .engine-guard-silent td { color: var(--text-muted); }
    .engine-day-state { font-size: 0.8rem; line-height: 1.45; }
    .engine-day-state.is-over { color: var(--accent-rose-soft); font-weight: 600; }
    .engine-day-state.is-small { color: var(--accent-gold-soft); }

    @media (max-width: 767px) {
      .engine-figures { grid-template-columns: minmax(0, 1fr); }
      .engine-figure-value { font-size: 1.2rem; }
      #tab-telemetry .engine-table { min-width: 560px; }
      .engine-section .toolbar-inline { flex-wrap: wrap; }
      .engine-section .toolbar-inline .btn,
      .engine-section .quality-toggle { flex: 1 1 100%; justify-content: center; }
    }
    @media (max-width: 480px) {
      .engine-caveats { padding: 10px 12px; }
      .engine-note, .engine-caveat { font-size: 0.8rem; }
      #tab-telemetry .engine-table { min-width: 480px; font-size: 0.82rem; }
    }

    /* Плашка «коуч ждёт запроса»: разбор в тренажёре теперь по кнопке, и пустая
       панель без объяснения читалась как поломка. */
    .trainer-coach-idle {
      font-size: 0.88rem;
      line-height: 1.5;
      color: var(--text-muted);
      padding: 14px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--surface-tint-weak);
    }
    [data-theme="light"] .trainer-coach-idle { background: rgba(241, 245, 249, 0.6); }
    #trainer-history-table th { white-space: nowrap; }

    /* Панель тренажёра — не фильтры (.toolbar-filters), а «что тренируем»
       слева и «действия тренировки» справа. Разнос по краям держит
       деструктивное «Сбросить» подальше от «Создать сценарий». */
    .trainer-toolbar {
      display: flex;
      flex-wrap: wrap;
      align-items: flex-end;
      justify-content: space-between;
      gap: 14px 20px;
      padding: 14px 16px;
      margin-bottom: 16px;
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      backdrop-filter: var(--blur-glass);
    }
    /* Левая группа — «что тренируем»: сценарий и психотип рядом, но каждый со
       своим лейблом, чтобы селект психотипа не слипался с кнопками сценария. */
    .trainer-toolbar-group {
      display: flex;
      flex-wrap: wrap;
      align-items: flex-end;
      gap: 12px 16px;
      min-width: 0;
      flex: 1 1 460px;
    }
    .trainer-toolbar-field {
      display: flex;
      flex-direction: column;
      gap: 6px;
      min-width: 0;
    }
    .trainer-toolbar-field:first-child { flex: 1 1 300px; }
    .trainer-toolbar-field:last-child { flex: 0 1 220px; }
    .trainer-toolbar-field .form-select { width: 100%; }
    .trainer-toolbar-label {
      font-size: 0.78rem;
      font-weight: 600;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.04em;
    }
    .trainer-scenario-picker {
      display: flex;
      align-items: center;
      gap: 6px;
      min-width: 0;
    }
    .trainer-scenario-picker .form-select { flex: 1 1 auto; min-width: 0; }
    /* Иконочные кнопки сценария — тот же квадрат, что и в таблицах
       (.btn-table-action.icon-only), плюс фиксированная высота под селектом:
       без неё ➕ / ✏️ / 🗑 разъезжались по длине подписи. */
    .trainer-scenario-picker .btn-table-action.icon-only {
      flex: 0 0 auto;
      height: 38px;
      min-width: 38px;
      font-size: 0.95rem;
    }
    .trainer-toolbar-actions {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
    }

    /* Психотип виден до старта: он приходит из сценария, отдельного селектора
       в панели нет. */
    .trainer-hint-row {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: 8px;
    }
    .trainer-personality-badge {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 3px 10px;
      border-radius: var(--radius-pill);
      font-size: 0.76rem;
      color: var(--text-body);
      background: rgba(99, 102, 241, 0.12);
      border: 1px solid rgba(99, 102, 241, 0.32);
    }
    .trainer-personality-badge.is-off {
      color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.12);
      border-color: rgba(245, 158, 11, 0.35);
    }

    @media (max-width: 768px) {
      .trainer-toolbar { flex-direction: column; align-items: stretch; }
      .trainer-toolbar-group { flex: 1 1 auto; flex-direction: column; align-items: stretch; }
      .trainer-toolbar-field,
      .trainer-toolbar-field:first-child,
      .trainer-toolbar-field:last-child { flex: 1 1 auto; }
      .trainer-toolbar-actions { justify-content: flex-start; }
    }
    @media (max-width: 480px) {
      .trainer-toolbar { padding: 12px; }
      /* Кнопки действий на всю ширину — иначе «Начать тренировку» ужимается
         до нечитаемого огрызка. */
      .trainer-toolbar-actions { flex-direction: column; align-items: stretch; }
      .trainer-toolbar-actions .btn { width: 100%; justify-content: center; }
      .trainer-scenario-picker { flex-wrap: wrap; }
      .trainer-scenario-picker .form-select { flex: 1 1 100%; }
    }

    /* ── Действия активной тренировки ────────────────────────────────────────
       Рабочая пара «Ответить покупателю» (главное, btn-primary) и «Попросить
       подсказку» (вспомогательное, btn-secondary) держится слева единым блоком.
       «Завершить тренировку» — терминальное действие: уходит к правому краю за
       разделитель, чтобы не читаться как третья равноправная кнопка и не ловить
       случайный клик. Цвет — существующий .btn-danger (rose), новых оттенков не
       заводим. */
    /* Ряд переносится, а не ужимается: в колонке тренажёра (≈620px даже на
       1440) три кнопки в одну строку не помещались, и подпись главного
       действия обрезалась до «Отве…» — это уже не подпись. Переносом
       «Завершить тренировку» уходит на вторую строку целиком, оставаясь
       справа за разделителем. Многоточие ниже остаётся страховкой. */
    .trainer-reply-actions {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 10px 16px;
      margin-top: 10px;
      min-width: 0;
    }
    .trainer-reply-actions-main {
      display: flex;
      flex-wrap: nowrap;
      align-items: center;
      gap: 8px;
      min-width: 0;
      flex: 0 1 auto;
    }
    .trainer-reply-actions-end {
      display: flex;
      align-items: center;
      margin-left: auto;
      padding-left: 16px;
      border-left: 1px solid var(--card-border);
      flex: 0 1 auto;
      min-width: 0;
    }
    /* Ряд теперь переносится, и на второй строке «Завершить тренировку» остаётся
       одна — а разделитель уезжал вместе с ней и отделял пустоту слева (находка
       ревью 17.08.2026, тот же дефект, что убрали у точки-разделителя в
       виджете). На перенесённой строке блок прижат к левому краю, поэтому
       отделять нечего: черта и отступ снимаются. */
    .trainer-reply-actions-end:not(:nth-child(2)),
    .trainer-reply-actions-end:only-child {
      border-left: none;
      padding-left: 0;
    }
    /* Один визуальный вес: одинаковая высота и не сжимающиеся иконки, чтобы
       кнопки не «дышали» по-разному при разной длине подписи. Обрезка
       многоточием — крайняя мера: сначала ряд переносится (см. flex-wrap
       выше), и только если не хватило и этого, подпись ужимается. */
    .trainer-reply-actions .btn {
      min-height: 40px;
      line-height: 1.2;
      min-width: 0;
      flex: 0 1 auto;
      white-space: nowrap;
      overflow: hidden;
    }
    .trainer-reply-actions .btn svg { flex: 0 0 auto; }
    .trainer-btn-label {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    /* До старта тренировки все три кнопки заблокированы — общий приглушённый
       вид, чтобы «Ответить покупателю» не выглядела нажимаемой. */
    .trainer-reply-actions .btn:disabled { opacity: 0.5; cursor: not-allowed; }
    .trainer-reply-actions .btn:disabled:hover { transform: none; box-shadow: none; }

    @media (max-width: 768px) {
      /* Ряд к этой ширине уже переносится (см. flex-wrap выше) — здесь экономим
         на воздухе, чтобы строк было как можно меньше. */
      .trainer-reply-actions { gap: 8px 10px; }
      .trainer-reply-actions-end { padding-left: 10px; }
      .trainer-reply-actions .btn { padding-left: 12px; padding-right: 12px; }
    }
    @media (max-width: 480px) {
      /* Места на подписи уже нет: остаются иконки, смысл держат aria-label и
         title — их дублирует подсказка при удержании. */
      .trainer-reply-actions .trainer-btn-label { display: none; }
      .trainer-reply-actions .btn {
        justify-content: center;
        padding-left: 10px;
        padding-right: 10px;
      }
    }

    /* Ячейка действий в базе знаний: подписи вместо иконок-загадок, поэтому
       кнопкам нужен воздух и перенос на вторую строку на узком экране. */
    .rag-actions-expanded { gap: 6px 8px; }
    .rag-actions-expanded .btn {
      padding: 5px 10px;
      font-size: 0.78rem;
      border-radius: var(--radius-sm);
    }

    /* Протокол прошедшей тренировки: стенограмма и разбор в одном окне */
    .trainer-session-summary {
      background: rgba(0, 0, 0, 0.2);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      padding: 14px;
    }
    [data-theme="light"] .trainer-session-summary { background: rgba(241, 245, 249, 0.8); }
    #trainer-view-feed { min-height: 0; }

    /* ── Карточки психотипов покупателей ─────────────────────────────────────
       Психотип влияет сразу на два контура — живого бота и тренажёр, поэтому
       тумблеры вынесены на карточку: видно, где профиль уже работает. */
    .personality-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 14px 16px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--surface-tint);
    }
    [data-theme="light"] .personality-card { background: rgba(241, 245, 249, 0.7); }
    .personality-card.is-muted { opacity: 0.6; }
    .personality-card-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .personality-card-head strong {
      font-family: var(--font-display);
      color: var(--text-main);
      font-size: 0.98rem;
    }
    .personality-key {
      font-size: 0.72rem;
      color: var(--text-muted);
      font-family: 'SF Mono', Consolas, monospace;
    }
    .personality-directive {
      font-size: 0.82rem;
      line-height: 1.45;
      color: var(--text-body);
      display: -webkit-box;
      -webkit-line-clamp: 4;
      line-clamp: 4;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }
    /* Сводка «где применяется» вместо пульта: карточка рассказывает состояние,
       а переключают его в модалке правки. */
    .personality-usage {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
      padding-top: 8px;
      border-top: 1px solid var(--glass-border);
      font-size: 0.8rem;
      color: var(--text-muted);
    }
    .personality-usage-badge {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      padding: 3px 10px;
      border: 1px solid rgba(99, 102, 241, 0.28);
      border-radius: var(--radius-pill);
      background: rgba(99, 102, 241, 0.12);
      color: var(--text-body);
      font-size: 0.76rem;
      white-space: nowrap;
    }
    .personality-usage.is-off .personality-usage-note { color: var(--accent-rose); }
    .personality-toggle {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 0.8rem;
      color: var(--text-muted);
      cursor: pointer;
    }
    .personality-toggle input { accent-color: var(--accent-indigo); cursor: pointer; }

    /* Переключатель «Показывать демо» в панели фильтров вкладки «Сделки & Память» */
    .leads-demo-toggle {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      align-self: flex-end;
      padding: 9px 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm, 10px);
      font-size: 0.85rem;
      color: var(--text-muted);
      white-space: nowrap;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .leads-demo-toggle:hover {
      color: var(--text-main);
      border-color: var(--accent-purple);
    }
    .leads-demo-toggle input { accent-color: var(--accent-purple); cursor: pointer; }
    .leads-demo-toggle:has(input:checked) {
      color: var(--text-main);
      border-color: var(--accent-purple);
      background: rgba(139, 92, 246, 0.12);
    }

    /* Уточнение под фильтрами диалогов: дата в отчёте — не начало сессии, а
       её последняя активность. Это спокойная служебная подпись, а не второй
       заголовок, поэтому не конкурирует с таблицей за внимание. */
    .dialogs-export-note {
      margin: -2px 0 14px;
      color: var(--text-muted);
      font-size: 0.78rem;
      line-height: 1.45;
    }

    /* Демо-строка в таблице сделок: боевую от тестовой видно без вчитывания */
    tr.lead-row-demo > td { background: rgba(139, 92, 246, 0.07); }
    [data-theme="light"] tr.lead-row-demo > td { background: rgba(139, 92, 246, 0.09); }
    /* Плашки под именем клиента: свой ряд ячейки, а не хвост строки с именем.
       Ширина колонки — 140px, поэтому длинный текст задачи переносится внутри
       плашки; ряд не растягивает таблицу и горизонтальный скролл не растёт. */
    .lead-badges {
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
      margin-top: 5px;
    }
    .lead-badges > .lead-demo-badge,
    .lead-badges > .lead-approval-badge,
    .lead-badges > .lead-reengage-badge {
      margin-left: 0;
      max-width: 100%;
      line-height: 1.3;
      /* Тексты задач длиннее колонки: переносим внутри плашки, иначе её снова
         срежет overflow: hidden ячейки. */
      white-space: normal;
      overflow-wrap: anywhere;
    }
    .lead-demo-badge {
      display: inline-block;
      margin-left: 6px;
      padding: 1px 7px;
      border-radius: 999px;
      border: 1px solid rgba(139, 92, 246, 0.45);
      background: rgba(139, 92, 246, 0.16);
      color: var(--accent-purple-soft);
      font-size: 0.68rem;
      font-weight: 700;
      vertical-align: middle;
      white-space: nowrap;
    }

    /* Плашка задачи в строке сделки: янтарь = от менеджера ждут действия. */
    .lead-approval-badge {
      display: inline-block;
      margin-left: 6px;
      padding: 1px 7px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(245, 158, 11, 0.5);
      background: rgba(245, 158, 11, 0.16);
      color: var(--accent-amber);
      font-size: 0.68rem;
      font-weight: 700;
      vertical-align: middle;
      white-space: nowrap;
    }
    /* На жемчужном фоне светлые оттенки бейджей пропадают: берём затемнённую
       ступень той же ветки палитры (как сделано с индиго заголовков). */
    [data-theme="light"] .lead-demo-badge { color: var(--accent-purple-deep); }
    [data-theme="light"] .lead-approval-badge { color: var(--accent-amber-deep); }
    [data-theme="light"] .lead-approval-badge-legacy { color: var(--text-muted); }

    /* Кто взял сделку в работу. Изумруд = «этим уже занимаются», а не тревога:
       плашка должна остановить второй звонок, а не выглядеть ошибкой. */
    .lead-assignee-badge {
      display: inline-block;
      max-width: 100%;
      margin-top: 6px;
      padding: 2px 8px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(16, 185, 129, 0.45);
      background: rgba(16, 185, 129, 0.14);
      color: var(--accent-emerald-soft);
      font-size: 0.68rem;
      font-weight: 700;
      line-height: 1.35;
      /* Имя менеджера — свободный текст: длинное переносится внутри плашки, а не
         режет колонку. Соседние ячейки строки однострочные, эта — нет. */
      white-space: normal;
      overflow-wrap: anywhere;
    }
    [data-theme="light"] .lead-assignee-badge { color: var(--accent-green); }
    /* В строке диалога та же плашка живёт в общем ряду .lead-badges (там свой
       gap), поэтому собственный верхний отступ ей не нужен — иначе ряд плашек
       разъезжается по высоте. */
    .lead-badges > .lead-assignee-badge { margin-top: 0; }

    /* ======================================================================
       Горячий лид без реакции: сигнал внутри кабинета
       ----------------------------------------------------------------------
       Не уведомление и не модалка — только видимая метка там, где менеджер и
       так смотрит: строка очереди действий на главной (.dash-queue-row) и
       пометка строки в таблице сделок. Стили KPI-плиток (.stat-card-clickable,
       .stat-drill, .stat-compare, .stat-aside, .stats-grid-note,
       .hot-unattended-flag) уехали вместе с самими плитками 05.09.2026.
       ====================================================================== */

    /* Фильтр «только горячие без реакции» — та же геометрия, что у демо-тумблера,
       но своя ветка палитры: это тревога, а не служебный признак. */
    .leads-hot-toggle:hover { border-color: var(--accent-rose); }
    .leads-hot-toggle input { accent-color: var(--accent-rose); }
    .leads-hot-toggle:has(input:checked) {
      color: var(--text-main);
      border-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.12);
    }

    /* Строка сделки, которую никто не взял: акцентная полоса слева у первой
       ячейки — рамка на <tr> в таблице не рисуется. */
    tr.lead-row-hot-unattended > td { background: rgba(244, 63, 94, 0.08); }
    [data-theme="light"] tr.lead-row-hot-unattended > td { background: rgba(244, 63, 94, 0.09); }
    tr.lead-row-hot-unattended > td:first-child {
      box-shadow: inset 3px 0 0 0 var(--accent-rose);
    }

    /* Скругление и отступы — те же токены, что у кнопок таблицы
       (`.btn-table-action`): владелец (сессия 05.09.2026) попросил убрать
       «таблеточную» пилюлю рядом с прямоугольными кнопками действий строки.
       Цвет и вес шрифта остаются тревожными — меняется только форма. */
    .lead-hot-badge {
      display: inline-block;
      margin-left: 6px;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(244, 63, 94, 0.5);
      background: rgba(244, 63, 94, 0.16);
      color: var(--accent-rose-soft);
      font-size: 0.72rem;
      font-weight: 700;
      vertical-align: middle;
      white-space: nowrap;
    }
    .lead-badges > .lead-hot-badge {
      margin-left: 0;
      max-width: 100%;
      line-height: 1.3;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    [data-theme="light"] .lead-hot-badge { color: var(--accent-rose); }

    /* Кнопка «Взял в работу» — первичное действие строки, но своей ветки цвета */
    .take-hot-lead-btn {
      border-color: rgba(244, 63, 94, 0.5) !important;
      background: rgba(244, 63, 94, 0.14) !important;
      color: var(--accent-rose-soft) !important;
      transition: all 0.2s ease;
    }
    .take-hot-lead-btn:hover:not(:disabled) {
      background: rgba(244, 63, 94, 0.24) !important;
      border-color: var(--accent-rose) !important;
      transform: translateY(-1px);
    }
    .take-hot-lead-btn:disabled { opacity: 0.6; cursor: progress; }
    [data-theme="light"] .take-hot-lead-btn { color: var(--accent-rose) !important; }

    /* Фильтр «ждут контакта» — своя ветка палитры на cyan: НЕ rose (это не
       тревога, ни к чему не зовёт — заявка сама уедет, как только появится
       телефон) и НЕ серая dispatch-плашка (та про «уже пробуем сами», здесь —
       «пока и пробовать нечего»). Спокойный, но заметный акцент. */
    .leads-hold-toggle:hover { border-color: var(--accent-cyan); }
    .leads-hold-toggle input { accent-color: var(--accent-cyan); }
    .leads-hold-toggle:has(input:checked) {
      color: var(--text-main);
      border-color: var(--accent-cyan);
      background: rgba(6, 182, 212, 0.12);
    }

    /* Плашка на карточке/строке задержанной заявки. Текст приходит с сервера
       дословно (crm_hold_reason) — сюда его вставляют через textContent/строку
       уже готовой, ничего не пересказываем.
       Скругление и центровка — тот же токен и приём, что у .lead-hot-badge
       (владелец, сессия 10.09.2026: «такое же закругление, иконка и текст на
       одной линии»). display: inline-flex вместо inline-block — надёжнее
       vertical-align для центровки .btn-icon по вертикали независимо от
       font-size конкретной плашки. */
    .lead-hold-badge {
      display: inline-flex;
      align-items: center;
      margin-left: 6px;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(6, 182, 212, 0.45);
      background: rgba(6, 182, 212, 0.14);
      color: var(--accent-cyan);
      font-size: 0.68rem;
      font-weight: 600;
      line-height: 1.3;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    .lead-badges > .lead-hold-badge { margin-left: 0; }

    /* Род заявки («Не покупатель квартиры» и т.п., leadKindBadgeHtml). Тон
       нейтральный, а не тревожный — та же серая ветка, что у остывшего BANT
       (.badge-gray): это не брак и не ошибка доставки, просто другой род
       обращения, и заявка ушла в отдельную воронку по решению владельца.
       Скругление и центровка — см. комментарий у .lead-hold-badge выше. */
    .lead-kind-badge {
      display: inline-flex;
      align-items: center;
      margin-left: 6px;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: rgba(148, 163, 184, 0.16);
      color: var(--text-muted);
      font-size: 0.68rem;
      font-weight: 600;
      line-height: 1.3;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    .lead-badges > .lead-kind-badge { margin-left: 0; }
    /* Таблица диалогов: та же плашка второй строкой под этапом заявки. */
    .dialog-kind-row { margin-top: 4px; }
    .dialog-kind-row .lead-kind-badge { margin-left: 0; }

    /* Прогрев в строке диалога — та же .bant-bar-track, что в списке сделок,
       но своей строкой под этапом заявки: колонка «Заявка» узкая, и полоса
       рядом с плашкой в один ряд не помещается. Новой колонки не заводим —
       таблица и так девятиколоночная. */
    .dialog-score-row {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 2px;
      margin-top: 6px;
    }
    .dialog-score-row .bant-bar-track { margin-right: 6px; }
    /* На узком экране полоса уступает место цифре: процент читается всегда,
       шкала — украшение поверх него. */
    @media (max-width: 768px) {
      .dialog-score-row .bant-bar-track { width: 48px; }
    }

    /* Куда уехала доставленная заявка: короткий статус («Ушла в amoCRM») +
       кнопка на сделку сверху, а воронка/стадия/ответственный — отдельным
       блоком «подпись → значение» ниже. Разложено по требованию владельца
       (30.08.2026): стрелочная строка слепляла разные факты в кашу, особенно
       с длинным ФИО ответственного. Изумруд = «доставлено», та же ветка, что
       у .lead-assignee-badge. */
    .lead-crm-route-row {
      margin-top: 6px;
    }
    .lead-crm-route-block {
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .lead-crm-route-block > .lead-crm-route,
    .lead-crm-route-block > .lead-crm-deal-link {
      align-self: flex-start;
    }
    /* Компактный режим (таблица диалогов): статус и ссылка на сделку в один
       ряд, а не колонкой — <dl> воронки/стадии/ответственного здесь не
       рисуется вовсе (лежит в <template>, см. openCrmRouteModal), поэтому
       колоночный gap самой строке уже не нужен. */
    .lead-crm-route-block-compact {
      flex-direction: row;
      flex-wrap: wrap;
      align-items: center;
    }
    .lead-crm-route-block-compact > .lead-crm-route,
    .lead-crm-route-block-compact > .lead-crm-deal-link {
      align-self: auto;
    }
    /* Скругление и центровка — тот же токен и приём, что у .lead-hot-badge
       (владелец, сессия 10.09.2026: «такое же закругление, иконка и текст на
       одной линии»). display: inline-flex вместо inline-block — надёжнее
       vertical-align для центровки .btn-icon/каретки по вертикали. */
    .lead-crm-route {
      display: inline-flex;
      align-items: center;
      max-width: 100%;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(16, 185, 129, 0.4);
      background: rgba(16, 185, 129, 0.12);
      color: var(--accent-emerald-soft);
      font-size: 0.72rem;
      font-weight: 600;
      line-height: 1.35;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    [data-theme="light"] .lead-crm-route { color: var(--accent-green); }

    /* Оговорка у ДОСТАВЛЕННОЙ заявки (PLAN_DOMOPLANER_API.md, раздел 2.5):
       CRM создала заявку, но при этом вернула `warnings` — например,
       неверный project_id. Это не отказ доставки (своей ветки в leadCrmAction
       не получает, caseId остаётся 'delivered'), а оговорка к уже доставленной
       заявке — поэтому янтарный тон и СВОЯ строка внутри .lead-crm-route-row,
       под .lead-crm-route-block (тот — display:flex БЕЗ inline, блочный, и
       тянется на всю ширину строки — сосед после него и так уходит на новую
       строку, здесь это НАРОЧНО: то же требование «не по горизонтали, а по
       вертикали», что и у блока «Сейчас в CRM», см. .lead-crm-external-status
       выше). Скругление и центровка — тот же приём, что у остальных плашек
       состояния (см. .lead-hold-badge выше). */
    .lead-crm-warning-badge {
      display: inline-flex;
      align-items: center;
      margin-top: 6px;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(245, 158, 11, 0.45);
      background: rgba(245, 158, 11, 0.14);
      color: var(--accent-amber);
      font-size: 0.72rem;
      font-weight: 600;
      line-height: 1.35;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }

    /* ПРИСОЕДИНЕНА К ЧУЖОЙ СДЕЛКЕ (ДЕЛЬТА H, п.4): сделку по этой заявке не
       заводили — она легла примечанием в уже открытую сделку того же
       телефона (crm_service.joined_deal_mark, CRMLeadDeliverySchema
       .joined_label). Тот же янтарный тон и приём, что у
       .lead-crm-warning-badge — это тоже оговорка к уже доставленной заявке,
       а не сбой и не отдельное состояние маршрута, только своя причина: две
       заявки честно делят один номер сделки. */
    .lead-crm-joined-badge {
      display: inline-flex;
      align-items: center;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(245, 158, 11, 0.45);
      background: rgba(245, 158, 11, 0.14);
      color: var(--accent-amber);
      font-size: 0.72rem;
      font-weight: 600;
      line-height: 1.35;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }

    /* Компактная сводка — сама кнопка открытия модалки: сброс браузерных
       стилей <button>, чтобы выглядела ровно тем же бейджем, что и <span> в
       полном режиме (тот же приём, что у .lead-reengage-status-badge). */
    button.lead-crm-route.lead-crm-route-clickable {
      font-family: inherit;
      border: 1px solid rgba(16, 185, 129, 0.4);
      cursor: pointer;
      transition: filter 0.2s ease, transform 0.2s ease;
    }
    button.lead-crm-route.lead-crm-route-clickable:hover {
      filter: brightness(1.15);
      transform: translateY(-1px);
    }
    button.lead-crm-route.lead-crm-route-clickable:active { transform: translateY(0); }
    /* Воронка / стадия / ответственный — подпись мелко и приглушённо,
       значение крупнее и весомее, чтобы взгляд сразу находил нужное поле. */
    .lead-crm-route-details {
      margin: 0;
      display: flex;
      flex-direction: column;
      gap: 3px;
    }
    .lead-crm-route-item {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      gap: 6px;
      margin: 0;
    }
    .lead-crm-route-item dt {
      flex: 0 0 auto;
      min-width: 92px;
      font-size: 0.68rem;
      font-weight: 600;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.02em;
    }
    .lead-crm-route-item dd {
      margin: 0;
      font-size: 0.78rem;
      font-weight: 600;
      color: var(--text-primary);
      white-space: normal;
      overflow-wrap: anywhere;
    }
    /* «Маршрут не сохранён» — не тревога и не успех, нейтральный серый тон:
       заявка доставлена, просто карточка сделки для неё не записана.
       Скругление и отступы переопределены на токен кнопок (см. .lead-hot-badge
       выше) — та же просьба владельца, «убрать круги» рядом с прямоугольными
       кнопками строки. */
    .lead-crm-route-unknown {
      border-color: var(--card-border);
      background: var(--surface-tint);
      color: var(--text-muted);
      border-radius: var(--radius-sm);
      padding: 4px 10px;
    }
    .lead-crm-route-block-unknown {
      flex-direction: row;
      flex-wrap: wrap;
      align-items: center;
      gap: 8px;
    }
    .lead-crm-deal-link {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      padding: 2px 10px;
      font-size: 0.72rem;
      line-height: 1.3;
    }
    /* Номер сделки во внешней CRM рядом с «Маршрут не сохранён»: моноширинный,
       чтобы цифры читались и переносились по символу — его копируют руками в
       поиск CRM. Тон нейтральный: это идентификатор, а не состояние. */
    .lead-crm-deal-number {
      display: inline-flex;
      align-items: center;
      padding: 4px 10px;
      border: 1px solid var(--card-border);
      border-radius: var(--radius-sm);
      background: var(--surface-tint);
      font-family: var(--font-mono);
      font-size: 0.72rem;
      line-height: 1.3;
      color: var(--text-body);
      user-select: all;
      overflow-wrap: anywhere;
    }
    @media (max-width: 480px) {
      .lead-crm-route { font-size: 0.7rem; }
      .lead-crm-deal-link { font-size: 0.7rem; }
      .lead-crm-route-item dt { min-width: 78px; font-size: 0.66rem; }
      .lead-crm-route-item dd { font-size: 0.74rem; }
    }

    /* Где сделка ЛЕЖИТ СЕЙЧАС по данным обратной синхронизации с amoCRM —
       АНТИПОД блока выше, не его продолжение. `.lead-crm-route` (пилюля
       «Ушла в amoCRM») красит изумрудом «мы доставили», эта — индиго
       «застройщик сделал со сделкой ТАМ». Разный цвет и разная иконка
       (циклическая стрелка синхронизации, не ссылка) нужны, чтобы два блока
       не читались одним и тем же фактом даже при беглом взгляде (задача
       владельца, 03.09.2026: «не смешать наш маршрут с тем, где сделка
       лежит сейчас»). Отступ чуть меньше — блок обычно идёт сразу под
       маршрутом, а не как отдельная невязанная строка. */
    .lead-crm-external-row { margin-top: 4px; }
    /* Модалка полного маршрута (#crm-route-modal-body): маршрут и текущее
       состояние в amoCRM — два самостоятельных раздела в столбик с общим
       зазором, а не слипшиеся <dl> впритык друг к другу. */
    .lead-crm-route-modal-sections {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    /* Скругление и отступы на токене кнопок (см. .lead-hot-badge выше) — тот
       же селектор используют и «Сделки» (кнопка-тумблер .lead-crm-external-
       toggle), и «Диалоги» (кликабельный вариант .lead-crm-route-clickable),
       поэтому оба раздела получают одинаковую форму одной правкой.
       flex-wrap: wrap — правка от владельца по бою (сессия 10.09.2026,
       четвёртый круг): этот статус несёт до четырёх дочерних элементов
       (иконка, «Сейчас в CRM», «· обновлено …», каретка `.lead-crm-external-
       toggle-caret`), а .lead-crm-route выше по каскаду задаёт
       `display: inline-flex` без wrap — четыре строки в один нерастяжимый
       ряд не помещались в узкой колонке и вылезали за карточку. wrap отдаёт
       лишнему содержимому вторую строку ВНУТРИ той же пилюли, а не ломает
       раскладку соседних элементов (.lead-hot-badge — эталон скругления и
       центровки — не трогаем, эту находку он не затрагивает: у него всего
       два ребёнка, иконка и текст, и им ломаться было нечем). */
    .lead-crm-external-status {
      border-color: rgba(99, 102, 241, 0.4);
      background: rgba(99, 102, 241, 0.12);
      color: var(--accent-indigo-soft);
      border-radius: var(--radius-sm);
      padding: 4px 10px;
      flex-wrap: wrap;
      row-gap: 2px;
    }
    [data-theme="light"] .lead-crm-external-status { color: var(--accent-indigo); }
    /* Кликабельный вариант «Сейчас в CRM» (таблица диалогов, когда своего
       маршрута ещё нет вовсе): `button.lead-crm-route.lead-crm-route-clickable`
       задаёт зелёную рамку маршрута через shorthand `border` — специфичность у
       него выше, чем у одноклассового `.lead-crm-external-status`, поэтому
       индиго без явного переопределения тут перебивалось бы зелёным. */
    button.lead-crm-route.lead-crm-route-clickable.lead-crm-external-status {
      border-color: rgba(99, 102, 241, 0.4);
    }
    /* Время обновления — приглушённым продолжением той же пилюли, а не
       отдельной подписью: это уточнение, а не второй факт. white-space: nowrap
       — при переносе на вторую строку (см. flex-wrap у .lead-crm-external-status
       выше) дата с временем не должна рваться сама посередине. */
    .lead-crm-external-updated {
      font-weight: 500;
      opacity: 0.85;
      white-space: nowrap;
    }
    /* Обратная синхронизация Bitrix24/Домопланера (RES-10): оговорка к снимку —
       отказ CRM, нет права на имена, сеть. Янтарь, как остальные «посмотри»
       состояния кабинета: это не ошибка нашей системы, а ограничение чтения. */
    .lead-crm-external-warn-mark {
      font-weight: 600;
      color: var(--accent-amber);
      white-space: nowrap;
    }
    [data-theme="light"] .lead-crm-external-warn-mark { color: var(--accent-amber-deep); }
    .lead-crm-external-warning dd { color: var(--accent-amber); }
    [data-theme="light"] .lead-crm-external-warning dd { color: var(--accent-amber-deep); }
    .lead-crm-external-hint { color: var(--text-muted); font-size: 0.92em; }
    /* Самостоятельная пилюля в таблице диалогов (leadCrmExternalStateCompactHtml)
       часто идёт следом за другой плашкой состояния доставки в той же ячейке —
       без отступа они слипались бы в одну строку без зазора. */
    .lead-crm-external-block-compact { margin-top: 4px; }
    @media (max-width: 480px) {
      .lead-crm-external-status { font-size: 0.7rem; }
    }

    /* Разворот «Сейчас в CRM» на месте (владелец, сессия 03.09.2026: «данные
       должны разворачиваться прямо на месте, в свёрнутом виде их не видно»).
       Статус — сама кнопка (сброс браузерных стилей <button>, тот же приём,
       что у .lead-crm-route-clickable), <dl> скрыт до .is-expanded. */
    button.lead-crm-route.lead-crm-external-status.lead-crm-external-toggle {
      font-family: inherit;
      cursor: pointer;
      transition: filter 0.2s ease, transform 0.2s ease;
    }
    button.lead-crm-route.lead-crm-external-status.lead-crm-external-toggle:hover {
      filter: brightness(1.15);
      transform: translateY(-1px);
    }
    button.lead-crm-route.lead-crm-external-status.lead-crm-external-toggle:active { transform: translateY(0); }
    .lead-crm-external-toggle-caret {
      display: inline-flex;
      margin-left: 4px;
      vertical-align: -1px;
      transition: transform 0.2s ease;
    }
    .lead-crm-external-toggle[aria-expanded="true"] .lead-crm-external-toggle-caret { transform: rotate(180deg); }
    /* Общее правило раскрытия для обоих кликабельных статусов маршрута —
       «Сейчас в CRM» (`.lead-crm-external-block`) и «Ушла в amoCRM»
       (`leadCrmRouteHtml`, `opts.collapsible`): оба оборачиваются в общий
       `.lead-crm-route-block.is-collapsible`, поэтому селектор общий, без
       завязки на класс-модификатор конкретного варианта (владелец, сессия
       04.09.2026). */
    .lead-crm-route-block.is-collapsible > .lead-crm-route-details { display: none; }
    .lead-crm-route-block.is-collapsible.is-expanded > .lead-crm-route-details {
      display: flex;
      animation: fadeInUp 0.25s ease;
    }
    /* Кнопка-статус «Ушла в amoCRM» в раскрываемом режиме — тот же сброс
       браузерных стилей <button> и hover/active, что у .lead-crm-route-clickable
       и .lead-crm-external-toggle, плюс своя каретка. */
    button.lead-crm-route.lead-crm-route-toggle {
      font-family: inherit;
      cursor: pointer;
      transition: filter 0.2s ease, transform 0.2s ease;
    }
    button.lead-crm-route.lead-crm-route-toggle:hover {
      filter: brightness(1.15);
      transform: translateY(-1px);
    }
    button.lead-crm-route.lead-crm-route-toggle:active { transform: translateY(0); }
    .lead-crm-route-toggle-caret {
      display: inline-flex;
      margin-left: 4px;
      vertical-align: -1px;
      transition: transform 0.2s ease;
    }
    .lead-crm-route-toggle[aria-expanded="true"] .lead-crm-route-toggle-caret { transform: rotate(180deg); }

    /* ======================================================================
       Показ: заявка покупателя на просмотр квартиры
       ----------------------------------------------------------------------
       Хвост I (22.09.2026): подтвердить показ раньше можно было только
       кнопкой в пульте MAX — здесь то же действие из кабинета. Плашка в общем
       ряду .lead-badges (открытый визит) — циан, та же ветка, что у
       .lead-hold-badge («ждём», не тревога и не успех). Полный блок ниже
       собран по тому же приёму «подпись → значение», что и .lead-crm-route-
       block: статус пилюлей, кнопка «Подтвердить показ» разворачивает
       маленькую форму на месте, без модалки и перехода на другую вкладку.
       ====================================================================== */
    .lead-visit-badge {
      display: inline-flex;
      align-items: center;
      margin-left: 6px;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(6, 182, 212, 0.45);
      background: rgba(6, 182, 212, 0.14);
      color: var(--accent-cyan);
      font-size: 0.68rem;
      font-weight: 600;
      line-height: 1.3;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    .lead-badges > .lead-visit-badge { margin-left: 0; }

    .lead-visit-row { margin-top: 6px; }
    .lead-visit-block {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 6px;
    }
    /* Статус — та же геометрия пилюли, что у .lead-crm-route/.lead-hold-badge
       (скругление и центровка иконки — общий приём во всём блоке сделок), цвет
       меняется по состоянию визита. */
    .lead-visit-status {
      display: inline-flex;
      align-items: center;
      max-width: 100%;
      padding: 4px 10px;
      border-radius: var(--radius-sm);
      font-size: 0.72rem;
      font-weight: 600;
      line-height: 1.35;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    .lead-visit-status.lead-visit-open {
      border: 1px solid rgba(6, 182, 212, 0.45);
      background: rgba(6, 182, 212, 0.14);
      color: var(--accent-cyan);
    }
    .lead-visit-status.lead-visit-confirmed {
      border: 1px solid rgba(16, 185, 129, 0.4);
      background: rgba(16, 185, 129, 0.12);
      color: var(--accent-emerald-soft);
    }
    [data-theme="light"] .lead-visit-status.lead-visit-confirmed { color: var(--accent-green); }
    .lead-visit-status.lead-visit-cancelled {
      border: 1px solid var(--glass-border);
      background: var(--surface-tint);
      color: var(--text-muted);
    }
    @media (max-width: 480px) { .lead-visit-status { font-size: 0.7rem; } }

    /* Кнопка «Подтвердить показ» — вторичная кнопка строки, своя ветка циана,
       тем же приёмом, что у .take-hot-lead-btn на своей (розовой) ветке. */
    .lead-visit-confirm-toggle {
      border-color: rgba(6, 182, 212, 0.5) !important;
      background: rgba(6, 182, 212, 0.12) !important;
      color: var(--accent-cyan) !important;
      transition: all 0.2s ease;
    }
    .lead-visit-confirm-toggle:hover:not(:disabled) {
      background: rgba(6, 182, 212, 0.22) !important;
      border-color: var(--accent-cyan) !important;
      transform: translateY(-1px);
    }
    .lead-visit-confirm-toggle:disabled { opacity: 0.6; cursor: progress; }

    /* Форма даты/часа — свёрнута по умолчанию, раскрывается на месте по
       клику на кнопку выше (toggleLeadVisitConfirmForm), тем же приёмом
       is-expanded, что у .lead-crm-route-block.is-collapsible. */
    .lead-visit-confirm-form { display: none; width: 100%; }
    .lead-visit-confirm-form.is-expanded {
      display: flex;
      flex-direction: column;
      gap: 8px;
      width: 100%;
      max-width: 360px;
      padding: 10px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--surface-tint);
      animation: fadeInUp 0.25s ease;
    }
    .lead-visit-confirm-fields {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 6px;
    }
    .lead-visit-confirm-fields .lead-visit-date-input {
      flex: 1 1 140px;
      min-width: 0;
      padding: 6px 8px;
      font-size: 0.8rem;
    }
    .lead-visit-confirm-fields .lead-visit-hour-select {
      flex: 0 0 auto;
      padding: 6px 8px;
      font-size: 0.8rem;
    }
    .lead-visit-confirm-fields .lead-visit-submit-btn {
      flex: 1 1 auto;
      padding: 6px 12px;
      font-size: 0.8rem;
    }
    @media (max-width: 480px) {
      .lead-visit-confirm-form.is-expanded { max-width: 100%; }
      .lead-visit-confirm-fields .lead-visit-date-input,
      .lead-visit-confirm-fields .lead-visit-hour-select,
      .lead-visit-confirm-fields .lead-visit-submit-btn { flex: 1 1 100%; }
    }
    /* Свободные часы/дни от сервера (слот занят или прошёл) — компактными
       кнопками-чипами под формой; пусто, пока сервер не прислал choices. */
    .lead-visit-choices {
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
    }
    .lead-visit-choices:empty { display: none; }
    .lead-visit-choice-btn {
      padding: 4px 10px;
      font-size: 0.74rem;
      border-color: rgba(6, 182, 212, 0.4) !important;
      color: var(--accent-cyan) !important;
      transition: all 0.2s ease;
    }
    .lead-visit-choice-btn:hover:not(:disabled) {
      background: rgba(6, 182, 212, 0.16) !important;
      transform: translateY(-1px);
    }

    /* ======================================================================
       Хроника сделки: журнал переходов в amoCRM (`amocrm_lead_events`)
       ----------------------------------------------------------------------
       Третий раздел той же модалки (#crm-route-modal), не отдельная вкладка:
       два блока выше — СНИМОК (наш маршрут / текущее состояние), этот —
       ЖУРНАЛ, откуда снимок появился. Заголовок — приглушённая надпись, а не
       цветная пилюля `.lead-crm-route`: это не статус сделки, а подпись
       раздела. Список подгружается асинхронно (loadCrmLeadHistory) уже после
       открытия модалки, поэтому есть отдельные состояния «загружаем» /
       «недоступно» / «событий не было».
       ====================================================================== */
    .lead-crm-history-block {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .lead-crm-history-head {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 0.7rem;
      font-weight: 700;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.02em;
    }
    .lead-crm-history-head .lead-crm-history-icon { color: var(--text-muted); }
    .lead-crm-history-status {
      margin: 0;
      font-size: 0.78rem;
      color: var(--text-muted);
    }
    .lead-crm-history-status-muted { font-style: italic; }
    /* «В этой стадии: N дней» — над списком хроники, из stage_age_seconds /
       stage_changed_at того же ответа ручки истории. Спокойный тон по
       умолчанию: обычная сделка в работе, это не сигнал тревоги. */
    .lead-crm-history-stage-age {
      display: flex;
      align-items: center;
      gap: 6px;
      margin: 0;
      padding: 5px 9px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--surface-tint);
      font-size: 0.78rem;
      color: var(--text-body);
    }
    .lead-crm-history-stage-age .btn-icon { color: var(--text-muted); }
    /* Долго без движения (>= 14 дней, тот же порог, что «давно не трогали»
       у закрытых сделок на бэкенде) — янтарь, как и остальные «нужно
       посмотреть» состояния кабинета (не rose: это не отказ, а сигнал
       присмотреться). */
    /* Полнота истории Bitrix24/Домопланера над хроникой (RES-10). Наблюдения —
       янтарная рамка: «неполные наблюдения» должны читаться оговоркой. */
    .lead-crm-history-coverage {
      display: flex;
      align-items: flex-start;
      gap: 6px;
      margin: 0 0 6px;
      padding: 5px 9px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--surface-tint);
      font-size: 0.76rem;
      color: var(--text-body);
    }
    .lead-crm-history-coverage .btn-icon { color: var(--text-muted); flex-shrink: 0; margin-top: 1px; }
    .lead-crm-history-coverage.is-observed { border-color: rgba(245, 158, 11, 0.45); }
    .lead-crm-history-coverage.is-observed .btn-icon { color: var(--accent-amber); }
    .lead-crm-history-stage-age-stale {
      border-color: rgba(245, 158, 11, 0.45);
      background: rgba(245, 158, 11, 0.12);
      color: var(--accent-amber);
      font-weight: 600;
    }
    .lead-crm-history-stage-age-stale .btn-icon { color: var(--accent-amber); }
    [data-theme="light"] .lead-crm-history-stage-age-stale { color: var(--accent-amber-deep); }
    .lead-crm-history-stage-age-flag {
      margin-left: auto;
      padding: 1px 7px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(245, 158, 11, 0.5);
      background: rgba(245, 158, 11, 0.18);
      font-size: 0.68rem;
      font-weight: 700;
      white-space: nowrap;
    }
    .lead-crm-history-list {
      list-style: none;
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .lead-crm-history-item {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      padding: 6px 8px;
      border-radius: var(--radius-sm);
      border-left: 2px solid var(--glass-border);
      background: var(--surface-tint);
      font-size: 0.78rem;
      line-height: 1.4;
      color: var(--text-main);
    }
    .lead-crm-history-icon {
      flex: 0 0 auto;
      display: inline-flex;
      margin-top: 1px;
      color: var(--accent-cyan);
    }
    .lead-crm-history-text { overflow-wrap: anywhere; }
    /* Тревожное: сделка реально удалена в amoCRM, наша заявка ещё на неё
       ссылается — та же розовая ветка, что у критичных статусов доставки. */
    .lead-crm-history-item-alert {
      border-left-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.14);
      color: var(--accent-rose-soft);
      font-weight: 600;
    }
    .lead-crm-history-item-alert .lead-crm-history-icon { color: var(--accent-rose-soft); }
    [data-theme="light"] .lead-crm-history-item-alert { color: var(--accent-rose); }
    .lead-crm-history-more-btn {
      align-self: flex-start;
      margin-top: 2px;
      padding: 4px 10px;
      font-size: 0.76rem;
    }
    @media (max-width: 480px) {
      .lead-crm-history-item { font-size: 0.74rem; }
      .lead-crm-history-status { font-size: 0.74rem; }
      .lead-crm-history-stage-age { flex-wrap: wrap; font-size: 0.74rem; }
      .lead-crm-history-stage-age-flag { margin-left: 0; }
    }

    /* ======================================================================
       Что бот делал дальше: напоминания уснувшему покупателю
       ----------------------------------------------------------------------
       `crm_delivery` отвечает «куда уехала заявка», этот блок — «звонить
       прямо сейчас или бот сам разбудит клиента». `blocked` — самое заметное
       состояние (диалог с ботом закрыт, писать бесполезно), поэтому у него
       своя плашка в общем ряду, той же розовой ветки, что у горячих лидов.
       Остальное — спокойная строка под маршрутом CRM, деталь на второй план.
       ====================================================================== */
    .lead-reengage-status-badge {
      display: inline-block;
      margin-left: 0;
      padding: 1px 7px;
      border-radius: var(--radius-pill);
      font-size: 0.68rem;
      font-weight: 700;
      line-height: 1.3;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    .lead-reengage-status-blocked {
      border: 1px solid rgba(244, 63, 94, 0.5);
      background: rgba(244, 63, 94, 0.16);
      color: var(--accent-rose-soft);
    }
    [data-theme="light"] .lead-reengage-status-blocked { color: var(--accent-rose); }
    /* Тон по исходу напоминаний (владелец, сессия 03.09.2026: «менять цвет в
       зависимости от исхода»). Изумруд — та же ветка, что у «Ушла в amoCRM»
       (.lead-crm-route) рядом в той же ячейке: все касания доставлены —
       нейтрально-успешный факт, а не тревога. Янтарь — хоть одна попытка не
       дошла: заметно, но не так критично, как розовый «звоните». */
    .lead-reengage-status-ok {
      border: 1px solid rgba(16, 185, 129, 0.4);
      background: rgba(16, 185, 129, 0.12);
      color: var(--accent-emerald-soft);
    }
    [data-theme="light"] .lead-reengage-status-ok { color: var(--accent-green); }
    .lead-reengage-status-warn {
      border: 1px solid rgba(245, 158, 11, 0.4);
      background: rgba(245, 158, 11, 0.12);
      color: var(--accent-amber);
    }
    /* Плашка сама ведёт в переписку/историю: сброс браузерных стилей
       <button>, чтобы кнопка выглядела ровно тем же бейджем (та же плашка,
       что у «Диалог с ботом закрыт — звоните» выше по строке). */
    button.lead-reengage-status-badge.is-clickable {
      font-family: inherit;
      cursor: pointer;
      transition: filter 0.2s ease, transform 0.2s ease;
    }
    button.lead-reengage-status-badge.is-clickable:hover {
      filter: brightness(1.15);
      transform: translateY(-1px);
    }
    button.lead-reengage-status-badge.is-clickable:active { transform: translateY(0); }

    .lead-reengage-summary-row {
      margin-top: 6px;
    }
    /* Подробности одного касания — подпись/значение, не через запятую одной
       строкой: время, статус и причина — разные факты, длинная причина не
       должна слипаться с коротким статусом (по требованию владельца,
       30.08.2026). Теперь живут карточками журнала в модалке
       #reengage-history-modal, а не всегда развёрнутой строкой таблицы. */
    .lead-reengage-summary-details {
      margin: 4px 0 0;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }
    .lead-reengage-summary-item {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      gap: 6px;
      margin: 0;
    }
    .lead-reengage-summary-item dt {
      flex: 0 0 auto;
      min-width: 68px;
      font-size: 0.66rem;
      font-weight: 600;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.02em;
    }
    .lead-reengage-summary-item dd {
      margin: 0;
      font-size: 0.72rem;
      color: var(--text-body);
      white-space: normal;
      overflow-wrap: anywhere;
    }
    /* Журнал напоминаний (#reengage-history-modal-body) — карточка на каждое
       касание, разделённые тонкой границей, а не одним слитным списком. */
    .lead-reengage-journal {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    .lead-reengage-journal-item {
      padding-bottom: 12px;
      border-bottom: 1px solid var(--card-border);
    }
    .lead-reengage-journal-item:last-child {
      padding-bottom: 0;
      border-bottom: none;
    }
    .lead-reengage-journal-item-title {
      font-weight: 600;
      color: var(--text-main);
      font-size: 0.88rem;
    }
    .lead-reengage-journal-item.is-failed .lead-reengage-journal-item-title { color: var(--accent-amber); }
    @media (max-width: 480px) {
      .lead-reengage-status-badge { font-size: 0.66rem; }
    }

    @media (max-width: 768px) {
      .leads-hot-toggle { align-self: stretch; justify-content: center; }
      .leads-hold-toggle { align-self: stretch; justify-content: center; }
    }

    /* Ссылка «Открыть диалог» не сработала: объяснение остаётся на экране, пока
       его не закроют, — в отличие от тоста. */
    .deeplink-notice {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 16px;
      padding: 14px 16px;
      border: 1px solid rgba(244, 63, 94, 0.4);
      border-left: 3px solid var(--accent-rose);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      box-shadow: var(--shadow-card);
      animation: fadeInUp 0.35s ease;
    }
    .deeplink-notice-body { min-width: 0; }
    .deeplink-notice-title {
      font-family: var(--font-display);
      font-weight: 700;
      font-size: 0.95rem;
      color: var(--accent-rose-soft);
      margin-bottom: 4px;
    }
    .deeplink-notice-text {
      font-size: 0.86rem;
      color: var(--text-body);
      line-height: 1.5;
    }
    .deeplink-notice-id {
      margin-top: 6px;
      font-size: 0.76rem;
      color: var(--text-muted);
      word-break: break-all;
    }
    .deeplink-notice-id code { font-family: var(--font-mono); }
    [data-theme="light"] .deeplink-notice-title { color: var(--accent-rose); }
    @media (max-width: 480px) {
      .deeplink-notice { padding: 12px; gap: 8px; }
      .deeplink-notice-text { font-size: 0.82rem; }
    }

    /* Похоже, покупатель просил больше не писать. Ветка rose, а не янтарь задач:
       одно из двух решений необратимо, и плашка не должна читаться как рядовой
       доответ. Геометрия — общая с .lead-approval-badge. */
    .lead-reengage-badge {
      display: inline-block;
      margin-left: 6px;
      padding: 1px 7px;
      border-radius: var(--radius-pill);
      border: 1px solid rgba(244, 63, 94, 0.45);
      background: rgba(244, 63, 94, 0.14);
      color: var(--accent-rose-soft);
      font-size: 0.68rem;
      font-weight: 700;
      vertical-align: middle;
      line-height: 1.3;
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }
    /* Пастельный rose задуман для обсидиана: на жемчужном фоне берём базовый. */
    [data-theme="light"] .lead-reengage-badge { color: var(--accent-rose); }
    /* Решение уже принято: это факт, а не задача — гасим до второстепенного. */
    .lead-reengage-badge-off {
      border-color: var(--card-border);
      background: rgba(128, 142, 163, 0.16);
      color: var(--text-muted);
    }
    [data-theme="light"] .lead-reengage-badge-off { color: var(--text-muted); }

    /* Легаси-черновик пре-модерации: приглушённая плашка, это архивный хвост. */
    .lead-approval-badge-legacy {
      border-color: var(--card-border);
      background: rgba(128, 142, 163, 0.16);
      color: var(--text-muted);
    }

    /* === Задачи менеджера по диалогу (окно диалога) ========================= */
    .approval-queue {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin: 10px 0;
    }
    .approval-queue-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      font-size: 0.85rem;
      font-weight: 700;
      color: var(--accent-amber);
    }
    .approval-queue-count {
      font-size: 0.74rem;
      font-weight: 600;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .approval-queue-state {
      padding: 12px;
      text-align: center;
      font-size: 0.82rem;
      color: var(--text-muted);
    }
    /* Черновик, а не отправленное сообщение: пунктирная янтарная рамка. */
    .approval-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 12px;
      border: 1px dashed rgba(245, 158, 11, 0.55);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-md);
      background: rgba(245, 158, 11, 0.08);
      transition: opacity 0.2s ease, box-shadow 0.2s ease;
    }
    [data-theme="light"] .approval-card { background: rgba(245, 158, 11, 0.12); }
    .approval-card.is-busy { opacity: 0.55; }
    .approval-card-meta {
      font-size: 0.74rem;
      font-weight: 600;
      color: var(--text-muted);
    }
    .approval-card-text {
      font-size: 0.9rem;
      line-height: 1.5;
      color: var(--text-body);
      white-space: pre-wrap;
      word-break: break-word;
    }
    /* Доответ по документам: текст клиенту УЖЕ ушёл, поэтому не пунктир черновика,
       а сплошная индиговая рамка — визуально это отправленное сообщение. */
    .approval-card-followup {
      border-style: solid;
      border-color: rgba(99, 102, 241, 0.35);
      border-left: 3px solid var(--accent-indigo);
      background: rgba(99, 102, 241, 0.09);
    }
    [data-theme="light"] .approval-card-followup { background: rgba(99, 102, 241, 0.1); }
    /* Архивный хвост пре-модерации: приглушаем, чтобы не спорил с живой задачей. */
    .approval-card-legacy {
      border-color: rgba(128, 142, 163, 0.45);
      border-left-color: var(--text-muted);
      background: rgba(128, 142, 163, 0.08);
    }
    [data-theme="light"] .approval-card-legacy { background: rgba(128, 142, 163, 0.12); }
    /* Подпись над текстом: чей это текст и видел ли его клиент. */
    .approval-card-label {
      font-size: 0.74rem;
      font-weight: 700;
      letter-spacing: 0.01em;
      color: var(--accent-emerald-soft);
    }
    .approval-card-label-legacy { color: var(--text-muted); }
    .approval-card-hint {
      font-size: 0.76rem;
      line-height: 1.45;
      color: var(--text-muted);
    }
    .approval-queue-head-legacy { color: var(--text-muted); }
    .approval-card-editor { min-height: 92px; }
    .approval-card-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }
    /* Атрибут hidden проигрывает авторскому display:flex у .approval-card-actions —
       без этого правила скрытый ряд кнопок остался бы на экране. */
    .approval-card [hidden] { display: none !important; }
    .approval-btn {
      font-size: 0.8rem;
      padding: 5px 12px;
    }
    .approval-btn:disabled { cursor: progress; }
    .approval-btn-reject { color: var(--accent-rose); }
    .approval-btn-reject:hover { border-color: var(--accent-rose); }

    /* === Решение по напоминаниям: «больше не писать?» ======================== */
    /* Карточка стоит над очередью задач и держит ветку rose: пока решения нет,
       авто-касания диалога остановлены, а одна из кнопок необратима. */
    .reengage-decision {
      display: flex;
      flex-direction: column;
      gap: 8px;
      margin: 10px 0;
      padding: 12px 14px;
      border: 1px solid rgba(244, 63, 94, 0.4);
      border-left: 3px solid var(--accent-rose);
      border-radius: var(--radius-md);
      background: rgba(244, 63, 94, 0.09);
      transition: opacity 0.2s ease, box-shadow 0.2s ease;
      animation: fadeInUp 0.35s ease;
    }
    [data-theme="light"] .reengage-decision { background: rgba(244, 63, 94, 0.1); }
    .reengage-decision.is-busy { opacity: 0.55; }
    .reengage-decision-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .reengage-decision-title {
      font-family: var(--font-display);
      font-size: 0.9rem;
      font-weight: 700;
      color: var(--accent-rose-soft);
    }
    [data-theme="light"] .reengage-decision-title { color: var(--accent-rose); }
    .reengage-decision-when {
      font-size: 0.74rem;
      font-weight: 600;
      color: var(--text-muted);
      white-space: nowrap;
    }
    /* Пауза уже действует — это первая строка карточки и единственная, набранная
       акцентом: без неё менеджер решает второпях, думая, что бот вот-вот напишет. */
    .reengage-decision-pause {
      padding: 7px 10px;
      border-radius: var(--radius-sm);
      background: rgba(245, 158, 11, 0.14);
      border: 1px solid rgba(245, 158, 11, 0.35);
      font-size: 0.8rem;
      font-weight: 600;
      line-height: 1.45;
      color: var(--accent-amber);
    }
    [data-theme="light"] .reengage-decision-pause { color: var(--accent-amber-deep); }
    .reengage-decision-label {
      font-size: 0.74rem;
      font-weight: 700;
      color: var(--text-muted);
    }
    /* Сама фраза покупателя: решение без неё принимается вслепую. */
    .reengage-decision-quote {
      margin: 0;
      padding: 8px 12px;
      border-left: 2px solid var(--accent-rose);
      border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
      background: var(--card-bg);
      font-size: 0.9rem;
      line-height: 1.5;
      color: var(--text-body);
      white-space: pre-wrap;
      word-break: break-word;
    }
    .reengage-decision-noquote {
      font-size: 0.82rem;
      line-height: 1.45;
      color: var(--accent-amber);
    }
    [data-theme="light"] .reengage-decision-noquote { color: var(--accent-amber-deep); }
    .reengage-decision-hint {
      font-size: 0.76rem;
      line-height: 1.45;
      color: var(--text-muted);
    }
    .reengage-decision-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }
    .reengage-btn {
      font-size: 0.8rem;
      padding: 5px 12px;
    }
    .reengage-btn:disabled { cursor: progress; }

    @media (max-width: 768px) {
      .leads-demo-toggle { align-self: stretch; justify-content: center; }
      .approval-card-actions { flex-direction: column; align-items: stretch; }
      .approval-card-actions .approval-btn { width: 100%; justify-content: center; }
      .approval-queue-head { flex-direction: column; align-items: flex-start; gap: 2px; }
      .reengage-decision-head { flex-direction: column; align-items: flex-start; gap: 2px; }
      .reengage-decision-actions { flex-direction: column; align-items: stretch; }
      .reengage-decision-actions .reengage-btn { width: 100%; justify-content: center; }
    }
    @media (max-width: 480px) {
      .reengage-decision { padding: 11px 12px; }
      .reengage-decision-quote { padding: 8px 10px; font-size: 0.86rem; }
      .reengage-decision-title { font-size: 0.86rem; }
    }

    /* ===== Аккаунты операторов (блок #profile-users-section) =====
       Таблица обязана помещаться в ширину вкладки: горизонтальный скролл
       обрезал колонку действий, и «Сбросить пароль» читалось до середины.
       Ширину освободили двумя решениями, оба в разметке и в renderUsers():
       подписи кнопок свёрнуты в иконки с title/aria-label, а колонка
       застройщика показывается только платформе — у сотрудника застройщика во
       всех строках стоял бы один и тот же арендатор.
       Проценты в сумме дают ровно 100 и min-width — 770px с колонкой
       застройщика, 640px без неё. Под таблицу на 1280px остаётся ≈968px, на
       1440px ≈1128px, поэтому скролла нет ни там, ни там; на узком экране
       таблица по-прежнему листается внутри .table-container, не утаскивая за
       собой страницу. */
    #profile-users-section table { table-layout: fixed; width: 100%; min-width: 770px; }
    #profile-users-section th,
    #profile-users-section td { vertical-align: middle; }
    #profile-users-section th:nth-child(1) { width: 17%; min-width: 110px; }  /* Логин */
    #profile-users-section th:nth-child(2) { width: 25%; min-width: 170px; }  /* Роль */
    #profile-users-section th:nth-child(3) { width: 15%; min-width: 130px; }  /* Застройщик */
    #profile-users-section th:nth-child(4) { width: 11%; min-width: 90px;  }  /* Статус */
    #profile-users-section th:nth-child(5) { width: 16%; min-width: 120px; }  /* Последний вход */
    #profile-users-section th:nth-child(6) { width: 16%; min-width: 150px; }  /* Действия */
    /* Выравнивание ячеек ТЕЛА — по классу, а не по позиции: колонка застройщика
       у сотрудника застройщика не рендерится вовсе, и позиционные правила
       съезжали на соседей — «Статус» уходил влево под центрированным
       заголовком. Заголовки остаются на nth-child: в разметке они все шесть
       всегда, скрывается только один. */
    #profile-users-section th:nth-child(4),
    #profile-users-section th:nth-child(5) { text-align: center; }
    #profile-users-section th:nth-child(6) { text-align: right; }
    #profile-users-section td.users-cell-center { text-align: center; }
    #profile-users-section td.users-cell-actions { text-align: right; }
    #profile-users-section td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    /* Служебная строка на всю ширину («Аккаунтов пока нет», текст ошибки) —
       единственная, которой перенос нужен: обрезать её многоточием нельзя. */
    #profile-users-section td[colspan] { white-space: normal; text-overflow: clip; }
    #profile-users-section td .stat-badge {
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    /* Бейдж роли и селектор смены — одной колонкой во всю ширину ячейки:
       в строку они не помещались, и селектор схлопывался до многоточия. */
    .user-role-cell {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 6px;
      min-width: 0;
    }
    .user-role-cell .form-select {
      width: 100%;
      font-size: 0.8rem;
      padding: 4px 28px 4px 8px;
      text-overflow: ellipsis;
    }
    /* Кнопки строки — ряд иконок, прижатый вправо, с переносом: если ячейка
       сузилась, четвёртая кнопка уходит на вторую строку внутри своей ячейки,
       а не вытекает поверх соседних столбцов. */
    #profile-users-section .users-actions-cell {
      justify-content: flex-end;
      flex-wrap: wrap;
      gap: 6px;
    }
    #profile-users-section .users-actions-cell .btn {
      white-space: nowrap;
      font-size: 0.95rem;
      line-height: 1;
      padding: 7px 9px !important;
    }

    @media (max-width: 768px) {
      /* Таблица не сжимается ниже min-width — её листают внутри
         .table-container, страница по горизонтали остаётся на месте. */
      #profile-users-section th,
      #profile-users-section td { padding: 10px; }
    }

    /* ===== Динамическая диагностика системы (модалка #tests-modal) ===== */
    .modal-card.diagnostic-modal { max-width: 780px; }

    .diag-report { display: flex; flex-direction: column; gap: 16px; }

    /* Сводка — одна спокойная строка: слева число и подпись, справа факты,
       под ними волосяная шкала и пояснение. Крупный обведённый счётчик рядом
       с обведёнными пилюлями создавал нагромождение рамок в верхней части. */
    .diag-summary {
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 14px 16px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--surface-tint);
    }
    [data-theme="light"] .diag-summary { background: rgba(241, 245, 249, 0.7); }

    .diag-summary-head {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 8px 16px;
    }

    .diag-score {
      display: flex;
      align-items: baseline;
      gap: 8px;
      min-width: 0;
    }
    .diag-score-value {
      font-family: var(--font-display);
      font-size: 1.5rem;
      font-weight: 700;
      line-height: 1.1;
      letter-spacing: -0.01em;
      color: var(--accent-emerald);
    }
    .diag-score.is-warning .diag-score-value { color: var(--accent-amber); }
    .diag-score.is-error .diag-score-value { color: var(--accent-rose); }
    .diag-score-label {
      font-size: 0.78rem;
      color: var(--text-muted);
      font-weight: 500;
    }

    /* Волосяная шкала того же процента: цвет наследуется от статуса сводки. */
    .diag-score-track {
      height: 3px;
      border-radius: var(--radius-pill);
      background: rgba(148, 163, 184, 0.22);
      overflow: hidden;
    }
    .diag-score-fill {
      display: block;
      height: 100%;
      border-radius: var(--radius-pill);
      background: var(--accent-emerald);
      transition: width 0.5s ease;
    }
    .diag-summary.is-warning .diag-score-fill { background: var(--accent-amber); }
    .diag-summary.is-error .diag-score-fill { background: var(--accent-rose); }

    .diag-summary-body { min-width: 0; }
    .diag-summary-text {
      font-size: 0.85rem;
      line-height: 1.45;
      color: var(--text-muted);
      overflow-wrap: anywhere;
    }
    .diag-summary-meta {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      gap: 4px 14px;
    }
    /* Без рамок и заливки: это подписи, а не управляющие элементы. */
    .diag-chip {
      display: inline-flex;
      align-items: baseline;
      gap: 5px;
      font-size: 0.78rem;
      font-weight: 500;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .diag-chip b { color: var(--text-body); font-weight: 600; }

    .diag-modules {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
      gap: 12px;
    }
    .diag-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 14px 16px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--text-muted);
      border-radius: var(--radius-md);
      background: var(--surface-tint-mid);
      min-width: 0;
      transition: all 0.2s ease;
      animation: fadeInUp 0.35s ease both;
    }
    [data-theme="light"] .diag-card { background: rgba(241, 245, 249, 0.7); }
    .diag-card:hover {
      transform: translateY(-2px);
      box-shadow: var(--shadow-card);
    }
    .diag-card.is-ok { border-left-color: var(--accent-emerald); }
    .diag-card.is-warning { border-left-color: var(--accent-amber); }
    .diag-card.is-error { border-left-color: var(--accent-rose); }

    .diag-card-head {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
    }
    .diag-card-icon { font-size: 1.1rem; line-height: 1; }
    .diag-card-name {
      flex: 1 1 auto;
      min-width: 0;
      font-family: var(--font-display);
      font-weight: 600;
      font-size: 0.95rem;
      color: var(--text-main);
      overflow-wrap: anywhere;
    }
    .diag-badge {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      padding: 3px 10px;
      border-radius: var(--radius-pill);
      font-size: 0.7rem;
      font-weight: 700;
      letter-spacing: 0.02em;
      white-space: nowrap;
      border: 1px solid transparent;
    }
    .diag-badge.is-ok {
      color: var(--accent-emerald);
      background: rgba(16, 185, 129, 0.12);
      border-color: rgba(16, 185, 129, 0.35);
    }
    .diag-badge.is-warning {
      color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.12);
      border-color: rgba(245, 158, 11, 0.35);
    }
    .diag-badge.is-error {
      color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.12);
      border-color: rgba(244, 63, 94, 0.35);
    }

    .diag-card-details {
      font-size: 0.82rem;
      line-height: 1.45;
      color: var(--text-body);
      overflow-wrap: anywhere;
    }

    .diag-empty {
      padding: 18px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      color: var(--text-muted);
      font-size: 0.88rem;
      text-align: center;
    }
    .diag-state {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 18px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      color: var(--text-muted);
      font-size: 0.9rem;
    }
    .diag-state.is-error { color: var(--accent-rose); border-color: rgba(244, 63, 94, 0.35); }
    .diag-spinner {
      width: 16px;
      height: 16px;
      flex: 0 0 auto;
      border-radius: 50%;
      border: 2px solid var(--glass-border);
      border-top-color: var(--accent-indigo);
      animation: diagSpin 0.8s linear infinite;
    }
    @keyframes diagSpin { to { transform: rotate(360deg); } }
    @keyframes fadeInUp {
      from { opacity: 0; transform: translateY(6px); }
      to { opacity: 1; transform: none; }
    }

    @media (max-width: 768px) {
      .diag-modules { grid-template-columns: 1fr; }
      .diag-summary { gap: 9px; padding: 12px 14px; }
      .diag-score-value { font-size: 1.3rem; }
    }
    @media (max-width: 480px) {
      /* Число и факты в столбик: на телефоне они не умещаются в одну строку. */
      .diag-summary-head { flex-direction: column; align-items: flex-start; gap: 6px; }
      .diag-summary-meta { gap: 4px 12px; }
      .diag-card-name { flex: 1 1 100%; }
    }
    @media (prefers-reduced-motion: reduce) {
      .diag-card { animation-duration: 0.01ms; }
      .diag-spinner { animation-duration: 2s; }
    }

    /* === Активность в CRM (вкладка «Сделки & Память», GET /chat/leads/activity) ===
       Отвечает на вопрос владельца «в CRM у кого есть AMO никто не заходит» —
       заходят и работают, просто мимо бота. Секция — первое, что видно во
       вкладке, поэтому сама карточка спокойнее .stat-card::before (без
       верхней радужной полосы на каждой из шести мини-карточек: полос было бы
       шесть подряд, и главная пара чисел героя тонула бы в рамках). */
    .crm-activity {
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-lg);
      padding: 18px 20px 20px;
      margin-bottom: 20px;
      backdrop-filter: var(--blur-glass);
    }
    .crm-activity .faq-toolbar { margin-bottom: 16px; }

    /* Герой: два больших числа рядом — прямой ответ на «сколько мимо бота
       против наших». Цвет кодирует смысл, как и везде в кабинете: золото —
       ручной ввод (внимание, не авария), индиго-фиолетовый — сделки от ИИ. */
    .crm-activity-hero {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 22px;
      padding: 8px 0 4px;
      flex-wrap: wrap;
    }
    .crm-activity-hero-figure { text-align: center; min-width: 140px; }
    .crm-activity-hero-value {
      font-family: var(--font-display);
      font-size: 2.6rem;
      font-weight: 800;
      line-height: 1.05;
      background: var(--accent-gradient-gold);
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    .crm-activity-hero-figure-bot .crm-activity-hero-value {
      background: var(--accent-gradient-primary);
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    .crm-activity-hero-caption {
      font-size: 0.86rem;
      color: var(--text-muted);
      margin-top: 2px;
    }
    .crm-activity-hero-sep {
      font-family: var(--font-display);
      font-size: 1.6rem;
      color: var(--text-muted);
      opacity: 0.5;
      flex: 0 0 auto;
    }
    .crm-activity-takeaway {
      text-align: center;
      font-size: 1rem;
      color: var(--text-main);
      font-weight: 600;
      max-width: 620px;
      margin: 10px auto 0;
      line-height: 1.4;
    }
    .crm-activity-caveat {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      margin: 14px auto 0;
      max-width: 640px;
      font-size: 0.78rem;
      color: var(--text-muted);
      line-height: 1.4;
      text-align: left;
    }
    .crm-activity-caveat svg { flex: 0 0 auto; margin-top: 1px; }

    /* Мини-метрики: остальные типы событий, без героического кегля — вторая
       по важности строка чисел, не первая. */
    .crm-activity-metrics {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
      gap: 10px;
      margin-top: 18px;
    }
    .crm-activity-metric {
      background: var(--glass-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      padding: 10px 12px;
      text-align: center;
      transition: border-color 0.2s ease, transform 0.2s ease;
    }
    /* «Не измеряется» — не ноль: приглушённая плитка с пунктиром (RES-10). */
    .crm-activity-metric.is-unmeasured { border-style: dashed; opacity: 0.75; }
    .crm-activity-metric.is-unmeasured .crm-activity-metric-value { color: var(--text-muted); }
    .crm-activity-metric:hover { border-color: rgba(99, 102, 241, 0.4); transform: translateY(-1px); }
    .crm-activity-metric-value {
      font-family: var(--font-display);
      font-size: 1.3rem;
      font-weight: 700;
      color: var(--text-main);
    }
    .crm-activity-metric-label {
      font-size: 0.72rem;
      color: var(--text-muted);
      margin-top: 2px;
    }

    /* Разбивка по людям: заголовок + таблица переиспользуют .table-container,
       удаления — badge-rose (тот же токен удаления, что у остальной админки),
       без отдельного «тревожного» варианта — цвет уже говорит достаточно. */
    .crm-activity-people-title {
      margin: 20px 0 10px;
      font-size: 1rem;
      color: var(--text-main);
      display: flex;
      align-items: center;
    }
    .crm-activity-deleted-badge { font-variant-numeric: tabular-nums; }
    .crm-activity-footnote {
      margin-top: 10px;
      font-size: 0.78rem;
      color: var(--text-muted);
      line-height: 1.4;
    }

    /* Система (created_by=0 amoCRM) — отдельная строка вне таблицы людей: она
       не сотрудник, и смешивать её с разбивкой по людям нельзя (ловушка на
       бою — 496 событий из 876 у арендатора 111, «рекордсмен» без лица). */
    .crm-activity-system {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin-top: 14px;
      padding: 10px 12px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      font-size: 0.82rem;
      color: var(--text-muted);
      line-height: 1.4;
    }
    .crm-activity-system .stat-badge { flex: 0 0 auto; margin-top: 1px; }

    /* Плашка «срез применён» — предупреждает, что цифры ниже отфильтрованы
       по источнику сделки, кроме героической пары сверху (она всегда полная,
       это точка сравнения — задача владельца «разводить бот/менеджеров»). */
    .crm-activity-source-note {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin: 14px auto 0;
      max-width: 640px;
      padding: 8px 12px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      font-size: 0.78rem;
      color: var(--text-muted);
      line-height: 1.4;
      text-align: left;
    }
    .crm-activity-source-note .stat-badge { flex: 0 0 auto; margin-top: 1px; }

    /* Качество работы по CRM (crm_sla) — единственная объективная оценка
       менеджеров: внутри бота эти сделки не переписываются, SLA-аудит по
       messages для них пуст. Живёт в той же карточке .crm-activity, поэтому
       переиспользует её отступы, а не заводит новую панель. */
    .crm-sla { margin-top: 24px; }
    .crm-sla-headline {
      display: flex;
      align-items: baseline;
      gap: 10px;
      flex-wrap: wrap;
      padding: 14px 16px;
      margin-bottom: 14px;
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--text-muted);
      border-radius: var(--radius-md);
    }
    .crm-sla-headline.is-alert { border-left-color: var(--accent-rose); }
    .crm-sla-headline-value {
      font-family: var(--font-display);
      font-size: 2rem;
      font-weight: 800;
      color: var(--text-main);
      line-height: 1;
    }
    .crm-sla-headline.is-alert .crm-sla-headline-value { color: var(--accent-rose); }
    .crm-sla-headline-label {
      font-size: 0.88rem;
      color: var(--text-body);
      line-height: 1.4;
      max-width: 60ch;
    }

    /* Движение по воронке (CRMLeadStagesBlockSchema.stages) — «двигал по
       стадиям, а по каким?». Живёт под .crm-sla в той же карточке
       .crm-activity, поэтому переиспользует её отступ, а не заводит панель.
       Полоса доли в колонке «Сейчас» — те же .funnel-bar-track/.funnel-bar-fill,
       что и у воронки виджета выше на дашборде: приёма для полос в проекте
       ровно один, дублировать его отдельными классами не нужно. */
    .crm-stages { margin-top: 24px; }
    .crm-stages .table-container { margin-top: 4px; }
    .crm-stages-share {
      display: flex;
      flex-direction: column;
      gap: 5px;
      min-width: 72px;
    }
    .crm-stages-share-value {
      font-family: var(--font-display);
      font-weight: 700;
      color: var(--text-main);
      font-variant-numeric: tabular-nums;
    }
    /* Ширина полосы — доля от МАКСИМАЛЬНОЙ стадии (строки уже отсортированы
       по убыванию leads_now), не от суммы: на бою «Закрыто и не реализовано»
       (2618) на порядок больше соседей (171, 58…) — полу-пустой контейнер
       ужимает бар до минимума 2%, а не до нуля, иначе мелкие стадии тонут. */
    .crm-stages-share .funnel-bar-track { width: 100%; }

    /* «Без смены стадии» — подсветка по доле, не по факту наличия: «часть
       зависла» и «зависло почти всё» разные новости (тот же приём, что и
       заметные удаления в разбивке по людям выше). Янтарь — предупреждение,
       розовый — больше половины стадии не меняла этап дольше порога. НЕ
       ПУТАТЬ с «брошенными» (stale_leads) блока выше: там порог по ЛЮБОМУ
       событию сделки, здесь — только по смене стадии, см. пояснение в JS. */
    .crm-stages-stuck-badge {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 30px;
      padding: 2px 9px;
      border-radius: var(--radius-pill);
      font-weight: 700;
      font-variant-numeric: tabular-nums;
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      color: var(--text-body);
    }
    .crm-stages-stuck-badge.is-warn {
      background: color-mix(in srgb, var(--accent-amber) 16%, transparent);
      border-color: color-mix(in srgb, var(--accent-amber) 40%, transparent);
      color: var(--accent-amber);
    }
    .crm-stages-stuck-badge.is-severe {
      background: color-mix(in srgb, var(--accent-rose) 16%, transparent);
      border-color: color-mix(in srgb, var(--accent-rose) 40%, transparent);
      color: var(--accent-rose);
    }
    /* Прочерк без значения — «Застряло: 0» и «отрезков не набралось» в
       колонке среднего времени в стадии; сам прочерк остаётся приглушённым
       текстом в обоих местах. */
    .crm-stages-dash { color: var(--text-muted); }

    /* Самые частые переходы за период — компактный список, не таблица:
       «откуда → куда · сколько» читается строкой. */
    .crm-stages-transitions { margin-top: 16px; }
    .crm-stages-transitions-title {
      margin: 0 0 8px;
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .crm-stages-transitions-list {
      list-style: none;
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .crm-stages-transition-row {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
      padding: 8px 12px;
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      font-size: 0.84rem;
    }
    .crm-stages-transition-path { color: var(--text-body); }
    .crm-stages-transition-arrow { color: var(--text-muted); margin: 0 2px; }
    .crm-stages-transition-cross { flex: 0 0 auto; }
    .crm-stages-transition-count {
      margin-left: auto;
      font-family: var(--font-display);
      font-weight: 700;
      color: var(--text-main);
      font-variant-numeric: tabular-nums;
    }
    .crm-stages-transition-count::before { content: '· '; color: var(--text-muted); font-weight: 400; }

    .crm-stages-notes { margin-top: 14px; display: flex; flex-direction: column; gap: 6px; }

    @media (max-width: 768px) {
      .crm-activity-hero { gap: 14px; }
      .crm-activity-hero-value { font-size: 2.1rem; }
      .crm-activity-metrics { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
    }
    @media (max-width: 480px) {
      .crm-activity { padding: 16px 14px 18px; }
      .crm-activity-hero { flex-direction: column; gap: 10px; }
      .crm-activity-hero-sep { display: none; }
      .crm-activity-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .crm-sla-headline { flex-direction: column; gap: 4px; }
      .crm-sla-headline-value { font-size: 1.7rem; }
      .crm-stages-transition-row { flex-direction: column; align-items: flex-start; gap: 4px; }
      .crm-stages-transition-count { margin-left: 0; }
      .crm-stages-transition-count::before { content: ''; }
    }

    /* === Подключение канала мессенджера: два разных секрета в одной форме ===
       Поля секретов визуально отделены рамкой и подписью состояния: наладчик
       должен видеть, что значение уже лежит в базе, хотя сервер его не отдаёт. */
    .channel-state-row {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      align-items: center;
      margin-top: 2px;
    }

    /* Причина, по которой канал не работает: её отдаёт провайдер, и в бейдж она
       не помещается. Строка занимает всю ширину состояния (flex-basis: 100%),
       чтобы не растягивать ряд бейджей. */
    .channel-state-note {
      flex: 1 0 100%;
      margin: 2px 0 0;
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }

    .channel-secrets-note { margin: 4px 0 2px; }
    .channel-verify-na { border-left-color: var(--text-muted); }
    /* Заглушка вместо блока секретов там, где вебхук подключает сама платформа
       (Telegram): тон информационный — это не ограничение, а сделанная работа. */
    .channel-secret-na { margin: 4px 0 2px; }

    .secret-field {
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      padding: 12px;
      background: rgba(148, 163, 184, 0.05);
      transition: border-color 0.2s ease, background 0.2s ease;
    }
    .secret-field:focus-within {
      border-left-color: var(--accent-indigo);
      background: var(--glass-hover);
    }
    /* Активная галка «стереть» — предупреждающий тон: действие разрушительное. */
    .secret-field.is-clearing {
      border-left-color: var(--accent-rose);
    }
    .secret-field.is-clearing .form-input {
      opacity: 0.5;
    }

    .form-hint {
      font-size: 0.78rem;
      color: var(--text-muted);
      line-height: 1.5;
    }
    /* Вложенный блок настроек модалки (параметры субсидии, признак рыночной
       ставки): визуально отделён от основной сетки полей, но не выглядит
       отдельной карточкой. */
    .mortgage-subgroup {
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--surface-tint-weak);
      padding: 12px 14px;
      margin-bottom: 12px;
    }
    @media (max-width: 480px) {
      .mortgage-subgroup > div[style*="grid-template-columns"] {
        grid-template-columns: 1fr !important;
      }
    }

    .form-hint code {
      font-family: var(--font-mono);
      color: var(--accent-cyan);
    }
    .form-hint strong { color: var(--text-body); }

    .secret-clear-toggle {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.8rem;
      font-weight: 500;
      color: var(--text-muted);
      cursor: pointer;
      transition: color 0.2s ease;
    }
    .secret-clear-toggle:hover { color: var(--text-body); }
    .secret-clear-toggle input[type="checkbox"] {
      width: 16px;
      height: 16px;
      flex: 0 0 auto;
      accent-color: var(--accent-rose);
      cursor: pointer;
    }

    /* Переключатель «Свой бот застройщика» / «Бот платформы» — модалка канала
       Telegram/MAX. Радиокнопки в одну строку на десктопе, в столбец на узком
       экране (см. медиа-запрос у .secret-clear-toggle ниже). */
    .bot-mode-toggle {
      display: flex;
      gap: 18px;
      flex-wrap: wrap;
      margin-bottom: 4px;
    }
    .bot-mode-option {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.85rem;
      font-weight: 500;
      color: var(--text-muted);
      cursor: pointer;
      transition: color 0.2s ease;
    }
    .bot-mode-option:hover { color: var(--text-body); }
    .bot-mode-option input[type="radio"] {
      width: 16px;
      height: 16px;
      flex: 0 0 auto;
      accent-color: var(--accent-cyan);
      cursor: pointer;
    }
    .bot-mode-option input[type="radio"]:checked + span {
      color: var(--text-body);
    }
    /* В админской палитре зелёный акцент называется --accent-green. */
    #channel-active { accent-color: var(--accent-green); }

    /* Баннер «конфигурация не загружена»: разрушительный сценарий — тон rose. */
    .channel-load-error {
      margin-bottom: 16px;
      padding: 12px 14px;
      border: 1px solid var(--accent-rose);
      border-left: 3px solid var(--accent-rose);
      border-radius: var(--radius-sm);
      background: rgba(244, 63, 94, 0.1);
      color: var(--text-body);
      font-size: 0.82rem;
      line-height: 1.5;
    }

    /* === «Проверить связь»: кнопка и итог проверки ========================
       Стоит вплотную к состоянию канала — это ответ на тот же вопрос («бот
       принимает сообщения?»), только полученный сейчас, а не при открытии
       окна. Рычаг рядом с тем, чем он управляет. */
    .channel-check {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 8px 12px;
      margin-top: 10px;
    }
    .channel-check-btn {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      flex: 0 0 auto;
    }
    .channel-check-hint {
      flex: 1 1 240px;
      margin: 0;
      min-width: 0;
    }
    /* Занятая кнопка: спиннер вместо иконки, никакого ховера и курсора-руки —
       обращение уходит в api.telegram.org и длится секунды. */
    .channel-check-spinner {
      display: none;
      width: 14px;
      height: 14px;
      flex: 0 0 auto;
      border-radius: 50%;
      border: 2px solid var(--glass-border);
      border-top-color: var(--accent-indigo);
      animation: diagSpin 0.8s linear infinite;
    }
    .channel-check-btn.is-busy .channel-check-spinner { display: inline-block; }
    .channel-check-btn.is-busy .channel-check-icon { display: none; }
    .channel-check-btn:disabled { cursor: default; opacity: 0.75; }
    .channel-check-icon { flex: 0 0 auto; }

    .channel-check-result { margin-top: 10px; }
    /* Карточка итога. Тон — единственное, чем отличаются три исхода визуально:
       зелёный «всё в порядке», янтарный «нашли расхождение и починили»,
       розовый «не получилось». Различать их по тексту наладчик не обязан. */
    .channel-check-card {
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--text-muted);
      border-radius: var(--radius-sm);
      background: rgba(148, 163, 184, 0.06);
      animation: fadeInUp 0.35s ease;
    }
    .channel-check-card.is-ok {
      border-left-color: var(--accent-emerald);
      background: rgba(16, 185, 129, 0.08);
    }
    .channel-check-card.is-fixed {
      border-left-color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.08);
    }
    .channel-check-card.is-error {
      border-left-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.08);
    }
    .channel-check-head { margin-bottom: 8px; }
    .channel-check-text {
      margin: 0;
      font-size: 0.84rem;
      line-height: 1.55;
      color: var(--text-body);
    }
    .channel-check-facts {
      display: grid;
      gap: 6px;
      margin-top: 10px;
    }
    .channel-check-fact {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      gap: 4px 10px;
    }
    .channel-check-fact-label {
      flex: 0 0 auto;
      font-size: 0.74rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: var(--text-muted);
    }
    /* Адрес вебхука длинный и обязан быть виден целиком: обрезанный многоточием
       адрес не даёт сравнить его с тем, что стоит у Telegram. */
    .channel-check-fact-value {
      font-family: var(--font-mono);
      font-size: 0.76rem;
      color: var(--accent-cyan);
      overflow-wrap: anywhere;
      min-width: 0;
    }
    .channel-check-warn {
      margin: 8px 0 0;
      font-size: 0.8rem;
      line-height: 1.5;
      color: var(--accent-gold-soft);
    }
    .channel-check-note {
      margin: 8px 0 0;
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    /* Кнопка повторной проверки живой лампочки канала (channel-health-box) —
       та же карточка .channel-check-card, отдельный только отступ сверху. */
    .channel-health-retry-btn {
      margin-top: 10px;
    }
    /* Пастельное золото задумано под обсидиан: на жемчужном фоне очередь и
       ошибка доставки на нём не читаются — на свету берём глубокий янтарь. */
    [data-theme="light"] .channel-check-warn { color: var(--accent-amber-deep); }

    @media (max-width: 480px) {
      /* На телефоне форма канала идёт одной колонкой во всю ширину, а рамка
         секрета сжимается — иначе поля вылезают за карточку модалки. */
      .secret-field { padding: 10px; }
      .secret-clear-toggle { align-items: flex-start; }
      .channel-state-row { gap: 4px; }
      .channel-load-error { padding: 10px 12px; font-size: 0.78rem; }
      /* Кнопка во всю ширину, подсказка под ней: в строку они не помещаются, и
         кнопка сжималась до нечитаемой. */
      .channel-check { gap: 8px; }
      .channel-check-btn { width: 100%; justify-content: center; }
      .channel-check-hint { flex: 1 1 100%; }
      .channel-check-card { padding: 10px 12px; }
      .channel-check-fact { gap: 2px; }
      .channel-check-fact-label { flex: 1 0 100%; }
    }
    @media (prefers-reduced-motion: reduce) {
      .channel-check-spinner { animation-duration: 2s; }
      .channel-check-card { animation-duration: 0.01ms; }
    }

    /* ══════════════════════════════════════════════════════════════════════
       ЛИМИТЫ АРЕНДАТОРА: чип в шапке и баннер бюджета
       Чип стоит рядом с плашкой пользователя, потому что отвечает на тот же
       вопрос — «в каких границах я сейчас работаю». В шапке остались только
       пакет и доля израсходованного; остаток в диалогах, места операторов и
       документы читают раз в день, и они переехали во вкладку «Профиль».
       Доля набрана моноширинным: она меняется на глазах, и прыгающая ширина
       цифр в пропорциональном шрифте дёргала бы всю шапку.
       ══════════════════════════════════════════════════════════════════════ */
    .limits-widget {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      max-width: 260px;
      /* Чипу разрешено ужиматься: внутри обрезается только название пакета,
         доля израсходованного остаётся целой (см. .limits-plan). */
      min-width: 0;
      padding: 7px 14px;
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-pill);
      backdrop-filter: var(--blur-glass);
      cursor: pointer;
      font: inherit;
      text-align: left;
      transition: all 0.2s ease;
    }
    .limits-widget:hover {
      background: var(--glass-hover);
      border-color: var(--accent-indigo);
      transform: translateY(-1px);
    }
    .limits-widget:active { transform: scale(0.97); }
    .limits-widget:focus-visible {
      outline: 2px solid var(--accent-indigo);
      outline-offset: 2px;
    }
    /* Доля израсходованного — единственная цифра чипа, поэтому она контрастнее
       названия пакета: на неё смотрят, названием только уточняют. */
    .limits-chip-percent {
      font-family: var(--font-mono);
      font-size: 0.8rem;
      font-weight: 700;
      color: var(--text-main);
      white-space: nowrap;
    }
    .limits-widget.is-warning {
      border-color: rgba(245, 158, 11, 0.55);
      background: rgba(245, 158, 11, 0.1);
    }
    .limits-widget.is-warning .limits-chip-percent { color: var(--accent-amber); }
    .limits-widget.is-exhausted {
      border-color: rgba(244, 63, 94, 0.6);
      background: rgba(244, 63, 94, 0.1);
      box-shadow: 0 0 0 1px rgba(244, 63, 94, 0.18);
    }
    .limits-widget.is-exhausted .limits-chip-percent { color: var(--accent-rose); }
    .limits-widget.is-warning:hover,
    .limits-widget.is-exhausted:hover { border-color: currentColor; }
    .limits-plan {
      font-family: var(--font-display);
      font-size: 0.68rem;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--accent-indigo-soft);
      white-space: nowrap;
      /* Название пакета — единственное, что в шапке разрешено обрезать: доля
         израсходованного рядом важнее и остаётся целой. */
      overflow: hidden;
      text-overflow: ellipsis;
      min-width: 0;
    }
    .limits-budget {
      font-family: var(--font-mono);
      font-size: 0.78rem;
      font-weight: 600;
      color: var(--text-main);
      white-space: nowrap;
    }
    .limits-track {
      position: relative;
      height: 4px;
      border-radius: var(--radius-pill);
      background: var(--glass-border);
      overflow: hidden;
    }
    .limits-fill {
      display: block;
      height: 100%;
      border-radius: inherit;
      background: var(--accent-gradient-primary-h);
      transition: width 0.35s ease, background 0.2s ease;
    }
    /* Подробный блок лимитов во вкладке «Профиль». Классы состояния на нём те
       же, что на чипе в шапке (их ставит renderLimitsWidget), поэтому и цвета
       здесь описаны один раз для обоих носителей. */
    .profile-own-limits {
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .profile-own-limits .limits-budget { font-size: 0.92rem; white-space: normal; }
    .profile-own-limits .limits-quotas {
      display: flex;
      flex-wrap: wrap;
      gap: 6px 16px;
    }
    .limits-widget.is-warning .limits-fill,
    .profile-own-limits.is-warning .limits-fill { background: var(--accent-gradient-gold); }
    .limits-widget.is-exhausted .limits-fill,
    .profile-own-limits.is-exhausted .limits-fill { background: var(--accent-rose); }
    /* Остаток в диалогах — главная строка для владельца кабинета:
       рубли ему больше не показываются, а токены сами по себе ничего не значат.
       Поэтому она чуть контрастнее квот и стоит сразу под шкалой. */
    .limits-dialogs {
      display: flex;
      flex-direction: column;
      gap: 2px;
      margin: 6px 0 4px;
      font-size: 0.74rem;
      line-height: 1.35;
      color: var(--text-body);
    }
    /* Оговорка про тренировки — тише главной строки, но рядом с ней: складывать
       числа в уме владелец не должен. */
    .limits-dialogs-note {
      font-size: 0.68rem;
      color: var(--text-muted);
    }
    .limits-widget.is-warning .limits-dialogs,
    .profile-own-limits.is-warning .limits-dialogs { color: var(--accent-amber); }
    .limits-widget.is-exhausted .limits-dialogs,
    .profile-own-limits.is-exhausted .limits-dialogs { color: var(--accent-rose); }
    .limits-seats {
      font-size: 0.72rem;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .limits-docs {
      font-size: 0.72rem;
      color: var(--text-muted);
      white-space: nowrap;
    }
    /* Квота кончилась — это не ошибка, а стоп для найма и загрузки: янтарь, не роза. */
    .limits-seats.is-full,
    .limits-docs.is-full { color: var(--accent-amber); font-weight: 600; }
    /* Раздел не входит в тариф — это факт комплектации, а не тревога:
       приглушённый текст без цвета и без жирного начертания. */
    .limits-seats.is-off,
    .limits-docs.is-off { color: var(--text-muted); font-weight: 400; opacity: 0.85; }

    /* ══════════════════════════════════════════════════════════════════════
       ЗАНЯТОСТЬ ЛИМИТОВ ЗАСТРОЙЩИКА: строки в таблице и плитки в карточке
       Формат везде один — «занято / квота». Приближение к порогу — янтарь,
       исчерпание — роза, но без заливки во всю ячейку: оператор просматривает
       список целиком, и красный фон у каждой второй строки перестаёт значить
       что-либо.
       ══════════════════════════════════════════════════════════════════════ */
    .tenant-usage {
      display: flex;
      flex-direction: column;
      gap: 3px;
      min-width: 168px;
    }
    .usage-row {
      display: flex;
      align-items: baseline;
      /* Метка рядом со значением может быть фразой («действует общее
         ограничение установки»), поэтому строке разрешён перенос между
         элементами — иначе колонка таблицы растягивалась бы на пол-экрана. */
      flex-wrap: wrap;
      gap: 6px;
      font-size: 0.78rem;
      color: var(--text-body);
      white-space: nowrap;
    }
    .usage-label {
      flex: 0 0 auto;
      min-width: 74px;
      font-size: 0.72rem;
      color: var(--text-muted);
    }
    .usage-value {
      font-family: var(--font-mono);
      font-weight: 600;
      color: var(--text-main);
    }
    /* Тонкая черта внутри одной ячейки: сверху границы расхода, снизу занятые
       квоты. Раньше это были три отдельные колонки, и они читались как три
       независимых показателя; черта оставляет их одним ответом, но не даёт
       строкам слипнуться в сплошной столбик цифр. */
    .tenant-usage-split {
      height: 1px;
      margin: 6px 0 5px;
      background: var(--glass-border);
    }
    .usage-flag {
      font-size: 0.66rem;
      text-transform: lowercase;
      color: inherit;
      opacity: 0.9;
      /* Пояснение к состоянию лимита — фраза, а не слово: переносим её по
         словам, а не растягиваем ячейку. */
      white-space: normal;
      line-height: 1.3;
    }
    .usage-row.is-warning .usage-value,
    .usage-row.is-warning .usage-flag { color: var(--accent-amber); }
    .usage-row.is-exhausted .usage-value,
    .usage-row.is-exhausted .usage-flag { color: var(--accent-rose); }
    /* «Не входит в тариф» — нейтральное состояние: моноширинный ряд цифр здесь
       не нужен, а цвет не должен спорить с янтарём и розой. */
    .usage-row.is-off .usage-value {
      font-family: var(--font-body);
      font-weight: 500;
      color: var(--text-muted);
    }
    .usage-row.is-off .usage-flag { color: var(--text-muted); }
    /* Остаток в диалогах под цифрами лимита: мельче строк лимита, потому что
       это пересчёт, а не третья граница. */
    .usage-dialogs {
      font-size: 0.7rem;
      font-weight: 600;
      color: var(--accent-emerald);
      white-space: nowrap;
      cursor: help;
    }

    .tenant-usage-summary {
      margin: 4px 0 16px;
      padding: 12px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
    }
    .tenant-usage-summary-title {
      margin-bottom: 10px;
      font-family: var(--font-display);
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text-main);
    }
    /* Плиток до семи, и блок теперь во всю ширину страницы: жёсткие две
       колонки растягивали бы каждую плитку на пол-экрана. Число колонок
       считает раскладка, минимум — читаемая ширина цифры с подписью. */
    .tenant-usage-summary-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: 10px;
    }
    .usage-tile {
      display: flex;
      flex-direction: column;
      gap: 2px;
      padding: 8px 10px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--glass-border);
      border-radius: var(--radius-sm);
      transition: all 0.2s ease;
    }
    .usage-tile-label {
      font-size: 0.7rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: var(--text-muted);
    }
    /* Display, а не mono: в плитке теперь стоит не голое число, а фраза
       «не больше 8 млн токенов» — моноширинный набор её разбивает. */
    .usage-tile-value {
      font-family: var(--font-display);
      font-size: 0.92rem;
      font-weight: 700;
      line-height: 1.3;
      color: var(--text-main);
    }
    .usage-tile-note {
      font-size: 0.7rem;
      line-height: 1.35;
      color: var(--text-muted);
    }
    .usage-tile.is-warning {
      border-left-color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.08);
    }
    .usage-tile.is-warning .usage-tile-value { color: var(--accent-amber); }
    .usage-tile.is-exhausted {
      border-left-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.1);
    }
    .usage-tile.is-exhausted .usage-tile-value { color: var(--accent-rose); }
    /* Раздел не входит в тариф: плитка гасится, а не подсвечивается. */
    .usage-tile.is-off { opacity: 0.75; }
    .usage-tile.is-off .usage-tile-value {
      font-family: var(--font-body);
      font-size: 0.86rem;
      font-weight: 600;
      color: var(--text-muted);
    }

    /* Редактор лимита расхода: состояние словами слева, число справа.
       На узком экране колонки складываются, чтобы поле не сжималось в пару
       символов и цифра лимита оставалась читаемой. */
    .limit-editor {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      align-items: center;
    }
    /* Скрытое поле числа места не занимает — список состояний тянется на всю
       ширину, и «Без ограничений» не выглядит обрезанным. */
    .limit-editor .form-select { flex: 1 1 220px; margin: 0; }
    .limit-editor .form-input { flex: 1 1 160px; margin: 0; }
    @media (max-width: 640px) {
      .limit-editor .form-select,
      .limit-editor .form-input { flex: 1 1 100%; }
    }

    .budget-banner {
      display: flex;
      align-items: flex-start;
      gap: 12px;
      margin-bottom: 18px;
      padding: 14px 16px;
      border-radius: var(--radius-md);
      border: 1px solid var(--accent-amber);
      border-left: 3px solid var(--accent-amber);
      background: rgba(245, 158, 11, 0.1);
      color: var(--text-body);
      font-size: 0.85rem;
      line-height: 1.5;
      animation: fadeInUp 0.35s ease;
    }
    .budget-banner.is-exhausted {
      border-color: var(--accent-rose);
      border-left-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.12);
    }
    .budget-banner-icon { font-size: 1.05rem; line-height: 1.4; flex: 0 0 auto; }
    .budget-banner-body { display: flex; flex-direction: column; gap: 2px; }
    .budget-banner-body strong { font-family: var(--font-display); color: var(--text-main); }

    /* ══════════════════════════════════════════════════════════════════════
       ТАРИФНЫЕ ПАКЕТЫ в карточке застройщика
       ══════════════════════════════════════════════════════════════════════ */
    .plan-panel-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      margin-top: 4px;
    }
    .plan-panel-title {
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .plan-panel-hint {
      margin: 6px 0 12px;
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    /* Число колонок не фиксируем: пакетов в сетке было три, стало четыре, и
       жёсткий repeat(3) резал бы последнюю карточку на отдельную строку во всю
       ширину. Сетка приходит с сервера — раскладка обязана подстраиваться. */
    .plan-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
      gap: 10px;
    }
    .plan-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 12px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .plan-card:hover {
      transform: translateY(-2px);
      border-color: var(--accent-indigo);
      box-shadow: var(--shadow-card);
    }
    .plan-card.is-active {
      border-color: var(--accent-green);
      background: rgba(16, 185, 129, 0.08);
    }
    /* Пакет, закрывающий работу: тревожная рамка вместо вида «дешёвый тариф». */
    .plan-card.is-blocking {
      border-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.07);
    }
    .plan-card.is-blocking:hover { border-color: var(--accent-rose); }
    .plan-card.is-blocking .plan-card-price { color: var(--accent-rose); }

    /* ── Вкладка «Профиль застройщика»: две колонки ──────────────────────
       Карточка витрины и реквизиты слева, лимиты и тариф справа. Одна колонка
       перестала вмещать содержимое после перевода тарифа на токены: панель из
       четырёх пакетов уезжала за нижний край. На планшете и телефоне колонки
       складываются в одну, порядок остаётся смысловым — сначала «кто мы»,
       затем «что нам положено». */
    .tenant-modal-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 20px;
      align-items: start;
    }
    .tenant-modal-col {
      display: flex;
      flex-direction: column;
      gap: 16px;
      min-width: 0;
    }
    /* Внутри колонок отступы держит flex-gap, поэтому собственные поля групп
       обнуляем — иначе интервалы удваивались бы. */
    .tenant-modal-col .form-group { margin: 0; }
    @media (max-width: 900px) {
      .tenant-modal-grid { grid-template-columns: minmax(0, 1fr); }
    }

    /* Секция профиля — стеклянная карточка того же строя, что и реквизиты.
       Отдельный класс, а не .tenant-brand-section: у реквизитов свои правила
       для режима «только просмотр», и мешать их с остальными блоками нельзя. */
    .profile-section {
      display: flex;
      flex-direction: column;
      gap: 12px;
      padding: 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
    }
    .profile-section-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      flex-wrap: wrap;
    }
    .profile-section-title {
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .profile-section .form-group { margin: 0; }
    /* Блоки во всю ширину ПОД сеткой: аккаунты операторов (таблица из шести
       колонок в половину экрана не помещается ни на одном разрешении) и рычаги
       платформы — лимиты расхода и наложение пакета. Внутри колонок интервалы
       держит flex-gap, поэтому отступ ставим только прямым детям вкладки —
       иначе он удвоился бы. */
    #tab-profile > .profile-section { margin-top: 20px; }
    .profile-subcard {
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      padding: 14px;
      margin-top: 18px;
    }
    /* Снимок конфигурации установки: предпросмотр загрузки — карточки по
       разделам файла. Разложение по колонкам, а не список: разделов до
       десятка, и построчный список растягивал бы блок на весь экран. */
    .config-snapshot-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
      gap: 10px;
    }
    .config-snapshot-card {
      background: var(--bg-deep);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      padding: 10px 12px;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .config-snapshot-card-title {
      font-weight: 600;
      font-size: 0.85rem;
      color: var(--text-main);
    }
    .config-snapshot-row {
      font-size: 0.8rem;
      color: var(--text-muted);
      line-height: 1.4;
      word-break: break-word;
    }
    /* Перезапись — единственная строка, которая реально теряет прежнее
       значение, поэтому у неё единственной тревожный акцент рамкой слева. */
    .config-snapshot-row-overwrite {
      color: var(--text-main);
      border-left: 2px solid var(--accent-rose);
      padding-left: 8px;
    }
    .config-snapshot-row-new {
      color: var(--text-main);
      border-left: 2px solid var(--accent-green);
      padding-left: 8px;
    }
    .config-snapshot-row-warn { color: var(--accent-amber); }
    .config-snapshot-row-muted { color: var(--text-muted); }
    .config-snapshot-actions {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
      margin-top: 4px;
    }
    .config-snapshot-foreign-confirm {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      margin-top: 10px;
      font-size: 0.85rem;
      color: var(--text-main);
      cursor: pointer;
    }
    .config-snapshot-foreign-confirm input { margin-top: 3px; }
    @media (max-width: 480px) {
      .config-snapshot-grid { grid-template-columns: 1fr; }
    }

    /* Системный код и код виджета не растягиваются: их только читают и
       копируют, а ручка изменения размера ломала бы ритм колонки. */
    .profile-copy-row {
      display: flex;
      gap: 8px;
      align-items: center;
      flex-wrap: wrap;
    }
    .profile-copy-row .form-input {
      flex: 1 1 180px;
      font-family: var(--font-mono);
    }
    .profile-copy-row .btn { flex: 0 0 auto; }
    #tenant-embed-snippet {
      font-family: var(--font-mono);
      font-size: 0.78rem;
      resize: none;
    }
    /* Предупреждение «в строке локальный адрес». Янтарь, а не красный: ничего
       не сломано — строка честно собрана из того адреса, по которому открыт
       кабинет, и для проверки на этой машине она годится. Тот же рисунок, что
       у .crm-token-expiry.is-warning, чтобы предупреждения кабинета читались
       как одно семейство. */
    .embed-origin-warning {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin-top: 10px;
      padding: 11px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(245, 158, 11, 0.45);
      background: rgba(245, 158, 11, 0.12);
      color: var(--accent-amber);
      font-size: 0.82rem;
      line-height: 1.5;
      transition: all 0.2s ease;
    }
    .embed-origin-warning-icon { flex-shrink: 0; line-height: 1; margin-top: 1px; }
    .embed-origin-warning-text { min-width: 0; overflow-wrap: anywhere; }
    .embed-origin-warning-text b { font-weight: 700; }
    .embed-origin-warning-text code {
      font-family: var(--font-mono);
      font-size: 0.78rem;
      overflow-wrap: anywhere;
    }
    @media (max-width: 480px) {
      .embed-origin-warning { padding: 10px 11px; font-size: 0.78rem; }
    }

    /* Адреса сайтов, где стоит виджет: поле ввода + список уже добавленных
       адресов-чипов. Тот же рисунок, что у .keywords-list/.keyword-chip выше,
       но со своей кнопкой удаления в каждом чипе. */
    .widget-origin-add-row {
      display: flex;
      gap: 8px;
      margin-top: 8px;
      flex-wrap: wrap;
    }
    .widget-origin-add-row .form-input { flex: 1 1 240px; min-width: 0; }
    .widget-origin-preview {
      margin-top: 6px;
      font-family: var(--font-mono);
    }
    .widget-origin-preview.is-invalid { color: var(--accent-rose); }
    .widget-origin-preview.is-valid { color: var(--text-muted); }
    .widget-origin-preview code { overflow-wrap: anywhere; }
    .widget-origin-list {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: 10px;
    }
    .widget-origin-chip {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 0.8rem;
      font-family: var(--font-mono);
      background: rgba(51, 65, 85, 0.5);
      color: var(--text-body);
      padding: 4px 6px 4px 10px;
      border-radius: 999px;
      border: 1px solid var(--glass-border);
    }
    .widget-origin-chip-remove {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 18px;
      height: 18px;
      border-radius: 50%;
      border: none;
      background: transparent;
      color: var(--text-muted);
      cursor: pointer;
      transition: all 0.2s ease;
      flex-shrink: 0;
    }
    .widget-origin-chip-remove:hover { background: rgba(244, 63, 94, 0.18); color: var(--accent-rose); }
    .widget-origin-empty { font-size: 0.8rem; color: var(--text-muted); margin-top: 10px; }
    @media (max-width: 480px) {
      .widget-origin-add-row { flex-direction: column; }
      .widget-origin-add-row .btn { width: 100%; }
    }

    /* Предупреждение «сверка не выполняется» на вкладке наш.дом.рф — тот же
       рисунок, что у .embed-origin-warning: не поломка, а честное состояние,
       требующее действия владельца (привязать корпус к объекту реестра). */
    .nashdomrf-coverage-warning {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin-bottom: 12px;
      padding: 11px 14px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(245, 158, 11, 0.45);
      background: rgba(245, 158, 11, 0.12);
      color: var(--accent-amber);
      font-size: 0.82rem;
      line-height: 1.5;
      transition: all 0.2s ease;
    }
    .nashdomrf-coverage-warning-icon { flex-shrink: 0; line-height: 1; margin-top: 1px; }
    .nashdomrf-coverage-warning-text { min-width: 0; overflow-wrap: anywhere; }
    @media (max-width: 480px) {
      .nashdomrf-coverage-warning { padding: 10px 11px; font-size: 0.78rem; }
    }
    /* Поле, которое смотрят, а не правят: тот же приём, что у реквизитов в
       режиме просмотра — приглушённая заливка вместо запертого на вид поля. */
    .profile-section .form-input.is-readonly {
      opacity: 0.75;
      cursor: default;
      background: rgba(148, 163, 184, 0.08);
    }
    .profile-section .form-input.is-readonly:focus {
      box-shadow: none;
      border-color: var(--glass-border);
    }
    /* Строка «этот раздел ведёт платформа»: не тревога и не ошибка, а факт
       договора — приглушённая индиговая полоса слева вместо цветной заливки. */
    .brand-section-hint.is-platform-note {
      padding: 8px 10px;
      border-left: 2px solid var(--accent-indigo);
      border-radius: var(--radius-sm);
      background: rgba(99, 102, 241, 0.08);
      color: var(--text-body);
    }

    /* Каналы мессенджеров в профиле: строка = канал. Название слева, состояние
       бейджами по центру, действие справа. На узком экране строка складывается
       в колонку — бейджей у WhatsApp четыре, и в одну линию они не встают. */
    .profile-channels {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .profile-channel-row {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
      padding: 10px 12px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--bg-deep);
      transition: all 0.2s ease;
    }
    .profile-channel-row:hover { border-color: var(--accent-indigo); }
    .profile-channel-name {
      flex: 0 0 auto;
      min-width: 120px;
      font-weight: 600;
      color: var(--text-main);
    }
    .profile-channel-state {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      flex: 1 1 200px;
      min-width: 0;
    }
    .profile-channel-row .btn { flex: 0 0 auto; }
    @media (max-width: 480px) {
      .profile-channel-row { align-items: flex-start; flex-direction: column; }
      .profile-channel-row .btn { width: 100%; justify-content: center; }
    }

    /* Пульт менеджера в MAX: код привязки крупно и с моноширинным трекингом —
       его переписывают глазами в чат бота, а не копируют кликом. */
    .manager-alerts-issue { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
    .manager-alerts-code-box {
      display: flex;
      flex-direction: column;
      gap: 6px;
      padding: 14px 16px;
      border: 1px solid var(--accent-indigo);
      border-radius: var(--radius-sm);
      background: rgba(99, 102, 241, 0.08);
    }
    .manager-alerts-code {
      font-family: var(--font-mono);
      font-size: 28px;
      font-weight: 700;
      letter-spacing: 4px;
      color: var(--text-main);
    }
    .manager-alerts-code-hint { margin: 0; font-size: 13px; color: var(--text-muted); }
    .manager-alerts-not-configured {
      margin: 0;
      padding: 8px 10px;
      border-left: 2px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: rgba(245, 158, 11, 0.1);
      color: var(--text-body);
      font-size: 13px;
    }
    .manager-alerts-list { display: flex; flex-direction: column; gap: 8px; }
    .manager-alerts-empty { margin: 0; color: var(--text-muted); font-size: 13px; }
    @media (max-width: 480px) {
      .manager-alerts-code { font-size: 22px; letter-spacing: 2px; }
    }

    /* Привязан ли МОЙ аккаунт в MAX — три состояния должны читаться с одного
       взгляда и не только цветом: у каждого своя иконка и левая полоса, как у
       .manager-alerts-not-configured выше. */
    .manager-alerts-identity {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      padding: 10px 12px;
      margin-bottom: 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--glass-border);
      background: var(--bg-deep);
    }
    .manager-alerts-identity-icon {
      flex: 0 0 auto;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 28px;
      height: 28px;
      border-radius: 50%;
      margin-top: 1px;
    }
    .manager-alerts-identity-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
    .manager-alerts-identity-title { font-weight: 600; color: var(--text-main); font-size: 13.5px; }
    .manager-alerts-identity-text { margin: 0; color: var(--text-body); font-size: 13px; }
    .manager-alerts-identity.is-linked {
      border-left-color: var(--accent-green);
      background: rgba(16, 185, 129, 0.08);
    }
    .manager-alerts-identity.is-linked .manager-alerts-identity-icon {
      color: var(--accent-green);
      background: rgba(16, 185, 129, 0.15);
    }
    .manager-alerts-identity.is-not-linked {
      border-left-color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.08);
    }
    .manager-alerts-identity.is-not-linked .manager-alerts-identity-icon {
      color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.15);
    }
    .manager-alerts-identity.is-neutral .manager-alerts-identity-icon {
      color: var(--text-muted);
      background: var(--glass-border);
    }
    .manager-alerts-identity.is-error {
      border-left-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.08);
    }
    .manager-alerts-identity.is-error .manager-alerts-identity-icon {
      color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.15);
    }

    /* Сводка по выбранному застройщику стоит во всю ширину под сеткой, рядом с
       редакторами лимитов: это одна и та же экономика клиента. Отступ сверху —
       как у соседних блоков вкладки (внутри колонок его держит flex-gap). */
    #tab-profile > .tenant-usage-summary { margin: 20px 0 0; }

    /* Подсветка после клика по чипу в шапке: только тень и рамка — размеры не
       меняются, вёрстка не прыгает. Гаснет сама через 2,2 секунды (класс
       снимает openProfileLimits). Целей две, потому что блоков два: свои лимиты
       у застройщика и сводка по клиенту у платформы. */
    #profile-limits-block.is-highlighted,
    #tab-profile > .tenant-usage-summary.is-highlighted {
      border-color: var(--accent-indigo);
      box-shadow: var(--shadow-glow-indigo);
      animation: profileHighlight 2.2s ease-out;
    }
    @keyframes profileHighlight {
      0%   { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.5); }
      35%  { box-shadow: 0 0 0 6px rgba(99, 102, 241, 0.18); }
      100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
    }
    @media (prefers-reduced-motion: reduce) {
      #profile-limits-block.is-highlighted,
      #tab-profile > .tenant-usage-summary.is-highlighted { animation-duration: 0.01ms; }
    }

    /* ── Реквизиты застройщика для клиентских документов ─────────────────
       Блок внутри колонки модалки. Собственные отступы полей обнулены
       правилом .tenant-modal-col .form-group, поэтому интервалы держит
       flex-gap самого блока. */
    .tenant-brand-section {
      display: flex;
      flex-direction: column;
      gap: 12px;
      padding: 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
    }
    .brand-section-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .brand-section-title {
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .brand-section-hint {
      margin: 0;
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    /* РОП и менеджер реквизиты читают, но не правят (право brand:manage только
       у владельца кабинета и платформы). Бейдж объясняет, почему поля не
       вводятся, — иначе заблокированная форма читается как поломка. */
    .brand-readonly-badge {
      flex-shrink: 0;
      font-size: 0.7rem;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      padding: 3px 9px;
      border-radius: var(--radius-pill);
      color: var(--text-muted);
      background: rgba(148, 163, 184, 0.14);
      border: 1px solid var(--glass-border);
    }
    .tenant-brand-section .form-input.is-readonly {
      opacity: 0.75;
      cursor: default;
      background: rgba(148, 163, 184, 0.08);
    }
    .tenant-brand-section .form-input.is-readonly:focus {
      box-shadow: none;
      border-color: var(--glass-border);
    }
    .brand-field-hint {
      font-size: 0.75rem;
      color: var(--text-muted);
      margin-top: 4px;
      line-height: 1.45;
    }
    .brand-logo-row {
      display: flex;
      align-items: flex-start;
      gap: 12px;
      flex-wrap: wrap;
    }
    /* Предпросмотр показывает логотип так, как он ляжет в документ: подложка
       белая в обеих темах — это лист КП, а не панель кабинета. Логотипы обычно
       прозрачные и тёмные, и на обсидиановом фоне они попросту исчезали бы,
       хотя в напечатанном документе видны. */
    .brand-logo-preview {
      flex: 0 0 auto;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 108px;
      height: 68px;
      padding: 6px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-sm);
      background: rgba(255, 255, 255, 0.92);
      overflow: hidden;
      transition: all 0.2s ease;
    }
    .brand-logo-preview:hover { border-color: var(--accent-indigo); }
    .brand-logo-preview img {
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
      display: block;
    }
    .brand-logo-empty {
      font-size: 0.72rem;
      color: var(--text-muted);
      text-align: center;
    }
    .brand-logo-controls {
      display: flex;
      flex: 1 1 220px;
      min-width: 0;
      flex-direction: column;
      gap: 8px;
    }
    .brand-logo-controls .form-input[type="file"] {
      padding: 8px 10px;
      font-size: 0.85rem;
      cursor: pointer;
    }
    .brand-logo-controls .form-input[type="file"]::file-selector-button {
      margin-right: 10px;
      padding: 5px 12px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      color: var(--text-body);
      font: inherit;
      font-size: 0.8rem;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .brand-logo-controls .form-input[type="file"]::file-selector-button:hover {
      border-color: var(--accent-indigo);
      color: var(--text-main);
    }
    .brand-logo-controls .btn { align-self: flex-start; }
    /* Телефон: предпросмотр и кнопки уходят в колонку, чтобы поле выбора файла
       не сжималось до нечитаемой ширины. */
    @media (max-width: 480px) {
      .brand-logo-row { flex-direction: column; align-items: stretch; }
      .brand-logo-preview { width: 100%; height: 96px; }
      .brand-logo-controls .btn { align-self: stretch; text-align: center; }
    }

    /* Аватар бота — круглый превью того же паттерна, что и логотип, но форма
       другая нарочно: аватар — портрет собеседника в чате, а не лист документа,
       и на прямоугольнике покупатель принял бы его за логотип. */
    .brand-avatar-preview {
      width: 68px;
      height: 68px;
      border-radius: 50%;
      padding: 0;
    }
    .brand-avatar-preview img { object-fit: cover; width: 100%; height: 100%; }
    @media (max-width: 480px) {
      .brand-avatar-preview { width: 68px; height: 68px; }
    }

    /* Подсказка о закрытой работе — в модалке застройщика и над полями лимитов. */
    .plan-blocked-hint {
      margin: 6px 0 12px;
      padding: 10px 12px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-indigo);
      border-radius: var(--radius-sm);
      background: var(--card-bg);
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    .plan-blocked-hint.is-alert {
      border-left-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.1);
    }

    /* Строка «Не работает» в колонке статуса списка застройщиков. */
    .tenant-blocked-note {
      display: flex;
      flex-direction: column;
      gap: 2px;
      margin-top: 6px;
      font-size: 0.7rem;
      line-height: 1.35;
      color: var(--text-muted);
    }
    .tenant-blocked-note .stat-badge { align-self: flex-start; }
    .plan-card-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 6px;
    }
    .plan-card-title {
      font-family: var(--font-display);
      font-size: 0.92rem;
      font-weight: 700;
      color: var(--text-main);
    }
    .plan-card-price {
      font-family: var(--font-display);
      font-size: 1.05rem;
      font-weight: 700;
      color: var(--text-main);
    }
    .plan-card-price span {
      font-size: 0.7rem;
      font-weight: 500;
      color: var(--text-muted);
      margin-left: 4px;
    }
    .plan-card-sub {
      margin-top: -4px;
      font-size: 0.72rem;
      color: var(--text-muted);
    }
    /* Пересчёт лимита пакета в диалоги: цифра, которой продавец меряет тариф.
       Тон акцентный, но негромкий — это пояснение к лимиту, а не второй лимит. */
    .plan-card-dialogs {
      margin-top: 2px;
      font-size: 0.74rem;
      font-weight: 600;
      color: var(--accent-emerald);
      cursor: help;
    }
    /* Сетка едет с сервера: пока её нет, место занимает подпись, а не пустота. */
    .plan-grid-note {
      grid-column: 1 / -1;
      margin: 0;
      padding: 14px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      font-size: 0.8rem;
      color: var(--text-muted);
      text-align: center;
    }
    .plan-retry-btn {
      margin-left: 8px;
      padding: 3px 10px;
      font-size: 0.75rem;
    }
    .plan-card-specs {
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin: 0;
      padding: 0;
      font-size: 0.74rem;
      line-height: 1.4;
      color: var(--text-body);
      flex: 1 1 auto;
    }
    .plan-card-btn {
      justify-content: center;
      width: 100%;
      padding: 7px 10px;
      font-size: 0.8rem;
    }
    .plan-card-btn:disabled { opacity: 0.6; cursor: default; }
    .plan-card-btn:disabled:hover { transform: none; }

    /* ══════════════════════════════════════════════════════════════════════
       ЖУРНАЛ АУДИТА: колонка деталей
       Детали — это сериализованный JSON произвольной длины. Без ограничения
       одна запись растягивала таблицу на два экрана вширь; полный текст
       остаётся в title строки.
       ══════════════════════════════════════════════════════════════════════ */
    .audit-details-cell {
      max-width: 320px;
      font-family: var(--font-mono);
      font-size: 0.74rem;
      color: var(--text-muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      vertical-align: middle;
    }

    /* ══════════════════════════════════════════════════════════════════════
       МАСТЕР ПЕРВИЧНОЙ НАСТРОЙКИ (Onboarding Wizard)
       ══════════════════════════════════════════════════════════════════════ */
    .wizard-steps {
      display: flex;
      align-items: center;
      gap: 8px;
      list-style: none;
      margin: 0 0 16px;
      padding: 0;
    }
    .wizard-step {
      display: flex;
      align-items: center;
      gap: 8px;
      flex: 1 1 0;
      min-width: 0;
      color: var(--text-muted);
      font-size: 0.8rem;
      transition: color 0.2s ease;
    }
    .wizard-step:not(:last-child)::after {
      content: '';
      flex: 1 1 auto;
      height: 1px;
      background: var(--glass-border);
    }
    .wizard-step-dot {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 24px;
      height: 24px;
      flex: 0 0 24px;
      border-radius: 50%;
      border: 1px solid var(--glass-border);
      font-family: var(--font-display);
      font-size: 0.78rem;
      font-weight: 700;
      transition: all 0.2s ease;
    }
    .wizard-step-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    /* На планшете пяти подписям места уже не хватает — до 480px их спасает
       уменьшенный кегль и воздух, ниже 480px подписи убираются совсем. */
    @media (max-width: 768px) {
      .wizard-steps { gap: 6px; }
      .wizard-step { font-size: 0.72rem; gap: 6px; }
      .wizard-step-dot { width: 22px; height: 22px; flex: 0 0 22px; }
    }
    .wizard-step.is-active { color: var(--text-main); }
    .wizard-step.is-active .wizard-step-dot {
      background: var(--accent-gradient-primary);
      border-color: transparent;
      color: #fff;
      box-shadow: var(--shadow-glow-indigo);
    }
    .wizard-step.is-done { color: var(--accent-green); }
    .wizard-step.is-done .wizard-step-dot {
      border-color: var(--accent-green);
      color: var(--accent-green);
    }

    .wizard-pane { display: none; }
    .wizard-pane.is-active { display: block; animation: fadeInUp 0.3s ease; }
    .wizard-pane-lead {
      margin-bottom: 12px;
      font-size: 0.85rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    /* ══════════════════════════════════════════════════════════════════════
       СКРЫТОЕ ПОЛЕ ВЫБОРА ФАЙЛА
       ----------------------------------------------------------------------
       `display: none` убирает поле не только с экрана, но и из обхода табом:
       подпись <label> сама фокус не принимает, и шаги мастера «Выбрать файл
       шахматки» и «Выбрать документ застройщика», как и загрузка документа
       в базу знаний, были недоступны с клавиатуры полностью. Прячем клипом:
       поле остаётся фокусируемым, Enter и пробел открывают выбор файла.
       ══════════════════════════════════════════════════════════════════════ */
    .file-input-hidden {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: 0;
      padding: 0;
      border: 0;
      opacity: 0;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }

    .wizard-upload {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 10px;
      padding: 22px 16px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      color: var(--text-body);
      font-size: 0.86rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    /* Поле идёт СЛЕДУЮЩИМ узлом за подписью, поэтому :focus-within здесь не
       сработает — обод вешаем через :has() на соседа. */
    .wizard-upload:has(+ .file-input-hidden:focus-visible) {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
      border-color: var(--accent-indigo);
    }

    .wizard-upload:hover {
      border-color: var(--accent-indigo);
      background: var(--glass-hover);
      color: var(--text-main);
    }
    /* ── Окно «Версия и история выкладок» ───────────────────────────────────
       Список машинных отметок о выкладках: слева момент, ниже — хэш и число
       коммитов приглушённой строкой. Своих цветов не заводим: всё из токенов
       дизайн-системы, иначе светлая тема получила бы тёмный текст на тёмном. */
    .deploy-history-list {
      list-style: none;
      margin: 0;
      padding: 0;
      max-height: 320px;
      overflow-y: auto;
    }
    .deploy-history-item {
      display: flex;
      flex-direction: column;
      gap: 3px;
      padding: 10px 12px;
      border: 1px solid var(--card-border);
      border-left: 3px solid var(--accent-indigo);
      border-radius: var(--radius-sm);
      background: var(--card-bg);
      margin-bottom: 8px;
      transition: all 0.2s ease;
    }
    .deploy-history-item:last-child { margin-bottom: 0; }
    .deploy-history-item:hover { border-color: var(--accent-indigo); background: var(--glass-hover); }
    .deploy-history-when { font-size: 0.85rem; font-weight: 600; color: var(--text-main); }
    .deploy-history-meta { font-size: 0.78rem; color: var(--text-muted); }
    .deploy-history-meta code { font-family: var(--font-mono); color: var(--accent-cyan); }
    .deploy-history-muted { color: var(--text-muted); }
    .deploy-history-empty { padding: 14px 4px; font-size: 0.83rem; color: var(--text-muted); }
    @media (max-width: 480px) {
      .deploy-history-list { max-height: 260px; }
      .deploy-history-item { padding: 9px 10px; }
      .deploy-history-when { font-size: 0.82rem; }
    }

    .wizard-result:empty { display: none; }
    .wizard-result {
      margin-top: 12px;
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      font-size: 0.8rem;
      line-height: 1.5;
      color: var(--text-body);
    }
    .wizard-result.is-ok {
      border-color: var(--accent-green);
      background: rgba(16, 185, 129, 0.1);
    }
    .wizard-result.is-error {
      border-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.1);
    }
    /* Отчёт сервера об отклонённых строках шахматки: не ошибка и не успех —
       файл прочитан, но каталог не создан, поэтому отдельный жёлтый статус. */
    .wizard-result.is-warn {
      border-color: var(--accent-amber);
      background: rgba(245, 158, 11, 0.12);
    }
    .catalog-reject-head { margin-bottom: 8px; }
    /* Оговорка внутри УСПЕШНОГО импорта: строки залились, но значение в них
       поправлено. Янтарный, а не розовый: отказ и правка значения требуют от
       владельца разных действий, и путать их цветом нельзя. */
    .catalog-warn-head {
      margin: 8px 0;
      padding-left: 8px;
      border-left: 2px solid var(--accent-amber);
      color: var(--accent-amber);
    }
    .catalog-reject-list {
      margin: 0 0 10px;
      padding-left: 18px;
      max-height: 190px;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 3px;
    }
    .catalog-reject-list li { color: var(--text-muted); }
    .catalog-reject-list li strong { color: var(--text-body); }
    .catalog-reject-list .catalog-reject-more {
      list-style: none;
      margin-left: -18px;
      font-style: italic;
      opacity: 0.85;
    }
    /* Кнопка «загрузить всё равно» под построчным отчётом. Правило по КЛАССУ:
       тот же отчёт рисуется и в мастере, и в модалке загрузки каталога из
       «Базы Предложений», а id у элемента один — по нему стилизовать нельзя. */
    #onboarding-catalog-force,
    .catalog-force-btn {
      width: 100%;
      padding: 8px 12px;
      font-size: 0.78rem;
    }

    /* Отчёт о загрузке каталога во вкладке «База Предложений»: места больше,
       чем в узком мастере, поэтому список отклонённых строк показывается
       целиком и прокручивается внутри блока. */
    #catalog-table-import-result { margin-top: 14px; }
    #catalog-table-import-result .catalog-reject-list { max-height: 260px; }
    @media (max-width: 768px) {
      #catalog-table-import-result .catalog-reject-list { max-height: 180px; }
    }

    .wizard-actions { align-items: center; }

    /* === Отчёт о синхронизации каталога с XML-фидом поставщика ============
       Главное, что владелец обязан прочитать с экрана: выгрузка — это СНИМОК
       того, что сейчас в продаже, и загрузка не только добавляет объекты, но и
       СНИМАЕТ их с продажи. Поэтому «нет в выгрузке» и «снято с продажи» стоят
       первой строкой крупными цифрами, а «заведено/обновлено» — второй и мельче:
       иерархия размера здесь и есть объяснение смысла операции.
       ==================================================================== */
    .xml-report-card { max-width: 780px; }
    .xml-report-lead {
      margin-bottom: 14px;
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-indigo);
      background: var(--glass-hover);
      font-size: 0.85rem;
      line-height: 1.55;
      color: var(--text-body);
    }
    .xml-report-lead strong { color: var(--text-main); }
    .xml-report-lead.is-warn { border-left-color: var(--accent-amber); }
    .xml-report-lead.is-error { border-left-color: var(--accent-rose); }
    .xml-report-lead.is-ok { border-left-color: var(--accent-green); }

    /* Первый ряд — последствия синхронизации. Две плитки, крупные цифры. */
    .xml-report-primary {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 12px;
    }
    /* Второй ряд — что пришло из файла. Четыре плитки, цифры на ступень мельче. */
    .xml-report-secondary {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 10px;
      margin-top: 10px;
    }
    .xml-report-tile {
      padding: 12px 14px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      min-width: 0;
      animation: fadeInUp 0.3s ease;
    }
    .xml-report-tile .xml-report-num {
      font-family: var(--font-display);
      font-size: 1.9rem;
      font-weight: 800;
      line-height: 1.1;
      color: var(--text-main);
    }
    .xml-report-secondary .xml-report-tile .xml-report-num { font-size: 1.25rem; }
    .xml-report-tile .xml-report-label {
      margin-top: 4px;
      font-size: 0.82rem;
      font-weight: 600;
      color: var(--text-body);
    }
    .xml-report-tile .xml-report-sub {
      margin-top: 3px;
      font-size: 0.75rem;
      line-height: 1.45;
      color: var(--text-muted);
    }
    /* Снятие с продажи — необратимое последствие, поэтому янтарь/роза, а не
       нейтральное стекло. Ноль в этих плитках подсветку не получает: гореть
       постоянно — верный способ перестать быть замеченным. */
    .xml-report-tile.is-alert {
      border-color: var(--accent-rose);
      background: color-mix(in srgb, var(--accent-rose) 10%, var(--card-bg));
    }
    .xml-report-tile.is-alert .xml-report-num { color: var(--accent-rose-soft); }
    .xml-report-tile.is-watch {
      border-color: var(--accent-amber);
      background: color-mix(in srgb, var(--accent-amber) 10%, var(--card-bg));
    }
    .xml-report-tile.is-watch .xml-report-num { color: var(--accent-gold-soft); }
    .xml-report-tile.is-good .xml-report-num { color: var(--accent-emerald-soft); }

    /* Предупреждения (требуют решения) и заметки (к сведению) отличаются
       цветом полосы: у них разный вес, и слипаться в один список они не должны. */
    .xml-report-msg {
      margin-top: 10px;
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-amber);
      background: color-mix(in srgb, var(--accent-amber) 10%, var(--card-bg));
      font-size: 0.83rem;
      line-height: 1.55;
      color: var(--text-body);
    }
    .xml-report-msg.is-note {
      border-left-color: var(--accent-cyan);
      background: var(--glass-hover);
      color: var(--text-muted);
    }
    /* «Подробнее» у длинных предупреждений (например, про лоты без
       комнатности): факт читается сразу строкой выше, разбор причины и
       совет — по клику. Тон остаётся тем же жёлтым, что и у самой строки —
       это не ошибка импорта, а замечание к результату. */
    .xml-report-msg-details {
      margin-top: 6px;
    }
    .xml-report-msg-details summary {
      cursor: pointer;
      font-weight: 600;
      color: var(--accent-gold-soft);
      list-style: none;
    }
    .xml-report-msg-details summary::-webkit-details-marker { display: none; }
    .xml-report-msg-details summary::before {
      content: '▸ ';
    }
    .xml-report-msg-details[open] summary::before {
      content: '▾ ';
    }
    .xml-report-msg-details > div {
      margin-top: 6px;
      color: var(--text-body);
    }
    /* Перечень самих лотов без комнатности — по контракту `unspecified_rooms`
       ответа импорта. Владельцу нужно увидеть, о каких квартирах речь, а не
       идти их искать; при усечении список честно говорит «показано N из M»
       и не притворяется полным. */
    .xml-unspecified-rooms {
      margin-top: 8px;
    }
    .xml-unspecified-rooms-caption {
      font-size: 0.76rem;
      color: var(--text-muted);
      margin-bottom: 4px;
    }
    .xml-unspecified-rooms-list {
      margin: 0;
      padding-left: 18px;
      max-height: 190px;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 3px;
    }
    .xml-unspecified-rooms-list li {
      color: var(--text-body);
    }
    .xml-unspecified-area {
      color: var(--text-muted);
      font-family: var(--font-mono);
      font-size: 0.78rem;
    }
    .xml-report-section-title {
      margin: 18px 0 8px;
      font-family: var(--font-display);
      font-size: 0.78rem;
      font-weight: 600;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text-muted);
    }
    /* Паспорт файла: источник, дата выгрузки, число оферт. Моноширинный набор —
       это технические значения, их сверяют глазами со своей выгрузкой. */
    .xml-report-feed {
      display: flex;
      flex-wrap: wrap;
      gap: 6px 18px;
      font-size: 0.78rem;
      color: var(--text-muted);
    }
    .xml-report-feed b {
      font-family: var(--font-mono);
      font-weight: 500;
      color: var(--text-body);
    }

    /* Подтверждение снятия с продажи. Отдельная красная панель, а не галочка
       рядом с кнопкой: операция необратима на боевом каталоге, поэтому кнопка
       разблокируется только после того, как владелец своей рукой наберёт число
       снимаемых объектов — прочитать его придётся. */
    .xml-force-confirm {
      margin-top: 16px;
      padding: 14px 16px;
      border-radius: var(--radius-md);
      border: 1px solid var(--accent-rose);
      background: var(--card-bg);
      background: color-mix(in srgb, var(--accent-rose) 12%, var(--card-bg));
      backdrop-filter: var(--blur-glass);
    }
    .xml-force-confirm-title {
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 700;
      color: var(--text-main);
    }
    .xml-force-confirm-text {
      margin-top: 6px;
      font-size: 0.83rem;
      line-height: 1.55;
      color: var(--text-body);
    }
    .xml-force-confirm-row {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
      margin-top: 12px;
    }
    .xml-force-confirm-row .form-input {
      flex: 0 0 150px;
      max-width: 150px;
      font-family: var(--font-mono);
      text-align: center;
    }
    .xml-force-confirm-row .btn { flex: 0 0 auto; }
    .xml-force-confirm-row .btn:disabled { opacity: 0.55; cursor: not-allowed; }

    /* Ход загрузки: файл может весить десятки мегабайт, разбор идёт секунды. */
    .xml-report-progress {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 18px 4px;
      font-size: 0.88rem;
      color: var(--text-body);
    }
    .xml-report-progress .diag-spinner { width: 18px; height: 18px; }

    .xml-report-actions {
      display: flex;
      justify-content: flex-end;
      gap: 8px;
      flex-wrap: wrap;
      margin-top: 18px;
    }

    @media (max-width: 768px) {
      .xml-report-secondary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    @media (max-width: 480px) {
      .xml-report-primary,
      .xml-report-secondary { grid-template-columns: minmax(0, 1fr); }
      .xml-report-tile .xml-report-num { font-size: 1.6rem; }
      .xml-force-confirm-row .form-input,
      .xml-force-confirm-row .btn { flex: 1 1 100%; max-width: none; width: 100%; }
      .xml-report-actions .btn { flex: 1 1 100%; justify-content: center; }
    }
    /* Светлая тема: цвет текста цифр на жемчужном фоне берём из «глубокой»
       ветки палитры — светлые -soft оттенки там не добирают контраста. */
    [data-theme="light"] .xml-report-tile.is-alert .xml-report-num { color: var(--accent-rose); }
    [data-theme="light"] .xml-report-tile.is-watch .xml-report-num { color: var(--accent-amber-deep); }
    [data-theme="light"] .xml-report-tile.is-good .xml-report-num { color: var(--accent-green); }
    [data-theme="light"] .xml-report-msg { color: var(--text-body); }

    [data-theme="light"] .limits-plan { color: var(--accent-indigo); }
    [data-theme="light"] .limits-track { background: rgba(15, 23, 42, 0.12); }
    [data-theme="light"] .budget-banner { background: rgba(245, 158, 11, 0.14); }
    [data-theme="light"] .budget-banner.is-exhausted { background: rgba(244, 63, 94, 0.12); }
    [data-theme="light"] .usage-tile { border-left-color: rgba(15, 23, 42, 0.12); }
    [data-theme="light"] .usage-tile.is-warning { background: rgba(245, 158, 11, 0.14); }
    [data-theme="light"] .usage-tile.is-exhausted { background: rgba(244, 63, 94, 0.1); }

    /* На планшете тариф не помещается в три колонки внутри колонки профиля */
    @media (max-width: 768px) {
      .plan-grid { grid-template-columns: 1fr; }
      .audit-details-cell { max-width: 200px; }
      /* В узкой колонке две плитки лимитов рядом ужимают цифры до переносов */
      .tenant-usage-summary-grid { grid-template-columns: 1fr; }
    }

    @media (max-width: 480px) {
      /* Чип лимитов на телефоне занимает всю строку шапки: рядом с ним всё
         равно ничего не помещается, а обрезанный процент читался бы неверно. */
      .limits-widget { flex: 1 1 100%; justify-content: space-between; padding: 8px 12px; }
      .limits-budget { font-size: 0.8rem; }
      .profile-copy-row .form-input { flex: 1 1 100%; }
      .profile-copy-row .btn { width: 100%; justify-content: center; }
      .tenant-usage { min-width: 0; }
      .usage-row { flex-wrap: wrap; white-space: normal; }
      .usage-label { min-width: 0; }
      .budget-banner { padding: 12px; font-size: 0.8rem; }
      /* Подписи шагов на телефоне съедают всю ширину. Оставлять подпись только
         активному шагу оказалось хуже, чем убрать все: на 380px ей доставалось
         17px из нужных 55 — «Контакты» превращалось в «К…», то есть подпись без
         смысла и с потерей места. Остаются номера 1–5 с подсвеченным текущим —
         этого хватает, чтобы понять, где ты, а что делать, говорит текст шага. */
      .wizard-step-label { display: none; }
      .wizard-step { flex: 0 0 auto; }
      .wizard-step:not(:last-child)::after { min-width: 6px; }
      .wizard-steps { gap: 6px; }
      .wizard-actions { flex-wrap: wrap; }
      .wizard-actions .btn { flex: 1 1 auto; justify-content: center; }
    }

    /* ==========================================================================
       Единые правила для элементов ввода — по всему кабинету, а не по месту.
       Блок стоит последним намеренно: он перекрывает шорткат `background`
       у .form-select и любые локальные правила вкладок.
       ========================================================================== */

    /* 1. Стрелки-счётчики у числовых полей не нужны нигде: суммы, проценты и
       лимиты вводят с клавиатуры, а спиннеры съедали правый край поля и
       меняли его ширину при наведении. margin: 0 — чтобы ширина не «прыгала». */
    input[type="number"]::-webkit-outer-spin-button,
    input[type="number"]::-webkit-inner-spin-button {
      -webkit-appearance: none;
      appearance: none;
      margin: 0;
    }
    input[type="number"] {
      -moz-appearance: textfield;
      appearance: textfield;
    }

    /* 2. Своя стрелка выпадающего списка. Системная прижималась к самой рамке и
       выглядела вываливающейся за поле; рисуем шеврон двумя градиентами
       (currentColor — значит цвет сам следует за темой, без HEX) и отводим его
       от края на 14px. padding-right держит длинный текст опции под стрелку. */
    select,
    select.form-select {
      -webkit-appearance: none;
      -moz-appearance: none;
      appearance: none;
      padding-right: 34px;
      background-image:
        linear-gradient(45deg, transparent 50%, currentColor 50%),
        linear-gradient(135deg, currentColor 50%, transparent 50%);
      background-position:
        calc(100% - 19px) calc(50% - 2px),
        calc(100% - 14px) calc(50% - 2px);
      background-size: 5px 5px, 5px 5px;
      background-repeat: no-repeat;
      text-overflow: ellipsis;
    }
    select::-ms-expand { display: none; }

    /* 3. Уголок растягивания textarea. Системный хват (диагональные насечки ОС)
       выбивался из тёмной темы; гасим его и рисуем свой — те же три штриха
       цветом --text-muted, одинаково читаемые в Obsidian и Pearl. Поле
       по-прежнему тянется по высоте, текст под уголок не заезжает благодаря
       нижнему отступу. */
    textarea,
    .form-textarea {
      resize: vertical;
      padding-bottom: 18px;
    }
    textarea::-webkit-resizer,
    .form-textarea::-webkit-resizer {
      color: var(--text-muted);
      background-color: transparent;
      background-image: linear-gradient(
        135deg,
        transparent 0 44%,
        currentColor 44% 50%,
        transparent 50% 69%,
        currentColor 69% 75%,
        transparent 75%
      );
      background-repeat: no-repeat;
      background-size: 9px 9px;
      background-position: right 4px bottom 4px;
      border-bottom-right-radius: var(--radius-sm);
    }

    /* ======================================================================
       Кнопка сохранения на время запроса (guardSubmitHandler в admin.js).
       Ширину кнопки фиксирует JS перед сменой подписи, поэтому здесь только
       вид: приглушение, курсор ожидания и снятый hover — иначе кнопка
       продолжала бы «отзываться» на наведение, будучи заблокированной.
       Контейнер модалки при этом не перерисовывается: вёрстка не пляшет.
       ====================================================================== */
    .btn.is-submitting {
      opacity: 0.62;
      cursor: progress;
      filter: saturate(0.75);
      /* Подпись «Сохраняем…» длиннее исходной — обрезаем по зафиксированной
         ширине, а не растягиваем кнопку. */
      overflow: hidden;
      white-space: nowrap;
      justify-content: center;
    }

    .btn.is-submitting:hover,
    .btn.is-submitting:active {
      transform: none;
      box-shadow: none;
      filter: saturate(0.75);
    }

    /* Тонкая полоса прогресса по нижней кромке — единственная анимация
       состояния: спиннер внутри кнопки съел бы место у подписи. */
    .btn.is-submitting {
      position: relative;
    }

    .btn.is-submitting::after {
      content: '';
      position: absolute;
      left: 0;
      bottom: 0;
      height: 2px;
      width: 40%;
      border-radius: var(--radius-pill);
      background: currentColor;
      opacity: 0.85;
      animation: submitProgressSlide 1.1s ease-in-out infinite;
    }

    @keyframes submitProgressSlide {
      0%   { transform: translateX(-100%); }
      100% { transform: translateX(250%); }
    }

    @media (prefers-reduced-motion: reduce) {
      .btn.is-submitting::after { animation: none; width: 100%; opacity: 0.5; }
    }

    /* ======================================================================
       Похожие карточки клиентов (#tab-duplicates)
       Оформление намеренно «черновое»: это предположение, а не результат.
       Никакой галочки успеха и зелёного — до нажатия человека ничего не
       произошло. Акцент — янтарь (внимание), не изумруд (готово).
       ====================================================================== */

    .dup-lead-text {
      max-width: 70ch;
      margin: 0 0 14px;
      font-size: 0.88rem;
      line-height: 1.55;
      color: var(--text-muted);
    }

    .dup-list {
      display: flex;
      flex-direction: column;
      gap: 16px;
      margin-top: 16px;
    }

    .dup-group {
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-md);
      backdrop-filter: var(--blur-glass);
      padding: 16px 18px;
      animation: fadeInUp 0.35s ease both;
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }

    .dup-group:hover {
      box-shadow: var(--shadow-card);
    }

    .dup-group-head {
      display: flex;
      flex-wrap: wrap;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 12px;
    }

    /* Строка сервера показывается дословно: это его объяснение сходства,
       переписывать его в кабинете нельзя — разъедется с тем, что он считал. */
    .dup-reason {
      margin: 0;
      font-family: var(--font-display);
      font-size: 0.98rem;
      font-weight: 600;
      line-height: 1.4;
      color: var(--text-main);
    }

    .dup-group-note {
      margin: 4px 0 0;
      font-size: 0.78rem;
      color: var(--text-muted);
    }

    .dup-cards {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
      gap: 12px;
    }

    .dup-card {
      position: relative;
      display: block;
      cursor: pointer;
      padding: 14px;
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      transition: all 0.2s ease;
    }

    .dup-card:hover { transform: translateY(-2px); border-color: var(--accent-indigo); }
    .dup-card:focus-within { outline: 2px solid var(--accent-indigo); outline-offset: 2px; }

    .dup-card.is-main {
      border-color: var(--accent-indigo);
      box-shadow: var(--shadow-glow-indigo);
    }

    .dup-card-pick {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 0.78rem;
      color: var(--text-muted);
      margin-bottom: 10px;
    }

    .dup-card.is-main .dup-card-pick { color: var(--accent-indigo); font-weight: 600; }

    .dup-card-name {
      font-family: var(--font-display);
      font-size: 1rem;
      font-weight: 600;
      color: var(--text-main);
      word-break: break-word;
    }

    .dup-card-phone {
      font-family: var(--font-mono);
      font-size: 0.85rem;
      color: var(--text-body);
      margin-top: 2px;
    }

    .dup-card-meta {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-top: 10px;
      font-size: 0.78rem;
      color: var(--text-muted);
    }

    .dup-card-meta b {
      color: var(--text-body);
      font-weight: 600;
    }

    .dup-facts {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: 10px;
    }

    .dup-fact {
      padding: 3px 9px;
      border-radius: var(--radius-pill);
      border: 1px solid var(--glass-border);
      font-size: 0.72rem;
      color: var(--text-body);
      background: var(--card-bg);
    }

    .dup-group-actions {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 10px;
      margin-top: 14px;
      padding-top: 12px;
      border-top: 1px solid var(--glass-border);
    }

    .dup-group-actions .dup-hint {
      flex: 1 1 220px;
      font-size: 0.76rem;
      color: var(--text-muted);
    }

    /* Пустой список — обычное состояние, а не ошибка: и выглядит спокойно. */
    .dup-empty {
      padding: 32px 24px;
      text-align: center;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
    }

    .dup-empty-title {
      font-family: var(--font-display);
      font-size: 1.05rem;
      font-weight: 600;
      color: var(--text-main);
      margin: 0 0 8px;
    }

    .dup-empty-text {
      margin: 0 auto;
      max-width: 52ch;
      font-size: 0.86rem;
      line-height: 1.6;
      color: var(--text-muted);
    }

    .dup-empty.is-error { border-color: var(--accent-rose); }
    .dup-empty.is-error .dup-empty-title { color: var(--accent-rose); }

    @media (max-width: 768px) {
      .dup-cards { grid-template-columns: 1fr; }
      .dup-group { padding: 14px; }
      .dup-group-head { flex-direction: column; }
      .dup-group-actions .btn { width: 100%; justify-content: center; }
    }

    @media (max-width: 480px) {
      .dup-lead-text { font-size: 0.84rem; }
      .dup-empty { padding: 24px 16px; }
    }

    @media (prefers-reduced-motion: reduce) {
      .dup-group { animation: none; }
      .dup-card:hover { transform: none; }
    }

    /* ======================================================================
       Посторонние темы (срез 4 телеметрии движка)
       ====================================================================== */

    /* Карточка «не измеряется». Приглушена намеренно: это НЕ цифра, и выглядеть
       как цифра она не должна — иначе прочерк дочитывают до нуля. */
    .engine-figure-unmeasured {
      border-style: dashed;
    }

    .engine-figure-unmeasured .engine-figure-value {
      font-size: 1rem;
      font-weight: 600;
      color: var(--text-muted);
    }

    /* Замер векторного барьера. Отдельная рамка и отступ — единственное, что
       мешает прочитать «отказал бы» как реальные отказы из таблицы выше и
       сложить два числа глазами. Ставить его строкой рядом нельзя. */
    .engine-subblock {
      margin-top: 18px;
      padding: 14px 16px;
      border: 1px dashed var(--card-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-md);
      background: var(--card-bg);
    }

    .engine-subblock-title {
      margin: 0;
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-body);
    }

    @media (max-width: 768px) {
      .engine-subblock { padding: 12px; }
    }

    /* Полнота проверки похожих карточек. Янтарь, а не роза: это не ошибка, а
       оговорка об охвате — но и не мелкая сноска, иначе короткий список
       прочитают как «база разобрана». */
    .dup-scan-notice {
      margin: 12px 0 0;
      padding: 10px 14px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-amber);
      border-radius: var(--radius-sm);
      background: var(--card-bg);
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-body);
    }

    .dup-group-note-dismissed { color: var(--accent-amber); }

    /* Карточка, по паре с которой уже решили «разные люди». Помечена, но не
       заблокирована: решение остаётся за человеком, он мог передумать. */
    .dup-card.is-pair-dismissed {
      border-style: dashed;
      border-color: var(--accent-amber);
    }

    .dup-pair-dismissed-mark {
      display: block;
      margin-bottom: 8px;
      padding: 4px 8px;
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      font-size: 0.72rem;
      line-height: 1.4;
      color: var(--accent-amber);
    }

    /* === Вкладка «Документация» ============================================
       Документов десять, и списком одинаковых кнопок они не читались: человек
       искал глазами нужный, а видел стену. Здесь каждый документ — карточка с
       названием и строкой «кому и о чём», карточки собраны в три смысловые
       группы. Сетка авто-заполняемая: на десктопе 2–3 колонки, на телефоне
       одна, без отдельного медиазапроса под каждую ширину. */
    .docs-group { margin-top: 24px; }
    .docs-group:first-child { margin-top: 12px; }

    .docs-group-title {
      font-family: var(--font-display);
      font-size: 0.78rem;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text-main);
      margin: 0;
    }

    .docs-group-hint {
      font-size: 0.8rem;
      color: var(--text-muted);
      margin: 2px 0 12px;
    }

    .docs-card-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 10px;
    }

    .docs-card {
      display: flex;
      align-items: flex-start;
      gap: 12px;
      width: 100%;
      padding: 14px 16px;
      text-align: left;
      cursor: pointer;
      background: var(--card-bg);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-md);
      color: var(--text-body);
      font-family: var(--font-body);
      transition: all 0.2s ease;
    }

    .docs-card:hover {
      background: var(--glass-hover);
      border-color: var(--accent-indigo);
      transform: translateY(-2px);
      box-shadow: var(--shadow-card);
    }

    .docs-card:active { transform: scale(0.98); }

    .docs-card:focus-visible {
      outline: 2px solid var(--accent-indigo);
      outline-offset: 2px;
    }

    .docs-card-icon { font-size: 1.15rem; line-height: 1.3; }

    .docs-card-body {
      display: flex;
      flex-direction: column;
      gap: 3px;
      min-width: 0;
    }

    .docs-card-name {
      font-weight: 600;
      font-size: 0.92rem;
      color: var(--text-main);
    }

    .docs-card-desc {
      font-size: 0.78rem;
      line-height: 1.45;
      color: var(--text-muted);
    }

    /* Карточка документа, который ещё не опубликован. Не скрыта и не выглядит
       поломкой: приглушена, курсор обычный, наведение ничего не обещает. */
    .docs-card-soon {
      cursor: default;
      opacity: 0.62;
      border-style: dashed;
    }

    .docs-card-soon:hover {
      background: var(--card-bg);
      border-color: var(--glass-border);
      transform: none;
      box-shadow: none;
    }

    .docs-card-soon:active { transform: none; }

    .docs-card-badge {
      display: inline-block;
      margin-left: 6px;
      padding: 1px 7px;
      border-radius: 999px;
      background: var(--glass-hover);
      border: 1px solid var(--glass-border);
      color: var(--text-muted);
      font-size: 0.64rem;
      font-weight: 700;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      vertical-align: middle;
    }

    /* Контекстная ссылка на инструкцию рядом с настройкой (выбор CRM).
       Кнопка, а не <a>: страница открывается тем же openIntegrationGuide(). */
    .docs-inline-links {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 8px;
      margin-top: 8px;
      font-size: 0.82rem;
      color: var(--text-muted);
    }

    .docs-inline-link {
      padding: 0;
      background: none;
      border: none;
      cursor: pointer;
      font: inherit;
      color: var(--accent-cyan);
      border-bottom: 1px dashed currentColor;
      transition: all 0.2s ease;
    }

    .docs-inline-link:hover { color: var(--accent-indigo); }

    @media (max-width: 480px) {
      .docs-card-grid { grid-template-columns: 1fr; }
      .docs-card { padding: 12px 14px; }
    }

    @media (prefers-reduced-motion: reduce) {
      .docs-card { transition-duration: 0.01ms; }
      .docs-card:hover { transform: none; }
    }

    /* === ИСТОЧНИКИ КАТАЛОГА ПО ССЫЛКЕ (PLAN_FEED_SOURCES, этап 6) =========
       Список карточек, а не таблица: у каждой ссылки есть исход последней
       синхронизации — текст, числа и, при неудаче, причина. В строке таблицы
       это не помещается, а именно это владелец и должен прочитать.

       Неудача видна цветом слева и заголовком, а не мелкой серой пометкой: на
       боевом сервере исходящие Telegram и почта заблокированы хостингом, и
       кабинет — единственное место, где владелец узнает о поломке.
       ==================================================================== */
    .feed-sources {
      margin: 0 0 16px;
      padding: 14px 16px;
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      -webkit-backdrop-filter: var(--blur-glass);
    }

    .feed-sources-head {
      display: flex;
      flex-wrap: wrap;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 12px;
    }

    .feed-sources-title {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 0 0 4px;
      font-family: var(--font-display);
      font-size: 1rem;
      color: var(--text-main);
    }

    .feed-sources-sub {
      margin: 0;
      max-width: 62ch;
      font-size: 0.82rem;
      line-height: 1.5;
      color: var(--text-muted);
    }

    .feed-sources-head-actions { display: flex; flex-wrap: wrap; gap: 8px; }

    .feed-sources-list { display: grid; gap: 10px; }

    .feed-sources-loading {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 14px;
      font-size: 0.85rem;
      color: var(--text-muted);
    }

    /* Пусто — это нормальное начало, а не ошибка: приглашение, а не плашка. */
    .feed-sources-empty {
      padding: 18px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      background: var(--glass-hover);
      text-align: center;
      animation: fadeInUp 0.3s ease;
    }
    .feed-sources-empty-title {
      font-family: var(--font-display);
      font-size: 0.95rem;
      color: var(--text-main);
      margin-bottom: 6px;
    }
    .feed-sources-empty-text {
      margin: 0 auto 12px;
      max-width: 58ch;
      font-size: 0.82rem;
      line-height: 1.55;
      color: var(--text-muted);
    }

    .feed-source-card {
      display: grid;
      grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) auto;
      align-items: start;
      gap: 14px;
      padding: 12px 14px;
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--text-muted);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: all 0.2s ease;
      animation: fadeInUp 0.3s ease;
    }
    .feed-source-card:hover {
      transform: translateY(-2px);
      box-shadow: var(--shadow-card);
      background: var(--glass-hover);
    }
    .feed-source-card.is-ok { border-left-color: var(--accent-emerald); }
    .feed-source-card.is-warn { border-left-color: var(--accent-amber); }
    .feed-source-card.is-error { border-left-color: var(--accent-rose); }
    .feed-source-card.is-idle { border-left-color: var(--accent-indigo); }
    .feed-source-card.is-paused { opacity: 0.78; }

    .feed-source-main { min-width: 0; }

    .feed-source-name {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 8px;
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
      margin-bottom: 4px;
    }

    .feed-source-pill {
      padding: 2px 8px;
      border-radius: var(--radius-pill);
      border: 1px solid var(--glass-border);
      background: var(--glass-hover);
      font-family: var(--font-body);
      font-size: 0.7rem;
      font-weight: 600;
      color: var(--text-muted);
      text-transform: none;
    }

    .feed-source-url {
      display: block;
      font-family: var(--font-mono);
      font-size: 0.76rem;
      color: var(--accent-cyan);
      overflow-wrap: anywhere;
      text-decoration: none;
      border-bottom: 1px dashed transparent;
      transition: all 0.2s ease;
    }
    .feed-source-url:hover { border-bottom-color: currentColor; }

    .feed-source-schedule {
      margin-top: 6px;
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }
    .feed-source-schedule strong { color: var(--text-body); }

    .feed-source-status { min-width: 0; font-size: 0.8rem; line-height: 1.5; }
    .feed-source-status-title {
      font-weight: 600;
      color: var(--text-main);
      margin-bottom: 2px;
    }
    .feed-source-card.is-error .feed-source-status-title { color: var(--accent-rose); }
    .feed-source-when { color: var(--text-muted); font-size: 0.76rem; margin-bottom: 4px; }
    .feed-source-status-text { color: var(--text-body); overflow-wrap: anywhere; }
    .feed-source-counts { margin-top: 4px; color: var(--text-muted); font-size: 0.78rem; }

    .feed-source-blocked {
      margin-top: 8px;
      padding: 8px 10px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-amber);
      background: var(--glass-hover);
      font-size: 0.78rem;
      color: var(--text-body);
    }

    .feed-source-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      justify-content: flex-end;
    }

    /* Форма источника: час и переключатель расписания стоят рядом — это одна
       мысль «когда и надо ли». */
    .feed-source-form-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 12px;
    }
    .feed-source-active-toggle {
      display: flex;
      align-items: center;
      gap: 10px;
      cursor: pointer;
    }
    .feed-source-active-text { font-size: 0.78rem; color: var(--text-muted); line-height: 1.45; }

    /* Название ЖК становится визуально обязательным только для формата
       Авито (см. updateFeedSourceComplexNameRequirement() в admin.js) —
       звёздочка и рамка красным, но без хардкода HEX вне токенов. */
    .feed-source-required-mark { color: var(--accent-rose); font-weight: 700; }
    #feed-source-complex-name.is-required-empty {
      border-color: var(--accent-rose);
      box-shadow: 0 0 0 1px var(--accent-rose);
    }
    #feed-source-complex-name-hint.is-required { color: var(--accent-rose); }

    .feed-source-form-error {
      margin: 10px 0 0;
      padding: 9px 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-rose);
      background: var(--glass-hover);
      font-size: 0.82rem;
      color: var(--text-body);
    }

    .feed-source-delete-text {
      margin: 0 0 4px;
      font-size: 0.86rem;
      line-height: 1.6;
      color: var(--text-body);
    }
    .feed-source-delete-text strong { color: var(--text-main); }

    .feed-sync-hint {
      margin-top: 12px;
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent-indigo);
      background: var(--glass-hover);
      font-size: 0.82rem;
      line-height: 1.55;
      color: var(--text-body);
    }
    .feed-sync-hint.is-warn { border-left-color: var(--accent-amber); }

    /* ------------------------------------------------------------------
       Журнал проходов по источнику каталога (модалка «История обновлений»).
       Исход кодируется левой полосой и точкой теми же тремя цветами, что и
       карточка источника: зелёный — прошло, янтарь — замечания, розовый —
       не прошло. Проваленный проход обязан читаться в списке сразу, а не
       после раскрытия — поэтому цвет несёт заголовок, а не только значок.
       ------------------------------------------------------------------ */
    .feed-runs-source {
      margin: 0 0 12px;
      font-size: 0.8rem;
      color: var(--text-muted);
      overflow-wrap: anywhere;
    }
    .feed-runs-body {
      display: flex;
      flex-direction: column;
      gap: 10px;
      max-height: min(58vh, 520px);
      overflow-y: auto;
      padding-right: 2px;
    }
    .feed-runs-empty,
    .feed-runs-error {
      padding: 18px;
      border-radius: var(--radius-md);
      border: 1px dashed var(--glass-border);
      background: var(--card-bg);
      text-align: center;
      font-size: 0.82rem;
      color: var(--text-muted);
      line-height: 1.55;
    }
    .feed-runs-error { display: grid; gap: 10px; justify-items: center; }
    .feed-runs-empty-title {
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
      margin-bottom: 6px;
    }

    .feed-run {
      border: 1px solid var(--glass-border);
      border-left: 3px solid var(--accent-indigo);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      transition: all 0.2s ease;
    }
    .feed-run:hover { border-color: rgba(99, 102, 241, 0.35); }
    .feed-run.is-ok { border-left-color: var(--accent-emerald); }
    .feed-run.is-warn { border-left-color: var(--accent-amber); }
    .feed-run.is-error { border-left-color: var(--accent-rose); }
    .feed-run.is-error { background: var(--glass-hover); }

    .feed-run-head {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      width: 100%;
      padding: 12px 14px;
      background: none;
      border: none;
      text-align: left;
      cursor: pointer;
      color: inherit;
      font: inherit;
      border-radius: var(--radius-md);
      transition: background 0.2s ease;
    }
    .feed-run-head:hover { background: var(--glass-hover); }
    .feed-run-head:active { transform: scale(0.995); }
    .feed-run-dot {
      width: 9px;
      height: 9px;
      margin-top: 5px;
      border-radius: var(--radius-pill);
      flex-shrink: 0;
      background: var(--accent-indigo);
    }
    .feed-run.is-ok .feed-run-dot { background: var(--accent-emerald); }
    .feed-run.is-warn .feed-run-dot { background: var(--accent-amber); }
    .feed-run.is-error .feed-run-dot { background: var(--accent-rose); }

    .feed-run-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
    .feed-run-title {
      font-size: 0.88rem;
      font-weight: 600;
      color: var(--text-main);
      line-height: 1.3;
    }
    .feed-run.is-error .feed-run-title { color: var(--accent-rose); }
    .feed-run-when { font-size: 0.76rem; color: var(--text-muted); }
    .feed-run-counts { font-size: 0.78rem; color: var(--text-body); overflow-wrap: anywhere; }
    .feed-run-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
    .feed-run-chip {
      display: inline-flex;
      align-items: center;
      padding: 2px 9px;
      border-radius: var(--radius-pill);
      font-size: 0.72rem;
      font-weight: 600;
      border: 1px solid var(--glass-border);
      color: var(--text-body);
    }
    .feed-run-chip.is-warn {
      color: var(--accent-amber);
      border-color: rgba(245, 158, 11, 0.4);
      background: rgba(245, 158, 11, 0.1);
    }
    .feed-run-chip.is-error {
      color: var(--accent-rose);
      border-color: rgba(244, 63, 94, 0.4);
      background: rgba(244, 63, 94, 0.1);
    }
    .feed-run-caret { color: var(--text-muted); font-size: 0.85rem; margin-top: 2px; }

    /* display:flex сильнее атрибута hidden — без этой строки свёрнутый проход
       оставался бы раскрытым (поймано живой проверкой 18.08.2026). */
    .feed-run-details[hidden] { display: none; }
    .feed-run-details {
      padding: 0 14px 12px 32px;
      display: flex;
      flex-direction: column;
      gap: 10px;
      animation: fadeInUp 0.25s ease;
    }
    .feed-run-block-title {
      font-size: 0.74rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: var(--text-muted);
      margin-bottom: 4px;
      font-family: var(--font-display);
    }
    .feed-run-note {
      font-size: 0.79rem;
      color: var(--text-body);
      line-height: 1.5;
      overflow-wrap: anywhere;
    }
    .feed-run-rejected,
    .feed-run-list {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .feed-run-rejected li {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      gap: 8px;
      padding: 7px 10px;
      border-radius: var(--radius-sm);
      background: var(--glass-hover);
      font-size: 0.78rem;
      color: var(--text-body);
    }
    .feed-run-rejected-row { font-weight: 600; color: var(--text-main); white-space: nowrap; }
    .feed-run-rejected-id {
      font-family: var(--font-mono);
      font-size: 0.72rem;
      color: var(--text-muted);
      overflow-wrap: anywhere;
    }
    .feed-run-rejected-reason { flex: 1 1 220px; overflow-wrap: anywhere; }
    /* Площадь лота без комнатности — тот же формат строки, что у отклонённых
       оферт (.feed-run-rejected), только вторая колонка не «причина», а метраж. */
    .feed-run-unspecified-area { color: var(--text-muted); overflow-wrap: anywhere; }
    .feed-run-list li {
      position: relative;
      padding-left: 14px;
      font-size: 0.78rem;
      color: var(--text-body);
      line-height: 1.5;
      overflow-wrap: anywhere;
    }
    .feed-run-list li::before {
      content: '';
      position: absolute;
      left: 0;
      top: 8px;
      width: 6px;
      height: 6px;
      border-radius: var(--radius-pill);
      background: var(--text-muted);
    }
    .feed-run-list.is-warn li::before { background: var(--accent-amber); }
    .feed-run-list.is-note li::before { background: var(--accent-indigo); }

    @media (max-width: 480px) {
      .feed-run-details { padding-left: 16px; }
      .feed-run-head { padding: 11px 12px; }
      .feed-run-rejected li { flex-direction: column; gap: 3px; }
      .feed-run-rejected-row { white-space: normal; }
    }

    /* Какой формат мы распознали в загруженном файле — первой строкой отчёта. */
    .catalog-format-note {
      margin-bottom: 6px;
      font-size: 0.8rem;
      color: var(--text-muted);
    }
    .catalog-format-note strong { color: var(--text-body); }

    @media (max-width: 900px) {
      .feed-source-card { grid-template-columns: minmax(0, 1fr); }
      .feed-source-actions { justify-content: flex-start; }
    }

    @media (max-width: 480px) {
      .feed-sources { padding: 12px; }
      .feed-sources-head-actions { width: 100%; }
      .feed-sources-head-actions .btn { flex: 1 1 100%; justify-content: center; }
      .feed-source-form-grid { grid-template-columns: minmax(0, 1fr); }
      .feed-source-actions .btn { flex: 1 1 100%; justify-content: center; }
    }

    @media (prefers-reduced-motion: reduce) {
      .feed-source-card { transition-duration: 0.01ms; animation: none; }
      .feed-source-card:hover { transform: none; }
      .feed-sources-empty { animation: none; }
    }

    /* ====================================================================
       ВЫПАДАЮЩИЙ СПИСОК В СТИЛЕ КРОШЕК КАТАЛОГА (.catalog-crumbs-menu)

       ПРОСЬБА ВЛАДЕЛЬЦА 18.08.2026, повторная: списки в фильтрах каталога
       должны выглядеть как меню хлебных крошек, и тем же стилем — остальные
       списки кабинета.

       ПОЧЕМУ КОМПОНЕНТ, А НЕ СТИЛИ НА <select>. Раскрытый список нативного
       селекта рисует операционная система: ни фон, ни рамку, ни отступы
       элементов оформить нельзя. Поэтому виден здесь наш собственный список, а
       сам <select> остаётся в разметке и продолжает быть источником истины —
       значение читает и слушает тот же код, что и раньше (`onchange` в
       разметке срабатывает от программного `change`).

       Токены — только из дизайн-системы: панель повторяет .catalog-crumbs-menu
       намеренно, чтобы при правке одной темы вторая не отставала.
       ==================================================================== */
    .fancy-select {
      position: relative;
      width: 100%;
    }

    /* Нативный селект остаётся В РАЗМЕТКЕ и в потоке фокуса: он источник
       значения, к нему ведёт <label for>, и его же читает вспомогательная
       техника. `display:none` сломал бы и то, и другое. */
    .fancy-select > select {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      padding: 0;
      border: 0;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }

    .fancy-select-trigger {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      width: 100%;
      padding: 12px;
      background: rgba(15, 23, 42, 0.7);
      border: 1px solid var(--glass-border);
      border-radius: var(--radius-sm);
      color: var(--text-main);
      font-family: inherit;
      font-size: 0.9rem;
      text-align: left;
      cursor: pointer;
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }

    .fancy-select-trigger:hover { border-color: var(--accent-purple); }

    /* Фокус виден ободом, а не только рамкой: рамка 1px с полупрозрачным
       ореолом на обсидиане не читается — то же замечание, что у .form-select. */
    .fancy-select-trigger:focus-visible {
      outline: 2px solid var(--accent-cyan);
      outline-offset: 2px;
    }

    .fancy-select.is-open > .fancy-select-trigger {
      border-color: var(--accent-purple);
      box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
    }

    .fancy-select-value {
      flex: 1 1 auto;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /* Выбор, отличный от «все», подсвечен так же, как заполненный фильтр
       каталога (`[data-filled]`): владелец видит, что список сужен, не
       раскрывая его. */
    .fancy-select[data-filled] > .fancy-select-trigger { border-color: var(--accent-purple); }
    .fancy-select[data-filled] .fancy-select-value { color: var(--text-main); font-weight: 600; }

    .fancy-select-caret {
      flex: 0 0 auto;
      color: var(--text-muted);
      transition: transform 0.18s ease;
    }
    .fancy-select.is-open .fancy-select-caret { transform: rotate(180deg); }

    /* Панель — копия .catalog-crumbs-menu. Полупрозрачной быть не может: под
       ней лежат карточки каталога, и текст читался бы поверх фотографий. */
    .fancy-select-menu {
      position: absolute;
      top: calc(100% + 4px);
      left: 0;
      z-index: 60;
      width: max(100%, 220px);
      max-width: min(420px, calc(100vw - 48px));
      max-height: 60vh;
      overflow-y: auto;
      overscroll-behavior: contain;
      padding: 6px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--bg-deep);
      box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
      animation: fadeInUp 0.18s ease;
    }
    .fancy-select-menu[hidden] { display: none; }

    /* Список открывается вверх, когда снизу нет места: панель фильтров стоит
       вплотную к таблице, и вниз список уезжал бы за край окна. */
    .fancy-select.opens-up > .fancy-select-menu {
      top: auto;
      bottom: calc(100% + 4px);
      animation: none;
    }

    .fancy-select-option {
      display: block;
      width: 100%;
      padding: 9px 10px;
      background: transparent;
      border: 0;
      border-radius: var(--radius-sm);
      color: var(--text-body);
      font-family: inherit;
      font-size: 0.9rem;
      text-align: left;
      cursor: pointer;
    }

    .fancy-select-option:hover,
    .fancy-select-option.is-active {
      background: rgba(139, 92, 246, 0.16);
      color: var(--text-main);
    }

    .fancy-select-option[aria-selected="true"] {
      color: var(--text-main);
      font-weight: 600;
    }

    .fancy-select-option[disabled] {
      color: var(--text-muted);
      cursor: default;
    }

    .fancy-select-empty {
      padding: 10px;
      color: var(--text-muted);
      font-size: 0.85rem;
    }

    @media (prefers-reduced-motion: reduce) {
      .fancy-select-menu { animation: none; }
      .fancy-select-caret { transition: none; }
    }

    /* ══════════════════════════════════════════════════════════════════════
       УТРЕННЯЯ СВОДКА (#morningDigest)
       Спокойная по умолчанию: карточка обычного стекла, цветом кричит только
       раздел в состоянии problem. Отдельный вид у «не смогли посчитать» —
       это наша ошибка, а не пустота у владельца, и путать их нельзя.
       ══════════════════════════════════════════════════════════════════════ */
    .digest-card {
      /* Ширину берём от надвкладочного стека, не от вкладки; отступ снизу даёт
         gap стека, поэтому собственного margin здесь больше нет. */
      width: 100%;
      min-width: 0;
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      background: var(--card-bg);
      backdrop-filter: var(--blur-glass);
      -webkit-backdrop-filter: var(--blur-glass);
      overflow: hidden;
      animation: fadeInUp 0.35s ease;
      transition: all 0.2s ease;
    }
    .digest-card.is-problem {
      border-color: rgba(244, 63, 94, 0.45);
      box-shadow: 0 6px 24px rgba(244, 63, 94, 0.12);
    }
    .digest-head {
      display: flex;
      align-items: center;
      gap: 12px;
      width: 100%;
      padding: 14px 16px;
      border: 0;
      background: transparent;
      color: inherit;
      text-align: left;
      cursor: pointer;
      font: inherit;
      transition: all 0.2s ease;
      /* Устойчивое место под шапку: подпись приходит с сервера и бывает то в
         одну, то в две строки — без запаса страница дёргалась после ответа. */
      min-height: 66px;
    }
    .digest-head:hover { background: var(--glass-hover); }
    .digest-head:active { transform: scale(0.995); }
    .digest-dot {
      flex: 0 0 auto;
      width: 10px; height: 10px;
      border-radius: var(--radius-pill);
      background: var(--accent-emerald);
      box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.16);
    }
    .digest-dot.is-problem {
      background: var(--accent-rose);
      box-shadow: 0 0 0 4px rgba(244, 63, 94, 0.18);
      animation: pulseGlow 2.4s ease-in-out infinite;
    }
    .digest-dot.is-muted {
      background: var(--text-muted);
      box-shadow: 0 0 0 4px rgba(128, 142, 163, 0.14);
    }
    .digest-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
    .digest-headline {
      overflow-wrap: anywhere;
      font-family: var(--font-display);
      font-size: 1rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .digest-subline {
      overflow-wrap: anywhere;
      font-size: 0.8rem;
      color: var(--text-muted);
      line-height: 1.45;
    }
    .digest-head-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
    .digest-updated { font-size: 0.72rem; color: var(--text-muted); font-family: var(--font-mono); }
    .digest-chevron { display: inline-flex; color: var(--text-muted); transition: transform 0.2s ease; }
    .digest-head[aria-expanded="true"] .digest-chevron { transform: rotate(180deg); }

    .digest-body {
      padding: 0 16px 14px;
      border-top: 1px solid var(--card-border);
      min-width: 0;
    }
    .digest-body[hidden] { display: none; }
    .digest-sections {
      display: grid;
      /* min(260px, 100%): жёсткие 260px в узкой колонке сами по себе шире
         контейнера, и сетка вылезала за карточку ещё до текста. */
      grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
      gap: 10px;
      padding-top: 14px;
      min-width: 0;
      /* Карточка раздела по своей высоте: при stretch короткий «Диалоги»
         растягивался под самый длинный текст и карточка росла впустую. */
      align-items: start;
    }
    .digest-item {
      /* Главная причина «блоки не влезают»: у элемента грида min-width по
         умолчанию auto, поэтому длинное слово из чужого текста (ссылка, адрес,
         слипшаяся фраза покупателя) распирало колонку, а карточка с
         overflow: hidden просто обрезала содержимое. */
      min-width: 0;
      overflow-wrap: anywhere;
      word-break: break-word;
      padding: 12px 14px;
      border: 1px solid var(--card-border);
      border-left: 3px solid var(--card-border);
      border-radius: var(--radius-sm);
      background: rgba(255, 255, 255, 0.02);
      transition: all 0.2s ease;
    }
    .digest-item:hover { transform: translateY(-2px); box-shadow: var(--shadow-card); }
    /* Порядок в сетке: то, что требует внимания, всегда первым, независимо от
       порядка разделов в ответе. */
    .digest-item.is-problem {
      order: -1;
      border-color: rgba(244, 63, 94, 0.4);
      border-left-color: var(--accent-rose);
      background: rgba(244, 63, 94, 0.08);
    }
    .digest-item.is-ok { border-left-color: var(--accent-emerald); }
    .digest-item.is-nodata { border-left-color: var(--text-muted); opacity: 0.85; }
    .digest-item-title {
      display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-width: 0;
      font-family: var(--font-display);
      font-size: 0.85rem;
      font-weight: 600;
      color: var(--text-main);
      margin-bottom: 4px;
    }
    .digest-item-note {
      font-size: 0.8rem; color: var(--text-body); line-height: 1.5;
      min-width: 0; overflow-wrap: anywhere;
    }
    .digest-item-meta {
      margin-top: 8px;
      display: flex; flex-wrap: wrap; gap: 6px;
    }
    .digest-chip {
      padding: 2px 10px;
      border-radius: var(--radius-pill);
      border: 1px solid var(--card-border);
      background: rgba(255, 255, 255, 0.03);
      font-size: 0.72rem;
      color: var(--text-muted);
      /* Раньше nowrap: подпись статьи расхода вместе с суммой длиннее колонки
         и выталкивала её за карточку. Перенос внутри чипа безопаснее обрезки. */
      max-width: 100%;
      overflow-wrap: anywhere;
    }
    .digest-chip.is-live { border-color: rgba(16, 185, 129, 0.4); color: var(--accent-emerald); }
    .digest-chip.is-money { border-color: rgba(6, 182, 212, 0.35); color: var(--accent-cyan); }
    .digest-tag {
      padding: 1px 8px;
      border-radius: var(--radius-pill);
      font-size: 0.66rem;
      font-weight: 600;
      letter-spacing: 0.02em;
      text-transform: uppercase;
    }
    .digest-tag.is-problem { background: rgba(244, 63, 94, 0.16); color: var(--accent-rose); }
    /* «Не смогли посчитать» — наша ошибка, а не пустота у владельца. Своя
       нейтральная метка: без неё он пошёл бы искать поломку у себя. */
    .digest-tag.is-failed { background: rgba(245, 158, 11, 0.14); color: var(--accent-amber); }
    /* Вопросы покупателей — чужой текст без ограничения длины. */
    .digest-list {
      margin: 8px 0 0; padding-left: 18px;
      font-size: 0.78rem; color: var(--text-muted); line-height: 1.55;
      min-width: 0; overflow-wrap: anywhere; word-break: break-word;
    }
    .digest-list li { margin-bottom: 2px; }
    .digest-foot {
      display: flex; align-items: center; justify-content: space-between; gap: 12px;
      margin-top: 12px;
    }
    .digest-window { font-size: 0.72rem; color: var(--text-muted); min-width: 0; overflow-wrap: anywhere; }
    .digest-skeleton {
      height: 76px;
      border-radius: var(--radius-sm);
      background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.07) 37%, rgba(255,255,255,0.03) 63%);
      background-size: 400% 100%;
      animation: shimmer 1.4s linear infinite;
    }

    [data-theme="light"] .digest-item { background: rgba(15, 23, 42, 0.02); }
    [data-theme="light"] .digest-item.is-problem { background: rgba(244, 63, 94, 0.08); }
    [data-theme="light"] .digest-chip { background: rgba(15, 23, 42, 0.03); }

    @media (max-width: 768px) {
      .digest-sections { grid-template-columns: 1fr; }
      .digest-updated { display: none; }
    }
    @media (max-width: 480px) {
      .digest-head { padding: 12px 12px; gap: 10px; align-items: flex-start; min-height: 0; }
      .digest-dot { margin-top: 5px; }
      .digest-headline { font-size: 0.92rem; }
      .digest-body { padding: 0 12px 12px; }
      .digest-foot { flex-direction: column; align-items: stretch; }
      .digest-foot .btn { width: 100%; }
    }

    /* ══════════════════════════════════════════════════════════════════════
       ДОСКА «ЧТО ТРЕБУЕТ ВНИМАНИЯ СЕЙЧАС» (PLAN_DASHBOARD_ACTION_QUEUE.md)
       ----------------------------------------------------------------------
       Заменила верхний ряд KPI-плиток. Сверху — очередь действий (четыре
       широкие строки, каждая вход в отфильтрованный список), под ней свёрнутая
       полоса состояния бота, под разворотом — четыре блока подробностей.

       Цвет строки очереди задаёт одна переменная --dash-tone: полоска слева,
       крупная цифра, ховер-рамка и обод фокуса обязаны совпадать, а три
       отдельных правила на каждый оттенок рано или поздно разъезжаются.

       Медиазапросы живут здесь же, а не в общем блоке responsive: тот идёт по
       файлу ВЫШЕ, и при равном весе селектора десктопная раскладка дожила бы
       до телефона (та же ловушка, что у #telemetry-cards).
       ══════════════════════════════════════════════════════════════════════ */
    .dashboard-board {
      display: flex;
      flex-direction: column;
      gap: 12px;
      min-width: 0;
    }

    .dashboard-head {
      display: flex;
      align-items: baseline;
      gap: 12px;
      flex-wrap: wrap;
    }
    .dashboard-title {
      margin: 0;
      font-family: var(--font-display);
      font-size: 1.15rem;
      font-weight: 700;
      color: var(--text-main);
    }
    .dashboard-updated { font-size: 0.72rem; color: var(--text-muted); }
    .dashboard-refresh {
      margin-left: auto;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 5px 12px;
      border-radius: var(--radius-pill);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      color: var(--text-muted);
      font-size: 0.74rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .dashboard-refresh:hover { color: var(--text-main); border-color: var(--accent-indigo); }
    .dashboard-refresh:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 2px; }

    /* ── Очередь действий ───────────────────────────────────────────────── */
    .dashboard-queue { display: flex; flex-direction: column; gap: 10px; }

    .dash-queue-row {
      position: relative;
      display: grid;
      /* Первая колонка шире единицы счёта («покупателей» ≈ 72px): каждая строка
         — свой grid, и от auto заголовки соседних строк разъезжались бы по
         длине слова под числом. */
      grid-template-columns: minmax(88px, auto) minmax(0, 1fr) auto;
      align-items: center;
      gap: 18px;
      padding: 14px 18px 14px 24px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      /* Заливка непрозрачная сама по себе: у карточек кабинета blur только
         украшение, и без базового фона строка осталась бы прозрачной там, где
         backdrop-filter не поддержан. */
      background: var(--card-bg);
      overflow: hidden;
      transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
    }
    .dash-queue-row::before {
      content: '';
      position: absolute;
      left: 0; top: 0; bottom: 0;
      width: 4px;
      background: var(--dash-tone);
    }
    .dash-queue-row.is-rose { --dash-tone: var(--accent-rose); }
    .dash-queue-row.is-amber { --dash-tone: var(--accent-amber); }
    .dash-queue-row.is-indigo { --dash-tone: var(--accent-indigo); }
    .dash-queue-row.is-cyan { --dash-tone: var(--accent-cyan); }

    /* Число и единица счёта — одна колонка: «22» отдельно от «покупателей»
       читается как заявки, а заявок в этой же цифре бывает больше. */
    .dash-queue-metric {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 3px;
      min-width: 0;
    }
    .dash-queue-count {
      font-family: var(--font-display);
      font-size: 2rem;
      font-weight: 700;
      line-height: 1;
      color: var(--dash-tone);
    }
    .dash-queue-unit {
      font-size: 0.7rem;
      line-height: 1.1;
      font-weight: 500;
      letter-spacing: 0.01em;
      color: var(--text-muted);
      white-space: nowrap;
    }
    /* Ноль в очереди — законный ответ, а не сигнал: цвет уходит в приглушённый,
       и строка перестаёт звать к действию, которого нет. */
    .dash-queue-row.is-zero .dash-queue-count { color: var(--text-muted); }
    .dash-queue-row.is-zero .dash-queue-unit { opacity: 0.75; }
    .dash-queue-row.is-zero::before { opacity: 0.35; }

    .dash-queue-text { min-width: 0; }
    .dash-queue-title {
      display: block;
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .dash-queue-note {
      display: block;
      margin-top: 2px;
      font-size: 0.76rem;
      line-height: 1.4;
      color: var(--text-muted);
    }
    .dash-queue-action {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 0.78rem;
      font-weight: 600;
      color: var(--text-muted);
      white-space: nowrap;
      transition: color 0.2s ease, transform 0.2s ease;
    }
    .dash-queue-action.is-off { font-weight: 400; opacity: 0.65; white-space: normal; }

    .dash-queue-row.is-clickable { cursor: pointer; }
    .dash-queue-row.is-clickable:hover {
      transform: translateY(-2px);
      border-color: var(--dash-tone);
      box-shadow: var(--shadow-card);
    }
    .dash-queue-row.is-clickable:hover .dash-queue-action { color: var(--dash-tone); transform: translateX(2px); }
    .dash-queue-row.is-clickable:active { transform: translateY(-1px) scale(0.997); }
    .dash-queue-row.is-clickable:focus-visible {
      outline: 2px solid var(--dash-tone);
      outline-offset: 2px;
    }
    .dash-queue-skeleton { height: 74px; border-radius: var(--radius-md); }

    /* ── Свёрнутая полоса состояния бота ─────────────────────────────────── */
    .dashboard-strip {
      display: flex;
      align-items: center;
      gap: 12px;
      width: 100%;
      padding: 12px 18px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
      color: var(--text-body);
      text-align: left;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .dashboard-strip:hover { background: var(--glass-hover); border-color: var(--accent-indigo); }
    .dashboard-strip:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 2px; }

    .dashboard-strip-dot {
      flex-shrink: 0;
      width: 10px; height: 10px;
      border-radius: 50%;
      background: var(--text-muted);
    }
    .dashboard-strip-dot.is-ok { background: var(--accent-emerald); animation: dashDotPulse 2.4s ease-in-out infinite; }
    .dashboard-strip-dot.is-warn { background: var(--accent-amber); }
    .dashboard-strip-dot.is-alarm { background: var(--accent-rose); }
    .dashboard-strip-dot.is-muted { background: var(--text-muted); }
    @keyframes dashDotPulse {
      0%, 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
      50% { box-shadow: 0 0 12px 0 rgba(16, 185, 129, 0.55); }
    }

    .dashboard-strip-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
    .dashboard-strip-title {
      font-family: var(--font-display);
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--text-main);
    }
    .dashboard-strip-sub { font-size: 0.76rem; line-height: 1.4; color: var(--text-muted); }

    .dashboard-strip-action {
      margin-left: auto;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 0.78rem;
      font-weight: 600;
      color: var(--text-muted);
      white-space: nowrap;
    }
    .dashboard-strip:hover .dashboard-strip-action { color: var(--accent-indigo); }
    .dashboard-chevron { display: inline-flex; transition: transform 0.25s ease; }
    .dashboard-strip.is-open .dashboard-chevron { transform: rotate(180deg); }

    /* ── Разворот ────────────────────────────────────────────────────────── */
    .dashboard-details {
      display: flex;
      flex-direction: column;
      gap: 18px;
      animation: fadeIn 0.25s ease;
    }
    /* Обязательно: авторский display перебивает display:none от атрибута
       hidden, и свёрнутый разворот остался бы на экране (тот же приём, что у
       .digest-body[hidden]). */
    .dashboard-details[hidden] { display: none; }
    .dashboard-block-title {
      margin: 0 0 10px;
      font-family: var(--font-display);
      font-size: 0.9rem;
      font-weight: 600;
      color: var(--text-body);
    }
    .dashboard-bot {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
      gap: 14px;
      align-items: start;
    }
    .dashboard-bot .dash-note { grid-column: 1 / -1; }
    /* Блок пришёл пустым — причина занимает всю ширину: узкая плашка в первой
       колонке выглядит как недогрузившаяся карточка, а не как ответ. */
    .dashboard-bot > .dash-nodata,
    .dashboard-value > .dash-nodata { grid-column: 1 / -1; }
    .dashboard-value {
      display: grid;
      grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
      gap: 14px;
      align-items: stretch;
    }
    .dashboard-trio {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 14px;
      align-items: stretch;
    }

    .dash-card,
    .dashboard-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      min-width: 0;
      padding: 14px 16px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
    }
    .dash-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; flex-wrap: wrap; }
    .dash-card-title { font-family: var(--font-display); font-size: 0.85rem; font-weight: 600; color: var(--text-main); }
    .dash-card-note { font-size: 0.72rem; color: var(--text-muted); }
    .dash-note { margin: 0; font-size: 0.74rem; line-height: 1.45; color: var(--text-muted); }
    .dash-note strong { font-family: var(--font-display); color: var(--text-body); }
    .dash-note-tz { font-style: italic; }
    .dash-bad { color: var(--accent-rose); }
    /* «Не измеряли» никогда не рисуем нулём: пунктирная плашка с причиной
       читается как отсутствие замера, а ноль — как факт. */
    .dash-nodata {
      /* В карточке ряда из трёх плашка тянется на всю высоту: полупустая
         карточка с прижатой к верху строкой читается как недогрузившаяся. */
      flex: 1 1 auto;
      padding: 12px 14px;
      border: 1px dashed var(--glass-border);
      border-radius: var(--radius-md);
      font-size: 0.78rem;
      line-height: 1.5;
      color: var(--text-muted);
    }

    /* Суточный ряд: инлайновый SVG, без библиотек графиков. */
    .dash-spark { display: block; width: 100%; height: 56px; }
    .dash-spark-area { fill: rgba(99, 102, 241, 0.16); stroke: none; }
    .dash-spark-line {
      fill: none;
      stroke: var(--accent-indigo);
      stroke-width: 2;
      stroke-linejoin: round;
      stroke-linecap: round;
      /* Ряд растянут по ширине карточки (preserveAspectRatio="none") — без
         этого линия растолстела бы вместе с ним. */
      vector-effect: non-scaling-stroke;
    }
    .dash-spark-dot { fill: var(--accent-cyan); }
    .dash-spark-axis { display: flex; justify-content: space-between; font-size: 0.66rem; color: var(--text-muted); }
    .dash-spark-empty { font-size: 0.78rem; color: var(--text-muted); }
    .dash-activity-last {
      display: flex;
      justify-content: space-between;
      gap: 10px;
      flex-wrap: wrap;
      padding-top: 8px;
      border-top: 1px dashed var(--glass-border);
      font-size: 0.76rem;
      color: var(--text-muted);
    }
    .dash-activity-last strong { font-family: var(--font-display); font-weight: 600; color: var(--text-body); }

    .dash-tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
    .dash-tile {
      display: flex;
      flex-direction: column;
      gap: 4px;
      min-width: 0;
      padding: 12px 14px;
      border-radius: var(--radius-md);
      border: 1px solid var(--glass-border);
      background: var(--card-bg);
    }
    .dash-tile-label { font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
    .dash-tile-value {
      font-family: var(--font-display);
      font-size: 1.5rem;
      font-weight: 700;
      line-height: 1.1;
      color: var(--text-main);
      overflow-wrap: anywhere;
    }
    /* Знаменатель стоит рядом с числом и мельче него: «75» без «из 98»
       прочитают как конверсию в продажу. */
    .dash-tile-of { font-size: 0.85rem; font-weight: 500; color: var(--text-muted); }
    .dash-tile-sub { font-size: 0.72rem; line-height: 1.4; color: var(--text-muted); }

    .dash-offhours-figure { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
    .dash-offhours-value {
      font-family: var(--font-display);
      font-size: 2.4rem;
      font-weight: 800;
      line-height: 1;
      color: var(--accent-emerald);
    }
    .dash-offhours-text { flex: 1 1 260px; min-width: 0; font-size: 0.8rem; line-height: 1.5; color: var(--text-body); }
    .dash-figure { display: flex; flex-direction: column; gap: 2px; }
    .dash-figure-value { font-family: var(--font-display); font-size: 1.6rem; font-weight: 700; line-height: 1.1; color: var(--text-main); }
    .dash-figure-sub { font-size: 0.74rem; line-height: 1.4; color: var(--text-muted); }

    .dash-pair { display: flex; gap: 18px; flex-wrap: wrap; }
    .dash-pair-item { font-size: 0.76rem; color: var(--text-muted); }
    .dash-pair-item strong { font-family: var(--font-display); font-size: 1.25rem; color: var(--text-main); margin-right: 4px; }

    /* Текст предупреждения сверки — ДОСЛОВНО с сервера, поэтому переносим как
       есть и не режем многоточием: в нём и лежит весь смысл. */
    .dash-warn {
      margin: 0;
      padding: 10px 12px;
      border-radius: var(--radius-sm);
      border: 1px solid rgba(245, 158, 11, 0.35);
      background: rgba(245, 158, 11, 0.1);
      color: var(--text-body);
      font-size: 0.74rem;
      line-height: 1.45;
      overflow-wrap: anywhere;
    }
    .dash-warn-label {
      display: block;
      margin-bottom: 4px;
      font-size: 0.66rem;
      font-weight: 700;
      letter-spacing: 0.04em;
      text-transform: uppercase;
      color: var(--accent-amber);
    }

    /* Часы обращений: 24 полосы флексом. Час без сообщений — ВИДИМЫЙ нулевой
       столбик, а не пропуск: дыра в ряду читается как «не считали». */
    .dash-hours { display: flex; align-items: flex-end; gap: 2px; height: 76px; }
    .dash-hour { flex: 1 1 0; min-width: 0; display: flex; align-items: flex-end; height: 100%; }
    .dash-hour-bar {
      display: block;
      width: 100%;
      border-radius: 2px;
      background: var(--text-muted);
      opacity: 0.45;
      transition: opacity 0.2s ease;
    }
    .dash-hour.is-off .dash-hour-bar { background: var(--accent-emerald); opacity: 0.85; }
    .dash-hour.is-zero .dash-hour-bar { opacity: 0.25; }
    .dash-hour:hover .dash-hour-bar { opacity: 1; }
    .dash-hours-axis { display: flex; justify-content: space-between; font-size: 0.66rem; color: var(--text-muted); }

    .dash-bar { display: flex; height: 10px; border-radius: var(--radius-pill); overflow: hidden; background: var(--glass-border); }
    .dash-bar-seg.is-ok { background: var(--accent-emerald); }
    .dash-bar-seg.is-bad { background: var(--accent-rose); }
    .dash-bar-seg.is-muted { background: var(--text-muted); opacity: 0.55; }
    .dash-legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 0.72rem; color: var(--text-muted); }
    .dash-legend-item { display: inline-flex; align-items: center; gap: 6px; }
    .dash-legend-item::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
    .dash-legend-item.is-ok::before { background: var(--accent-emerald); }
    .dash-legend-item.is-bad::before { background: var(--accent-rose); }
    .dash-legend-item.is-muted::before { background: var(--text-muted); }

    /* Плашка фильтра, пришедшего из очереди действий. */
    .leads-queue-filter {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
      margin-bottom: 14px;
      padding: 10px 14px;
      border-radius: var(--radius-md);
      border: 1px solid rgba(99, 102, 241, 0.35);
      background: rgba(99, 102, 241, 0.1);
      font-size: 0.8rem;
      color: var(--text-body);
    }
    .leads-queue-filter-label,
    .leads-queue-filter-count { font-size: 0.74rem; color: var(--text-muted); }
    .leads-queue-filter-reset {
      margin-left: auto;
      padding: 4px 12px;
      border-radius: var(--radius-pill);
      border: 1px solid var(--glass-border);
      background: transparent;
      color: var(--text-body);
      font: inherit;
      font-size: 0.74rem;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .leads-queue-filter-reset:hover { border-color: var(--accent-indigo); color: var(--text-main); }
    .leads-queue-filter-reset:focus-visible { outline: 2px solid var(--accent-indigo); outline-offset: 2px; }

    [data-theme="light"] .dash-spark-area { fill: rgba(99, 102, 241, 0.14); }
    [data-theme="light"] .dash-hour-bar { opacity: 0.35; }
    [data-theme="light"] .dash-hour.is-zero .dash-hour-bar { opacity: 0.2; }

    @media (max-width: 1200px) {
      .dashboard-bot { grid-template-columns: minmax(0, 1fr); }
      .dash-queue-count { font-size: 1.8rem; }
    }
    @media (max-width: 1024px) {
      .dashboard-value { grid-template-columns: minmax(0, 1fr); }
      .dashboard-trio { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    @media (max-width: 768px) {
      .dashboard-trio { grid-template-columns: minmax(0, 1fr); }
      .dashboard-updated { display: none; }
      .dash-offhours-value { font-size: 2rem; }
    }
    @media (max-width: 640px) {
      /* Три плитки бота держатся до последнего: разговоры с заявкой и оценка
         теряют смысл поодиночке — их читают вместе. */
      .dash-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    @media (max-width: 560px) {
      /* Телефон: действие уезжает под текст строки, а не жмётся к нему в третью
         колонку — иначе подпись очереди сжимается до многоточия. */
      .dash-queue-row {
        grid-template-columns: minmax(74px, auto) minmax(0, 1fr);
        gap: 6px 14px;
        padding: 12px 14px 12px 18px;
      }
      .dash-queue-count { font-size: 1.6rem; }
      /* Телефон: единица счёта не переносится и не жмёт текстовую колонку —
         подпись «покупателей» короче любого заголовка строки. */
      .dash-queue-unit { font-size: 0.66rem; }
      .dash-queue-action { grid-column: 1 / -1; }
      .dashboard-strip { align-items: flex-start; padding: 12px 14px; }
      .dashboard-strip-dot { margin-top: 6px; }
      .dashboard-strip-link { display: none; }
      .dashboard-strip-action { margin-left: auto; }
      .dash-tiles { grid-template-columns: minmax(0, 1fr); }
      .dashboard-title { font-size: 1.02rem; }
      .dash-hours { height: 60px; }
      .leads-queue-filter-reset { margin-left: 0; }
    }
    @media (prefers-reduced-motion: reduce) {
      .dash-queue-row.is-clickable:hover { transform: none; }
      .dash-queue-row.is-clickable:hover .dash-queue-action { transform: none; }
      .dashboard-strip-dot.is-ok { animation: none; }
      .dashboard-details { animation: none; }
    }
