
        /* Значения токенов. Менять палитру — ЗДЕСЬ, а не в view_N_X.js.
           Слои идут строго от тёмного к светлому: чем «выше» поверхность, тем светлее.

           ⚠️ Главная правка 07.08.2026 — ПОЧИНЕНА ПЕРЕВЁРНУТАЯ ИЕРАРХИЯ. Раньше inset
           (#0D1117) был ТЕМНЕЕ фона приложения (#0E1117), то есть поле ввода внутри
           карточки сливалось с фоном всего экрана. Именно это читалось как «плоскость»,
           а не отсутствие слоёв — слои были, но стояли не по порядку.

           Ориентир по средней яркости канала: app 16 → inset 23 → panel 31 → rowhover 40
           → edge 50. Разница между соседними ступенями ~8 пунктов: слой отличим, но
           интерфейс не становится «серым». */
        :root {
            --c-app:         11 15 22;   /* #0B0F16 — фон рабочей области, самый тёмный */
            --c-inset:       16 22 31;   /* #10161F — поля ввода, KPI, шапки таблиц */
            --c-panel:       23 30 41;   /* #171E29 — карточки, панели, модалки */
            --c-rowhover:    29 38 52;   /* #1D2634 — подсветка строки таблицы */
            /* Фирменный синий #1F4E78 (он же в печатных бланках) осветлён до #2E6BA8: на новом,
               более светлом фоне панелей прежний выглядел глуховато. Контраст с белым текстом
               5.3:1 — норма AA сохраняется. */
            --c-accent:        46 107 168; /* #2E6BA8 — заливки: активная вкладка, hover пунктов меню */
            --c-accent-bright: 74 144 217; /* #4A90D9 — тонкие индикаторы: полоса и иконка активного пункта */
            --c-warn-surface:   42 26 14;  /* #2A1A0E — фон блока-предупреждения (тёплый) */
            --c-danger-surface: 42 18 21;  /* #2A1215 — фон блока-ошибки */
            /* ⚠️ Приглушённый текст. Прежний gray-500 (#6B7280) давал контраст 3.5 при норме
               WCAG AA 4.5 — подписи полей и KPI формально были нечитаемы, а их в проекте 309
               штук на кегле 11px. Значение подобрано расчётом контраста, а не на глаз.
               ⚠️ Правился второй раз (09.08.2026) ВМЕСТЕ с faint. По отдельности faint поднять
               было нельзя: он дошёл бы до 4.58 против 4.93 у muted, и два уровня шкалы стали бы
               неразличимы — подпись «Договор» перестала бы отличаться от прочерка под ней.

               ⚠️ РАЗРЫВ МЕЖДУ УРОВНЯМИ СВЕРЯТЬ ПО L* (перцептивной светлоте), А НЕ ПО КОНТРАСТУ.
               Первая попытка развела их «по контрасту» (muted 6.11 против faint 4.58 — выглядит
               убедительно) и при этом посадила muted вплотную к ink-3: 64.4 L* против 66.8, то
               есть 2.4 L* при пороге различимости ~4. Глазу это два одинаковых серых. Действующая
               шкала тёмной темы: ink-2 85 → ink-3 67 → muted 61 → faint 56 L*. */
            --c-muted:          139 149 163; /* #8B95A3 */
            /* Текст. Значения = ровно те цвета, что были вшиты классами, поэтому переход на
               токены не меняет тёмную тему ни на пиксель. */
            --c-ink:         255 255 255; /* было text-white */
            --c-on-accent:   255 255 255; /* текст на цветных кнопках — белый в ОБЕИХ темах */
            --c-ink-2:       209 213 219; /* было text-gray-300 */
            --c-ink-3:       156 163 175; /* было text-gray-400 */
            /* ⚠️ Поднимался ДВАЖДЫ, и оба раза по одной причине — за этим токеном стоят не
               только «выключенные элементы», ради которых он заводился, но и ДАННЫЕ: остатки
               счетов «0.00» в Плане счетов, прочерки в реестрах, пояснения под фильтрами.
               07.08.2026: gray-600 (#4B5563) → #737D8B, контраст 2.2 → 4.0 («Нет движений за
               период» было фактически нечитаемо). 09.08.2026: → #7C8794, 4.0 → 4.58, то есть
               выше нормы AA. Второй шаг сделан ВМЕСТЕ с подъёмом muted — см. комментарий там:
               порознь эти два уровня шкалы схлопнулись бы в один. */
            --c-faint:       124 135 148; /* #7C8794 */
            /* Семантика статусов */
            --c-good:         52 211 153; /* было text-emerald-400 */
            --c-bad:         248 113 113; /* было text-red-400 */
            --c-warn:        251 191  36; /* было text-amber-400 */
            --c-info:         96 165 250; /* было text-blue-400 */
            --c-alt:         192 132 252; /* purple-400 — «особый случай»: внешние обстоятельства, продление ТС */
            /* Наведение: в тёмной теме — на ступень СВЕТЛЕЕ (оттенки -300, как было вшито) */
            --c-good-hover:  110 231 183; /* emerald-300 */
            --c-bad-hover:   252 165 165; /* red-300 */
            --c-warn-hover:  252 211  77; /* amber-300 */
            --c-info-hover:  147 197 253; /* blue-300 */
            /* Сплошные заливки кнопок. Значения = ровно те, что были вшиты классами, поэтому
               переход на токены не меняет тёмную тему ни на пиксель. ⚠️ bg-emerald-600 и
               bg-emerald-700 (равно как bg-blue-600 и bg-blue-700) исторически соседствовали
               без всякого смыслового различия — сведены к одному тону: два оттенка одной кнопки
               читаются как «разные кнопки», а не как оттенки. */
            /* ⚠️ #05835C, а не emerald-600 (#059669), с которого проект начинал (решение
               пользователя 09.08.2026 по сравнению трёх вариантов вживую). Прежний давал с
               белым текстом 3.77 при норме AA 4.5 для 12px — то есть подпись на ГЛАВНОЙ
               кнопке («Сохранить», «Провести», «Активировать», ~50 мест) формально была
               нечитаема. Сдвиг на 2 % яркости: тёмная тема на глаз осталась прежней, норма
               достигнута (4.77). Светлой темы это НЕ касается — там свой --c-good-solid. */
            --c-good-solid:        5 131  92; /* #05835C — белый текст 4.77 */
            --c-good-solid-hover: 11 160 111; /* #0BA06F — наведение по-прежнему ОСВЕТЛЯЕТ */
            --c-bad-solid:       185  28  28; /* red-700 */
            --c-bad-solid-hover: 220  38  38; /* red-600 */
            --c-info-solid:       37  99 235; /* blue-600 */
            --c-info-solid-hover: 59 130 246; /* blue-500 */
            --c-overlay:           0   0   0; /* вуаль под модалкой — чёрная, как было */
            --c-body-ink:        227 231 237; /* #E3E7ED — прежнее значение body { color } */
            --c-scroll:          255 255 255; /* тон ползунка скроллбара */
            /* Тонкая линия ПОВЕРХ фона: рамка матового стекла, фоновая сетка. ⚠️ Отдельный
               токен, а не --c-scroll с тем же значением: у ползунка своя судьба, и правка
               скроллбара не должна утаскивать за собой рамки. Всегда с прозрачностью. */
            --c-hairline:        255 255 255;
            /* Фирменный салатовый WIGO (#58D818 — акцент сайта wigorent.ae). Используется
               ТОЛЬКО как цвет знака «W» на экране входа, семантики в интерфейсе не несёт.
               ⚠️ Не путать с запретом на лайм #97D700 из раздела про Manrope: там речь про
               ПОДЛОЖКУ под белый текст (контраст 1.75), здесь — глиф на тёмном фоне. */
            --c-brand:            88 216  24;
            /* Нейтральные заливки: бейджи и служебные кнопки. ⚠️ surface-3 — это состояние
               hover для surface-2, поэтому в тёмной теме он СВЕТЛЕЕ, а в светлой будет ТЕМНЕЕ:
               наведение обязано давать контраст с фоном в обе стороны. */
            --c-surface-2:    31  41  55; /* было bg-gray-800 */
            --c-surface-3:    55  65  81; /* было bg-gray-700 */
            /* Сайдбар. ⚠️ Эти значения ОДИНАКОВЫ в обеих темах — навигация остаётся тёмной
               и в светлой теме (решение пользователя 07.08.2026). Поэтому и цвета текста в
               правилах .sidebar-* ниже намеренно оставлены хексами: они тоже не зависят от темы. */
            --c-nav:         23  30  41; /* совпадает с нынешним panel */
            --c-nav-2:       11  15  22; /* шапка сайдбара, чуть глубже */
            --c-nav-edge:    37  48  64;
            --c-nav-ink:    255 255 255;
            --c-nav-muted:  130 140 156;
            /* Цветовые якоря блоков меню (08.08.2026). Приглушённые пастельные тона:
               меню было монолитным, и глазу не за что было зацепиться при быстром
               сканировании. Значения — КАНАЛАМИ, как остальные токены: цвет нужен и
               заливкой (иконка корня), и с прозрачностью (иконки вложенных, полоса
               группы), а с готовым хексом подставить альфу нельзя.

               ⚠️ Насыщенность подобрана так, чтобы якорь читался, но не спорил с
               --c-accent-bright: синий остаётся признаком АКТИВНОГО пункта, цвет
               блока — признаком ПРИНАДЛЕЖНОСТИ. Две разные роли, два разных канала.
               Поэтому среди якорей нет чистого синего. */
            --c-grp-tasks:      210 188 138; /* песочный  — Центр задач */
            --c-grp-leasing:    134 168 207; /* стальной  — Лизинг */
            --c-grp-reporting:  168 153 214; /* лавандовый — Управленческая отчётность */
            --c-grp-accounting: 111 179 168; /* бирюзовый — Бухгалтерия */
            --c-grp-purchases:  220 160 122; /* персиковый — Purchases */
            --c-grp-sales:      143 191 143; /* шалфейный — Sales */
            --c-grp-settings:   153 166 181; /* серо-стальной — Настройки */
            --c-edge:        37 48 64;   /* #253040 — граница панели: прежняя #1F2937 с ней почти сливалась */
            --c-edge-strong: 55 65 81;   /* #374151 — границы кнопок и меню */
            --c-field:       75 85 99;   /* #4B5563 — границы полей ввода */
        }

        /* ── СВЕТЛАЯ ТЕМА (07.08.2026) ───────────────────────────────────────
           Включается атрибутом data-theme="light" на <html> — его ставит скрипт
           в <head> ДО первого рендера, иначе кадр мигнёт тёмным.

           ⚠️ Значения подобраны РАСЧЁТОМ контраста по WCAG, а не на глаз: каждый
           текстовый токен даёт ≥4.5 на самом светлом из своих фонов. `faint` —
           единственное исключение (3.5), он служебный: выключенные элементы и
           плейсхолдеры, к ним норма основного текста не применяется.

           ⚠️ Здесь НЕТ nav* и on-accent, и это осознанно. Сайдбар остаётся тёмным
           (решение пользователя), а текст на цветных кнопках — белым: под ним не
           фон страницы, а насыщенная заливка кнопки. */
        :root[data-theme="light"] {
            /* ⚠️ ЛЕСТНИЦА СЛОЁВ ЗЕРКАЛЬНА ТЁМНОЙ: там она идёт от тёмного к светлому, здесь —
               от светлого к ещё более светлому, и самая «высокая» поверхность (panel) должна
               быть белой. Прежние значения (app 244 / inset 247 / panel 255) разводили фон
               рабочей области и белую карточку всего на 11 пунктов: карточки не «поднимались»
               над фоном, а шапка таблицы (inset) на белой панели была практически неотличима
               от неё (6 пунктов). Разведено до 18 и 14 — слои читаются, но интерфейс не сереет. */
            --c-app:         237 240 245; /* #EDF0F5 — фон рабочей области */
            --c-inset:       241 244 248; /* #F1F4F8 — поля ввода, KPI, шапки таблиц */
            --c-panel:       255 255 255; /* #FFFFFF — карточки и панели */
            --c-rowhover:    238 242 247; /* #EEF2F7 — подсветка строки */
            --c-accent:       31  78 120; /* #1F4E78 — фирменный синий, он же в печатных бланках */
            --c-accent-bright: 46 107 168;
            --c-warn-surface: 254 243 199;
            --c-danger-surface: 254 226 226;
            --c-muted:        87  95 109; /* #575F6D — 40 L* */
            --c-ink:          22  28  36; /* #161C24 — 15.8 */
            --c-ink-2:        58  69  82; /* #3A4552 — 9.0 */
            --c-ink-3:        75  84  98; /* #4B5462 — 35 L* */
            /* ⚠️ Поднят с #78848F (3.5 — исходно «служебный, к нему норма не применяется»).
               Замер по 28 экранам показал, что за этим токеном стоят НЕ только плейсхолдеры:
               им же покрашены остатки счетов «0.00» в Плане счетов, прочерки в реестрах и
               пояснения под фильтрами — то есть данные, которые читают.
               ⚠️ Вся нижняя половина шкалы (ink-3 / muted / faint) пересчитана 09.08.2026 по
               L*, а не по контрасту: muted и faint расходились всего на 1.6 L* при пороге
               различимости ~4, то есть подпись и приглушённое значение выглядели одинаково.
               Действующая шкала светлой темы: ink-2 29 → ink-3 35 → muted 40 → faint 46 L*.
               ⚠️ ПОТОЛОК СЧИТАЕТСЯ ПО САМОМУ ТЁМНОМУ ИЗ СВЕТЛЫХ ФОНОВ — по фону рабочей области
               (app #EDF0F5), а не по белой панели: там контраст наименьший. faint на нём даёт
               ровно 4.52; поднимешь ещё — уйдёт ниже нормы на дашборде и в шапке фильтров,
               опустишь — схлопнется разрыв с muted. Дальше шкалу можно двигать только затемняя
               верхние уровни (ink-2/ink-3). */
            --c-faint:       101 110 123; /* #656E7B — 46 L*, 4.52 на фоне рабочей области */
            /* ── СЕМАНТИКА ТЕКСТА. ⚠️ Проверяется НЕ на белом, а на СВОЕЙ подложке-бейдже
               (bg-{тон}/15): именно там текст и живёт. Прежний good (emerald-700 #047857) давал
               на своей подложке 4.44 — формально ниже нормы, и визуально «жидкий» зелёный. */
            --c-good:         11 107  79; /* #0B6B4F — 6.5 на белом, 5.1 на подложке бейджа */
            --c-bad:         185  28  28; /* red-700 */
            --c-warn:        146  64  14; /* amber-800 — -700 не добирал контраста на фоне бейджа (4.47) */
            --c-info:         29  78 216; /* blue-700 */
            --c-alt:         109  40 217; /* violet-700 — «особый случай» (был purple-400: контраст 1.9) */
            /* Наведение в светлой теме — ТЕМНЕЕ базового (зеркально тёмной теме) */
            --c-good-hover:    7  84  62; /* на ступень темнее good */
            --c-bad-hover:   153  27  27; /* red-800 */
            --c-warn-hover:  120  53  15; /* amber-900 */
            --c-info-hover:   30  64 175; /* blue-800 */
            /* ── СПЛОШНЫЕ ЗАЛИВКИ КНОПОК. Здесь работает ОБРАТНОЕ правило: текст на них белый
               (--c-on-accent), поэтому заливка обязана быть достаточно ТЁМНОЙ. Прежние вшитые
               значения не годились дважды: emerald-600 давал с белым 3.05 (ниже AA для 11-12px),
               а на белом фоне читался как неоновый — та самая «режущая глаз» зелень. */
            --c-good-solid:        6 122  85; /* #067A55 — белый текст 5.4 */
            --c-good-solid-hover:  4 100  70;
            --c-bad-solid:       185  28  28; /* #B91C1C — белый текст 6.5 */
            --c-bad-solid-hover: 153  27  27;
            /* ⚠️ Синяя заливка в светлой теме = ФИРМЕННЫЙ синий, тот же, что у активной вкладки
               и в печатных бланках. Второй, не связанный с ним синий (blue-600/700) на белом фоне
               читается как чужой: в тёмной теме разница тонула в общей темноте, на светлом —
               видна сразу. Тёмная тема при этом не менялась (см. запрет на «третий синий»). */
            --c-info-solid:       31  78 120; /* #1F4E78 — белый текст 8.6 */
            --c-info-solid-hover: 22  58  92;
            /* Вуаль под модалкой — не чёрная, а холодно-серая (slate-700): переход от белой
               страницы к почти чёрному оверлею и обратно бьёт по глазам сильнее, чем сама модалка. */
            --c-overlay:      51  65  85;
            --c-body-ink:     58  69  82; /* = ink-2 */
            --c-scroll:       30  41  59; /* ползунок — тёмный: на белой панели белый был невидим */
            --c-hairline:     16  24  40; /* линия поверх фона — на белом тоже тёмная */
            /* ⚠️ Фирменный лайм в светлой теме ЗАТЕМНЁН: замер #58D818 на белой карточке
               даёт 1.87 — знак на ней практически исчезает. Тон сохранён, светлота ниже. */
            --c-brand:        62 156  16;
            --c-surface-2:   237 240 244; /* бейджи «Черновик» */
            --c-surface-3:   225 230 236; /* их hover */
            --c-edge:        228 232 238;
            --c-edge-strong: 210 217 226;
            --c-field:       196 204 216;
        }

        /* ── ФИРМЕННЫЙ ШРИФТ MANROPE (добавлено 08.08.2026) ──────────────────
           Раздаётся ЛОКАЛЬНО из lib/fonts/manrope, а не с fonts.googleapis.com —
           §1: ERP не зависит от доступности внешних CDN.

           ⚠️ Файла ДВА, а не восемь, и это не экономия на начертаниях: Manrope —
           ВАРИАТИВНЫЙ шрифт, один файл на подмножество несёт весь диапазон
           400-800. Проверено сравнением контрольных сумм — Google на запрос
           четырёх начертаний отдаёт четыре ссылки на физически одинаковые
           файлы. Поэтому здесь `font-weight: 400 800` (диапазон), а не по
           одному правилу на вес: 39 КБ вместо 157 КБ.

           ⚠️ unicode-range обязателен и перенесён от Google как есть. Без него
           браузер тянул бы латинский файл ради кириллического текста (и
           наоборот), то есть качал бы вдвое больше и подставлял глифы не из
           того подмножества. Символы вне обоих диапазонов — эмодзи в текстах
           appAlert/appConfirm, «▾» в меню действий, «✕» в модалках — падают
           на системный шрифт, и это правильно: своих глифов у Manrope для них
           нет, а системные отрисуются штатно.

           font-display: swap — до загрузки шрифта текст показывается системным,
           а не исчезает: интерфейс, который вбивают с клавиатуры, не должен
           моргать пустотой на старте. */
        @font-face {
            font-family: 'Manrope'; font-style: normal; font-weight: 400 800; font-display: swap;
            src: url('lib/fonts/manrope/manrope-cyrillic.woff2') format('woff2');
            unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
        }
        @font-face {
            font-family: 'Manrope'; font-style: normal; font-weight: 400 800; font-display: swap;
            src: url('lib/fonts/manrope/manrope-latin.woff2') format('woff2');
            unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                           U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                           U+2212, U+2215, U+FEFF, U+FFFD;
        }

        /* ⚠️ Цвет текста body — ТОКЕН, а не хекс. Он вшитым и остался после токенизации 07.08.2026,
           то есть в светлой теме всякий текст БЕЗ собственного класса (а такие места есть: подписи
           внутри вёрстки, текстовые узлы между элементами) наследовал почти белый #E3E7ED и
           пропадал на белой панели. В тёмной теме значение прежнее до байта. */
        body { background-color: rgb(var(--c-app)); color: rgb(var(--c-body-ink)); font-family: 'Manrope', system-ui, sans-serif; margin: 0; padding: 0; }

        /* ⚠️ hover:brightness-125 ОСВЕТЛЯЕТ карточку — в тёмной теме это заметный отклик, в
           светлой карточка уже белая, и осветлять её некуда: наведение переставало давать
           обратную связь вовсе (карточки «требует внимания» в Клиентах, Автопарке, PA, VRA,
           Покупателях, Кассе, Взаиморасчётах — они кликабельны и фильтруют реестр).
           Селектор «атрибут темы + класс» (0,2,0) сильнее утилиты Tailwind (0,1,0), поэтому
           !important не нужен — то же правило специфичности, что у чекбоксов и плотности. */
        :root[data-theme="light"] .hover\:brightness-125:hover { filter: brightness(0.965); }

        /* ── ТАБЛИЧНЫЕ ЦИФРЫ (добавлено 07.08.2026) ──────────────────────────
           В Segoe UI цифры пропорциональные: «1» уже «8». Поэтому колонки сумм
           в реестрах и отчётах не выстраивались в ровный столбик — разряды
           «плавали» от строки к строке, и глаз спотыкался на каждой. Для системы,
           где человек весь день сверяет числа, это ощутимее любого оттенка фона.

           Применяется ГЛОБАЛЬНО, а не точечно на числовые ячейки: сумм, дат,
           номеров документов и процентов в проекте тысячи, и точечные классы
           неминуемо разъехались бы. На буквы свойство не влияет.

           input/select/textarea/button перечислены явно — у элементов форм
           собственный шрифт, они не наследуют его от body. */
        body, input, select, textarea, button, table { font-variant-numeric: tabular-nums; }

        /* ⚠️ Шрифт элементам форм задаётся ОТДЕЛЬНО: у input/select/textarea/button
           собственный системный шрифт, от body они его не наследуют — тот же список и
           та же причина, что строкой выше у табличных цифр. Без этого правила поля
           ввода и кнопки остались бы на Segoe UI посреди набранного Manrope интерфейса. */
        input, select, textarea, button { font-family: inherit; }

        /* ── ВИДИМЫЙ ФОКУС КЛАВИАТУРЫ (добавлено 07.08.2026) ─────────────────
           В проекте 436 мест с классом `outline-none` и почти ни одного
           `focus:`-стиля взамен: браузерный контур фокуса был снят и ничем не
           заменён. При заполнении формы с клавиатуры (Tab между полями) не было
           видно, в каком поле стоит курсор — а документы в ERP вбивают именно
           с клавиатуры, мышью только открывают.

           ⚠️ Правится ОДНИМ правилом, а не 436 классами: селектор «тег +
           псевдокласс» (0,1,1) специфичнее класса `.outline-none` (0,1,0),
           поэтому перебивает его без !important и без правки модулей.

           ⚠️ Именно :focus-visible, а не :focus — браузер показывает кольцо
           только при навигации с клавиатуры. При клике мышью подсветки нет,
           то есть привычная работа мышью не меняется совсем. */
        input:focus-visible,
        select:focus-visible,
        textarea:focus-visible,
        button:focus-visible,
        a:focus-visible,
        [tabindex]:focus-visible {
            outline: 2px solid rgb(var(--c-accent-bright));
            outline-offset: 1px;
        }

        /* ── Скроллбар тёмной темы ───────────────────────────────────────────
           Дефолтный скроллбар Chrome — светло-серая полоса: на #0E1117/#161B22
           она читается как дефект вёрстки, особенно в сайдбаре. Стилизуем
           ГЛОБАЛЬНО, а не только сайдбар: в приложении десятки прокручиваемых
           областей (рабочая зона, модалки, панель уведомлений, длинные
           таблицы) — точечные классы разъехались бы. Firefox — через
           scrollbar-width/scrollbar-color, Chrome/Edge — через ::-webkit-*.
           Дорожка прозрачная (берёт фон родителя), ползунок — полупрозрачный
           белый, чтобы работать и на #161B22, и на #0E1117 без второго набора
           правил; при наведении светлеет. */
           ⚠️ Тон ползунка — переменная --c-scroll, а не вшитый белый (правка 09.08.2026).
           Полупрозрачный БЕЛЫЙ ползунок в светлой теме лежал на белой панели и был
           невидим: пользователь не видел ни того, что область прокручивается, ни за что
           её тащить. Переменная наследуется, поэтому ТЁМНЫЙ САЙДБАР (он остаётся тёмным
           и в светлой теме) переопределяет её у себя одной строкой — второго набора
           правил ::-webkit-* не нужно. */
        * { scrollbar-width: thin; scrollbar-color: rgb(var(--c-scroll) / 0.20) transparent; }
        ::-webkit-scrollbar { width: 10px; height: 10px; }
        ::-webkit-scrollbar-track { background: transparent; }
        ::-webkit-scrollbar-thumb {
            background-color: rgb(var(--c-scroll) / 0.20);
            border-radius: 999px;
            /* прозрачная рамка + background-clip дают визуальную ширину ~6px
               внутри 10px-дорожки — ползунок не липнет к краю контента */
            border: 2px solid transparent;
            background-clip: content-box;
        }
        ::-webkit-scrollbar-thumb:hover { background-color: rgb(var(--c-scroll) / 0.35); }
        ::-webkit-scrollbar-corner { background: transparent; }
        /* Сайдбар тёмный в обеих темах — ползунок в нём всегда светлый. */
        .app-nav { --c-scroll: 255 255 255; }

        /* ⚠️ Место под скроллбар рабочей области резервируется ВСЕГДА (08.08.2026).
           Без этого шапка «дышит» на 5px: у длинного модуля (Автопарк, План счетов,
           Главная) скроллбар есть и сужает область, у короткого (Договоры VRA с
           пустым реестром) — нет. Замерено: правый блок шапки стоял на 570.6 против
           575.6. Это остаток той же болезни, ради которой модульные кнопки унесли из
           шапки в ряд действий, поэтому лечится здесь, а не подгонкой отступов.
           Плата — постоянные ~5px ширины; для тесных реестров (Автопарк, 11 колонок)
           это не потеря: они и так всегда со скроллбаром. */
        .app-workspace { scrollbar-gutter: stable; }

        /* ── Чекбоксы ────────────────────────────────────────────────────────
           Дефолтный чекбокс Chrome — белый квадрат: на тёмном фоне он самый
           светлый элемент таблицы и перетягивает внимание с данных. Стилизуем
           ГЛОБАЛЬНО, тем же приёмом, что скроллбар и :focus-visible выше — в
           проекте 34 чекбокса в 16 модулях, и точечные классы в разметке
           неизбежно разъехались бы (в Bills красиво, в Плане счетов нет).

           ⚠️ Плагин @tailwindcss/forms для этого не подходит: §1 требует
           локальных библиотек, а локальная Play-сборка Tailwind плагины не
           подключает. Поэтому вручную: appearance-none + своя галочка.

           ⚠️ Цвет — токен --c-accent-bright (тот же, что у кольца фокуса и
           активного пункта сайдбара). Хардкод-хексов в проекте не осталось ни
           одного, а «фирменный» #1F4E78 — удалённый второй синий из старых
           вкладок: третий синий не заводим (см. раздел про токены в CLAUDE.md).
           На bg-panel он к тому же почти сливается с фоном.

           Галочка — background-image со встроенным SVG (не ::before): у
           input нет содержимого, псевдоэлементы к нему не применяются. */
        /* ⬆️⬇️ СТРЕЛОЧКИ-СПИННЕР У ЧИСЛОВЫХ ПОЛЕЙ — ПОГАШЕНЫ ГЛОБАЛЬНО (11.08.2026, замечание
           пользователя по полю «Кол-во»). Это ровно тот визуальный шум, ради которого в проекте
           заведён MoneyInput (см. CLAUDE.md): для денег он их убрал, а у полей-исключений —
           количество, курсы валют, процентные ставки, счётчики — они так и остались.

           ⚠️ Правило ОДНО и глобальное, а не класс в разметке: <input type="number"> стоит в
           десятке форм (кол-во, «Курс к AED», ставка по займу, срок в месяцах, база начисления),
           и точечные классы неизбежно разъедутся — тот же довод, по которому глобальными сделаны
           чекбоксы ниже, скроллбар и кольцо :focus-visible.
           ⚠️ Нужны ОБА правила: WebKit прячет спиннер только через ::-webkit-*-spin-button, а
           Firefox — только через appearance: textfield на самом поле. Ввод с клавиатуры и стрелки
           ↑/↓ при этом продолжают работать — исчезает лишь нарисованный контрол. */
        input[type="number"]::-webkit-outer-spin-button,
        input[type="number"]::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }
        input[type="number"] { appearance: textfield; -moz-appearance: textfield; }

        input[type="checkbox"] {
            appearance: none;
            -webkit-appearance: none;
            flex: none;
            width: 1rem;
            height: 1rem;
            /* --c-field, а не --c-edge: чекбокс — поле ввода, и его рамка должна
               совпадать с рамкой соседних input/select в той же форме. */
            border: 1px solid rgb(var(--c-field));
            border-radius: 0.25rem;
            background-color: transparent;
            cursor: pointer;
            transition: background-color 0.15s, border-color 0.15s;
        }
        input[type="checkbox"]:hover:not(:disabled) { border-color: rgb(var(--c-accent-bright)); }
        input[type="checkbox"]:checked,
        input[type="checkbox"]:indeterminate {
            background-color: rgb(var(--c-accent-bright));
            border-color: rgb(var(--c-accent-bright));
            background-repeat: no-repeat;
            background-position: center;
            background-size: 100% 100%;
        }
        input[type="checkbox"]:checked {
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-6'/%3E%3C/svg%3E");
        }
        /* Частичный выбор («выбрано 3 из 12») — горизонтальная черта вместо
           галочки. Ставится из JS через ref.indeterminate, атрибута в HTML нет. */
        input[type="checkbox"]:indeterminate {
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
        }
        input[type="checkbox"]:disabled { opacity: 0.4; cursor: not-allowed; }

        /* ── РЕЖИМ ПЛОТНОСТИ «ПРОСТОРНЫЙ» (08.08.2026) ───────────────────────
           Второй вид реестров: строки-карточки с зазором вместо строк,
           разделённых линией. Включается атрибутом data-density="airy" на
           <html> — тем же механизмом, что и тема (скрипт в <head> до первого
           рендера + localStorage), потому что оба свойства обязаны примениться
           ДО первого кадра, иначе интерфейс мигает чужой плотностью.

           ⚠️ Правки модулей не требуется вовсе: селектор «атрибут + тег»
           (специфичность 0,1,1) перебивает утилиты Tailwind (`py-3`, `border-t`
           — 0,1,0) БЕЗ !important. Тот же приём, что у чекбоксов выше и у
           кольца :focus-visible.

           ⚠️ ГОРИЗОНТАЛЬНЫЕ ОТСТУПЫ НЕ ТРОГАЮТСЯ НИ ПРИ КАКИХ УСЛОВИЯХ.
           Замерено: рост вертикальных отступов ширину таблицы не меняет, а
           тронь горизонтальные — Автопарк (11 колонок) вылезет за 1366px, где
           горизонтальный скролл запрещён (обрежет меню «Действия ▾»). Запас
           там 39px, то есть +2px на ячейку уже критичны.

           ⚠️ Фон карточки — rowhover, а НЕ panel. Таблица лежит внутри
           контейнера с bg-panel, и карточка цвета panel была бы невидима.
           Взяв следующую ступень лестницы слоёв, мы получаем видимую карточку
           БЕЗ правки контейнера — иначе пришлось бы гасить его фон через
           :has(> table), задевая заодно модалки и их скруглённые углы. */
        [data-density="airy"] table { border-collapse: separate; border-spacing: 0 7px; }
        [data-density="airy"] tbody td {
            padding-top: 17px; padding-bottom: 17px;
            border-top-color: transparent;
            background-color: rgb(var(--c-rowhover));
        }
        [data-density="airy"] tbody td:first-child { border-top-left-radius: 12px; border-bottom-left-radius: 12px; }
        [data-density="airy"] tbody td:last-child { border-top-right-radius: 12px; border-bottom-right-radius: 12px; }
        [data-density="airy"] thead th { padding-top: 6px; padding-bottom: 12px; }

        /* ⚠️ Подсветка строки живёт на <tr> (`hover:bg-rowhover`, `bg-accent/15`
           у выбранных строк пакетной оплаты), а фон карточки — на <td>, и он
           её перекрывает. Поэтому оба состояния переносятся на ячейку явно.
           Селектор по подстроке класса работает, потому что Tailwind оставляет
           имена классов в разметке буквально. */
        [data-density="airy"] tbody tr:hover td { background-color: rgb(var(--c-surface-2)); }
        [data-density="airy"] tbody tr[class*="bg-accent"] td { background-color: rgb(var(--c-accent) / 0.15); }

        /* ⚠️ Строки с colspan карточками НЕ становятся: это итоги отчётов,
           заголовки групп и заглушки «Нет записей» (в проекте их больше
           полусотни — от P&L до реестра ТС). Карточка на всю ширину читалась бы
           как ещё одна запись реестра. :has() здесь уместен — Chrome его
           поддерживает, а другого способа выбрать строку ПО СОДЕРЖИМОМУ нет. */
        [data-density="airy"] tbody tr:has(td[colspan]) td {
            background-color: transparent; border-radius: 0;
            padding-top: 12px; padding-bottom: 12px;
        }

        /* ── Сайдбар ─────────────────────────────────────────────────────────
           Иконки — контурные SVG из icons.js (window.Icon), эмодзи в меню не
           используются. Цвет иконки задаётся классом .sidebar-ico, а не пропом:
           у <svg> stroke="currentColor", поэтому раскраска состояний
           (обычное/hover/активное) живёт в одном месте — здесь. */
        /* ── ДВА УРОВНЯ МЕНЮ: раздел и модуль (08.08.2026) ───────────────────
           Раньше заголовок раздела и пункт модуля отличались только начертанием,
           а цвет текста у обоих был #9CA3AF — блоки сливались в один список.
           Разведены тремя признаками сразу, и каждый работает независимо от
           остальных (если пользователь уменьшит шрифт или отключит цвет —
           группировка не развалится):
             1. яркость подписи: раздел #E5E7EB против модуля #9CA3AF;
             2. цветной якорь: иконка раздела в тоне блока, иконки модулей — тот
                же тон вполсилы;
             3. вертикальная направляющая слева от группы + воздух между блоками.

           ⚠️ Заглавные буквы у разделов (частый SaaS-приём) сознательно НЕ
           применены: у нас разделы РАСКРЫВАЮТСЯ кликом, то есть это органы
           управления, а не пассивные подписи-заголовки. Капсом их набирать —
           обещать пользователю, что это просто заголовок. По той же причине у
           разделов остаются иконки. */
        .sidebar-folder {
            width: 100%; text-align: left; padding: 0.6rem 0.75rem; border-radius: 0.375rem;
            font-weight: 600; display: flex; align-items: center; justify-content: space-between;
            cursor: pointer; color: #E5E7EB; transition: all 0.15s;
        }
        .sidebar-folder:hover { background-color: rgba(255, 255, 255, 0.06); color: #FFFFFF; }
        /* Иконка раздела — в тоне блока. Тон приходит переменной --grp с обёртки
           блока, поэтому одно правило обслуживает все разделы сразу. */
        .sidebar-folder > span > .sidebar-ico { color: rgb(var(--grp, 107 114 128)); }
        .sidebar-folder:hover > span > .sidebar-ico { color: rgb(var(--grp, 229 231 235)); }

        /* Группа вложенных модулей: направляющая слева в тоне блока. Она связывает
           пункты с их разделом заметно надёжнее, чем один лишь отступ, и при этом
           не добавляет ни одного лишнего пикселя по высоте.
           ⚠️ Отступ 0.75rem+0.75rem, а не больше: подписи вроде «Реестр ОС и
           амортизация» и так переносятся на две строки в колонке 256px, и каждый
           лишний пиксель слева отнимает ширину у текста. */
        .sidebar-group {
            margin-left: 0.75rem; padding-left: 0.75rem;
            border-left: 1px solid rgb(var(--grp, 55 65 81) / 0.30);
        }
        /* Одиночный пункт вне раздела (Центр задач): тон нужен, направляющая и
           отступ — нет, он не является ничьим потомком. */
        .sidebar-group.is-flush { margin-left: 0; padding-left: 0; border-left: 0; }
        /* Иконки модулей — тот же тон, что у раздела, но вполсилы: блок читается
           как одна семья, а меню не превращается в новогоднюю ёлку. */
        .sidebar-group .sidebar-ico { color: rgb(var(--grp, 107 114 128) / 0.62); }
        .sidebar-group .sidebar-btn:hover .sidebar-ico { color: rgb(var(--grp, 229 231 235) / 0.95); }
        .sidebar-btn {
            position: relative;
            width: 100%; text-align: left; padding: 0.5rem 0.75rem; border-radius: 0.375rem;
            transition: all 0.15s; font-size: 0.875rem; display: flex; align-items: center; gap: 0.625rem;
            cursor: pointer; color: #9CA3AF;
        }
        .sidebar-btn:hover { background-color: rgba(255, 255, 255, 0.06); color: #F3F4F6; }
        /* Активный пункт: лёгкая подсветка строки + яркая вертикальная полоса
           слева (SaaS-паттерн Stripe/Jira/Odoo). Сплошная синяя заливка на всю
           ширину, что была раньше, перетягивала на себя внимание со всего меню. */
        /* ⚠️ Цвета берутся из токенов, а не из хексов: раньше здесь жил #3B82F6 (blue-500) —
           ВТОРОЙ синий системы, не связанный с фирменным #1F4E78 активной вкладки и печатных
           бланков. Теперь оба — одно семейство: accent для заливок, accent-bright для тонких
           индикаторов, которым на тёмном фоне нужна яркость (полоса 3px, иконка 15px). */
        .sidebar-btn.active { background-color: rgb(var(--c-accent-bright) / 0.14); color: #FFFFFF; font-weight: 600; }
        .sidebar-btn.active::before {
            content: ''; position: absolute; left: 0; top: 5px; bottom: 5px; width: 3px;
            border-radius: 0 3px 3px 0; background-color: rgb(var(--c-accent-bright));
        }
        .sidebar-ico { flex-shrink: 0; color: #6B7280; transition: color 0.15s; }
        .sidebar-btn:hover .sidebar-ico, .sidebar-folder:hover .sidebar-ico { color: #E5E7EB; }
        .sidebar-btn.active .sidebar-ico { color: rgb(var(--c-accent-bright)); }
        .sidebar-chevron { flex-shrink: 0; color: #4B5563; transition: transform 0.18s ease-out, color 0.15s; }
        .sidebar-folder:hover .sidebar-chevron { color: #9CA3AF; }
        /* Шеврон ОДИН (chevron-right) и ПОВОРАЧИВАЕТСЯ, а не подменяется на
           chevron-down: подмена — это два разных SVG, поворачивать там нечего.
           90°, а не 180°: вправо→вниз читается как «раскрыл», а перевёрнутая
           стрелка — как «сверни обратно». */
        .sidebar-folder.is-open .sidebar-chevron { transform: rotate(90deg); }

        /* ── РАСКРЫТИЕ РАЗДЕЛА: Slide + Fade (08.08.2026) ────────────────────
           height: auto не анимируется, поэтому высота едет через CSS Grid:
           grid-template-rows 0fr → 1fr. Внутренняя обёртка с overflow:hidden
           обязательна — она и есть та строка грида, которую мы сжимаем.

           ⚠️ Содержимое раздела ВСЕГДА в DOM (условный рендер {menuOpen && …}
           убран): анимировать нечего, если элемента ещё не существует. Пункты
           меню — просто ссылки, данных они не грузят, цены за это нет.

           ⚠️ Но из-за этого свёрнутые ссылки остались бы в Tab-обходе и в
           дереве доступности: overflow:hidden при нулевой высоте фокус НЕ
           убирает — пользователь с клавиатуры уезжал бы в невидимое меню (см.
           правило «⌨️ Видимый фокус клавиатуры»). Поэтому visibility:hidden, и
           применяется он ПОСЛЕ схлопывания (задержка перехода = его длительность),
           иначе содержимое пропадало бы рывком в начале анимации.

           Тайминги 180/150ms — порядок уже существующих переходов проекта
           (.app-nav 220ms, .sidebar-btn 150ms): меню не должно жить в своём темпе. */
        .sidebar-collapse {
            display: grid;
            grid-template-rows: 0fr;
            transition: grid-template-rows 0.18s ease-out;
        }
        .sidebar-collapse.is-open { grid-template-rows: 1fr; }
        .sidebar-collapse > div {
            overflow: hidden;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.12s ease-out, visibility 0s linear 0.18s;
        }
        .sidebar-collapse.is-open > div {
            opacity: 1;
            visibility: visible;
            transition: opacity 0.15s ease-out 0.03s, visibility 0s;
        }
        /* ⚠️ 3px под последним пунктом — не «воздух», а место под КОЛЬЦО ФОКУСА.
           Кольцо (a:focus-visible выше) выходит на 3px за габариты ссылки, а низ
           последней ссылки совпадал с краем overflow:hidden — замерено: запас был
           ровно 0, то есть у последнего пункта каждого раздела кольцо срезалось бы
           снизу. Отступ стоит на .sidebar-group ВНУТРИ бокса, а не на самом боксе:
           на боксе он даёт свёрнутому разделу 3px мёртвой высоты (замерено), а
           отсюда схлопывается вместе с содержимым — свёрнутый раздел остаётся 0.
           ⚠️ overflow:clip + overflow-clip-margin (казалось бы, ровно для этого и
           придуманный) НЕ подходит: с ним grid-элемент теряет нулевой минимальный
           размер, и свёрнутый раздел остаётся во всю высоту — проверено, 151px
           вместо 0. */
        .sidebar-collapse .sidebar-group { padding-bottom: 3px; }

        /* Уважаем системную настройку «уменьшить движение» — тем же приёмом, что
           уже применён к выезду самой панели и к рабочей области (см. .app-nav и
           .app-workspace ниже). В отличие от prefers-color-scheme (её мы намеренно
           не читаем — тема не должна меняться сама после обновления ОС), это
           настройка доступности: пользователь включает её именно для того, чтобы
           интерфейсы перестали двигаться.
           ⚠️ .app-nav здесь НЕ перечисляем — он уже погашен своим правилом ниже;
           второе место, гасящее то же самое, неизбежно разъедется. */
        @media (prefers-reduced-motion: reduce) {
            .sidebar-collapse,
            .sidebar-collapse > div,
            .sidebar-chevron { transition: none !important; }
        }

        /* ── СВОРАЧИВАЕМАЯ БОКОВАЯ ПАНЕЛЬ (08.08.2026) ───────────────────────
           Панель уезжает влево, рабочая область занимает освободившееся место.
           Заодно это фундамент под работу с планшета и телефона: там панель
           обязана не сжимать контент, а ложиться поверх него.

           ⚠️ ДВА РАЗНЫХ ПОВЕДЕНИЯ, а не одно с подгонкой ширины:
             • ≥1024px — панель остаётся в потоке и РАЗДВИГАЕТ контент
               (margin-left: -16rem при сворачивании);
             • <1024px — панель выходит из потока и НАКРЫВАЕТ контент
               (position: fixed + translateX). На 768px «сжимающая» панель
               оставила бы реестру 500px, чего не хватает ни одной нашей
               таблице — она обязана уходить целиком.

           ⚠️ Анимируем margin-left/transform, а НЕ width: анимация ширины
           заставляет браузер пересчитывать раскладку таблиц на каждом кадре —
           на реестре в 11 колонок это заметно дёргается. */
        .app-nav { transition: margin-left 0.22s ease; }
        .app-nav.is-closed { margin-left: -16rem; }

        @media (max-width: 1023px) {
            /* ⚠️ Селектор «тег + класс» (0,1,1), а НЕ голый .app-nav (0,1,0): на панели висит
               утилита z-10 той же специфичности, а Tailwind Play подключается ПОЗЖЕ этого
               блока и при равенстве выигрывает. Из-за этого на телефоне и планшете панель
               выезжала с z-index: 10 и оказывалась ПОД затемнением (z-55): пользователь
               жал кнопку, экран темнел, меню не появлялось — «кнопка не работает»
               (найдено по скриншотам пользователя 08.08.2026). Тот же приём и та же
               причина, что у button.app-nav-toggle ниже и у режима плотности. */
            div.app-nav {
                position: fixed; top: 0; bottom: 0; left: 0;
                z-index: 60;                 /* выше модалок контента (z-[60] у них тот же слой,
                                                но панель рендерится позже — перекрывает) */
                margin-left: 0;
                transition: transform 0.22s ease;
            }
            .app-nav.is-closed { margin-left: 0; transform: translateX(-100%); }
            /* Затемнение под панелью: клик по нему закрывает — привычный жест
               на сенсорном экране, где промахнуться мимо узкой кнопки легко. */
            .app-scrim { position: fixed; inset: 0; z-index: 55; background: rgba(0, 0, 0, 0.55); }
        }
        /* На широком экране затемнение не нужно: панель ничего не перекрывает. */
        @media (min-width: 1024px) { .app-scrim { display: none; } }

        /* Уважаем системную настройку «меньше движения» — иначе панель дёргается
           у пользователей, которым анимации противопоказаны. */
        @media (prefers-reduced-motion: reduce) { .app-nav { transition: none; } }

        /* ── ПОЗИЦИЯ КОНТЕНТА ПРИ СВОРАЧИВАНИИ ПАНЕЛИ ────────────────────────
           Контент ограничен max-w-[1600px] и центрируется в рабочей области.
           Пока он УПИРАЕТСЯ в доступную ширину (окно уже 1920px), сворачивание
           реально расширяет таблицу: правый край стоит, левый уходит вместе с
           панелью — движение объясняет само себя и ради него кнопка и делалась.
           От 1920px контент в лимит уже не упирается: ширины он не получит, а
           mx-auto пересчитает поля и весь блок переедет влево на 128px —
           движение без всякой выгоды (замечание пользователя 08.08.2026).
           Поэтому там позиция ФИКСИРУЕТСЯ: рабочая область добирает слева ровно
           те 16rem, что отдала панель, и центр остаётся на прежнем месте.
           ⚠️ padding на .app-workspace, а НЕ margin на внутреннем контейнере: у
           того стоит mx-auto (margin-left: auto), и явный margin-left перебил бы
           его, прижав контент к левому краю.
           ⚠️ Порог именно 1920 = 1600 + 16rem панели + 4rem отступов: ниже него
           сворачивание ещё добавляет таблице ширину, и фиксировать позицию
           значило бы отнять у кнопки её единственный смысл. Меняя лимит ширины
           контента, пересчитывать и порог. */
        @media (min-width: 1920px) {
            /* ⚠️ Отступ обязан ЕХАТЬ ВМЕСТЕ С ПАНЕЛЬЮ — той же длительностью и кривой.
               Без перехода он применяется мгновенно, панель же уползает 0.22s: контент
               рывком уходит вправо на 256px и плавно возвращается, то есть дёргается,
               хотя итоговая позиция верна (замечание пользователя 08.08.2026). При
               совпадающих transition два сдвига гасят друг друга в каждом кадре.
               Анимации ширины это не создаёт: контент здесь всегда 1600px, меняется
               только его положение — раскладка таблицы каждый кадр не пересчитывается
               (см. запрет на анимацию width у самой панели выше). */
            .app-workspace { transition: padding-left 0.22s ease; }
            .app-nav.is-closed + .app-workspace { padding-left: 18rem; }

            /* ⚠️ Гасить переход обязательно ПАРОЙ с панелью: останься он у одной из
               них, движения перестанут компенсироваться и вернётся тот же рывок. */
            @media (prefers-reduced-motion: reduce) {
                .app-workspace { transition: none; }
            }
        }

        /* Кнопка меню на сенсорном экране: 36px под мышь достаточно, под палец — нет
           (рекомендуемый минимум 44px). Растёт только там, где это нужно, чтобы не
           раздувать шапку на десктопе. */
        /* ⚠️ Селектор «тег + класс» (0,1,1), а не голый класс: у кнопки стоят утилиты
           w-9/h-9 той же специфичности (0,1,0), а стили Tailwind Play подключаются
           ПОЗЖЕ этого блока и при равенстве выигрывают. Тот же приём, что у режима
           плотности и кольца :focus-visible. */
        @media (max-width: 1023px) {
            button.app-nav-toggle { width: 2.75rem; height: 2.75rem; }

            /* ── ШАПКА МОДУЛЯ НА УЗКОМ ЭКРАНЕ ────────────────────────────────
               Замер на планшете 820px: единственное, что вылезало за экран, —
               правый блок шапки (профиль, пароль, выход, плотность, тема, поиск,
               уведомления, Excel, «Добавить»): 887-911px при доступных 820, и
               `shrink-0` не давал ему сжаться. Таблицы при этом помещались.

               Поэтому шапка переносится на вторую строку целиком, а не прячет
               кнопки: скрывать элементы управления по ширине экрана значит
               заводить второй, урезанный интерфейс — и потом объяснять, почему
               с планшета часть действий недоступна.

               ⚠️ `div.app-header-tools` — тег + класс: `shrink-0` в разметке имеет
               ту же специфичность, а Tailwind подключается позже. */
            div.app-header { flex-wrap: wrap; row-gap: 0.75rem; }
            div.app-header-tools { flex-shrink: 1; flex-wrap: wrap; width: 100%; justify-content: flex-end; }

            /* ── ШИРОКАЯ ТАБЛИЦА НА УЗКОМ ЭКРАНЕ ─────────────────────────────
               Прокручивается САМА ТАБЛИЦА, а не вся страница: иначе вместе с
               реестром уезжают шапка модуля и кнопка меню.

               ⚠️ Класс ставится ТОЛЬКО тем реестрам, чьё меню «Действия ▾» уже
               переведено в портал (row_actions.js). У остальных overflow обрежет
               меню — это прямой запрет из раздела про всплывающие элементы.
               Порядок работ: сначала портал, потом прокрутка. Не наоборот. */
            .table-scroll-narrow { overflow-x: auto; }
        }
        /* ══ ЭКРАН ВХОДА (11.08.2026) ═══════════════════════════════════════
           Фон целиком из CSS: три расфокусированных пятна и сетка с радиальной
           маской. Картинки сознательно НЕТ — §1 требует локальных ассетов, то
           есть фото пришлось бы класть в репозиторий (сотни КБ стока, который
           к нашему парку отношения не имеет). Градиент даёт тот же масштаб при
           нулевом весе и красится теми же токенами, что и весь интерфейс.

           ⚠️ Значения — ТОЛЬКО через токены (--c-accent и т.п.), не хексами:
           экран входа обязан работать в обеих темах, а тема ставится inline-
           скриптом в <head> ДО первого рендера (см. ниже) — то есть на первом
           же кадре здесь уже правильная палитра.

           ⚠️ Свечения НЕ анимируются намеренно: filter: blur(90px) на большом
           круге перерисовывается каждый кадр, и «дышащий» фон стоил бы заметно
           дороже, чем выглядит. Движения на этом экране нет вовсе, поэтому и
           prefers-reduced-motion тут гасить нечего. */
        .login-stage {
            position: relative; height: 100vh; height: 100dvh; overflow: hidden;
            display: flex; align-items: center; justify-content: center;
            background: rgb(var(--c-app));
        }
        .login-glow { position: absolute; border-radius: 50%; filter: blur(90px); pointer-events: none; }
        .login-glow-1 { width: 620px; height: 620px; left: -140px; top: -180px; background: rgb(var(--c-accent) / 0.55); }
        /* ⚠️ Второй тон — приглушённый фиолетовый ЧИСЛОМ, а не токеном, и это намеренно:
           --c-alt занят семантикой (внешние обстоятельства PA, продление ТС), и брать его под
           декоративное пятно значило бы смешать «смысл» с «оформлением». Своего токена он не
           получает — величина живёт ровно в этих двух строках и больше нигде. */
        .login-glow-2 { width: 520px; height: 520px; right: -120px; bottom: -160px; background: rgb(99 70 190 / 0.42); }
        .login-glow-3 { width: 380px; height: 380px; left: 52%; top: 58%; background: rgb(var(--c-accent-bright) / 0.22); }
        /* На белом фоне те же пятна в полную силу превращаются в кислотные
           разводы — в светлой теме они втрое слабее. */
        :root[data-theme="light"] .login-glow-1 { background: rgb(var(--c-accent) / 0.20); }
        :root[data-theme="light"] .login-glow-2 { background: rgb(99 70 190 / 0.16); }
        :root[data-theme="light"] .login-glow-3 { background: rgb(var(--c-accent-bright) / 0.14); }
        /* Сетка гаснет к краям маской, иначе она читается как «страница не
           доехала» — линии, обрубленные ровно по границе экрана. */
        .login-grid {
            position: absolute; inset: 0; pointer-events: none;
            background-image:
                linear-gradient(rgb(var(--c-hairline) / 0.05) 1px, transparent 1px),
                linear-gradient(90deg, rgb(var(--c-hairline) / 0.05) 1px, transparent 1px);
            background-size: 56px 56px;
            -webkit-mask-image: radial-gradient(ellipse at 50% 45%, #000 20%, transparent 72%);
                    mask-image: radial-gradient(ellipse at 50% 45%, #000 20%, transparent 72%);
        }
        .login-card {
            position: relative; width: 380px; max-width: calc(100vw - 2rem);
            padding: 2rem 2rem 1.75rem; border-radius: 20px;
            background: rgb(var(--c-panel) / 0.72);
            -webkit-backdrop-filter: blur(22px) saturate(140%);
                    backdrop-filter: blur(22px) saturate(140%);
            border: 1px solid rgb(var(--c-hairline) / 0.08);
            box-shadow: 0 30px 70px rgb(0 0 0 / 0.45), inset 0 1px 0 rgb(var(--c-hairline) / 0.10);
        }
        :root[data-theme="light"] .login-card {
            background: rgb(var(--c-panel) / 0.78);
            border-color: rgb(var(--c-hairline) / 0.10);
            box-shadow: 0 24px 60px rgb(16 24 40 / 0.14);
        }
        /* ⚠️ Отступ до полей живёт ЗДЕСЬ, а не на снятом заголовке: приветствия в карточке нет
           (решение пользователя 11.08.2026), и без этого margin название легло бы вплотную
           к первому полю. */
        .login-brand { display: flex; align-items: center; gap: 0.625rem; margin-bottom: 1.5rem; color: rgb(var(--c-ink)); }
        .login-brand .login-mark { color: rgb(var(--c-brand)); }
        .login-brand .login-name { font-size: 1.1875rem; font-weight: 800; letter-spacing: -0.02em; }

        .login-field { position: relative; margin-bottom: 0.75rem; }
        .login-field > input {
            width: 100%; height: 46px; padding: 0 2.75rem 0 2.5rem; border-radius: 10px;
            font-size: 0.875rem; color: rgb(var(--c-ink));
            background: rgb(var(--c-inset)); border: 1px solid rgb(var(--c-field));
            transition: border-color 0.18s;
        }
        .login-field > input::placeholder { color: rgb(var(--c-faint)); }
        .login-field > input:focus { border-color: rgb(var(--c-accent-bright)); }
        /* Иконка синеет вместе с рамкой. ⚠️ Кольцо фокуса своё НЕ рисуем — его
           даёт глобальный input:focus-visible выше, и второй halo поверх него
           читался бы как двойная обводка. */
        .login-field > input:focus ~ .login-ico { color: rgb(var(--c-accent-bright)); }
        .login-ico { position: absolute; left: 0.8125rem; top: 50%; transform: translateY(-50%); color: rgb(var(--c-faint)); pointer-events: none; transition: color 0.18s; }
        .login-eye { left: auto; right: 0.5rem; pointer-events: auto; background: none; border: 0; padding: 0.25rem; cursor: pointer; color: rgb(var(--c-faint)); display: flex; }
        .login-eye:hover { color: rgb(var(--c-ink-2)); }
        /* ⚠️ Пересиливает правило «иконка синеет при фокусе поля» (0,3,1): без этого наведение
           на глазик не давало бы отклика ровно тогда, когда в поле стоит курсор — то есть
           почти всегда, когда на него и жмут. */
        .login-field > input:focus ~ .login-eye:hover { color: rgb(var(--c-ink-2)); }
        .login-caps { display: flex; align-items: center; gap: 0.375rem; font-size: 0.6875rem; color: rgb(var(--c-warn)); margin: 0.5rem 0.125rem 0; }
        .login-error { font-size: 0.75rem; color: rgb(var(--c-bad)); margin: 0.5rem 0.125rem 0; }
        .login-submit {
            width: 100%; height: 46px; margin-top: 1rem; border: 0; border-radius: 10px; cursor: pointer;
            font-size: 0.875rem; font-weight: 800; color: rgb(var(--c-on-accent));
            /* ⚠️ Объём даёт ПОЛУПРОЗРАЧНОЕ наложение поверх токена, а не градиент от
               info-solid-hover к info-solid. Замерено: верх такого градиента (#3B82F6) с белым
               текстом даёт 3.68 при норме AA 4.5 для 14px bold — то есть подпись на ГЛАВНОЙ
               кнопке экрана формально нечитаема (ровно та же история, что с зелёной кнопкой
               #059669 09.08.2026). С наложением верх кромки — 4.68, низ ещё выше, и цвет
               по-прежнему приходит из токена, то есть светлая тема получает свой синий. */
            background: linear-gradient(180deg, rgb(255 255 255 / 0.06), rgb(0 0 0 / 0.10)), rgb(var(--c-info-solid));
            box-shadow: 0 6px 18px rgb(var(--c-info-solid) / 0.35), inset 0 1px 0 rgb(255 255 255 / 0.18);
            transition: box-shadow 0.18s, transform 0.12s;
        }
        .login-submit:hover { box-shadow: 0 8px 24px rgb(var(--c-info-solid) / 0.45), inset 0 1px 0 rgb(255 255 255 / 0.22); }
        .login-submit:active { transform: translateY(1px); }
        @media (prefers-reduced-motion: reduce) { .login-submit:active { transform: none; } }

        /* ── ПЕРВЫЙ КАДР БЕЗ «ГОЛОГО» HTML (добавлено 07.08.2026) ────────────
           Tailwind здесь генерирует ВСЕ стили в браузере (lib/tailwind.js —
           Play-сборка, статического .css в проекте нет). Пока он не отработал,
           разметка показывается неоформленной: сайдбар съезжает, таблица
           разъезжается. Раньше этого не было видно — Babel компилировал JSX
           ~4 секунды, и Tailwind успевал закончить первым. После перевода
           фронтенда на прекомпиляцию React рисует за ~200 мс, и «голый» кадр
           стал попадать на экран (поймано пользователем на скриншоте).

           Прячем содержимое до готовности стилей: ровный фон вместо мигания.
           ⚠️ visibility, а не display: разметка строится и меряется как обычно,
           не ломая расчёты ширины у таблиц. */
        #root { visibility: hidden; }
        #root.styles-ready { visibility: visible; }
    