/* ═══════════════════════════════════════════════════════════════════════════
 * kotomori · пульт — «командный терминал» (зелёный фосфор)
 *
 * Продолжает CRT-ретро язык промо-сайта (сканлайны, кандзи 守, моно-подписи,
 * реперные метки, свечение), но переведён в ОПЕРАТОРСКУЮ дисциплину ISA-101:
 * поле спокойное и тёмно-графитовое, КРАСНЫЙ зарезервирован строго под аварию,
 * ЯНТАРНЫЙ — под внимание, ФОСФОРНО-ЗЕЛЁНЫЙ — под «система жива / управление».
 * Если сайт — «красная трубка», то пульт — «зелёная трубка» той же ЭЛТ-вселенной.
 *
 * Контракт: имена классов и CSS-переменных сохранены 1:1 со старой темой —
 * все страницы SPA продолжают работать без правок разметки.
 * ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* поле — тёмный графит с зеленоватым подтоном */
  --bg:        #060809;
  --bg-2:      #0a0d0f;
  --panel:     #0e1315;   /* карточка */
  --panel-2:   #131a1d;   /* поднятая поверхность / инпут */
  --panel-3:   #182124;   /* hover */
  --ink:       #e9ede8;   /* кость (основной текст) */
  /* Лестница серых — три роли, а не три оттенка «на вкус». Смешение ролей и
     дало 3,4:1 на подписях секций конфигуратора:
       --ink   основной текст и значения;
       --ink-2 ВСЁ, что человек читает глазами и не является основным текстом:
               подписи полей, капс-подписи секций, марки, единицы, счётчики;
       --ink-3 ТОЛЬКО плейсхолдеры пустых полей и подписи, живущие в title.
     Текст роли --ink-3 нельзя делать носителем данных: он ниже AA. */
  --ink-2:     #97a29b;   /* приглушённый: подписи и данные второго плана */
  --ink-3:     #616c65;   /* едва заметный: плейсхолдеры и title */
  --line:      rgba(150,200,175,.13);
  --line-2:    rgba(150,200,175,.07);

  /* фосфорно-зелёный — бренд / интерактив / «онлайн» (это ХРОМ, не статус процесса) */
  --brand:      #33e0a3;
  --brand-2:    #17c48b;
  --brand-deep: #0e6f5c;
  --brand-soft: rgba(51,224,163,.13);
  --brand-glow: rgba(51,224,163,.45);

  /* статусы процесса. Красный/янтарь «толще» зелёного: аварию и внимание видно
     боковым зрением с 2 метров — ink ярче, подложки и свечение плотнее.
     Зелёный не трогаем: он опорный «всё хорошо». */
  --alarm:      #ff4d3f;  --alarm-ink: #ff8579;  --alarm-soft: rgba(255,77,63,.19);  --alarm-glow: rgba(255,77,63,.7);
  --warn:       #ffb020;  --warn-ink:  #ffcb5c;  --warn-soft:  rgba(255,176,32,.18);  --warn-glow:  rgba(255,176,32,.65);
  /* граница «внимания»: в тёмной теме её несёт само свечение, токен нужен
     светлой (там охра-заливка и охра-граница — разные цвета, см. ниже) */
  --warn-line:  rgba(255,176,32,.5);
  --ok:         #35c98f;  --ok-ink:    #4bdca2;  --ok-soft:    rgba(53,201,143,.12);  --ok-glow:    rgba(53,201,143,.5);
  /* «кольца» тревожных плиток: в тёмной теме — фосфорное свечение,
     в светлой (см. ниже) glow не работает — там чернильный обод */
  --alarm-ring:     0 0 20px -4px var(--alarm-glow);
  --alarm-ring-hot: 0 0 36px -2px var(--alarm-glow);
  --warn-ring:      0 0 16px -5px var(--warn-glow);
  /* офлайн — ХОЛОДНЫЙ сине-серый: зеленовато-серый на расстоянии читается
     как «зелёный», и статусы путаются */
  --offline:    #818ca0;  --offline-soft: rgba(129,140,160,.13);
  --manual:     #b98cff;  --manual-soft: rgba(185,140,255,.14);
  --maint:      #5aa8ff;  --maint-soft:  rgba(90,168,255,.14);
  /* Вода в баке на мнемосхеме (градиент #wgrad в schematic.js). Токеном, а не
     литералом в коде рисунка, ровно по одной причине: уровень воды — это
     ГЛАВНЫЙ ответ схемы на вопрос «сколько налито», и в светлой теме ночной
     циан на бумажном фоне давал 1,25:1 при норме 3:1 для нетекстовой графики,
     то есть ответ пропадал. Светлые значения — те же, что у плашки «вода» в
     легенде под схемой: легенда и то, что она подписывает, обязаны совпадать
     по цвету. */
  --water-a: #5ef0ea;  --water-b: #1aa7c8;
  /* ── Остальные среды и «материалы» мнемосхемы. ПОЧЕМУ токенами: до волны 7 в
     файле жили 59 уникальных hex (101 вхождение) и 131 уникальный rgba, причём
     «печатная» палитра схемы (#6d7a5e, #79826a, #b3afa4, #f0efe9, #cfccc2,
     #0b4550, #d9821f) была ВТОРЫМ, нигде не описанным набором цветов. Пока
     цвет живёт литералом в правиле, невозможно ни проверить его контраст, ни
     поменять оттенок, ни завести третью тему (высококонтрастная — реальный
     запрос диспетчерских), и каждая следующая правка добавляет ещё литерал.
     Пары *-a/*-b — верх и низ градиента среды; легенда и труба обязаны брать
     ОДНУ пару, иначе плашка легенды не совпадёт с тем, что она объясняет.
     Материалы: --metal арматура (ручки, ступицы, лопасти), --metal-2 патрубок,
     --case корпус трубы и «сухая» среда, --plate «бумага таблички» под
     символом, --ember язык пламени, --vlevel-ink цифра уровня в баке.
     Применяют владельцы регионов (К4 — схема и легенда). */
  --media-air-a: rgba(219,231,238,.55); --media-air-b: rgba(143,176,192,.6);
  --media-sew-a: #a3a05a;               --media-sew-b: #5d6337;
  --media-gas-a: #cbb26f;               --media-gas-b: #7d6a2f;
  --metal:   #8fb8a6;
  --metal-2: #8fb8a6;
  --case:    #26333a;
  --case-fill: #26333a;
  --plate:   rgba(10,16,18,.88);
  --ember:   #ffbf78;
  --vlevel-ink: #eafcff;

  /* ── ВЫСОТА (--elev-*) и СВЕЧЕНИЕ (--glow-*) — две разные вещи, и их нельзя
     писать «на глаз». Замер до волны 7: 27 уникальных box-shadow на живых
     страницах, причём одно и то же --alarm-glow встречалось шестью толщинами
     (0 0 10px -1px, 0 0 11px, 0 0 14px -4px, 0 0 18px -2px, 0 0 20px,
     0 0 22px -4px). Свечение в этой идентичности — главный носитель тревоги:
     оно кодирует ступень «тихо → внимание → авария», и если толщина случайна,
     ступени не читаются, остаётся просто «что-то светится».

     --glow-* задают ТОЛЬКО геометрию пятна, цвет подставляет место применения:
         box-shadow: var(--glow-md) var(--alarm-glow);
     так один и тот же размер ореола гарантированно достаётся и чипу, и плитке,
     и кнопке, а поменять «яркость тревоги» во всём продукте можно одной ручкой.
     --elev-* — ступени высоты над поверхностью (карточка, поповер, модалка):
     их роль не сигнальная, а пространственная, и мешать их со свечением нельзя. */
  --glow-sm: 0 0 10px -4px;
  --glow-md: 0 0 18px -4px;
  --glow-lg: 0 0 28px -3px;
  --elev-1: 0 1px 0 rgba(150,200,175,.04) inset, 0 2px 10px rgba(0,0,0,.5), 0 20px 44px -28px rgba(0,0,0,.7);
  --elev-2: 0 6px 20px -8px rgba(0,0,0,.66), 0 2px 6px rgba(0,0,0,.4);
  --elev-3: 0 18px 48px -16px rgba(0,0,0,.72), 0 4px 12px rgba(0,0,0,.45);
  --shadow:  var(--elev-1);   /* историческое имя ступени 1 — оставлено псевдонимом */
  --glow-brand: var(--glow-lg) var(--brand-glow);

  /* ── РАДИУС — четыре ступени, а не «сколько показалось». Замер до волны 7:
     в файле сырые 8px×14, 7px×6, 10px×6, 6px×3, 4px×2, плюс 9px и 13px дублями
     самих токенов; на живых страницах 8px стоял на 742 узлах против 66 у
     токена --r, то есть токен проигрывал литералу в 11 раз. Соседство 6, 7, 8,
     9, 10 и 13 px не читается как замысел — оно даёт лёгкую расфокусировку:
     контуры вложенных блоков «дрожат» относительно родительской карточки.
       --r-xs   мелкая метка, чип-код, миниатюрная плашка;
       --r-sm   ВСЕ контролы (кнопка, поле, сегмент) — один радиус на ряд;
       --r      карточки и панели (и вложенные блоки внутри них);
       --r-pill таблетка: чип статуса, счётчик, тумблер.
     --r-lg оставлен ПСЕВДОНИМОМ --r: имя стоит в семи регионах страниц, и
     менять его должен владелец региона, а не общий слой. Сведение 13→12 как
     раз и убирает «дрожание» контуров карточки и её содержимого. */
  --r-xs: 4px;
  --r-sm: 8px;
  --r: 12px;
  --r-lg: var(--r);
  --r-pill: 999px;

  /* ── ШАГ ОТСТУПОВ 4/8. Замер до волны 7: в одном каркасном блоке жили 3, 5,
     6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 18, 21, 22, 26, 40 px — семнадцать
     значений там, где смысла ровно на пять. Разница в 1 px по отдельности не
     видна, но в сумме читается как «собрано на глаз»: именно отсюда рассинхрон
     заголовка карточки и шапки таблицы (4 px) и три почти одинаковые высоты в
     одном ряду. Шкала: 4 · 8 · 12 · 16 · 24 · 32.
     Правило применения: отступ ВНУТРИ контрола — sp-1/sp-2, между контролами —
     sp-2/sp-3, внутри карточки — sp-3/sp-4, между блоками страницы — sp-5. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  /* Боковое поле страницы одним именем. ПОЧЕМУ отдельный токен, а не «просто
     --sp-5»: полосы, которые тянутся во всю ширину контента (липкая шапка
     страницы, липкая шапка конфигуратора), выходят за поле отрицательными
     полями, и это число обязано совпадать с полем .main до пикселя. Пока оно
     было записано литералом в трёх местах (22/26/34 px), любая правка ритма
     давала горизонтальную прокрутку на 2 px — ровно это и случилось при
     переходе на шкалу 4/8. */
  --gutter: var(--sp-5);
  --gutter-wide: var(--sp-6);   /* при свёрнутой рейке контенту отдают больше */

  /* ── РАЗРЯДКА КАПС-ПОДПИСЕЙ — три роли, а не тринадцать значений. Замер до
     волны 7: 13 значений letter-spacing на 39 правил text-transform:uppercase
     (шапка таблицы .06em, счётчик парка .12em, подпись поля .1em, разделитель
     меню .22em, бренд .28em), на живых страницах — 19 отрисованных.
     Разрядка в этой идентичности отличает МАШИННУЮ ПОДПИСЬ от текста; когда у
     признака девятнадцать значений, признак перестаёт работать.
       --ls-cap   все капс-моно-подписи (шапка таблицы, метка поля, тег канала);
       --ls-brand бренд и разделители меню — единственное место, где разрядка
                  сама по себе является знаком, а не подписью;
       --ls-tight чипы, кнопки, заголовки — «почти ноль», чтобы плотная строка
                  не расползалась. */
  --ls-cap: .1em;
  --ls-brand: .24em;
  --ls-tight: .01em;

  /* ── Плавность. Кривая одна на весь продукт: разные кривые у одинаковых по
     смыслу переходов читаются как разный почерк. --ease — «быстро тронулось,
     мягко село», то есть отклик прибора, а не отскок игрушки. */
  --ease: cubic-bezier(.2,.7,.3,1);

  /* ── Мера УСЕЧЕНИЯ (правило продукта, см. блок «.trunc» ниже). Подпись длиннее
     этой меры сокращается многоточием и получает title; число, единица и код
     не сокращаются никогда. */
  --label-max: 28ch;
  /* Высота строки таблицы: один прибор — один шаг. */
  --row-h: 44px;
  /* Высота липкой шапки страницы (под неё уезжает липкая шапка таблицы). */
  --head-h: 58px;

  /* ── ЧЕТЫРЕ РОЛИ «ХРОМА» — вещи, которые есть на каждом экране и потому
     чинились в двух местах: правило в тёмной теме плюс заплатка в светлой.
     Теперь у каждой одно имя, и светлая тема просто подставляет своё значение
     (F-064: цель волны — светлая тема как СМЕНА РОЛЕЙ, а не набор патчей).
       --scrim        подложка модального окна (за ней гасится вся страница);
       --scroll-thumb ползунок прокрутки и его состояние под курсором;
       --selection-ink чернила выделенного текста по акцентной подложке;
       --sheet        полупрозрачная плёнка панелей поверх контента
                      (нижняя навигация телефона, плашки поверх карты). */
  --scrim: rgba(3,6,5,.62);
  --scroll-thumb:    rgba(150,200,175,.18);
  --scroll-thumb-hi: rgba(150,200,175,.32);
  --selection-ink: #04110c;
  --sheet: rgba(8,11,12,.92);

  --mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --sans: 'IBM Plex Sans', -apple-system, 'Segoe UI', Roboto, sans-serif;
  --disp: 'Tektur', 'IBM Plex Sans', sans-serif;
  --kanji: 'Shippori Mincho B1', 'Yu Mincho', 'Hiragino Mincho ProN', serif;

  /* Мелкий кегль — ДВА размера, не шесть. В конфигураторе их было 8,5 / 9 /
     9,5 / 10 / 10,5 px вперемешку, и разнобой читается как небрежность:
       --fs-tag   капс-моно-теги («ВХ», «АН», «ВЫХ», подписи секций);
       --fs-micro микроподписи под контролами и в чипах.
     Ниже --fs-tag не опускаться: 9 px кириллицей нечитаемы на ноутбуке. */
  --fs-tag: 10.5px;
  --fs-micro: 11.5px;
  /* Остальные четыре ступени (F-021). До волны в теме было 19 объявленных
     кеглей и 15 отрисованных — десять из них в диапазоне 9–13,5 px с шагом в
     полпикселя. Полпикселя разницы глаз не читает как иерархию, он читает это
     как небрежность. Ступеней ровно шесть плюс отдельная для счётчиков парка:
       --fs-tag   капс-моно-теги и подписи величин (нижняя граница читаемости);
       --fs-micro микроподписи под контролами и в чипах;
       --fs-body  основной текст интерфейса, кнопки, ячейки таблиц;
       --fs-val   значения и данные (моноширь), вкладки;
       --fs-h2    заголовок карточки/секции;
       --fs-h1    заголовок страницы;
       --fs-count цифра счётчика на пульте парка. */
  --fs-body: 13px;
  --fs-val: 13.5px;
  --fs-h2: 15px;
  --fs-h1: 21px;
  --fs-count: 26px;

  /* ── Высоты контролов (F-032). Замер одного ряда фильтров «Журнала» до волны:
     38 · 38 · 38 · 36 · 36 · 35 · 27 px — семь высот в одной строке, причём
     высота .in молча зависела от гарнитуры содержимого (моноширь в «Журнале»
     давала 38, sans в «Мониторинге» — 36). Ряд контролов разной высоты — первое,
     что читается как «собрано без линейки». --ctl-h-lg включается при ≤860 px
     (см. блок тач-целей F-014). */
  --ctl-h: 36px;
  --ctl-h-sm: 30px;
  --ctl-h-lg: 44px;

  /* ── Мера строки и страницы (F-022). На 1920 действия строки «Доступов»
     отстояли от логина на 1260 px, а helper-тексты шли в 190–210 знаков при
     комфортных 45–90: глаз теряет строку на обратном ходе. --content-max —
     мера СТРАНИЦЫ (списки, формы, таблицы), --prose-max — мера АБЗАЦА.
     Пульт парка, карта и мнемосхема меру страницы не применяют: им нужна
     вся ширина. Применяют владельцы страниц у себя (контракт «движок → применение»). */
  --content-max: 1440px;
  --prose-max: 68ch;

  /* ── Граница интерактивного элемента (F-027). --line = 1,27:1 к панели: это
     верно для РАЗДЕЛИТЕЛЯ внутри карточки (его задача — не мешать), но не для
     границы поля ввода: WCAG 1.4.11 требует 3:1 для нетекстовых элементов,
     иначе человек не видит, где кончается подпись и начинается поле. Роли
     разведены: --line остаётся разделителем, --edge несёт границы
     .in / .btn / .seg / .switch / select. --field-bg — заливка поля: в светлой
     теме поле должно быть СВЕТЛЕЕ бумаги (иначе --panel-2 к --bg даёт 1,04:1
     и пустое поле неотличимо от текста). */
  --edge: rgba(150,200,175,.5);   /* 3,33:1 к панели и 3,30:1 к заливке поля */
  --field-bg: var(--panel-2);
  --field-inset: none;

  /* ── Чернила главной кнопки (F-023). Палитру бренда не трогаем (бриф §2):
     --brand-deep остаётся #0e6f5c и продолжает работать в свечениях и рамках.
     Но текст #04140e на нижней точке этого градиента даёт 3,10:1 при кегле
     13 px — единственная кнопка, означающая «сделать», внизу нечитаема.
     Поэтому у кнопки СВОИ локальные токены заливки: верх --btn-primary-a,
     низ --btn-primary-b. Замер после правки: верх 8,37:1, низ 5,69:1. */
  --btn-primary-a: var(--brand-2);
  --btn-primary-b: #12a077;
  --btn-primary-ink: #04140e;

  /* ── Глубокий красный (F-024, F-025). Тот же #cf2418, что уже стоял литералом
     в градиенте .btn.danger, — теперь токен. Нужен там, где по красному идёт
     БЕЛЫЙ ТЕКСТ: на самом --alarm #ff4d3f белое даёт 3,29:1, то есть кнопка с
     максимальной ценой ошибки и счётчик аварий в меню были нечитаемы.
     Сам --alarm НЕ меняется: он остаётся цветом статуса, свечений и точек. */
  --alarm-deep: #cf2418;
  --alarm-ink-on: #fff;
  /* Обод «разрушающего» действия: тише полноценной рамки, но уже красный. */
  --alarm-edge: rgba(255,77,63,.42);

  /* ── Длительности движения. 150–300 мс: короче — движение не успевает
     объяснить связь, длиннее — интерфейс начинает тормозить руку.
     --dur-1 отклик элемента, --dur-2 появление панели, --dur-3 переход экрана. */
  --dur-1: .14s;
  --dur-2: .18s;
  --dur-3: .26s;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * СВЕТЛАЯ ТЕМА — «печатная» палитра промо-сайта (переключается в Профиле).
 * Принцип: бумага / чернила #16130c — «чернила не светятся», свечения гаснут,
 * тени становятся чернильными. Пульт остаётся «зелёной трубкой»: бренд днём —
 * чернильный малахит, красный по ISA-101 зарезервирован строго под аварию.
 * Бумага намеренно слабо насыщена (HSL 45° 12% 90%): на жёлтой бумаге охра
 * отличалась бы от фона только темнотой, и роль «жёлтый = внимание» переставала
 * читаться боковым зрением.
 * Статусная триада на бумаге: малахит #036c3c 5,3:1 / охра-чернила #7d5802
 * 5,2:1 / сургуч #b81a10 5,3:1 — все ≥4,5:1. Атрибут data-theme ставит boot.js
 * (до первой отрисовки) и ui.js applyTheme().
 * ═══════════════════════════════════════════════════════════════════════════ */
html[data-theme="light"] {
  color-scheme: light;

  /* бумага (печатные токены сайта: --bg/--bg2/--panel) */
  --bg:        #e9e7e1;
  --bg-2:      #dfdcd4;
  --panel:     #f4f3ef;
  --panel-2:   #e5e3dc;
  --panel-3:   #dad7ce;
  --ink:       #16130c;
  --ink-2:     #4b473e;   /* лестница серых сайта: dim  (7,5:1) */
  /* F-026: чернила выверялись только против --panel #f4f3ef, а живут ещё на
     --panel-3 #dad7ce (hover, активная вкладка), --panel-2, --bg и --bg-2.
     На самой светлой из них вся лестница уезжала под AA: --ink-3 давал 3,98,
     активная вкладка .seg button.on — 4,43, чип «Норма» — 4,14. Пересчитано
     против --panel-3 с запасом: худшее значение каждого токена ≥4,7:1
     (текст) и ≥3,3:1 (графика --ok / --warn-line). */
  --ink-3:     #5f5b4e;   /* faint: 4,72:1 на самой светлой поверхности */
  --line:      rgba(24,20,12,.18);
  --line-2:    rgba(24,20,12,.09);

  /* фосфор днём — чернильный малахит (~5:1 на панели), цветность на максимуме
     sRGB при этой светлоте: бледнее он выглядит выцветшим рядом с --ok */
  --brand:      #036c3c;
  --brand-2:    #026035;
  --brand-deep: #014e2a;
  --brand-soft: rgba(3,108,60,.10);
  --brand-glow: rgba(3,108,60,.20);

  /* Статусная триада чернилами. Glow на бумаге не работает — «толщина» здесь
     это плотность подложки и чернильные ободы (--*-ring ниже).

     Роли охры разведены по трём токенам, потому что одним цветом задачу не
     решить: яркая охра #f0a800 нужна ЗАЛИВКОЙ (её видно цветом, но по яркости
     она почти как бумага — 1,7:1, тонкой линией потеряется), --warn-ink нужен
     ТЕКСТОМ (тёмный, иначе не прочесть), --warn-line держит ГРАНИЦЫ (3,0:1 —
     порог WCAG для нетекстовых элементов). Жёлтой темнее L*≈0,75 в sRGB не
     существует — отсюда и три токена.

     Цветность всех трёх выкручена в максимум, доступный sRGB при сохранённом
     AA: иначе зелёный и охра вдвое бледнее красного, и глаз читает не три
     ЦВЕТА, а три одинаково тёмных чернила. Контрасты: ok 3,5:1 (графика),
     ok-ink 5,4:1, warn-ink 5,2:1, alarm 6,1:1 (белый текст по чипу),
     alarm-ink 6,6:1.

     ⚠ Те же значения — в промо-сайте (kotomori-site/index.html, блок
     html.theme-light): там лежат запечённые светлые кадры пульта, рассинхрон
     видно на стыке рамки. */
  --alarm:      #b81a10;  --alarm-ink: #a30404;  --alarm-soft: rgba(184,26,16,.15);  --alarm-glow: rgba(184,26,16,.4);
  --warn:       #f0a800;  --warn-ink:  #6f4e02;  --warn-soft:  rgba(240,168,0,.22);  --warn-glow:  rgba(160,110,0,.4);
  --warn-line:  #96680a;
  --ok:         #0a8340;  --ok-ink:    #055b2a;  --ok-soft:    rgba(11,148,72,.14);  --ok-glow:    rgba(11,148,72,.22);
  --alarm-ring:     0 0 0 2px var(--alarm);
  --alarm-ring-hot: 0 0 0 2px var(--alarm), 0 0 16px -2px var(--alarm-glow);
  --warn-ring:      0 0 0 1.5px var(--warn-line);
  /* офлайн — холодный слейт, максимально далёкий от зелёного и от сепии
     бумаги: тёплый серый на бумаге сливался бы и с «нормой», и с декором */
  --offline:    #545c69;  --offline-soft: rgba(92,100,114,.14);
  --manual:     #6b40c4;  --manual-soft: rgba(107,64,196,.12);
  --maint:      #0f5299;  --maint-soft:  rgba(19,95,176,.12);
  /* Вода на бумаге. Верхний стоп градиента лежит РОВНО НА УРОВНЕ ВОДЫ (заливка
     бака — прямоугольник, положение стопа от заполнения не зависит), то есть
     именно он рисует ту единственную линию, которая отвечает на вопрос
     «сколько налито». Ночной циан давал на ней 1,25:1, чернила плашки легенды
     (#17a0bd) — 2,6:1 при норме 3:1 для нетекстовой графики. Здесь взяты те же
     чернила, что у ВОДЯНОЙ ТРУБЫ на бумаге (§F-156, .pipe-fill #0e7f97): вода
     в баке и вода в трубе — одна среда, и разными по цвету они быть не должны.
     Замер кромки против пустой части бака — 3,9:1, низ (--water-b) — 5,2:1.
     Плашка «вода» в легенде схемы ходит за этими же токенами (см. ниже):
     легенда и то, что она подписывает, обязаны совпадать по цвету. */
  --water-a: #0e7f97;  --water-b: #0b6b81;
  /* Печатная палитра схемы — теперь ролями, а не литералами по правилам (F-054,
     F-064). Значения ровно те, что стояли в компонентных оверрайдах ниже: это
     перенос цвета в роль, а не смена оттенка. */
  --media-air-a: rgba(170,190,200,.5); --media-air-b: rgba(120,150,166,.6);
  --media-sew-a: #8a8749;              --media-sew-b: #4d5230;
  --media-gas-a: #b09a52;              --media-gas-b: #6b5a26;
  --metal:   #6d7a5e;
  --metal-2: #79826a;
  --case:    #b3afa4;
  --case-fill: #cfccc2;
  --plate:   #f0efe9;
  --ember:   #d9821f;
  --vlevel-ink: #0b4550;

  /* Ступени высоты на бумаге — чернильные и мельче ночных: лист не светится и
     не парит, он лежит. Геометрия свечений (--glow-*) остаётся общей, меняется
     только то, ЧЕМ светить: днём --*-glow — это полупрозрачные чернила. */
  --elev-1: 0 1px 0 rgba(255,255,255,.5) inset, 0 1px 3px rgba(24,20,12,.10), 0 16px 34px -24px rgba(24,20,12,.30);
  --elev-2: 0 4px 14px -6px rgba(24,20,12,.22), 0 1px 3px rgba(24,20,12,.12);
  --elev-3: 0 14px 36px -14px rgba(24,20,12,.28), 0 3px 10px rgba(24,20,12,.14);
  --shadow: var(--elev-1);
  --glow-brand: var(--glow-md) var(--brand-glow);

  /* Четыре роли «хрома» на бумаге. Каждое из этих значений раньше было
     отдельным правилом html[data-theme=light] в конце файла — здесь их четыре
     строки вместо шести правил, и ни одно новое место не забудется. */
  --scrim: rgba(38,34,24,.4);
  --scroll-thumb:    rgba(24,20,12,.26);
  --scroll-thumb-hi: rgba(24,20,12,.40);
  --selection-ink: #e9e7e1;
  --sheet: rgba(244,243,239,.92);

  /* Граница интерактива и заливка поля на бумаге (F-027). Поле СВЕТЛЕЕ бумаги
     плюс лёгкая внутренняя тень: печатный бланк — это углубление в листе,
     а не такой же лист. До правки поле (--panel-2 #e5e3dc) на странице
     (--bg #e9e7e1) давало 1,04:1, то есть было невидимо. */
  --edge: rgba(24,20,12,.5);      /* 3,45:1 к бумаге, 3,50:1 к заливке поля */
  --field-bg: #fbfaf7;
  --field-inset: inset 0 1px 2px rgba(24,20,12,.08);

  /* Днём по малахитовому градиенту идёт БЕЛЫЙ текст (7,7–9,9:1), проблемы
     нижней точки нет — заливка главной кнопки остаётся штатной брендовой. */
  --btn-primary-a: var(--brand-2);
  --btn-primary-b: var(--brand-deep);
  --btn-primary-ink: #fff;

  /* Сургуч глубже сургуча: белый текст 9,32:1 (на самом --alarm — 6,58:1) */
  --alarm-deep: #8f1109;
  --alarm-ink-on: #fff;
  --alarm-edge: rgba(184,26,16,.45);
}

/* ═══════════════════════════════════════════════════════════════════════════
 * КАРТА ВЛАДЕНИЯ ФАЙЛОМ (волна ux/v03, этап 1). Файл правят восемь дорожек
 * одновременно, поэтому у каждого куска есть ровно один хозяин. Правило:
 *
 *   • правь ТОЛЬКО внутри своего именованного региона `/* === К_: … === *\/`;
 *   • общие токены (:root, html[data-theme=light]) и общие компоненты
 *     (.btn, .in, .seg, .chip, .switch, состояния, движение, тач-цели)
 *     НЕ трогает никто — они закрыты, правка через оркестратора;
 *   • светлая тема твоей страницы живёт В ТВОЁМ регионе, а не в общем блоке
 *     светлых оверрайдов: иначе один экран чинится в двух местах;
 *   • новый цвет/отступ/кегль не заводится литералом — берётся токен. Нет
 *     подходящего токена — это разговор с оркестратором, а не rgba() в строке.
 *
 *   К1-А1  токены, состояния, движение, тач-цели, иконки (этот слой, закрыт).
 *          В волне 7 к нему же отошёл весь общий слой ниже (шкалы отступов,
 *          радиусов, разрядки, теней и свечений, усечение текста, липкая шапка
 *          страницы, таблица-прибор) — дорожки К1-А2 в волне 7 нет.
 *   К1-А2  общие компоненты ui.js (.modal, .toast, .empty, .loading, table.t
 *          на телефоне, статусная точка, .page-head .sub)
 *   К1-Б   каркас: навигация, шапка, таб-бар, вход, приглашение, профиль
 *   К2     пульт парка: KPI-приборка, плитки объектов, лента, карта
 *   К3     аварии, история и отчёты, график
 *   К4     мнемосхема объекта и faceplate
 *   К5     объект «Подробно», оперативные правки, ряд действий, журнал команд
 *   К6     конфигуратор
 *   К7     структура парка, связи плат, группы, новые платы, прошивки
 *   К8     доступы, журнал (аудит), настройки мониторинга, блокировки
 *   К10    справочники, карта Modbus, подсказки датчиков (.cat- .mbx- .snx-)
 *
 * ⚠ ЛОВУШКА, ЗАМЕЧЕННАЯ ПРИЁМКОЙ ВОЛНЫ 7. У К10 своего маркера НЕТ: правила
 *   справочников писались раньше регионов и лежат вразброс между 2740 и 4440,
 *   то есть ВНУТРИ регионов К6, К2 и К4. Регион кончается не там, где кончается
 *   ваша страница, а на следующем маркере — поэтому «правлю до конца региона»
 *   в этой полосе означает «правлю чужие справочники». Ищите свои селекторы
 *   поимённо, а не по границам маркеров. Свести .cat-/.mbx-/.snx- в один регион
 *   — работа отдельной правки: это перенос ~170 строк, и делать его вместе с
 *   содержательными правками нельзя, иначе в диффе не видно ни того, ни другого.
 * ═══════════════════════════════════════════════════════════════════════════ */

* { box-sizing: border-box; }
/* базовый стиль линейных иконок (nav/tabbar/«Ещё») — размер переопределяется */
.ic svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
/* ── ОДНО МЕСТО, КОТОРОЕ ДЕЛАЕТ ИКОНКУ ИКОНКОЙ. До волны 7 набор ICONS
   стилизовали ТРИ независимых механизма: правило `.ic svg` (для обёрнутых),
   атрибуты, которые дописывал iconSized() (для необёрнутых), и точечные
   правила страниц (.ovr-ban, .lg-ban). Гарантии, что значок из ICONS выглядит
   как значок из ICONS, не было ни у одного — и любая забытая обёртка давала
   на экране чёрный прямоугольник 300×150 (браузерный размер по умолчанию для
   <svg> без размеров плюс чёрная заливка по умолчанию).
   Класс km-ic ставит сам icon() в main.js, то есть он есть у КАЖДОЙ иконки
   набора, как бы её ни вставили. Размер здесь намеренно не задаётся: его несёт
   либо обёртка .ic, либо атрибуты width/height от iconSized() — правило
   :not([width]) страхует только случай «забыли и то, и другое».
   Почему не общее `svg:not([stroke])`: у графиков (chart.js) корневой <svg>
   тоже без stroke, и унаследованный fill:none погасил бы в них подписи осей —
   страховка «на всё» сломала бы то, что сейчас работает. */
svg.km-ic { stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
svg.km-ic:not([width]) { width: 1.15em; height: 1.15em; }
/* ЛОГИЧЕСКОЕ НАПРАВЛЕНИЕ. Стрелка «дальше», шеврон раскрытия и знак перехода
   указывают ПО ходу чтения. В арабской раскладке ход чтения обратный, а сама
   иконка нарисована один раз — поэтому направление задаётся не вторым файлом
   и не другим глифом, а зеркалом на уровне темы. Ставится на любую иконку,
   которая означает направление, и только на неё (часы, корзина, колокол
   зеркалить нельзя — они направления не несут). Класс ставится РОВНО НА ОДИН
   узел цепочки (на сам <svg> или на его обёртку): два вложенных .dir-flip дают
   двойное зеркало, то есть отсутствие зеркала. */
html[dir="rtl"] .dir-flip { transform: scaleX(-1); }
html, body { margin: 0; height: 100%; }
html { color-scheme: dark; }
body {
  background:
    radial-gradient(1200px 700px at 80% -8%, rgba(51,224,163,.05), transparent 60%),
    radial-gradient(900px 600px at 0% 100%, rgba(14,111,92,.06), transparent 55%),
    var(--bg);
  background-attachment: fixed;
  color: var(--ink);
  font: var(--fs-val)/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
::selection { background: var(--brand); color: var(--selection-ink); }
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* ── ЭЛТ-слой: сканлайны + виньетка на всё поле (без DOM, поверх контента,
 *    не перехватывает клики). Статичный — операторский пульт не должен мерцать. */
body::before {
  content: ""; position: fixed; inset: 0; z-index: 9998; pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px);
  mix-blend-mode: multiply; opacity: .55;
}
body::after {
  content: ""; position: fixed; inset: 0; z-index: 9997; pointer-events: none;
  background: radial-gradient(ellipse 100% 85% at 50% 42%, transparent 60%, rgba(0,0,0,.42));
}

/* тонкие скроллбары под тему */
* { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb-hi) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: var(--r-sm); border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hi); background-clip: content-box; }

.app { display: flex; min-height: 100vh; }

/* === К1-Б: каркас — навигация, шапка, свёрнутая рейка, таб-бар === */
/* ───────────────────────────────────────────────────────── навигация ─── */
.nav {
  width: var(--nav-w, 232px); flex: 0 0 auto;
  background: linear-gradient(180deg, var(--bg-2), var(--bg));
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh; z-index: 30;
  transition: width .16s ease;
}
@media (prefers-reduced-motion: reduce) { .nav { transition: none; } }
.nav .brand {
  display: flex; align-items: center; gap: 11px;
  padding: 18px 18px 15px;
}
.nav .brand .mark {
  width: 38px; height: 38px; border-radius: 9px; flex: 0 0 auto;
  text-decoration: none;
  background: radial-gradient(120% 120% at 30% 20%, #14342c, #08130f);
  border: 1px solid rgba(51,224,163,.35);
  color: var(--brand); display: grid; place-items: center;
  font-family: var(--kanji); font-weight: 800; font-size: 21px; line-height: 1;
  text-shadow: 0 0 16px var(--brand-glow);
  box-shadow: inset 0 0 14px -6px var(--brand-glow), 0 0 0 3px rgba(51,224,163,.04);
}
.nav .brand .nm { font-family: var(--disp); font-weight: 700; font-size: var(--fs-h2); letter-spacing: .22em; text-transform: uppercase; line-height: 1.1; }
/* Первая строка, которую видит человек, до волны давала 3,67:1 (9,5 px цветом
   --ink-3). Кегль — нижняя ступень шкалы, цвет — --ink-2: --ink-3 по
   собственному правилу темы предназначен только для плейсхолдеров. */
.nav .brand small { display: block; font: 500 var(--fs-tag)/1.4 var(--mono); letter-spacing: .28em; color: var(--ink-2); text-transform: uppercase; margin-top: 3px; }
/* Колокольчик уведомлений — вторая точка взгляда после марки, и до волны он
   был сплошным чёрным пятном: <svg> шёл без обёртки .ic, правило `.ic svg` до
   него не доставало, и браузер применял дефолт замещаемого элемента (чёрная
   заливка, 300×150 в ячейке 30×30). В светлой теме это была единственная
   сплошная чёрная фигура на экране, в тёмной колокольчика не было видно вовсе.
   Обёртка добавлена в main.js; здесь — геометрия внутри неё и якорь бейджа
   (счётчик наследовал margin-left:auto от пункта меню и висел сбоку). */
.nav .brand .bell { position: relative; text-decoration: none; color: var(--ink-2); display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px; flex: 0 0 auto; }
.nav .brand .bell:hover { color: var(--brand); background: var(--brand-soft); }
.nav .brand .bell .ic { display: grid; place-items: center; }
.nav .brand .bell .ic svg { width: 18px; height: 18px; }
/* Счётчик — В УГОЛ, а не поверх колокольчика: двузначное число накрывало сам
   знак, и от единственной точки входа в уведомления оставалась красная
   таблетка без опоры. inset-inline-end, а не right, — в арабской раскладке
   панель зеркалится. */
.nav .brand .bell .badge-n {
  position: absolute; top: -5px; inset-inline-end: -8px; margin: 0;
  min-width: 16px; padding: 0 5px; line-height: 15px;
}

.nav .scroll { flex: 1; overflow-y: auto; padding: 4px 10px 8px; }
.nav a.item {
  display: flex; align-items: center; gap: 11px; position: relative;
  padding: 8px 12px; margin: 1px 0; border-radius: 8px;
  color: var(--ink-2); text-decoration: none; font-size: var(--fs-val); font-weight: 500;
  transition: background .14s, color .14s;
}
.nav a.item:hover { background: var(--panel-2); color: var(--ink); text-decoration: none; }
.nav a.item.active { background: var(--brand-soft); color: var(--ink); font-weight: 600; }
.nav a.item.active::before {
  /* Метка активного пункта стоит у КРАЯ РЕЙКИ, а не слева: в арабской
     раскладке рейка живёт справа, и физический left уводил метку внутрь
     страницы. Скругление тоже логическое — оно смотрит внутрь пункта. */
  content: ""; position: absolute; inset-inline-start: -10px; top: 7px; bottom: 7px; width: 3px;
  border-start-start-radius: 0; border-end-start-radius: 0;
  border-start-end-radius: 3px; border-end-end-radius: 3px;
  background: var(--brand); box-shadow: 0 0 10px var(--brand-glow);
}
.nav a.item .ic { width: 20px; height: 20px; flex: 0 0 auto; display: grid; place-items: center; opacity: .85; }
.nav a.item .ic svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.nav a.item.active .ic { color: var(--brand); opacity: 1; }
.nav .sep { margin: 9px 12px 8px; border-top: 1px solid var(--line-2); position: relative; }
/* Подпись секции рейки начинается там же, где начинаются её пункты: в
   арабской раскладке это правый край, поэтому логические свойства. */
.nav .sep[data-label]::after { content: attr(data-label); position: absolute; top: -7px; inset-inline-start: 0; background: var(--bg); padding-inline-end: 8px; font: 600 var(--fs-tag) var(--mono); letter-spacing: .22em; text-transform: uppercase; color: var(--ink-2); }
.nav .foot { padding: 12px 16px; border-top: 1px solid var(--line-2); font-size: var(--fs-body); color: var(--ink-2); }
.nav .foot .who { font-family: var(--mono); font-size: var(--fs-micro); color: var(--ink); }
.nav .foot a { color: var(--ink-2); }
/* Число активных аварий — самая важная цифра для диспетчера, и до волны она
   была нечитаема: белый на --alarm #ff4d3f = 3,29:1 при 10,5 px. Сам --alarm не
   трогаем (бриф §2) — заливку бейджа берём с локального токена --alarm-deep
   (тот же #cf2418, что уже стоял в кнопке danger): белый на нём 5,36:1 в тёмной
   теме и 9,32:1 в светлой. Свечение остаётся аварийным — бейдж по-прежнему
   читается как красный сигнал, а не как нейтральный счётчик. */
.nav .badge-n {
  margin-left: auto; background: var(--alarm-deep); color: var(--alarm-ink-on);
  font: 700 var(--fs-tag) var(--mono);
  border-radius: 999px; padding: 1px 6px; min-width: 18px; text-align: center;
  box-shadow: 0 0 10px -1px var(--alarm-glow);
}
.nav .foot-out { display: none; }

/* ── тумблер «свернуть меню»: шеврон на кромке панели ── */
.nav-toggle {
  /* inset-inline-end, а не right: в арабской раскладке панель уходит вправо, и
     физический «правый минус 13» выносил кнопку «свернуть меню» наполовину ЗА
     край вьюпорта — управление меню становилось недоступным в языке, RTL
     которого продукт заявляет поддержанным. */
  position: absolute; top: 26px; inset-inline-end: -13px; z-index: 31;
  width: 26px; height: 26px; padding: 0; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink-2);
  box-shadow: var(--elev-2);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.nav-toggle:hover { color: var(--brand); border-color: var(--brand); background: var(--panel-3); }
.nav-toggle .ic svg { width: 16px; height: 16px; }

/* ── свёрнутое меню: рейка с одними иконками ── */
body.nav-collapsed { --nav-w: 64px; }
body.nav-collapsed .nav .lbl { display: none; }
/* Рейка 64 px: марка и колокольчик в один ряд не помещаются (38 + 11 + 30),
   поэтому шапка встаёт колонкой. Колокольчик обязан пережить сворачивание —
   индикатор, исчезающий при смене режима меню, хуже отсутствующего: человек
   читает пустое место как «новых событий нет». */
body.nav-collapsed .nav .brand { padding: 16px 0 13px; justify-content: center; flex-direction: column; gap: 9px; }
body.nav-collapsed .nav .scroll { padding: 4px 8px 8px; }
body.nav-collapsed .nav a.item { justify-content: center; padding: 10px 0; gap: 0; }
body.nav-collapsed .nav a.item.active::before { left: -8px; }
body.nav-collapsed .nav .sep { margin: 9px 10px 8px; }
body.nav-collapsed .nav .sep[data-label]::after { display: none; }
/* бейдж аварий в рейке — точкой поверх иконки, цифра не влезает */
body.nav-collapsed .nav .badge-n {
  position: absolute; top: 4px; inset-inline-end: 9px; margin: 0;
  min-width: 16px; padding: 0 4px; font-size: var(--fs-tag);
}
body.nav-collapsed .nav .foot { padding: 12px 0; display: flex; justify-content: center; }
/* В рейке подпись «кто вошёл» прячется вместе с .lbl, и до волны там не было
   видно ВООБЩЕ, под кем открыт пульт: на общем компьютере смены это прямая
   дорога к действию не от своего имени. Кружок с инициалом возвращает ответ на
   «кто я», а полное имя и роль лежат в title кнопки выхода. */
body.nav-collapsed .nav .foot { flex-direction: column; align-items: center; gap: 8px; }
body.nav-collapsed .nav .foot-out { display: flex; flex-direction: column; align-items: center; gap: 5px; color: var(--ink-2); border-radius: 8px; padding: 4px 0; width: 44px; }
body.nav-collapsed .nav .foot-out:hover { color: var(--brand); background: var(--brand-soft); text-decoration: none; }
body.nav-collapsed .nav .foot-out .who-ini {
  width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  font: 700 var(--fs-micro) var(--mono); letter-spacing: 0;
  color: var(--ink); background: var(--panel-2); border: 1px solid var(--line);
}
body.nav-collapsed .nav .foot-out:hover .who-ini { border-color: var(--brand); }
/* контент забирает освободившуюся ширину и дышит крупнее (шаг шкалы, а не «на
   глаз»: 34 px не лежали ни на 4, ни на 8, а под них подстроена липкая шапка) */
body.nav-collapsed .main { padding: var(--sp-5) var(--gutter-wide) 40px; }

/* мобайл: нижний таб-бар */
.tabbar { display: none; }
@media (max-width: 860px) {
  .nav { display: none; }
  .tabbar {
    display: flex; position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
    background: rgba(8,11,12,.92); backdrop-filter: blur(12px);
    border-top: 1px solid var(--line);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .tabbar a {
    flex: 1; text-align: center; padding: 9px 0 7px; text-decoration: none;
    color: var(--ink-2); font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em;
    position: relative;
  }
  .tabbar a.active { color: var(--brand); }
  .tabbar a.active::before { content: ""; position: absolute; top: 0; left: 30%; right: 30%; height: 2px; background: var(--brand); box-shadow: 0 0 8px var(--brand-glow); }
  .tabbar a .ic { display: grid; place-items: center; height: 22px; margin-bottom: 2px; }
  .tabbar a .ic svg { width: 21px; height: 21px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
  /* Счётчик аварий висит на том же плече значка в любой раскладке: с
     физическим left в арабской он перескакивал на другую сторону иконки. */
  .tabbar a .badge-n { position: absolute; top: 4px; inset-inline-start: 54%; }
  .main { padding-bottom: 68px; }
}

/* ─────────────────────────────────────────────────────────── layout ─── */
.main { flex: 1; min-width: 0; padding: var(--sp-5) var(--gutter) 40px; animation: page-in var(--dur-3) var(--ease); }
@keyframes page-in { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .main { animation: none; } }

.page-head { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-4); }
.page-head h1 {
  font-family: var(--disp); font-weight: 700; font-size: var(--fs-h1); letter-spacing: var(--ls-tight); margin: 0;
  display: flex; align-items: center; gap: var(--sp-3);
}
/* ── ЛИПКАЯ ШАПКА СТРАНИЦЫ. Страница объекта — 1446 px (а «Подробно» — 6587 px);
   на прокрутке 700 px не видно ни имени объекта, ни его статуса, ни команд
   «Включить обслуживание» / «Вернуть автоматике», при том что боковое меню
   липкое — асимметрия, из-за которой диспетчер, разбирающий историю событий,
   теряет ответ на вопрос «о каком объекте речь».
   Шапка полосуется на всю ширину .main отрицательными полями, иначе под ней
   просвечивал бы контент в боковых отступах страницы. Липкость включается
   только на десктопе: на 390 px экран короткий, а 58 px постоянной шапки — это
   восьмая часть высоты телефона.
   Липкая шапка ТАБЛИЦЫ уезжает под неё (--head-h), иначе две «прилипшие» полосы
   встают друг на друга и шапка колонок перекрывает заголовок страницы. */
@media (min-width: 861px) {
  .page-head {
    position: sticky; top: 0; z-index: 20;
    background: var(--bg);
    margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter);
    margin-block-start: calc(var(--sp-5) * -1); padding-block: var(--sp-3);
    border-bottom: 1px solid var(--line-2);
  }
  body.nav-collapsed .page-head { margin-inline: calc(var(--gutter-wide) * -1); padding-inline: var(--gutter-wide); }
}
/* ⚠ Липкую шапку таблицы под липкую шапку страницы уводить НЕЛЬЗЯ (проверено
   кадром alarms_l_vp.png): таблицы продукта лежат в обёртке с собственной
   прокруткой, и для th «верх окна прокрутки» — это верх обёртки, а не верх
   экрана. Смещение top:58px опускало шапку колонок на 58 px внутрь таблицы и
   рвало первую строку. Конфликта двух липких полос при этом нет: шапка колонок
   прилипает к верху своей обёртки и до верха экрана не доходит. */
.page-head h1::before {
  /* Реперная метка-эйдж. На десктопе её НЕТ: в 230 px левее, в той же
     горизонтали, стоит марка меню .nav .brand .mark с тем же знаком 守 —
     повтор одного знака дважды в одной строке ничего не кодирует, это чистое
     украшение («перед выходом сними одно украшение»). Метка остаётся ровно
     там, где марки не видно: на телефоне (меню скрыто в таб-баре) и при
     свёрнутой рейке (марка ужимается до полоски). */
  content: none;
  font-family: var(--kanji); font-size: var(--fs-h2); color: var(--brand);
  opacity: .5; text-shadow: 0 0 12px var(--brand-glow); font-weight: 800;
}
body.nav-collapsed .page-head h1::before { content: "守"; }
@media (max-width: 860px) { .page-head h1::before { content: "守"; } }
/* Подзаголовок — КОНТЕКСТ страницы (кто / что / сколько), одна короткая
   строка рядом с h1. Длинный .sub переносился на отдельную строку и повисал
   оторванным от заголовка, поэтому длина ограничена явно: всё, что длиннее, —
   это уже пояснение, и ему место в .page-note. Контракт — pageHead() в ui.js. */
.page-head .sub {
  color: var(--ink-2); font-size: var(--fs-body); font-family: var(--mono);
  max-width: 40ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.spacer, .page-head .spacer { flex: 1; }

/* Мера страницы и мера абзаца (F-022). Классы-утилиты, а не глобальное правило
   на .main: пульт парка, карта и мнемосхема обязаны занимать всю ширину, и
   единая «колонка по центру» испортила бы именно главные экраны. Владелец
   страницы вешает .measured на свой контейнер-список/форму и .prose — на
   строку-пояснение. На 1920 без этого действия строки «Доступов» отстояли от
   логина на 1260 px, а пояснения шли в 190–210 знаков при комфортных 45–90. */
.measured { max-width: var(--content-max); }
.prose { max-width: var(--prose-max); }

/* ═══ ПРАВИЛО УСЕЧЕНИЯ ТЕКСТА ═════════════════════════════════════════════
   Продукт режет текст в трёх местах — подписи на мнемосхеме, колонки таблиц,
   панели на 390 px — и каждое место режет по-своему. Итог виден на кадрах:
   «Давлени… 3,48 бар», две РАЗНЫЕ величины, подписанные одинаково
   («ТЕМПЕРАТУРА ПОД…» и «ТЕМПЕРАТУРА ПОД…»), и — самое дорогое — обрезанное
   ЧИСЛО «253 918,» в панели накопителей. Подпись, которую нельзя дочитать,
   хуже отсутствующей: она занимает место и не отвечает на вопрос.

   Правило продукта, три класса:
     .trunc     можно сократить — это ИМЯ (объекта, платы, канала, человека).
                Обязателен title с полным текстом: сокращение без возможности
                прочитать целиком — потеря данных, а не экономия места;
     .clamp-2   лучше перенести — это ФРАЗА (пояснение, причина, адрес).
                Две строки и многоточие в конце второй;
     .nowrap    НИКОГДА не режется и не переносится — число, единица,
                идентификатор, время, диапазон. Число без последнего разряда
                это неверное число, а не сокращённое.
   Мера сокращения одна на продукт (--label-max), чтобы имена в соседних
   колонках обрывались на одной ширине, а не «кто где успел». */
.trunc {
  min-width: 0; max-width: var(--label-max);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.clamp-2 {
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;
}
.nowrap, .v, .num { white-space: nowrap; }

/* ССЫЛКА «ДАЛЬШЕ» — один компонент на весь продукт (F-056). Три самых заметных
   перехода («журнал →» на пульте парка, «отчёты →» в объекте, «все команды →»
   в ряду действий) были набраны тремя разными способами — .small с --ink-2,
   .small.muted с --ink-3 и голым --brand, — а стрелка стояла ЗНАКОМ ВНУТРИ
   СТРОКИ и потому не переворачивалась в арабской раскладке: в RTL она
   показывала назад. Здесь вид один, а направление несёт иконка с классом
   .dir-flip, то есть оно зеркалится вместе с макетом.
   Разметка: <a class="more-link tap"><span>журнал</span>+iconSized("arrowRight",12)
   с классом dir-flip</a>. Текст — БЕЗ стрелки. */
.more-link {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
  color: var(--brand); text-decoration: none;
}
.more-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.more-link svg { opacity: .85; }
/* Мера абзаца внутри центрированного состояния (пусто/ошибка/нет доступа):
   без авто-полей ограниченный по ширине текст прижимался бы к левому краю
   карточки, а карточка выровнена по центру — получалась «ступенька». */
.empty .prose, .modal .prose { margin-inline: auto; }

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--elev-1);
  position: relative;
}
/* Заголовок карточки и шапка таблицы внутри неё стоят на ОДНОЙ вертикали.
   Замер до волны 7 («Прошивки», 1280): левый край карточки 258, текст
   заголовка «Платы парка» — 275, а текст заголовка колонки «ПЛАТА» и самой
   ячейки — 271. Четыре пикселя повторялись на «Доступах», «Журнале»,
   «Авариях», «Истории» — везде, где таблица лежит в карточке. Глаз видит
   сломанную вертикаль, не находит причины и читает это как небрежность,
   поэтому горизонтальный отступ у заголовка, тела и ячеек — одно число. */
.card .card-h {
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--line-2); font-weight: 600;
  display: flex; align-items: center; gap: var(--sp-2);
  font-family: var(--disp); font-size: var(--fs-val); letter-spacing: var(--ls-tight);
}
.card .card-b { padding: var(--sp-4); }
.grid { display: grid; gap: var(--sp-3); }

/* ──────────────────────────────────────────────────────────── chips ─── */
/* СЕМАНТИКА ЧИПА — контракт для всех страниц (F-049). Вид чипа выбирается не
   по «настроению строки», а по цене события:
     .chip.alarm  ТОЛЬКО авария — то, ради чего человек бросает дела и едет на
                  объект. Шапка этой темы обещает: «КРАСНЫЙ зарезервирован
                  строго под аварию». Красный на рутине (четыре «неудачный
                  вход» подряд в журнале, «36 ↓ ниже нормы» в отчёте по
                  моточасам) обесценивает сам сигнал: цвет перестаёт значить
                  «бросай всё и беги», а на этом держится пульт парка;
     .chip.warn   внимание: отклонение, которое требует решения, но не сегодня
                  (ниже норматива, откат прошивки, срок ТО подходит);
     .chip.norm   норма, подтверждение;
     .chip.neutral факт без оценки — вид события в журнале, вход в систему,
                  код канала, тип отчёта. Большинство чипов в продукте — эти;
     .chip.offline/.manual/.maint  режимы связи и управления. */
.chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  border-radius: var(--r-pill); padding: var(--sp-1) var(--sp-3); font: 600 var(--fs-body) var(--sans);
  border: 1px solid transparent; white-space: nowrap; letter-spacing: var(--ls-tight);
}
/* геометрия и цвет точки — в общем компоненте .sdot ниже (К1-А2) */
/* авария/внимание: рамка плотнее + CRT-glow чипа целиком (текст и коробка) —
   должно считываться боковым зрением; светлая тема ниже глушит свечение и
   вместо него заливает чип «сургучом»/охрой целиком */
/* Границы выводятся ИЗ ТОКЕНА СТАТУСА, а не пишутся литералом rgba(255,77,63,.55):
   литерал знает только ночное значение, поэтому днём рамка «нормы» оказывалась
   бледнее самой бумаги и чинилась отдельной заплаткой в светлом блоке. С
   color-mix обе темы получают рамку одной и той же ПЛОТНОСТИ от своего цвета —
   светлая тема перестаёт быть набором заплаток и становится сменой ролей. */
.chip.alarm    { background: var(--alarm-soft);  color: var(--alarm-ink);  border-color: color-mix(in srgb, var(--alarm) 55%, transparent);
  box-shadow: var(--glow-md) var(--alarm-glow); text-shadow: 0 0 9px var(--alarm-glow); }
.chip.warn     { background: var(--warn-soft);   color: var(--warn-ink);   border-color: color-mix(in srgb, var(--warn-line) 80%, transparent);
  box-shadow: var(--glow-sm) var(--warn-glow); text-shadow: 0 0 8px var(--warn-glow); }
.chip.norm     { background: var(--ok-soft);     color: var(--ok-ink);     border-color: color-mix(in srgb, var(--ok) 34%, transparent); }
.chip.offline  { background: var(--offline-soft); color: var(--offline);   border-color: color-mix(in srgb, var(--offline) 34%, transparent); }
.chip.neutral  { background: var(--panel-2); color: var(--ink-2); border-color: var(--line); }
.chip.manual   { background: var(--manual-soft); color: var(--manual); border-color: color-mix(in srgb, var(--manual) 30%, transparent); }
.chip.maint    { background: var(--maint-soft);  color: var(--maint);  border-color: color-mix(in srgb, var(--maint) 30%, transparent); }
/* Пульс точки — только размером. Прозрачность из кадра убрана: мигающий
   индикатор аварии человек ЖДЁТ погаснуть и невольно перестаёт его замечать,
   а на скриншоте, в печати и в момент паузы кадра его может не быть вовсе. */
@keyframes pulse-dot { 50% { transform: scale(.78); } }

/* === К1-А2: статусная точка — ОДИН компонент вместо семи === */
/* Атом всего пульта: он встречается по 30–50 раз на экране. Реализаций было
   семь — четыре размера (7/8/9/10 px), пять радиусов свечения (7–11 px),
   пульсация у трёх из семи, а точка в легенде мнемосхемы вообще без свечения:
   образец не был похож на то, что объясняет. Когда один и тот же сигнал каждый
   раз чуть другой, экран «дребезжит» и глаз не может его выучить.

   Здесь — одно правило. Старые имена (.dot в чипе, .ot-dot на плитке объекта,
   .lg-dot в легенде схемы, .oa-dot в ленте действий) оставлены ПСЕВДОНИМАМИ:
   разметку страниц переписывают их владельцы, а вид обязан сойтись сегодня.
   Размер задаётся ступенью (sm 7 / md 9 / lg 11), авария и внимание растут на
   одну и ту же величину — размер тоже несёт смысл, и он один на весь продукт.
   Позиционирование точки (absolute у плитки объекта) остаётся у страницы:
   это компоновка, а не сам сигнал. */
.sdot, .chip .dot, .obj-tile .ot-dot, .schm-legend .lg-dot, .oa-feed .oa-dot {
  --dot-sz: 9px; --dot-c: var(--ink-3); --dot-g: transparent;
  display: inline-block; flex: 0 0 auto; border-radius: 50%;
  width: var(--dot-sz); height: var(--dot-sz);
  background: var(--dot-c); box-shadow: 0 0 calc(var(--dot-sz) + 1px) var(--dot-g);
}
.sdot.sm, .chip .dot, .oa-feed .oa-dot { --dot-sz: 7px; }
.sdot.md, .obj-tile .ot-dot, .schm-legend .lg-dot { --dot-sz: 9px; }
.sdot.lg { --dot-sz: 11px; }
.sdot.norm, .chip.norm .dot, .obj-tile .ot-dot, .schm-legend .lg-dot.norm, .oa-feed .oa-dot.norm {
  --dot-c: var(--ok); --dot-g: var(--ok-glow);
}
.sdot.warn, .chip.warn .dot, .obj-tile.warn .ot-dot, .schm-legend .lg-dot.warn, .oa-feed .oa-dot.warn {
  --dot-c: var(--warn); --dot-g: var(--warn-glow); width: calc(var(--dot-sz) + 1px); height: calc(var(--dot-sz) + 1px);
}
.sdot.alarm, .chip.alarm .dot, .obj-tile.alarm .ot-dot, .schm-legend .lg-dot.alarm {
  --dot-c: var(--alarm); --dot-g: var(--alarm-glow);
  width: calc(var(--dot-sz) + 2px); height: calc(var(--dot-sz) + 2px);
  animation: pulse-dot 1.1s infinite;
}
.sdot.manual, .schm-legend .lg-dot.man { --dot-c: var(--manual); --dot-g: transparent; }
.sdot.maint { --dot-c: var(--maint); --dot-g: transparent; }
/* Офлайн — ПУСТОЕ КОЛЬЦО: форма, а не только цвет. Серо-зелёная заливка на
   расстоянии читается как «норма», и два противоположных состояния сливаются. */
.sdot.offline, .chip.offline .dot, .obj-tile.offline .ot-dot {
  background: transparent; box-shadow: inset 0 0 0 2px var(--offline);
}
@media (prefers-reduced-motion: reduce) {
  .sdot.alarm, .chip.alarm .dot, .obj-tile.alarm .ot-dot { animation: none; }
}
/* ДВОЙНИК ТОЙ ЖЕ ТОЧКИ ДЛЯ SVG (контракт для К4 — мнемосхема и легенда).
   На холсте схемы точку нельзя нарисовать <span>-ом: там свои единицы и своя
   система координат, поэтому статус живёт кружком <circle>. Раньше это была
   отдельная реализация со своими цветами — и образец в легенде не совпадал с
   тем, что он объясняет. Здесь у SVG-точки те же ТОКЕНЫ и та же лестница
   размеров, что у DOM-точки: меняется только способ покраски (fill вместо
   background, drop-shadow вместо box-shadow — box-shadow на SVG-фигуру не
   действует). Разметка: <circle class="sdot-s alarm" r="4"> — статус классом,
   радиус атрибутом; ступени задаёт r (sm 3,5 · md 4,5 · lg 5,5). */
.sdot-s { --dot-c: var(--ink-3); --dot-g: transparent; fill: var(--dot-c); stroke: none; }
.sdot-s.norm    { --dot-c: var(--ok);      --dot-g: var(--ok-glow); }
.sdot-s.warn    { --dot-c: var(--warn);    --dot-g: var(--warn-glow); }
.sdot-s.alarm   { --dot-c: var(--alarm);   --dot-g: var(--alarm-glow); animation: pulse-dot 1.1s infinite; }
.sdot-s.manual  { --dot-c: var(--manual); }
.sdot-s.maint   { --dot-c: var(--maint); }
.sdot-s.offline { fill: none; stroke: var(--offline); stroke-width: 2; }
.sdot-s.norm, .sdot-s.warn, .sdot-s.alarm { filter: drop-shadow(0 0 4px var(--dot-g)); }
@media (prefers-reduced-motion: reduce) { .sdot-s.alarm { animation: none; } }

/* значения — моноширь с табличными цифрами */
.v { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* === К2: пульт парка — KPI-приборка (.counters/.counter) === */
/* ────────────────────────────────────────────────────────────── kpi ─── */
/* Ряд разделён на ДВЕ группы, и это иерархия, а не украшение.
   Слева (.c-status) — четыре состояния парка кнопками-фильтрами, крупными
   цифрами; справа (.c-sum) — сводка «объектов / на связи / команд в пути» и
   отметка среза времени тихой серой строкой без рамок.
   До волны в ряду стояли семь равновеликих плиток шести цветов, и самой
   заметной была нейтральная белая цифра «Объектов» — взгляд первым ловил самое
   бесполезное. Крупная цифра и ширина — ресурс приоритета, он отдан аварии
   (одна смелость на экран). */
.counters { display: flex; align-items: stretch; gap: var(--sp-3) var(--sp-5); flex-wrap: wrap; }
/* Четыре состояния — ГРИД, а не флекс. Во флексе ширину плитки задавала длина
   подписи («ВНИМАНИЕ» против «НОРМА»), и приборный ряд — первое, что видит
   диспетчер, заступая на смену, — стоял рваным: 128…175 px. Сетка из равных
   долей делает ряд ровным при любых словах и в любом языке (в английском и
   арабском длины другие), а «одна смелость» никуда не девается: аварийная
   доля шире остальных и её цифра крупнее.
   max-width — чтобы на широком мониторе четыре плитки не растянулись на всю
   стену: приборный ряд читается как ряд, пока он компактный. */
.c-status {
  display: grid; grid-template-columns: 1.3fr 1fr 1fr 1fr; gap: var(--sp-2);
  flex: 1 1 640px; max-width: 900px;
}
.counter {
  display: flex; align-items: center; gap: 11px; cursor: pointer;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--line); border-radius: var(--r); padding: 11px 16px 11px 14px;
  min-width: 0; position: relative; overflow: hidden;
  font: inherit; color: inherit; text-align: start;
  transition: border-color var(--dur-1), transform var(--dur-1), box-shadow var(--dur-1);
}
/* Полоса статуса стоит в НАЧАЛЕ плитки, а не слева: её работа — вести взгляд
   к первому слову, и в арабской раскладке начало справа. С физическим left она
   оставалась у конца строки и переставала работать вовсе (тот же класс, что
   волна чинила у тумблера .switch). */
.counter::before { content: ""; position: absolute; inset-inline-start: 0; top: 0; bottom: 0; width: 3px; background: var(--ink-3); opacity: .6; }
.counter:hover { transform: translateY(-1px); border-color: var(--line); }
.counter b {
  font-family: var(--disp); font-weight: 900; font-size: var(--fs-count); line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
/* Разрядка — по шкале (--ls-cap), а не .12em: собственное значение разгоняло
   «НЕТ СВЯЗИ» шире доли сетки и роняло подпись на вторую строку.
   nowrap здесь обязателен: подпись состояния, разорванная переносом, читается
   как две разные подписи. */
.counter i { font-style: normal; font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase; color: var(--ink-2); white-space: nowrap; }
.counter.sel { border-color: var(--brand); box-shadow: var(--glow-brand); }
.counter.alarm  b { color: var(--alarm-ink); text-shadow: 0 0 20px var(--alarm-glow); }
.counter.alarm::before  { background: var(--alarm); box-shadow: 0 0 10px var(--alarm-glow); }
.counter.warn   b { color: var(--warn-ink); text-shadow: 0 0 16px var(--warn-glow); }
.counter.warn::before   { background: var(--warn); box-shadow: 0 0 10px var(--warn-glow); }
.counter.offline b { color: var(--offline); }
.counter.offline::before {
  background: repeating-linear-gradient(-45deg, var(--offline) 0 5px, transparent 5px 9px); }
.counter.norm   b { color: var(--ok-ink); }
.counter.norm::before   { background: var(--ok); }
/* Единственная смелость экрана: аварийная плитка физически шире и её цифра
   крупнее остальных. Кегль берётся ОТ токена шкалы (--fs-count), а не литералом,
   чтобы вся приборка двигалась одной ручкой. */
/* Ширину аварийной доли задаёт сетка (1.3fr) — фиксированный min-width здесь
   ломал бы равенство остальных трёх. */
.c-status .counter.alarm { min-width: 0; }
.c-status .counter.alarm b { font-size: calc(var(--fs-count) * 1.3); }

/* НОЛЬ НЕ СВЕТИТСЯ. Плитка «0 ВНИМАНИЕ» с янтарным свечением и «0 АВАРИЯ» с
   красным — это alarm fatigue: если тревожно выглядит спокойный парк, настоящая
   тревога теряет силу (ISA-101 и обещание темы «красный строго под аварию»).
   Ноль не притворяется и кнопкой: у него нет ни курсора, ни hover, ни
   обработчика (park.js вешает .still и aria-disabled).
   Цифра гасится до --ink-2, а не до --ink-3: ноль — это ДАННЫЕ, и правило
   «--ink-3 не несёт данные» сильнее желания приглушить посильнее. */
.counter.zero b { color: var(--ink-2); text-shadow: none; }
.counter.zero::before { background: var(--line); box-shadow: none; opacity: 1; }
.counter.still { cursor: default; }
.counter.still:hover { transform: none; }

/* Сводка: те же данные, но это размер парка и здоровье канала, а не состояния,
   — им хватает строки петитом без рамок. */
.c-sum {
  display: flex; align-items: center; gap: 4px 16px; flex-wrap: wrap;
  margin-inline-start: auto; padding-inline-start: 4px;
}
.c-sum .si {
  display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap;
  font: 500 var(--fs-micro) var(--mono); color: var(--ink-2);
  background: none; border: 0; padding: 2px 0; text-align: start;
}
.c-sum .si b { font: 700 var(--fs-h2) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); }
.c-sum .si i { font-style: normal; letter-spacing: .08em; text-transform: uppercase; }
.c-sum .si.act { cursor: pointer; }
.c-sum .si.act:hover { color: var(--ink); }
.c-sum .si.sel { color: var(--brand); }
.c-sum .si.sel b { color: var(--brand); }
/* Отметка среза («данные на 12:41:03») стоит В ПРИБОРНОМ РЯДУ и видна всегда, а
   не только в беде: человек привыкает к её ходу и сам замечает, что часы встали. */
.c-sum .si.asof { color: var(--ink-2); font-weight: 500; letter-spacing: .02em; }
.c-sum .si.asof.old { color: var(--warn-ink); font-weight: 700; }

@media (max-width: 860px) {
  /* Тач-цель: приборная плитка — кнопка, и палец обязан по ней попадать. */
  .counter { min-height: var(--ctl-h-lg); }
  .c-sum { margin-inline-start: 0; }
  .c-sum .si { min-height: var(--ctl-h-lg); align-items: center; }
}

/* ─────────────────────────────────────────────────────────── tables ─── */
/* ТАБЛИЦА — ПРИБОР, А НЕ ТЕКСТ. Замер до волны 7 («Журнал», 1280): высота
   строки гуляла от 46 px (одна строка текста) до 59 px (перенос «справочник
   оборудования»), зебры не было, наведения на строку — тоже. В журнале на 24
   экрана взгляд теряет строку на пути от колонки «КОГДА» к «ПОДРОБНОСТИ» и
   читает соседнюю — а в журнале это ответ на вопрос «кто это сделал».
   Лечение из трёх частей: (1) один шаг строки --row-h, (2) очень тихая зебра
   (--line-2 — та же линия, которой рисуются разделители, то есть новый цвет не
   заводится), (3) подсветка строки под курсором на ВСЕЙ ширине. Отступ по
   горизонтали — 16 px, ровно как у заголовка карточки: шапка колонки, ячейка и
   заголовок карточки стоят на одной вертикали. */
table.t { width: 100%; border-collapse: collapse; font-size: var(--fs-val); }
table.t th {
  text-align: left; color: var(--ink-2); font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; background: var(--panel); z-index: 1;
}
table.t td { padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--line-2); vertical-align: middle; }
table.t tbody tr { height: var(--row-h); }
/* Зебра и наведение живут на ЯЧЕЙКЕ: строка в table-layout не принимает фон
   поверх собственного фона ячеек в части браузеров, и полоса получалась рваной.
   Селектор завёрнут в :where() и потому весит НОЛЬ: собственная подсветка
   строки у страницы («выключен правкой», «строка-черновик») обязана побеждать
   зебру, а не наоборот — фон состояния несёт смысл, фон зебры только помогает
   вести взгляд. */
:where(table.t tbody tr:nth-child(even)) td { background: var(--line-2); }
:where(table.t tbody tr:hover) td { background: var(--panel-2); }
table.t tr:last-child td { border-bottom: 0; }
/* ТАБЛИЦА ВО ВСЮ ШИРИНУ КАРТОЧКИ — контракт для владельцев страниц (F-046).
   Отступ ячейки (16 px) равен отступу заголовка карточки, поэтому таблица,
   лежащая прямо в карточке, встаёт с ним на одну вертикаль сама. Но если её
   завернуть в .card-b (или в свою обёртку внутри .card-b), к отступу ячейки
   прибавляется отступ тела — и шапка колонки уезжает от заголовка карточки на
   те же 16 px. Замер на «Прошивках» (1280): «Платы парка» 273, «ПЛАТА» 289.
   Лечение — класс на ОБЁРТКЕ, а не инлайновый style="padding:0" (так уже
   сделано в «Группах» — это и есть след проблемы):
       <div class="card-b tbl-flush"> …таблица… </div>
       <div class="card-b"><div class="ota-tbl tbl-flush"> …таблица… </div></div>
   Вертикальный отступ тела сохраняется: убирается ровно горизонтальный. */
.card-b.tbl-flush, .card-b > .tbl-flush { padding-inline: 0; }
/* === К1-А2: кликабельная строка и карточная раскладка таблиц ≤860 px === */
/* Строка-ссылка. Одного :hover мало: на тач-экране его нет вовсе, а с
   клавиатуры строка была недостижима — стиль фокуса, написанный в теме, не мог
   сработать никогда, потому что фокус на <tr> никто не ставил. Разметку даёт
   clickRow() из ui.js (tabindex, role, Enter/Space), вид — эти три правила. */
table.t tr.click { cursor: pointer; transition: background var(--dur-1); }
table.t tr.click:hover { background: var(--panel-2); }
table.t tr.click:focus-visible {
  background: var(--panel-2); outline: 2px solid var(--brand); outline-offset: -2px;
}

/* КАРТОЧНАЯ ДЕГРАДАЦИЯ ТАБЛИЦ на телефоне.
   На 390 px строка аварии обрезалась по правому краю карточки: колонка
   «Возникло» целиком за экраном, «Т подачи 86,5…» — на полуслове, и ни одного
   признака, что таблицу можно листать вбок (ни тени у кромки, ни половины
   следующей колонки). Диспетчер открывает «Аварии» именно с телефона и именно
   ради «что и когда» — и не видит ни того, ни другого.

   Лекарство: строка становится карточкой, ячейка — парой «подпись → значение».
   Подпись берётся из data-th ячейки; проставляет её td()/tableCards() из ui.js.
   Ячейка без подписи (сводка, «ничего не найдено», колонка кнопок) занимает всю
   ширину карточки — ей подпись не нужна. */
@media (max-width: 860px) {
  table.t, table.t > tbody, table.t > tbody > tr, table.t > tbody > tr > td { display: block; width: 100%; }
  /* Шапка скрыта ВИЗУАЛЬНО, но остаётся в дереве доступности: подписи колонок
     теперь дублируются в каждой карточке, и второй раз читать их не нужно. */
  table.t > thead { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); }
  table.t > tbody > tr {
    border: 1px solid var(--line); border-radius: var(--r); background: var(--panel-2);
    margin-bottom: var(--sp-2); padding: 2px 0;
    /* ⚠ Единый шаг строки --row-h снимается: здесь строка не строка таблицы, а
       БЛОК-карточка из нескольких пар «подпись → значение», и для блока height
       это жёсткая высота, а не минимум. С 44 px карточки наезжали друг на
       друга (проверено кадром alarms_m.png). */
    height: auto;
  }
  table.t > tbody > tr:last-child { margin-bottom: 0; }
  table.t > tbody > tr > td {
    padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--line-2); vertical-align: baseline;
    /* Зебра выключена: строка здесь и так отдельная карточка со своим фоном,
       а полоски внутри карточки читались бы как ещё одно деление. */
    background: transparent;
  }
  table.t > tbody > tr > td:last-child { border-bottom: 0; }
  table.t > tbody > tr > td[data-th] {
    display: grid; grid-template-columns: minmax(6.5em, 38%) minmax(0, 1fr);
    gap: var(--sp-1) var(--sp-3); align-items: baseline;
  }
  table.t > tbody > tr > td[data-th]::before {
    content: attr(data-th);
    font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
    color: var(--ink-2); word-break: break-word;
  }
  /* Ячейка-сетка блокирует растяжение: чип и кнопка — грид-элементы, а грид
     по умолчанию тянет их во всю колонку, и «активен» превращался в полосу
     во весь экран. Текст при этом обязан оставаться тянущимся, иначе длинная
     строка вылезет за карточку. */
  table.t > tbody > tr > td[data-th] > .chip,
  table.t > tbody > tr > td[data-th] > .btn,
  table.t > tbody > tr > td[data-th] > .sdot,
  table.t > tbody > tr > td[data-th] > .badges { justify-self: start; }
  /* Колонки действий страницы приходят с инлайновым white-space:nowrap (он
     держал ряд кнопок в одну строку на десктопе). В карточке ширина другая, и
     ряд ОБЯЗАН переноситься — иначе кнопки уезжают за правый край. */
  table.t > tbody > tr > td { white-space: normal !important; }
  /* Тач-цель строки-ссылки: по контракту ≤860 px не меньше 44 px. */
  table.t > tbody > tr.click { min-height: var(--ctl-h-lg); }
}

/* ─────────────────────────────────────────────────────────── inputs ─── */
.btn {
  appearance: none; border: 1px solid var(--edge); background: var(--panel-2);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-4); font: 600 var(--fs-body) var(--sans);
  /* Высота задаётся ТОКЕНОМ, а не отступами: с padding высота кнопки молча
     зависела от гарнитуры и от того, есть ли внутри иконка, и в одном ряду
     стояли контролы 27 / 35 / 36 / 38 px (F-032). */
  min-height: var(--ctl-h); line-height: 1;
  color: var(--ink); cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease), transform .08s var(--ease);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
}
.btn:hover { border-color: var(--brand); color: var(--brand); background: var(--panel-3); }
/* Нажатие. До волны в теме было ровно два правила :active, и оба про курсор
   grabbing: ни кнопка, ни вкладка, ни строка не показывали нажатие. На пульте
   каждая кнопка шлёт команду реальному насосу, ответ сервера приходит не сразу,
   и без отклика человек жмёт второй раз. На сенсорном экране диспетчерской
   :hover не существует вовсе — :active там единственный возможный отклик. */
.btn:active:not(:disabled) { transform: translateY(1px); filter: brightness(.94); }
.btn.primary { background: linear-gradient(180deg, var(--btn-primary-a), var(--btn-primary-b)); border-color: var(--btn-primary-a); color: var(--btn-primary-ink); text-shadow: none; }
.btn.primary:hover { color: var(--btn-primary-ink); box-shadow: var(--glow-brand); filter: brightness(1.06); }
/* Разрушающее действие — ПЛОСКАЯ глубокая заливка, а не градиент: на верхней
   точке прежнего градиента (#ff5a4d) белый текст давал 3,08:1, то есть кнопка
   с максимальной ценой ошибки («Заблокировать объект», «Удалить аккаунт») была
   единственным компонентом, полностью выпавшим из системы. Замер после правки:
   5,36:1 в тёмной теме, 9,32:1 в светлой. */
.btn.danger { background: var(--alarm-deep); border-color: var(--alarm); color: var(--alarm-ink-on); }
.btn.danger:hover { color: var(--alarm-ink-on); background: var(--alarm); border-color: var(--alarm); box-shadow: 0 0 22px -4px var(--alarm-glow); }
/* Рамка ghost-кнопки — в ОБЕИХ темах. Раньше её возвращала только светлая, и
   восемнадцать действий «Доступов» («Инвайт заново», «Отключить», «Сброс
   второго фактора») в основной тёмной теме выглядели статичным серым текстом:
   главный вопрос интуитивности — «что здесь можно нажать» — оставался без
   ответа именно в той теме, в которой продукт работает по умолчанию. */
.btn.ghost { border-color: var(--line); background: transparent; color: var(--ink-2); }
.btn.ghost:hover { border-color: var(--brand); background: var(--panel-2); color: var(--brand); }
/* Разрушающее действие в строке таблицы отличается от безобидного РАНЬШЕ, чем
   человек успеет прочитать подпись: «Отключить» и «Сброс второго фактора»
   стояли рядом в одном сером цвете, одной высоты, и различались только словом.
   Цвет несёт предупреждение уже В ПОКОЕ — на сенсорном экране диспетчерской
   наведения не существует, и «покажем при hover» там значит «не покажем». */
.btn.ghost.danger { color: var(--alarm-ink); border-color: var(--alarm-edge); background: transparent; }
.btn.ghost.danger:hover { color: var(--alarm-ink); border-color: var(--alarm); background: var(--alarm-soft); box-shadow: none; }
/* Заблокированная кнопка — не полупрозрачный призрак (opacity .42 на градиенте
   давала тёмному тексту 1,4:1), а погашенная кнопка с читаемой подписью:
   человек должен видеть, что кнопка ЕСТЬ, и прочитать в title, почему не жмётся.
   Граница ПУНКТИРНАЯ: недоступность обязана читаться формой, а не только
   цветом — при дальтонизме, в печати и на выцветшем экране проходной пункт
   «серее соседней» не работает. Выключенная кнопка — это ответ на вопрос
   «почему я не могу выпустить конфигурацию», и ответ должен быть виден. */
.btn:disabled, .btn.primary:disabled, .btn.danger:disabled, .btn.ghost:disabled {
  background: var(--panel-2); border: 1px dashed var(--line); color: var(--ink-2);
  opacity: 1; cursor: not-allowed; box-shadow: none; filter: none; transform: none;
}
.btn.sm { padding: var(--sp-1) var(--sp-3); font-size: var(--fs-micro); min-height: var(--ctl-h-sm); }
/* «Команда ушла, ждём плату». Кнопки пульта объекта уходят на реальное реле, и
   ответ приходит через секунды: без видимого признака «уже отправлено» человек
   жмёт второй раз и включает насос дважды. Состояние ставит страница —
   aria-busy="true" (его же читает скринридер), кнопка при этом не нажимается.
   Контракт для владельцев страниц: подпись меняется на глагол в процессе
   («Отправляю…»), после ответа сервера aria-busy снимается. */
.btn[aria-busy="true"] { pointer-events: none; color: var(--ink-2); }
.btn[aria-busy="true"]::before {
  content: ""; width: 13px; height: 13px; flex: 0 0 auto; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: btn-spin .7s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
/* Погашенная кнопка не оживает от наведения. Правило общее для всех видов:
   раньше «Выпустить на плату» в заблокированном состоянии всё равно
   подсвечивалась брендом, и человек считал её рабочей. */
.btn:disabled:hover, .btn.primary:disabled:hover, .btn.danger:disabled:hover, .btn.ghost:disabled:hover {
  background: var(--panel-2); border: 1px dashed var(--line); color: var(--ink-2); box-shadow: none; filter: none;
}

input.in, select.in, textarea.in {
  /* Граница поля — --edge, а не --line: разделитель внутри карточки обязан не
     мешать (1,27:1), граница ПОЛЯ обязана быть видна (WCAG 1.4.11 — 3:1).
     Пока роли были смешаны, на «Настройках мониторинга» и в «Журнале» не было
     видно, где кончается подпись и начинается поле ввода. */
  border: 1px solid var(--edge); border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3);
  font: var(--fs-val) var(--sans); background: var(--field-bg); color: var(--ink);
  box-shadow: var(--field-inset);
  /* Одна высота у всего ряда фильтров (F-032): раньше .in рендерился 36 px в
     «Мониторинге» и 38 px в «Журнале» — высота зависела от гарнитуры значения. */
  min-height: var(--ctl-h); line-height: 1.2;
  outline: none; min-width: 0; transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
textarea.in { line-height: 1.5; }
input.in::placeholder, textarea.in::placeholder { color: var(--ink-3); }
input.in:hover:not(:disabled), select.in:hover:not(:disabled), textarea.in:hover:not(:disabled) { border-color: var(--ink-3); }
input.in:focus, select.in:focus, textarea.in:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
input.in:disabled, select.in:disabled, textarea.in:disabled { color: var(--ink-2); background: var(--panel); cursor: not-allowed; box-shadow: none; }
/* Собственный шеврон вместо системного: родная стрелка Chrome — единственный
   элемент фосфорной палитры, нарисованный чужой рукой, и в тёмной теме она
   почти не видна. Шеврон — тот же штрих 1,6, что и весь набор ICONS. */
select.in {
  color: var(--ink); appearance: none; -webkit-appearance: none;
  padding-inline-end: var(--sp-6);   /* место под шеврон — шаг шкалы, не «на глаз» */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2397a29b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.5 9.5 12 14l4.5-4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 8px center; background-size: 15px 15px;
}
html[data-theme="light"] select.in {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234b473e' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.5 9.5 12 14l4.5-4.5'/%3E%3C/svg%3E");
}
/* Отступы у select уже логические; чужой рукой остаётся только положение
   фонового шеврона — у background-position логической записи нет. */
html[dir="rtl"] select.in { background-position: left var(--sp-2) center; }
select.in option { background: var(--panel-2); color: var(--ink); }
label.lb { display: block; font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase; color: var(--ink-2); margin: var(--sp-3) 0 var(--sp-1); }
/* Капс с разрядкой — приём для КОРОТКОЙ служебной метки. На подписи-предложении
   («Обслуживание выключится само через») он превращает строку в набор букв и
   разносит её на две строки, а в переводе даёт «PARTNER ADDRESS NO. 1».
   Модификатор ставит форм-кит по длине подписи, а не человек по вкусу. */
label.lb.long { text-transform: none; letter-spacing: 0; font-family: var(--sans); font-size: var(--fs-body); font-weight: 600; }
input[type="checkbox"], input[type="radio"] { accent-color: var(--brand); width: 16px; height: 16px; cursor: pointer; }
/* Ползунок порога был системным синим Chrome посреди фосфорной палитры.
   accent-color красит бегунок и ЗАПОЛНЕННУЮ часть дорожки, color-scheme —
   незаполненную: без него тёмная тема получала светло-серую дорожку от
   браузера (страница схему не объявляет). */
input[type="range"] { accent-color: var(--brand); color-scheme: dark; width: 100%; height: 20px; cursor: pointer; }
html[data-theme="light"] input[type="range"] { color-scheme: light; }
input[type="range"]:disabled { accent-color: var(--ink-3); cursor: default; opacity: .6; }

/* === К1-А2: форм-кит поля — подпись, единица, счётчик длины, ошибка === */
/* (кит общий для всего продукта, хотя вырос в конфигураторе; страницы его только применяют) */
/* ─────────────────────────────── форм-кит конфигуратора (WF-E1) ─────────── */
/* Цифры — моноширь с табличными цифрами, как значения во всём пульте: в столбце
   полей «120 / 15 / 480» разряды тогда стоят друг под другом. */
input.in[type="number"] { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* Единица ВНУТРИ поля: подпись остаётся человеческой фразой («Связь с пультом
   каждые»), а «с» / «°C» / «мин» не отрывается от числа. Правый отступ поля
   освобождает место под неё; ширину суффикса задаёт сам текст. */
.in-suffix { position: relative; display: block; }
/* Логические свойства, а не right/padding-right: в арабской раскладке единица
   обязана стоять с той же стороны, с какой в поле кончается число. */
.in-suffix > .in { width: 100%; padding-inline-end: 46px; }
.in-suffix::after {
  content: attr(data-unit); position: absolute; inset-inline-end: var(--sp-3); top: 50%; transform: translateY(-50%);
  font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); pointer-events: none; white-space: nowrap;
}
/* Счётчик длины имени: «7/7» справа в поле. Красным — когда имя уже не влезает
   в плату, то есть ДО выпуска, а не после отказа валидатора. */
.in-count { position: relative; display: block; }
.in-count > .in { width: 100%; padding-inline-end: 52px; }
.in-count::after {
  content: attr(data-count); position: absolute; inset-inline-end: var(--sp-3); top: 50%; transform: translateY(-50%);
  font: 600 var(--fs-micro) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-2);
  pointer-events: none; white-space: nowrap;
}
.in-count.over::after { color: var(--alarm-ink); }

/* Поле с ошибкой: красная рамка + мягкое кольцо. Кольцо, а не одна рамка, —
   чтобы ошибку было видно и на соседе, и в светлой теме, где линии тоньше. */
.in.bad, .in-suffix.bad > .in, .in-count.bad > .in, .in-count.over > .in {
  border-color: var(--alarm); box-shadow: 0 0 0 3px var(--alarm-soft);
}
.in.bad:focus, .in-suffix.bad > .in:focus, .in-count.bad > .in:focus { border-color: var(--alarm); box-shadow: 0 0 0 3px var(--alarm-soft); }
/* Поле с ЖЁЛТЫМ замечанием («стоит поправить»): та же механика, янтарный цвет.
   Красная рамка над жёлтым текстом врала о серьёзности — по §7.3 красный
   значит «не даёт выпустить», и ставить его там, где выпуск не блокирован,
   нельзя. */
.in.warn-bad, .in-suffix.warn-bad > .in, .in-count.warn-bad > .in {
  border-color: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft);
}
.in.warn-bad:focus, .in-suffix.warn-bad > .in:focus, .in-count.warn-bad > .in:focus {
  border-color: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft);
}
/* Текст замечания ПОД полем: причина и что сделать. Живёт рядом с контролом,
   а не в общей полосе — иначе человек чинит, глядя в другой конец экрана. */
.field-err { font: var(--fs-body) var(--sans); color: var(--alarm-ink); margin-top: 4px; line-height: 1.4; }
.field-err.warn { color: var(--warn-ink); }

/* КАРКАС раскрывателя «Дополнительно» — ровно столько, чтобы компонент more()
   не рисовал ДВА значка (родной треугольник <details> плюс свой chevron).
   Оформление (цвет, отступы, разделитель) — за потоком каркаса. */
.cfg-more > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) 0; user-select: none; font: 500 var(--fs-body) var(--sans); color: var(--ink-2); }
.cfg-more > summary::-webkit-details-marker { display: none; }
.cfg-more > summary:hover { color: var(--ink); }
.cfg-more > summary > svg { transition: transform var(--dur-1) var(--ease); }
/* Свёрнутый шеврон указывает ПО ходу чтения (в арабской раскладке — влево),
   раскрытый смотрит вниз: «вниз» направления чтения не несёт и не зеркалится. */
html[dir="rtl"] .cfg-more > summary > svg { transform: scaleX(-1); }
.cfg-more[open] > summary > svg,
html[dir="rtl"] .cfg-more[open] > summary > svg { transform: rotate(90deg); }
.cfg-more .cm-n { font-family: var(--mono); font-size: var(--fs-micro); color: var(--ink-2); }

/* Единая клавиатурная видимость. До волны в theme.css не было ни одного
   :focus-visible — Chrome рисовал своё синее кольцо поверх фосфорной палитры,
   а кастомные кликабельные узлы (чип канала, ячейка матрицы) не фокусировались
   вовсе. Селекторы перечислены наперёд: часть узлов появится в потоках A–D. */
.btn:focus-visible,
.seg button:focus-visible,
.chan-chip:focus-visible,
.rmx-cell:focus-visible,
.cr-item:focus-visible,
.pal-item:focus-visible,
.eqc-head:focus-visible,
.trm-cell:focus-visible,
.cfg-more > summary:focus-visible,
tr.click:focus-visible {
  outline: 2px solid var(--brand); outline-offset: 2px;
}
/* Штатные фокусируемые элементы — тем же кольцом. Без этой строки Chrome рисует
   своё синее кольцо на раскрывателях палитры, полях поиска и ссылках бокового
   меню: половина клавиатурного прохода по конфигуратору шла в чужом цвете.
   Кнопка перечислена ЭЛЕМЕНТОМ, а не списком классов: голых <button> в приложении
   десятки (шеврон свёртки меню, пункты меню правил, вкладки-сегменты, пункты
   поповеров, кнопки редактора схем), и любая новая, забытая в списке, снова
   впустила бы на экран чужое синее кольцо. */
button:focus-visible,
/* Прокручиваемый блок Chrome тоже делает остановкой клавиатуры (журнал аудита,
   плитки парка, ленты объекта, маркеры карты — это простые div без класса и
   без tabindex), и там всплывало то же чужое синее кольцо. Правило безопасно
   именно потому, что :focus-visible на div случается ТОЛЬКО когда браузер
   действительно поставил на него фокус; у блоков со своим кольцом
   (.cfg-pick-row, .rmx-cell) правило специфичнее и остаётся за ними. */
div:focus-visible,
/* Полотно карты (maplibre) — тоже остановка клавиатуры: с него карту двигают
   стрелками. Правим СВОИМ стилем, чужой код не трогаем. */
canvas:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible {
  outline: 2px solid var(--brand); outline-offset: 2px;
}
/* У незаданной границы порога («граница не задана») бегунок скрыт: сидя в
   минимуме шкалы, он читается как выставленный ноль — а ноль и «не задана»
   на объекте значат разное. Дорожка остаётся видимой: поле не исчезает. */
input[type=range]:disabled::-webkit-slider-thumb { opacity: 0; }
input[type=range]:disabled::-moz-range-thumb { opacity: 0; }

/* ═══ НАЖАТИЕ (:active) ═══════════════════════════════════════════════════
   До волны в теме было ровно два правила :active, и оба про курсор grabbing.
   На сенсорном экране диспетчерской :hover физически не существует, поэтому
   :active там — единственный отклик на прикосновение; а на пульте объекта
   каждое нажатие уходит командой реальному насосу, и пока сервер отвечает,
   палец обязан получить подтверждение — иначе человек жмёт второй раз.
   Смещение на 1 px + затемнение 6 % — приём «клавиша ушла вниз»: он читается
   мгновенно и не требует ни цвета, ни звука. */
tr.click:active { background: var(--panel-3); }
.counter:active { transform: translateY(0); filter: brightness(.94); }
.nav a.item:active, .tabbar a:active { filter: brightness(.9); }
.nav-toggle:active, .switch i:active { filter: brightness(.92); }

/* ═══ ДВИЖЕНИЕ ════════════════════════════════════════════════════════════
   Правило продукта: анимируем только то, что ОБЪЯСНЯЕТ связь — откуда взялась
   панель, куда уехала строка, что именно изменилось. Длительности берутся из
   --dur-1/2/3 (140–260 мс): короче — движение не успевает объяснить, длиннее —
   начинает тормозить руку.
   Мигание прозрачностью запрещено: на аварийном экране мерцающий текст читать
   невозможно, а по ISA-101 мигание — самый дорогой сигнальный ресурс, его
   тратят на аварию и только на неё. Тревогу показываем ПУЛЬСОМ КОЛЬЦА (форма
   и размер), а сам знак остаётся всё время видимым и полностью непрозрачным.
   Готовое кольцо — km-ring: рисуется на ::before/::after элемента, элемент под
   ним не мигает. Применяют владельцы страниц (плитка объекта, маркер карты). */
@keyframes km-ring { 0% { transform: scale(1); opacity: .8; } 100% { transform: scale(2.1); opacity: 0; } }

/* Кто попросил систему убрать движение, получает статику — ВСЮ, а не ту, о
   которой мы вспомнили. Правило-страховка на весь документ: любая анимация,
   заведённая любой дорожкой позже, гаснет здесь автоматически. Обработчиков
   animationend/transitionend в приложении нет (проверено грепом), поэтому
   сворачивание длительности ничего не ломает. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .field-blame { animation: none; }
  .cfg-more > summary > svg { transition: none; }
  .rmx-wrap::before, .rmx-wrap::after { transition: none; }
}

/* СЕГМЕНТИРОВАННЫЙ ПЕРЕКЛЮЧАТЕЛЬ — единственный компонент выбора одного из N
   (вид списка, период, тип отчёта, вкладка «Схема / Подробно»).
   Контракт для всех страниц (F-013):
     • «выбрать один из N» — ТОЛЬКО .seg. Ряд кнопок .btn, у которого активная
       помечена классом .primary, запрещён: .primary в системе означает
       «нажми это, чтобы сделать», и человек ищет, что «применить», вместо того
       чтобы понять, что таблица уже перерисовалась;
     • .primary на экране одна и стоит за единственным главным действием;
     • сегментов больше семи — это уже не переключатель, а список: берите select;
     • разметка: <div class="seg" role="tablist"> кнопки, активная — class="on"
       и aria-selected="true" (или aria-pressed для двух состояний). */
.seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--edge); border-radius: var(--r-sm); overflow: hidden; background: var(--panel-2); }
.seg button {
  border: 0; background: transparent; padding: 0 var(--sp-4); font: 600 var(--fs-val) var(--sans);
  min-height: var(--ctl-h); line-height: 1; color: var(--ink-2); cursor: pointer;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.seg button:hover:not(:disabled) { color: var(--ink); background: var(--panel-3); }
.seg button:active:not(:disabled) { filter: brightness(.94); }
.seg button:disabled { color: var(--ink-3); cursor: not-allowed; }
.seg button.on { background: var(--brand-soft); color: var(--brand); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand) 30%, transparent); }
/* Днём --brand #036c3c на собственной подложке даёт 4,43:1, а на поднятой
   поверхности — 3,98:1: активная вкладка есть на каждом втором экране, и это
   самый часто читаемый элемент продукта. Малахит глубже — 6,7:1. */
html[data-theme="light"] .seg button.on { color: var(--brand-deep); box-shadow: inset 0 0 0 1px rgba(3,108,60,.35); }

/* ТУМБЛЕР ЗЕРКАЛИТСЯ ВМЕСТЕ С МАКЕТОМ. Бегунок «включено» ездил в физический
   правый край (left: 21px), а в арабской раскладке правый край — это НАЧАЛО
   строки, то есть включённый тумблер выглядел выключенным. Здесь бегунок
   ходит от начала строки к её концу: inset-inline-start 2px → calc(100% − 19px)
   (ширина бегунка 17 + поле 2). Ошибка стоила дорого именно в этом компоненте:
   тумблерами включают оповещение об аварии и вывод объекта в обслуживание. */
.switch { position: relative; width: 42px; height: 23px; flex: 0 0 auto; }
.switch input { display: none; }
.switch i { position: absolute; inset: 0; border-radius: var(--r-pill); background: var(--panel-3); border: 1px solid var(--edge); transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease); cursor: pointer; }
.switch i::after { content: ""; position: absolute; top: 2px; inset-inline-start: 2px; width: 17px; height: 17px; background: var(--ink-2); border-radius: 50%; transition: inset-inline-start var(--dur-1) var(--ease), background var(--dur-1) var(--ease); }
.switch input:checked + i { background: var(--brand-soft); border-color: var(--brand); }
.switch input:checked + i::after { inset-inline-start: calc(100% - 19px); background: var(--brand); box-shadow: 0 0 8px var(--brand-glow); }
.switch input:disabled + i { cursor: not-allowed; opacity: .55; }

/* ═══ ТАЧ-ЦЕЛИ НА ТЕЛЕФОНЕ (≤860 px) ══════════════════════════════════════
   Базовое правило системы, а не заплатка на конкретной странице: до волны
   44 px были доданы только кнопкам внутри таблиц и точечно ряду действий
   объекта, а сегмент «Карта / Список» оставался 30 px, .btn — 35, .btn.sm —
   27, «Скрыть» в профиле — 27. Половина органов управления была меньше
   подушечки пальца; диспетчер на объекте работает в перчатках.

   КРИТЕРИЙ СУЖЕН (решение волны 1, P-01): требование «ни одной цели меньше
   44 px» относится только к страницам, ДОСТУПНЫМ С ТЕЛЕФОНА — вход, пульт
   парка, аварии, история, объект, профиль, «Ещё». Конфигуратор и справочники
   осознанно desktop-only: к ним требование другое — «не ломается», то есть
   нет горизонтальной прокрутки и наложений. Поэтому правило поднимает высоту
   ОБЩИХ контролов и не лезет в сетки конфигуратора.

   Ссылка-действие («журнал →», «все команды →», «отчёты →») размером с строку
   текста пальцем не берётся. Для них заведён класс .tap: он не меняет вид на
   десктопе и разворачивает зону нажатия на телефоне. Владельцы страниц ставят
   .tap своим ссылкам-действиям (контракт «движок → применение», F-014). */
.tap { display: inline-flex; align-items: center; }
@media (max-width: 860px) {
  .btn, .btn.sm, .seg button, input.in, select.in, textarea.in { min-height: var(--ctl-h-lg); }
  /* Кнопка-значок (корзина, карандаш) шириной 36 px — цель 36×44, то есть
     всё ещё промах. Ширину добираем min-width, а не отступами: у кнопок с
     подписью ширина и так больше, и раздувать их незачем. */
  .btn, .btn.sm { min-width: var(--ctl-h-lg); }
  .tap { min-height: var(--ctl-h-lg); padding-block: 4px; }
  /* Ряд действий .row-g — служебная строка с органами управления (кнопки,
     ссылки-переключатели, флаги). Ссылка в таком ряду — действие, а не текст:
     переключатель языка на экране входа состоял из одиннадцати ссылок высотой
     20 px. Внутри абзаца ссылки правило не трогает — там .row-g не бывает. */
  .row-g > a, .row-g > span > a { min-height: var(--ctl-h-lg); min-width: var(--ctl-h-lg); display: inline-flex; align-items: center; justify-content: center; }
  /* Галка/радио 16×16 — половина цели; промах по «инверсии выхода» даёт реле,
     работающее наоборот, и выясняется это выездом на объект. Нажимается при
     этом не сам квадратик, а обнимающая строка <label> — ей и задаём высоту
     цели; квадратик просто становится крупнее, чтобы его было видно. */
  input[type="checkbox"], input[type="radio"] { width: 22px; height: 22px; }
  label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) {
    min-height: var(--ctl-h-lg); align-items: center;
  }
  /* Хвост правила, найденный приёмкой волны 7. Само правило опознаёт орган
     управления по классу .btn — и мимо него проходит любая кнопка со своим
     классом страницы. На телефонных страницах таких осталось две, обе живые:
     чип замечаний пульта («в ручном · 2», 112×30) и раскрыватель обозначений
     мнемосхемы («Обозначения», 101×22). Правим здесь, а не в регионах К2 и К4:
     промах одинаковый и причина одна — цель меньше подушечки пальца.
     Имя тега у чипа замечаний — не украшение: свою высоту он объявляет ниже по
     файлу одним классом, и правило общего слоя иначе проиграло бы ему порядком. */
  button.ph-chip, .schm-legend-t { min-height: var(--ctl-h-lg); }
  /* Тумблер остаётся тумблером (44 px высотой он выглядит нелепо); зоной
     нажатия ему служит вся строка — он всегда живёт внутри <label>. */
  .switch { width: 46px; height: 26px; }
  .switch i::after { width: 20px; height: 20px; }
  .switch input:checked + i::after { inset-inline-start: calc(100% - 22px); }
}

/* ────────────────────────────────────────────────────────── banners ─── */
/* Полоса-предупреждение. Толстая кромка — на НАЧАЛЕ строки (border-inline-start),
   а не слева физически: в арабской раскладке акцентная кромка обязана стоять
   там, откуда человек начинает читать. Цвета кромок и границ выводятся из
   токенов статуса, поэтому в светлой теме ничего не «перекрашивается» вручную. */
.banner {
  border-radius: var(--r); padding: var(--sp-3) var(--sp-4); font-weight: 600;
  display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-3);
  border: 1px solid transparent;
}
.banner.alarm { background: var(--alarm-soft); color: var(--alarm-ink); border-color: color-mix(in srgb, var(--alarm) 50%, transparent);
  border-inline-start: 4px solid var(--alarm); box-shadow: var(--glow-lg) var(--alarm-glow); }
.banner.warn  { background: var(--warn-soft);  color: var(--warn-ink);  border-color: color-mix(in srgb, var(--warn-line) 70%, transparent);
  border-inline-start: 4px solid var(--warn); box-shadow: var(--glow-md) var(--warn-glow); }
.banner.maint { background: var(--maint-soft);  color: var(--maint);  border-color: color-mix(in srgb, var(--maint) 30%, transparent); }
.banner.info  { background: var(--brand-soft); color: var(--brand); border-color: color-mix(in srgb, var(--brand) 24%, transparent); }

/* === К1-А2: тосты — вид сообщения, доступность, пауза чтения === */
/* Тост — единственный канал подтверждений и ошибок продукта. Вид несут ТРИ
   признака сразу (кромка цветом, знак формой, роль в разметке): цвет один
   различителем быть не может — он не работает ни на печати, ни при
   дальтонизме, ни на выцветшем экране в котельной. */
.toasts {
  /* Логические стороны, а не right/left: в арабской локали (dir=rtl) панель
     навигации стоит справа, и «правый верхний угол» — это поверх меню. */
  position: fixed; inset-inline-end: var(--sp-4); top: var(--sp-4); z-index: 200;
  display: flex; flex-direction: column; gap: var(--sp-2);
  width: min(380px, calc(100vw - var(--sp-6)));
}
.toast {
  background: var(--panel-2); color: var(--ink); padding: var(--sp-3); border-radius: var(--r);
  border: 1px solid var(--line); box-shadow: var(--elev-2); font-size: var(--fs-body);
  animation: slidein var(--dur-2) var(--ease); border-inline-start: 3px solid var(--brand);
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--sp-1) var(--sp-2); align-items: start;
  line-height: 1.45;
}
.toast .toast-ic { color: var(--brand); display: grid; place-items: center; height: 1.45em; }
.toast .toast-t { min-width: 0; overflow-wrap: anywhere; }
/* Кликабельный тост («Новая авария на КНС-7 · перелив») — настоящая кнопка:
   писк без адреса заставлял диспетчера искать изменение среди десятков плиток. */
button.toast-t {
  appearance: none; background: none; border: 0; padding: 0; margin: 0; text-align: start;
  color: inherit; font: inherit; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}
button.toast-t:hover { text-decoration-thickness: 2px; }
/* Крестик: двухстрочная ошибка читается дольше пяти секунд, и её нужно уметь
   убрать самому — тост перекрывает правую колонку таблицы, то есть ровно те
   кнопки, которыми только что пользовались. */
.toast .toast-x {
  appearance: none; background: none; border: 0; padding: 2px;
  margin: -2px 0 0; margin-inline-end: -4px;
  color: var(--ink-2); cursor: pointer; border-radius: var(--r-xs); display: grid; place-items: center;
  min-width: 22px; min-height: 22px;
}
.toast .toast-x:hover { color: var(--ink); background: var(--panel-3); }
.toast.ok    { border-inline-start-color: var(--ok); }
.toast.ok .toast-ic { color: var(--ok); }
/* Предупреждение — ЯНТАРЬ. До волны классов .warn/.ok не существовало вовсе,
   хотя kind="warn" передавали из «Мониторинга» и из шапки: «СМС-шлюз выключен»
   приходил с той же зелёной кромкой, что «Аккаунт создан». */
.toast.warn  { border-inline-start-color: var(--warn); border-inline-start-width: 4px; color: var(--warn-ink);
  box-shadow: var(--elev-2), var(--glow-md) var(--warn-glow); }
.toast.warn .toast-ic, .toast.warn .toast-x { color: var(--warn-ink); }
.toast.alarm { border-inline-start-color: var(--alarm); border-inline-start-width: 4px; color: var(--alarm-ink);
  box-shadow: var(--elev-2), var(--glow-lg) var(--alarm-glow); }
.toast.alarm .toast-ic, .toast.alarm .toast-x { color: var(--alarm-ink); }
/* На бумаге свечение не работает — тень остаётся чернильной (парная строка
   для .toast.alarm живёт в общем блоке светлой темы). */
html[data-theme="light"] .toast.warn { box-shadow: var(--elev-2); }
@keyframes slidein { from { transform: translateX(24px); opacity: 0; } }
/* В RTL тост выезжает с той же стороны, где живёт: слева. */
@keyframes slidein-rtl { from { transform: translateX(-24px); opacity: 0; } }
html[dir="rtl"] .toast { animation-name: slidein-rtl; }
@media (max-width: 860px) {
  /* На телефоне — снизу и над нижней навигацией: сверху тост накрывал шапку
     объекта, а под таб-баром его не было видно вовсе. */
  .toasts { top: auto; left: 12px; right: 12px; width: auto;
    bottom: calc(64px + env(safe-area-inset-bottom)); }
  .toast .toast-x { min-width: var(--ctl-h-lg); min-height: var(--ctl-h-lg);
    margin: -6px 0 0; margin-inline-end: -8px; }
  @keyframes slidein { from { transform: translateY(24px); opacity: 0; } }
}

/* === К1-А2: диалог — окно, последствие, каркас === */
.modal-back { position: fixed; inset: 0; background: var(--scrim); backdrop-filter: blur(4px); z-index: 190; display: grid; place-items: center; padding: var(--sp-4); animation: fade var(--dur-1) var(--ease); }
@keyframes fade { from { opacity: 0; } }
.modal { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--elev-3); width: min(460px, 100%); padding: var(--sp-5); animation: modal-in var(--dur-2) var(--ease);
  /* Высокое окно прокручивается ВНУТРИ себя: подложка центрирует по обеим осям,
     и без ограничения верх окна (заголовок, сводка выпуска) уезжал за верхнюю
     кромку экрана насовсем — прокруткой страницы его не достать, окно
     позиционировано fixed. */
  max-height: 100%; overflow-y: auto; overscroll-behavior: contain; }
/* Окно под таблицу, а не под вопрос (панель прибора, карточка позиции):
   в 460 px строка «Напряжение звена постоянного тока» переносится трижды. */
.modal.wide { width: min(680px, 100%); }
@keyframes modal-in { from { transform: translateY(10px) scale(.98); opacity: 0; } }
.modal h3 { margin: 0 0 var(--sp-3); font: 700 var(--fs-h2) var(--disp); }
.modal .row { display: flex; gap: var(--sp-2); justify-content: flex-end; margin-top: var(--sp-5); flex-wrap: wrap; }
/* ПОСЛЕДСТВИЕ ОПАСНОГО ДЕЙСТВИЯ. Человек соглашается на необратимое тем
   увереннее, чем точнее знает, что произойдёт и сколько это продлится.
   Блок обязателен для danger-окон (контракт confirmModal) и стоит ПОСЛЕ
   вопроса и ПЕРЕД кнопками — последнее, что читают перед нажатием. */
.modal-conseq {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-2); align-items: start;
  margin-top: var(--sp-4); padding: var(--sp-3); border-radius: var(--r);
  background: var(--warn-soft); border: 1px solid var(--warn-line);
  color: var(--ink); font-size: var(--fs-body); line-height: 1.5;
}
.modal-conseq .mc-ic { color: var(--warn-ink); display: grid; place-items: center; height: 1.5em; }
.modal-conseq.danger { background: var(--alarm-soft); border-color: var(--alarm-edge); }
.modal-conseq.danger .mc-ic { color: var(--alarm-ink); }
@media (max-width: 860px) {
  /* Кнопки окна на телефоне — в столбец и во всю ширину: «Отмена» и
     «Удалить» в 20 px друг от друга на большом пальце — это лотерея. */
  .modal .row { flex-direction: column-reverse; }
  .modal .row .btn { width: 100%; }
}

/* ─────────────────────────────────────────────────────── faceplate ─── */
/* Компактный поповер-карточка у правого верхнего угла холста: панель во всю
   правую сторону перекрывала схему ради капли контента. Скруглённый, с тенью
   и лёгким scale/fade-появлением ~150 мс. */
/* inset-inline-end и логическая точка роста: в арабской раскладке холст схемы
   зеркалится вместе с макетом, и поповер, приколоченный к физическому правому
   краю, вылезал бы поверх содержимого, а не «выезжал из своего угла». */
.face {
  position: fixed; top: 84px; inset-inline-end: var(--sp-5); z-index: 80;
  width: min(324px, 94vw); max-height: calc(100vh - 108px);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: var(--elev-3);
  display: flex; flex-direction: column; animation: face-pop var(--dur-1) var(--ease);
  transform-origin: top right;
}
html[dir="rtl"] .face { transform-origin: top left; }
@keyframes face-pop { from { transform: scale(.94) translateY(-6px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .face { animation: none; } }
.face .f-head { padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: var(--sp-2); font-family: var(--disp); }
.face .f-sub { font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase; color: var(--ink-2); margin-top: var(--sp-1); }
.face .f-body { padding: var(--sp-3) var(--sp-4); overflow: auto; flex: 1; font-size: var(--fs-body); }
.face .kv { gap: var(--sp-1) var(--sp-3); }
@media (max-width: 860px) {
  /* на телефоне — прежний нижний лист во всю ширину */
  .face { top: auto; bottom: 0; left: 0; right: 0; width: 100%; max-height: 80vh;
    border-radius: var(--r) var(--r) 0 0; animation: faceup var(--dur-2) var(--ease); transform-origin: bottom center; }
  @keyframes faceup { from { transform: translateY(48px); } }
}

/* === К5: объект «Подробно» — таблицы, действия, оперативные правки, журнал === */
/* ─────────────────────── объектный пульт (единая страница) ─── */
/* Пульт объекта: слева — компактная сетка оборудования РОВНО В 2 СТОЛБЦА,
   справа — расширенная лента истории/логов (задача: 2 столбца оборудования и
   за счёт этого шире логи). Левая колонка зажата под ширину двух карточек,
   правая забирает всё остальное. Ниже 1000px — одна колонка. */
/* Журнал объекта — горизонтальный блок под схемой: колонки газетного потока
   (сверху вниз, затем вправо), фиксированная высота, лишнее прокручивается
   вбок. Так лента не отъедает ширину у схемы. */
.obj-logbar { margin-top: 14px; }
.obj-log { overflow: auto; }
.obj-log.cols { column-width: 330px; column-gap: 0; column-rule: 1px solid var(--line-2);
  column-fill: auto;   /* заполнять колонку сверху донизу, затем следующую (хронология читается) */
  height: 244px; overflow-x: auto; overflow-y: hidden; padding: 4px 0; }
.obj-log.cols .tl-item { break-inside: avoid; padding: 7px 15px; }
.obj-log.cols .tl-item.alarm, .obj-log.cols .tl-item.warn { border-left-width: 4px; }
@media (max-width: 700px) { .obj-log.cols { height: auto; max-height: 300px; column-width: auto; overflow-y: auto; overflow-x: hidden; } }
.tl-item { padding: 9px 15px; border-bottom: 1px solid var(--line-2); }
.tl-item:last-child { border-bottom: 0; }
/* тревожные записи ленты: полоса толще + подкраска строки своим статусом,
   чтобы красное/жёлтое не терялось в общем потоке */
.tl-item.alarm { border-left: 4px solid var(--alarm); background: linear-gradient(90deg, var(--alarm-soft), transparent 55%); }
.tl-item.warn { border-left: 4px solid var(--warn); background: linear-gradient(90deg, var(--warn-soft), transparent 55%); }
/* Время события — данные, по которым диспетчер восстанавливает картину аварии,
   а --ink-3 в теме отведён только плейсхолдерам (3,4:1). */
.tl-time { font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); }
.tl-body { margin-top: 3px; font-size: var(--fs-body); line-height: 1.5; }

/* ── Индикатор связи платы «НА СВЯЗИ / НЕТ СВЯЗИ» (ui.js linkBadge). Явно и
   заметно отмечает офлайн/онлайн на каждом пульте. Антенна с радиоволнами:
   онлайн — зелёная, волны пульсируют; офлайн — серая, перечёркнута красным. */
.linkb { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--line); font: 600 var(--fs-tag) var(--mono); letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; flex: 0 0 auto; max-width: 100%; }
/* палочки сигнала (сотовая связь) — fill, не stroke */
.linkb-ic { width: 16px; height: 16px; flex: 0 0 auto; }
.linkb-ic .bar { stroke: none; }
.linkb-ago { font-weight: 500; letter-spacing: .02em; text-transform: none; }
.linkb.on  { color: var(--ok-ink); background: var(--ok-soft); border-color: rgba(53,201,143,.35); }
.linkb.on  .linkb-ic .bar { fill: var(--ok); filter: drop-shadow(0 0 3px var(--ok-glow)); }
.linkb.on  .linkb-ic .bar.b4 { animation: signal-pulse 1.6s ease-in-out infinite; } /* верхняя мигает */
.linkb.on  .linkb-ago { color: var(--ink-2); }
.linkb.off { color: var(--offline); background: var(--offline-soft); border-color: rgba(100,116,139,.4); }
.linkb.off .linkb-ic .bar { fill: var(--offline); opacity: .38; } /* нет сигнала — приглушены */
.linkb.off .linkb-ago { color: var(--offline); }
@keyframes signal-pulse { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .linkb.on .linkb-ic .bar.b4 { animation: none; } }

/* ── офлайн-плата: контент «замирает» (ISA-101 §15.4) ──
   .stale вешается на контейнер карточек/схемы: цвета глушатся, свечение гаснет */
.stale { filter: saturate(.3) brightness(.85); }
/* заметная НЕЙТРАЛЬНАЯ (не красная) плашка «нет связи» поверх устаревших данных */
.obj-stale-banner {
  position: sticky; top: 10px; z-index: 25;
  display: flex; align-items: center; gap: 10px; margin-bottom: 14px;
  padding: 12px 16px; border-radius: var(--r);
  background: linear-gradient(180deg, #1c2220, #151b19);
  border: 1px solid rgba(100,116,139,.5); border-left: 4px solid var(--offline);
  color: var(--ink); font-weight: 600; font-size: var(--fs-val);
  box-shadow: 0 10px 26px -14px rgba(0,0,0,.75);
}
.obj-stale-banner .v, .obj-stale-banner small { color: var(--offline); font-weight: 500; }

/* ── ряд действий и лента исполнения (object_actions.js, поток W1) ── */
/* Ряд встаёт В СТРОКУ ВИДА справа: своей карточки с заголовком у него нет —
   она стоила бы ~130 px и увела бы нижнюю треть мнемосхемы за сгиб. */
.oa-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-inline-start: auto; }
.oa-row > .btn { height: 34px; }
.oa-row .oa-more { padding-inline: 10px; }
/* Билет действия — чип у самой кнопки: тост живёт 4 с, а команда до часа. */
.oa-tk { display: inline-flex; align-items: center; gap: 6px; }

/* Лента исполнения — тонкая полоса ПОД строкой вида. Пустой ленты не бывает:
   узла нет вовсе, и строка вида не растёт (object_actions.js::actionsFeed). */
.oa-feed-wrap { margin: -6px 0 12px; border-top: 1px solid var(--line-2); }
.oa-feed {
  display: grid; grid-template-columns: auto 1fr auto auto auto;
  gap: 8px; align-items: center; padding: 5px 2px; font-size: var(--fs-body);
  animation: oa-feed-in .16s ease-out;
}
.oa-feed + .oa-feed { border-top: 1px solid var(--line-2); }
/* точка исхода команды — общий компонент .sdot (К1-А2), здесь только место */
/* Имя действия обязано ужиматься, а не растягивать страницу: горизонтальной
   прокрутки у пульта быть не должно ни в одном виде. */
.oa-feed .oa-feed-t { min-width: 0; }
.oa-feed .oa-feed-t > div:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Пояснение состояния — второй строкой, с переносом: «Плата не на связи с …,
   команда ждёт до …» в одну строку не влезает, а обрезанное бессмысленно. */
.oa-feed .oa-feed-n { white-space: normal; line-height: 1.35; margin-top: 1px; }
.oa-feed .oa-feed-time { white-space: nowrap; }
.oa-feed .oa-feed-tail { display: inline-flex; align-items: center; gap: 8px; }
/* Ссылка «все команды» — общий компонент .more-link (F-056): свой цвет и своё
   поведение при наведении у неё больше не описываются, иначе на трёх ссылках
   одного смысла снова разъедется вид. Здесь остаётся только запрет переноса:
   в ленте она стоит в конец строки рядом с возрастом и кнопкой отмены. */
.oa-feed .oa-feed-all { white-space: nowrap; }
@keyframes oa-feed-in { from { opacity: 0; transform: translateY(4px); } }
/* Смена состояния чипа — БЕЗ анимации: мигающий статус в аварийном
   интерфейсе читается как новая авария (ISA-101). */
@media (prefers-reduced-motion: reduce) { .oa-feed { animation: none; } }

/* Поповер «Ещё»: список «имя + строка объяснения», а не меню-невидимка. */
.oa-pop {
  position: fixed; z-index: 120; width: min(360px, calc(100vw - 16px));
  max-height: min(72vh, 620px); overflow: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 18px 50px rgba(0,0,0,.55); padding: 6px; display: grid; gap: 1px;
}
.oa-pop .oa-grp { font: 700 var(--fs-tag) var(--disp); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); padding: 8px 10px 4px; }
.oa-pop .oa-grp.danger { color: var(--alarm-ink); }
.oa-pop .oa-sep { border-top: 1px solid var(--line); margin: 5px 4px 0; }
.oa-pop .oa-item {
  appearance: none; display: grid; gap: 2px; width: 100%; text-align: start;
  background: transparent; border: 1px solid transparent; border-radius: 9px;
  padding: 8px 10px; cursor: pointer; color: var(--ink); font: 600 var(--fs-body) var(--sans);
}
.oa-pop .oa-item .oa-item-t { display: flex; align-items: center; gap: 8px; }
.oa-pop .oa-item .oa-note { font: 400 var(--fs-micro)/1.35 var(--sans); color: var(--ink-2); }
.oa-pop .oa-item:hover { background: var(--panel-3); border-color: var(--line); }
.oa-pop .oa-item.danger .oa-item-t { color: var(--alarm-ink); }
.oa-pop .oa-item.danger:hover { border-color: var(--alarm); }
.oa-pop .oa-nav { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 10px 6px; }
.oa-pop .oa-nav .btn { text-decoration: none; }
/* Список выходов в подтверждении «Вернуть автоматике». */
.oa-list { margin: 8px 0 0; padding-inline-start: 20px; display: grid; gap: 2px; }

@media (max-width: 860px) {
  /* Первичный ряд — ОДНА строка с горизонтальной прокруткой, а не сетка 2×2:
     сетка добавила бы ~180 px и увела мнемосхему с первого экрана целиком. */
  .oa-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: thin; width: 100%; margin-inline-start: 0; padding-bottom: 4px; }
  .oa-row > * { flex: 0 0 auto; }
  .oa-feed { grid-template-columns: auto minmax(0,1fr) auto auto; }
  .oa-feed .oa-feed-time { display: none; }
  /* На 390 px имя действия важнее ссылки в журнал: пока лента свёрнута и в
     хвосте стоит раскрыватель, ссылку убираем — она вернётся на последней
     строке раскрытой ленты. Иначе от имени остаётся «измен…». */
  .oa-feed .oa-feed-more + .oa-feed-all { display: none; }
  /* Обрезка многоточием на 390 px съедала не хвост имени, а СОСТОЯНИЕ команды:
     на 134 px ширины строка «Обновить данные · доставлена» читалась как
     «Обновить данные · …», и у завершённой команды исход не был виден нигде
     (время на мобильном скрыто правилом выше, чип билета живёт только пока
     команда в очереди). Разрешаем перенос: у длинных имён строка станет
     двухэтажной — это дешевле, чем лента без исходов. */
  .oa-feed .oa-feed-t > div:first-child { white-space: normal; overflow: visible; line-height: 1.3; }
  /* Палец — не курсор: тач-цель ниже 44 px промахивается. Ряд действий и лента
     — основной мобильный путь диспетчера, и мельче 44 px здесь ничего быть не
     может (десктопные 34 px остаются: там указатель точный). */
  .oa-row > .btn { height: 44px; }
  .oa-row > .oa-tk .btn { min-height: 44px; }
  .oa-feed button { min-height: 44px; }
  .oa-feed .oa-feed-all { display: inline-flex; align-items: center; min-height: 44px; }
  .oa-pop .oa-item { min-height: 44px; align-content: center; }
  .oa-pop .oa-nav .btn { min-height: 44px; }
  /* Нижний лист: ВЫШЕ нижней навигации (.tabbar, z-index 40) и не под ней. */
  .oa-pop.sheet {
    inset-inline: 8px; left: 8px; right: 8px; top: auto; width: auto;
    bottom: calc(68px + env(safe-area-inset-bottom));
    max-height: calc(100vh - 140px);
  }
}

/* ── оперативные правки: секция, редактор, .row-off (object_overlay.js, поток W2) ── */
/* Новых цветов здесь нет ни одного. Маркер оперативной правки — существующий
   .chip.manual (фиолетовый «человек вмешался в автоматику»): янтарь в этой
   системе означает уровень «внимание», и янтарный чип в строке канала
   диспетчер прочитал бы как состояние процесса, а не как след человека. */

/* Колонка страницы объекта не имеет права раздуваться под самую широкую
   таблицу: без minmax(0,1fr) грид-колонка меряется по max-content, и на
   телефоне в горизонтальный скролл уезжает ВСЯ страница вместе с шапкой,
   вместо того чтобы прокручивалась одна таблица внутри своей обёртки
   (у каждой из них уже есть overflow-x:auto). */
#obj-root .grid { grid-template-columns: minmax(0, 1fr); }

/* секция «Что сейчас действует на плате» */
/* Ширина колонки. На 1920 строка «имя … значение … [⋯]» растягивалась во всю
   страницу, и между значением и меню оставалось ~600 px пустоты: глазу нечем
   связать концы одной строки. Ограничение — как у колонки страницы объекта. */
.ovr-eff-card { max-width: 1100px; }
/* Шапка секции — СВОЙ модификатор: общий .card-h переносов не допускает
   (flex-wrap по умолчанию nowrap), и при 1280 значения рвались посреди себя
   («телефонов 1 из / 4», «по состоянию на 22.08 / 19:41:56»). Здесь наоборот:
   каждый элемент неразрывен, а переносится шапка целиком. */
.card .card-h.ovr-eff-h { flex-wrap: wrap; row-gap: 2px; }
.card .card-h.ovr-eff-h > * { white-space: nowrap; }
/* Ссылка держится правого края и на своей строке: перенос не должен утаскивать
   её к левому краю под заголовок — она бы читалась как часть названия секции. */
.card .card-h.ovr-eff-h > a { margin-inline-start: auto; }
.card .card-h.ovr-eff-h .ovr-h-short { display: none; }
@media (max-width: 860px) {
  /* На 390 px распорка съедала всю ширину, зажимая элементы в колонки по 70 px;
     счётчики и время печатаем коротко. */
  .card .card-h.ovr-eff-h { padding: 10px 12px; column-gap: 8px; }
  .card .card-h.ovr-eff-h .spacer { display: none; }
  .card .card-h.ovr-eff-h .ovr-h-long { display: none; }
  .card .card-h.ovr-eff-h .ovr-h-short { display: inline; }
  /* Счётчики и время стоят подряд одним размером — без разделителя они
     читаются как одно число («1/4 23.08»). */
  .card .card-h.ovr-eff-h .ovr-h-short ~ .ovr-h-short::before { content: "· "; }
}
.ovr-eff .ovr-grp {
  font: 600 var(--fs-micro) var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-2); margin: 16px 0 4px;
}
.ovr-eff > .ovr-grp:first-child { margin-top: 0; }
.ovr-row {
  display: grid; grid-template-columns: minmax(0, 1.5fr) auto minmax(0, 1.1fr) auto;
  gap: 4px 12px; align-items: center; padding: 7px 0;
  border-bottom: 1px solid var(--line-2); font-size: var(--fs-body);
}
.ovr-row:last-child { border-bottom: 0; }
.ovr-row .val { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: end; white-space: nowrap; }
.ovr-row .note { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.ovr-row .act { display: flex; gap: 6px; justify-content: flex-end; }
.ovr-row svg { vertical-align: -2px; }
/* Конец связки, которого нет в действующей конфигурации: строка провода как
   есть, приглушённо — это технический идентификатор, а не имя оборудования. */
.ovr-lost { font-family: var(--mono); font-size: var(--fs-body); }
.ovr-note { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ovr-foot { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.ovr-foot > .small { flex: 1 1 340px; }
/* Предупреждение о непереносимости — рамка + янтарные чернила: здесь янтарь на
   месте, это «обрати внимание», а не состояние канала. */
.ovr-warn {
  display: flex; align-items: flex-start; gap: 8px; margin-top: 10px;
  padding: 8px 10px; border: 1px solid var(--warn-line); border-radius: 8px;
  background: var(--warn-soft); color: var(--warn-ink); font-size: var(--fs-body); line-height: 1.45;
}
.ovr-warn svg { flex: 0 0 auto; margin-top: 1px; }
.ovr-empty { padding: 10px 0; }
.ovr-list { margin: 8px 0 0; padding-inline-start: 18px; font-size: var(--fs-body); line-height: 1.6; }
.ovr-recip { border-inline-start: 2px solid var(--line); padding-inline-start: 10px; }

/* Тач-цели самой секции — то же правило 44 px, что у ряда действий и у
   редактора правок. Без него главный мобильный вход в редактор — кнопка
   «Оперативные правки…» в подвале секции — оставался 27 px, ссылка «история
   правок →» в шапке 18 px, а «Включить канал» и ВКЛ/ВЫКЛ/АВТО в столбце
   «Управление» таблицы выходов — 27 px: палец промахивается ровно по тем
   кнопкам, ради которых секцию открывали. */
@media (max-width: 860px) {
  .ovr-eff .btn { min-height: 44px; }
  .card .card-h.ovr-eff-h > a { display: inline-flex; align-items: center; min-height: 44px; }
  table.t td .btn { min-height: 44px; }
}

/* выключенный правкой канал в таблицах «Подробно».
   БЕЗ line-through: моноширинные цифры от перечёркивания разваливаются.
   Чернила --ink-2, а НЕ --ink-3: правило красит ВСЕ ячейки строки, то есть
   данные (клемма, имя, состояние, режим, оборудование, датчик), а --ink-3 на
   тёмной панели даёт 3,4:1 — ниже AA для текста 13 px (см. шапку файла:
   «--ink-3 нельзя делать носителем данных»). Приглушённость строки набирается
   фоном и иконкой banned в подписи, а не понижением контраста данных. */
table.t tr.row-off td { color: var(--ink-2); background: var(--panel-2); }
table.t tr.row-off td .v { color: var(--ink-2); }
/* Подпись живёт в ЯЧЕЙКЕ имени канала: без переноса она распирает всю таблицу
   вширь и уводит страницу в горизонтальный скролл на телефоне. */
/* Цвет — --manual («человек вмешался»), а не янтарь: янтарь в этой системе
   означает уровень «внимание», и в строке канала диспетчер прочитал бы его как
   состояние процесса. Тот же цвет у чипа «правка» и у ромба на мнемосхеме. */
.ovr-off-note {
  display: flex; align-items: center; gap: 5px; margin-top: 2px; flex-wrap: wrap;
  max-width: 180px; font-size: var(--fs-micro); line-height: 1.35; color: var(--manual);
}
.ovr-off-note svg { color: var(--manual); }
.ovr-off-note .chip { padding: 1px 7px; font-size: var(--fs-micro); }

/* блок «ПОРОГИ» в карточке канала */
.ovr-face { margin-top: 5px; }
/* Карточка канала — 324 px: четыре колонки в ней рвут подпись пополам, поэтому
   строка порога двухэтажная: «граница — значение» сверху, «источник — действие»
   снизу. */
.ovr-face-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 8px;
  align-items: center; padding: 5px 0; border-bottom: 1px solid var(--line-2); font-size: var(--fs-body);
}
.ovr-face-row > :nth-child(2) { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: end; }
/* чип «правка» — по содержимому, иначе он растянется во всю ячейку сетки */
.ovr-face-row > :nth-child(3) { justify-self: start; }
.ovr-face-row > :nth-child(4) { justify-self: end; }
.ovr-face-row:last-child { border-bottom: 0; }
.ovr-face-row .chip { padding: 1px 8px; font-size: var(--fs-micro); }

/* раскрыватели («показать пороги ▾», «показать SMS-команду ▾») */
.ovr-disc > svg { transition: transform .16s; transform: rotate(-90deg); }
.ovr-disc.on > svg { transform: rotate(0deg); }
@media (prefers-reduced-motion: reduce) { .ovr-disc > svg { transition: none; } }

/* тихое меню строки */
.ovr-menu-b { padding: 4px 7px; }
.ovr-menu {
  position: fixed; z-index: 200; min-width: 200px; padding: 5px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow);
  display: flex; flex-direction: column;
}
.ovr-menu-i {
  appearance: none; border: 0; background: transparent; text-align: start;
  padding: 7px 10px; border-radius: 7px; font: 500 var(--fs-body) var(--sans); color: var(--ink); cursor: pointer;
}
.ovr-menu-i:hover { background: var(--panel-2); color: var(--brand); }
.ovr-menu-i.danger { color: var(--alarm-ink); }

/* редактор: выдвижная панель справа (десктоп) */
.ovr-drawer {
  position: fixed; inset-block: 0; inset-inline-end: 0; z-index: 120;
  width: min(520px, 100vw); display: flex; flex-direction: column;
  background: var(--panel); border-inline-start: 1px solid var(--line);
  box-shadow: 0 18px 50px rgba(0,0,0,.55), 0 0 0 1px rgba(51,224,163,.06);
  animation: ovr-in .18s ease-out;
}
@keyframes ovr-in { from { transform: translateX(12px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .ovr-drawer { animation: none; } }
html[dir=rtl] .ovr-drawer { animation: none; }
.ovr-head { display: flex; align-items: center; gap: 8px; padding: 12px 14px 6px; }
.ovr-head h3 { margin: 0; font: 700 var(--fs-h2) var(--disp); }
.ovr-sub { padding: 0 14px 8px; }
.ovr-slots { display: flex; gap: 6px; flex-wrap: wrap; padding: 0 14px 10px; }
.ovr-tabs { margin: 0 14px; align-self: flex-start; max-width: calc(100% - 28px); overflow-x: auto; scrollbar-width: thin; }
.ovr-body { flex: 1; overflow: auto; padding: 12px 14px 16px; font-size: var(--fs-body); }
.ovr-foot-w { padding: 0 14px 14px; }
.ovr-tab-h {
  display: flex; align-items: center; gap: 9px; margin-bottom: 8px;
  font: 600 var(--fs-micro) var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2);
}
/* В панели 520 px четыре колонки строки не помещаются: пояснение («было 105 °C»,
   «в конфигурации границы нет») уезжает во вторую строку и рвётся по слогам.
   Поэтому здесь строка двухэтажная: имя-значение-действие, пояснение под ними. */
.ovr-drawer .ovr-row { grid-template-columns: minmax(0, 1fr) auto auto; }
.ovr-drawer .ovr-row > :first-child { grid-area: 1 / 1; }
.ovr-drawer .ovr-row > .val { grid-area: 1 / 2; }
.ovr-drawer .ovr-row > .act { grid-area: 1 / 3; }
.ovr-drawer .ovr-row > .note { grid-area: 2 / 1 / 3 / 4; }

/* «Свободных слотов нет» — ВИДИМАЯ строка рядом с погашенной кнопкой (title не
   существует на тач-экране). Янтарь здесь уместен: это «обрати внимание»,
   а не состояние канала. */
.ovr-full {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 7px 10px; border: 1px solid var(--warn-line); border-radius: 8px;
  background: var(--warn-soft); color: var(--warn-ink); line-height: 1.4;
}
.ovr-full svg { flex: 0 0 auto; }

/* SMS-эквивалент: свёрнут по умолчанию — на объекте с интернетом это шум */
.ovr-sms-b { margin-top: 6px; }
.ovr-sms-line {
  font-family: var(--mono); font-size: var(--fs-body); line-height: 1.5;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 8px 10px; overflow-x: auto; white-space: nowrap;
}
.ovr-sms-line .ph { color: var(--ink-3); }

/* мобайл: нижний лист ВЫШЕ нижней навигации (.tabbar z-index 40), иначе кнопки
   правки оказались бы под кнопкой «Аварии» */
@media (max-width: 860px) {
  .ovr-drawer {
    inset-inline: 0; inset-block-start: auto; width: 100%; max-height: 92vh;
    border-radius: 16px 16px 0 0; border-inline-start: 0; border-top: 1px solid var(--line);
    animation: ovr-up .2s ease-out;
  }
  @keyframes ovr-up { from { transform: translateY(48px); opacity: 0; } }
  .ovr-body { padding-bottom: calc(68px + env(safe-area-inset-bottom)); }
  .ovr-row { grid-template-columns: 1fr auto; }
  .ovr-row .note { grid-column: 1 / -1; }
  .ovr-row .act { grid-column: 1 / -1; justify-content: flex-start; }
  .ovr-face-row { grid-template-columns: 1fr auto; }
  /* Тач-цели: то же правило 44 px, что у ряда действий. Меню строки [⋯] и
     вкладки редактора — самые мелкие цели волны, промах по ним открывает
     соседнюю правку. */
  .ovr-menu-b { min-width: 44px; min-height: 44px; padding: 4px 10px; }
  .ovr-menu-i { min-height: 44px; }
  .ovr-tabs button { min-height: 44px; }
  .ovr-drawer .ovr-row .btn { min-height: 44px; }
  .ovr-disc { min-height: 44px; }
}
/* Светлая тема: «призрачная» кнопка в таблице (АВТО у выхода) без рамки
   читается как неактивный текст рядом с двумя обведёнными соседями — на бумаге
   свечения нет, и границу приходится рисовать чернилами. */

/* «Диагностика» (object_diag.js), список «Кому уйдёт авария»: номер позиции
   обязан стоять против ПЕРВОЙ строки записи. Общий .kv центрирует подпись по
   всей высоте ячейки (align-self:center), и у записи в три строки «2» вставало
   против второй строки — список читался как перепутанный. Общий .kv не трогаем:
   он чужой и живёт на десятке экранов; удвоенный класс поднимает вес правила
   выше объявления .kv ниже по файлу. */
.kv.kv-recip > :nth-child(odd) { align-self: start; }

/* === К4: мнемосхема объекта и faceplate === */
/* ═══════════════ графическая мнемосхема объекта (schematic.js, §15.4) ═════ */
.schm-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 4px 2px 10px; }
.schm-tools .schm-status { min-width: 118px; text-align: right; }
.schm {
  width: 100%; min-height: 0; display: block;
  background:
    radial-gradient(760px 380px at 40% -8%, rgba(51,224,163,.05), transparent 62%),
    repeating-linear-gradient(0deg, rgba(150,200,175,.04) 0 1px, transparent 1px 28px),
    repeating-linear-gradient(90deg, rgba(150,200,175,.04) 0 1px, transparent 1px 28px),
    var(--bg-2);
  border: 1px solid var(--line); border-radius: var(--r-lg);
}
.schm text { font-family: var(--sans); fill: var(--ink); dominant-baseline: middle; }
@media (max-width: 700px) { .schm { min-height: 0; } }
.schm.tool-pipe .snode, .schm.tool-flip .snode { cursor: crosshair; }
.schm.tool-erase .pipe-hit { cursor: pointer; }
.schm.tool-erase .pipe-hit:hover { stroke: rgba(255,77,63,.30); }

/* ── земля ── */
.schm .earth { fill: rgba(80,72,56,.18); stroke: none; }
.schm .earth-pit { fill: var(--bg-2); stroke: none; }
.schm .pit-wall { fill: none; stroke: rgba(180,150,110,.45); stroke-width: 2; stroke-linecap: round; }
.schm .ground-line { stroke: rgba(180,150,110,.55); stroke-width: 2.4; }
.schm .earth-hatch { stroke: rgba(180,150,110,.35); stroke-width: 1.4; stroke-linecap: round; }
.schm .support .post  { stroke: var(--ink-3); stroke-width: 2.6; stroke-linecap: round; }
.schm .support .brace { stroke: rgba(150,200,175,.22); stroke-width: 1.5; }
.schm .support .beam  { stroke: var(--ink-3); stroke-width: 1.8; }

/* ── трубы: корпус + бирюзовая вода с бегущими белыми точками ── */
.schm .pipe-trench { fill: none; stroke: var(--bg-2); stroke-width: 23; stroke-linecap: round; stroke-linejoin: round; }
.schm .pipe-case { fill: none; stroke: #26333a; stroke-width: 13; stroke-linecap: round; stroke-linejoin: round; }
/* ⚠ Заливка труб — СПЛОШНОЙ цвет, не url(#град): градиент по умолчанию живёт
   в координатах bbox, а у СТРОГО ГОРИЗОНТАЛЬНОЙ трубы («подача одной прямой»)
   высота bbox нулевая — по спецификации SVG такой путь вообще не рисуется.
   Подача котельной годами ходила «пустой» (тёмный кейс + точки без воды). */
.schm .pipe-fill { fill: none; stroke: #38c9d8; stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; opacity: .9; }
.schm .pipe-water.flow {
  fill: none; stroke: #eafcff; stroke-width: 3.2; stroke-linecap: round;
  stroke-dasharray: 2.5 13; filter: drop-shadow(0 0 4px rgba(94,240,234,.7));
  animation: schm-flow 1s linear infinite;
}
.schm .pipe-water.flow.rev { animation: schm-flow-rev 1s linear infinite; }
.schm .pipe-hit { fill: none; stroke: transparent; stroke-width: 20; stroke-linecap: round; }
/* §1.4: подсветка трубы по уровню её оборудования — янтарь «внимание»,
   красный СТРОГО «авария» (ISA-101); статично, без ряби */
.schm .pipe-glow { fill: none; stroke-width: 19; stroke-linecap: round; stroke-linejoin: round; opacity: .32; }
/* Ореол трубы — ВСПОМОГАТЕЛЬНЫЙ признак («на конце этой линии что-то не
   так»), а не сам сигнал: при прежней плотности .4/.48 красным была залита
   вся линия от коллектора до насосов, а источник аварии — сосуд — оставался
   спокойно-бирюзовым. Взгляд уходил на окрестности вместо причины. */
.schm .pipe-glow.warn  { stroke: var(--warn);  opacity: .22; filter: drop-shadow(0 0 5px var(--warn-glow)); }
.schm .pipe-glow.alarm { stroke: var(--alarm); opacity: .26; filter: drop-shadow(0 0 6px var(--alarm-glow)); }
/* §1.5: среда трубы — вода (дефолтный wgrad), воздух/стоки/газ своими градиентами */
.schm .pipe-fill.m-air { stroke: #b4c8d4; opacity: .6; }
.schm .pipe-fill.m-sew { stroke: #83854c; }
.schm .pipe-fill.m-gas { stroke: #a58f4c; }
/* шевроны направления потока НА заливке трубы: анимация точек не видна на
   статичном кадре (постер, печать, reduced-motion) — направление держат они */
/* Шеврон направления — знак НА КОРПУСЕ трубы, поэтому и цвет корпуса
   (--case): тёмно-серый глиф поверх бирюзовой заливки не читался ни в одной
   теме, а рядом с бегущими точками был ещё и лишним (F-176). Рисуется только
   там, где точек нет, — значит имеет право быть заметным. */
.schm .flow-dir { fill: var(--case); }

/* ── узел: свечение всей плитки по худшему статусу датчиков ── */
.schm .snode .sym-body { fill: url(#mgrad); stroke: var(--ok-ink); stroke-width: 2.2; filter: drop-shadow(0 0 7px var(--ok-glow)); transition: stroke .2s; }
.schm .snode .edge { stroke: var(--ok-ink); stroke-width: 2.4; fill: none; stroke-linecap: round; }
.schm .snode .pipe-stub { stroke: #8fb8a6; stroke-width: 3.4; }
/* «внимание»/«авария» узла: обводка толще опорной зелёной + сильнее glow —
   тревожный глиф должен выделяться и толщиной линии, не только цветом */
.schm .snode.warn  .sym-body, .schm .snode.warn  .edge { stroke: var(--warn); }
.schm .snode.warn  .sym-body { stroke-width: 2.8; filter: drop-shadow(0 0 12px var(--warn-glow)); }
.schm .snode.warn  .edge { stroke-width: 3; }
.schm .snode.alarm .sym-body, .schm .snode.alarm .edge { stroke: var(--alarm); }
.schm .snode.alarm .sym-body { stroke-width: 3.4; filter: drop-shadow(0 0 18px var(--alarm-glow)); animation: schm-apulse 1.2s infinite; }
.schm .snode.alarm .edge { stroke-width: 3.2; }
.schm .snode:hover .sym-body { stroke: var(--brand); cursor: pointer; }
.schm .snode.pipe-pending .sym-body { stroke: var(--brand); stroke-width: 2.8; stroke-dasharray: 7 5; }
.schm .snode .hub { fill: #8fb8a6; }
.schm .snode .gauge { fill: rgba(9,15,17,.6); stroke: var(--ink-2); stroke-width: 1.6; }
/* детали глифов по референсам equip-img */
.schm .snode .pmain { stroke-width: 5.2; }              /* прямая труба насоса */
.schm .snode .neckpipe { stroke-width: 5; }             /* горловина бака */
.schm .snode .flange { stroke-width: 3.6; }             /* фланцы труб */
.schm .snode .ring { fill: none; stroke: var(--ink-3); stroke-width: 1.5; opacity: .85; }
.schm .snode .strap { stroke: rgba(150,200,175,.30); stroke-width: 2; }
.schm .snode .panel { fill: none; stroke: rgba(150,200,175,.28); stroke-width: 1.4; }
.schm .snode .bolt { fill: var(--ink-3); }
.schm .snode .tick { stroke: var(--ink-3); stroke-width: 1.3; }

/* Ридаут-пилюли датчиков/выходов над узлом: тёмная подложка-панелька, имя
 * приглушённым каптионом слева, значение крупным моно справа. Уровень красит
 * рамку/подложку и значение — читается как приборная панель, а не подписи. */
.schm .lblp-bg { fill: rgba(8,13,15,.88); stroke: var(--line); stroke-width: 1; }
.schm .lblp.warn  .lblp-bg { fill: rgba(255,176,32,.14);  stroke: rgba(255,176,32,.6);  stroke-width: 1.5; }
.schm .lblp.alarm .lblp-bg { fill: rgba(255,77,63,.16);   stroke: rgba(255,77,63,.75);  stroke-width: 1.5;
  filter: drop-shadow(0 0 7px var(--alarm-glow)); }
.schm .lblp { cursor: pointer; }
.schm .lblp:hover .lblp-bg { stroke: var(--brand); }
/* ⚠ Кегль ВСЕХ подписей схемы умножен на --tk (schematic.js ставит его на
   корневом <svg>). Текст в SVG живёт в пользовательских единицах и физически
   выходит fs × k, где k — масштаб рисунка; при k = 0,67 подписи каналов
   превращались в 7–9 экранных пикселей. --tk = 1/k (с потолком 1,35)
   возвращает им паспортный размер. Новый текст на схеме обязан заводиться по
   этому же правилу, иначе он снова окажется нечитаемым на 1280 px. */
.schm .lbl-k { font: 500 calc(var(--fs-micro) * var(--tk, 1)) var(--mono); fill: var(--ink-2); letter-spacing: .03em; }
/* --sq: имя канала не влезло в пилюлю — ужимаем КЕГЛЬ, а не режем имя (см.
   schematic.js::fitLabel). «Т пода…» на схеме неотличимо от «Т подпитки», а
   разница между ними — это разница между «котёл греет» и «не греет». Ставится
   инлайново и только на конкретную подпись; пол ужатия держит fitLabel. */
.schm .lbl-k { font-size: calc(var(--fs-micro) * var(--tk, 1) * var(--sq, 1)); }
.schm .lbl-v { font: 700 calc(var(--fs-val) * var(--tk, 1)) var(--mono); fill: var(--ink); font-variant-numeric: tabular-nums; }
/* --sq у ЗНАЧЕНИЯ: число не режется никогда (обрезанное число — это другое
   число), поэтому не влезшее значение ужимается кеглем. */
.schm .lbl-v { font-size: calc(var(--fs-val) * var(--tk, 1) * var(--sq, 1)); }
.schm .lbl-v.warn  { fill: var(--warn-ink); filter: drop-shadow(0 0 4px var(--warn-glow)); }
.schm .lbl-v.alarm { fill: var(--alarm-ink); filter: drop-shadow(0 0 5px var(--alarm-glow)); }
.schm .lblp.alarm .lbl-k { fill: var(--alarm-ink); opacity: .8; }
.schm .lblp.warn  .lbl-k { fill: var(--warn-ink); opacity: .8; }
/* Выход — ОРГАН УПРАВЛЕНИЯ, и выглядеть он обязан иначе, чем показание:
   миниатюрный тумблер вместо точки. Прежнее отличие (заливка подложки и точка
   Ø4) при масштабе 0,67 не читалось вовсе, и клик «посмотреть карточку»
   открывал диалог «Выключить насос?» — каждое нажатие было лотереей. */
.schm .lblp-sw { fill: rgba(9,15,17,.75); stroke: var(--edge); stroke-width: 1.2; }
.schm .lblp-knob { fill: var(--ink-3); transition: fill .15s; }
.schm .lblp.do.on .lblp-sw { fill: var(--brand-soft); stroke: var(--brand); }
.schm .lblp.do.on .lblp-knob { fill: var(--brand); filter: drop-shadow(0 0 4px var(--brand-glow)); }
.schm .lblp.do.on .lbl-v { fill: var(--brand); }
/* Ручной приоритет: штриховой фиолетовый обод пилюли. Точка Ø4 при масштабе
   схемы давала 2–3 экранных пикселя — интерфейс сообщал «есть выходы в ручном
   режиме» плашкой и не показывал, где они. */
.schm .lblp.man .lblp-bg { stroke: var(--manual); stroke-width: 1.6; stroke-dasharray: 5 3; }
/* команда в пути (queued|sent, ack ещё нет): вместо точки состояния —
   радиальный спиннер: 8 палочек по кругу поочерёдно гаснут. Цвет наследует
   ТЕКУЩЕЕ состояние выхода: серый (выключен, включаем) / фосфор (включён,
   выключаем) — видно и «куда идём», и «откуда». */
.schm .lblp-spin line { stroke: var(--ink-3); stroke-width: 1.7; stroke-linecap: round;
  opacity: .25; animation: lblp-spin .9s linear infinite; }
.schm .lblp.do.on .lblp-spin line { stroke: var(--brand); filter: drop-shadow(0 0 3px var(--brand-glow)); }
@keyframes lblp-spin { from { opacity: 1; } to { opacity: .2; } }
/* ручной приоритет — едва заметная фиолетовая точка, без анимации */
.schm .lblp-man { fill: var(--manual); opacity: .85; }
.schm .lbl.note { font: 500 calc(var(--fs-micro) * var(--tk, 1)) var(--sans); fill: var(--warn-ink); } /* «обратная связь не подтверждает» */
/* шильдик имени под глифом */
.schm .eplate { fill: rgba(8,13,15,.82); stroke: var(--line); stroke-width: 1; }
.schm .ename { font: 600 calc(var(--fs-val) * var(--tk, 1)) var(--sans); fill: var(--ink); letter-spacing: .03em; }
.schm .state-dot { fill: var(--ok); filter: drop-shadow(0 0 5px var(--ok-glow)); }
/* Та же лестница, что у DOM-точки .sdot (К1-А2): опорная r=4 из schematic.js,
   «внимание» +0,5, «авария» +1 и пульс. Свечение ОДНО на все три — разные
   радиусы читались как разные сигналы, хотя сигнал один. */
.schm .state-dot.warn  { fill: var(--warn);  r: 4.5px; filter: drop-shadow(0 0 5px var(--warn-glow)); }
.schm .state-dot.alarm { fill: var(--alarm); r: 5px;   filter: drop-shadow(0 0 5px var(--alarm-glow)); animation: pulse-dot 1.1s infinite; }

/* ── насос: крыльчатка крутится при работе ── */
.schm .impeller { stroke: #8fb8a6; stroke-width: 2.2; stroke-linecap: round; }
.schm .snode.run .impeller { stroke: var(--brand); }
.schm .rotor { transform-box: fill-box; transform-origin: center; }
.schm .snode.run .rotor { animation: schm-spin 1.1s linear infinite; }
.schm .snode.run.warn .rotor, .schm .snode.run.alarm .rotor { animation-duration: 1.1s; }
.schm .flow-arrow { fill: none; stroke: var(--brand); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; opacity: .0; }
.schm .snode.run .flow-arrow { opacity: .85; }

/* ── вентилятор: лопасти ── */
.schm .blade { fill: rgba(143,184,166,.28); stroke: #8fb8a6; stroke-width: 1.4; stroke-linejoin: round; }
.schm .snode.run .blade { fill: rgba(51,224,163,.34); stroke: var(--brand); }

/* ── компрессор: стрелка манометра ДРОЖИТ у отметки (реалистичнее, чем
   круговое вращение — стрелка не пропеллер) ── */
.schm .needle { transform-box: fill-box; transform-origin: center; }
.schm .needle-l { stroke: var(--brand); stroke-width: 2; stroke-linecap: round; }
.schm .snode.run .needle { animation: schm-needle 1.4s ease-in-out infinite alternate; }
@keyframes schm-needle { from { transform: rotate(26deg); } to { transform: rotate(44deg); } }

/* ── котёл: оранжевое пламя при работе (исключение из цвета линий) ──
   Живой огонь БЕЗ дёрганья: язык медленно и несимметрично покачивается
   (rotate ±пара градусов + лёгкое «дыхание» по высоте, 4 опорных кадра —
   не туда-сюда), ядро дышит в противофазе быстрее, свечение мерцает едва
   заметно, изредка всплывают искры. Розжиг/гашение — мягким fade. */
.schm .flame { opacity: 0; transition: opacity .45s; }
.schm .snode.run .flame { opacity: 1; }
.schm .flpath { fill: url(#hgrad); filter: drop-shadow(0 0 6px rgba(255,120,40,.7)); }
.schm .fl-in { fill: rgba(10,16,18,.88); }   /* капля-вырез внутри пламени (референс) */
.schm .fl, .schm .fl-core { transform-box: fill-box; transform-origin: 50% 100%; }
/* фильтр #flameFx (feTurbulence+feDisplacementMap в defs) колышет сам контур
   пламени — «настоящий» огонь; поверх — покачивание языка и дыхание ядра */
.schm .snode.run .fl { animation: schm-flame 2.4s ease-in-out infinite; filter: url(#flameFx); }
.schm .snode.run .fl-core { animation: schm-flame-core 1.7s ease-in-out infinite; }
.schm .fl-glow { fill: url(#fglow); opacity: 0; transform-box: fill-box; transform-origin: center; }
.schm .snode.run .fl-glow { animation: schm-flame-halo 2.4s ease-in-out infinite; }
.schm .ember { fill: #ffbf78; opacity: 0; }
.schm .snode.run .ember { animation: schm-ember 2.6s linear infinite; }
@keyframes schm-flame {
  0%, 100% { transform: rotate(0deg) scale(1, 1); }
  24%      { transform: rotate(-3.2deg) scale(0.97, 1.07); }
  52%      { transform: rotate(2.6deg) scale(1.03, 0.93); }
  78%      { transform: rotate(-1.4deg) scale(0.99, 1.04); }
}
@keyframes schm-flame-core {
  0%, 100% { transform: scale(1, 1); }
  36%      { transform: scale(0.82, 1.18); }
  68%      { transform: scale(1.12, 0.88); }
}
@keyframes schm-flame-halo {
  0%, 100% { opacity: .5;  transform: scale(1); }
  30%      { opacity: .95; transform: scale(1.14); }
  65%      { opacity: .65; transform: scale(0.95); }
}
@keyframes schm-ember {
  0%   { transform: translate(0, 0); opacity: 0; }
  10%  { opacity: .95; }
  55%  { transform: translate(1.5px, -9px); opacity: .6; }
  100% { transform: translate(-1px, -17px); opacity: 0; }
}

/* ── клапан: заслонка/ручка (без анимации, только открыт/закрыт) ── */
.schm .snode .stem, .schm .snode .handle { stroke: #8fb8a6; stroke-width: 2.4; stroke-linecap: round; }
.schm .snode .handle.sq { fill: rgba(13,20,22,.92); }
/* вода сквозь работающий насос — гасится/зажигается по .run */
.schm .snode .pumpflow { opacity: 0; transition: opacity .3s; }
.schm .snode.run .pumpflow { opacity: 1; }
/* сплошной цвет по той же причине (bbox-градиент на горизонтали не рисуется) */
.schm .snode .pump-fill { fill: none; stroke: #38c9d8; stroke-width: 3.4; opacity: .95; }
.schm .snode .disc { stroke: var(--brand); stroke-width: 3; stroke-linecap: round; }
.schm .snode .disc.closed { stroke: #9fb0aa; stroke-width: 4; }

/* ── бак/сосуд: вода бирюзовая, волна; пузырьки при реальном притоке ── */
.schm .bub { fill: rgba(234,252,255,.65); opacity: 0; }
.schm .snode.wet .bub { animation: schm-bub 2.8s linear infinite; }
@keyframes schm-bub {
  0%   { transform: translateY(0); opacity: 0; }
  12%  { opacity: .8; }
  88%  { opacity: .55; }
  100% { transform: translateY(calc(-1 * var(--rise, 30px))); opacity: 0; }
}
.schm .water { opacity: .95; }
/* контур сосуда БЕЗ заливки (иначе гасит бирюзу воды), тёмный фон — отдельным
   слоем ПОД водой; специфичность = .snode .sym-body, порядок решает */
.schm .snode .vessel { fill: none; }
.schm .snode .vessel-bg { fill: rgba(9,15,17,.6); stroke: none; }
.schm .wave  { opacity: .95; animation: schm-wave 2.6s linear infinite; }
.schm .wave2 { opacity: .55; animation: schm-wave 3.6s linear infinite reverse; }
.schm .vlevel { font: 700 calc(var(--fs-body) * var(--tk, 1)) var(--mono); fill: #eafcff; stroke: rgba(7,16,18,.85); stroke-width: 3px; paint-order: stroke; }

/* ── блок непривязанных сигналов (слева сверху) ── */
.schm .misc-bg { fill: rgba(14,19,21,.9); stroke: var(--line); stroke-width: 1; }
.schm .misc-rule { stroke: var(--line-2); stroke-width: 1; }
.schm .misc-title { font: 600 calc(var(--fs-tag) * var(--tk, 1)) var(--mono); fill: var(--brand); letter-spacing: .22em; }
/* подзаголовок группы каналов «голой» платы (ВХОДЫ/АНАЛОГОВЫЕ/ВЫХОДЫ) */
/* Подзаголовок группы каналов и подпись вида накопителя НЕСУТ ДАННЫЕ, а
   значит не могут жить на --ink-3 (контракт чернил К1-А1): на фоне схемы это
   3,38:1 при норме 4,5:1, да ещё в 7 экранных пикселей после масштаба. */
.schm .misc-sub { font: 600 calc(var(--fs-tag) * var(--tk, 1)) var(--mono); fill: var(--ink-2); letter-spacing: .14em; text-transform: uppercase; }
.schm .misc-k { font: 500 calc(var(--fs-body) * var(--tk, 1)) var(--sans); fill: var(--ink-2); }
.schm .misc-v { font: 600 calc(var(--fs-body) * var(--tk, 1)) var(--mono); fill: var(--ink); }
/* --sq: строка «Сигналов объекта» такая же узкая, как пилюля узла, и правило
   там то же — имя ужимается кеглем прежде, чем резаться, а число не режется
   никогда (schematic.js::fitLabel). */
.schm .misc-k, .schm .misc-v { font-size: calc(var(--fs-body) * var(--tk, 1) * var(--sq, 1)); }
/* подпись вида в «Накопителях» — приглушённо, не в один тон со значением */
.schm .misc-t { font: 500 calc(var(--fs-micro) * var(--tk, 1)) var(--sans); fill: var(--ink-2); }
.schm .misc-row { cursor: pointer; }
.schm .misc-hit { fill: transparent; }
/* зебра чётных строк — едва заметно, чтобы длинный список не сливался */
.schm .misc-row.zeb .misc-hit { fill: var(--line-2); }
.schm .misc-row:hover .misc-hit { fill: rgba(150,200,175,.08); }
/* активный канал (DI замкнут / DO включён): фосфорная «лампа» слева +
   значение цветом бренда — вкл/выкл различимы не только словом (17.08) */
.schm .misc-led { fill: var(--brand); filter: drop-shadow(0 0 4px var(--brand-glow)); }
.schm .misc-row.on .misc-v { fill: var(--brand); }
/* на строке с уровнем «лампа» не рисуется: зелёный штрих поверх охры/сургуча
   спорил бы со статусом — там весь фон и есть сигнал */
.schm .misc-row.warn .misc-led, .schm .misc-row.alarm .misc-led { display: none; }
/* гибрид: alarm — полноразмерный «чип» на misc-hit (самая громкая строка),
   warn — «маркер-текстовыделитель» .misc-mark высотой по тексту (рисует rowAt
   в schematic.js); в тёмной теме подложки приглушённые, поверх — CRT-свечение */
.schm .misc-mark { fill: var(--warn-soft); }
.schm .misc-row.alarm .misc-hit { fill: var(--alarm-soft); }
.schm .misc-row.warn  .misc-k, .schm .misc-row.warn  .misc-v { fill: var(--warn-ink); filter: drop-shadow(0 0 4px var(--warn-glow)); }
.schm .misc-row.alarm .misc-k, .schm .misc-row.alarm .misc-v { fill: var(--alarm-ink); filter: drop-shadow(0 0 5px var(--alarm-glow)); }
/* «Накопители»: кликабельна каждая строка (канал или карточка накопителя) */

/* ── легенда схемы (§1.8): одна строка, ненавязчиво ── */
/* Подсказка-«коуч» демо-аккаунта (main.js themeCoach): плавающая карточка
   у пункта меню. Позицию ставит JS, здесь — вид и мягкое появление. */
.coach {
  position: fixed; z-index: 300; max-width: 240px;
  background: var(--panel); border: 1px solid var(--brand); border-radius: 10px;
  box-shadow: 0 8px 28px rgba(0,0,0,.45), 0 0 12px var(--brand-glow);
  padding: 10px 12px; font-size: var(--fs-body); line-height: 1.45; color: var(--ink);
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
  animation: coach-in .35s ease-out;
}
@keyframes coach-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .coach { animation: none; } }

.schm-legend { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 8px 6px 2px;
  font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); }
.schm-legend .lg-i { display: inline-flex; align-items: center; gap: 6px; }
.schm-legend i { display: inline-block; flex: 0 0 auto; }
/* Точка легенды — БУКВАЛЬНО тот же компонент .sdot, что и на схеме: образец,
   нарисованный иначе, чем объясняемый сигнал, объясняет не тот сигнал. */
.schm-legend .lg-pipe { width: 22px; height: 6px; border-radius: 3px; border: 1px solid #26333a; }
/* Плашка «вода» — теми же токенами, что заливка бака (--water-a/b): значения
   здесь и там совпадали и раньше, но двумя литералами, и разъехались бы при
   первой же правке одного из них. */
.schm-legend .lg-pipe.water { background: linear-gradient(180deg, var(--water-a), var(--water-b)); }
.schm-legend .lg-pipe.air   { background: linear-gradient(180deg, rgba(219,231,238,.55), rgba(143,176,192,.6)); }
.schm-legend .lg-pipe.sew   { background: linear-gradient(180deg, #a3a05a, #5d6337); }
.schm-legend .lg-pipe.gas   { background: linear-gradient(180deg, #cbb26f, #7d6a2f); }
.schm-legend .lg-pipe.dry   { background: #26333a; }

/* ── поповер настройки оборудования (§3.10) ── */
.schm-cfg {
  position: fixed; right: 26px; top: 120px; z-index: 60; width: 288px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 18px 50px rgba(0,0,0,.55); padding: 14px; display: grid; gap: 10px;
}
.schm-cfg .cfg-head { font: 700 var(--fs-body) var(--disp); color: var(--brand); letter-spacing: .02em; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
/* Две колонки, а не space-between (F-176): при разной длине подписи («Среда»,
   «Открыт при») контролы начинались в разных местах, и поповер выглядел
   рассыпанным. Колонка подписи фиксирована — селекты встают в одну линию. */
.schm-cfg .cfg-row { display: grid; grid-template-columns: 92px 1fr; align-items: center; gap: var(--sp-2); font-size: var(--fs-body); color: var(--ink-2); }
.schm-cfg .cfg-sel { background: var(--panel-3); color: var(--ink); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 5px 8px; font: 500 var(--fs-body) var(--sans); width: 100%; min-width: 0; }
.schm-cfg .cfg-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-top: 4px; }

@keyframes schm-spin { to { transform: rotate(360deg); } }
@keyframes schm-flow { to { stroke-dashoffset: -31; } }
@keyframes schm-flow-rev { to { stroke-dashoffset: 31; } }
@keyframes schm-wave { to { transform: translateX(30px); } }
@keyframes schm-apulse { 50% { stroke-opacity: .45; } }
.schm.no-motion .pipe-water.flow, .schm.no-motion .rotor, .schm.no-motion .needle,
.schm.no-motion .wave, .schm.no-motion .wave2, .schm.no-motion .fl, .schm.no-motion .fl-core,
.schm.no-motion .fl-glow, .schm.no-motion .ember, .schm.no-motion .bub,
.schm.no-motion .snode.alarm .sym-body, .schm.no-motion .state-dot.alarm,
.schm.no-motion .lblp-spin line { animation: none !important; }
.schm.no-motion .snode.run .fl { filter: none; }          /* турбулентность SMIL стопится снятием фильтра */
.schm.no-motion .snode.run .fl-glow { opacity: .55; }     /* гало остаётся статичной подсветкой топки */
@media (prefers-reduced-motion: reduce) {
  .schm .pipe-water.flow, .schm .rotor, .schm .needle, .schm .wave, .schm .wave2,
  .schm .fl, .schm .fl-core, .schm .fl-glow, .schm .ember, .schm .bub,
  .schm .snode.alarm .sym-body, .schm .state-dot.alarm,
  .schm .lblp-spin line { animation: none !important; }
  .schm .snode.run .fl { filter: none; }
  .schm .snode.run .fl-glow { opacity: .55; }
}
/* без анимации спиннер остаётся видимым «веером» — палочки полупрозрачные */
.schm.no-motion .lblp-spin line { opacity: .55; }
@media (prefers-reduced-motion: reduce) { .schm .lblp-spin line { opacity: .55; } }

/* === К1-Б: вход, приглашение, профиль === */
/* ──────────────────────────────────────────────────────────── login ─── */
.login-wrap {
  min-height: 100vh; display: grid; place-items: center; width: 100%; padding: 20px;
  background:
    radial-gradient(1100px 700px at 50% -10%, rgba(51,224,163,.08), transparent 55%),
    var(--bg);
  position: relative; overflow: hidden;
}
.login-wrap::before { /* гигантский водяной знак-кандзи */
  content: "守"; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-54%);
  font-family: var(--kanji); font-weight: 800; font-size: min(58vw, 620px); line-height: 1;
  color: transparent; -webkit-text-stroke: 1px rgba(51,224,163,.09); pointer-events: none; user-select: none;
}
.login {
  width: min(390px, 100%); background: rgba(14,19,21,.82); backdrop-filter: blur(8px);
  border-radius: var(--r-lg); box-shadow: var(--shadow); border: 1px solid var(--line);
  padding: 30px 28px; position: relative; z-index: 1;
}
/* ПРОБОВАЛИ И СНЯЛИ: реперная шкала из штрихов по верхней кромке карточки
   (замечание жюри «вход и приглашение — самые безликие кадры»). На кадре
   штрихи цвета --line-2 на подложке карточки не читались вовсе, а одинокая
   фосфорная метка посередине выглядела артефактом отрисовки, а не прибором.
   Украшение, которое не читается, — это шум: экран входа оставлен чистым,
   его знак — марка и водяной кандзи фона. */
.login .mark {
  width: 50px; height: 50px; border-radius: 13px;
  background: radial-gradient(120% 120% at 30% 20%, #14342c, #08130f);
  border: 1px solid rgba(51,224,163,.4);
  color: var(--brand); display: grid; place-items: center; font: 800 25px var(--kanji);
  text-shadow: 0 0 18px var(--brand-glow); margin-bottom: 16px;
}
.login h1 { font: 700 var(--fs-h1) var(--disp); letter-spacing: .04em; margin: 0 0 3px; }
.login .sub { color: var(--ink-2); font: 500 var(--fs-body) var(--mono); letter-spacing: .04em; margin-bottom: 18px; }
.login .in { width: 100%; margin-bottom: 11px; }
.login .btn { width: 100%; margin-top: 6px; justify-content: center; }

/* Поле входа с подписью. Подпись обычным регистром (.lb.long), а не капсом с
   разрядкой: «ЛОГИН» и «ПАРОЛЬ» набором букв читаются медленнее, а это первая
   форма продукта. Плейсхолдеры сняты — имя поля обязано остаться видимым
   после ввода, иначе заполненную форму нельзя перепроверить. */
.login .lf { text-align: start; }
.login .lf .lb { margin: 12px 0 5px; }
.login .lf .small { margin: -5px 0 11px; }

/* «Показать пароль»: на мягкой клавиатуре телефона опечатку в пароле иначе не
   найти, а совет «проверьте Caps Lock» нечем проверить. Кнопка стоит В поле,
   поэтому полю нужен отступ справа; в RTL и то и другое зеркалится. */
.login .pw { position: relative; display: block; }
.login .pw > .in { padding-inline-end: 42px; }
.login .pw-eye {
  position: absolute; inset-inline-end: 3px; top: 0; height: var(--ctl-h); width: 36px;
  display: grid; place-items: center; cursor: pointer;
  background: none; border: 0; border-radius: 8px; color: var(--ink-2); padding: 0;
}
.login .pw-eye:hover { color: var(--brand); }
/* На телефоне поле вырастает до 44 px — вместе с ним растёт и зона нажатия
   кнопки: 36×36 внутри 44-пиксельного поля это промах у самого края. */
@media (max-width: 860px) {
  .login .pw-eye { height: var(--ctl-h-lg); width: var(--ctl-h-lg); }
}

/* Ошибка входа — не строка мелким шрифтом под кнопкой, а сообщение со знаком
   и рамкой прямо над кнопкой: человек с фокусом в поле пароля прежнюю строку
   просто не замечал. Пустой блок места не занимает. */
.login .err {
  color: var(--alarm-ink); font-size: var(--fs-body); line-height: 1.45;
  margin-top: 12px; display: flex; gap: 8px; align-items: flex-start; text-align: start;
  background: var(--alarm-soft); border: 1px solid var(--alarm-edge);
  border-radius: 8px; padding: 9px 11px;
}
.login .err:empty { display: none; }
.login .err .err-ic { flex: 0 0 auto; margin-top: 1px; }
/* Вторая строка — не причина отказа, а подсказка «куда идти, если пароль
   верный»: отделяем её тоном и переносом, чтобы не спорила с главной фразой. */
.login .err .err-x { display: block; margin-top: 5px; color: var(--ink-2); }
.login .lg-help { margin-top: 12px; line-height: 1.45; text-align: start; }
.login .field-err { text-align: start; margin-bottom: 8px; }

/* Приглашение — первый экран нового сотрудника. Заголовок отвечает «куда меня
   позвали», .sub — «кто я здесь», .iv-role — «что мне теперь можно» (фраза
   приходит с сервера и снята с реальных гейтов кода), .iv-step — «что делать
   прямо сейчас». */
.login h1 + .sub { margin-bottom: 10px; }
.login .iv-role {
  line-height: 1.45; text-align: start; margin-bottom: 14px;
  padding-inline-start: 10px; border-inline-start: 2px solid var(--line-2);
}
.login .iv-step { font-size: var(--fs-body); color: var(--ink); margin-bottom: 6px; text-align: start; }
/* Ключ второго фактора переносится по любому знаку: это длинная строка без
   пробелов, и без разрыва она вылезала за карточку. */
.login .iv-key { word-break: break-all; text-align: start; margin-bottom: 10px; }
.login .v.iv-key { background: var(--bg); padding: 10px; border-radius: 8px; }

/* Caps Lock. Янтарь, а не красный: это не отказ, а предупреждение до отправки
   формы. Строка стоит под своим полем и снимается вместе с уходом из него.
   Правило [hidden] обязательно: display:flex перебивает атрибут hidden. */
.caps-hint {
  display: flex; gap: var(--sp-2); align-items: flex-start; text-align: start;
  color: var(--warn-ink); font-size: var(--fs-body); line-height: 1.45;
  margin: 0 0 var(--sp-3);
}
.caps-hint[hidden] { display: none; }
.caps-hint .err-ic { flex: 0 0 auto; margin-top: 1px; }

/* «Оставаться в системе»: подпись — предложение с большой буквы, как соседние,
   отступ задаётся классом, а не строкой в разметке. */
.login .lg-keep { margin: var(--sp-2) 0; }

/* Язык — один список внизу карточки, тихий: единственный акцент экрана входа
   это кнопка «Войти». Ширина по содержимому (самоназвания разной длины),
   рамка приглушённая, а сам язык — данные, поэтому чернила основные. */
.lg-lang { margin-top: var(--sp-4); display: flex; justify-content: center; }
.lang-sel {
  max-width: 100%; background: transparent; box-shadow: none;
  border-color: var(--line-2); color: var(--ink);
}
.lang-sel:hover:not(:disabled) { border-color: var(--edge); }
/* Поля формы входа тянутся во всю карточку (.login .in) — списку языка это не
   нужно: он не поле ввода, а подпись с выбором, и ширина у него по слову. */
.login .lg-lang .lang-sel { width: auto; margin-bottom: 0; }

/* ── Резервные коды второго фактора (показываются ровно один раз) ─────────
   Коды столбиком: их переписывают на бумагу и сверяют по одному, а строкой
   через два пробела спутать 0 и O проще всего. Кнопки «Копировать»/«Скачать»
   не растягиваются на всю карточку (общее правило .login .btn), иначе рядом
   с ними теряется главная кнопка «Войти». */
.bk {
  text-align: start; margin-top: var(--sp-4); padding: var(--sp-4);
  background: var(--bg); border: 1px solid var(--warn-line); border-radius: var(--r-sm);
}
.bk-t { font: 700 var(--fs-h2) var(--disp); letter-spacing: var(--ls-tight); }
.bk-h { line-height: 1.45; margin: var(--sp-1) 0 var(--sp-3); }
.bk-codes {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-1) var(--sp-3); font-size: var(--fs-val);
}
.bk-act { margin-top: var(--sp-3); }
.login .bk-act .btn { width: auto; margin-top: 0; }
.bk-ok { margin-top: var(--sp-3); }

/* ─────────────────────────────────────────────────────────── misc ─── */
.muted { color: var(--ink-2); }
.small { font-size: var(--fs-body); }
.mt { margin-top: var(--sp-4); }
.row-g { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
.kv { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2) var(--sp-4); font-size: var(--fs-body); }
.kv > :nth-child(odd) { color: var(--ink-2); font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase; align-self: center; }
.kv b { font-weight: 600; }
/* === К1-А2: состояния списка — пусто · не найдено · ошибка · нет доступа === */
/* ЧЕТЫРЕ РАЗНЫХ СОСТОЯНИЯ. Раньше «идёт загрузка» и «здесь ничего нет»
   рисовались ОДНИМ классом .empty с водяным кандзи 守 и отличались одним
   словом. На медленной связи (пульт часто смотрят через VPN) человек читал
   «Аккаунтов нет» на экране, где данные ещё едут, и уходил со страницы.
   Плюс сами пустые состояния были тупиками — констатация без выхода.
   Теперь: .empty — пусто (守 + действие), .empty.nf — не нашлось по фильтру
   (воронка), .empty.err — не загрузилось (разрыв связи + «Обновить»),
   .empty.wall — раздел не для вашей роли (щит), .loading — данные едут. */
.empty {
  color: var(--ink-2); text-align: center; padding: var(--sp-6) var(--sp-5);
  border: 1px dashed var(--line); border-radius: var(--r); background: var(--panel);
}
/* 守 «страж» — знак ИМЕННО пустоты: всё в порядке, показывать просто нечего.
   У остальных трёх состояний свой знак, поэтому кандзи у них снят. */
.empty::before { content: "守"; display: block; font: 800 30px var(--kanji); color: var(--ink-3); opacity: .5; margin-bottom: var(--sp-2); }
.empty.noka::before, .empty.nf::before, .empty.err::before, .empty.wall::before { content: none; }
.empty .e-ic { display: flex; justify-content: center; margin-bottom: var(--sp-2); color: var(--ink-3); }
.empty.err .e-ic  { color: var(--warn-ink); }
.empty.wall .e-ic { color: var(--ink-2); }
.empty .e-t { font: 700 var(--fs-h2) var(--disp); color: var(--ink); letter-spacing: var(--ls-tight); }
/* Строка «почему пусто и что это даст» — мерой абзаца и по центру: 200 знаков
   во всю ширину монитора глаз теряет на обратном ходе. */
.empty .e-h { margin: var(--sp-2) auto 0; color: var(--ink-2); font-size: var(--fs-body); line-height: 1.55; }
.empty .e-a { margin-top: var(--sp-4); display: flex; justify-content: center; gap: var(--sp-2); flex-wrap: wrap; }

/* ДАННЫЕ ЕДУТ. Габариты будущего списка + одна тонкая линия развёртки.
   Рамки приёма намеренно жёсткие: линия БЕЗ свечения и БЕЗ статусного цвета
   (иначе ожидание конкурирует за периферийное зрение с аварией), включается
   не раньше 400 мс (быстрый ответ не должен мигать полосой), гаснет при
   prefers-reduced-motion и на экране, где уже горит авария (класс .quiet). */
.loading {
  position: relative; overflow: hidden;
  border: 1px solid var(--line-2); border-radius: var(--r); background: var(--panel);
  padding: var(--sp-4); display: grid; gap: var(--sp-3);
}
.loading .ld-row { height: 13px; border-radius: var(--r-xs); background: var(--panel-2); opacity: .75; }
.loading .ld-row:nth-child(3n) { width: 62%; }
.loading .ld-row:nth-child(3n + 2) { width: 84%; }
.loading .ld-scan {
  position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: var(--ink-3); opacity: 0;
  animation: ld-scan 1.7s linear .4s infinite;
}
@keyframes ld-scan {
  0%   { top: 0;    opacity: 0; }
  12%  { opacity: .55; }
  88%  { opacity: .55; }
  100% { top: 100%; opacity: 0; }
}
.loading.quiet .ld-scan { display: none; }
@media (prefers-reduced-motion: reduce) { .loading .ld-scan { display: none; } }

/* === К1-А2: значение с порогом и возраст события === */
/* «Т подачи 86,52» — строка, по которой решают, ехать ли сейчас. Порог идёт
   вторичной моно-подписью ПОД числом: число обязано читаться с одного взгляда,
   а расстояние до уставки — со второго. */
.valx { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 1px; }
.valx .valx-lim {
  /* Без КАПСА: единица внутри подписи («0,5 бар», «85 °C») в верхнем регистре
     перестаёт быть единицей и начинает читаться как код. */
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-tight);
  color: var(--ink-2); white-space: nowrap;
}
/* Возраст события живым числом (data-ago). Табличные цифры — чтобы столбец
   «Длится» не дёргался на каждой секунде. */
.ago { font-variant-numeric: tabular-nums; }

/* Пояснение страницы — отдельной строкой под заголовком, мерой абзаца.
   Подзаголовок .sub при этом остаётся КОНТЕКСТОМ (кто/что/сколько) и не
   превращается ни в инструкцию, ни в статус — см. контракт pageHead() в ui.js. */
/* Отрицательный верхний отступ подтягивает пояснение к заголовку: у .page-head
   свой нижний отступ шкалы, и без подтяжки пояснение отрывалось от того, что
   поясняет. Число равно шагу шкалы, а не «сколько показалось». */
.page-note { margin: calc(var(--sp-3) * -1) 0 var(--sp-4); color: var(--ink-2); font-size: var(--fs-body); line-height: 1.55; }
.badges { display: flex; gap: var(--sp-1); flex-wrap: wrap; }

/* ─── карта (maplibre) под тёмную тему ─── */
/* Фосфорная тонировка подложки: dark-тайлы уже обесцвечены raster-paint-ом
 * (park.js), CSS-фильтр канвы красит монохром в зелёный ЭЛТ-люминофор.
 * Маркеры и попапы — DOM поверх канвы, фильтр их не трогает. */
#park-map { position: relative; }
#park-map .maplibregl-canvas { filter: hue-rotate(55deg) saturate(.75) brightness(1.18) contrast(1.02); }
#park-map::after { /* лёгкая виньетка поверх карты — сшивает её с темой */
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  box-shadow: inset 0 0 70px 4px rgba(3,6,5,.4);
}

/* ── маркер объекта: светлячок со статусным свечением + подпись-имя ── */
.mk { position: relative; width: 18px; height: 18px; cursor: pointer; }
.mk i {
  position: absolute; inset: 0; border-radius: 50%;
  background: var(--ok); border: 2px solid rgba(233,237,232,.85);
  box-shadow: 0 0 14px 2px var(--ok-glow), 0 1px 4px rgba(0,0,0,.6);
}
.mk.warn i    { background: var(--warn);    box-shadow: 0 0 14px 2px var(--warn-glow), 0 1px 4px rgba(0,0,0,.6); }
.mk.alarm i   { background: var(--alarm);   box-shadow: 0 0 16px 3px var(--alarm-glow), 0 1px 4px rgba(0,0,0,.6); animation: pulse-dot 1.1s infinite; }
.mk.alarm::before { /* расходящееся кольцо тревоги */
  content: ""; position: absolute; inset: -3px; border-radius: 50%;
  border: 2px solid var(--alarm); opacity: 0; animation: map-ping 1.6s ease-out infinite;
}
/* офлайн-маркер — полый (кольцо без заливки): на карте читается мгновенно */
.mk.offline i { background: var(--panel); border: 3px solid var(--offline); box-shadow: 0 1px 4px rgba(0,0,0,.6); }
.mk-tag {
  position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  white-space: nowrap; pointer-events: none;
  /* ПОДПИСЬ МАРКЕРА — НЕ МОНОШИРИННАЯ. Глиф «№» в IBM Plex Mono 600 на 10,5 px
     сливается в тёмный блок и слипается с цифрой: «Насосная станция №4» на
     карте была нечитаема ровно в том месте, которым один объект отличается от
     другого. Моноширинность здесь не давала ничего — подпись ни с чем не
     выравнивается по колонкам (в плитке тот же текст набран --sans и читается).
     Разрядку убираем до --ls-tight: она разгоняла и без того узкую подпись. */
  font: 600 var(--fs-tag) var(--sans); letter-spacing: var(--ls-tight); color: var(--ink);
  /* Плёнка панели поверх контента — роль, а не литерал ночного цвета
     (контракт К1-А1 §12): та же подложка работает в обеих темах.
     ⚠ К1-А1: после этого строка `html[data-theme="light"] .mk-tag { background: … }`
     в общем светлом блоке лишняя — её можно снять. */
  background: var(--sheet); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 2px 8px;
}
.mk.alarm .mk-tag   { color: var(--alarm-ink); border-color: rgba(255,77,63,.5); }
.mk.warn .mk-tag    { color: var(--warn-ink);  border-color: rgba(255,176,32,.45); }
.mk.offline .mk-tag { color: var(--offline); }
@media (prefers-reduced-motion: reduce) { .mk.alarm i, .mk.alarm::before { animation: none; } }

/* ── кластер: тёмный диск с кольцом по худшему статусу ── */
.mk-cluster {
  display: flex; align-items: center; justify-content: center; border-radius: 50%;
  /* Та же плёнка, что и у подписи маркера (контракт К1-А1 §12).
     ⚠ К1-А1: светлая строка `html[data-theme="light"] .mk-cluster { background: … }`
     после этого лишняя. */
  background: var(--sheet); border: 2.5px solid var(--ok); color: var(--ok-ink);
  font: 700 var(--fs-body) var(--mono); cursor: pointer;
  box-shadow: 0 0 16px -2px var(--ok-glow), 0 1px 5px rgba(0,0,0,.55);
}
.mk-cluster.warn    { border-color: var(--warn);    color: var(--warn-ink);  box-shadow: 0 0 16px -2px var(--warn-glow), 0 1px 5px rgba(0,0,0,.55); }
.mk-cluster.alarm   { border-color: var(--alarm);   color: var(--alarm-ink); box-shadow: 0 0 18px -2px var(--alarm-glow), 0 1px 5px rgba(0,0,0,.55); animation: pulse-dot 1.4s infinite; }
.mk-cluster.offline { border-color: var(--offline); color: var(--offline);   box-shadow: 0 1px 5px rgba(0,0,0,.55); }
@media (prefers-reduced-motion: reduce) { .mk-cluster.alarm { animation: none; } }

/* Двойная специфичность (.maplibregl-popup .maplibregl-popup-content): vendor
 * CSS грузится ПОЗЖЕ theme.css и его `.maplibregl-popup-content{background:#fff}`
 * перебивал наш фон одинарной специфичностью → белый фон + светлый текст =
 * «белым по белому». Двойной селектор гарантированно выигрывает каскад. */
.maplibregl-popup .maplibregl-popup-content { background: var(--panel-2); color: var(--ink); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); font-family: var(--sans); padding: 10px 12px; }
.maplibregl-popup .maplibregl-popup-content b { color: var(--ink); }
.maplibregl-popup .maplibregl-popup-content a { color: var(--brand); }
.maplibregl-popup-tip { border-top-color: var(--panel-2) !important; border-bottom-color: var(--panel-2) !important; }
.maplibregl-popup-close-button { color: var(--ink-2); font-size: var(--fs-h2); }
.maplibregl-ctrl-group { background: var(--panel-2); border: 1px solid var(--line); box-shadow: var(--shadow); overflow: hidden; }
.maplibregl-ctrl-group button { background: transparent; transition: background .12s; }
.maplibregl-ctrl-group button:hover { background: var(--panel-3); }
.maplibregl-ctrl-group button + button { border-top: 1px solid var(--line); }
/* иконки +/- маплибра — тёмные png в CSS-фоне; инвертируем в светлые (статично,
 * без hue-rotate, который мерцал) */
.maplibregl-ctrl button .maplibregl-ctrl-icon { filter: invert(0.92); opacity: .85; }
.maplibregl-ctrl-attrib { background: rgba(10,13,15,.7) !important; color: var(--ink-3); }
.maplibregl-ctrl-attrib a { color: var(--ink-2); }
.maplibregl-ctrl-attrib-button { filter: invert(0.92); opacity: .7; }

/* шапка и подпись печатного отчёта (разметка в history.js) — на экране скрыты */
.print-head, .print-sign { display: none; }

@media print {
  /* светлая палитра: переопределяем переменные — карточки/текст/линии
     перекрашиваются сами, без правки остальных правил.
     ⚠ Селекторы тем перечислены явно: у голого :root специфичность (0,1,0), а
     у html[data-theme="light"] в начале файла — (0,1,1), и @media её не
     добавляет. Без этого печать из ДНЕВНОЙ темы шла по бумажным токенам
     (--panel #f4f3ef, --line rgba(24,20,12,.18)): отчёт выходил тонированным
     сепией, а на монохромном принтере — серым растром по всем таблицам.
     Ночная палитра объявлена на голом :root и перекрывается здесь по порядку,
     отдельного селектора ей не нужно. */
  :root,
  html[data-theme="light"] {
    --bg: #fff; --bg-2: #fff;
    --panel: #fff; --panel-2: #f4f4f4; --panel-3: #ececec;
    --ink: #000; --ink-2: #333; --ink-3: #666;
    --line: #ccc; --line-2: #ddd;
    --brand: #0e6f5c; --brand-2: #0e6f5c; --brand-soft: transparent; --brand-glow: transparent;
    --alarm: #c62828; --alarm-ink: #b3261e; --alarm-soft: transparent; --alarm-glow: transparent;
    --warn: #b26a00; --warn-ink: #8a5a00; --warn-soft: transparent; --warn-glow: transparent;
    --ok: #1b7a4d; --ok-ink: #1b7a4d; --ok-soft: transparent; --ok-glow: transparent;
    --offline: #667069; --offline-soft: transparent;
    --shadow: none;
  }
  body::before, body::after { display: none !important; }
  .nav, .tabbar, .btn, .toasts, .face, .modal-back { display: none !important; }
  .main { padding: 0; animation: none; }
  /* Шапка страницы на ЛИСТЕ. Экранная шапка липкая и вытянута отрицательными
     боковыми полями на всю ширину полосы (см. @media (min-width:861px)) — на
     бумаге, где .main уже обнулил свои отступы, эти поля выносили её на 24 px
     за оба края листа, и подпись фильтра теряла последние буквы за правым
     обрезом (замер: scrollWidth 1304 при ширине листа 1280 на «Авариях»,
     «Журнале», «Мониторинге», «Пульте парка», «Структуре»). Для страницы
     объекта то же снятие уже сделано отдельным правилом ниже — у неё свой
     титульный отступ; здесь общее правило для всех остальных страниц.
     Фон не нужен: на белом листе он лишний слой, отчёркивает линия. */
  .page-head, body.nav-collapsed .page-head {
    position: static; z-index: auto;
    margin-inline: 0; padding-inline: 0;
    margin-block-start: 0; padding-block: 0 10px;
    background: none; border-bottom: 2px solid #000;
  }
  body { background: #fff; color: #000; }
  * { text-shadow: none !important; box-shadow: none !important; }
  /* Дневные правила живут НИЖЕ в файле и при равной специфичности выигрывают
     по порядку, поэтому здесь селектор на класс выше: :root[data-theme] вместо
     html[data-theme]. Гасим бумажный фон-градиент body и «плотные» чипы
     статуса — сплошная заливка это решение для ЭКРАНА (см. блок light-темы),
     на принтере она даёт чёрный прямоугольник вместо подписи, а на бумаге
     статус несут чернила и рамка, как в ночной теме. */
  :root[data-theme="light"] body { background: #fff; color: #000; }
  :root[data-theme="light"] .chip.alarm { background: transparent; color: var(--alarm-ink); border-color: var(--alarm); }
  :root[data-theme="light"] .chip.warn { background: transparent; color: var(--warn-ink); border-color: var(--warn); }
  :root[data-theme="light"] .chip.alarm .dot { background: var(--alarm); }
  :root[data-theme="light"] .chip.warn .dot { background: var(--warn); }
  .card { break-inside: avoid; }

  .print-head { display: block; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 2px solid #000; }
  .print-head .sub, .print-head small { font: 500 var(--fs-micro) var(--mono); color: #333; }
  .print-sign { display: block; margin-top: 32px; padding-top: 12px; border-top: 1px solid #ccc; break-inside: avoid; font-size: var(--fs-body); color: #333; }
}

/* ───────────────────────── справочники: дерево категорий ─── */
.cat-tree { display: grid; gap: 8px; }
.cat-group { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel); overflow: hidden; }
.cat-group > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; font: 700 var(--fs-val) var(--disp); letter-spacing: .01em; user-select: none; }
.cat-group > summary::-webkit-details-marker { display: none; }
.cat-group > summary:hover { background: var(--panel-2); }
/* Значок раскрытия — SVG из общего набора ICONS (бриф §2): типографский «▸»
   рисовался по-разному на разных платформах и не масштабировался под кегль.
   Поворот на 90° — здесь, чтобы разметка знала только про сам значок.
   Логическое свойство: в RTL закрытая ветка обязана указывать влево. */
.cat-tw { display: inline-flex; color: var(--ink-2); transition: transform var(--dur-1); flex: none; }
[dir="rtl"] .cat-tw { transform: rotate(180deg); }
details[open] > summary > .cat-tw { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .cat-tw { transition: none; } }
/* Счётчик позиций в группе — ДАННЫЕ (искомое число), а не плейсхолдер:
   --ink-3 по шапке файла живёт только в подсказках пустых полей и в title, и в
   тёмной теме давал 3,22:1 при норме 4,5:1. */
/* Логическое свойство, а не margin-left: в арабской раскладке счётчик обязан
   уходить к противоположному краю вместе со всей строкой. */
.cat-count { margin-inline-start: auto; font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 20px; padding: 1px 9px; }
.cat-brand { border-top: 1px solid var(--line-2); }
/* Ступень дерева — отступ ОТ НАЧАЛА строки: физический left уводил бы её в
   арабской раскладке к противоположному краю, и вложенность читалась наоборот. */
.cat-brand > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px;
  padding: 7px 14px; padding-inline-start: 30px; font: 600 var(--fs-body) var(--sans); color: var(--ink-2); user-select: none; }
.cat-brand > summary::-webkit-details-marker { display: none; }
.cat-brand > summary:hover { background: var(--panel-2); }
.cat-item { border-top: 1px solid var(--line-2); }
.cat-item > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 10px;
  padding: 8px 14px; padding-inline-start: 46px; user-select: none; }
.cat-item > summary::-webkit-details-marker { display: none; }
.cat-item > summary:hover { background: var(--panel-2); }
/* Код позиции справочника — по нему позицию ищут в бумажном каталоге и вводят
   в настройки платы: это данные второго плана (--ink-2, 7,4:1), а не
   плейсхолдер (--ink-3 давал 3,42:1 в тёмной теме). */
.cat-item .ci-code { font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); min-width: 30px; }
.cat-item .ci-name { font-weight: 600; font-size: var(--fs-body); }
.cat-item .ci-specs { display: flex; gap: 5px; flex-wrap: wrap; margin-inline-start: auto; align-items: center;
  justify-content: flex-end; }
.cat-spec { display: inline-flex; align-items: center; gap: 4px;
  font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); background: var(--panel-2);
  border: 1px solid var(--line-2); border-radius: 6px; padding: 1px 7px; white-space: nowrap; }
/* Слово, называющее число («внимание 1,2 МПа»): без него чип понимал только
   автор кода — знаки «~» и «!» смысла порога не несли и читались как обрывок
   соседнего диапазона. Подпись тише значения, но это ДАННЫЕ (--ink-2). */
.cat-spec .cs-l { font-family: var(--sans); letter-spacing: .01em; opacity: .85; }
.cat-spec .cs-ic { display: inline-flex; flex: none; }
/* Заливок `.cat-spec.warn/.alarm` здесь БОЛЬШЕ НЕТ — и возвращать их нельзя:
   справочник печатает пороги по умолчанию, а не состояние парка (см. точку
   уровня ниже). Правило удалено, а не оставлено «на всякий случай», чтобы
   класс не приманил ту же ошибку обратно. */
/* Точка уровня внутри НЕЙТРАЛЬНОГО чипа порога. Справочник печатает значения
   по умолчанию, а не состояние: залитые цветом пилюли на раскрытом дереве
   давали десятки красных пятен при нуле аварий в парке и обесценивали красный
   (ISA-101). Уровень различает точка — площадь 38 px², пятна она не делает. */
.cat-spec .cs-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; display: block; }
.cat-spec .cs-dot.warn { background: var(--warn); }
.cat-spec .cs-dot.alarm { background: var(--alarm); }
/* «Нигде не стоит» / «на 5 объектах» — факт, решающий судьбу позиции; он не
   про тревогу, поэтому тихий, а не цветной. */
.cat-spec.free { border-style: dashed; }
.cat-item .ci-body { padding: 4px 14px 12px; padding-inline-start: 46px; }
.cat-item .ci-body .kv { margin-top: 6px; }
/* Ячейка ЗНАЧЕНИЯ раскрытой карточки: там лежит проза («Тип: центробежный
   дымосос, колесо Ø900 мм») и непереносимые ссылки, а общая утилита
   `.nowrap, .v, .num { white-space: nowrap }` запрещала перенос всем ячейкам
   `.kv` сразу. Колонка грида `auto 1fr` брала ширину по min-content, `.cat-group
   { overflow: hidden }` отрезал лишнее — и текст не «уезжал», а ПРОПАДАЛ:
   горизонтальной прокрутки у страницы нет, достать его было нечем. Обрыв
   приходился на середину предложения и выглядел как конец текста. */
.cat-item .ci-body .kv > .v { white-space: normal; overflow-wrap: anywhere; min-width: 0; }
/* Ссылка на паспорт: печатается доменом, полный адрес — в href и подсказке. */
.cat-src { color: var(--brand); overflow-wrap: anywhere; }

/* ─────────────────── конфигуратор: клеммник платы (WF-C) ─── */
/* Единственная «боевая смелость» шага «Оборудование»: карта винтовых клемм,
   на которой видно занятое, свободное и остаток. Всё остальное на шаге —
   тихо и дисциплинированно. */
.trm { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel);
  padding: 9px 12px 10px; margin-bottom: 10px; }
.trm-h { display: flex; align-items: baseline; gap: 12px; margin-bottom: 8px; }
.trm-h .trm-t { font: 700 var(--fs-body) var(--disp); letter-spacing: .05em; text-transform: uppercase; color: var(--ink); }
.trm-caps { margin-inline-start: auto; display: flex; gap: 12px; font: 600 var(--fs-tag) var(--mono);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); }
/* Заполнение ≥80 % — янтарь, 100 % — красный: остаток клемм человек читает
   боковым зрением, не пересчитывая чипы. */
.trm-caps .near { color: var(--warn-ink); }
.trm-caps .over { color: var(--alarm-ink); }
.trm-row { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-bottom: 5px; }
.trm-lb { flex: 0 0 96px; display: inline-flex; align-items: center; gap: 4px;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); }
.trm-cell { display: inline-flex; align-items: center; gap: 6px; max-width: 158px;
  padding: 3px 9px; border-radius: 7px; border: 1px solid var(--line); background: transparent;
  color: var(--ink-2); font: 500 var(--fs-body) var(--sans); cursor: pointer; text-align: left; }
.trm-cell .tc-n { font: 700 var(--fs-tag) var(--mono); flex: 0 0 auto; }
.trm-cell .tc-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trm-cell.busy { background: var(--brand-soft); border-color: rgba(51,224,163,.35); color: var(--ink); }
.trm-cell.free { border-style: dashed; }
.trm-cell.svc { border-style: solid; background: var(--panel-2); color: var(--ink-2); cursor: default; }
.trm-cell.svc-busy { border-color: var(--warn-line); color: var(--warn-ink); cursor: pointer; }
.trm-cell:hover:not(:disabled) { border-color: var(--brand); }
.trm-cell:disabled { cursor: default; opacity: .75; }
/* «Горячая» клемма — наведение на чип канала на холсте и обратно: связь
   «жгут ↔ экран» держится подсветкой обоих концов. */
.trm-cell.hot, .chan-chip.hot { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-glow); }
.trm-busn { font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); margin-inline-end: 4px; }
.trm-hint { font-size: var(--fs-micro); color: var(--ink-2); margin-top: 4px; }

/* ───────────────────────── конфигуратор: холст-контейнеры ─── */
.pal-item { display: flex; align-items: center; gap: 7px; padding: 6px 9px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--panel-2); cursor: grab; font-size: var(--fs-body); color: var(--ink);
  width: 100%; text-align: left; }
.pal-item:hover { border-color: var(--brand); }
.pal-item:active { cursor: grabbing; }
.pal-item .pi-code { font: 600 var(--fs-tag) var(--mono); color: var(--ink-2); margin-left: auto; flex: 0 0 auto; }
.cvs-board { min-height: 460px; background:
  radial-gradient(900px 500px at 50% 0%, rgba(51,224,163,.03), transparent 60%), var(--panel-2);
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 12px;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: 12px; align-content: start; }
.cvs-board.drop-hot { outline: 2px dashed var(--brand); outline-offset: -6px; }
.eqc { position: relative; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel); display: flex; flex-direction: column; }
/* Кнопка удаления лежит поверх шапки-кнопки, а не внутри неё: вложенная кнопка
   невалидна, а нужны обе — открыть настройки и удалить. */
.eqc-del { position: absolute; top: 6px; inset-inline-end: 7px; z-index: 1; }
.eqc.sel { border-color: var(--brand); box-shadow: 0 0 0 1px rgba(51,224,163,.4); }
.eqc.drop-hot { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-glow); }
.eqc-head { display: flex; align-items: center; gap: 8px; padding: 8px 38px 8px 11px; border-bottom: 1px solid var(--line-2);
  cursor: pointer; width: 100%; background: transparent; color: var(--ink); text-align: left; }
.eqc-ico { flex: 0 0 auto; display: block; }
/* Фигуры иконки заливки не несут: без явной обводки и прозрачной заливки
   котёл/бак рисуется чёрным квадратом. */
.eqc-ico .body, .eqc-ico rect { fill: none; stroke: var(--ink-2); stroke-width: 1.5; }
.eqc-ico .glyph { color: var(--ink-2); }
/* Марка оборудования и подписи секций — ДАННЫЕ, а не декор: с --ink-3 они
   давали 3,4:1 и читались как выключенные. */
.eqc-head .eqc-name { font: 700 var(--fs-body) var(--disp); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.eqc-head .eqc-marka { font: 500 var(--fs-tag) var(--mono); color: var(--ink-2); }
/* Карточки в ряду тянутся до общей высоты: без этого низ ряда был рваным. */
.cvs-board > .eqc { align-self: stretch; }
/* Тело растёт до низа карточки: без flex:1 карточка с одним каналом
   заканчивалась выше соседей и ряд получался рваным. */
.eqc-body { flex: 1; padding: 8px 11px 11px; display: grid; gap: 8px; min-height: 40px; align-content: start; }
.eqc-sec { font: 600 var(--fs-tag) var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); margin-bottom: -2px; }
.eqc-chips { display: flex; gap: 5px; flex-wrap: wrap; }
/* Пустое место карточки — не курсив на грани видимости, а честная зона
   перетаскивания: она и объясняет, и принимает. */
.eqc-drop { color: var(--ink-2); font-size: var(--fs-micro); padding: 6px 8px;
  border: 1px dashed var(--line); border-radius: 8px; }
.eqc-empty { color: var(--ink-2); font-size: var(--fs-micro); padding: 4px 0; }
/* Подтверждающая связь — внутри карточки, между входом и выходом: обе
   стороны пары человек видит разом. */
.eqc-fb { display: flex; align-items: center; gap: 5px; font-size: var(--fs-micro); color: var(--ink-2);
  border-left: 2px solid var(--line); padding-left: 7px; }
.chan-chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; border-radius: 7px;
  border: 1px solid var(--line); background: var(--panel-2); cursor: grab; font-size: var(--fs-body); user-select: none;
  color: var(--ink); text-align: left; max-width: 190px; }
.chan-chip .cc-n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chan-chip:hover { border-color: var(--brand); }
.chan-chip.sel { border-color: var(--brand); box-shadow: 0 0 0 1px rgba(51,224,163,.4); }
.chan-chip:active { cursor: grabbing; }
.chan-chip .cc-t { font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em; padding: 0 4px; border-radius: 4px; flex: 0 0 auto; }
.chan-chip.di .cc-t { background: rgba(90,168,255,.18); color: #7fb6ff; }
.chan-chip.ai .cc-t { background: rgba(185,140,255,.18); color: #c3a3ff; }
.chan-chip.do .cc-t { background: rgba(51,224,163,.16); color: var(--brand); }
/* Лоток по содержимому, а не во всю ширину: на 1920 полоса под два чипа была
   пуста на 93 %. Приём перетаскивания при этом не сужается — прозрачная
   подложка ловит drop по всей строке. */
.cvs-tray { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel);
  padding: 9px 11px; margin-bottom: 10px; width: max-content; max-width: 100%; min-width: 320px; }
.cvs-tray.drop-hot { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-glow); }
.cvs-tray .tray-h { font: 600 var(--fs-tag) var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); margin-bottom: 6px; }
.cvs-tray .cfg-more { margin-top: 8px; }

/* ── единственный режим «контейнер»: слева палитра «Оборудование», в центре
   холст-контейнеры, справа палитра «Датчики». Инспектор — ПОД холстом.
   Палитры резиновые: 212 px жёстко обрезали и плейсхолдер поиска, и названия
   моделей на любом экране. */
.cvs2 { display: grid; gap: 12px; align-items: start;
  grid-template-columns: clamp(212px, 16vw, 300px) minmax(0, 1fr) clamp(212px, 16vw, 300px); }
@media (max-width: 1100px) { .cvs2 { grid-template-columns: minmax(0, 1fr); } }
.cvs-mid { min-width: 0; }
.cvs-pal { display: grid; gap: 6px; max-height: calc(100vh - 130px); overflow: auto; }
.cvs-pal .card-b { padding: 8px; }
.pal-title { font: 700 var(--fs-body) var(--disp); letter-spacing: .05em; text-transform: uppercase; color: var(--ink); margin: 0 0 4px; }
/* дерево тип → производитель → номенклатура (native <details>, свёрнуто) */
.pal-node > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 7px;
  padding: 5px 7px; border-radius: 7px; user-select: none; }
.pal-node > summary::-webkit-details-marker { display: none; }
.pal-node > summary::before { content: "▸"; color: var(--ink-3); font-size: var(--fs-tag); flex: 0 0 auto; transition: transform .15s; }
.pal-node[open] > summary::before { transform: rotate(90deg); }
.pal-node > summary:hover { background: var(--panel-2); }
.pal-node .pn-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pal-type > summary { color: var(--ink); font: 600 var(--fs-body) var(--sans); }
.pal-type > summary .eqc-ico { width: 17px; height: 17px; }
.pal-brand { margin-left: 13px; border-left: 1px solid var(--line-2); }
.pal-brand > summary { color: var(--ink-2); font: 500 var(--fs-body) var(--sans); }
.pn-n { font: 600 var(--fs-tag) var(--mono); color: var(--ink-2); background: var(--panel-2); border-radius: 6px; padding: 0 6px; flex: 0 0 auto; }
.pal-brand .pal-item { margin: 3px 0 3px 16px; width: calc(100% - 16px); }
.pal-item .pi-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Всплывающее меню холста («что меряет», «каким датчиком заменить»,
   «куда добавить»): раньше каждое рисовало свою копию inline-стилями. */
.cfg-menu { position: fixed; z-index: 60; display: grid; gap: 4px; min-width: 230px; max-width: 320px;
  max-height: 60vh; overflow: auto; padding: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); }

/* Инспектор — панель ПОД холстом на всю ширину, в две-три колонки: высота на
   этом экране дёшева, ширина дефицитна. Панель В ПОТОКЕ (внизу страницы)
   забраковал владелец на живом стенде (24.08): «добавляешь датчик — весь экран
   улетает вниз и дезориентирует». Теперь панель выдвижная справа (fixed):
   страница не прокручивается ни на пиксель, холст остаётся перед глазами, а
   палитру «Датчики» она накрывает только пока открыта — датчик выбирают ДО
   открытия панели, а закрытие возвращает ровно ту же картинку. */
.cfg-drawer-panel { position: fixed; inset-inline-end: 12px; top: 96px; bottom: 12px;
  width: min(460px, 92vw); z-index: 60; display: flex; flex-direction: column;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--panel); overflow: hidden; box-shadow: var(--elev-3); }
.cfg-drawer-panel > .cfg-drawer-card { overflow: auto; flex: 1; min-height: 0; }
@media (max-width: 860px) {
  .cfg-drawer-panel { inset-inline: 0; top: auto; bottom: 0; width: auto;
    max-height: 72vh; border-radius: var(--r-lg) var(--r-lg) 0 0; }
}
.cfg-drawer-bar { display: flex; align-items: center; gap: 8px; padding: 7px 12px;
  border-bottom: 1px solid var(--line-2); background: var(--panel-2); }
.cfg-drawer-card { border: 0 !important; border-radius: 0; background: transparent; }
.ins-grid { display: grid; gap: 0 18px; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(258px, 1fr)); }
/* Поля, которым колонка мала: строка датчика, блок «Резервуар», раскрыватели. */
.ins-grid > .ins-wide, .ins-grid > .cfg-more, .ins-grid > .card-h { grid-column: 1 / -1; }
.ins-sens { border-left: 2px solid var(--line); padding: 2px 0 6px 8px; margin-bottom: 8px; }
.ins-ro { padding: 6px 0; color: var(--ink); font-size: var(--fs-body); }
/* Сырое имя канала на проводе — вторичная моно-подпись у заголовка инспектора,
   а не заголовок поля. */
.cfg-raw { font: 500 var(--fs-tag) var(--mono); color: var(--ink-2); margin-inline-start: 10px; }
/* Замечание уровня записи (про канал или единицу целиком, а не про поле) —
   один раз, сразу под заголовком инспектора. */
.ins-note { padding: 8px 12px 0; display: grid; gap: 4px; }
.ins-note .field-err { margin: 0; display: flex; align-items: flex-start; gap: 6px; }
.ins-note .field-err > svg { flex: 0 0 auto; margin-top: 1px; }

/* ─────────────────── конфигуратор: липкая шапка и полоса готовности ─── */
/* Шапка держит на экране объект, плату, шаги, остаток ёмкости платы, признак
   сохранения и полосу «Что мешает выпуску». Отрицательные поля равны
   горизонтальным полям .main: без них содержимое проезжало бы под шапкой в
   боковых просветах. */
/* ⚠ правка К1-А1 (волна 7): поле берётся из --gutter, а не из литералов
   22/26/34 px. Литералы повторяли прежний padding .main, и при переходе на
   шкалу 4/8 (24/24/40) шапка стала шире контента на 2 px с каждой стороны —
   на «Конфигураторе» появлялась горизонтальная прокрутка всей страницы. */
.cfg-head { position: sticky; top: 0; z-index: 25;
  margin: calc(var(--sp-5) * -1) calc(var(--gutter) * -1) var(--sp-3); padding: var(--sp-3) var(--gutter) 0;
  background: var(--bg); border-bottom: 1px solid var(--line); }
body.nav-collapsed .cfg-head { margin-inline: calc(var(--gutter-wide) * -1); padding-inline: var(--gutter-wide); }
.cfg-head-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.cfg-h1 { font-family: var(--disp); font-weight: 700; font-size: var(--fs-h2); margin: 0; letter-spacing: .01em; }
.cfg-obj { font-size: var(--fs-val); font-weight: 600; color: var(--ink); min-width: 0; }
/* Идентификатор платы — вторичная моно-подпись: он написан на наклейке прибора
   и нужен для сверки, но заголовок объекта важнее. */
.cfg-obj-id { margin-inline-start: 8px; font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); letter-spacing: .04em; }
.cfg-head-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0 0; }
.cfg-save { display: inline-flex; align-items: center; gap: 6px; font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); }
.cfg-save.bad { color: var(--alarm-ink); }
/* Ёмкость платы одной строкой: сколько клемм, правил и номеров занято. */
.cfg-caps { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 7px; margin: 7px 0 0;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); }
.cfg-caps .cap { display: inline-flex; align-items: baseline; gap: 5px; }
.cfg-caps .cap-n { font-variant-numeric: tabular-nums; color: var(--ink); letter-spacing: .04em; }
.cfg-caps .cap.over .cap-n, .cfg-caps .cap.over .cap-l { color: var(--alarm-ink); }
/* Разделитель «·» в полосе счётчиков конфигуратора. Был единственным местом
   во всём продукте, где отрисованный текст не добирал до 4,5:1 (--ink-3 #616c65
   на --panel #0e1315 даёт 3,67:1). Заплатка на --ink-3 снята, а не оправдана
   исключением: во ВСЕХ остальных местах продукта «·» стоит внутри строки и
   красится её же цветом, то есть тише соседей он нигде и не был — выбивалось
   как раз это правило. Для скринридера разделитель остаётся невидимым
   (aria-hidden в config.js): он делит строку глазу, а не слуху. */
.cfg-caps .cap-sep { color: var(--ink-2); }

/* Полоса «Что мешает выпуску» — на всех четырёх шагах, внутри липкой шапки.
   Высота ограничена: длинный список прокручивается внутри себя, а шаги и поля
   остаются на экране. */
.cfg-ready { margin: 9px 0 0; border-top: 1px solid var(--line); }
.cfg-ready .cr-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; padding: 8px 0; }
.cfg-ready .cr-ttl { font: 700 var(--fs-body) var(--disp); }
.cfg-ready .cr-ttl.ok { color: var(--ok-ink); display: inline-flex; align-items: center; gap: 7px; }
.cfg-ready .cr-toggle { background: none; border: 0; padding: 2px; color: var(--ink-2); cursor: pointer; display: inline-flex; }
.cfg-ready .cr-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }
.cfg-ready .cr-toggle svg { transition: transform .16s; }
.cfg-ready .cr-toggle.open svg { transform: rotate(90deg); }
.cfg-ready .cr-only { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-2); cursor: pointer; }
.cfg-ready .chip { display: inline-flex; align-items: center; gap: 5px; }
.cfg-ready .cr-groups { max-height: 40vh; overflow-y: auto; padding: 0 0 10px; display: grid; gap: 4px; }
/* Список длиннее своей высоты — нижняя кромка растворяется: иначе последняя
   видимая строка читается как последняя вообще. */
.cfg-ready .cr-groups.fade { mask-image: linear-gradient(180deg, #000 calc(100% - 22px), transparent); }
.cfg-ready .cr-grp { font: 600 var(--fs-tag) var(--mono); letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-2); margin: 6px 0 2px; }
.cfg-ready .cr-grp.bad { color: var(--alarm-ink); }
.cfg-ready .cr-grp.warn { color: var(--warn-ink); }
.cfg-ready .cr-none { padding: 4px 0 8px; }
/* Строка замечания, а не пилюля: текст «что случилось и что сделать» в пилюлю
   не помещается, а именно он и нужен. */
.cr-item { display: flex; align-items: flex-start; gap: 9px; width: 100%; text-align: start;
  padding: 7px 10px; border-radius: 8px; border: 1px solid var(--line); background: var(--panel-2);
  color: var(--ink); font-size: var(--fs-body); line-height: 1.4; cursor: pointer; }
.cr-item:hover { border-color: var(--brand); }
.cr-item .cr-ic { flex: 0 0 auto; display: inline-flex; padding-top: 1px; }
.cr-item .cr-tx { flex: 1; min-width: 0; }
.cr-item .cr-go { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 3px;
  font: 600 var(--fs-micro) var(--sans); color: var(--ink-2); white-space: nowrap; }
.cr-item:hover .cr-go { color: var(--brand); }
/* Нетекстовая графика — не тоньше 3:1: рамка rgba(...,.4) давала 2:1. */
.cr-item.red { border-color: var(--alarm); background: var(--alarm-soft); }
.cr-item.red .cr-ic { color: var(--alarm-ink); }
.cr-item.warn { border-color: var(--warn); background: var(--warn-soft); }
.cr-item.warn .cr-ic { color: var(--warn-ink); }
/* Замечание ТЕКУЩЕГО шага — точкой слева: человек видит, что чинится здесь. */
.cr-item.here { box-shadow: inset 3px 0 0 var(--brand); }

/* конфигуратор: панель шагов (1 Объект → … → 4 Выпуск) */
.cfg-steps button { display: inline-flex; align-items: center; gap: 6px; }
.cfg-steps .st-n { font: 700 var(--fs-tag) var(--mono); color: var(--ink-2); }
.cfg-steps button.on .st-n { color: var(--brand); }
.cfg-steps .st-m { display: inline-flex; align-items: center; gap: 3px;
  font: 700 var(--fs-micro) var(--mono); line-height: 1; min-width: 11px; }
.cfg-steps .st-m.st-ok { color: var(--ok-ink); }
.cfg-steps .st-m.st-warn { color: var(--warn-ink); }
.cfg-steps .st-m.st-bad { color: var(--alarm-ink); text-shadow: 0 0 8px var(--alarm-glow); }
.cfg-steps .st-m.st-empty { color: var(--ink-3); }

/* Поле, к которому привели из полосы: кольцо гаснет за 2 с — этого хватает,
   чтобы глаз нашёл поле, и не остаётся мусора на экране. */
.field-blame { animation: blame 2s ease-out; border-radius: var(--r); }
@keyframes blame {
  0%   { box-shadow: 0 0 0 3px var(--brand-soft), 0 0 0 1px var(--brand); }
  70%  { box-shadow: 0 0 0 3px var(--brand-soft), 0 0 0 1px var(--brand); }
  100% { box-shadow: none; }
}

/* Меню шапки: редкие операции, каждая со своим пояснением. Общий каркас
   выпадающего меню объявлен один раз выше (.cfg-menu); здесь только ширина —
   у пунктов с пояснением она фиксированная, иначе строки прыгают по ширине
   самого длинного пояснения. Меню типов правил и меню клетки матрицы делят
   тот же каркас и ширину подбирают сами. */
#cfg-head-menu { width: 300px; max-width: 92vw; gap: 2px; }
.cfg-menu .btn { justify-content: flex-start; text-align: start; display: block; }

/* Стартовая страница выбора платы: группа «населённый пункт · предприятие»,
   строки с идентификатором и состоянием черновика. */
.cfg-pick-h { display: flex; align-items: baseline; gap: 8px; margin: 16px 0 6px;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-2); }
/* «7 из 16» — счёт, то есть данные. */
.cfg-pick-h .cfg-pick-n { margin-inline-start: auto; color: var(--ink-2); }
.cfg-pick-box { border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; background: var(--panel); }
.cfg-pick-row { display: flex; align-items: center; gap: 12px; padding: 9px 14px; text-decoration: none;
  color: inherit; border-top: 1px solid var(--line-2); }
.cfg-pick-box .cfg-pick-row:first-child { border-top: 0; }
.cfg-pick-row:hover { background: var(--panel-2); text-decoration: none; }
.cfg-pick-row .cfg-pick-obj { flex: 1; min-width: 0; font-size: var(--fs-val); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cfg-pick-row .cfg-pick-id { flex: 0 0 auto; width: 108px; font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); }
.cfg-pick-row .cfg-pick-st { flex: 0 0 auto; width: 186px; display: flex; }
.cfg-pick-row .cfg-pick-st .chip { display: inline-flex; align-items: center; gap: 5px; }
.cfg-pick-row .cfg-pick-ts { flex: 0 0 auto; width: 40px; font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); }
.cfg-pick-row .cfg-pick-go { flex: 0 0 auto; color: var(--ink-3); }
.cfg-pick-row:hover .cfg-pick-go { color: var(--brand); }
.cfg-pick-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* Тосты не должны накрывать липкую шапку с шагами: страница конфигуратора
   опускает их ниже полосы готовности (значение ставит JS по её высоте). */
body:has(#cfg-root) .toasts { top: var(--cfg-toast-top, 18px); }

/* Узкий экран: конфигуратор остаётся читаемым (редактировать на телефоне
   неудобно осознанно, но ПОСМОТРЕТЬ настройки и телефон дежурного наладчик
   должен уметь). Сегмент шагов переносится по строкам — иначе четвёртый шаг
   «Выпуск» уезжает за край и человек не догадывается, что он есть. */
@media (max-width: 860px) {
  .cfg-head .cfg-steps { flex-wrap: wrap; }
  .cfg-head .cfg-steps button { flex: 1 0 auto; }
  .cfg-head .cr-groups { max-height: 45vh; }
}

@media (prefers-reduced-motion: reduce) {
  .field-blame { animation: none; box-shadow: 0 0 0 1px var(--brand); }
  .cfg-ready .cr-toggle svg { transition: none; }
}

/* Правила инспектора-панели живут выше, в блоке холста (WF-C): панель
   переехала из плавающей справа под холст и делит место с ним, а не с
   палитрой «Датчики». */

/* сцена «Правила» (список+панель) и сетка «Настройки»: на узком экране —
   в один столбец, без горизонтального скролла (образец — .cvs2 выше) */
/* Шаг «Правила» — ОДНА колонка: панель параметров уехала под список и матрицу
   (ширина на этом шаге дефицитна — матрице нужны все клеммы разом, а высота
   не дефицитна). Прежние 320 px боковой панели съедали треть ширины и прятали
   за краем матрицы правило уровня «авария». */
.cfg-rules-stage { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }
/* Колонки шага «Объект» неравные: слева «Кто и где» — восемь текстовых полей с
   пояснениями, справа номера и связь; при 1fr/1fr справа оставалось ~455 px
   пустоты, а слева подписи ломались надвое. */
.cfg-settings { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); gap: 12px; align-items: start; }
/* Колонка обязана уметь быть уже своего содержимого: у элемента сетки
   минимальный размер по умолчанию — min-content, и карточка номеров (два поля
   ввода в строке) распирала колонку изнутри, вынося горизонтальную прокрутку
   на всю страницу. */
.cfg-settings > *, .cfg-settings > * > * { min-width: 0; }
/* Поле ввода шире ~660 px не читается: глаз не связывает подпись слева с
   концом строки, а «Саратов» в поле на 900 px выглядит ошибкой вёрстки.
   Карточка при этом остаётся во всю колонку — рвать сетку не нужно. */
.cfg-fields > * { max-width: 660px; }
/* На широком мониторе форма не растягивается на всю ширину: строка настройки
   с пояснением длиннее ~90 знаков читается уже хуже, чем короче. */
.cfg-settings { max-width: 1440px; }
/* minmax(0,1fr), а НЕ 1fr: у 1fr минимальный размер трека — auto, и широкая
   матрица правил распирает колонку изнутри, вынося горизонтальную прокрутку на
   ВСЮ страницу вместо своей обёртки .rmx-wrap. */
@media (max-width: 1100px) { .cfg-rules-stage, .cfg-settings { grid-template-columns: minmax(0, 1fr); } }

/* ─────────────────── конфигуратор: шаг «Объект» (кто и где, номера) ─── */

/* Строка «только чтение» (координаты, идентификатор платы): это ДАННЫЕ, а не
   поле ввода, и выглядеть полем они не должны — иначе в них тыкают. */
.cfg-ro { display: flex; align-items: baseline; gap: 8px; padding: 6px 0 2px; flex-wrap: wrap; }
.cfg-ro .cro-k { font: 600 var(--fs-tag) var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); }
.cfg-ro .cro-v { font-family: var(--mono); font-size: var(--fs-body); color: var(--ink); }
/* Часовой пояс, разошедшийся с карточкой объекта, — янтарём: расписание
   отработает не в то время, а красным это не назвать (значение допустимо). */
.cfg-tz-diff { color: var(--warn-ink); font-family: var(--mono); }

/* Номер оповещения — двухуровневая строка: сверху телефон и подпись, снизу
   права. В одну строку не помещается: три галки с подписями занимают больше,
   чем сам номер, и на 1280 px крестик уезжал на второй ряд без объяснения. */
.phones { display: grid; gap: 8px; }
.phone-row { border: 1px solid var(--line-2); border-radius: var(--r); padding: 8px 10px; background: var(--panel-2); }
.phone-row .ph-top { display: flex; align-items: center; gap: 8px; }
.phone-row .ph-no { font: 600 var(--fs-body) var(--mono); color: var(--ink-2); flex: 0 0 auto; min-width: 24px; }
/* width: 0 при flex-basis: 0 — не декорация: у поля ввода собственная
   внутренняя ширина (~177 px по атрибуту size), и она становится минимальной
   шириной всей карточки, а та — минимальной шириной колонки сетки. Два поля в
   строке распирали правую колонку и выносили горизонтальную прокрутку на всю
   страницу. Растут поля флексом, а не своей шириной. */
.phone-row .ph-tel { flex: 1 1 0; width: 0; min-width: 0; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.phone-row .ph-lbl { flex: 1.2 1 0; width: 0; min-width: 0; }
.phone-row .ph-del { flex: 0 0 auto; padding: 5px 7px; }
.phone-row .ph-del:hover { border-color: var(--alarm); color: var(--alarm-ink); background: var(--alarm-soft); }
.phone-row .ph-flags { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 6px; padding-inline-start: 32px; }
.phone-row .ph-flags .row-g { gap: 6px; }
.phone-row .field-err { padding-inline-start: 32px; }
.ph-add { display: flex; align-items: center; gap: 10px; margin-top: 8px; flex-wrap: wrap; }

/* ───────────── конфигуратор: шаг «Проверка и выпуск» (паспорт) ─── */

/* Состав платы — плитки «что/сколько». Моноширинные числа, чтобы столбик
   читался как показания прибора, а не как строка текста. */
.cfg-kpi { display: flex; flex-wrap: wrap; gap: 6px 8px; margin-bottom: 10px; }
.cfg-kpi .kpi { display: inline-flex; align-items: baseline; gap: 7px; padding: 4px 10px;
  border: 1px solid var(--line-2); border-radius: var(--r); background: var(--panel-2); }
.cfg-kpi .kpi-k { font: 600 var(--fs-tag) var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); }
.cfg-kpi .kpi-v { font: 600 var(--fs-body) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); }
/* Паспорт — текст, а не таблица: строка длиннее ~110 знаков читается хуже,
   поэтому карточки шага ограничены по ширине, как и форма шага «Объект». */
.cfg-passport { max-width: 1440px; }
.psp-line { font-size: var(--fs-body); line-height: 1.5; margin-top: 4px; max-width: 1100px; }
.psp-mem { margin-top: 8px; font: var(--fs-body) var(--mono); color: var(--ink-2); }
.psp-diff { margin: 0; padding-inline-start: 18px; display: grid; gap: 5px; font-size: var(--fs-body); line-height: 1.45; }
.psp-empty { display: flex; align-items: center; gap: 12px; color: var(--ink-2); flex-wrap: wrap; }

/* Кнопка выпуска — единственная «смелость» шага: крупная, с объяснением
   рядом, и стоит ПОСЛЕ всего, что нужно прочитать. */
.cfg-release-b { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.cfg-release .rel-state { display: flex; align-items: center; gap: 8px; flex: 1 1 240px; font-family: var(--disp); }
.cfg-release .rel-state .small { font-family: var(--sans); color: var(--ink-2); }
.cfg-release .rel-go { display: flex; align-items: center; gap: 12px; flex: 1 1 380px; flex-wrap: wrap; }
.cfg-release .rel-go .small { flex: 1 1 220px; }
.btn.lg { padding: 11px 22px; font-size: var(--fs-val); }

/* Отчёт о правках, сделанных на объекте по СМС (модалка после выпуска). */
.ovl-grp { margin-bottom: 12px; }
.ovl-h { font: 600 var(--fs-micro) var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); margin-bottom: 5px; }
.ovl-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; font-size: var(--fs-body); line-height: 1.45; }
.ovl-list li { display: flex; gap: 8px; align-items: flex-start; }
.ovl-list li > svg { flex: 0 0 auto; margin-top: 2px; }
.ovl-check li > svg { color: var(--ok-ink); }
.ovl-warn li > svg { color: var(--warn-ink); }

/* Карточка шаблона: выбор радио-кнопкой, состав в подписи. */
.tpl-list { display: grid; gap: 8px; max-height: 50vh; overflow: auto; }
.tpl-card { display: flex; align-items: flex-start; gap: 10px; padding: 9px 12px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r); background: var(--panel-2); }
.tpl-card:hover { border-color: var(--brand); }
.tpl-card:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.tpl-card .tc-b { flex: 1; min-width: 0; }
.tpl-card .tc-n { font-weight: 600; }
.tpl-card .tc-del { flex: 0 0 auto; padding: 5px 7px; }
.tpl-card .tc-del:hover { border-color: var(--alarm); color: var(--alarm-ink); background: var(--alarm-soft); }

/* ───────────────────────── конфигуратор: матрица правил ─── */
.rmx-top { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }
/* Прокрутка обозначена тенями у кромок и ТОЛЬКО когда она есть (классы sc-l/sc-r
   ставит matrixTable): постоянный градиент читался бы как обрезанная таблица. */
.rmx-scroll { position: relative; }
.rmx-wrap { overflow-x: auto; }
.rmx-scroll::before, .rmx-scroll::after { content: ""; position: absolute; top: 0; bottom: 0; width: 26px;
  pointer-events: none; opacity: 0; transition: opacity .16s; z-index: 3; }
.rmx-scroll::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
.rmx-scroll::after { right: 0; background: linear-gradient(270deg, var(--bg), transparent); }
.rmx-scroll.sc-l::before, .rmx-scroll.sc-r::after { opacity: 1; }
.rmx-eqh { font: 700 var(--fs-body) var(--disp); margin: 14px 0 6px; display: flex; align-items: center; gap: 8px; }
table.rmx { border-collapse: collapse; font-size: var(--fs-body); }
table.rmx th, table.rmx td { border: 1px solid var(--line-2); padding: 0; }
table.rmx .rmx-corner { background: var(--panel-2); min-width: 132px; padding: 6px 9px; text-align: left;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-2); }
table.rmx thead th { background: var(--panel-2); padding: 6px 9px; font: 600 var(--fs-micro) var(--sans); color: var(--ink);
  vertical-align: bottom; white-space: nowrap; text-align: left; }
/* Две строки в шапке: клемма («Вх4») и человеческое имя канала («Проток»).
   Прежде у заведённой клеммы стояло сырое имя, у свободной — «DI-5», и связь
   «имя ↔ клемма» глазами не восстанавливалась. */
table.rmx th .rmx-t { font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em; display: block; color: var(--ink-2); }
table.rmx th .rmx-nm { display: block; max-width: 104px; overflow: hidden; text-overflow: ellipsis; }
/* z-index и НЕПРОЗРАЧНЫЙ фон обязательны: без них уезжающие под липкую колонку
   клетки просвечивают сквозь имя выхода. */
table.rmx tbody th { background: var(--panel-2); padding: 6px 9px; text-align: left; font: 600 var(--fs-body) var(--sans);
  color: var(--ink); white-space: nowrap; position: sticky; left: 0; z-index: 2; }
table.rmx tbody th.rmx-dead { color: var(--warn-ink); }
table.rmx tbody th.rmx-dead .rmx-t { color: var(--warn-ink); }
table.rmx th.rmx-svc { color: var(--ink-2); }
.rmx-td { padding: 0; }
.rmx-cell { display: flex; align-items: center; justify-content: center; width: 100%; min-width: 74px; height: 42px;
  border: 0; background: none; color: inherit; font: inherit; cursor: pointer; transition: background .12s; }
.rmx-cell:hover { background: var(--panel-2); }
.rmx-cell.has { background: rgba(51,224,163,.06); }
.rmx-cell.has:hover { background: rgba(51,224,163,.12); }
.rmx-rc { display: inline-flex; flex-direction: column; gap: 2px; padding: 3px; }
.rmx-rc .rc-k { font: 600 var(--fs-tag) var(--mono); color: var(--brand); }
.rmx-rc .rc-lv { width: 7px; height: 7px; border-radius: 50%; margin: 0 auto; }
.rmx-rc .rc-lv.alarm { background: var(--alarm); }
.rmx-rc .rc-lv.warn { background: var(--warn); }
/* Плюс на свободной клетке виден в покое: при opacity 0 аффорданса не было ни
   на планшете, ни с клавиатуры — «связать вход с выходом» приходилось угадывать. */
.rmx-add { color: var(--ink-2); opacity: .18; transition: opacity .12s; display: flex; }
.rmx-cell:hover .rmx-add, .rmx-cell:focus-visible .rmx-add { opacity: 1; }
/* Клемма без заведённого канала: заголовок приглушён, клетка заштрихована и не
   нажимается. Штриховка, а не только цвет текста: в светлой теме оттенком
   такие клетки от заведённых пустых не отличаются. */
table.rmx th.rmx-empty { color: var(--ink-2); font-weight: 500; background: var(--panel); }
table.rmx th.rmx-empty .rmx-t { color: var(--ink-2); }
.rmx-cell.rmx-off { cursor: default;
  background: repeating-linear-gradient(-45deg, transparent, transparent 5px, var(--panel-2) 5px, var(--panel-2) 6px); }
.rmx-cell.rmx-off:hover { background: repeating-linear-gradient(-45deg, transparent, transparent 5px, var(--panel-2) 5px, var(--panel-2) 6px); }
.rmx-legend { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-top: 8px; line-height: 1.5; }
.rmx-legend .rmx-dot { color: var(--ink-3); }
/* Секции под матрицей ограничены по ширине: на 1920 таблица растягивалась на
   1560 px, и чип уровня оказывался в 700 px от текста правила. */
.rmx-other { margin-top: 16px; max-width: 1100px; }

/* === К6: конфигуратор — список правил и панель параметров === */
/* ───────────────────── конфигуратор: список правил и панель параметров ─── */
.rul-bar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.rul-bar .rul-prio { flex: 1 1 320px; min-width: 0; line-height: 1.45; }
.rul-wrap { overflow-x: auto; }
.rul-table td, .rul-table th { vertical-align: top; }
.rul-table .rul-n { width: 34px; font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-2); }
/* Номер вложенной строки «второй уровень» — тише родительского, но на месте:
   он занимает позицию в списке, и пропуск читается как «правило исчезло». */
.rul-table .rul-n.sub { opacity: .62; }
.rul-table .rul-kind { white-space: nowrap; color: var(--ink-2); }
.rul-table .rul-where { color: var(--ink-2); max-width: 200px; }
.rul-table .rul-act { text-align: end; white-space: nowrap; width: 1%; }
/* Тени прокрутки у списка правил живут поверх КАРТОЧКИ, а не поверх страницы:
   градиент к цвету фона страницы читался бы на панели как светлая полоса. */
.rmx-scroll:has(> .rul-wrap)::before { background: linear-gradient(90deg, var(--panel), transparent); }
.rmx-scroll:has(> .rul-wrap)::after { background: linear-gradient(270deg, var(--panel), transparent); }
/* Узкий экран: конфигуратор здесь только ЧИТАЮТ (§7.4), и читают прежде всего
   «что делает правило» и «каким уровнем оповестит». Колонки «Тип» и «Где»
   уводили таблицу за правый край вместе с чипом уровня — на телефоне уровень
   пропадал целиком, а он и есть то, ради чего в список заглядывают. Тип виден
   по самому предложению, каналы в нём же и названы. */
@media (max-width: 860px) {
  .rul-table .rul-kind, .rul-table .rul-where { display: none; }
  .rul-table td, .rul-table th { padding-inline: 6px; }
}
.rul-row { cursor: pointer; }
/* Вложенная строка — второй уровень порога. Отступ и штрих слева говорят
   «это часть строки выше»: своих стрелок и удаления у неё нет. */
.rul-table .rul-sub > td:nth-child(2) { padding-left: 18px; box-shadow: inset 2px 0 0 var(--line-2); }
.rul-table .rul-sub { background: color-mix(in srgb, var(--panel-2) 55%, transparent); }
.rs .rs-k { font-weight: 600; color: var(--brand); }
.rs svg { vertical-align: -2px; margin-inline-end: 2px; color: var(--ink-2); }
.rul-empty { display: grid; gap: 10px; justify-items: center; text-align: center; }
.rul-panel { border-color: var(--brand); }
/* Ниже 1700 px панель правила — выдвижная справа, как «Настройки элемента»
   шага «Оборудование» и редактор оперативных правок: паттерн назвал владелец
   («вместо улетающего экрана — всплывающий блок справа»). Страница при выборе
   строки не прокручивается; на ≥1700 px остаётся липкая правая колонка. */
@media (max-width: 1699.98px) {
  .cfg-rules-stage > .rul-panel { position: fixed; inset-inline-end: 12px; top: 96px; bottom: 12px;
    width: min(560px, 92vw); z-index: 60; display: flex; flex-direction: column;
    background: var(--panel); box-shadow: var(--elev-3); overflow: hidden; }
  .cfg-rules-stage > .rul-panel > .card-b, .cfg-rules-stage > .rul-panel > .rul-body { overflow: auto; flex: 1; min-height: 0; }
  /* Три колонки в узкой панели складываются в одну — читается сверху вниз. */
  .cfg-rules-stage > .rul-panel .rul-cols { grid-template-columns: 1fr; }
}
@media (max-width: 860px) {
  .cfg-rules-stage > .rul-panel { inset-inline: 0; top: auto; bottom: 0; width: auto;
    max-height: 72vh; border-radius: var(--r-lg) var(--r-lg) 0 0; }
}
.rul-panel .rul-n { font-family: var(--mono); color: var(--ink-2); margin-inline-end: 8px; }
.rul-echo { margin: 0; padding: 9px 14px; border-inline-start: 3px solid var(--brand);
  background: var(--brand-soft); font-size: var(--fs-body); line-height: 1.5; }
/* Три колонки: за чем следим · какие границы · что сделать. Ширины дефицита
   нет (панель на всю ширину), поэтому колонки не жмутся, а переносятся. */
.rul-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px 22px; }
.rul-col { min-width: 0; }
.rul-col-h { font: 600 var(--fs-tag) var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-2); margin-bottom: 8px; padding-bottom: 5px; border-bottom: 1px solid var(--line); }
.rul-ro { padding: 7px 0; color: var(--ink-2); }
.rul-thr { margin-bottom: 10px; }
.rul-thr input[type=range] { margin-top: 5px; }
/* Время в 24-часовом виде рядом с полем: браузер с 12-часовой локалью рисует
   «08:00 PM» там, где предложение правила говорит «до 20:00». */
.hhmm-w { display: flex; align-items: center; gap: 8px; }
.hhmm-w > .in { flex: 1; min-width: 0; }
.hhmm-24 { font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); white-space: nowrap; }
.rul-acts { margin-top: 4px; }
.rul-act-line { border: 1px solid var(--line); border-radius: var(--r); padding: 8px; margin-bottom: 6px;
  background: var(--panel-2); }

/* Меню типов правил: якорится к кнопке, прокручивается внутри себя, у нижней
   кромки — градиент (без него третья группа уходила под обрез без единого
   признака прокрутки, и человек видел 10 типов из 14). */
.rul-menu { position: fixed; z-index: 60; background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: var(--shadow); max-width: 92vw; overflow: hidden; display: flex; flex-direction: column; }
.rul-menu::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 26px; pointer-events: none;
  background: linear-gradient(180deg, transparent, var(--panel)); }
.rul-menu-body { overflow: auto; padding: 6px; display: grid; gap: 2px; }
.rul-menu-h { font: 600 var(--fs-tag) var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-2); padding: 9px 8px 4px; border-top: 1px solid var(--line); margin-top: 2px; }
.rul-menu-body > .rul-menu-h:first-child { border-top: 0; margin-top: 0; padding-top: 4px; }
.rul-menu-i { display: block; width: 100%; text-align: start; background: none; border: 0; border-radius: 8px;
  padding: 7px 9px; color: var(--ink); cursor: pointer; font: inherit; }
.rul-menu-i:hover { background: var(--panel-2); }
.rul-menu-i .rmi-t { font-weight: 600; font-size: var(--fs-body); }
.rul-menu-i .rmi-h { font-size: var(--fs-micro); color: var(--ink-2); white-space: normal; line-height: 1.4; margin-top: 2px; }
.rul-menu-empty { padding: 12px; display: grid; gap: 10px; font-size: var(--fs-body); color: var(--ink-2); max-width: 320px; }

/* ── мнемосхема: выключенный правкой канал, кликабельность пилюли (поток W3) ── */

/* Пилюля ВЫХОДА — настоящий переключатель, и выглядеть она обязана как кнопка:
   поднятая заливка и рамка, а на ховере меняется И заливка, И рамка. Пилюля
   датчика только открывает карточку — у неё поверхность холста, и разница видна
   БЕЗ наведения (аффорданс, за которым надо гоняться мышью, — не аффорданс).
   Уровни «внимание»/«авария» и выключенный правкой канал исключены: там заливка
   сама несёт смысл. */
.schm .lblp.do:not(.warn):not(.alarm):not(.ovr-off) .lblp-bg { fill: var(--panel-2); stroke: var(--line); }
.schm .lblp.do:not(.warn):not(.alarm):not(.ovr-off):hover .lblp-bg { fill: var(--panel-3); stroke: var(--brand); }

/* Канал, выключенный оперативной правкой. Плата его не опрашивает (вход) или
   держит принудительно ВЫКЛ (выход), поэтому пилюля гасится: она больше не
   несёт живых данных и не является кнопкой. Никакого line-through — моноширинные
   цифры от него разваливаются; демоция делается цветом и перечёркнутым кругом.
   Значение остаётся на --ink-2, а не на --ink-3: --ink-3 ниже AA, а показание —
   всё ещё данные, которые человек читает. Рамка становится ПУНКТИРНОЙ:
   «выключено» читается формой, а не только приглушённостью — работает и на
   бумаге, и при дальтонизме, и не требует сравнения с соседней пилюлей. */
.schm .lblp.ovr-off .lblp-bg { fill: var(--panel); stroke: var(--line); stroke-dasharray: 3 2.6; }
.schm .lblp.ovr-off:hover .lblp-bg { stroke: var(--ink-3); }
.schm .lblp.ovr-off .lbl-k { fill: var(--ink-3); }
.schm .lblp.ovr-off .lbl-v,
.schm .lblp.ovr-off .lbl-v.warn,
.schm .lblp.ovr-off .lbl-v.alarm,
.schm .lblp.do.on.ovr-off .lbl-v { fill: var(--ink-2); font-weight: 500; filter: none; }
.schm .lblp.ovr-off .lblp-dot,
.schm .lblp.do.on.ovr-off .lblp-dot { fill: var(--ink-3); filter: none; }
.schm .lblp.ovr-off .lblp-spin line,
.schm .lblp.do.on.ovr-off .lblp-spin line { stroke: var(--ink-3); filter: none; }

/* Перечёркнутый круг (ICONS.banned) и ромб правки. Штрих 1,6 и currentColor —
   как во всём наборе; ромб — тот же --manual, что у «ручного» режима: и там, и
   там в автоматику вмешался человек, а форма разводит два разных смысла. */
.schm .ovr-ban { fill: none; stroke: var(--warn); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.schm .ovr-edit { fill: var(--manual); stroke: none; }

/* «Сигналы объекта» — те же две пометки в списке непривязанных каналов */
.schm .misc-row.ovr-off:not(.warn):not(.alarm) .misc-k { fill: var(--ink-3); }
.schm .misc-row.ovr-off:not(.warn):not(.alarm) .misc-v,
.schm .misc-row.on.ovr-off:not(.warn):not(.alarm) .misc-v { fill: var(--ink-2); filter: none; }
.schm .misc-row.ovr-off .misc-led { display: none; }

/* Легенда: пункты появляются только когда правки на плате есть */
.schm-legend .lg-ban { fill: none; stroke: var(--warn); stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }
.schm-legend .lg-dia { width: 7px; height: 7px; background: var(--manual); transform: rotate(45deg); }

/* Светлая тема: охра #f0a800 хороша ЗАЛИВКОЙ, но тонкой линией на бумаге она
   1,7:1 — перечёркнутый круг рисуется границей, поэтому берём --warn-line
   (3,0:1, порог WCAG для нетекстовой графики). */
html[data-theme="light"] .schm .ovr-ban,
html[data-theme="light"] .schm-legend .lg-ban { stroke: var(--warn-line); }

/* §7.6: частная заплатка «рамка ghost-кнопке АВТО» снята — рамку в обеих темах
   теперь несёт сам класс .btn.ghost (F-012). */

/* === К2: пульт парка — плитки объектов, лента, маркеры карты === */
/* ═══════════════ пульт парка: KPI-приборка, плитки, лента (§15.3) ═════════ */

/* Плашки «объектов / на связи / команд в пути» переехали в сводную строку
   .c-sum (см. регион KPI выше): синий --maint из приборного ряда убран — он
   остался у чипа «команда в пути» на плитке объекта, где у него есть адрес. */
@media (max-width: 480px) {
  .counters { gap: 8px; }
  /* Честный перенос 2×2: четыре равные доли, ни одной «сироты» во второй
     строке. Аварийная доля здесь не шире — на 390 px лишние 30 % ширины
     отнимали бы их у соседней плитки, а не у пустого места. */
  .c-status { gap: 8px; width: 100%; grid-template-columns: 1fr 1fr; }
  .c-status .counter, .c-status .counter.alarm { min-width: 0; padding: 8px 12px 8px 11px; }
  .counter b, .c-status .counter.alarm b { font-size: var(--fs-h1); }
}

/* раскладка тела парка: список ⇄ карта + лента событий.
   «Список» — одна колонка: весь парк (плитки, вписанные зумом в экран),
   лента событий — ВНИЗУ во всю ширину.
   В режиме карты высота тела берётся из --park-h — меры, снятой с ЖИВОГО
   лэйаута (park.js::syncBodyHeight). Раньше здесь стояла константа
   «100vh − 300px», которая не совпадала с фактическим верхом тела и не знала
   про перенос приборного ряда на две строки: лента оказывалась видна на 49 px
   из 300, а в одном столбце жили три разные прокрутки. */
.park-body { display: grid; gap: 14px; align-items: start; }
.park-body.list { grid-template-columns: 1fr; }
.park-body.map  {
  grid-template-columns: minmax(280px, 372px) minmax(0, 1fr);
  grid-template-rows: minmax(260px, 1fr) auto;
  align-items: stretch;
  /* auto — честный запас: пока мера не снята (первый кадр, телефон), тело
     ведёт себя как обычный поток, а не схлопывается в ноль. */
  height: var(--park-h, auto);
}
.park-body.map  .park-feed { grid-column: 1 / -1; }
.park-body.map  .park-demo-note { grid-column: 1 / -1; }
/* Колонка списка: сам список прокручивается, подпись «ещё N ниже» стоит под ним
   и не уезжает вместе с содержимым. */
#park-col { display: flex; flex-direction: column; min-height: 0; }
.park-body.list #park-col { display: block; }
/* grid-auto-rows: max-content — обязательный: у грида с ОПРЕДЕЛЁННОЙ высотой
   (а он её получает как flex-элемент колонки) автоматические строки делят её
   между собой, и плитки срезались по живому вместо того, чтобы прокручиваться. */
.park-body.map #park-list { overflow: auto; flex: 1; min-height: 0; align-content: start; grid-auto-rows: max-content; }
/* Мягкий срез внизу списка — признак того, что колонка продолжается. Только
   когда снизу и правда что-то есть (класс ставит park.js::syncMore). */
#park-col.faded #park-list {
  mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
}
.park-more {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-2); padding: 6px 2px 0; text-align: center;
}
#park-map { height: 100%; min-height: 260px; border-radius: var(--r); overflow: hidden; border: 1px solid var(--line); }
.park-body.list #park-map,
.park-body.list .pm-close,
.park-body.list .park-demo-note { display: none; }
@media (max-width: 1000px) {
  .park-body.map { grid-template-columns: 1fr; height: auto; }
  .park-body.map #park-list { overflow: visible; }
  #park-map { min-height: 380px; }
  #park-col.faded #park-list { mask-image: none; }
}

/* сетка плиток объектов */
#park-list.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(236px, 1fr)); gap: 10px; align-content: start; }
.park-body.map #park-list.tile-grid { grid-template-columns: 1fr; }
@media (max-width: 560px) { #park-list.tile-grid { grid-template-columns: 1fr; } }

/* ── плитка объекта: свечение по худшему уровню, пульс рамки при аварии ── */
.obj-tile {
  /* Колонка-флекс, а не просто блок: плитки в ряду сетки растягиваются до общей
     высоты, и нижняя строка (бейджи + метка типа прибора) в блочном потоке
     висела бы сразу под контентом — у «пустой» плитки на середине её высоты.
     margin-top:auto у .ot-foot прижимает её к НИЗУ плитки, чего и ждёт глаз от
     подписи в углу. Дети — обычные блоки, порядок и отступы не меняются. */
  display: flex; flex-direction: column;
  position: relative; cursor: pointer; overflow: hidden;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 10px 13px 11px; box-shadow: var(--shadow);
  transition: border-color .15s, transform .12s, box-shadow .2s;
  /* Плитка — настоящая ссылка <a> (клавиатура, Ctrl+клик, средняя кнопка),
     поэтому снимаем с неё подчёркивание и наследуем цвет текста. */
  color: inherit; text-decoration: none;
}
/* Логические свойства, а не left/right: в арабской раскладке полоса статуса и
   точка статуса менялись ролями — у начала карточки (справа) оказывалась
   точка, у конца — полоса. */
.obj-tile::before { content: ""; position: absolute; inset-inline-start: 0; top: 0; bottom: 0; width: 5px; background: var(--ok); }
.obj-tile:hover { transform: translateY(-1px); border-color: rgba(150,200,175,.3); }
.obj-tile .ot-name { font: 600 var(--fs-val) var(--sans); overflow-wrap: anywhere; line-height: 1.35; padding-right: 14px; }
.obj-tile .ot-ent { font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.obj-tile .ot-dot { position: absolute; top: 11px; inset-inline-end: 11px; }
.obj-tile .ot-meta { display: flex; align-items: center; gap: 8px; margin-top: 7px; font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); flex-wrap: wrap; }
.obj-tile .ot-meta .v { color: var(--ink-2); }
.obj-tile .ot-kv { display: flex; gap: 6px 14px; flex-wrap: wrap; margin-top: 7px; border-top: 1px solid var(--line-2); padding-top: 7px; }
.obj-tile .ot-kv:empty { display: none; }
.obj-tile .ot-kv .kvi { display: flex; flex-direction: column; min-width: 0; }
/* Подпись величины («П ВХОД», «Т ПОДАЧИ») — это ключ к числу: без неё «0,12 бар»
   бессмысленно, то есть подпись НЕСЁТ ДАННЫЕ. До волны она была набрана 9 px
   цветом --ink-3 (3,67:1): число видно с двух метров, а его смысл — нет.
   Кегль — нижняя ступень шкалы, цвет — --ink-2 (7,6:1). */
.obj-tile .ot-kv .kvi i { font: 600 var(--fs-tag) var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); font-style: normal; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 120px; }
.obj-tile .ot-kv .kvi b { font: 600 var(--fs-body) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); }
.obj-tile .ot-kv .kvi.warn b  { color: var(--warn-ink); }
.obj-tile .ot-kv .kvi.alarm b { color: var(--alarm-ink); text-shadow: 0 0 10px var(--alarm-glow); }
/* Причина состояния — единственная строка, ради которой плитку читают в аварии.
   Репер перед текстом — значок из общего набора (круг у аварии, треугольник у
   «внимания»): форма различает их там, где цвет не работает — печать,
   дальтонизм, монохромный щитовой монитор. */
.obj-tile .ot-alarms, .obj-tile .ot-warns {
  display: inline-flex; align-items: center; gap: 3px;
  font: 600 var(--fs-micro) var(--mono);
}
.obj-tile .ot-alarms { color: var(--alarm-ink); }
.obj-tile .ot-warns  { color: var(--warn-ink); }
.obj-tile .r-ic { display: inline-flex; }
/* Отметка «взято в работу» — вторичная и НЕЙТРАЛЬНАЯ: она не отменяет аварийный
   цвет плитки и не уменьшает счётчик. Её смысл ровно один — сменщик видит, что
   по этой аварии уже кто-то поехал, и не дублирует вызов бригады. */
.obj-tile .ot-ack, .obj-tile .ot-cmd {
  display: inline-flex; align-items: center; gap: 4px;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .04em;
  border-radius: 999px; padding: 1px 7px;
}
.obj-tile .ot-ack { color: var(--ink-2); background: var(--panel-3); border: 1px solid var(--line); }
/* «Команда в пути» — голубой обслуживания и БЕЗ свечения: это не тревога, а
   «система выполняет вашу волю». */
.obj-tile .ot-cmd { color: var(--maint); background: var(--maint-soft); border: 1px solid var(--line); }
/* «SMS-резерв» в плитке парка: предупреждающий жёлтый, но без пульсации —
   это не авария, а деградация канала (интернета нет, телеметрия едет платными
   SMS). Слабее аварии, заметно сильнее серой meta-строки вокруг. */
.obj-tile .ot-sms { font: 600 var(--fs-micro) var(--mono); color: var(--warn-ink);
  background: var(--warn-soft); border: 1px solid var(--warn-line);
  border-radius: 999px; padding: 1px 7px; letter-spacing: .02em; }
/* Нижняя строка плитки: бейджи слева, ручная метка типа прибора — справа.
   min-width:0 на группе бейджей, чтобы длинный набор бейджей переносился
   внутри себя, а не выдавливал метку за пределы плитки (у неё overflow:hidden
   — выдавленное просто исчезло бы). */
.obj-tile .ot-foot { display: flex; align-items: flex-end; justify-content: space-between;
  gap: 4px 8px; flex-wrap: wrap; margin-top: auto; padding-top: 6px; }
.obj-tile .ot-foot .badges { min-width: 0; }
/* Метка типа прибора (ktm/2it/2it-u) — САМЫЙ тихий элемент плитки: это паспорт
   железа, а не состояние, и он не должен конкурировать со свечением аварии или
   жёлтой пилюлей канала. Отсюда faint-цвет, размер меньше meta-строки и
   отсутствие фона; margin-inline-start:auto держит её у правого края и когда
   бейджей нет вовсе. Значения НЕ переводятся (термины владельца), поэтому
   text-transform не ставим — «2it-u» должно остаться в нижнем регистре. */
/* Метка модели — данные о плате (ktm / 2it / 2it-u), а не украшение: --ink-3
   в теме отведён плейсхолдерам и ниже AA (свип по правилу «--ink-3 не несёт
   данные»). */
.obj-tile .ot-model { margin-inline-start: auto; flex: 0 0 auto;
  font: 600 var(--fs-tag) var(--mono); color: var(--ink-2); letter-spacing: .06em;
  white-space: nowrap; opacity: .95; }
.obj-tile.norm::before { background: var(--ok); }
/* «внимание»/«авария»: не только полоса и точка — сам фон плитки подкрашен
   статусом, полоса толще (7px), точка крупнее, плитка несёт постоянное «кольцо»
   (--*-ring: в тёмной теме glow, в светлой чернильный обод), авария дышит между
   обычным и раскалённым кольцом (tile-pulse). Иначе состояния плохо различимы. */
.obj-tile.warn::before, .obj-tile.alarm::before { width: 7px; }
.obj-tile.warn .ot-dot, .obj-tile.alarm .ot-dot { top: 10px; inset-inline-end: 10px; }
/* подсветка плитки при наведении на маркер карты (park.js highlightCard):
   класс, а не инлайновый style — переживает обновление через patch() */
.obj-tile.hl { outline: 2px solid var(--brand); outline-offset: 1px; }
.obj-tile.warn { border-color: rgba(255,176,32,.65);
  background: linear-gradient(180deg, var(--warn-soft), var(--panel));
  box-shadow: var(--shadow), var(--warn-ring); }
.obj-tile.warn::before { background: var(--warn); box-shadow: 0 0 11px var(--warn-glow); }

.obj-tile.alarm { border-color: var(--alarm); animation: tile-pulse 1.6s ease-in-out infinite;
  background: linear-gradient(180deg, var(--alarm-soft), var(--panel));
  box-shadow: var(--shadow), var(--alarm-ring); }
.obj-tile.alarm::before { background: var(--alarm); box-shadow: 0 0 12px var(--alarm-glow); }

/* Репер перед причиной рисуется значком из набора ICONS (park.js::reason), а не
   типографским знаком «▲»: глиф не масштабируется под кегль, по-разному рисуется
   на платформах и не читается скринридером. */
/* «нет связи» кодируется ещё и ФОРМОЙ, не только цветом: штрихованная полоса
   (данных нет — линия «прерывается»), точка — пустое кольцо, плитка залита
   серым: с «нормой» уже не спутать ни в одной теме */
.obj-tile.offline { border-color: rgba(100,116,139,.45);
  background: linear-gradient(180deg, var(--offline-soft), var(--panel)); }
.obj-tile.offline::before {
  background: repeating-linear-gradient(-45deg, var(--offline) 0 5px, transparent 5px 9px); }

.obj-tile.offline .ot-name { color: var(--ink-2); }
.obj-tile.offline .ot-meta { color: var(--offline); font-weight: 600; }
@keyframes tile-pulse { 50% { box-shadow: var(--shadow), var(--alarm-ring-hot); } }
@media (prefers-reduced-motion: reduce) { .obj-tile.alarm { animation: none; } .obj-tile.alarm .ot-dot { animation: none; } }

/* палочки сигнала (ui.js signalBars) */
.sigbars { flex: 0 0 auto; }
.sigbars .bar { fill: var(--ok); }
.sigbars .bar.low { fill: var(--warn); }
.sigbars .bar.dim { fill: var(--ink-3); opacity: .4; }

/* ── лента последних событий парка: нижний блок во всю ширину ── */
.park-feed { display: flex; flex-direction: column; min-height: 0; }
.park-feed .pf-scroll { overflow: auto; flex: 1; }
/* Рядом с картой лента живёт в нижней строке грида и держит ровно пять строк:
   «один экран» не должен распадаться на три независимые прокрутки. Отдельно —
   ленте не даём расти выше половины экрана в одноколоночной раскладке. */
.park-body.map .park-feed .pf-scroll { max-height: calc(5 * 34px); }
.park-body.list .park-feed .pf-scroll { max-height: 300px; }
.park-feed .tl-item { padding: 8px 14px; }
/* Имя объекта в ленте — НЕ моноширинное (сиблинг-свип к правке .mk-tag): глиф
   «№» в IBM Plex Mono на 11,5 px сливается в тёмный блок и слипается с цифрой,
   а «Насосная станция №4» и «№1» обязаны различаться именно этой частью.
   Моноширинность здесь ничего не выравнивает — это имя, а не число. */
.park-feed .pf-obj { font: 600 var(--fs-micro) var(--sans); letter-spacing: var(--ls-tight); color: var(--ink-2); }
/* Строка ленты — ссылка на объект: цвет и подчёркивание у неё не ссылочные,
   иначе лента превратилась бы в синее полотно. */
.park-feed .tl-item.click { display: block; cursor: pointer; color: inherit; text-decoration: none; transition: background .12s; }
.park-feed .tl-item.click:hover { background: var(--panel-2); }
/* Подпись охвата ленты: «только по 2 объектам» — рядом с заголовком карточки,
   чтобы «пусто» в ленте не читалось как «в парке ничего не происходит». */
.park-feed .pf-scope:not(:empty) {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-2); margin-inline-start: 8px;
}
/* Метка события («PEREGREV») — вторичная моно-подпись внутри русской фразы, а
   не цветное слово: это имя, под которым событие уходит в СМС. */
.pf-tok {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em;
  color: var(--ink-2); background: var(--panel-3);
  border: 1px solid var(--line); border-radius: 5px; padding: 0 5px;
}

/* ── скелетоны загрузки: тихое дыхание, не мерцание ── */
.skel { position: relative; overflow: hidden; border-radius: var(--r);
  background: var(--panel); border: 1px solid var(--line-2); min-height: 74px; }
.skel::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba(150,200,175,.05) 50%, transparent 70%);
  animation: skel-sweep 1.6s ease-in-out infinite; }
.skel.sm { min-height: 40px; }
@keyframes skel-sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .skel::after { animation: none; } }

/* === К4: объект — приборная шапка и тренды === */
/* ═══════════════ пульт объекта: приборная шапка + тренды (§15.4) ═════════ */
.obj-vitals { display: flex; align-items: stretch; gap: 0; flex-wrap: wrap;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow);
  padding: 4px 6px; margin-bottom: 12px; }
.obj-vitals .vt { display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding: 7px 16px; border-right: 1px solid var(--line-2); min-width: 0; }
.obj-vitals .vt:last-child { border-right: 0; }
.obj-vitals .vt i { font: 600 var(--fs-tag) var(--mono); letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-2); font-style: normal; white-space: nowrap; }
.obj-vitals .vt b { display: flex; align-items: center; gap: 6px;
  font: 600 var(--fs-val) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); white-space: nowrap; }
.obj-vitals .vt b .unit { color: var(--ink-2); font-weight: 500; font-size: var(--fs-micro); }
.obj-vitals .vt.warn b  { color: var(--warn-ink); text-shadow: 0 0 10px var(--warn-glow); }
.obj-vitals .vt.alarm b { color: var(--alarm-ink); text-shadow: 0 0 12px var(--alarm-glow); }
/* Метка пути «плата → сервер» для владельца (намеренно без легенды): тонкое
   подчёркивание значения «Связь» — сплошное http, точечное webdav, штриховое
   email. Приглушённый цвет, никакой сигнальности. */
.obj-vitals .vt b.via-http   { border-bottom: 1px solid  rgba(150,200,175,.42); padding-bottom: 1px; }
.obj-vitals .vt b.via-webdav { border-bottom: 1px dotted rgba(150,200,175,.55); padding-bottom: 1px; }
.obj-vitals .vt b.via-email  { border-bottom: 1px dashed rgba(150,200,175,.48); padding-bottom: 1px; }
/* SMS-резерв — единственный канал, который НЕ прячется в тонкое подчёркивание:
   это не «как удобнее доехало», а деградация связи за деньги владельца.
   Ячейка «Связь» получает класс .warn (жёлтый текст + свечение), а палочки
   с CSQ и типом радиосети уезжают во вторую строку .vt-sub — они остаются
   правдой о регистрации модема, просто перестают быть главным ответом. */
.obj-vitals .vt b.via-sms    { letter-spacing: .04em; }
/* А вот у стороннего прибора 2it SMS — не деградация, а единственный штатный
   канал (интернета у него нет вовсе). Поэтому ячейка остаётся нейтральной:
   тот же приглушённый пунктир, что у прочих путей, никакого жёлтого. */
.obj-vitals .vt b.via-sms-oko { border-bottom: 1px dashed rgba(150,200,175,.48); padding-bottom: 1px;
  letter-spacing: .04em; }
.obj-vitals .vt .vt-sub { display: flex; align-items: center; gap: 5px; margin-top: 1px;
  font: 500 var(--fs-tag) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-2); white-space: nowrap; }
/* Единица измерения читается вместе с числом — это часть значения. */
.obj-vitals .vt .vt-sub .unit { color: var(--ink-2); font-size: var(--fs-tag); }
.obj-vitals .vt .vt-sub .sigbars { width: 12px; height: 12px; }
@media (max-width: 700px) {
  .obj-vitals .vt { padding: 6px 11px; }
}

/* ── вид «Подробно»: плотные технические таблицы + сетка «ключ-значение» ── */
table.t.dense { font-size: var(--fs-body); }
table.t.dense th { padding: 9px 12px; }
table.t.dense td { padding: 8px 12px; }
table.t td.v { font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
table.t td.muted { color: var(--ink-2); }
table.t td.c-alarm { color: var(--alarm-ink); font-weight: 600; text-shadow: 0 0 10px var(--alarm-glow); }
table.t td.c-warn { color: var(--warn-ink); text-shadow: 0 0 8px var(--warn-glow); }
table.t td.c-on { color: var(--brand); font-weight: 600; }
table.t td.c-manual { color: var(--manual); }
.dtl-kv { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: 8px 20px; padding: 13px 16px; font-size: var(--fs-body); }
.dtl-kv b { color: var(--ink-2); font: 600 var(--fs-micro) var(--mono); letter-spacing: .05em; text-transform: uppercase; align-self: center; }
/* «Канал доставки: SMS-резерв» в таблице «Подробно» — жёлтым, наравне с
   аварийными значениями таблиц (table.t td.c-warn), чтобы строка не терялась
   среди двух десятков нейтральных «Аптайм / Температура / Сбросов модема». */
.dtl-kv .c-sms-fb { color: var(--warn-ink); font-weight: 600; text-shadow: 0 0 8px var(--warn-glow); }
/* Правимая ячейка в таблице «Плата»/«Прибор» (пока единственная — select метки
   типа прибора). Полноразмерный select.in с padding 8px поднял бы эту строку
   вдвое против соседних, и таблица из двенадцати строк «поехала» бы вокруг
   одного поля; здесь высота строки остаётся текстовой. */
.dtl-kv select.in { padding: 2px 8px; font-size: var(--fs-body); }
@media (max-width: 900px) { .dtl-kv { grid-template-columns: auto 1fr; } }

/* тренд в faceplate: обёртка с плавающей подсказкой */
.trend-wrap { position: relative; margin-top: 6px; }
.trend-tip { position: absolute; top: 2px; pointer-events: none; z-index: 2;
  background: var(--panel-3); border: 1px solid var(--line); border-radius: 7px;
  padding: 3px 8px; font: 600 var(--fs-micro) var(--mono); color: var(--ink); white-space: nowrap;
  box-shadow: 0 4px 14px rgba(0,0,0,.4); }

/* карта: одноразовый «пинг» на маркере при живом событии платы */
.map-ping { position: absolute; left: 50%; top: 50%; width: 16px; height: 16px;
  margin: -8px 0 0 -8px; border-radius: 50%; border: 2px solid var(--brand);
  opacity: 0; pointer-events: none; animation: map-ping 1.3s ease-out 1; }
@keyframes map-ping { 0% { transform: scale(.6); opacity: .85; } 100% { transform: scale(3.4); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .map-ping { display: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
 * СВЕТЛАЯ ТЕМА — компонентные оверрайды. Всё, что в тёмной теме зашито
 * литералами (тёмные подложки SVG, градиенты, инверсии карты), здесь
 * переводится на светлую сторону. Токены палитры — в html[data-theme="light"]
 * в начале файла.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* поле: бумага с едва заметным малахитовым дыханием (сепийные сканлайны
   и виньетка — как у сайта: rgba(50,44,32,…) / rgba(68,60,44,…)) */
html[data-theme="light"] body {
  background:
    radial-gradient(1200px 700px at 80% -8%, rgba(3,108,60,.05), transparent 60%),
    radial-gradient(900px 600px at 0% 100%, rgba(11,72,53,.05), transparent 55%),
    var(--bg);
  background-attachment: fixed;
}
html[data-theme="light"] body::before {
  background: repeating-linear-gradient(0deg, rgba(50,44,32,.075) 0 1px, transparent 1px 3px);
  mix-blend-mode: multiply; opacity: .5;
}
html[data-theme="light"] body::after {
  background: radial-gradient(ellipse 100% 85% at 50% 42%, transparent 62%, rgba(68,60,44,.14));
}
/* Четыре правила «выделение текста + ползунок прокрутки» здесь БОЛЬШЕ НЕ НУЖНЫ:
   их значения стали ролями --selection-ink / --scroll-thumb / --scroll-thumb-hi
   в блоке светлых токенов. Это и есть переход, ради которого затевалась F-064:
   компонент описан ОДИН раз, а тема подставляет своё значение. */

/* марка-кандзи: светлая эмаль на бумаге вместо тёмного стекла */
html[data-theme="light"] .nav .brand .mark,
html[data-theme="light"] .login .mark {
  background: radial-gradient(120% 120% at 30% 20%, #e8e6df, #d5d2c8);
  border-color: rgba(3,108,60,.45);
  text-shadow: none;
  box-shadow: inset 0 0 10px -6px rgba(3,108,60,.35), 0 0 0 3px rgba(3,108,60,.05);
}
/* Заплатка на тень кнопки «свернуть меню» снята: сама кнопка теперь стоит на
   ступени --elev-2, а ступень у бумаги своя, чернильная. */
html[data-theme="light"] .tabbar { background: rgba(244,243,239,.92); }  /* ← К1-Б: заменить на var(--sheet) и снять эту строку */

/* Цвет подписи главной кнопки днём (белый по малахиту) держит токен
   --btn-primary-ink в блоке светлых токенов — отдельного правила не нужно. */

/* заголовки/цифры: свечения дневным не нужны */
html[data-theme="light"] .counter.alarm b,
html[data-theme="light"] .counter.warn b,
html[data-theme="light"] .counter.online b,
html[data-theme="light"] .obj-tile .ot-kv .kvi.alarm b,
html[data-theme="light"] .obj-vitals .vt.alarm b,
html[data-theme="light"] .obj-vitals .vt.warn b,
html[data-theme="light"] .dtl-kv .c-sms-fb,
html[data-theme="light"] table.t td.c-alarm,
html[data-theme="light"] table.t td.c-warn,
html[data-theme="light"] .chip.alarm,
html[data-theme="light"] .chip.warn,
html[data-theme="light"] .cfg-steps .st-m.st-bad { text-shadow: none; }
html[data-theme="light"] .page-head h1::before { text-shadow: none; opacity: .65; }

/* ── статусы «толщиной», не свечением: glow на бумаге не работает, берём
   плотность/заливку. Чип аварии = сплошной «сургуч» под бумажным текстом;
   «внимание» — обратная схема: заливка #f0a800 = чистый жёлтый сигнал,
   ЧЁРНЫЕ чернила по ней дают 9,1:1 (на жёлтой бумаге охру пришлось бы топить
   до коричневого ради белого текста — см. блок токенов). */
/* ОДНА АНАТОМИЯ НА ВСЁ СЕМЕЙСТВО (F-063). До волны 7 приём «сплошная заливка +
   выворотный текст» был применён к двум чипам из семи — аварии и вниманию, —
   а норма, офлайн, ручной и обслуживание оставались мягкой подложкой. Рядом в
   одной строке это читалось так, будто «Норма» выключена, а список парка с
   тремя предупреждениями превращался в лоскутное одеяло из ярких плашек.
   Днём «толщину» даёт не свечение (на бумаге его нет), а ПЛОТНОСТЬ ПОДЛОЖКИ и
   чернильная граница 1,5 px — та же анатомия, что ночью, и одинаковая для всех
   семи видов. Сплошная заливка остаётся ровно одному чипу — аварии: это
   единственная смелость светлой темы, и именно она обязана бросаться в глаза.
   Замер: белый текст по сургучу 6,58:1, охра-чернила по охре-подложке 5,2:1. */
html[data-theme="light"] .chip.alarm { background: var(--alarm); color: #f7f6f2; border-color: #7d0f06; box-shadow: none; }
html[data-theme="light"] .chip.alarm .dot { background: #f7f6f2; box-shadow: none; }
html[data-theme="light"] .chip.warn { color: var(--warn-ink); box-shadow: none; border-width: 1.5px; }
/* Точка внутри чипа «внимание» днём — тёмная охра: яркая #f0a800 к бумаге
   даёт 1,7:1, и знак диаметром 7 px на своей же подложке просто пропадает. */
html[data-theme="light"] .chip.warn .dot { background: var(--warn-line); box-shadow: none; }
/* Точка уровня в чипе порога справочника: яркая охра #f0a800 к бумаге даёт
   1,7:1, и знак 7 px на своей же подложке пропадает — обводим тёмной охрой
   кольцом (а не рамкой: рамка при border-box съела бы половину заливки). */
html[data-theme="light"] .cat-spec .cs-dot.warn { box-shadow: 0 0 0 1px var(--warn-line); }
html[data-theme="light"] .chip.norm,
html[data-theme="light"] .chip.offline,
html[data-theme="light"] .chip.manual,
html[data-theme="light"] .chip.maint { border-width: 1.5px; }
/* Счётчики парка (KPI-строка) — днём цвет берётся ПЛОЩАДЬЮ. Ночью статус
   у счётчика несут цифра и свечение полоски слева; на бумаге свечения нет,
   а полоска в 3 px — это меньше промилле площади плашки, и строка из семи
   одинаково белых плашек читалась как таблица, а не как светофор.
   Лечение: полоска 5 px + подложка своего тона. Тонируются ТОЛЬКО авария и
   внимание — по ISA-101 «норма» не должна конкурировать за внимание, её
   зелёного (цифра + полоска) достаточно. */
html[data-theme="light"] .counter.alarm,
html[data-theme="light"] .counter.warn,
html[data-theme="light"] .counter.norm { padding-left: 16px; }
html[data-theme="light"] .counter.alarm::before,
html[data-theme="light"] .counter.warn::before,
html[data-theme="light"] .counter.norm::before { width: 5px; }
html[data-theme="light"] .counter.alarm {
  background: linear-gradient(var(--alarm-soft), var(--alarm-soft)),
              linear-gradient(180deg, var(--panel-2), var(--panel));
  border-color: rgba(184,26,16,.4);
}
html[data-theme="light"] .counter.warn {
  background: linear-gradient(var(--warn-soft), var(--warn-soft)),
              linear-gradient(180deg, var(--panel-2), var(--panel));
  border-color: var(--warn-line);
}
/* НОЛЬ НЕ СВЕТИТСЯ — и днём тоже. Правило выше красит плитку ПЛОЩАДЬЮ, и оно
   ничего не знает про ноль: у «АВАРИЯ 0» оставались красная подложка и красная
   рамка, то есть спокойный парк на первом экране диспетчера выглядел аварийным
   ровно в ту смену, когда светлой темой и пользуются. Ночную половину правила
   (цифра и полоска) гасит `.counter.zero` выше; здесь снимается дневная
   заливка. Стоит СРАЗУ после `.alarm`/`.warn` — тот же вес селектора, побеждает
   порядок. */
html[data-theme="light"] .counter.zero {
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border-color: var(--line);
}
/* Заплатка «рамка нормы» снята: цвет границы теперь выводится color-mix из
   --ok самой темы, то есть днём он уже дневной. Это и есть переход от заплаток
   к переопределению ролей (F-064) — минус одно правило из ста пятидесяти двух. */
/* Плашка и лента: подложка — прозрачная охра (её видно ЦВЕТОМ), текст и
   граница — тёмная охра (их видно ЯРКОСТЬЮ). Рамка из literal-rgba тёмной
   темы (255,176,32) на бумаге исчезала — перекрываем. */
/* Кромка полосы «внимание» днём — тёмная охра: 4 px это ещё ШТРИХ, а не пятно,
   а яркая #f0a800 к бумаге даёт 1,7:1 и штрихом пропадает (граница «тонкого»
   проведена по 6 px, см. ниже). Заплатка на border-color при этом снята —
   границу выводит color-mix из --warn-line самой темы. */
html[data-theme="light"] .banner.warn { border-inline-start-color: var(--warn-line); }
html[data-theme="light"] .tl-item.warn { border-left-color: var(--warn-line); }
/* тревожные плитки: чернильный обод несут --alarm-ring/--warn-ring (токены
   светлой темы), рамка «внимания» — тёмная охра (яркая на 1px исчезает) */
html[data-theme="light"] .obj-tile.warn { border-color: var(--warn-line); }
/* Всё, что в тёмной теме рисуется тонким штрихом, точкой или полоской и
   держалось на свечении, на бумаге переводим на --warn-line: у яркой охры
   #f0a800 к бумаге всего 1,7:1 по яркости, и линия в 1–2 px просто пропадает
   (заливка размером с чип при этом видна прекрасно — её несёт цвет). */
html[data-theme="light"] .counter.warn::before,
html[data-theme="light"] .obj-tile.warn::before,
html[data-theme="light"] .rmx-rc .rc-lv.warn { background: var(--warn-line); }
/* ⚠ v23: граница «тонкого» проведена по 6 px. Всё, что КРУПНЕЕ, — уже не
   штрих, а пятно: пятно должно быть ЖЁЛТЫМ (иначе объект «внимание» читался
   коричневым и путался с «нет связи»), а контраст ему даёт обод --warn-line.
   Всё, что тоньше (полоски плиток и счётчиков, шкалы сигнала), остаётся
   на --warn-line целиком: яркая охра в 1–3 px на бумаге просто исчезает. */
html[data-theme="light"] .mk.warn i,
html[data-theme="light"] .schm-legend .lg-dot.warn,
html[data-theme="light"] .obj-tile.warn .ot-dot {
  background: var(--warn); border: 1.5px solid var(--warn-line);
  box-shadow: 0 1px 3px rgba(24,20,12,.25); }
html[data-theme="light"] .schm .state-dot.warn {
  fill: var(--warn); stroke: var(--warn-line); stroke-width: 1.6; }
html[data-theme="light"] .sigbars .bar.low { fill: var(--warn-line); }
html[data-theme="light"] .schm .snode.warn .sym-body,
html[data-theme="light"] .schm .snode.warn .edge { stroke: var(--warn-line); }
html[data-theme="light"] .mk-cluster.warn,
html[data-theme="light"] .cr-item.warn { border-color: var(--warn-line); }
html[data-theme="light"] .cr-item.warn .cr-ic { color: var(--warn-ink); }
html[data-theme="light"] .obj-tile.warn::before, html[data-theme="light"] .obj-tile.alarm::before { box-shadow: none; }
html[data-theme="light"] .banner.alarm, html[data-theme="light"] .banner.warn,
html[data-theme="light"] .chip.alarm, html[data-theme="light"] .chip.warn { box-shadow: none; }
html[data-theme="light"] .counter.warn::before { box-shadow: none; }
/* Полоска НУЛЯ — нейтральная и днём. Правило «тонкая охра днём пропадает»
   выше по файлу перекрашивает `.counter.warn::before` в тёмную охру и про ноль
   не знает: у «ВНИМАНИЕ 0» оставалась янтарная полоса при полностью нейтральной
   плашке. Стоит ПОСЛЕ него — вес селектора тот же, решает порядок. */
html[data-theme="light"] .counter.zero::before { background: var(--line); }
html[data-theme="light"] .toast.alarm { box-shadow: var(--shadow); }
/* схема на бумаге: чернила не светятся — глушим drop-shadow, толщину линий
   тревога уже несёт сама (stroke-width выше) */
html[data-theme="light"] .schm .lbl-v.warn, html[data-theme="light"] .schm .lbl-v.alarm,
html[data-theme="light"] .schm .misc-row.warn .misc-k, html[data-theme="light"] .schm .misc-row.warn .misc-v,
html[data-theme="light"] .schm .misc-row.alarm .misc-k, html[data-theme="light"] .schm .misc-row.alarm .misc-v { filter: none; }
html[data-theme="light"] .schm .lblp.alarm .lblp-bg { fill: rgba(184,26,16,.14); stroke: rgba(184,26,16,.8); filter: none; }
html[data-theme="light"] .schm .lblp.warn  .lblp-bg { fill: rgba(240,168,0,.3);  stroke: var(--warn-line); }
html[data-theme="light"] .schm .pipe-glow.warn  { opacity: .3; }
html[data-theme="light"] .schm .pipe-glow.alarm { opacity: .34; }

/* модалка/поповеры: тень мягче, шторка чернильная, не чёрная.
   Две заплатки отсюда сняты: шторка окна ходит за ролью --scrim, поповер-щиток
   .face — за ступенью --elev-3, и обе роли у бумаги уже дневные.
   Оставшиеся две ждут владельцев: .schm-cfg — К4/К6, .trend-tip — К4;
   лечение одинаковое — поставить в тёмном правиле var(--elev-3) / var(--elev-2)
   и снять строку ниже. */
html[data-theme="light"] .schm-cfg { box-shadow: 0 18px 44px rgba(24,20,12,.22); }
html[data-theme="light"] .trend-tip { box-shadow: 0 4px 14px rgba(24,20,12,.18); }
html[data-theme="light"] .toast { box-shadow: var(--shadow); }

/* логин: светлая карточка-«бумага», водяной кандзи тонким малахитом */
html[data-theme="light"] .login-wrap {
  background: radial-gradient(1100px 700px at 50% -10%, rgba(3,108,60,.07), transparent 55%), var(--bg);
}
html[data-theme="light"] .login-wrap::before { -webkit-text-stroke: 1px rgba(3,108,60,.13); }
html[data-theme="light"] .login { background: rgba(247,246,242,.88); }

/* «замершая» офлайн-плашка */
html[data-theme="light"] .obj-stale-banner {
  background: linear-gradient(180deg, #f1f0eb, #e3e0d8);
  box-shadow: 0 10px 22px -16px rgba(24,20,12,.35);
}
/* Полоса готовности живёт в липкой шапке: на бумаге ей нужен не «подъём со
   дна экрана», а тот же фон страницы, что и у шапки. */
html[data-theme="light"] .cfg-head { background: var(--bg); }

/* чипы каналов конфигуратора: те же роли, но читаемые на светлом */
html[data-theme="light"] .chan-chip.di .cc-t { background: rgba(19,95,176,.12); color: #135fb0; }
html[data-theme="light"] .chan-chip.ai .cc-t { background: rgba(107,64,196,.12); color: #6b40c4; }
html[data-theme="light"] .chan-chip.do .cc-t { background: rgba(3,108,60,.12); }

/* ── графическая схема: «чертёж на бумаге» ── */
html[data-theme="light"] .schm {
  background:
    radial-gradient(760px 380px at 40% -8%, rgba(3,108,60,.05), transparent 62%),
    repeating-linear-gradient(0deg, rgba(24,20,12,.05) 0 1px, transparent 1px 28px),
    repeating-linear-gradient(90deg, rgba(24,20,12,.05) 0 1px, transparent 1px 28px),
    var(--bg-2);
}
html[data-theme="light"] .schm .snode .sym-body { fill: #f0efe9; }
html[data-theme="light"] .schm .snode .vessel { fill: none; }
html[data-theme="light"] .schm .lblp-bg { fill: rgba(249,248,245,.94); }
html[data-theme="light"] .schm .eplate { fill: rgba(249,248,245,.9); }
html[data-theme="light"] .schm .misc-bg { fill: rgba(249,248,245,.92); }
/* зебра на бумаге: --line-2 (.09 чернил) для полосы через строку тяжеловат —
   ослабляем до лёгкого печатного растра */
html[data-theme="light"] .schm .misc-row.zeb .misc-hit { fill: rgba(24,20,12,.05); }
html[data-theme="light"] .schm .misc-row:hover .misc-hit { fill: rgba(24,20,12,.06); }
/* внимание/авария в списке каналов, гибрид: alarm — ПЛОТНЫЙ
   «чип» (сургуч под бумажным текстом, 6,1:1) — самая громкая строка; warn —
   «маркер-подсветка»: полупрозрачная охра высотой по тексту (.misc-mark),
   текст остаётся обычным чернильным, без glow — эффект текстовыделителя */
html[data-theme="light"] .schm .misc-mark { fill: rgba(240,168,0,.5); }
html[data-theme="light"] .schm .misc-row.warn .misc-k { fill: var(--ink-2); filter: none; }
html[data-theme="light"] .schm .misc-row.warn .misc-v { fill: var(--ink); filter: none; }
html[data-theme="light"] .schm .misc-row.alarm .misc-hit { fill: var(--alarm); }
html[data-theme="light"] .schm .misc-row.alarm .misc-k,
html[data-theme="light"] .schm .misc-row.alarm .misc-v { fill: #f7f6f2; }
/* «лампа» активного канала: чернила не светятся — малахит без ореола */
html[data-theme="light"] .schm .misc-led { filter: none; }
html[data-theme="light"] .schm .snode .gauge { fill: rgba(249,248,245,.72); }
html[data-theme="light"] .schm .snode .vessel-bg { fill: rgba(249,248,245,.55); }
html[data-theme="light"] .schm .snode .handle.sq { fill: #f1f0eb; }
html[data-theme="light"] .schm .fl-in { fill: #f0efe9; }
html[data-theme="light"] .schm .ember { fill: #d9821f; }
html[data-theme="light"] .schm .pipe-case { stroke: #b3afa4; }
html[data-theme="light"] .schm .pipe-trench { stroke: var(--bg); }
/* ── Среды труб на бумаге (§F-156) ──
   Цвет трубы — основной носитель смысла мнемосхемы: где вода, где газ, где
   стоки. Ночные неоновые заливки на бумажном фоне давали 1,47:1 (вода) и
   2,20:1 (газ) при норме 3:1 для несущей смысл графики — то есть в светлой
   теме и на печати ответ на главный вопрос схемы просто исчезал. Здесь те же
   среды взяты в плотных «типографских» тонах, а бегущие точки потока
   инвертированы: белое по светлой воде не видно ничем. */
html[data-theme="light"] .schm .pipe-fill { stroke: #0e7f97; opacity: 1; }
html[data-theme="light"] .schm .pipe-fill.m-gas { stroke: #8a6c17; opacity: 1; }
html[data-theme="light"] .schm .pipe-fill.m-sew { stroke: #5c5e2e; opacity: 1; }
html[data-theme="light"] .schm .pipe-fill.m-air { stroke: #61798a; opacity: 1; }
html[data-theme="light"] .schm .snode .pump-fill { stroke: #0e7f97; }
html[data-theme="light"] .schm .pipe-water.flow { stroke: rgba(247,253,255,.95); filter: none; }
/* Плашка «вода» — теми же токенами, что заливка бака: разъехавшись, легенда
   начинает подписывать не тот цвет, а это единственное место, где читатель
   узнаёт, что синее на схеме — вода. */
html[data-theme="light"] .schm-legend .lg-pipe.water { background: linear-gradient(180deg, var(--water-a), var(--water-b)); }
html[data-theme="light"] .schm-legend .lg-pipe.gas   { background: linear-gradient(180deg, #a8861d, #6d5511); }
html[data-theme="light"] .schm-legend .lg-pipe.sew   { background: linear-gradient(180deg, #74763a, #474926); }
html[data-theme="light"] .schm-legend .lg-pipe.air   { background: linear-gradient(180deg, #7d95a6, #4e6373); }
html[data-theme="light"] .schm .impeller, html[data-theme="light"] .schm .snode .stem,
html[data-theme="light"] .schm .snode .handle { stroke: #6d7a5e; }
html[data-theme="light"] .schm .snode .hub { fill: #6d7a5e; }
html[data-theme="light"] .schm .snode .pipe-stub { stroke: #79826a; }
html[data-theme="light"] .schm .blade { fill: rgba(109,122,94,.24); stroke: #6d7a5e; }
html[data-theme="light"] .schm .snode .strap { stroke: rgba(24,20,12,.28); }
html[data-theme="light"] .schm .snode .panel { stroke: rgba(24,20,12,.26); }
html[data-theme="light"] .schm .support .brace { stroke: rgba(24,20,12,.2); }
html[data-theme="light"] .schm .earth { fill: rgba(80,72,56,.16); }
html[data-theme="light"] .schm .earth-pit { fill: var(--bg); }
html[data-theme="light"] .schm .bub { fill: rgba(249,248,245,.92); }
html[data-theme="light"] .schm-legend .lg-pipe { border-color: #b3afa4; }
html[data-theme="light"] .schm-legend .lg-pipe.dry { background: #cfccc2; }
/* процент уровня: тёмные чернила с белой обводкой — читается и на бирюзовой
   воде, и на белой стенке сосуда над зеркалом */
html[data-theme="light"] .schm .vlevel { fill: #0b4550; stroke: rgba(255,255,255,.9); }

/* ── карта: светлая подложка (positron из park.js), тёплая сепийная рамка ── */
html[data-theme="light"] #park-map .maplibregl-canvas { filter: saturate(.78) contrast(1.02) sepia(.03); }
html[data-theme="light"] #park-map::after { box-shadow: inset 0 0 60px 4px rgba(68,60,44,.14); }
html[data-theme="light"] .mk i { border-color: rgba(249,248,245,.95); }
html[data-theme="light"] .mk-tag { background: rgba(249,248,245,.9); }
html[data-theme="light"] .mk-cluster { background: rgba(249,248,245,.94); }
html[data-theme="light"] .maplibregl-ctrl button .maplibregl-ctrl-icon { filter: none; opacity: .8; }
html[data-theme="light"] .maplibregl-ctrl-attrib { background: rgba(244,243,239,.75) !important; }
html[data-theme="light"] .maplibregl-ctrl-attrib-button { filter: none; opacity: .7; }

/* ── скелетоны: блик тёмным по светлому ── */
html[data-theme="light"] .skel::after {
  background: linear-gradient(100deg, transparent 30%, rgba(24,20,12,.05) 50%, transparent 70%);
}

/* ── редактор схем v2: клетка, направляющие, палитра ── */
.schm .grid-minor { stroke: rgba(110, 235, 210, .045); stroke-width: 1; fill: none; }
.schm .grid-major { stroke: rgba(110, 235, 210, .09); stroke-width: 1; fill: none; }
.schm .schm-guide { stroke: #5ef0ea; stroke-width: 1.2; stroke-dasharray: 5 5; opacity: .85;
  filter: drop-shadow(0 0 4px rgba(94, 240, 234, .5)); }
/* «Убрать» подсвечивает и добавленное оборудование, не только трубы */
.schm.editing.tool-erase .snode { cursor: not-allowed; }
.schm.editing.tool-erase .snode.virtual-node { cursor: pointer; }
.schm.editing.tool-erase .snode.virtual-node:hover .sym-body { stroke: rgba(255, 77, 63, .8); }
/* «Двигать» по вертикали трубы — колено */
.schm.editing.tool-move .pipe-hit { cursor: ew-resize; }
.schm.editing.tool-move .snode { cursor: grab; }
/* палитра добавления */
.schm-cfg .cfg-palette { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding-top: 2px; }
.schm-cfg .cfg-palette .btn { justify-content: flex-start; }
.schm-cfg input.cfg-sel { min-width: 172px; }

/* ═══════════════ i18n: RTL (арабский) ═══════════════
   Как на промо-сайте: честный dir=rtl на <html> (ставит i18n.js по языку),
   вёрстка зеркалится сама (flex/grid), а ГРАФИКА остаётся LTR: мнемосхема и
   оси времени читаются слева направо, и text-anchor start/end в SVG под
   dir=rtl меняет смысл — подписи боксов наезжали друг на друга. */
html[dir=rtl] .schm, html[dir=rtl] .trend-wrap { direction: ltr; }

/* Пользовательские имена и пути «город · предприятие · объект» — данные
   любой письменности (в наших парках — кириллица). В RTL-абзаце нейтральные
   символы на КРАЯХ LTR-имени («ёлочки», №, скобки, разделители) перепрыгивали
   на другую сторону строки: «КНС-7 «Восточная»» теряла закрывающую ёлочку.
   plaintext: элемент — отдельный bidi-абзац, направление — по первому
   сильному символу САМОГО содержимого (кириллица → LTR, арабские имена →
   RTL). .bidi-name — та же изоляция как утилитарный класс для элементов
   без собственного стилевого класса. */
html[dir=rtl] .ot-name, html[dir=rtl] .ot-ent, html[dir=rtl] .mk-tag,
html[dir=rtl] .tl-time, html[dir=rtl] .maplibregl-popup-content,
html[dir=rtl] .page-head .sub, html[dir=rtl] .dtl-kv .v,
html[dir=rtl] .kv > span, html[dir=rtl] .bidi-name {
  unicode-bidi: plaintext;
}

/* ═══════════════ ЯКОРЯ ВОЛНЫ wf4 (справочники приборов RS-485) ═══════════════
 * Третьего файла стилей завести нельзя: index.html подключает ровно fonts.css
 * и theme.css. Значит theme.css общий, и чтобы три потока волны не правили
 * один хвост файла одновременно, у каждого свой маркер: пишем ТОЛЬКО под
 * своим. Светлую тему (html[data-theme="light"] .селектор) поток кладёт туда
 * же, под свой маркер, а не в общий блок светлых оверрайдов выше — иначе
 * правка расползается по файлу и её нельзя ни найти, ни снять целиком.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* --- wf4 mbx --- */
/* Панель и строка «Прибор на кабеле RS-485» в конфигураторе (поток П3).
   Классы .mbx-* */

/* Свёрнутая строка в инспекторе экземпляра. Та же левая линейка, что у блока
   датчика (.ins-sens): оба отвечают на вопрос «что к этой единице привязано»,
   и разными рамками они спорили бы друг с другом. */
.mbx-row { border-left: 2px solid var(--line); padding: 2px 0 8px 8px; margin-bottom: 8px; }
.mbx-h { font: 600 var(--fs-micro) var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-2); margin: 10px 0 5px; }
.mbx-model { font: 600 var(--fs-body) var(--sans); color: var(--ink); }
.mbx-list { margin-top: 2px; line-height: 1.5; }
/* Строка-предложение («считать моточасы по току»): предложение, а не команда —
   поэтому тихая рамка и кнопка-призрак, а не заливка. */
.mbx-sug { margin-top: 8px; padding: 6px 8px; border: 1px dashed var(--line);
  border-radius: var(--r); display: grid; gap: 3px; justify-items: start; }

/* Окно панели шире обычной модалки: в нём таблица величин с адресами
   регистров, а в 460 px строка «Напряжение звена постоянного тока» переносится
   трижды и список перестаёт читаться. */
/* Прокрутка ВНУТРИ окна обязательна: список величин ОМ-310 — шестнадцать строк
   с подсказками, и без ограничения высоты окно вылезает за экран вместе с
   кнопкой «Добавить каналы» — нажать её становится нечем. */
.mbx-modal { width: min(680px, 100%); max-height: calc(100vh - 32px); overflow: auto; }
/* Кнопки прилипают к низу окна: чек-лист настройки прибора длиннее экрана, и
   без этого «Добавить каналы» оказывается за краем — человек читает список
   величин и не видит, чем закончить. */
.mbx-modal .row { position: sticky; bottom: -20px; margin: 18px -20px -20px;
  padding: 10px 20px 20px; background: var(--panel); border-top: 1px solid var(--line); }
/* Шапка прилипает к верху по той же причине, по какой кнопки прилипли к низу:
   решение «добавить N каналов» принимается у нижнего края окна длиной около
   1800 px, и там не было видно ни агрегата, ни модели прибора, ни адреса. */
.mbx-hd { position: sticky; top: -20px; z-index: 3; margin: -20px -20px 8px;
  padding: 20px 20px 8px; background: var(--panel); border-bottom: 1px solid var(--line); }
.mbx-hd h3 { margin: 0; }
.mbx-sub { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin-top: 4px;
  font-size: var(--fs-body); color: var(--ink-2); }
/* Агрегат — главный ответ этой строки: для КОГО заводятся каналы. */
.mbx-sub .mbx-sub-eq { color: var(--ink); font-weight: 600; }
.mbx-sub .mbx-sub-d { color: var(--ink-2); opacity: .5; }
.mbx-top { display: flex; align-items: baseline; gap: 10px; justify-content: flex-end; margin-bottom: 8px; }
.mbx-jump { margin: 0 0 8px; }
.mbx-note { margin: 6px 0; line-height: 1.5; }
.mbx-addr { margin: 10px 0; }
.mbx-addr .lb { margin-top: 0; }

/* Плашка совместимости: цвет несёт исход («услышит / не услышит»), поэтому
   у неё есть и граница — на монохромном экране и при дальтонизме заливка одна
   не различается. */
.mbx-badge { display: block; border-radius: var(--r); padding: 7px 10px; margin: 8px 0;
  font-size: var(--fs-body); line-height: 1.45; border: 1px solid var(--line); color: var(--ink-2); }
.mbx-badge.ok   { background: var(--ok-soft);    color: var(--ok-ink);    border-color: rgba(53,201,143,.35); }
.mbx-badge.warn { background: var(--warn-soft);  color: var(--warn-ink);  border-color: var(--warn-line); }
.mbx-badge.bad  { background: var(--alarm-soft); color: var(--alarm-ink); border-color: rgba(255,77,63,.45); }
.mbx-badge.mut  { background: var(--panel-2); }
/* Компактный вид — в списке выбора прибора, одной строкой справа. */
.mbx-pick .mbx-badge { display: inline-block; margin: 0; padding: 2px 7px; font-size: var(--fs-micro); white-space: nowrap; }

.mbx-grp { font: 600 var(--fs-micro) var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-2); margin: 12px 0 6px; }
.mbx-pick { display: grid; grid-template-columns: 1fr auto auto auto; gap: 10px; align-items: center;
  width: 100%; text-align: start; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--r); padding: 8px 10px; margin-bottom: 6px; color: var(--ink); cursor: pointer; }
.mbx-pick:hover { background: var(--panel-3); border-color: var(--brand-deep); }
.mbx-pick .mp-n { font-weight: 600; }
.mbx-pick .mp-b, .mbx-pick .mp-c { color: var(--ink-2); font-size: var(--fs-body); }

/* Величина карты. Первая строка — имя и единица (то, что выбирают), вторая —
   адрес регистра и оговорки: адрес нужен один раз, при сверке с паспортом, и
   не должен спорить с именем за внимание. */
.mbx-reg { padding: 6px 0; border-bottom: 1px solid var(--line-2); }
.mbx-reg .mr-1 { display: flex; align-items: center; gap: 8px; }
.mbx-reg .mr-n { font-size: var(--fs-body); }
.mbx-reg .mr-2 { margin: 2px 0 0 24px; font-size: var(--fs-micro); color: var(--ink-2); line-height: 1.45; }
.mbx-reg .mr-2.warn { color: var(--warn-ink); }
.mbx-reg .mr-has { font-size: var(--fs-micro); color: var(--ok-ink); }
/* Уже заведённая величина — строка состояния, а не отключённая галочка:
   галочка означала бы сразу «выбрано к добавлению» и «уже есть», и счёт в
   кнопке переставал сходиться с тем, что видно глазами. */
.mbx-reg .mr-ok { display: inline-flex; color: var(--ok-ink); }
.mbx-reg.has .mr-1 { color: var(--ink-2); }
/* Недоступная величина — крестик и причина, а НЕ серая галка: серое читается
   как «нет прав», крестик с объяснением — как свойство железа. */
.mbx-reg.off .mr-1 { color: var(--ink-2); }
.mbx-reg.off .mr-x { display: inline-flex; color: var(--alarm-ink); opacity: .8; }
.mbx-off-h { margin: 12px 0 2px; font: 600 var(--fs-micro) var(--mono); letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-2); border-top: 1px solid var(--line); padding-top: 8px; }
.mbx-rowerr { margin: 0 0 6px 24px; }

.mbx-budget { margin: 12px 0; padding: 7px 10px; border-radius: var(--r);
  background: var(--panel-2); font-size: var(--fs-body); }
.mbx-budget.bad { background: var(--alarm-soft); color: var(--alarm-ink); }
.mbx-setup { margin: 4px 0 0; padding-inline-start: 18px; font-size: var(--fs-body); line-height: 1.6; }

/* Светлая тема: подложки статусов в ней почти прозрачные (см. блок токенов),
   поэтому плашке нужна более плотная граница — иначе исход «прибор не будет
   услышан» читается как обычный абзац. */
html[data-theme="light"] .mbx-badge.ok   { border-color: var(--ok); }
html[data-theme="light"] .mbx-badge.warn { border-color: var(--warn); }
html[data-theme="light"] .mbx-badge.bad  { border-color: var(--alarm); }
html[data-theme="light"] .mbx-pick:hover { background: var(--panel-3); border-color: var(--brand-2); }

/* --- wf4 snx --- */
/* Подсказки карточки датчика: шкала токовой петли, блок реле контроля фаз
   (поток П3). Классы .snx-* */

/* Блок «Реле контроля фаз». Рамка, а не голый текст: это единственный датчик,
   который наладчик заводит в разрыв силовой цепи, и три факта о нём (полярность
   контакта, занятость групп, собственные выдержки прибора) он обязан прочитать
   до выезда, а не найти взглядом среди подсказок полей. */
.snx-box { border: 1px solid var(--line); border-radius: var(--r); padding: 9px 11px;
  margin: 4px 0 10px; background: var(--panel-2); }
.snx-box .snx-h { font: 600 var(--fs-micro) var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-2); margin-bottom: 6px; }
.snx-box .snx-l { font-size: var(--fs-body); line-height: 1.5; }
.snx-box .snx-src { margin-top: 2px; line-height: 1.45; }
.snx-box .snx-p { margin-top: 7px; font-size: var(--fs-body); color: var(--ink-2); line-height: 1.5; }
/* Что ПОЛУЧИЛОСЬ, а не что нажато: расхождение с карточкой прибора означает
   перевёрнутую аварию, и оно обязано быть видно цветом. */
.snx-box .snx-st { margin-top: 6px; font-size: var(--fs-body); line-height: 1.5; }
.snx-box .snx-st.ok  { color: var(--ok-ink); }
.snx-box .snx-st.bad { color: var(--warn-ink); }
.snx-box .snx-warn { margin-top: 7px; padding: 6px 8px; border-radius: var(--r);
  background: var(--warn-soft); color: var(--warn-ink); border: 1px solid var(--warn-line);
  font-size: var(--fs-body); line-height: 1.5; }
/* Единственное место в продукте, где ошибка сжигает вход платы: значок + своя
   строка, чтобы предупреждение нельзя было дочитать «по диагонали» вместе со
   справкой «куда провода». Цвет тот же янтарный — красный в продукте занят
   аварией на объекте, и занимать его подсказкой монтажа нельзя. */
.snx-box .snx-warn.snx-danger { display: flex; gap: 8px; align-items: flex-start; font-weight: 500; }
.snx-box .snx-warn .snx-warn-i { display: inline-flex; flex: none; margin-top: 2px; }
/* Факт про вшитый детект 3,6 / 21 мА и подсказка обобщённой позиции — тихие
   строки: это не замечание, чинить в них нечего. */
.snx-fact, .snx-gen { line-height: 1.5; margin: -2px 0 8px; }

/* «Готовые правила» реле контроля фаз. Кнопка и подпись под ней, а не кнопка
   с подсказкой в title: вторая кнопка гасит выход и отменяет ручную команду —
   последствие обязано быть прочитано ДО нажатия, а не после. */
/* Ярлык «Модели нет в справочнике?» над деревом датчиков: у наладчика без
   марки прибора нет слова, по которому искать, — даём ему пять слов. */
.pal-quick { border: 1px dashed var(--line); border-radius: var(--r); padding: 6px 8px; margin-bottom: 8px; }
.pal-quick .pq-t { font: 600 var(--fs-micro) var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-2); }
.pal-quick .pq-b { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
.pal-quick .pq-b .btn { text-transform: none; }

.snx-act { margin: 4px 0 10px; }
.snx-act-i { margin-top: 6px; display: grid; gap: 3px; justify-items: start; }
.snx-act-i .small { line-height: 1.45; }

html[data-theme="light"] .snx-box { background: transparent; }
html[data-theme="light"] .snx-box .snx-warn { border-color: var(--warn); }

/* --- wf4 cat --- */
/* Страница «Справочники»: поиск, фильтры, разделы формы, таблица регистров
   (поток П4). Новые классы .cat-* — сюда; существующие .cat-* остаются на
   своих местах выше по файлу и правятся там же. */

/* Полоса выборки: поиск слева, чипы-фильтры рядом, счётчик найденного справа.
   Ниже 900 px переносится сама — справочник десктопный, но ломаться не должен. */
.cat-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.cat-search { position: relative; display: flex; align-items: center; flex: 1 1 280px; max-width: 460px; }
.cat-search svg { position: absolute; inset-inline-start: 10px; color: var(--ink-3); pointer-events: none; }
.cat-search .in { width: 100%; padding-inline-start: 32px; }
.cat-filters { display: flex; gap: 6px; flex-wrap: wrap; }
/* Чип-фильтр — переключатель, а не статус: в выключенном виде он тихий, во
   включённом берёт цвет бренда (тот же язык, что у сегментов вкладок). */
.cat-chip {
  border: 1px solid var(--line); background: var(--panel-2); color: var(--ink-2);
  border-radius: 999px; padding: 4px 12px; font: 600 var(--fs-body) var(--sans); cursor: pointer;
  transition: color .14s, background .14s, border-color .14s;
}
.cat-chip:hover { color: var(--ink); }
.cat-chip.on { background: var(--brand-soft); color: var(--brand); border-color: rgba(51,224,163,.36); }
.cat-found { font: 600 var(--fs-micro) var(--mono); letter-spacing: .04em; color: var(--ink-2); margin-inline-start: auto; }

/* Подпись виртуальной ветки («телеметрию с них плата читает по кабелю») —
   тихая, чтобы не спорить с названием группы, но это ПРОЗА, которую читают
   глазами: на --ink-3 она давала 3,42:1 в тёмной теме при норме 4,5:1. */
.cat-ghint { font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); letter-spacing: 0; }
/* Чип «Modbus · N из M величин» — возможность, а не предупреждение: цвет
   бренда, не янтарь. */
.cat-spec.mb { color: var(--brand); border-color: rgba(51,224,163,.3); background: var(--brand-soft); }
/* Вторая тихая строка значения: цитата из руководства, причина недоступности.
   Это ПРОЗА, которую читают глазами («РЭ п.3.1.2: при исправной сети…»), а не
   подсказка пустого поля, поэтому --ink-2: на --ink-3 она давала 3,42:1 в
   тёмной теме при норме 4,5:1 и кегле 11,5 px. */
.cat-sub { font-size: var(--fs-micro); color: var(--ink-2); margin-top: 2px; }

/* Таблица регистров в раскрытой карточке позиции: широкая — прокручивается
   ВНУТРИ карточки, страница горизонтально не едет.
   min-width:0 обязателен: обёртка лежит в ячейке грида (.kv), а ячейка грида по
   умолчанию min-width:auto — то есть растягивается под содержимое, и прокрутка
   не включается никогда. Карточка (.cat-group, overflow:hidden) при этом просто
   ОБРЕЗАЛА последнюю колонку «читается» — самую нужную наладчику. */
.cat-regv-wrap { overflow-x: auto; max-width: 100%; min-width: 0; }
/* Ячейка грида под составное значение (таблица регистров, список строк). */
.kv > .v.cat-v-node { min-width: 0; }
.cat-regv { border-collapse: collapse; font-size: var(--fs-body); min-width: 720px; }
/* Шапка таблицы — единственное, что называет колонки: это данные, а не
   плейсхолдер (--ink-3 давал 3,42:1 в тёмной теме при кегле 10,5 px). */
.cat-regv th {
  text-align: start; font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-2); padding: 4px 10px 4px 0; border-bottom: 1px solid var(--line);
  /* Заголовок не переносится: слово «НАЗНАЧЕНИЕ», разложенное по буквам в
     столбик, — не заголовок, а орнамент. Таблица шире обёртки прокручивается
     внутри карточки (.cat-regv-wrap), для того обёртка и заведена. */
  white-space: nowrap;
}
.cat-regv td { padding: 4px 10px 4px 0; border-bottom: 1px solid var(--line-2); vertical-align: top; }
.cat-regv td.k, .cat-regv td.n { font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Название с пояснением — самая широкая колонка; единица не должна рваться
   по слогам в узкой. */
.cat-regv td:nth-child(2) { min-width: 260px; }
.cat-regv td.u { min-width: 132px; }
/* Величина, которую плата не прочитает, отступает на второй план ФОНОМ, а не
   цветом текста: причина недоступности («плата эту величину не прочитает») —
   ровно тот текст, который наладчик обязан прочитать, чтобы не вписывать адрес
   руками, и на --ink-3 он давал 3,42:1 при норме 4,5:1. */
.cat-regv tr.off td { color: var(--ink-2); background: var(--panel-2); }

/* ── окно правки позиции ── */
.cat-modal { width: min(620px, 100%); }
/* Поля по краям обязательны: при 1280 широкое окно занимало 1248 px и упиралось
   в края экрана — оно переставало читаться как слой поверх страницы, и
   пропадало ощущение «отсюда можно выйти». */
.cat-modal.wide { width: min(1400px, calc(100vw - 64px)); }
.cat-form { display: grid; gap: 10px; }
.cat-sec { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel-2); overflow: hidden; }
.cat-sec > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; font: 700 var(--fs-body) var(--disp); letter-spacing: .02em; user-select: none;
}
.cat-sec > summary::-webkit-details-marker { display: none; }
.cat-sec > summary:hover { background: var(--panel); }
.cat-sec-b { padding: 4px 12px 14px; display: grid; gap: 12px; }
/* Широкое окно (оборудование) раскладывает поля в две колонки: одним столбцом
   раздел «Машинное» превращался в тоннель со скроллом. */
.cat-modal.wide .cat-sec-b { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cat-modal.wide .cat-sec-b > .cat-f.area, .cat-modal.wide .cat-sec-b > .cat-mb { grid-column: 1 / -1; }
.cat-f { display: grid; gap: 4px; }
/* Скрытие через hidden обязано побеждать собственный display: у элемента с
   display:grid атрибут hidden (display:none браузера) проигрывает по
   специфичности, и «спрятанное» поле оставалось на экране. */
.cat-f[hidden], .cat-reg-more[hidden], .cat-err[hidden] { display: none; }
.cat-f > .in { width: 100%; }
.cat-f.chk > label { display: flex; align-items: center; gap: 8px; font-size: var(--fs-body); cursor: pointer; }
.cat-f .lb { font: 600 var(--fs-micro) var(--mono); letter-spacing: .04em; text-transform: uppercase; color: var(--ink-2); }
/* Ошибка стоит ПОД своим полем и не закрывает окно: до этого любая ошибка
   валидации выбрасывала человека из формы, теряя фокус и прокрутку. */
.cat-err { color: var(--alarm-ink); font-size: var(--fs-body); }
/* align-items:start обязателен: у поля без подсказки ячейка растягивалась по
   высоте самой высокой в ряду и опускала контрол на 12 px — четыре поля одного
   ряда вставали на двух уровнях и читались как две группы. */
.cat-grid4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; align-items: start; }
.cat-grid4 > .span2 { grid-column: span 2; }
.cat-rows { display: grid; gap: 6px; }
.cat-row { display: grid; grid-template-columns: 1fr auto; gap: 6px; align-items: center; }

/* ── пара «от / до» одной ячейкой ──
   Диапазон — ОДНО значение из двух чисел. Разорванный по двум подписям («низ» /
   «верх»), он читался как два независимых параметра, а в двухколоночной форме
   половинки уезжали в разные колонки и разные строки: пару заполняют одним
   движением мысли, и искать второе поле по диагонали человек не должен. */
.cat-pair-b { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: start; }
.cat-half { display: grid; gap: 3px; min-width: 0; }
.cat-half-l { font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); }
.cat-half > .in { width: 100%; }

/* Строка-пояснение раздела: объясняет, почему полей стало меньше, чем в
   соседней карточке. Не поле — отступ сверху отделяет её от подписи раздела. */
.cat-note { grid-column: 1 / -1; margin: -2px 0 2px; line-height: 1.45; }

/* ── паспорт построчно ──
   Одна строка «ключ: значение; ключ: значение» в окошке на три ряда: у ОМ-310
   это 700 знаков, потерянная точка с запятой молча склеивала два поля, а ошибка
   формата всплывала уже в раскрытой карточке. Двумя блоками ниже, для списка
   «что настроить на приборе», в том же окне уже жил нормальный построчный
   редактор — два решения одной задачи внутри одного окна учат не доверять форме. */
.cat-prow { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(0, 2fr) auto; gap: 6px; align-items: start; }
.cat-pass-txt { margin-top: 6px; border: 1px solid var(--line-2); border-radius: var(--r); background: var(--panel); }
.cat-pass-txt > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; user-select: none;
  font: 600 var(--fs-micro) var(--mono); letter-spacing: .04em; text-transform: uppercase; color: var(--ink-2); }
.cat-pass-txt > summary::-webkit-details-marker { display: none; }
.cat-pass-txt > summary:hover { color: var(--ink); }
.cat-pass-txt-b { display: grid; gap: 6px; padding: 0 10px 10px; }
.cat-pass-txt textarea { width: 100%; }

/* ── таблица величин карты Modbus ── */
.cat-mb { display: grid; gap: 12px; }
.cat-regs-wrap { overflow-x: auto; max-width: 100%; padding-bottom: 4px; }
.cat-regs { display: grid; gap: 4px 6px; align-items: center; min-width: max-content; }
/* Заголовок колонки — единственное, что называет её содержимое: данные, а не
   плейсхолдер. Знак вопроса говорит, что у заголовка есть расшифровка. */
.cat-regs .hd { font: 600 var(--fs-tag) var(--mono); letter-spacing: .05em; text-transform: uppercase; color: var(--ink-2);
  align-self: end; padding-bottom: 2px; }
.cat-regs .hd .hd-q { color: var(--ink-2); opacity: .6; margin-inline-start: 3px; cursor: help; }
.cat-regs .cl { min-width: 0; }
.cat-regs .cl.c { display: flex; justify-content: center; }
.cat-regs .cl .in { width: 100%; padding: 5px 7px; font-size: var(--fs-body); }
/* ── три служебные колонки липнут к правому краю ──
   Замер при окне 1280: обёртка 1172 px, содержимое 1332 px, правый край на
   x=1226, а первый чекбокс «читается» — на x=1262, то есть за краем. «Читается»
   решает, предложат ли величину галочкой в панели прибора; без него и без
   кнопки удаления редактор карты выглядит недоделанным. Полоса прокрутки
   внутренняя, ничем не обозначена и лежит под шестнадцатью строками величин.
   Смещения считаны из ширин колонок сетки: кнопки по 34 px, зазор 6 px. */
.cat-regs .st { position: sticky; z-index: 2; background: var(--panel-2);
  /* Обводка того же цвета закрывает зазоры сетки (6 px между колонками, 4 px
     между строками): иначе уехавшее под липкие колонки содержимое просвечивало
     бы полосками между ними. */
  outline: 3px solid var(--panel-2); }
.cat-regs .cl.st { align-self: stretch; display: flex; align-items: center; justify-content: center; }
/* Пустая ячейка шапки (над кнопками) без этого имеет нулевую высоту — и её фон
   ничего не закрывает: в шапке проступали заголовки уехавших колонок. */
.cat-regs .hd.st { align-self: stretch; }
/* Смещения считаны из ширин колонок сетки (кнопки по 40 px, зазор 6 px) —
   они заданы в catalogs.js::modbusEditor и меняются вместе с этими числами. */
.cat-regs .st1 { inset-inline-end: 92px; border-inline-start: 1px solid var(--line); padding-inline-start: 6px; }
.cat-regs .st2 { inset-inline-end: 46px; }
.cat-regs .st3 { inset-inline-end: 0; }
/* Ошибки строки и её раскрытые поля — во всю ширину сетки, сразу под строкой. */
.cat-regs .row-span, .cat-reg-more { grid-column: 1 / -1; }
.cat-reg-more {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;
  padding: 10px 12px; margin: 2px 0 6px; background: var(--panel);
  border: 1px solid var(--line-2); border-radius: 9px;
}

/* ── действия строки позиции ──
   Три серых прямоугольника на КАЖДОЙ из 130 строк (до сорока одновременно на
   экране) спорили с чипами за правый край и делали список тяжелее, чем он есть,
   а необратимое удаление стояло в одном ряду с безобидной правкой и в 20 px от
   неё. Постоянна одна кнопка «…»; правка выезжает при наведении и по
   :focus-within — то есть остаётся достижимой с клавиатуры. */
.ci-act { position: relative; gap: 2px; margin-inline-start: 6px; }
.ci-act .ci-edit { opacity: 0; transition: opacity var(--dur-1); }
.cat-item > summary:hover .ci-act .ci-edit,
.cat-item > summary:focus-within .ci-act .ci-edit,
.cat-item[open] > summary .ci-act .ci-edit { opacity: 1; }
@media (hover: none) { .ci-act .ci-edit { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .ci-act .ci-edit { transition: none; } }
.ci-menu {
  position: fixed; z-index: 60;
  display: grid; min-width: 190px; padding: 4px; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow);
}
.ci-mi {
  display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 9px;
  background: none; border: 0; border-radius: 7px; cursor: pointer;
  font: 500 var(--fs-body) var(--sans); color: var(--ink); text-align: start;
}
.ci-mi:hover { background: var(--panel-2); }
.ci-mi.danger { color: var(--alarm-ink); }
.ci-mi.danger:hover { background: var(--alarm-soft); }

@media (max-width: 900px) {
  .cat-grid4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cat-grid4 > .span2 { grid-column: span 2; }
  .cat-reg-more { grid-template-columns: 1fr; }
  .cat-prow { grid-template-columns: 1fr auto; }
  .cat-prow > .in:first-child { grid-column: 1 / -1; }
}

/* ── телефон: раздел десктопный, но ломаться не должен (бриф §4) ──
   На 390 px строка позиции разваливалась на три яруса (чипы переносились по
   одному, кнопки вставали «две + одна»), высота строки доходила до 120 px, а
   вся страница — до 8443 px. Чипы-спеки на такой ширине не помещаются рядом с
   именем и уезжают в раскрытую карточку, где они и так есть словами; в строке
   остаются код, имя и одна кнопка действий нормального размера. */
@media (max-width: 860px) {
  .cat-item > summary { padding-inline-start: 22px; }
  .cat-item .ci-body { padding-inline-start: 22px; }
  .cat-item .ci-specs > .cat-spec { display: none; }
  .ci-act .ci-edit { display: none; }
  .ci-act .ci-more { min-width: 44px; min-height: 44px; }
  .cat-chip { min-height: 44px; padding: 0 14px; }
  .cat-brand > summary { padding-inline-start: 22px; }
}
/* Чип-фильтр на десктопе — 32 px: 27 px промахивались мышью и ломали ритм
   полосы выборки рядом с полем поиска высотой 36 px. */
@media (min-width: 861px) { .cat-chip { min-height: 32px; } }

/* Светлая («бумажная») тема: заливки чипа и раскрытой панели величины на белом
   листе слишком тяжелы — оставляем рамку и лёгкий тон. */
html[data-theme="light"] .cat-chip.on { background: var(--brand-soft); border-color: var(--brand); }
html[data-theme="light"] .cat-spec.mb { background: transparent; border-color: var(--brand); }
html[data-theme="light"] .cat-sec { background: var(--panel); }
html[data-theme="light"] .cat-reg-more { background: var(--panel-2); }
/* Липкие колонки редактора карты стоят на фоне раздела, а он в светлой теме
   другой: без этого под ними просвечивало бы уехавшее содержимое. */
html[data-theme="light"] .cat-regs .st { background: var(--panel); outline-color: var(--panel); }
html[data-theme="light"] .cat-regv tr.off td { background: var(--panel-2); }


/* ═══════════════════════════════════════════════════════════════════════════
 * РЕГИОНЫ СТРАНИЦ (волна ux/v03, этап 1). Каждой дорожке — своё место, чтобы
 * правки восьми исполнителей не встречались в одной строке. Стили страницы,
 * которых ещё нет в файле, пишутся СЮДА, в свой регион, вместе со светлой
 * темой этой же страницы. Регион остаётся пустым, если дорожке хватило
 * существующих секций выше — это нормально.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* === К1-Б: каркас, вход, приглашение, профиль === */

/* ── Профиль ───────────────────────────────────────────────────────────────
   Колонки набираются сами и КАЖДАЯ ограничена по ширине. Раньше сетка была
   инлайновой строкой `1fr 1fr` в разметке: медиазапросы её не перебивали, и на
   390 px получались две колонки по ~170 px (сегмент языка рассыпался на
   одиннадцать строк), а на 1920 px карточка растягивалась до 810 px — подпись
   «Звук новых аварий» отстояла от своего тумблера почти на всю карточку.
   min(340px,100%) в нижней границе трека обязателен: без него на 390 px трек
   в 340 px + поля страницы вылезают за экран и дают горизонтальную прокрутку. */
.prof-grid {
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  align-items: start; gap: 14px; max-width: 1160px;
}
/* Шапка страницы кончается там же, где сетка карточек под ней (та же причина,
   что у .acc-head): край шапки, уехавший за край содержимого, читается как
   часть оболочки. */
.page-head.prof-head, .page-note.prof-head { max-width: 1160px; }
.prof-col { min-width: 0; }
/* Строка «подпись — контрол»: связь между ними держится близостью, поэтому
   ширина ограничена независимо от ширины карточки. */
.prof-row { justify-content: space-between; gap: 12px; padding: 6px 0; max-width: 520px; }
/* Тач-цель строки-тумблера на телефоне. Общее правило ≤860 px поднимает до
   44 px только <label> с ГАЛКОЙ прямым потомком, а у тумблера чекбокс лежит
   внутри <span class="switch">, и строка оставалась 38 px. Нажимается здесь
   вся строка, ей и задаём высоту цели. */
@media (max-width: 860px) {
  label.prof-row { min-height: var(--ctl-h-lg); align-items: center; }
}
.prof-kv { max-width: 520px; }
.prof-kv > :nth-child(even) { text-align: start; }
.prof-act { gap: 8px; margin-top: 14px; }
.prof-hint { line-height: 1.45; margin-bottom: 10px; max-width: var(--prose-max); }
.prof-act + .prof-hint { margin: 12px 0 0; }
/* Ключ второго фактора — длинная строка без пробелов: без разрыва по знаку
   она вылезала за карточку. */
.prof-key {
  background: var(--bg); padding: 9px 10px; border-radius: 8px;
  word-break: break-all; margin: 0 0 10px;
}

/* Лента уведомлений внутри карточки: своя прокрутка, чтобы длинный список не
   растягивал соседнюю колонку. */
.nt-body { padding: 0; max-height: 70vh; overflow: auto; }
.nt-row { padding: 9px 14px; border-bottom: 1px solid var(--line); align-items: flex-start; gap: 10px; }
.nt-row:last-child { border-bottom: 0; }
.nt-t { flex: 1; min-width: 0; line-height: 1.45; }
.nt-meta { gap: 8px; margin-top: 3px; }
/* Строка ведёт на объект своей платы. Правило .click в теме написано для строк
   таблицы (tr.click), а лента — список блоков: отклик на наведение и нажатие
   ей нужен тот же, иначе кликабельность видна только курсором. */
.nt-row.click { cursor: pointer; transition: background var(--dur-1) var(--ease); }
.nt-row.click:hover { background: var(--panel-2); }
.nt-row.click:active { background: var(--panel-3); }
/* Уже скрытые (их показывает переключатель): тон приглушён — они здесь как
   справка, а не как работа. */
.nt-off { color: var(--ink-2); }
.nt-off .nt-t > div:first-child { color: var(--ink-2); }
/* Шапка карточки с двумя органами управления: на узкой колонке они переезжают
   под заголовок, а не режут его. */
.card .card-h.nt-h { flex-wrap: wrap; row-gap: var(--sp-2); }
.nt-sw { gap: var(--sp-2); font-weight: 500; color: var(--ink-2); }
/* Список языка в строке профиля: ширина по слову, а не во всю строку. */
.prof-row .prof-lang { width: auto; max-width: 100%; }

/* ── «Ещё» (телефон) ──────────────────────────────────────────────────────
   Строка раздела — целиком тач-цель: подпись «рассчитан на большой экран»
   делает карточку двухстрочной, поэтому высота задаётся содержимым, а не
   фиксируется. */
.more-list { gap: 8px; }
/* Заголовок секции — тот же капс-моно, что у разделителя бокового меню: одна и
   та же структура парка на телефоне и на компьютере читается одинаково. */
.more-sec + .more-sec { margin-top: var(--sp-5); }
.more-sec-t {
  font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-cap);
  text-transform: uppercase; color: var(--ink-2); margin: 0 0 var(--sp-2);
}
.more-item {
  padding: 12px 14px; display: flex; align-items: center; gap: 12px;
  text-decoration: none; color: inherit; min-height: var(--ctl-h-lg);
}
.more-item:hover { border-color: var(--edge); text-decoration: none; }
.more-ic { width: 22px; height: 22px; display: grid; place-items: center; color: var(--brand); flex: 0 0 auto; }
.more-t { flex: 1; min-width: 0; }
.more-nm { font-weight: 600; }
.more-out { margin-top: 14px; }
.more-out-h { margin-top: 10px; line-height: 1.45; }

/* ── Реперная рейка длинной страницы (F-071, движок дорожки К1-Б) ──────────
   Идентичность продукта заявляет «реперные метки» приборной панели, но на
   страницах пульта их не было ни одной, и длинные экраны («Объект» ~1450 px,
   «Конфигуратор» ~1180 px) шли сплошным полотном: человек не понимал, сколько
   ещё ниже и на каком он разделе. Рейка решает три задачи одной деталью —
   делает сетку видимой, даёт разделам номера для устного указания («смотри
   блок 03») и добавляет приборности, не вводя ни одного нового цвета.

   КАК ПРИМЕНЯТЬ (контракт для К4 и К6):
       <div class="rail">
         <section class="rail-s">…</section>   ← номер 01, ставится сам
         <section class="rail-s">…</section>   ← 02
       </div>
   Номера считает CSS-счётчик: разделы можно переставлять и скрывать по роли,
   разметку править не нужно. Заголовок раздела обязан быть ПЕРВЫМ элементом
   .rail-s — засечка встаёт по его базовой линии.

   ГДЕ НЕ ПРИМЕНЯТЬ: короткие страницы (рейка на два блока — украшение) и
   «Журнал» — там страница длиной в 21 метр, ей нужна страничная выдача, а не
   шкала (CRITIQUE §4). На телефоне рейка выключается: 34 px отступа на 390 px
   стоят дороже, чем даёт нумерация. */
.rail { position: relative; counter-reset: rail; padding-inline-start: 34px; }
.rail::before {
  content: ""; position: absolute; inset-block: 6px 6px; inset-inline-start: 11px;
  width: 1px; background: var(--line-2);
}
.rail-s { position: relative; counter-increment: rail; }
/* Номер раздела: моноширь, капс-кегль шкалы — тот же приём, что у капс-подписей
   пульта. Цвет --ink-2, а не --ink-3: это ориентир, который читают глазами. */
.rail-s::before {
  content: counter(rail, decimal-leading-zero);
  position: absolute; inset-inline-start: -34px; top: 2px;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em; color: var(--ink-2);
  background: var(--bg); padding: 2px 0 3px; width: 23px; text-align: center;
}
/* Засечка — короткий штрих от рейки к разделу; у раздела с аварией она
   аварийного цвета: шкала заодно показывает, где на длинной странице беда. */
.rail-s::after {
  content: ""; position: absolute; inset-inline-start: -23px; top: 9px;
  width: 8px; height: 1px; background: var(--line);
}
.rail-s.alarm::before { color: var(--alarm-ink); }
.rail-s.alarm::after { background: var(--alarm); }
@media (max-width: 860px) {
  .rail { padding-inline-start: 0; }
  .rail::before, .rail-s::before, .rail-s::after { display: none; }
}

/* === К2: пульт парка === */

/* ── полоса «нет связи с пультом» ─────────────────────────────────────────
   Худший класс отказа операторского экрана — спокойная зелёная картина на
   данных получасовой давности: диспетчер видит «норму» и не знает, что смотрит
   на прошлое. Полоса липкая (относится ко ВСЕМ цифрам страницы сразу), несёт
   причину, момент среза и действие, а тело парка при ней приглушается —
   приглушённый экран сам по себе читается как «это не сейчас». */
.park-stale { position: sticky; top: 0; z-index: 30; }
.park-stale:empty { display: none; }
.park-stale .ps-in {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 12px; padding: 9px 13px;
  border: 1px solid var(--warn-line); border-radius: var(--r);
  background: var(--warn-soft); color: var(--warn-ink);
  box-shadow: var(--shadow);
  /* Фон обязан быть НЕПРОЗРАЧНЫМ: полоса стоит поверх прокручиваемого парка. */
  backdrop-filter: blur(10px);
}
.park-stale .ps-ic { display: inline-flex; flex: 0 0 auto; }
.park-stale .ps-t { font-size: var(--fs-body); line-height: 1.45; min-width: 0; }
.park-stale .ps-t b { color: var(--warn-ink); }
.park-stale .btn { margin-inline-start: auto; }
/* Устаревшая картина: цифры и плитки гасятся и обесцвечиваются. Это НЕ
   декоративный эффект — это признак «данные не живые», второй после полосы. */
.park-body.stale, .counters.stale { opacity: .55; filter: grayscale(.35); }

/* ── плашка массовой потери связи ─────────────────────────────────────────
   Появляется в самый напряжённый момент смены, поэтому собрана как строка с
   тремя ролями: знак — что за событие, текст — что случилось и какой из этого
   вывод, кнопка — что можно сделать. Абзац с инструкцией «нажмите сюда» кнопкой
   не является ни для мыши, ни для клавиатуры, ни для скринридера. */
.banner.park-storm { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.park-storm .st-ic { display: inline-flex; flex: 0 0 auto; color: var(--warn-ink); }
.park-storm .st-t { flex: 1 1 320px; min-width: 0; line-height: 1.45; }
.park-storm .btn { flex: 0 0 auto; margin-inline-start: auto; }

/* ── строка активных фильтров ──────────────────────────────────────────────
   Фильтр, обозначенный только подсветкой одной из плиток приборного ряда,
   человек теряет: он видит короткий список и решает, что объекты пропали.
   Метка называет фильтр словом и снимается одним нажатием. */
.park-filters { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.park-filters:empty { display: none; }
.park-filters .pf-cap {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2);
}
.park-filters .pf-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font: 600 var(--fs-micro) var(--mono); color: var(--ink);
  background: var(--panel-2); border: 1px solid var(--edge); border-radius: 999px;
  padding: 3px 8px 3px 11px; cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.park-filters .pf-chip:hover { border-color: var(--brand); background: var(--panel-3); }
.park-filters .pf-all {
  font: 600 var(--fs-micro) var(--mono); color: var(--brand);
  background: none; border: 0; padding: 3px 4px; cursor: pointer; text-decoration: underline;
}

/* ── пустые состояния внутри сетки плиток и ленты ── */
.park-empty { grid-column: 1 / -1; }
.park-feed .park-empty .empty { border: 0; }

/* ── карта: доводка vendor-слоя до темы ───────────────────────────────────
   Атрибуция OSM/CARTO приезжает из vendor-CSS чёрным по тёмной подложке
   (контраст ≈1,06:1). Лицензии обеих подложек требуют ВИДИМОЙ атрибуции, а
   невидимая её не даёт; плюс это был единственный чёрный текст на тёмном
   экране — тема просто не была доведена до чужого слоя. */
#park-map .maplibregl-ctrl-attrib,
#park-map .maplibregl-ctrl-attrib-inner { color: var(--ink-2); }
#park-map .maplibregl-ctrl-attrib { background: var(--panel-2) !important; border-radius: 6px 0 0 0; }
#park-map .maplibregl-ctrl-attrib a { color: var(--ink-2); text-decoration: underline; }
#park-map .maplibregl-ctrl-attrib a:hover { color: var(--ink); }
#park-map .maplibregl-ctrl-group { background: var(--panel-2); border: 1px solid var(--line); }
#park-map .maplibregl-ctrl-group button { color: var(--ink-2); }

/* Подписи маркеров: ширина ограничена, длинное имя срезается многоточием (а не
   краем карты), у краёв подпись прижимается внутрь, а при наложении гаснет —
   расстановку считает park.js::placeTags по важности статуса.
   visibility, а не display: скрытая подпись обязана сохранять размеры, иначе её
   прямоугольник нечем мерить на следующем кадре. */
.mk-tag { max-width: 160px; overflow: hidden; text-overflow: ellipsis; }
.mk-tag.off { visibility: hidden; }
.mk-tag.at-r { left: auto; right: -4px; transform: none; }
.mk-tag.at-l { left: -4px; right: auto; transform: none; }

/* Плашка «под фильтр не попал ни один объект»: полкарты без единого маркера и
   без слова объяснения читается как поломка. */
.pm-none {
  position: absolute; inset-inline: 0; top: 50%; transform: translateY(-50%); z-index: 2;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  margin: 0 auto; width: max-content; max-width: 90%;
  padding: 14px 18px; text-align: center;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow); color: var(--ink);
}

/* «Подложка не загрузилась» — та же плашка, но ВВЕРХУ карты: объекты при этом
   на месте, и закрывать их серединой экрана нельзя (J6-08). */
.pm-none.pm-tiles { top: 12px; transform: none; max-width: min(92%, 460px); }

/* Кнопка выхода из полноэкранной карты — только на телефоне (см. ниже). */
.pm-close { display: none; }

/* ── телефон: карта — полноэкранный лист, а не ловушка прокрутки ───────────
   На 390×844 карта начиналась на y≈955 (за фолдом), а список над ней имел
   собственную прокрутку и перехватывал вертикальный свайп — классический
   scroll trap. Бриф §4: мобильный парк — это список; карта остаётся доступной,
   но открывается листом поверх экрана и закрывается одной кнопкой. */
@media (max-width: 860px) {
  .park-body.map #park-map {
    position: fixed; inset: 0; z-index: 60;
    height: auto; min-height: 0; border: 0; border-radius: 0;
  }
  .park-body.map #park-col,
  .park-body.map .park-feed,
  .park-body.map .park-demo-note { display: none; }
  /* Слева: справа у карты живут её собственные кнопки масштаба (vendor), и
     кнопка «Закрыть» перекрывала бы их. */
  .park-body.map .pm-close {
    display: inline-flex; position: fixed; z-index: 61;
    top: calc(10px + env(safe-area-inset-top)); inset-inline-start: 10px;
  }
  .park-stale .ps-in { padding: 10px 12px; }
  .park-stale .btn { margin-inline-start: 0; }
  .park-filters { margin-top: 8px; }
}

/* ── поле поиска по парку ──────────────────────────────────────────────────
   Лупа слева говорит «это поиск» без слов; крестик справа очищает поле одним
   касанием. Ширина — не фиксированная: поле берёт остаток шапки, но не шире
   460 px (длинная пустая строка в шапке читается как незаполненная форма). */
.park-search { position: relative; display: flex; flex: 1 1 320px; max-width: 460px; min-width: 220px; }
.park-search .in { width: 100%; padding-inline: 34px 32px; }
.park-search .ps-mag {
  position: absolute; inset-inline-start: 11px; top: 50%; transform: translateY(-50%);
  display: inline-flex; color: var(--ink-2); pointer-events: none;
}
.park-search .ps-clr {
  position: absolute; inset-inline-end: 4px; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; cursor: pointer;
  background: none; border: 0; border-radius: var(--r-sm); color: var(--ink-2);
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.park-search .ps-clr:hover { color: var(--ink); background: var(--panel-3); }
.park-search .ps-clr[hidden] { display: none; }
/* Крестик от type=search у самого браузера — свой уже есть, второй не нужен. */
.park-search .in::-webkit-search-cancel-button { display: none; }

/* Пояснение к сводной строке приборного ряда: то, что раньше жило только в
   подсказке мыши. Появляется, лишь когда команды в пути есть. */
.c-hint {
  flex: 1 1 100%; margin-top: var(--sp-1);
  font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); line-height: 1.4;
}

/* ── замечания по парку: чипы по тяжести ───────────────────────────────────
   До волны все замечания были одинаковыми серыми кнопками в фиксированном
   порядке: «цикл перезагрузок» (плата не может стартовать) выглядел ровно так
   же, как «в ручном» (человек сам перевёл выход в ручное управление). Теперь
   тяжесть кодируется точкой, цветом и ПОРЯДКОМ, а число стоит отдельным
   бейджем — колонка чисел сканируется, не вчитываясь в слова. */
.ph-chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  /* Отступы ЛОГИЧЕСКИЕ: в арабской раскладке точка уезжает к правому краю
     вместе с макетом, и физический padding-left оставил бы её вплотную к обводу. */
  min-height: var(--ctl-h-sm); padding-block: 3px; padding-inline: 10px 6px; cursor: pointer;
  font: 600 var(--fs-micro) var(--sans); letter-spacing: var(--ls-tight);
  color: var(--ink); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.ph-chip .ph-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: var(--ink-2); }
.ph-chip b {
  font: 700 var(--fs-micro) var(--mono); font-variant-numeric: tabular-nums;
  min-width: 20px; padding: 1px 6px; text-align: center;
  border-radius: var(--r-pill); background: var(--panel-3); color: var(--ink);
}
.ph-chip:hover { border-color: var(--edge); background: var(--panel-3); }
.ph-chip.on { border-color: var(--brand); box-shadow: var(--glow-brand); }
.ph-chip.alarm { color: var(--alarm-ink); border-color: color-mix(in srgb, var(--alarm) 55%, transparent); background: var(--alarm-soft); }
.ph-chip.alarm .ph-dot { background: var(--alarm); }
.ph-chip.warn { color: var(--warn-ink); border-color: color-mix(in srgb, var(--warn-line) 80%, transparent); background: var(--warn-soft); }
.ph-chip.warn .ph-dot { background: var(--warn); }
.ph-chip.alarm b, .ph-chip.warn b { background: rgb(0 0 0 / .18); color: inherit; }

/* ── плитка: пустая витрина и «плата ещё не выходила на связь» ──────────────
   Пустая плитка без слова объяснения читается как поломка платы, а «нет связи ·
   никогда» — как обрывок фразы. Обе строки набраны тихо (это не тревога), но
   ВИДИМО. */
.obj-tile .ot-kvnone { font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); line-height: 1.35; }
.obj-tile .ot-new {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font: 600 var(--fs-micro) var(--sans); letter-spacing: var(--ls-tight); color: var(--ink-2);
}
/* Объект без плат ссылкой не притворяется: ни курсора-руки, ни подъёма. */
.obj-tile.nodev { cursor: default; }
.obj-tile.nodev:hover { transform: none; border-color: var(--line); }

/* ── лента: переключение выхода без чипа ───────────────────────────────────
   Чип «Событие» повторялся на каждой второй строке и не различал ничего.
   Различитель — значок вкл/выкл: он в четыре раза уже чипа и несёт РАЗНЫЕ
   состояния разной формой. */
.pf-io { display: inline-flex; margin-inline-end: 6px; vertical-align: -2px; }
.pf-io.on { color: var(--ok-ink); }
.pf-io.off { color: var(--ink-2); }

/* ── легенда меток парка ───────────────────────────────────────────────────
   Сюда переехали объяснения, которые жили в подсказках мыши (на планшете и с
   клавиатуры их не существовало вовсе). Одна строка на экран вместо повтора на
   каждой плитке; блок скрыт, пока соответствующих меток на экране нет. */
.pf-legend {
  display: flex; flex-direction: column; gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--line-2);
  font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); line-height: 1.45;
}
.pf-legend[hidden] { display: none; }

/* ── ступени плотности стены плиток ────────────────────────────────────────
   Вместо CSS-зума, который уменьшал ВСЁ, включая подписи на пороге читаемости
   (при 25 объектах они превращались в 5 px). Кегли не трогаются: плитка теряет
   содержимое, а не размер букв. */
.park-dens {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}
.park-dens[hidden] { display: none; }
.park-dens .pd-cap {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap);
  text-transform: uppercase; color: var(--ink-2);
}
/* «Плотно»: витрина значений и нижняя строка меток уходят — остаются имя,
   адрес, статус, причина и возраст, то есть всё, ради чего на стену смотрят. */
#park-list.d-dense .obj-tile { padding: 8px 11px 9px; }
#park-list.d-dense .obj-tile .ot-kv,
#park-list.d-dense .obj-tile .ot-foot { display: none; }
#park-list.d-dense .obj-tile .ot-meta { margin-top: 5px; gap: 6px; }
/* «Строки»: больше тридцати объектов плиткой на экран не положить. Полоса
   статуса · имя · причина · возраст в одну строку, сетка сама встаёт в 2–3
   колонки по ширине. */
#park-list.d-rows { grid-template-columns: repeat(auto-fill, minmax(460px, 1fr)); gap: var(--sp-1); }
#park-list.d-rows .obj-tile {
  flex-direction: row; align-items: center; flex-wrap: nowrap; gap: 4px 10px;
  /* Логические отступы: полоса статуса рисуется на inline-start, и её сосед
     обязан переезжать вместе с ней в арабской раскладке. */
  min-height: 28px; padding-block: 3px; padding-inline: 12px 10px;
  border-radius: var(--r-sm); box-shadow: none;
}
#park-list.d-rows .obj-tile .ot-ent,
#park-list.d-rows .obj-tile .ot-kv,
#park-list.d-rows .obj-tile .ot-foot,
#park-list.d-rows .obj-tile .ot-new,
#park-list.d-rows .obj-tile .ot-dot { display: none; }
/* Строка не переносится: перенесённая строка перестаёт быть строкой и рвёт
   сетку по высоте. Длинное имя режется многоточием, остальное — возраст и
   причина — держится в конце строки целиком. */
#park-list.d-rows .obj-tile .ot-name {
  padding-right: 0; line-height: 1.25; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#park-list.d-rows .obj-tile .ot-meta {
  margin-top: 0; margin-inline-start: auto; flex-wrap: nowrap; white-space: nowrap; flex: 0 0 auto;
}

/* ── карточка объекта на карте ─────────────────────────────────────────────
   Порядок строк отвечает вопросу, ради которого её открывают: ехать или нет.
   Имя → адрес → причина → значения → связь → действия. */
.maplibregl-popup .mkp { font: var(--fs-body)/1.45 var(--sans); color: var(--ink); }
.mkp .mkp-name { font: 600 14px var(--sans); color: var(--ink); line-height: 1.3; }
.mkp .mkp-ent { font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); margin-top: 1px; }
.mkp .mkp-why {
  display: block; margin-top: 5px;
  font: 600 var(--fs-micro) var(--sans); letter-spacing: var(--ls-tight);
}
.mkp .mkp-why.alarm { color: var(--alarm-ink); }
.mkp .mkp-why.warn { color: var(--warn-ink); }
.mkp .mkp-kvs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 2px 12px;
  margin-top: 7px; padding-top: 6px; border-top: 1px solid var(--line-2);
}
.mkp .mkp-kv { display: flex; flex-direction: column; min-width: 0; }
.mkp .mkp-kv i {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
  color: var(--ink-2); font-style: normal;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mkp .mkp-kv b { font: 600 var(--fs-body) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); white-space: nowrap; }
.mkp .mkp-kv b.warn { color: var(--warn-ink); }
.mkp .mkp-kv b.alarm { color: var(--alarm-ink); }
.mkp .mkp-link { margin-top: 6px; font: 500 var(--fs-micro) var(--mono); color: var(--ink-2); }
.mkp .mkp-sms { margin-top: 3px; font: 600 var(--fs-micro) var(--sans); color: var(--warn-ink); line-height: 1.35; }
.mkp .mkp-act {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: 8px; padding-top: 7px; border-top: 1px solid var(--line-2);
}
.mkp .mkp-feed {
  font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-tight);
  color: var(--ink-2); background: none; border: 0; padding: 0; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}
.mkp .mkp-feed:hover { color: var(--ink); }

/* ── светлая («бумажная») тема ──────────────────────────────────────────── */
html[data-theme="light"] .park-stale .ps-in { background: var(--warn-soft); border-color: var(--warn-line); }
html[data-theme="light"] .ph-chip.alarm b, html[data-theme="light"] .ph-chip.warn b { background: rgb(255 255 255 / .55); }
html[data-theme="light"] .obj-tile .ot-ack { background: var(--panel-2); }
html[data-theme="light"] .obj-tile .ot-cmd { background: var(--maint-soft); border-color: var(--line); }
html[data-theme="light"] .pf-tok { background: var(--panel-2); }
/* «Нет связи» на бумажной карте. Общий светлый обод маркера
   (html[light] .mk i) перебивал обод офлайн-маркера по специфичности, и полый
   маркер превращался в белый диск с белым ободом — на светлой подложке он
   исчезал совсем, то есть молчащий объект переставал существовать на карте.
   Форма («пустое кольцо») — носитель смысла, её нельзя терять вместе с цветом. */
html[data-theme="light"] #park-map .mk.offline i { border-color: var(--offline); }
html[data-theme="light"] #park-map .maplibregl-ctrl-attrib { background: var(--panel-2) !important; }
html[data-theme="light"] #park-map .maplibregl-ctrl-attrib,
html[data-theme="light"] #park-map .maplibregl-ctrl-attrib-inner,
html[data-theme="light"] #park-map .maplibregl-ctrl-attrib a { color: var(--ink-2); }
html[data-theme="light"] .pm-none { background: var(--panel); }

/* === К3: аварии, история и отчёты, график === */

/* ── выбор периода: ОДИН компонент на «Аварии», «Историю» и «Журнал» ──
   (period.js). Пресеты — сегментом, поля своей даты появляются только при
   «Свой период»: две системные даты в постоянном виде занимали место и
   заставляли вводить руками то, что в девяти случаях из десяти типовое. */
.pp { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pp-own { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.pp-lab {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-2);
}
.pp-own .in { max-width: 170px; }
/* Ошибка периода — рядом с полями, с выходом действием. Сервер на
   перевёрнутый период отвечает пустым списком, и экран называл неверную
   причину («Нет данных за период»): человек шёл искать неисправность в
   оборудовании вместо своей опечатки. */
.pp-err:empty { display: none; }
.pp-err {
  display: flex; align-items: center; gap: 8px; flex-basis: 100%;
  color: var(--alarm-ink); font-size: var(--fs-micro);
}

/* ── лента аварий ── */
.a-filters { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
/* Счётчик на вкладке: обе цифры (аварии и внимания) объяснены там, где их
   видно, — бейдж меню считает только аварии, и расхождение «в меню 2, на
   экране 4» подтачивало доверие к числам пульта. */
.seg button .tab-n { color: var(--ink-2); font-weight: 500; }
.seg button.on .tab-n { color: inherit; opacity: .8; }
.a-tbl { overflow-x: auto; }
/* Имя объекта — первичный ключ строки, и рвать его на «Насосная 2-» + «го
   подъёма» нельзя: столбец получает нижнюю границу ширины. На телефоне
   ограничение снято — там строка становится карточкой. */
.a-obj { min-width: 170px; }
@media (max-width: 860px) { .a-obj { min-width: 0; } }

/* Код события рядом с человеческим именем: «Перегрев» словом, PEREGREV —
   вторичной моношрифтовой подписью. Токен нужен, чтобы сверить строку экрана
   со строкой СМС и с правилом в конфигураторе, но он не заголовок. */
.evt-code {
  display: inline-block; margin-inline-start: 7px; padding: 1px 5px;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em;
  color: var(--ink-2); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 5px; vertical-align: 1px;
}
.evt-val { margin-top: 3px; }

/* Отметка «взято в работу»: вторичный чип и действие рядом. Это КООРДИНАЦИЯ,
   а не статус аварии, поэтому чип нейтральный и не спорит с красным. */
.ack-on { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ack-on .ack-t { margin-inline-start: 6px; color: var(--ink-2); font-weight: 500; }

/* Сводка смены под заголовком: сколько, где и что висит дольше всех. Моно и
   вторичным цветом — это подпись к ленте, а не второй заголовок. Своего
   компонента не заводим: .page-head .sub держит одну короткую строку без
   переноса, а здесь строка длинная и живая. */
.a-sum {
  margin: -4px 0 var(--sp-4); font: 500 var(--fs-body) var(--mono);
  color: var(--ink-2); line-height: 1.5;
}
.a-sum:empty { display: none; }
.a-sum-old { color: var(--ink); }

/* Панель фильтров: подпись СНАРУЖИ поля (в списке остаются только значения).
   Полоса тянется на всю ширину карточки: колонка .a-filters прижимает детей по
   контенту, и полоса получала ширину «по узкой раскладке» (453 px) независимо
   от экрана — поле поиска в неё не помещалось и переносилось под чужую подпись. */
.a-frow { gap: var(--sp-2); flex-wrap: wrap; align-items: center; align-self: stretch; }
.a-frow .in { max-width: 260px; }
/* Пара «подпись + поле» — неразрывный блок переноса: подпись физически не может
   оторваться от своего поля, куда бы полоса ни перенеслась. */
.f-pair { display: inline-flex; align-items: center; gap: var(--sp-2); max-width: 100%; }
/* Поле поиска шире общего правила полосы: ширину задаёт самая длинная подсказка
   из десяти языков (арм. 269 px), иначе «объект, предприятие, канал…» обрезается
   посередине слова и строка не дочитывается. */
.a-frow .a-q-in { min-width: 290px; max-width: 100%; }
/* Что отрезано от ленты — чипами с крестиком. Полоса пропадает, когда фильтров
   нет: пустой ряд читался бы как «фильтр стоит, но какой — неизвестно». */
.a-chips { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.a-chips:empty { display: none; }
.f-chip { gap: var(--sp-1); padding-inline-end: var(--sp-1); max-width: 32ch; }
.f-chip .f-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; border: 0; border-radius: var(--r-pill);
  background: transparent; color: var(--ink-2); cursor: pointer;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.f-chip .f-x:hover { color: var(--ink); background: var(--panel-3); }

/* ── пульс смены: ритм последних 12 часов над лентой (F-145) ──
   Риска = событие, высота = длительность, правый край = «сейчас». Полоса живёт
   в шапке карточки ленты и выходит на всю её ширину. */
.a-pulse { padding: var(--sp-3) var(--sp-4) var(--sp-2); border-bottom: 1px solid var(--line-2); }
.a-pulse:empty { display: none; }
/* Ось времени рисуется слева направо в любой раскладке — как и все графики
   продукта (chart.js). В RTL полоса зеркалилась бы, а подписи «12 ч назад» и
   «сейчас» менялись местами с рисками: у арабского читателя «сейчас» стояло
   слева, а пульсирующая вертикаль «сейчас» — справа. */
.a-pulse-svg { display: block; width: 100%; height: 28px; overflow: visible; direction: ltr; }
.a-pulse-ax {
  display: flex; direction: ltr; margin-top: var(--sp-1);
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap);
  text-transform: uppercase; color: var(--ink-2);
}
.a-pulse-ax .spacer { flex: 1; }
/* Полоса растянута по ширине карточки (preserveAspectRatio: none в alarms.js),
   поэтому ВСЕ штрихи на ней рисуются с non-scaling-stroke: иначе одна и та же
   риска выходит волоском на телефоне и жирной чертой на 1920. */
.a-pl-grid { stroke: var(--line-2); stroke-width: 1; vector-effect: non-scaling-stroke; }
.a-pl-grid.h3 { stroke: var(--line); }
.a-pl {
  cursor: pointer; transition: opacity var(--dur-1) var(--ease);
  stroke-width: 3; stroke-linecap: round; vector-effect: non-scaling-stroke;
}
.a-pl.alarm { stroke: var(--alarm); }
.a-pl.warn { stroke: var(--warn); }
/* Снятая авария — та же риска, но приглушённая: смена помнит и её, однако
   внимания она больше не требует. */
.a-pl.done { opacity: .55; }
.a-pl:hover, .a-pl.hot { opacity: 1; filter: brightness(1.25); }
.a-pl-now { stroke: var(--brand); stroke-width: 1.5; vector-effect: non-scaling-stroke; animation: pl-now 2s var(--ease) infinite; }
@keyframes pl-now { 50% { stroke-opacity: .35; } }
/* Метка «сейчас» дышит ради живости прибора, а не ради смысла: тому, кто
   попросил систему убрать движение, она обязана просто стоять. Линия при этом
   остаётся на месте — гасим анимацию, а не элемент. */
@media (prefers-reduced-motion: reduce) { .a-pl-now { animation: none; } }
/* Подсветка строки, связанной с риской под курсором. */
table.t tbody tr.hot td { background: var(--panel-3); }

/* Разделитель суток в архиве: главный ориентир при разборе ленты. */
table.t tr.day-sep { height: auto; }
table.t tr.day-sep td {
  padding: var(--sp-4) var(--sp-4) var(--sp-2); border-bottom: 1px solid var(--line-2);
  background: transparent; font: 600 var(--fs-tag) var(--mono);
  letter-spacing: var(--ls-cap); text-transform: uppercase; color: var(--ink-2);
}
/* Зебра считает строки, а разделитель — не строка данных: без сброса фона она
   перекрашивала бы половину лент. */
:where(table.t tbody tr.day-sep:nth-child(even)) td { background: transparent; }
:where(table.t tbody tr.day-sep:hover) td { background: transparent; }

/* Шеврон строки: виден без наведения (на тач-экране hover не существует). */
.th-x, .td-x { width: 1%; }
.a-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0; border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--ink-2); cursor: pointer;
  transition: color var(--dur-1) var(--ease), transform var(--dur-2) var(--ease);
}
.a-x:hover { color: var(--brand); }
.a-x[aria-expanded="true"] { transform: rotate(180deg); color: var(--brand); }

/* Раскрытая авария: нитка решений и тренд вокруг момента срабатывания. */
table.t tr.a-open td { background: var(--panel-2); }
table.t tr.a-det-row td { background: var(--panel-2); padding: 0 var(--sp-4) var(--sp-4); }
:where(table.t tbody tr.a-det-row:nth-child(even)) td { background: var(--panel-2); }
.a-det { display: grid; gap: var(--sp-3); max-width: 720px; }
.a-thread { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.a-thread li {
  display: flex; align-items: baseline; gap: var(--sp-3);
  padding-inline-start: var(--sp-4); border-inline-start: 2px solid var(--line);
}
.a-thread li.alarm { border-inline-start-color: var(--alarm); }
.a-thread li.warn { border-inline-start-color: var(--warn); }
.a-thread li.ok { border-inline-start-color: var(--ok); }
.a-det-act { gap: var(--sp-2); flex-wrap: wrap; }
.a-det-tr { max-width: 520px; }

/* Архив: чип-контур без заливки и свечения. Сто закрытых событий, горящих тем
   же красным, что текущая авария, обесценивают красный — а на нём держится
   пульт парка. */
.a-tbl.arch .chip.alarm, .a-tbl.arch .chip.warn {
  background: transparent; box-shadow: none; text-shadow: none;
}
.a-tbl.arch .chip.alarm { color: var(--alarm-ink); border-color: var(--alarm-edge); }
.a-tbl.arch .chip.warn { color: var(--warn-ink); border-color: var(--warn-line); }
.a-tbl.arch .chip .dot { box-shadow: none; }
/* В светлой теме активный аварийный чип залит «сургучом», и точка на нём —
   белая. На контурном архивном чипе белая точка исчезает: возвращаем ей цвет
   статуса (правило адресное, общий компонент не трогаем). */
html[data-theme="light"] .a-tbl.arch .chip.alarm .dot { background: var(--alarm); }
.a-hint { padding: var(--sp-3) var(--sp-4); margin: 0; }

/* ── хвост смены: что уже погасло ──
   Пока активных аварий мало, под лентой оставалась пустая половина экрана, и
   экран не предлагал ничего — хотя первый вопрос заступающей смены («что тут
   было») отвечается именно этими строками. Блок тихий: без чипов и свечений,
   он не спорит с лентой над собой. */
.a-tail { padding: var(--sp-4); border-top: 1px solid var(--line-2); }
.a-tail-h {
  display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-2);
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap);
  text-transform: uppercase; color: var(--ink-2);
}
.a-tail-h .spacer { flex: 1; }
.a-tail-l { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.a-tail-l li {
  display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap;
  font-size: var(--fs-body); color: var(--ink-2);
}
.a-tail-l li b { color: var(--ink); }

/* ── история и отчёты ── */
/* Панель шапки: ряд контролов и под ним период. Раньше «охват», две даты и
   «шаг» стояли в одну строку с кнопкой, и на 390 px подписи отрывались от
   своих полей. */
.h-panel { display: flex; flex-direction: column; gap: 10px; }
.h-panel > .row-g { width: 100%; }
/* Две зоны шапки: слева параметры запроса, справа неразрывный блок действий.
   Пока всё лежало одним flex-wrap, «Печать» уезжала на вторую строку в
   одиночестве, а состав второй строки менялся от отчёта к отчёту — панель
   выглядела каждый раз иначе. */
.h-row { gap: var(--sp-4); align-items: center; flex-wrap: wrap; }
.h-par { gap: var(--sp-2); align-items: center; flex-wrap: wrap; flex: 1 1 auto; min-width: 0; }
.h-act { gap: var(--sp-2); flex-shrink: 0; margin-inline-start: auto; }
/* Охват: в дереве из четырёх уровней подпись обрывалась ровно там, где
   начиналось имя платы. min() — чтобы на телефоне поле не вылезало за экран. */
.h-scope { max-width: min(420px, 100%); }
/* Выбор отчёта — сегмент. Семь пунктов на узком экране переносятся по
   строкам, а не уезжают за край. */
.rep-seg { margin-bottom: 4px; }
/* Семь имён отчётов — самый длинный ряд сегментов в продукте: на 1280 px он
   переносится по строкам, и лишние поля внутри плиток стоят целой строки. */
.rep-seg button { padding-inline: var(--sp-3); }
/* Тонкая черта перед первой плиткой смысловой группы: работа оборудования /
   события / техника. Отделяет пачки, не рисуя ещё одну рамку. */
.rep-seg button.grp { border-inline-start: 1px solid var(--line); margin-inline-start: var(--sp-2); }
/* Перенос строки по границе группы: до 1500 px семь имён в строку не влезают,
   и без этого разделителя последняя группа рвалась посередине — «Состояние
   парка» висело сиротой во второй строке. Шире 1500 px ряд идёт одной
   строкой, и разделитель выключен. */
.rep-seg .seg-break { flex-basis: 100%; height: 0; }
@media (min-width: 1500px) { .rep-seg .seg-break { display: none; } }
/* Итоговая строка отчёта: то, ради чего таблицу читают снизу вверх. */
table.t tfoot tr.rep-total td {
  border-top: 1px solid var(--line); border-bottom: 0;
  font-weight: 700; color: var(--ink); background: var(--panel-2);
}
table.t tfoot tr.rep-total td.v { font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* Хорошая новость («ни одной перезагрузки») — словами, а не таблицей нулей. */
.rep-good { margin-bottom: var(--sp-3); }
/* На телефоне таблица становится колонкой карточек, и <tfoot> по порядку в
   разметке вставал ПЕРЕД строками: отчёт открывался итогом, а из чего он
   сложился — ниже. Порядок восстанавливаем flex-ом, а не переносом итога в
   разметке: на десктопе и на бумаге он обязан остаться строкой таблицы. */
@media (max-width: 860px) {
  #rep-body table.t:has(> tfoot) { display: flex; flex-direction: column; }
  #rep-body table.t > tbody { order: 1; }
  #rep-body table.t > tfoot { order: 2; display: block; width: 100%; }
  #rep-body table.t > tfoot tr { display: block; width: 100%; }
  #rep-body table.t > tfoot td { display: block; width: 100%; }
  #rep-body table.t > tfoot td:empty { display: none; }
}

/* ── предпросмотр печатного листа (F-144, F-147) ──
   Экран существует ради бумаги, а бумагу на нём было не видно: шапка отчёта и
   место для подписи жили только в @media print. Предпросмотр показывает тот же
   лист — поля, ширина страницы, шапка, подпись, — и печать перестаёт быть
   прыжком в неизвестность. */
.rep-pbar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: var(--sp-3); padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--brand); border-radius: var(--r);
  background: var(--brand-soft); color: var(--ink-2);
}
.rep-pbar:empty { display: none; }
.rep-pbar .spacer { flex: 1; }
/* Лист: ширина страницы A4 при обычном масштабе, поля как у печати. В тёмной
   теме тот же лист, только приглушённый, — «бумага» не должна слепить в
   диспетчерской, где смотрят на экран всю смену. */
.rep-sheet {
  max-width: 820px; margin-inline: auto;
  box-shadow: var(--elev-2); border-color: var(--line);
}
.rep-sheet > .card-b { padding: var(--sp-6) 56px; }
.rep-sheet .print-head { display: block; margin-bottom: var(--sp-5); padding-bottom: var(--sp-3); border-bottom: 2px solid var(--ink); }
.rep-sheet .print-sign { display: block; margin-top: var(--sp-6); padding-top: var(--sp-3); border-top: 1px solid var(--line); color: var(--ink-2); }
/* На листе таблица — чертёж, а не прибор: свечения и заливки статусов уходят,
   остаются чернила и рамка (ровно то, что доживёт до бумаги). */
.rep-sheet .chip { box-shadow: none; text-shadow: none; background: transparent; }
/* Лист не прокручивается вбок: колонка, уехавшая за правое поле, на бумаге
   просто исчезает. Поэтому в предпросмотре таблица плотнее — ровно так же
   она встанет и на печати. */
.rep-sheet .tbl-x { overflow-x: visible; }
.rep-sheet table.t { font-size: var(--fs-body); }
.rep-sheet table.t th, .rep-sheet table.t td { padding: var(--sp-2) var(--sp-3); }
.rep-sheet table.t tbody tr { height: auto; }
.rep-sheet .rep-more { display: none; }
@media (max-width: 860px) {
  .rep-sheet { max-width: none; }
  .rep-sheet > .card-b { padding: var(--sp-4); }
}
/* Бумага. Общий блок @media print выше красит палитру и прячет оболочку; здесь
   лист отчёта доводится до документа: панель параметров и переключатель
   отчётов на бумаге не нужны (охват и период названы в шапке), таблица не
   прокручивается вбок (уехавшая колонка на бумаге просто исчезает), а полоса
   предпросмотра — экранная, её на листе быть не должно. */
@media print {
  .rep-pbar, .a-hint, .td-x, .th-x { display: none !important; }
  .h-panel, .rep-seg { display: none !important; }
  .card:has(> .h-panel) { display: none !important; }
  /* Липкая шапка страницы на бумаге прилипала к первой строке листа и
     наезжала на заголовок отчёта. Имя раздела на документе и не нужно: лист
     называет себя сам («kotomori — отчёт …»). Правило намеренно сужено до
     страницы отчётов — общий компонент .page-head за собой не тянем. */
  #page:has(#rep-body) .page-head { display: none !important; }
  .tbl-x, .a-tbl { overflow: visible !important; }
  table.t { font-size: var(--fs-body); }
  table.t th, table.t td { padding: 6px 8px; }
  table.t tbody tr { height: auto; }
  #rep-body { max-width: none; border: 0; }
  #rep-body > .card-b { padding: 0; }
  table.t tr.day-sep td { padding-top: 12px; }
}

/* ═══════════ ПЕЧАТНЫЙ СЛОЙ: ЛИСТ A4 — НЕ УЗКИЙ ЭКРАН (F-147) ═══════════════
   Проверено настоящим PDF (не кадром в media:print): лист A4 в печатных
   единицах — 794 px, то есть УЖЕ порога 860 px, на котором таблицы продукта
   сознательно рассыпаются в карточки для телефона. На бумагу уходил телефонный
   вид: отчёт «Наработка и расход» на десять строк разворачивался парами
   «ПОДПИСЬ → значение» на четыре страницы, «Входы · 8» во вкладке «Подробно» —
   на две. Сверить строку такого листа со строкой выгрузки CSV нельзя, а именно
   ради сверки отчёт и печатают. Правило снимает карточную деградацию ТОЛЬКО на
   печати; экранное поведение ≤860 px не меняется ни на пиксель.
   ⚠ !important здесь неизбежен: снимаемые правила живут в @media (max-width)
   с той же специфичностью, и на печати совпадают ОБА медиа-условия сразу. */
@media print {
  /* Общий печатный блок выше переопределяет всю палитру, КРОМЕ «ручного
     приоритета»: при печати из ночной темы --manual уходил на бумагу светлой
     сиренью #b98cff (≈1,9:1 на белом), и чипы «Насос сет · вручную», обод
     пилюли на схеме и точка легенды «ручной» на листе не читались вовсе.
     Берём дневное значение того же токена — оно и рассчитано на светлую
     подложку. Правило печатное, экрана не касается; постоянное место этой
     строки — печатная палитра дорожки К1-А1 (передано в отчёте). */
  :root, html[data-theme="light"] { --manual: #6b40c4; --manual-soft: transparent; }

  table.t { display: table !important; width: 100%; }
  /* Шапка таблицы повторяется на каждой странице: в журнале на четыре листа
     без повторной шапки читатель третьей страницы не знает, что в колонках. */
  table.t > thead {
    display: table-header-group !important;
    position: static; width: auto; height: auto; margin: 0; overflow: visible; clip-path: none;
  }
  table.t > tbody { display: table-row-group !important; }
  table.t > tfoot { display: table-footer-group !important; }
  table.t > tbody > tr, table.t > tfoot > tr {
    display: table-row !important; height: auto;
    border: 0; border-radius: 0; background: transparent; margin: 0; padding: 0;
    /* Строка данных не рвётся между листами: половина строки на обороте — это
       уже не документ. */
    break-inside: avoid;
  }
  table.t > tbody > tr > td, table.t > tfoot > tr > td { display: table-cell !important; width: auto; }
  /* Подпись колонки в ячейке — костыль карточного вида; на листе она снова в
     шапке, и второй раз печатать её в каждой клетке значит утроить лист. */
  table.t > tbody > tr > td[data-th]::before { content: none; }
  /* Лист не прокручивается вбок: колонка, не поместившаяся в ширину страницы,
     на бумаге просто исчезает — во вкладке «Подробно» так пропадали столбцы
     «Обратная связь» и «Управление» у восьмиколоночных таблиц каналов.
     Лечим ПЛОТНОСТЬЮ: поля клеток вдвое уже, а таблицы от семи колонок
     набираются на кегль мельче. Через `overflow-wrap: anywhere` не лечим —
     проверено кадром: это правило входит в расчёт МИНИМАЛЬНОЙ ширины колонки,
     ширины схлопываются до одного знака, и лист выходит рваным насквозь
     («КЛЕ ММА», «нор ма», «0,12 ба р»). break-word оставлен страховкой на
     случай по-настоящему длинного значения — на ширину колонки он не влияет. */
  table.t th, table.t td { padding: 4px 6px; overflow-wrap: break-word; }
  table.t:has(> thead > tr > th:nth-child(7)) { font-size: var(--fs-micro); }
  /* Итог отчёта на телефоне переставляется flex-ом под строки; на бумаге он
     обязан остаться строкой таблицы (селектор с id — чтобы перебить тот). */
  #rep-body table.t:has(> tfoot) { display: table !important; }

  /* Карточка с длинной таблицей ОБЯЗАНА разрываться. `break-inside: avoid` на
     всех .card (общий печатный блок выше) уводил четырёхстраничный отчёт
     целиком на следующий лист — и первая страница документа выходила из
     принтера ПУСТОЙ. Разрывы держим на строках таблицы, а не на карточках. */
  .card { break-inside: auto; }
  /* Заголовок не остаётся один в конце листа, оторванный от своей таблицы. */
  .card > .card-h, .page-head { break-after: avoid; }

  /* Всё, что прокручивается на экране, на бумаге ОБРЕЗАЕТ содержимое: уехавшая
     за правое поле колонка на листе просто исчезает, и заметить пропажу
     нельзя. */
  .tbl-x, .a-tbl, .schm-card, .obj-log, .ch-box { overflow: visible !important; }

  /* Экранные органы без смысла на бумаге: ссылка «Перейти к содержимому»
     печаталась поверх текста на КАЖДОЙ странице (она fixed), «Все снятые →» и
     «Отчёты →» на листе никуда не ведут. */
  .skip, .more-link { display: none !important; }
  /* Столбец шеврона «развернуть строку» — орган управления. Скрытие живёт выше
     по файлу, но правило `display: table-cell` этого блока сильнее и вернуло
     бы пустой столбец с шевронами на бумагу: повторяем скрытие сильнее. */
  table.t > thead > tr > th.th-x, table.t > tbody > tr > td.td-x { display: none !important; }

  /* Вкладка печатается ОДНИМ словом. Пустая полоса переключателя на листе
     ничего не сообщает, а вот КАКАЯ половина ленты напечатана — «Активные» или
     «Снятые», «Схема» или «Подробно» — обязано быть на бумаге: без этого лист
     не отвечает, что на нём. Оставляем только выбранную кнопку и снимаем с неё
     вид органа управления. */
  .seg { border: 0; background: none; padding: 0; }
  .seg button:not(.on) { display: none !important; }
  .seg button.on {
    background: none; box-shadow: none; color: #000;
    padding: 0; min-height: 0; font-weight: 700;
  }

  /* ── «Аварии» на бумаге: журнал смены, а не снимок экрана ──
     Панель фильтров — это органы управления, на листе от них остаются пустое
     поле поиска и выпадашка «любой». А ЧТО именно отрезано от ленты, лист
     говорит словами: сводка под заголовком и чипы активных фильтров. Если
     фильтров нет, чипов нет — и вся карточка на бумаге лишняя. */
  .a-filters > .pp, .a-filters > .a-frow { display: none !important; }
  .card:has(.a-chips:empty) { display: none !important; }
  /* Пульс смены остаётся на листе: заступающая смена читает бумагу ровно ради
     ответа «как прошли эти двенадцать часов», и полоса отвечает на него за
     секунду. На бумаге она статична по определению — гасим только пульсацию
     метки «сейчас» и подложку риски приводим к чернилам. */
  .a-pulse { border-bottom: 1px solid #ccc; break-inside: avoid; }
  .a-pl-now { animation: none !important; stroke: #000; }
  .a-pl.done { opacity: .45; }
  .a-pl-grid { stroke: #ccc; }
}

/* Архивный чип отчёта: событие в отчёте давно кончилось — гореть ему незачем
   (та же логика, что во вкладке «Снятые» на «Авариях»). */
#rep-body .chip.alarm, #rep-body .chip.warn { box-shadow: none; text-shadow: none; background: transparent; }
#rep-body .chip.alarm { color: var(--alarm-ink); border-color: var(--alarm-edge); }
#rep-body .chip.warn { color: var(--warn-ink); border-color: var(--warn-line); }
#rep-body .chip .dot { box-shadow: none; }
html[data-theme="light"] #rep-body .chip.alarm .dot { background: var(--alarm); }
/* Полоса усечения над таблицей отчёта: «журнал за месяц», обрезанный до 500
   строк без единого слова об этом, как доказательный документ недостоверен. */
.rep-cut { margin-bottom: 10px; display: block; }
.rep-cut .small { color: inherit; opacity: .85; margin-top: 2px; }
.rep-note { margin-top: 10px; }
/* Таблица отчёта: на телефоне работает карточная раскладка table.t, на
   десктопе — обычная прокрутка внутри своего контейнера. */
.tbl-x { overflow-x: auto; }
.ch-box { max-width: 660px; }
/* Отклонение от паспорта: цвет «внимания», не аварии. Красный в этом продукте
   зарезервирован под аварию, и отчёт, красящий исправный насос в аварийный
   цвет, обучает игнорировать красное. */
.norm-off { color: var(--warn-ink); font-weight: 600; }
.num-bad { color: var(--alarm-ink); font-weight: 600; }
.num-warn { color: var(--warn-ink); font-weight: 600; }
/* Подпись под графиком — заголовок и ось: диаграмма без подписи оси не
   отвечает на вопрос «чего именно столько». */
.ch-title { font: 600 var(--fs-micro) var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); margin: 4px 0 2px; }
.ch-axis { font-size: var(--fs-micro); color: var(--ink-2); margin-top: 2px; }
/* Легенда полосы присутствия — три состояния, а не два: «данных ещё не было»
   больше не красится цветом разрыва связи. */
/* Образцы легенды обязаны совпадать с заливкой полосы ТОЧНО: presentation-
   атрибут fill в SVG не понимает var(), поэтому цвета полосы в chart.js —
   литералы, и легенда повторяет ровно их. Разошедшийся образец хуже, чем
   его отсутствие: он объясняет не то, что нарисовано. */
.pb-legend { display: flex; gap: 14px; flex-wrap: wrap; margin: 4px 0 12px; font-size: var(--fs-micro); color: var(--ink-2); }
.pb-legend i { display: inline-block; width: 14px; height: 10px; border-radius: 2px; margin-inline-end: 5px; vertical-align: -1px; }
.pb-legend i.up { background: #35c98f; }
.pb-legend i.down { background: #616c65; }
.pb-legend i.none { background: #131a1d; box-shadow: inset 0 0 0 1px var(--line); }
html[data-theme="light"] .pb-legend i.up { background: #108457; }
html[data-theme="light"] .pb-legend i.down { background: #a5a196; }
html[data-theme="light"] .pb-legend i.none { background: #dad7ce; }
.rep-dev { font-weight: 600; margin: 10px 0 2px; }
.rep-dev a { text-decoration: none; color: inherit; }

@media (max-width: 860px) {
  /* На телефоне сегмент периода переносится по строкам, а поля своей даты
     встают в колонку: подпись обязана оставаться у СВОЕГО поля. */
  .pp-own { width: 100%; display: grid; grid-template-columns: max-content 1fr; row-gap: 6px; }
  .pp-own .in { max-width: none; width: 100%; }
  /* На телефоне полоса фильтров встаёт колонкой — той же сеткой, что и поля
     своей даты выше: подпись в первом столбце, поле во втором, поля обоих
     фильтров начинаются от одной линии. Пары раскрываются в сетку (display:
     contents), иначе каждая считала бы ширину подписи по себе и поля разъезжались.
     Нижняя граница 290 px здесь снята — она дала бы горизонтальную прокрутку. */
  .a-frow { display: grid; grid-template-columns: max-content 1fr; row-gap: 6px; column-gap: var(--sp-2); width: 100%; }
  .f-pair { display: contents; }
  .a-frow .in, .a-frow .a-q-in { max-width: none; min-width: 0; width: 100%; }
  /* Ось «пульса смены» на 390 px в одну строку не влезает, и подпись рвалась
     между словом и часом («12 Ч НАЗАД ·» / «05:45») — то есть в самом
     неудачном месте. Концы оси держатся целиком и остаются у своих краёв, а
     счётчик событий уходит на вторую строку: он не привязан к точке времени. */
  .a-pulse-ax { flex-wrap: wrap; }
  .a-pulse-ax > span { white-space: nowrap; }
  .a-pulse-ax > span:nth-child(3) { order: 5; flex: 1 0 100%; text-align: center; margin-top: 2px; }
  /* В карточной раскладке шеврон занимает свою строку — значит, он обязан быть
     полноценной тач-целью, а не значком 28 px посреди пустой полосы. */
  .td-x { width: auto; }
  .td-x .a-x {
    width: 100%; min-height: var(--ctl-h-lg);
    border: 1px solid var(--line); border-radius: var(--r-sm);
  }
}

/* Светлая («бумажная») тема: чип кода события на белом листе — только рамка,
   заливка панели там читается как выделение. */
html[data-theme="light"] .evt-code { background: transparent; border-color: var(--line); }

/* === К4: объект — схема и faceplate === */

/* ── Карточка схемы: признак того, что рисунок шире экрана (§F-151) ──
   Мнемосхема КНС не влезает в 390 px никаким масштабом, и это законно — но
   человек обязан ЗНАТЬ, что её можно тянуть. Кромочная тень появляется САМА
   ровно тогда, когда есть куда прокручивать: два слоя фона прибиты к
   содержимому (background-attachment: local) и «съезжают» вместе с ним,
   открывая тень под собой. Никакого JS и никаких замеров.
   Раньше вместо этого стоял жёсткий min-width:680 на самом рисунке. */
.schm-card {
  padding: 8px; overflow: auto;
  background-image:
    linear-gradient(to right, var(--panel), rgba(0,0,0,0)),
    linear-gradient(to left,  var(--panel), rgba(0,0,0,0)),
    linear-gradient(to right, rgba(0,0,0,.42), rgba(0,0,0,0)),
    linear-gradient(to left,  rgba(0,0,0,.42), rgba(0,0,0,0));
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-size: 26px 100%, 26px 100%, 13px 100%, 13px 100%;
  background-attachment: local, local, scroll, scroll;
}
.schm-pan-note { padding: 4px 6px 0; }
@media (min-width: 861px) { .schm-pan-note { display: none; } }

/* ── Рисунок схемы всегда читается слева направо (F-179) ──
   Сама мнемосхема НЕ зеркалится: газовый клапан слева, теплосеть справа — это
   технологический порядок, а не порядок письма. А вот текст ВНУТРИ рисунка
   наследовал направление интерфейса, и в арабской раскладке `text-anchor:start`
   означал правый край: подпись канала оказывалась справа, значение — слева, при
   потоке слева направо. В кадре сходились три направления чтения сразу.
   Направление текстовых узлов холста фиксируем по рисунку; сами слова
   (арабские, ивритские) внутри своей строки раскладывает bidi-алгоритм, он от
   этого не страдает. Окружающий интерфейс — тулбар, легенда, строка прокрутки —
   остаётся зеркальным: у них направление своё, по языку. */
.schm text { direction: ltr; }

/* ── Тулбар редактора: знак + слово, три группы (F-174) ──
   Семь одинаковых по весу текстовых кнопок читались как список слов, и смысл
   каждой жил только в подсказке. Значок из ICONS даёт узнавание с одного
   взгляда, разделитель — границу смысловых групп («чем рисую» | «что добавляю
   и убираю» | «шаги истории»). */
.schm-t { display: inline-flex; align-items: center; gap: var(--sp-2); }
/* Высота задана числом, а не stretch: полоса инструментов переносится по
   строкам, и растянутый по высоте ряда разделитель на переносе превращался бы
   в линию во всю строку. */
.schm-tsep {
  width: 1px; height: var(--ctl-h-sm); margin-inline: var(--sp-1);
  background: var(--edge); flex: 0 0 auto; align-self: center;
}

/* ── Легенда: заголовок-раскрыватель (F-177) ──
   Строка обозначений учит одному и тому же при каждом открытии объекта.
   Свёрнутая, она остаётся видимой строкой-кнопкой с числом пунктов — то есть
   человек знает, что объяснение здесь, но не читает его в сорок первый раз. */
.schm-legend-wrap { padding: var(--sp-1) 6px 0; }
.schm-legend-t {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  background: none; border: 0; padding: var(--sp-1) 0; cursor: pointer;
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
  color: var(--ink-2);
}
.schm-legend-t:hover { color: var(--ink); }
.schm-legend-t svg { transition: transform var(--dur-1) var(--ease); }
.schm-legend-t[aria-expanded="true"] svg { transform: rotate(180deg); }
.schm-legend-wrap.open .schm-legend { padding-top: var(--sp-1); }

/* ── «Сначала причина»: выделение аварийной цепочки (F-180) ──
   В аварии экран должен отвечать на один вопрос — «откуда это и что с ним
   связано». До волны схема светилась вся одинаково громко: источник, его
   трубы и весь остальной объект, — и диспетчер сканировал картинку глазами.
   Нажатие на строку в «Активных авариях» гасит объект до полутени и оставляет
   в полном свете только цепочку. Это единственная правка волны, которая делает
   свечений МЕНЬШЕ, а не больше. */
.schm.focus .snode,
.schm.focus .pipes > *,
.schm.focus .misc-box {
  opacity: .34; transition: opacity var(--dur-3) var(--ease);
  /* Гаснет не только яркость, но и ЦВЕТ: на приборной схеме сигнал несёт
     фосфор, янтарь и красный, и приглушённая одной прозрачностью зелёная
     плитка всё равно кричит «я статус». Обесцвечивание оставляет от неё
     чертёж — «объект тут, но сейчас разговор не про него». */
  filter: saturate(.3);
}
.schm.focus .snode.focus-on,
.schm.focus .snode.focus-src,
.schm.focus .pipes > .focus-on { opacity: 1; filter: none; }
/* Причина без своего агрегата (загазованность, взлом, авария ввода) живёт
   строкой в «Сигналах объекта»: гасить блок целиком нельзя — в нём и стоит
   ответ, поэтому гаснут его СТРОКИ, кроме причинной. */
.schm.focus.focus-misc .misc-box:not(.cnt-box) { opacity: 1; filter: none; }
.schm.focus.focus-misc .misc-box:not(.cnt-box) .misc-row { opacity: .34; filter: saturate(.3); }
.schm.focus.focus-misc .misc-box:not(.cnt-box) .misc-row.cause { opacity: 1; filter: none; }
/* Однократная пульсация контура источника: «вот отсюда». Повторяющееся
   мигание на аварийном экране — это шум, поэтому ровно один проход.
   Сосуд рисуется .vessel, а не .sym-body: без него единственный объект,
   ради которого режим и придуман (резервуар с переливом), не пульсировал
   вовсе. В prefers-reduced-motion анимация гасится централизованно (F-060). */
.schm.focus .snode.focus-src .sym-body,
.schm.focus .snode.focus-src .vessel { animation: schm-focus-in .9s var(--ease) 1; }
@keyframes schm-focus-in { 0% { stroke-opacity: .25; } 60% { stroke-opacity: 1; } }
/* САМА причина — пилюля канала, по которому идёт авария. Без неё в свете
   остаётся вся плитка котла с шестью датчиками, и вопрос «какой из них»
   остаётся без ответа. Обод бренда, а не ещё один красный: красным на этой
   пилюле уже сказан уровень, фосфор говорит «вот на что ты нажал». */
.schm.focus .lblp.cause .lblp-bg { stroke: var(--brand); stroke-width: 2; }
.schm.focus .lblp.cause .lbl-k { fill: var(--ink); opacity: 1; }
.schm.focus.focus-misc .misc-row.cause .misc-hit { stroke: var(--brand); stroke-width: 1.5; }

/* Фосфорное послесвечение пилюли, значение которой изменилось этим пакетом
   (§F-180). На схеме из тридцати показаний иначе не видно, что живёт, а что
   стоит уже час: телеметрия приезжает молча и подменяет цифру. 400 мс —
   ровно столько, чтобы поймать движение боковым зрением и не отвлечься.
   Тумблер «Показывать движение потока» выключает и её: человек попросил
   покоя, а не «покоя, кроме подсветки». */
.schm .lblp.fresh .lblp-bg { animation: lblp-fresh .4s var(--ease) 1; }
/* Гало, а не перекраска: у пилюли в аварии обод красный, у пилюли во внимании
   янтарный, и подменять их фосфором на полсекунды значит на полсекунды
   отменять уровень. Обод перекрашивается только у спокойной пилюли — ей
   отменять нечего. */
@keyframes lblp-fresh {
  0% { filter: drop-shadow(0 0 5px var(--brand-glow)); }
  100% { filter: none; }
}
.schm .lblp.fresh:not(.alarm):not(.warn) .lblp-bg { animation-name: lblp-fresh-calm; }
@keyframes lblp-fresh-calm {
  0% { stroke: var(--brand); filter: drop-shadow(0 0 5px var(--brand-glow)); }
  100% { filter: none; }
}
.schm.no-motion .lblp.fresh .lblp-bg { animation: none !important; }

/* Строка аварии, ведущая выделение: тонкая кромка бренда, без заливки —
   строка остаётся строкой аварии, а не превращается во «вкладку». */
.obj-alarm.focused { box-shadow: inset 2px 0 0 var(--brand); }
/* Ручка режима в заголовке карточки схемы. Нажатое состояние — тем же
   рисунком, что у активного сегмента (.seg button.on): в системе «включено»
   выглядит одинаково везде, иначе кнопка читается как «применить». */
.schm-cause.on {
  background: var(--brand-soft); color: var(--brand); border-color: transparent;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand) 30%, transparent);
}
.schm-cause.on:hover { color: var(--brand); border-color: transparent; }
/* Имя режима не переносится: на 390 px кнопка ломалась на две строки внутри
   собственной рамки и переставала читаться как кнопка. Пусть лучше перенесётся
   заголовок карточки — он и так стоит в две строки. */
.schm-cause { white-space: nowrap; }
.obj-cause-note { margin-inline-start: var(--sp-1); }
/* На 390 px заголовок карточки и так стоит в четыре колонки: подпись «видна
   цепочка: …» ломалась там на три строки и отжимала схему. Состояние режима
   на телефоне несёт сама нажатая кнопка, а имя причины стоит строкой аварии
   выше — подпись здесь дублирует и то и другое. */
@media (max-width: 860px) { .obj-cause-note { display: none; } }
html[data-theme="light"] .schm-cause.on { color: var(--brand-deep); box-shadow: inset 0 0 0 1px rgba(3,108,60,.35); }
/* Бумажная тема: полутень делается не тем же числом. На белом листе
   прозрачность 0,34 стирает линию до неразличимости — гасим мягче, зато
   обесцвечиваем сильнее: днём цвет и есть главный сигнал. */
html[data-theme="light"] .schm.focus .snode,
html[data-theme="light"] .schm.focus .pipes > *,
html[data-theme="light"] .schm.focus .misc-box,
html[data-theme="light"] .schm.focus.focus-misc .misc-box:not(.cnt-box) .misc-row { opacity: .42; filter: saturate(.18); }
html[data-theme="light"] .schm.focus .snode.focus-on,
html[data-theme="light"] .schm.focus .snode.focus-src,
html[data-theme="light"] .schm.focus .pipes > .focus-on,
html[data-theme="light"] .schm.focus.focus-misc .misc-box:not(.cnt-box),
html[data-theme="light"] .schm.focus.focus-misc .misc-box:not(.cnt-box) .misc-row.cause { opacity: 1; filter: none; }

/* ── Режим правки схемы (§F-158) ──
   Тулбар редактора больше не висит над схемой постоянно: в просмотре здесь
   одна кнопка «Изменить схему». Открытый режим обязан быть виден целиком —
   иначе владелец не понимает, почему схема вдруг тащится за мышью. */
.schm-tools.editing {
  border: 1px solid var(--brand); border-radius: var(--r);
  background: var(--brand-soft); padding: 7px 10px; margin-bottom: 8px;
}
.schm-mode {
  font: 700 var(--fs-tag) var(--mono); letter-spacing: .16em; text-transform: uppercase;
  color: var(--brand); margin-inline-end: 4px;
}

/* ── Поповеры настройки схемы (§F-160) ──
   Подложка ловит клик мимо — как у faceplate; прозрачная, потому что поповер
   не модалка и не блокирует чтение схемы. */
.schm-cfg-shade { position: fixed; inset: 0; z-index: 59; }
/* Поле настройки: подпись, контрол и строка «что делает и на что влияет». */
.schm-cfg .cfg-field { display: grid; gap: 3px; }
.schm-cfg .cfg-hint { line-height: 1.4; }
.schm-cfg .cfg-name {
  width: 100%; background: var(--field-bg); color: var(--ink);
  border: 1px solid var(--edge); border-radius: 7px; padding: 6px 9px;
  font: 500 var(--fs-body) var(--sans);
}
.schm-cfg .cfg-foot { justify-content: flex-end; }
/* Палитра оборудования: символ + подпись, три колонки равной высоты.
   Панель для расстановки картинок обязана показывать картинки (§F-163). */
.schm-cfg .cfg-palette { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; padding-top: 2px; }
.schm-cfg .cfg-palette .btn.eqp {
  flex-direction: column; gap: 4px; height: auto; min-height: 66px; padding: 7px 4px;
  justify-content: center; text-align: center; line-height: 1.2;
  font-size: var(--fs-tag); white-space: normal;
}
.schm-cfg .cfg-palette .btn.eqp .eqc-ico { flex: 0 0 auto; }
.schm-cfg .cfg-palette .btn.eqp span { display: block; }
/* Цель правки подсвечена фосфорным штрихом: поповер отвечает на вопрос
   «а что именно я сейчас настраиваю» рисунком, а не только заголовком. */
.schm .snode.cfg-target .sym-body,
.schm .snode.cfg-target .vessel { stroke: var(--brand); stroke-dasharray: 7 4; }

/* ── Клавиатура и фокус на схеме (§F-168) ──
   Внутри SVG было 30 кликабельных зон и ноль фокусируемых: переключить выход
   или открыть карточку канала с клавиатуры было нельзя вовсе. */
.schm .lblp:focus-visible,
.schm .snode:focus-visible,
.schm .misc-row:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.schm .lblp, .schm .snode, .schm .misc-row { outline-color: var(--brand); }

/* ── Авария у сосуда (§F-170) ──
   Источник аварии обязан быть самым громким объектом кадра. Резервуар с
   активным «Переливом» получал тонкий красный контур и оставался полностью
   спокойного бирюзового цвета, пока красным горели трубы вокруг него. */
.schm .snode.alarm .water,
.schm .snode.alarm .wave,
.schm .snode.alarm .wave2 { fill: var(--alarm); opacity: .62; }
.schm .snode.warn .water,
.schm .snode.warn .wave,
.schm .snode.warn .wave2 { fill: var(--warn); opacity: .5; }
.schm .snode.alarm .vlevel { fill: #fff; stroke: rgba(60,6,2,.9); }

/* Легенда: тумблер выхода и подсветка трубы объясняются наравне с уровнями. */
.schm-legend .lg-sw {
  width: 18px; height: 10px; border-radius: 5px;
  background: var(--brand-soft); border: 1px solid var(--brand); position: relative;
}
.schm-legend .lg-sw::after {
  content: ""; position: absolute; top: 1px; inset-inline-end: 1px; width: 6px; height: 6px;
  border-radius: 50%; background: var(--brand);
}
.schm-legend .lg-glow {
  width: 22px; height: 6px; border-radius: 3px;
  background: var(--alarm); opacity: .28; box-shadow: 0 0 6px var(--alarm-glow);
}

/* ── Строка состояний объекта (§F-172) ──
   Постоянные состояния («есть оперативные правки», «выход в ручном») больше
   не занимают полноширинную янтарную плашку: две таких плашки отжимали
   мнемосхему на 100 px и не требовали немедленной реакции. Чип называет
   состояние, несёт счётчик и ведёт туда, где с ним можно что-то сделать. */
.obj-state-chips { flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.obj-state-chips .chip-act {
  cursor: pointer; font: inherit; gap: 4px;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.obj-state-chips .chip-act:hover { border-color: var(--brand); }
.obj-state-chips .chip-act svg { opacity: .7; }

/* Тумблер движения: подпись объясняет, чем именно управляет флажок, а строка
   под ней — что будет, если его снять (§F-171). */
.obj-motion { align-items: flex-start; gap: 8px; }
.obj-motion .obj-motion-hint { display: block; margin-top: 1px; }

/* ── Активные аварии объекта (§F-108) ──
   Взятая в работу авария приглушается, но не прячется: сверху то, чем ещё
   никто не занят. */
.obj-alarm { gap: 10px; flex-wrap: wrap; }
.obj-alarm.acked { opacity: .78; }
.obj-alarm .valx { display: inline-flex; align-items: baseline; gap: 6px; }

/* ── Лента «История» под схемой (§F-152) ──
   Вертикальная лента вместо горизонтальных колонок. Прежняя многоколоночная
   раскладка (.obj-log.cols) при 244 записях давала внутреннюю ширину 23 000 px
   при видимых 994 — человеку было доступно около 4 % журнала, и ни одного
   признака прокрутки. */
/* Своей прокрутки у ленты НЕТ: пятнадцать записей видны целиком, дальше —
   кнопка «Показать ещё». Внутренний скроллер вернул бы ту же беду в другой
   плоскости — часть журнала за краем без видимого признака (в macOS полосы
   прокрутки всплывающие и в покое не нарисованы вовсе). */
.obj-log.feed { overflow: visible; }
.obj-log.feed .tl-item { padding: 8px 15px; }
.obj-log-more {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 15px; border-top: 1px solid var(--line-2);
}

/* Чип «исполняет команды» — единственный чип шапки объекта с переменной
   длиной: имя платы-командира приходит с сервера. На узком экране он обязан
   ужиматься, а не выталкивать страницу в горизонтальную прокрутку. */
.chip.obj-peer { max-width: 100%; min-width: 0; overflow: hidden; }

/* Вторая строка подзаголовка карточки канала: соглашение рисунка, а не
   классификация оборудования (§F-165). */
.face .f-sub.muted { color: var(--ink-2); }

@media (max-width: 860px) {
  /* Поповер настройки на телефоне — нижний лист: поповер шириной 288 px,
     прибитый к правому верхнему углу, накрывал половину схемы. */
  .schm-cfg {
    left: 0; right: 0; top: auto; bottom: 0; width: auto;
    border-radius: var(--r-lg) var(--r-lg) 0 0; max-height: 76vh; overflow: auto;
  }
  .schm-cfg .cfg-palette { grid-template-columns: repeat(2, 1fr); }
  .obj-log.feed { overflow: visible; }
  /* Чип состояния, который ВЕДЁТ куда-то, — орган управления и обязан брать
     палец: высота 28 px на телефоне даёт гарантированный промах. */
  .obj-state-chips .chip-act { min-height: var(--ctl-h-lg); }
}

/* Светлая тема: тень прокрутки на бумаге — мягкая сепия, чёрная выглядит
   грязным пятном; заливка кромки берёт цвет самой карточки. */
html[data-theme="light"] .schm-card {
  background-image:
    linear-gradient(to right, var(--panel), rgba(255,255,255,0)),
    linear-gradient(to left,  var(--panel), rgba(255,255,255,0)),
    linear-gradient(to right, rgba(24,20,12,.18), rgba(255,255,255,0)),
    linear-gradient(to left,  rgba(24,20,12,.18), rgba(255,255,255,0));
}
html[data-theme="light"] .schm .lblp-sw { fill: rgba(24,20,12,.06); }
html[data-theme="light"] .schm .snode.alarm .water,
html[data-theme="light"] .schm .snode.alarm .wave,
html[data-theme="light"] .schm .snode.alarm .wave2 { fill: var(--alarm); opacity: .5; }
html[data-theme="light"] .schm .snode.warn .water,
html[data-theme="light"] .schm .snode.warn .wave,
html[data-theme="light"] .schm .snode.warn .wave2 { fill: var(--warn); opacity: .55; }
html[data-theme="light"] .schm .snode.alarm .vlevel { fill: #fff; stroke: rgba(90,10,4,.85); }
html[data-theme="light"] .schm-legend .lg-glow { box-shadow: none; opacity: .45; }

/* ═══════ МНЕМОСХЕМА НА БУМАГЕ: МОНТАЖНЫЙ ЧЕРТЁЖ (F-147 / X1-44) ═══════════
   Дорожка К3-идеи, этап 3 волны 7. Правки ТОЛЬКО печатные (@media print) —
   экранная схема, включая режим «покажи причину» дорожки К4, не затронута ни
   одним правилом: ни одно из них не действует вне печати.

   Что было на настоящем листе A4 (проверено PDF, а не кадром в media:print):
   1. Рисунок ОБРЕЗАН по правому полю. Ширина <svg> проставлена инлайном в
      экранных пикселях (schematic.js: svgRoot.style.width), карточка вокруг
      прокручивается — и всё, что не влезло, на бумаге просто исчезало: у КНС-7
      не печатались оба насоса и напорная задвижка. Заметить пропажу по листу
      нельзя: обрыв выглядит как край схемы.
   2. Приборные подложки — тёмные по замыслу («фосфорный терминал»), а чернила
      на печати чёрные: пилюли каналов, шильдики имён и панель «Сигналы
      объекта» выходили ЧЁРНЫМИ ПРЯМОУГОЛЬНИКАМИ с чёрным же текстом внутри.
      Половина подписей схемы была нечитаема физически.
   3. Заливка сосуда в аварии — сплошной сургуч во всю высоту бака: пятно
      краски на четверть листа, съедающее тонер и глушащее сам рисунок.
   4. Свечения. Общее правило `box-shadow: none` печатного блока до SVG не
      достаёт: свечение узлов и подписей сделано `filter: drop-shadow`, и на
      бумаге давало серые ореолы вокруг каждой аварийной пилюли.

   Приём: на бумаге сигнал несут ЛИНИЯ и СЛОВО, а не свет и заливка. Корпус —
   чёрный контур на белом, статус — цвет и толщина обводки (они же в легенде),
   значение — текст. Так рисуют щитовые чертежи, и такой лист не жалко
   повесить в щитовой. */
@media print {
  /* 1. Рисунок целиком и по ширине листа. !important обязателен: ширину и
        высоту SVG ставит JS инлайновым стилем, а инлайн сильнее любого
        селектора. viewBox + preserveAspectRatio сами держат пропорции. */
  .schm { width: 100% !important; height: auto !important; max-width: 100%; }
  .schm-card {
    overflow: visible !important; padding: 0; background: none;
    /* Чертёж не режется между листами. */
    break-inside: avoid;
  }
  /* Экранные органы вокруг рисунка: инструменты редактора, подсказка «тяните
     вбок» и тумблер движения потока — на бумаге движения нет по определению. */
  .schm-tools, .schm-pan-note, .obj-motion { display: none !important; }

  /* 2. Ни одного свечения и ни одной анимации внутри рисунка. */
  .schm *, .schm-legend * { filter: none !important; animation: none !important; }

  /* 3. Приборные подложки → белая бумага с контуром. */
  .schm .lblp-bg, .schm .eplate, .schm .misc-bg,
  .schm .snode .gauge, .schm .snode .vessel-bg,
  .schm .snode .handle.sq, .schm .fl-in { fill: #fff; stroke: #000; stroke-width: 1; }
  .schm .snode .vessel-bg, .schm .fl-in { stroke: none; }
  /* Уровень в пилюле по-прежнему различается по статусу — но обводкой. */
  .schm .lblp.warn .lblp-bg { stroke: var(--warn); stroke-width: 1.6; }
  .schm .lblp.alarm .lblp-bg { stroke: var(--alarm); stroke-width: 2.2; }
  .schm .lblp.man .lblp-bg { stroke: #666; stroke-dasharray: 5 3; }
  /* Тумблер выхода: рамка и язычок, положение читается по цвету язычка. */
  .schm .lblp-sw { fill: #fff; stroke: #000; }
  .schm .lblp-knob { fill: #999; }
  .schm .lblp.do.on .lblp-sw { stroke: var(--brand); }
  .schm .lblp.do.on .lblp-knob { fill: var(--brand); }
  /* Текст — чернила. Подпись серая, значение чёрное: та же пара, что в
     таблицах листа. */
  .schm text { fill: #000; }
  .schm .lbl-k, .schm .misc-k, .schm .misc-t, .schm .misc-sub { fill: #333; }
  .schm .lbl-v, .schm .misc-v, .schm .ename, .schm .misc-title { fill: #000; }
  .schm .lbl-v.warn, .schm .misc-row.warn .misc-k, .schm .misc-row.warn .misc-v { fill: var(--warn-ink); }
  .schm .lbl-v.alarm, .schm .misc-row.alarm .misc-k, .schm .misc-row.alarm .misc-v { fill: var(--alarm-ink); }
  .schm .lbl.note { fill: var(--warn-ink); }
  /* Строка непривязанного канала в аварии: на экране её метит заливка, на
     бумаге — рамка вокруг строки (заливки статуса печатным блоком обнулены). */
  .schm .misc-row.alarm .misc-hit { fill: none; stroke: var(--alarm); stroke-width: 1; }
  .schm .misc-mark { fill: #f0f0f0; }
  .schm .misc-row.zeb .misc-hit { fill: #f4f4f4; }
  .schm .misc-led { fill: #000; }
  .schm .misc-rule { stroke: #ccc; }

  /* 4. Трубы — двойной линией, как на чертеже: чёрный корпус, светлое ядро.
        Среда различается тоном ядра и названа в легенде теми же образцами. */
  .schm .pipe-trench { stroke: #fff; }
  .schm .pipe-case { stroke: #000; stroke-width: 11; }
  .schm .pipe-fill { stroke: #fff; stroke-width: 7; opacity: 1; }
  .schm .pipe-fill.m-air { stroke: #f2f2f2; opacity: 1; }
  .schm .pipe-fill.m-sew { stroke: #c9c9c9; }
  .schm .pipe-fill.m-gas { stroke: #e2e2e2; }
  .schm .pipe-water.flow { display: none; }        /* бегущие точки — движение */
  .schm .flow-dir { fill: #000; }                  /* направление потока держит шеврон */
  /* Ореол трубы остаётся признаком «на конце этой линии что-то не так», но
     серым: цветная полоса в 19 единиц на бумаге спорит с самим рисунком. */
  .schm .pipe-glow.warn { stroke: #000; opacity: .08; }
  .schm .pipe-glow.alarm { stroke: #000; opacity: .13; }

  /* 5. Корпуса аппаратов: белая заливка вместо тёмного «металла» (на экране
        это url(#mgrad) — на бумаге он печатался чёрным диском). Статус держат
        цвет и толщина обводки, они уже заданы выше по файлу. */
  .schm .snode .sym-body { fill: #fff; }
  .schm .snode .pipe-stub, .schm .snode .stem, .schm .snode .handle { stroke: #000; }
  /* .edge и .disc НЕ красим в чёрный: их цвет — это статус узла и положение
     заслонки, а чёрная линия стёрла бы оба различия с листа. */
  .schm .snode .disc.closed { stroke: #999; }
  /* Крыльчатка: работающий насос — жирная чёрная линия, стоящий — серая. На
     экране разницу несёт фосфор, на бумаге — плотность чернил. Слово рядом
     («ВКЛ» / «НЕ ВКЛ») говорит то же самое, и одно проверяет другое. */
  .schm .impeller { stroke: #999; }
  .schm .snode.run .impeller { stroke: #000; }
  .schm .snode .hub, .schm .snode .bolt { fill: #666; }
  .schm .snode .ring, .schm .snode .tick,
  .schm .snode .strap, .schm .snode .panel { stroke: #999; }
  .schm .blade { fill: #eee; stroke: #000; }
  .schm .snode .pump-fill { stroke: #ccc; }
  .schm .flow-arrow { stroke: #000; }
  /* Точка состояния у шильдика — единственное цветное пятно узла. */
  .schm .state-dot { fill: #666; }
  .schm .state-dot.warn { fill: var(--warn); }
  .schm .state-dot.alarm { fill: var(--alarm); }
  /* Пламя котла: контур, а не оранжевая заливка с ореолом. */
  .schm .flpath { fill: #d8d8d8; }
  .schm .fl-glow, .schm .ember, .schm .bub { display: none; }

  /* 6. Уровень в сосуде — светлая заливка с чёрной цифрой поверх. Сплошной
        сургуч «100 %» во всю высоту бака был самым тёмным пятном листа, а
        сообщал ровно то же, что цифра и обводка сосуда. */
  .schm .water, .schm .wave, .schm .wave2 { fill: #dcdcdc !important; opacity: 1 !important; }
  .schm .vlevel { fill: #000 !important; stroke: #fff !important; }

  /* 7. Грунт и опоры — тонкая графика подосновы, не серый растр. */
  .schm .earth { fill: #f5f5f5; }
  .schm .earth-pit { fill: #fff; }
  .schm .earth-hatch, .schm .pit-wall { stroke: #b0b0b0; }
  .schm .ground-line { stroke: #000; }
  .schm .support .post, .schm .support .beam { stroke: #666; }
  .schm .support .brace { stroke: #b0b0b0; }

  /* 8. Легенда — это ключ к чертежу, и образцы в ней обязаны совпасть с тем,
        что напечатано на рисунке (иначе ключ объясняет не тот рисунок). */
  .schm-legend { color: #333; break-inside: avoid; }
  .schm-legend .lg-pipe { border-color: #000; box-shadow: none; }
  .schm-legend .lg-pipe.water { background: #fff; }
  .schm-legend .lg-pipe.air { background: #f2f2f2; }
  .schm-legend .lg-pipe.sew { background: #c9c9c9; }
  .schm-legend .lg-pipe.gas { background: #e2e2e2; }
  .schm-legend .lg-pipe.dry { background: #000; }
  .schm-legend .lg-glow { background: #000; opacity: .13; box-shadow: none; }
  .schm-legend .lg-sw { background: #fff; border-color: var(--brand); }

  /* 9. Шапка страницы объекта на листе — «титул» документа: имя объекта, его
        адрес и текущий статус, отчёркнутые от рисунка. Липкость экранная, на
        бумаге она вжимала заголовок в первую строку схемы. */
  #obj-root .page-head {
    position: static; margin: 0 0 14px; padding: 0 0 10px;
    border-bottom: 2px solid #000; background: none;
  }
  #obj-root .page-head .sub { color: #333; }
  /* Подсказка «Нажмите на аварию — на схеме останется только её цепочка»
     объясняет ЖЕСТ, а на листе жестов нет: инструкция по работе с экраном на
     документе выглядит опечаткой. Селектор сужен до карточки активных аварий
     (только она содержит строки .obj-alarm) — соседние подписи шапок карточек
     объекта на бумаге остаются. */
  #obj-root .card:has(.obj-alarm) > .card-h > .small.muted { display: none; }
}


/* === К5: объект — «Подробно», действия, оперативные правки === */
/* Волна ux/v03, дорожка К5. Правки живут ЗДЕСЬ, а не в блоке «К5» выше по
   файлу: общий слой компонентов закрыт, а переопределение в конце файла
   выигрывает по каскаду при равной силе селектора. Светлая тема — в этом же
   блоке рядом со своим правилом: чинить один экран в двух местах нельзя. */

/* ── F-202: единственное редактируемое поле паспорта платы ────────────────
   Нативный select высотой 24 px посреди моно-паспорта читался как чужой
   виджет; общий .in уже даёт ему appearance:none и наш chevron, здесь только
   ширина под расшифровку («2it — сторонний прибор OKO-U») и тач-цель. */
.dtl-kv select.in.dtl-model { width: 100%; max-width: 320px; padding: 4px 30px 4px 10px; }
/* Подпись прибита к ВЕРХУ своей ячейки. Пока все значения паспорта были в одну
   строку, разницы не было видно; строка с полем и пояснением втрое выше
   остальных, и центрированная подпись «Имя платы» уезжала под своё значение —
   пара читалась как перепутанная. */
.dtl-kv b { align-self: start; }
.dtl-kv .v, .dtl-kv > span { align-self: start; }
@media (max-width: 860px) { .dtl-kv select.in.dtl-model { min-height: var(--ctl-h-lg); } }

/* ── F-186: пороги читаются КОЛОНКОЙ, а не построчно ──────────────────────
   Каждая .ovr-row была отдельным гридом, и её первая колонка (1.5fr) меряла
   остаток по ширине своего же пояснения: левые края значений разъезжались на
   117 px (замер: 1022 / 1045 / 1030 / 1139 / 1116 / 1108), кнопки — на 78.
   Задача секции — СРАВНИТЬ восемь порогов, а рваная колонка заставляет читать
   каждую строку отдельно. Лечится одним гридом на весь список: строки отдают
   свои ячейки родителю (display:contents), и колонки становятся общими. */
/* Общий грид — только на широком экране. На телефоне колонок нет вовсе:
   строка там двухэтажная (имя-значение, под ними пояснение и действие), и
   сквозное выравнивание в 390 px не только не нужно, но и вредно. */
@media (min-width: 861px) {
  .ovr-rows {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) auto minmax(0, 1.1fr) auto;
    column-gap: 12px;
  }
  .ovr-rows > .ovr-row { display: contents; }
  /* Заголовок группы («ПОРОГИ», «СВЯЗКИ ВЫХОД ← ВХОД») — во всю ширину грида. */
  .ovr-rows > .ovr-grp { grid-column: 1 / -1; }
  /* Разделитель строки рисует каждая ячейка сама: у display:contents своей
     геометрии нет, и border на строке не отрисовался бы вовсе. */
  .ovr-rows > .ovr-row > * { padding: 7px 0; border-bottom: 1px solid var(--line-2); align-self: center; }
  .ovr-rows > .ovr-row:last-child > * { border-bottom: 0; }
}

/* Счётчик в шапке вкладки — обычным регистром: капс с разрядкой в этой системе
   означает ПОДПИСЬ РАЗДЕЛА, и фраза «изменено правкой: 3 порога из 8
   возможных», набранная им, читается как второй заголовок и занимает две
   строки вместо одной. */
.ovr-tab-h .small { text-transform: none; letter-spacing: normal; font-weight: 400; }

/* ── F-212: движение уставки вместо голого значения ───────────────────────
   «90,00 → 96,25 °C»: прежнее значение приглушено и НЕ перечёркнуто —
   моноширинные цифры от перечёркивания разваливаются (тот же довод, что у
   .row-off). Стрелку рисует хелпер по направлению письма, здесь только цвет. */
.ovr-delta { display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap; }
.ovr-delta .was { color: var(--ink-2); }
.ovr-delta .arr { color: var(--manual); font-family: var(--sans); }
.ovr-delta .now { color: var(--ink); }

/* ── F-213: шкала датчика (ОБЩИЙ компонент sensor-scale.js) ───────────────
   Стили лежат в регионе К5, потому что компонент заведён этой дорожкой; его
   второй потребитель — редактор правил конфигуратора (К6), и своей копии этих
   правил заводить нельзя. Полоса СПРАВОЧНАЯ: без свечения и без статусного
   цвета, иначе она конкурировала бы с аварией за периферийное зрение. */
.ss {
  position: relative; display: block; height: 6px; margin-top: 5px;
  max-width: 260px; border-radius: 3px;
  background: var(--panel-3); border: 1px solid var(--line-2);
}
.ss .ss-mark { position: absolute; top: -2px; width: 2px; height: 10px; border-radius: 1px; }
/* Засечка конфигурации — нейтральная (так задумано), правки — цвет следа
   человека (тот же --manual, что у чипа «правка» и ромба на мнемосхеме). */
.ss .ss-mark.cfg { background: var(--ink-2); }
.ss .ss-mark.ovr { background: var(--manual); }
.ss .ss-now {
  position: absolute; top: 50%; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
  border-radius: 50%; background: var(--brand);
  box-shadow: 0 0 6px var(--brand-glow);
  animation: ss-breathe 2.4s ease-in-out infinite;
}
@keyframes ss-breathe { 50% { opacity: .45; } }
/* Дыхание гасим и по настройке системы, и на объекте со штормом аварий:
   движение в аварийном интерфейсе имеет право быть только у аварии. */
.ss .ss-now.quiet { animation: none; }
@media (prefers-reduced-motion: reduce) { .ss .ss-now { animation: none; } }
html[data-theme="light"] .ss { background: var(--panel-2); }
html[data-theme="light"] .ss .ss-now { box-shadow: none; }
/* Подпись краёв шкалы (необязательная, см. sensor-scale.js::range). Стоит
   СПРАВА от полосы, а не под ней: строка порога уже двухэтажная, и третий
   этаж развалил бы колонку значений. Числа второстепенны по отношению к
   значению порога — тот же цвет, что у всех подписей величин. */
.ss-w { display: flex; align-items: center; gap: var(--sp-2); }
.ss-w .ss { flex: 1 1 auto; min-width: 90px; }
.ss-range {
  flex: 0 0 auto; margin-top: 5px; white-space: nowrap;
  color: var(--ink-2); font: 500 var(--fs-tag) var(--mono); font-variant-numeric: tabular-nums;
}
/* На телефоне строка порога и без того тесная — числа краёв уходят: положение
   точки на полосе остаётся, а сама шкала прочитана скринридеру в aria-label. */
@media (max-width: 860px) { .ss-range { display: none; } }

/* ── F-199: строка канала на вкладке «Каналы» ─────────────────────────────
   Переключатель стоит в колонке действий и не растягивается: у .switch своя
   ширина 42 px, а грид иначе отдаёт ему всю колонку. */
.ovr-row-chan .act { justify-content: flex-end; }
.ovr-row-chan .act .switch { flex: 0 0 auto; }

/* Заголовок группы в РЕДАКТОРЕ. До волны его стиль был описан только для
   секции (.ovr-eff .ovr-grp), и в панели правок группы каналов и правил
   выглядели обычной строкой — то есть не отличались от того, что они
   озаглавливают. */
.ovr-drawer .ovr-grp {
  font: 600 var(--fs-micro) var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-2); margin: 14px 0 2px;
}
.ovr-drawer .ovr-grp:first-child { margin-top: 0; }

/* ── F-189: шапка секции на телефоне — чипы СО СЛОВАМИ ────────────────────
   Прежняя короткая форма «3/8 · 1/4 · 1/4» держала смысл только в подсказке
   title, которой на тач-экране нет. Разделитель «·» между короткими блоками
   больше не нужен: чипы разделены сами собой, а время стоит отдельным блоком. */
.card .card-h.ovr-eff-h .ovr-h-chips { display: none; flex-wrap: wrap; gap: 4px; }
.card .card-h.ovr-eff-h .ovr-h-chips .chip { font-size: var(--fs-tag); padding: 1px 7px; }
@media (max-width: 860px) {
  .card .card-h.ovr-eff-h .ovr-h-chips { display: flex; }
  /* Точку-разделитель ставим только СОСЕДНИМ коротким подписям (время), а не
     контейнеру чипов — иначе «· » прилипало бы к первому чипу. */
  .card .card-h.ovr-eff-h .ovr-h-chips ~ .ovr-h-short::before { content: ""; }
  /* Шапка на 390 px: заголовок, чипы и время — тремя строками, каждая целиком. */
  .card .card-h.ovr-eff-h > * { flex: 0 0 auto; }
  .card .card-h.ovr-eff-h .ovr-h-chips { flex: 1 0 100%; }
}

/* ── F-185 / F-191: панель правок ДЕЙСТВИТЕЛЬНО модальна ──────────────────
   Панель живёт внутри общей подложки .modal-back (её и поднимает наблюдатель
   ui.js до диалога с ловушкой фокуса). Подложка по умолчанию центрирует своё
   содержимое — здесь оно прижато к краю экрана, поэтому раскладку снимаем.
   Общий компонент при этом не тронут: правило адресовано модификатору. */
.modal-back.ovr-back { display: block; padding: 0; }
/* Кнопка выхода в шапке — полноценная тач-цель в любой ширине (была 38×26). */
.ovr-drawer .ovr-x { min-width: 40px; min-height: 40px; }
/* Прилипшая «Готово»: на десктопе она не нужна — там есть и Esc, и клик мимо
   панели, и крестик в двух сантиметрах от курсора. */
.ovr-drawer .ovr-done { display: none; }

/* ── F-190: полосы прокрутки признаются в том, что прокручиваются ─────────
   Ряд действий и вкладки редактора на 390 px уезжают вбок без единого
   намёка: за краем оставались «Вернуть автоматике · 2», «Ещё» и вкладка
   «Сброс», обрезанная до «Сбр». Маска гасит содержимое у правого края —
   ровно там, где оно продолжается. Маска НЕ применяется, когда прокручивать
   нечего: у неё нет состояния, поэтому включаем её только на узком экране,
   где переполнение и возникает. */
@media (max-width: 860px) {
  .oa-row, .ovr-tabs {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
  }
  html[dir="rtl"] .oa-row, html[dir="rtl"] .ovr-tabs {
    -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 28px), transparent 100%);
    mask-image: linear-gradient(to left, #000 calc(100% - 28px), transparent 100%);
  }
  /* Панель правок — полноэкранный лист: выход из него обязан быть в двух
     местах, и оба размером с палец. */
  .ovr-drawer .ovr-x { min-width: 44px; min-height: 44px; }
  .ovr-drawer .ovr-done {
    display: block; position: sticky; bottom: 0; z-index: 1;
    margin-top: auto; padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
    background: var(--panel); border-top: 1px solid var(--line);
  }
  .ovr-drawer .ovr-done .btn { width: 100%; min-height: 44px; }
  /* Тело больше не отбивает место под нижнюю навигацию: её закрывает сама
     кнопка «Готово», а двойной отступ съедал экран под последней строкой. */
  .ovr-body { padding-bottom: 12px; }
}

/* ── F-184: на ручке видно текущее положение выхода ───────────────────────
   Три одинаковые кнопки не отвечали на вопрос «а как сейчас?»: диспетчер
   искал положение в другой колонке и на всякий случай нажимал ещё раз — на
   СМС-резерве каждое такое нажатие стоит денег. Активная кнопка помечена
   обводкой и чернилами бренда, а не заливкой: заливка сделала бы её похожей
   на главную кнопку экрана, то есть на «нажми меня». */
.do-ctl .btn.on {
  border-color: var(--brand); color: var(--brand);
  background: var(--brand-soft); box-shadow: inset 0 0 0 1px var(--brand-soft);
}
.do-ctl .btn.on:hover { border-color: var(--brand); }
html[data-theme="light"] .do-ctl .btn.on { background: var(--brand-soft); color: var(--brand-deep); }

/* ── F-214: билет команды — три засечки пути ──────────────────────────────
   «Доставлена» не отвечает на вопрос «куда»: серверу или плате. Рейка из трёх
   засечек показывает путь целиком — сервер принял, ушла на плату, плата
   подтвердила. Засечка «ждём» намеренно тусклая и БЕЗ анимации: мигающий
   индикатор рядом с аварией читается как новая авария. */
.oa-path { display: inline-flex; align-items: center; gap: 3px; margin-inline-end: 2px; }
.oa-path .oa-step { width: 9px; height: 3px; border-radius: 2px; background: var(--line); }
.oa-path .oa-step.done { background: var(--ok); }
.oa-path .oa-step.wait { background: var(--ink-3); }
.oa-path .oa-step.bad { background: var(--warn); }
.oa-path .oa-step.none { background: var(--line); opacity: .5; }

/* ── Хвост свипа F-014: последняя тач-цель < 44 px на странице объекта ────
   Чип «исполняет команды» в шапке — это ССЫЛКА на «Связи плат» (a.chip), а не
   подпись: на 390 px он был 336×25 px. Правило живёт в регионе К5, потому что
   регион К4 (шапка объекта) уже сдан; сам чип не трогаем — только его высоту
   как цели пальца. */
@media (max-width: 860px) {
  a.chip.obj-peer { min-height: var(--ctl-h-lg); }
}

/* ── F-019: карточка таблицы на телефоне без пустых строк ─────────────────
   Карточная деградация (К1-А2) печатает КАЖДУЮ ячейку строкой «подпись →
   значение», и у канала без обратной связи, без оборудования и без датчика
   выходило три строки со знаком «—»: на 390 px вид «Подробно» вырастал до
   21 000 px, из которых треть не несла ничего. Прочерк в карточке — это
   отсутствие данных, и говорить о нём отдельной строкой не нужно: в таблице
   на десктопе колонка остаётся на месте и по-прежнему видна. */
@media (max-width: 860px) {
  /* Селектор повторяет форму правила карточной деградации (К1-А2): у того
     `table.t > tbody > tr > td[data-th]` вес выше, и короткая запись просто не
     сработала бы. Общий компонент при этом не тронут — правило адресовано
     ячейке с прочерком. */
  table.t > tbody > tr > td.dash[data-th] { display: none; }
}

/* ── K10-02: страница объекта ездила вбок ─────────────────────────────────
   Замер на #/object/DEMO-NAS2 → «Подробно», 1440 px: scrollWidth 1660 при
   clientWidth 1440. Виновник — паспорт платы .dtl-kv: его колонки объявлены
   `auto 1fr auto 1fr`, но `1fr` в гриде по умолчанию не уже своего
   min-content, а значения там завёрнуты в `.v`, которому общий слой ставит
   `white-space: nowrap`. Одна строка помощи под выбором типа платы («Метка
   железа: по ней пульт понимает, какие команды…») в одну строку требовала
   803 px — и растягивала всю страницу вместе с мнемосхемой и шапкой.
   Лечим НЕ отменой nowrap в общем слое (он держит числа, единицы и коды от
   разрыва по всему продукту), а разрешением переноса ровно в этом паспорте:
   min-width:0 снимает распорку колонки, overflow-wrap ловит длинный
   идентификатор без пробелов. */
.dtl-kv > * { min-width: 0; }
.dtl-kv > .v, .dtl-kv > span { white-space: normal; overflow-wrap: anywhere; }

/* ── F-204: тосты не накрывают состояние объекта ──────────────────────────
   Контейнер тостов стоит в правом верхнем углу, а у объекта там же липкая
   шапка с именем и бейджами «НА СВЯЗИ / ОБСЛУЖИВАНИЕ / ЕСТЬ ПРАВКИ». Первое
   же подтверждение команды закрывало собой ровно тот статус, ради которого
   человек и смотрит на объект. Приём тот же, что у конфигуратора парой сотен
   строк выше (`body:has(#cfg-root)`): опускаем полосу тостов под шапку — сам
   компонент при этом не тронут. */
body:has(#obj-root) .toasts { top: 84px; }
@media (max-width: 860px) { body:has(#obj-root) .toasts { top: auto; } }

/* ── F-211: пустая ячейка приборной полосы ────────────────────────────────
   «нет данных» на месте числа — это ОТСУТСТВИЕ показания, а не показание:
   набирается как подпись, не моноширинно и не в полный контраст, иначе взгляд
   считает его значением. */
.obj-vitals .vt b .vt-nd { color: var(--ink-2); font: 500 var(--fs-micro) var(--sans); }

/* ── K10-22: единица Modbus-величины рядом с числом ───────────────────────
   «Гц» и «кВт» кода на проводе не имеют, единица приходит из карты регистров.
   Она вторична по отношению к числу — тем же цветом, что единицы в шапке. */
.ai-unit { color: var(--ink-2); font-weight: 500; font-size: var(--fs-micro); }

/* ── F-203: адресат команды СМС неизвестен ────────────────────────────────
   Не warnBox во всю ширину: это не предупреждение об опасности, а недостающая
   половина инструкции — янтарные чернила и место в той же строке, что и
   кнопка «Скопировать команду СМС». */
.ovr-sms-noaddr { color: var(--warn-ink); max-width: 46ch; }

/* ── F-178: действие блока порогов в faceplate ────────────────────────────
   Правка строки — значок-карандаш (тач-цель ниже), а имя действия стоит один
   раз под группой. Пояснение про две цены изменения занимает всю ширину:
   вторая строка ряда, а не хвост за кнопками. */
.ovr-face-edit { padding-inline: 6px; }
.ovr-face-note { flex-basis: 100%; }
@media (max-width: 860px) {
  .ovr-face-edit { min-width: var(--ctl-h-lg); min-height: var(--ctl-h-lg); }
}

/* === К6: конфигуратор === */
/* Волна ux/v03, дорожка К6. Правки живут ЗДЕСЬ, а не в блоках конфигуратора
   выше по файлу: общий слой компонентов закрыт, а переопределение в конце
   файла выигрывает по каскаду при той же силе селектора. Светлая тема — в этом
   же блоке рядом со своим правилом: чинить один экран в двух местах нельзя. */

/* ── полоса «Что мешает выпуску» ────────────────────────────────────────────
   Семь-девять янтарных плашек подряд сливались в сплошное пятно, и
   единственный красный пункт — тот, что реально держит выпуск, — терялся среди
   них: «один акцент на экран» нарушался тринадцать раз сразу (F-223). Жёлтому
   оставлен только левый корешок; заливка и рамка — исключительно у красного. */
.cfg-ready .cr-item.warn {
  background: var(--panel-2); border-color: var(--line);
  box-shadow: inset 3px 0 0 var(--warn);
}
.cfg-ready .cr-item.warn:hover { border-color: var(--brand); }
/* «Правится сейчас» помечено фирменной кромкой; у жёлтой строки она ЗАМЕНЯЕТ
   янтарный корешок — две кромки на одной строке ничего не различают. */
.cfg-ready .cr-item.warn.here { box-shadow: inset 3px 0 0 var(--brand); }

/* Вложенного скроллера больше нет: до предела в три жёлтых строки список идёт
   в потоке страницы, а прокрутка появляется только после «Показать все N».
   Прежний max-height:40vh резал строку посередине текста и показывал 7 пунктов
   под заголовком «(13)» — счётчик не сходился с картинкой (F-216, G6-06). */
.cfg-ready .cr-groups { max-height: none; overflow: visible; }
.cfg-ready .cr-groups.all { max-height: min(38vh, 320px); overflow-y: auto; }
.cfg-ready .cr-more { margin-top: 6px; justify-self: start; }
.cfg-ready .cr-scope { margin-inline-start: 2px; }

/* Сгруппированная строка однотипных замечаний (F-222): снаружи — обычная
   строка полосы, внутри — поимённый список, из которого переход к каждому
   каналу остаётся на месте. */
.cfg-ready .cr-g {
  border-radius: 8px; border: 1px solid var(--line); background: var(--panel-2);
  box-shadow: inset 3px 0 0 var(--warn);
}
.cfg-ready .cr-g > summary {
  display: flex; align-items: flex-start; gap: 9px; padding: 7px 10px; cursor: pointer;
  list-style: none; font-size: var(--fs-body); line-height: 1.4; color: var(--ink);
}
.cfg-ready .cr-g > summary::-webkit-details-marker { display: none; }
.cfg-ready .cr-g > summary .cr-ic { flex: 0 0 auto; display: inline-flex; padding-top: 1px; color: var(--warn-ink); }
.cfg-ready .cr-g > summary .cr-tx { flex: 1; min-width: 0; }
.cfg-ready .cr-g > summary .cr-go {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 3px;
  font: 600 var(--fs-micro) var(--sans); color: var(--ink-2); white-space: nowrap;
}
.cfg-ready .cr-g > summary:hover .cr-go { color: var(--brand); }
.cfg-ready .cr-g > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; border-radius: 8px; }
.cfg-ready .cr-g[open] > summary .cr-go svg { transform: rotate(180deg); }
.cfg-ready .cr-g .cr-g-body { display: grid; gap: 4px; padding: 0 10px 8px; }

/* ── предложение правила: три выделения вместо одного ───────────────────────
   Имя канала — переход (фосфор), число с единицей — значение (обычные чернила,
   жирно), код события — вторичная моно-подпись. До этого всё пятеро шли одним
   зелёным акцентом, строка превращалась в рябь, а значение не отличалось от
   ссылки (F-231); код события внутри русской фразы цветным словом читался как
   опечатка и ничего не объяснял диспетчеру (F-387). */
.rs .rs-n { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.rs .rs-e {
  font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); letter-spacing: .04em;
  border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; white-space: nowrap;
}

/* Номер правила в клетке матрицы: номер — это адрес правки по СМС, по нему
   правило и находят (F-233). */
.rmx-rc .rc-n { font: 600 var(--fs-tag) var(--mono); color: var(--ink-3); line-height: 1; }

/* «Шаг ещё не заполнен» — словом: «—» одинаково читается и как «пусто», и как
   «минус», и как дефект вёрстки (F-239). */
.cfg-steps .st-empty-w {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3);
}

/* ── шаг «Правила»: панель параметров рядом с таблицей ──────────────────────
   Выбрал строку — страница прокручивалась к панели, и таблица целиком уходила
   под липкую шапку: человек правил правило, не видя ни его номера, ни строки,
   которую выбрал (F-229). Порог — 1700 px, а не 1280, как предполагала находка:
   замерено на стенде, что при 1280 таблице остаётся 550 px, при 1440 — 715 px,
   и предложение правила разваливается на четыре-шесть строк; читать его так
   тяжелее, чем прокручивать. От 1700 px таблице достаётся около 970 px, и
   правило снова читается строкой. Ниже порога сверку берёт на себя крошка в
   шапке панели («№2 · Порог · Т подачи → Горелка»). Матрице ширина нужна вся —
   её вид остаётся одноколоночным при любой ширине. */
@media (min-width: 1700px) {
  .cfg-rules-stage.rul-2col { grid-template-columns: minmax(0, 1fr) 430px; }
  /* Отступ сверху равен высоте липкой шапки: её меряет fitToasts() и кладёт в
     --cfg-toast-top, так что второго измерителя заводить не пришлось. */
  .cfg-rules-stage.rul-2col > .rul-panel { position: sticky; top: var(--cfg-toast-top, 150px); }
}
.rul-panel .rul-crumb {
  margin-inline-start: 10px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── шаг «Выпуск» ──────────────────────────────────────────────────────────
   Состав правил — список, а не абзац через «·»: проверить состав перед
   необратимой отправкой в сплошном тексте невозможно (F-234). */
.psp-rules { margin: 4px 0 0; padding-inline-start: 18px; display: grid; gap: 6px;
  font-size: var(--fs-body); line-height: 1.45; }
.psp-sub { margin-top: 3px; padding-inline-start: 10px; border-inline-start: 2px solid var(--line);
  color: var(--ink-2); }

/* Незаданные координаты — это замечание, а не значение поля: набранные как
   значение (моно, капс-метка слева) они терялись среди настроек (F-225). */
.cfg-warn-line { display: flex; align-items: flex-start; gap: 7px; color: var(--warn-ink);
  font-size: var(--fs-body); line-height: 1.45; }
.cfg-warn-line > svg { flex: 0 0 auto; margin-top: 2px; }
.cfg-warn-line a { color: inherit; }

/* Удаление шаблона стоит в двадцати пикселях от радио-кнопки выбора: до
   наведения или фокуса кнопки не видно вовсе, а само удаление теперь
   спрашивает подтверждение (F-220). На сенсорном экране наведения нет —
   там кнопка видна всегда. */
.tpl-card .tc-del { opacity: 0; transition: opacity var(--dur-2); margin-inline-start: 8px; }
.tpl-card:hover .tc-del,
.tpl-card:focus-within .tc-del { opacity: 1; }
@media (hover: none) { .tpl-card .tc-del { opacity: 1; } }

/* ── выбор платы: поиск, сужение по состоянию, колонка «когда» ──────────────
   Волна 7. Поиск отвечал только на «где эта плата», а вопрос «с чего начать
   сегодня» человек собирал глазами по всему списку (F-243). Правая колонка
   несла голое время без единого слова о том, что это за момент (F-242). */
.cfg-pick-bar { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: var(--sp-2); }
.cfg-pick-bar .cfg-pick-q { flex: 1 1 320px; max-width: 520px; min-width: 0; }
.cfg-pick-f { flex: 0 0 auto; }

/* Колонка «когда» выросла из 40 px под слово + отметку «правки не выпущены».
   Две строки, а не одна: чип отвечает на вопрос «уехало ли», время — «когда»,
   и мешать их в строку значит заставлять читать обе, чтобы понять любую. */
/* Один шаг строки на весь список: у платы с неотправленными правками ячейка
   «когда» в две строки, у остальных — в одну, и без общей высоты список шёл
   рваным ритмом. */
.cfg-pick-row { min-height: calc(var(--row-h) + var(--sp-3)); }
.cfg-pick-row .cfg-pick-ts { width: 176px; display: grid; gap: 2px; justify-items: flex-end;
  font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); text-align: end; }
.cfg-pick-row .cfg-pick-ts .pw-l { white-space: nowrap; }
.cfg-pick-row .cfg-pick-ts .pw-l.muted { color: var(--ink-3); }
.cfg-pick-row .cfg-pick-ts .pw-chip { font-size: var(--fs-tag); }
@media (max-width: 860px) {
  .cfg-pick-row .cfg-pick-ts { width: auto; justify-items: flex-start; text-align: start; }
}

/* ── остаток ёмкости текстового поля ────────────────────────────────────────
   Отношение «7/11» в правом краю поля не говорило ни единицы, ни смысла: знаки?
   места в плате? версия? (F-247). Теперь счётчик молчит, пока места вдоволь, а
   на последних знаках говорит словами — и стоит ПОД полем, а не поверх текста:
   он появляется ровно тогда, когда поле заполнено до края и накрывать его
   нечем. Компонент живёт только в конфигураторе, поэтому и правило здесь. */
.in-count > .in { padding-inline-end: var(--sp-3); }
.in-count::after { content: none; }
.in-count > .in-left {
  display: block; margin-top: var(--sp-1); text-align: end;
  font: 600 var(--fs-micro) var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-2);
}
.in-count.over > .in-left { color: var(--alarm-ink); }

/* ── клеммник: «Шины» ───────────────────────────────────────────────────────
   Строка стала длиннее (порядок «прибор → каналы → опрос»), кнопки заведения
   уезжали за кромку карточки на 1280 px. */
.trm-bus { flex-wrap: wrap; row-gap: var(--sp-2); }

/* ── шаг «Объект»: ритм двух колонок ────────────────────────────────────────
   Слева карточка кончалась на 930 px, справа стопка шла до 1400 — под левой
   зияло полтысячи пикселей пустоты (F-249). Из трёх карточек в две колонки
   балансируется ровно одно расположение: две компактные — рядом, самая длинная
   («Кому звонить и писать») — во всю ширину под ними. Порядок чтения при этом
   тот же, что и порядок работы наладчика: кто и где → как часто на связь →
   кому звонить (номера он спрашивает у заказчика последними). */
.cfg-settings > .cfg-phones { grid-column: 1 / -1; }

/* ── правила: поиск и сужение над таблицей ──────────────────────────────────
   Плата вмещает 48 правил, и в плоском списке из сорока строк нужное искали
   перебором глазами (F-259). */
.rul-find { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: var(--sp-2); }
.rul-find .rul-find-q { flex: 1 1 260px; max-width: 380px; min-width: 0; }
.rul-find .rul-find-n { color: var(--ink-2); font-size: var(--fs-micro); }

/* Вложенная строка «Второй уровень» больше не молчит пустой ячейкой действий:
   она объясняет, почему её нельзя двигать отдельно (F-258). */
.rul-table .rul-sub .rul-tie {
  /* --ink-2, а не --ink-3: это данные, которые читают глазами, а третьи чернила
     по контракту носят только плейсхолдеры и подсказки при наведении. */
  font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); white-space: nowrap;
}

/* Выход, который никто не включает: словом, а не значком «внимание». Тот же
   значок в легенде означал уровень оповещения, и одна картинка несла два
   разных смысла (F-261). */
.rmx .rmx-dead .rmx-off-w,
.rmx-legend .rmx-off-w {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
  color: var(--warn-ink); border: 1px solid var(--warn-line); border-radius: var(--r-xs);
  padding: 0 4px; white-space: nowrap;
}

/* ── предпросмотр СМС в редакторе правила (F-273) ───────────────────────────
   Плашка-«экран телефона»: ровно тот текст, который плата соберёт и отправит
   людям. Моноширинный — потому что счёт знаков в сегменте здесь имеет цену. */
.sms-pv { margin-top: var(--sp-2); display: grid; gap: var(--sp-2); }
.sms-pv .sms-pv-b {
  border: 1px solid var(--line-2); border-radius: var(--r); background: var(--panel-2);
  padding: var(--sp-3); font: 500 var(--fs-body) var(--mono); line-height: 1.45; color: var(--ink);
  overflow-wrap: anywhere;
}
.sms-pv .sms-pv-to { font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); }
.sms-pv .sms-pv-to.warn { color: var(--warn-ink); }
/* Шапка плашки: чем это придёт (СМС или звонок) и влезает ли оно. Счётчик
   знаков стоит У САМОГО текста, а не в подсказке снизу: вопрос «поместится
   ли» задаётся ровно в тот момент, когда читают текст. */
.sms-pv .sms-pv-h {
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
  margin-top: var(--sp-1);
}
.sms-pv .sms-pv-w {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
  color: var(--brand); border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: 0 5px; white-space: nowrap;
}
/* Метка звонка — того же хрома, что и метка СМС. Янтарь здесь читался как
   «внимание», хотя звонок бывает только по аварии: цвет обещал уровень, а
   означал среду доставки. Среды различают СЛОВА и вид самой плашки. */
.sms-pv .sms-pv-len { font: 500 var(--fs-micro) var(--sans); color: var(--ink-2); }
.sms-pv .sms-pv-len.warn { color: var(--warn-ink); }
/* Фраза звонка — речь, а не машинный текст: рубленый шрифт и курсив отделяют
   её от посимвольного СМС, где важен счёт знаков. */
.sms-pv .sms-pv-b.voice { font: 400 var(--fs-body)/1.5 var(--sans); font-style: italic; }

/* ── «Что придёт людям» на шаге «Выпуск» (F-273) ────────────────────────────
   Тот же предпросмотр, собранный по всем оповещающим правилам: последний
   экран перед необратимой отправкой обязан показать не только состав, но и
   то, что от этого состава увидят люди на телефоне. */
.psp-notice { display: grid; gap: var(--sp-3); }
.psp-notice .psp-nb { display: grid; gap: var(--sp-1); }
.psp-notice .psp-nh {
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
  font: 500 var(--fs-micro) var(--sans); color: var(--ink-2);
}
.psp-notice .psp-nn {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); color: var(--ink-2);
}
.psp-notice .psp-nt {
  border: 1px solid var(--line-2); border-radius: var(--r); background: var(--panel-2);
  padding: var(--sp-2) var(--sp-3); font: 500 var(--fs-body) var(--mono); line-height: 1.45;
  color: var(--ink); overflow-wrap: anywhere;
}
.psp-notice .psp-nt.voice { font: 400 var(--fs-body)/1.5 var(--sans); font-style: italic; }
.psp-notice .psp-nw { font: 500 var(--fs-micro) var(--sans); color: var(--warn-ink); }
/* Разделитель строки адресатов — знаком ::before у ВТОРОЙ половины, а не
   отдельным узлом: на 390 px отдельная точка оставалась висеть в конце строки,
   оторванная от того, что разделяет. Цвет — тот же, что у текста: приглушённый
   (--ink-3) давал 3,42:1 в тёмной теме, а видимый знак обязан читаться. */
.psp-notice .psp-naud > span + span::before { content: "· "; color: inherit; }
/* На узком экране половины и так стоят одна под другой — там разделитель
   превращался бы в маркер списка в начале строки. */
@media (max-width: 860px) {
  .psp-notice .psp-naud { display: grid; gap: 2px; }
  .psp-notice .psp-naud > span + span::before { content: none; }
}

/* Чип канала: имя и — у шинного канала — величина, которую он читает, тихой
   второй строкой. Без неё карточка насоса подписана «Насос 1 И»: имя канала
   ужато под 15 мест платы, потому что оно же адрес СМС-команды, и величина в
   нём превращается в букву (K10-45). */
.chan-chip .cc-nm { display: grid; min-width: 0; }
.chan-chip .cc-r {
  font: 500 var(--fs-tag) var(--sans); color: var(--ink-2); line-height: 1.25;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Пустая зона карточки БЕЗ приглашения (со второй и далее): пунктир остаётся —
   он и означает «сюда можно положить», — а высота держится своей, иначе полоса
   схлопывается в нитку и перестаёт читаться как место для канала (F-253). */
.eqc-drop.bare { min-height: 26px; }

/* Строка занятости памяти платы теперь несёт вывод, а не только процент, —
   значит и цвет её должен идти за выводом (F-265). */
.psp-mem { font-family: var(--sans); }
.psp-mem.near { color: var(--warn-ink); }
.psp-mem.over { color: var(--alarm-ink); }

/* ── плашка «оперативные правки на плате» на узком экране ───────────────────
   .banner — флекс из трёх детей; на 390 px одно предложение растягивалось на
   полтысячи пикселей высоты по одному-два слова в строке (F-240). */
.cfg-ovl { flex-wrap: wrap; }
@media (max-width: 860px) {
  .cfg-ovl { display: grid; grid-template-columns: 1fr; gap: 5px; }
  .cfg-ovl > a { justify-self: start; }
}


/* === К7: структура парка, связи плат, группы, новые платы, прошивки === */

/* Кнопка-значок (карандаш, корзина) — квадрат, а не растянутая кнопка:
   у неё нет подписи, и лишняя ширина читается как «здесь что-то пропало».
   Тач-цель на телефоне поднимает общее правило ≤860 px (.btn min 44). */
.k7-ic { padding-inline: var(--sp-2); }

/* Поле формы в модалке: тот же ритм, что у полей общей модалки (.mt),
   вынесен в класс, чтобы отступ не набирался инлайном в пяти местах. */
.k7-f { margin-top: var(--sp-3); }
.k7-f .lb { margin-top: 0; }
/* Поле во всю ширину окна — классом, а не инлайновым style: инлайн переживает
   любую правку темы и первым выпадает из общей ритмики. */
.st-wide, .b-wide { width: 100%; }
.b-intro { margin-bottom: var(--sp-3); }

/* ── Структура парка ───────────────────────────────────────────────────────
   Дерево открывалось одним свёрнутым рядом на пустом экране: чтобы увидеть
   свой парк, человек кликал трижды. Теперь оно раскрыто, и главная работа
   стилей — держать три уровня вложенности читаемыми и не давать строке
   объекта растянуться на 1920 px (имя слева, кнопки у правого края экрана). */
.st-sum {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-5);
  margin-bottom: var(--sp-3);
  font: var(--fs-micro) var(--mono); letter-spacing: var(--ls-tight); color: var(--ink-2);
}
.st-sum-warn { display: inline-flex; align-items: center; gap: var(--sp-1); color: var(--warn-ink); }
/* Мера страницы — общим классом .measured на контейнерах (см. §«мера»),
   своего max-width дерево не заводит. */

/* Шапка узла целиком кликабельна (сворачивание) — значит, она обязана быть
   достижима клавиатурой и показывать фокус. */
.st-tree { gap: var(--sp-3); }
.st-h { cursor: pointer; gap: var(--sp-2); }
.st-h:hover { background: var(--panel-2); }
.st-h:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.st-arw { display: inline-flex; color: var(--ink-2); }
/* Свёрнутый узел — ТОТ ЖЕ значок «вниз», повёрнутый на четверть оборота.
   Направление поворота зависит от направления письма: в арабской раскладке
   свёрнутое содержимое лежит слева, и стрелка обязана указывать туда же
   (контракт F-056 §7 — отдельная иконка «вправо» этого не умеет). */
.st-arw svg { transition: transform var(--dur-1) var(--ease); }
.st-arw.closed svg { transform: rotate(-90deg); }
html[dir="rtl"] .st-arw.closed svg { transform: rotate(90deg); }
.st-name { min-width: 0; overflow-wrap: anywhere; }
/* Три уровня дерева различаются ТИПОГРАФИКОЙ, а не тремя одинаковыми рамками:
   «населённый пункт → предприятие → объект» — смысловая ось экрана, и раньше
   она кодировалась слабее всего (одна и та же рамка, отличие в 26 px отступа). */
.st-l0 { font-family: var(--disp); font-size: var(--fs-h2); font-weight: 700; }
.st-l1 { font-size: var(--fs-val); font-weight: 600; }
/* Капс-подпись уровня: слово вместо догадки по отступу. */
.st-lvl {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap);
  text-transform: uppercase; color: var(--ink-2); white-space: nowrap;
}
.st-cnt { font-weight: 400; white-space: nowrap; }
.st-act { flex-wrap: wrap; gap: var(--sp-2); }

.st-kids { display: grid; gap: var(--sp-2); }
/* Предприятие — не коробка в коробке: рамку заменяет капс-подпись уровня и
   ступень кегля, поэтому контуры трёх уровней перестают «дрожать». */
.st-ent { border: 0; }
.st-eh { padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm); }
/* Объекты — единым списком с вертикальной направляющей и отводом на каждой
   строке: тот же приём, что на мнемосхеме, и он уже в идентичности продукта. */
.st-objs {
  display: grid; gap: 0;
  margin-inline-start: var(--sp-5); padding: 0 var(--sp-3) var(--sp-2) var(--sp-4);
  border-inline-start: 1px solid var(--line);
}

/* Узел без вложений — не пустой прямоугольник, а строка с выходом:
   «здесь ничего нет» без действия остаётся тупиком. */
.st-none {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3); border: 1px dashed var(--line); border-radius: var(--r);
  color: var(--ink-2); font-size: var(--fs-body);
  margin-inline-start: var(--sp-5);
}

.st-obj {
  position: relative;
  display: flex; align-items: center; gap: var(--sp-3);
  border: 0; border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3);
}
/* Отвод к направляющей: короткая линия от строки объекта к вертикали
   предприятия — связь уровней видна без рамок. */
.st-obj::before {
  content: ""; position: absolute; inset-inline-start: calc(var(--sp-4) * -1); top: 50%;
  width: var(--sp-3); border-top: 1px solid var(--line);
}
.st-obj:hover { background: var(--panel-2); }
.st-obj-m { min-width: 0; }
.st-obj-t { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--sp-2); }
.st-obj-name { font-weight: 700; color: var(--ink); }
/* Имя объекта, ведущее на его пульт, обязано ОТЛИЧАТЬСЯ от имени объекта без
   плат — иначе ссылку находят только мышью наугад. Пунктир в покое, сплошная
   фирменная линия при наведении и фокусе. */
a.st-obj-name {
  text-decoration: underline dotted; text-underline-offset: 3px;
  text-decoration-color: var(--line-2);
}
a.st-obj-name:hover, a.st-obj-name:focus-visible {
  color: var(--brand); text-decoration: underline solid; text-decoration-color: var(--brand);
}
/* Подпись объекта — набор фактов чипами: каждый отвечает на свой вопрос
   («сколько плат», «кому звонить», «встанет ли на карту», «в каком поясе»),
   и на узком экране они переносятся по одному, а не рвут строку посередине.
   Кегль чипа здесь на ступень меньше: это подпись строки, а не её главное. */
.st-obj-s { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-2); margin-top: var(--sp-1); }
.st-obj-s .chip { font-size: var(--fs-micro); font-weight: 500; gap: var(--sp-1); padding: 0 var(--sp-2); }
.st-devs { display: inline-flex; flex-wrap: wrap; gap: var(--sp-2); }
.st-devs a { text-decoration: none; }
.st-devs a:hover { border-color: var(--brand); color: var(--brand); }

/* ── Смены отчётов (окно правки границ) ────────────────────────────────────
   Ввод строкой «08:00, 20:00» человек не мог проверить: результат нигде не
   показывался. Здесь время — поле, а сутки — полоса, на которой смены видно. */
/* Строка «что дальше» под деревом. Держится меры страницы и не спорит с
   действиями узлов: это подсказка о следующем шаге, а не пятая кнопка. */
.st-next {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-5);
  margin-top: var(--sp-4);
}

.st-sh-note { margin-bottom: var(--sp-3); }
.st-sh-rows { display: grid; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.st-sh-row { display: flex; align-items: center; gap: var(--sp-2); }
.st-sh-lb { margin: 0; flex: 1; }
/* Поле времени: браузер может показывать 12-часовой формат с «AM/PM», и в
   8em он обрезался — ширина считается по самому длинному варианту. */
.st-sh-t { width: 11em; }
.st-sh-prev { margin-top: var(--sp-4); display: grid; gap: var(--sp-2); }
.st-sh-strip {
  display: flex; height: var(--sp-5); border-radius: var(--r-sm); overflow: hidden;
  border: 1px solid var(--line);
}
.st-sh-seg {
  min-width: 0; display: flex; align-items: center; justify-content: center;
  font: 600 var(--fs-tag) var(--mono); color: var(--ink);
}
/* Смены различаются плотностью одной и той же фирменной краски, а не новыми
   цветами: цвет в продукте занят состоянием, и занимать его расписанием нельзя. */
.st-sh-seg.s0, .st-sh-key.s0 { background: color-mix(in srgb, var(--brand) 34%, transparent); }
.st-sh-seg.s1, .st-sh-key.s1 { background: color-mix(in srgb, var(--brand) 16%, transparent); }
.st-sh-seg.s2, .st-sh-key.s2 { background: color-mix(in srgb, var(--brand) 46%, transparent); }
.st-sh-seg.s3, .st-sh-key.s3 { background: color-mix(in srgb, var(--brand) 8%, transparent); }
.st-sh-leg { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); font-size: var(--fs-micro); color: var(--ink-2); }
.st-sh-leg-i { display: inline-flex; align-items: center; gap: var(--sp-2); }
.st-sh-key { width: var(--sp-3); height: var(--sp-3); border-radius: var(--r-xs); display: inline-block; }

/* ── Прошивки (#/ota) ──────────────────────────────────────────────────────
   Экран не отвечал на главный вопрос владельца — как парк распределён по
   версиям и кто отстал: это считалось глазами по строкам таблицы. Полоса
   распределения отвечает на него одним взглядом и работает фильтром. */
/* Сводка версий парка — сетка РАВНЫХ долей (тот же приём, что у .c-status на
   пульте парка). Раньше доля парка задавалась шириной плитки (flex:count), и
   ряд из двух версий вставал рваным: «v.1 / 6» тянулась на 800 px и была на
   девять десятых пуста, «версия неизвестна / 1» сжималась в пилюлю 155 px.
   Долю несёт число, набранное крупно, — ширине её дублировать нечем. */
.ota-bar {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--sp-1); margin-bottom: var(--sp-3);
  border-radius: var(--r);
}
.ota-seg {
  min-width: 0; display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
  gap: var(--sp-1); padding: var(--sp-2) var(--sp-3); cursor: pointer; text-align: start;
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--panel-2); color: var(--ink-2);
  transition: border-color var(--dur-1), color var(--dur-1), background var(--dur-1);
}
.ota-seg:hover { border-color: var(--edge); color: var(--ink); }
.ota-seg:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Самая свежая версия — фирменным цветом: именно к ней парк подтягивают. */
.ota-seg.new { background: var(--brand-soft); border-color: var(--edge); color: var(--ink); }
/* «Версия неизвестна» — не отдельная версия, а пробел в данных: янтарь. */
.ota-seg.unk { background: var(--warn-soft); border-color: var(--warn-line, var(--line)); color: var(--warn-ink); }
.ota-seg.on { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.ota-seg-t { font: 600 var(--fs-micro) var(--mono); letter-spacing: var(--ls-tight); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.ota-seg-n { font: 700 var(--fs-h2) var(--disp); color: var(--ink); font-variant-numeric: tabular-nums; }

/* Три числа текущей волны — рядом с полосой, одной строкой. */
.ota-wave {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-4);
  font: var(--fs-micro) var(--mono); letter-spacing: var(--ls-tight); color: var(--ink-2);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--line); border-radius: var(--r);
}
.ota-wave-h { color: var(--ink); font-weight: 600; }
.ota-wave-bad { color: var(--warn-ink); }

.ota-tbl { overflow-x: auto; }
.ota-cnt { font-weight: 400; }
/* Строка запуска обновления: две подписи над двумя полями и кнопка справа —
   поля выбираются глазами сверху вниз, а не читаются как одна фраза. */
.ota-run { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: var(--sp-1) var(--sp-3); align-items: end; }
.ota-run > .lb { margin: 0; }
.ota-run > :nth-child(1) { grid-column: 1; grid-row: 1; }
.ota-run > :nth-child(2) { grid-column: 1; grid-row: 2; }
.ota-run > :nth-child(3) { grid-column: 2; grid-row: 1; }
.ota-run > :nth-child(4) { grid-column: 2; grid-row: 2; }
.ota-run > :nth-child(5) { grid-column: 3; grid-row: 2; }
.ota-mem { margin: var(--sp-2) 0 0; padding-inline-start: var(--sp-5); }

@media (max-width: 860px) {
  .g-gh .spacer { display: none; }
  /* Ячейка-число матрицы — это КНОПКА (открывает список связей пары), а не
     метка: общее правило поднимает .btn, чип приходится поднимать здесь. */
  .g-mx-n { min-height: var(--ctl-h-lg); min-width: var(--ctl-h-lg); justify-content: center; }
  .g-gh > .btn:first-of-type { margin-inline-start: auto; }
  /* Сводка версий — сетка равных долей (см. .ota-bar выше); на телефоне доля
     просто у́же, перенос делает auto-fit сам. Прежние flex-заплатки сняты:
     на grid-элементах они не работают и только вводят в заблуждение. */
  .ota-bar { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
  .ota-run { grid-template-columns: 1fr; }
  .ota-run > * { grid-column: 1 !important; grid-row: auto !important; }
}

/* ── Новые платы и приборы (#/boards) ─────────────────────────────────────
   Форма стояла инлайновой строкой: «ТЕЛЕФОН ПРИБОРА» с полем в 690 px рядом с
   «PIN» и полем в 80 px, ни одна вертикаль не совпадала, а на 390 px подписи
   отрывались от своих полей. Здесь одна сетка на все формы экрана: подпись
   слева фиксированной колонкой, поле — колонкой по содержимому. */
.b-form { display: grid; gap: var(--sp-3); }
.b-row { display: grid; grid-template-columns: 200px minmax(0, 360px); gap: var(--sp-2) var(--sp-4); align-items: start; }
.b-lb { margin: var(--sp-2) 0 0; }
.b-ctl { min-width: 0; display: grid; gap: var(--sp-1); }
.b-ctl > .in, .b-ctl > select.in { width: 100%; }
/* Поле PIN — два знака: поле в 360 px под них выглядит как ошибка вёрстки. */
.b-ctl > .b-pin { width: 90px; }
/* Строка сверки с заводской базой в подтверждении замены: «не найден» —
   отказ, который случится через секунду, и он обязан отличаться от факта. */
.b-nofact { color: var(--warn-ink); }

/* Галки инверсии выходов: подпись обнимает галку, поэтому нажимается всё
   слово целиком. Раньше это были голые чекбоксы 16×16 с номером отдельным
   span-ом — по цифре кликать было бесполезно. */
.b-inv { display: grid; gap: var(--sp-1); }
.b-inv-l { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-body); color: var(--ink-2); cursor: pointer; }
.b-inv-l:hover { color: var(--ink); }

.b-cards { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.b-cnt { font: 600 var(--fs-micro) var(--mono); color: var(--brand); }
.b-cnt:not(:empty) {
  min-width: var(--sp-5); text-align: center; padding: 0 var(--sp-2); border-radius: var(--r-pill);
  background: var(--brand-soft); border: 1px solid var(--edge);
}
/* Два самостоятельных дела в одной карточке — с заголовками и разделителем:
   без них «Заменить» и «Вывести» читались как одна форма с двумя кнопками. */
.b-two { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr)); gap: var(--sp-5); }
.b-half + .b-half { border-inline-start: 1px solid var(--line); padding-inline-start: var(--sp-5); }
.b-sub { font: 700 var(--fs-h2) var(--disp); color: var(--ink); margin: 0 0 var(--sp-2); letter-spacing: var(--ls-tight); }

@media (max-width: 860px) {
  /* Одна колонка: подпись над полем. Иначе «PIN» заканчивал строку, а его поле
     начиналось со следующей — рядом с чужой подписью. */
  .b-row { grid-template-columns: 1fr; gap: var(--sp-1); }
  .b-lb { margin-top: 0; }
  .b-ctl > .b-pin { width: 110px; }
  .b-half + .b-half { border-inline-start: 0; padding-inline-start: 0; border-top: 1px solid var(--line); padding-top: var(--sp-4); }
}

/* ── Связи плат (#/links) ─────────────────────────────────────────────────── */
.lk-adv { max-width: var(--prose-max); }
.lk-tbl { overflow-x: auto; }
.lk-acts { text-align: end; white-space: nowrap; }
.lk-cnt { font-weight: 400; }
/* Имя платы в таблице — ссылка на её пульт; подчёркивание пунктиром в покое,
   как у имени объекта в структуре: один приём на весь раздел. */
.lk-dev {
  font-weight: 700; color: var(--ink); text-decoration: underline dotted;
  text-underline-offset: 3px; text-decoration-color: var(--line-2);
}
.lk-dev:hover, .lk-dev:focus-visible {
  color: var(--brand); text-decoration: underline solid; text-decoration-color: var(--brand);
}
/* Предупреждение в мастере: значок и текст, а не типографский глиф в строке. */
.lk-warn { align-items: flex-start; }
.lk-warn > svg { flex: 0 0 auto; margin-top: var(--sp-1); }

/* ── Мастер «Связать платы» ────────────────────────────────────────────────
   Шаги — ПИЛЮЛИ, а не поля ввода: рамка с радиусом делала их неотличимыми от
   select.in ниже, а галочка на пройденном шаге приглашала вернуться туда, куда
   клик не вёл. Пройденный шаг теперь настоящая кнопка, непройденный выключен. */
.lk-steps { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.lk-step {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2); background: none; border: 0; border-bottom: 2px solid var(--line);
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
  color: var(--ink-2); text-align: start;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.lk-step-n {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: var(--r-pill);
  border: 1px solid currentColor; font-size: var(--fs-tag);
}
.lk-step-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.lk-step.done { color: var(--ink-2); border-bottom-color: var(--brand-deep); cursor: pointer; }
.lk-step.done:hover { color: var(--brand); }
.lk-step.done .lk-step-n { border-color: var(--brand-deep); color: var(--brand); }
.lk-step.on { color: var(--brand); border-bottom-color: var(--brand); }
.lk-step:disabled:not(.on) { opacity: .55; }
.lk-step:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.lk-f { margin-top: var(--sp-3); }
.lk-f:first-child { margin-top: 0; }
.lk-f > .in, .lk-f > select.in { width: 100%; }
.lk-num { width: 7.5em; }
.lk-when { margin-top: var(--sp-3); }
.lk-conds { display: grid; gap: var(--sp-2); }
/* Карточка условия: выбранная берёт фирменный обод и подложку — это выбор
   одного из четырёх, и он обязан читаться без чтения текста. */
.lk-cond {
  display: flex; gap: var(--sp-2); align-items: baseline;
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--line);
  border-radius: var(--r-sm); cursor: pointer;
}
.lk-cond:hover { border-color: var(--edge); }
.lk-cond.on { border-color: var(--brand); background: var(--brand-soft); }
.lk-mb { margin-bottom: var(--sp-2); }
.lk-mb2 { margin-bottom: var(--sp-3); }
.lk-wz-err:not([hidden]) { margin-top: var(--sp-3); }

/* Сводка шага «проверка»: что произойдёт — и через сколько. Задержка пути
   вынесена из сноски в чип, потому что именно она решает, годится ли связь
   для этой задачи (4 минуты — «включить подкачку», но не «остановить перелив»). */
.lk-sum {
  display: grid; gap: var(--sp-2);
  padding: var(--sp-3); border-inline-start: 3px solid var(--brand);
  background: var(--brand-soft); border-radius: var(--r-sm); margin-bottom: var(--sp-3);
}
.lk-sum-r { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); }
.lk-slow { color: var(--warn-ink); }

/* ── Расширенный режим связей (#/groups) ──────────────────────────────────
   Страницы нет в меню (решение владельца), поэтому у неё есть возврат наверху
   и одно имя с ведущей на неё ссылкой. */
/* Крошка возврата стоит ПЕРЕД липкой шапкой, а та поднята на 24 px вверх
   (margin-block-start у .page-head) и накрывает своим фоном всё, что не
   отодвинулось: без этого запаса единственный выход со страницы-сироты был
   не виден вовсе. */
.g-back { margin-bottom: var(--sp-6); }
.g-back a {
  display: inline-flex; align-items: center; gap: var(--sp-2); text-decoration: none;
  color: var(--ink-2); font: var(--fs-micro) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
}
.g-back a:hover { color: var(--brand); }
.g-tabs { flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.g-mem { display: grid; gap: var(--sp-1); max-height: 40vh; overflow: auto; }
.g-mem-r { display: flex; }
.g-cnt { font-weight: 400; }
.g-mb { margin-bottom: var(--sp-3); }
.g-mb-s { margin-bottom: var(--sp-2); }
.g-wide { width: 100%; }
.g-wrap { flex-wrap: wrap; }
.g-tbl, .g-mx-wrap { overflow-x: auto; }

/* Матрица «кто кем командует». Заголовок столбца несёт слово «исполняет» и
   имя платы под ним: одна общая подпись «источник ↓ · цель →» читалась как
   единое целое, и человек не понимал, что относится к строкам, а что к
   столбцам. */
/* Матрица живёт без класса .t: карточная раскладка ≤860 px разрушила бы
   перекрёстную таблицу. Оформление — те же токены, что у списков. */
.g-mx { width: 100%; border-collapse: collapse; font-size: var(--fs-val); }
.g-mx th {
  vertical-align: bottom; text-align: start; padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line); color: var(--ink-2);
  font: 600 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); text-transform: uppercase;
}
.g-mx td { padding: var(--sp-3); border-bottom: 1px solid var(--line-2); vertical-align: middle; }
.g-mx tr:last-child td { border-bottom: 0; }
.g-mx-dev { font: 600 var(--fs-body) var(--sans); text-transform: none; letter-spacing: var(--ls-tight); color: var(--ink); margin-top: var(--sp-1); }
.g-mx-c { text-align: center; }
/* Пустая ячейка — призрачный плюс: он есть, но не спорит за внимание с
   заполненными ячейками, ради которых на матрицу и смотрят. */
.g-mx-add { opacity: .45; padding-inline: var(--sp-2); }
.g-mx-c:hover .g-mx-add, .g-mx-add:focus-visible { opacity: 1; }
/* Заполненная ячейка — число связей цветом их состояния: протухшая команда
   видна прямо в матрице, а не только в таблице ниже. */
.g-mx-n { cursor: pointer; font-variant-numeric: tabular-nums; border-width: 1px; }
.g-mx-n:hover { border-color: var(--brand); }

.g-cell { display: grid; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.g-cell-r { border: 1px solid var(--line); border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3); }
.g-cell-t { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }
.g-acts { text-align: end; white-space: nowrap; }
/* Шапка карточки группы: имя, отметки и три действия. На узком экране она
   ОБЯЗАНА переноситься — иначе ряд кнопок выталкивает страницу вбок. */
.g-gh { flex-wrap: wrap; row-gap: var(--sp-2); }
.g-nores { color: var(--warn-ink); }

@media (max-width: 860px) {
  /* На 390 px карточка разваливалась: карандаш висел над строкой, «+ предприятие»
     переносилось на две, крестик уезжал под него. Заголовок узла и его действия
     становятся двумя ярусами, а кнопки — одной строкой во всю ширину. */
  .st-h { flex-wrap: wrap; }
  .st-act { width: 100%; justify-content: flex-start; }
  .st-h .spacer { display: none; }
  /* На 390 px направляющая съедает ширину имени: уровень здесь и так виден
     подписью, поэтому отступ минимальный, а отвод убран. */
  .st-objs { margin-inline-start: var(--sp-2); padding-inline: var(--sp-2) 0; }
  .st-none { margin-inline-start: var(--sp-2); }
  .st-obj::before { display: none; }
  .st-obj { flex-wrap: wrap; }
  .st-obj .spacer { display: none; }
  .st-obj .st-act { width: 100%; }
}

/* === К8: доступы, журнал, настройки мониторинга, блокировки === */

/* ── Доступы ───────────────────────────────────────────────────────────────
   Мера строки: на 1920 действия строки отстояли от логина на 1260 px, и глаз
   терял строку на обратном ходе. Семь колонок читаются в 1180 px без запаса
   пустоты, поэтому карточка ограничена жёстче общей меры страницы. */
/* Шапка страницы держит ТУ ЖЕ меру, что и карточка: иначе главное действие
   («Добавить сотрудника») уезжает к краю окна, а список, которым оно
   управляет, кончается на 456 px левее — кнопка читается как элемент
   оболочки, а не как действие над списком. Одно правило на оба узла, чтобы
   мера не разъехалась при следующей правке. */
/* Мера ужата с 1180 до 1020: колонка действий похудела с трёх названных кнопок
   (232 px) до одного «⋮» (64 px), и лишние 170 px уходили в пустоту между
   областью доступа и последним входом — на 1920 глаз снова терял строку. */
.acc-card, .page-head.acc-head { max-width: 1020px; }
/* Обёртка не прокручивается: по той же причине, что и в журнале — не-visible
   значение по любой оси делает обёртку окном прокрутки, липкая шапка колонок
   прилипает к её верху (то есть не работает), а меню «⋮» строки обрезается по
   нижнему краю таблицы. Ширины колонок заданы ниже и укладываются в 861 px, а
   на телефоне строка становится карточкой. */
.acc-scroll { overflow: visible; }
/* Тот же порог, что и в журнале, и по той же причине: в полосе 861…1100 шесть
   колонок шире рабочей области. Там обёртка снова прокручивается вбок, а меню
   «⋮» открывается внутри неё. */
@media (max-width: 1100px) {
  .acc-scroll { overflow-x: auto; }
}
@media (min-width: 1101px) {
  .acc-t thead th { top: 59px; }
}
.acc-t { table-layout: fixed; }
/* Шапка колонки не переносится: «ПОСЛЕДНИЙ ВХОД» разрывался на две строки при
   свободных 130 px рядом. */
.acc-t th { white-space: nowrap; }
/* Ширины заданы шапке: при table-layout:fixed раскладку определяет первая
   строка, и ширина, написанная у ячейки тела, на неё не влияет. Свободное место
   достаётся области доступа — главному на этом экране. */
.acc-t th:first-child, .acc-t td:first-child { white-space: nowrap; width: 140px; }
.acc-t th:nth-child(2) { width: 110px; }
.acc-t th:nth-child(4) { width: 132px; }
.acc-t th:nth-child(5) { width: 260px; }
/* Частое действие («выслать приглашение снова») стоит в колонке состояния —
   прямо под словами, которые оно исправляет; здесь остаётся только «⋮». */
.acc-t th:nth-child(6) { width: 64px; }
.acc-act { width: 64px; }
.acc-btns { display: flex; flex-wrap: nowrap; gap: 6px; justify-content: flex-end; align-items: center; }
.acc-reinv { margin-top: 2px; }

/* Меню редких действий строки. Раскрыватель браузера (<details>) даёт клавиатуру
   и роль кнопки сам; здесь — только вид списка. */
.acc-menu { position: relative; flex: none; }
.acc-menu > summary { list-style: none; }
.acc-menu > summary::-webkit-details-marker { display: none; }
/* Список раскрывается ВБОК, а не вниз: раскрытый вниз, он накрывал кнопку «⋮»
   соседней строки, и следующий щелчок попадал не в неё, а в пункт меню — среди
   пунктов есть «Отключить». Сбоку колонка действий остаётся свободной на всю
   высоту таблицы. */
.acc-menu-box {
  position: absolute; inset-inline-end: calc(100% + 6px); top: 0; z-index: 40;
  /* Ширина под самое длинное имя действия: перенос имени на вторую строку
     превращает список в абзац. */
  min-width: 268px; display: grid; gap: 2px; padding: var(--sp-1);
  background: var(--panel); border: 1px solid var(--edge); border-radius: var(--r);
  box-shadow: var(--elev-2);
}
.acc-mi {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: var(--sp-2) var(--sp-3); border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--ink); font: 500 var(--fs-body) var(--sans);
  text-align: start; cursor: pointer;
}
.acc-mi:hover, .acc-mi:focus-visible { background: var(--panel-3); }
/* Разрушающее действие отличается ЦВЕТОМ ЧЕРНИЛ в покое, а не только при
   наведении: на сенсорном экране наведения не существует. */
.acc-mi.danger { color: var(--alarm-ink); }
.acc-mi.danger:hover, .acc-mi.danger:focus-visible { background: var(--alarm-soft); }
/* Кнопка-значок «изменить область доступа»: до волны это был типографский
   знак «✎» 33×27 px без рамки и без доступного имени — единственный вход в
   область доступа был почти невидим в тёмной теме. */
.acc-ic { padding: 4px 7px; flex: none; }
.acc-sc { display: flex; align-items: flex-start; gap: 6px; }
.acc-scope { min-width: 0; }
/* «Сколько объектов он видит» — то самое число, ради которого сюда заходят;
   имена предприятий сами по себе на этот вопрос не отвечают. */
.acc-cnt { display: block; font: 600 var(--fs-micro) var(--mono); color: var(--ink-2); margin-top: 4px; white-space: nowrap; }
/* Узлы области — метками. Строка через запятую ломалась посередине имени и
   занимала три строки; метка переносится целиком или не переносится вовсе. */
.acc-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.acc-tag {
  display: inline-block; max-width: 100%; padding: 2px 8px;
  border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--panel-2); color: var(--ink-2); font-size: var(--fs-micro);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acc-tag.more { border-style: dashed; background: transparent; }
/* Состояние входа: одно исключение, одна строка про второй фактор, одна — про
   срок ссылки (и та лишь у тех, кто ещё не вошёл). */
.acc-st { display: grid; gap: 4px; justify-items: start; }
.acc-live { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-body); color: var(--ink-2); }
.acc-tfa {
  display: inline-flex; align-items: center; gap: 5px;
  font: 500 var(--fs-micro) var(--mono); color: var(--ink-2);
}
.acc-tfa.on { color: var(--ok-ink); }
.acc-inv { font-size: var(--fs-micro); color: var(--ink-2); }
/* Просроченная ссылка — не авария: её просто надо выдать заново. */
.acc-inv.dead { color: var(--warn-ink); }
.acc-log { display: inline-flex; align-items: center; gap: 4px; color: var(--ink-2); text-decoration: none; }
.acc-log:hover, .acc-log:focus-visible { color: var(--brand); }
/* Описание полномочий роли меняется вместе с выбором; фиксированная высота
   не даёт окну прыгать при переключении. */
.acc-about { min-height: 3.2em; margin-top: 6px; }

/* ── Дерево области доступа ───────────────────────────────────────────────
   Самое сложное разрешение в продукте показывалось самым техническим
   способом — строкой «предпр.:1, объект:3». Дерево с трёхсостоятельными
   галочками и живым счётом «видит N из M» — единственный вид, в котором
   выданный доступ можно проверить глазами. */
.modal:has(.sc-wrap) { width: min(620px, 100%); }
.sc-tree {
  max-height: 46vh; overflow: auto; overscroll-behavior: contain;
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--panel-2); padding: 5px;
}
.sc-row {
  display: flex; align-items: center; gap: 9px; padding: 5px 8px;
  border-radius: 7px; cursor: pointer; min-width: 0;
}
.sc-row:hover { background: var(--panel-3); }
.sc-row:focus-within { outline: 2px solid var(--brand); outline-offset: -2px; }
.sc-l0 { font-family: var(--disp); font-weight: 700; }
.sc-l1 { padding-inline-start: 26px; }
.sc-l2 { padding-inline-start: 50px; font-size: var(--fs-body); }
.sc-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sc-note {
  font: 600 var(--fs-tag) var(--mono); letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-2); white-space: nowrap;
}
.sc-empty { padding: 14px; color: var(--ink-2); font-size: var(--fs-body); }
/* Живой итог под деревом: он и есть ответ на вопрос «что я сейчас выдал». */
.sc-sum {
  margin-top: 10px; padding: 9px 12px; border-radius: var(--r);
  background: var(--brand-soft); border: 1px solid var(--line);
  font-size: var(--fs-body); display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline;
}
.sc-sum .sc-none { color: var(--warn-ink); }
.sc-orph { color: var(--ink-2); font-size: var(--fs-micro); }

html[data-theme="light"] .sc-tree { background: var(--panel); }
html[data-theme="light"] .sc-row:hover { background: var(--panel-2); }

/* ── Журнал ───────────────────────────────────────────────────────────────
   Ширины колонок заданы явно: без них «Объект / плата» получала 114 px против
   1013 px у «Подробностей», DEMO-NAS2 ломался по дефису на две строки, а на
   1920 подробности уезжали на 800 px от колонки «Что». */
/* Обёртка НЕ прокручивается по вертикали. Пара `overflow-x:auto` +
   `overflow-y:visible` в браузере невозможна: любое не-visible значение по одной
   оси делает `auto` и вторую, обёртка становится окном прокрутки — и липкая
   шапка колонок (общий слой, table.t th) прилипает к верху ЭТОЙ обёртки, то
   есть не срабатывает никогда. В журнале это стоило дороже всего: полотно за
   месяц — двадцать с лишним экранов, и со второго уже не видно, в какой колонке
   что. Ширины колонок здесь заданы явно и в 861 px укладываются, а на телефоне
   таблица становится карточками — горизонтальная прокрутка не нужна ни там, ни
   там. */
.au-scroll { overflow: visible; }
/* Шапка колонок уходит под липкую шапку страницы. Значение равно высоте
   .page-head: 12 + 34 + 12 + линия.
   Порог 1101, а не 861: в полосе 861…1100 пять колонок журнала (712 px явных
   ширин + подробности) шире рабочей области, и без прокрутки внутри обёртки
   вбок ехала бы ВСЯ страница — это хуже, чем неработающая липкая шапка. Ниже
   861 таблица и вовсе становится карточками, там ни то, ни другое не нужно. */
@media (max-width: 1100px) {
  .au-scroll { overflow-x: auto; }
}
@media (min-width: 1101px) {
  .au-t thead th { top: 59px; }
}
.au-t { table-layout: fixed; }
.au-t th:nth-child(1), .au-t td:nth-child(1) { width: 132px; }
.au-t th:nth-child(2), .au-t td:nth-child(2) { width: 120px; }
.au-t th:nth-child(3), .au-t td:nth-child(3) { width: 250px; }
.au-t th:nth-child(4), .au-t td:nth-child(4) { width: 210px; }
.au-when { white-space: nowrap; }
/* Имя объекта НЕ режется многоточием посередине («Котельная «Заводская» · Ко…»):
   журнал открывают, чтобы узнать, ГДЕ это случилось, и обрезанный ответ на этот
   вопрос ничем не лучше отсутствующего. Две строки — потолок; шаг строки
   таблицы это выдерживает. */
.au-tgt { white-space: normal; overflow-wrap: anywhere; }
.au-tgn, .au-tg > span:first-child {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.au-tg { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* Технический идентификатор — вторичная моно-подпись, а не заголовок поля. */
.au-id { font: 500 var(--fs-tag) var(--mono); letter-spacing: .1em; color: var(--ink-2); }
.au-unk { display: inline-flex; align-items: center; gap: 7px; flex-wrap: wrap; }
/* Подробности: две строки и не больше. Раньше строка просто обрезалась правым
   краем — доказательная запись обрывалась на полуслове. */
.au-det {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-height: 1.45;
}
.au-raw > summary { cursor: pointer; color: var(--ink-2); }
.au-raw > summary:hover { color: var(--brand); }
.au-raw > pre {
  margin: 6px 0 0; padding: 8px 10px; border-radius: var(--r);
  background: var(--panel-2); border: 1px solid var(--line);
  font: 500 var(--fs-micro) var(--mono); white-space: pre-wrap; word-break: break-word;
  color: var(--ink-2);
}
/* Имя действия обычным текстом. Чип в журнале остаётся ИСКЛЮЧЕНИЕМ (авария —
   красный, отклонение — янтарный), потому что двенадцать одинаковых пилюль
   подряд глаз перестаёт читать вовсе. */
.au-act { color: var(--ink); }
/* Подробности: фраза и, отдельной строкой под ней, строка команды. Раньше
   провод был вклеен в конец фразы и при переносе вставал вплотную к тексту. */
.au-dt { display: grid; gap: 3px; min-width: 0; }
.au-wire {
  font: 500 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap);
  color: var(--ink-2); overflow-wrap: anywhere;
}
/* Код канала внутри фразы («авария Pereliv») — моно-подписью рядом с русским
   именем, тем же приёмом, что в ленте «Аварий». */
.au-code { font: 500 var(--fs-tag) var(--mono); letter-spacing: var(--ls-cap); color: var(--ink-2); }
/* Служебная запись (автор — сервер) остаётся читаемой, но не спорит за внимание
   с действиями людей: ради них журнал и открывают. */
.au-sysrow td { color: var(--ink-2); }

/* Строка фильтров: период сверху, уточнения — под ним. */
.au-flt .card-b { display: grid; gap: var(--sp-3); }
.au-flt2 { display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-2); }
/* Поле с подписью. Плейсхолдер исчезает вместе с ответом на вопрос «по чему
   сужено», как только в поле появляется текст, — подпись остаётся всегда. */
.au-fld { display: grid; gap: var(--sp-1); }
.au-fld .lb { margin: 0; }
.au-fld .in { min-width: 170px; }
#au-f-type { min-width: 260px; }
/* Переключатель служебных записей: сам переключатель, сколько сейчас скрыто,
   и одна строка о том, что это за записи. */
.au-sys {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3);
  padding-top: var(--sp-3); border-top: 1px solid var(--line-2);
}
.au-sysl { display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer; font-size: var(--fs-body); }
.au-hid { font-family: var(--mono); white-space: nowrap; }
.au-syst { flex: 1; min-width: 220px; }

/* ── Настройки мониторинга ────────────────────────────────────────────────
   На 1920 все восемь полей растягивались до 777 px — включая числовое поле
   со значением «8»; форма читалась как незаполненная анкета, а подпись
   отрывалась от значения. Поле настройки шире 360 px не бывает полезнее. */
.mon-grid { grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); align-items: start; gap: 14px; }
.mon-card { min-width: 0; }
.mon-card .card-b { max-width: 520px; }
.mon-form { display: grid; gap: 14px; margin-top: 12px; }
.mon-f { display: grid; gap: 5px; }
.mon-f .in { max-width: 360px; }
.mon-f .in-suffix { max-width: 360px; }
.mon-ctx { color: var(--ink-2); }
.mon-reach { margin: 0 0 10px; }
/* Карточка общих порогов отличается от карточки объекта не только заголовком:
   нажатие «не той» кнопки меняло поведение всего парка. */
.mon-global { border-color: var(--edge); box-shadow: inset 3px 0 0 var(--brand); }
.mon-ovr { display: grid; gap: 6px; margin-top: 12px; }
.mon-ovr-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 6px 10px; border-radius: var(--r);
  background: var(--panel-2); border: 1px solid var(--line);
}
.mon-ovr-n { font-weight: 600; flex: 1; min-width: 0; }

html[data-theme="light"] .mon-ovr-row { background: var(--panel); }

/* ── Блокировки ───────────────────────────────────────────────────────────
   Экран владельческого стоп-крана. Порядок смелости: красная заливка — только
   у полной остановки, ограничение работы обратимо и красной кнопки не носит. */
.ks-ic { display: grid; place-items: center; }
/* Мера пары карточек. По общей мере страницы (1440) карточка ограничения
   растягивалась до 720 px под поле и две кнопки, и связь «поле → кнопка»
   терялась в пустоте; текст при этом шёл строками по 100 знаков — вдвое длиннее
   комфортных. */
.ks-grid { grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); align-items: start; gap: 14px; max-width: 1180px; }
.ks-card { min-width: 0; }
.ks-halt { border-color: var(--alarm-edge); }
.ks-halt .card-h { color: var(--alarm-ink); }
/* Разделитель между группами разной тяжести: «выдать минуты» и
   «заблокировать сейчас» стояли в 8 px друг от друга. */
.ks-sep { height: 1px; background: var(--line); margin: 18px 0 14px; }
.ks-sep2 { margin-top: 26px; padding-top: 14px; border-top: 1px dashed var(--line); }
/* Сводка «что сейчас зажато в парке». */
.ks-locked { display: grid; gap: 10px; }
.ks-none { display: flex; align-items: baseline; gap: 9px; font-size: var(--fs-body); }
.ks-none-ic { color: var(--ok-ink); align-self: center; display: grid; place-items: center; }
.ks-lr {
  display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.2fr) auto;
  gap: 10px 16px; align-items: start;
  padding: 10px 12px; border-radius: var(--r);
  background: var(--panel-2); border: 1px solid var(--line);
}
.ks-lr-n { display: grid; gap: 2px; min-width: 0; }
.ks-lr-s { display: grid; gap: 3px; justify-items: start; min-width: 0; }
.ks-lr-a { justify-content: flex-end; }
.ks-id { font: 500 var(--fs-tag) var(--mono); letter-spacing: .1em; color: var(--ink-2); }
/* «Кто поставил и до каких пор» у выбранной платы. */
.ks-det { display: grid; gap: 3px; justify-items: start; padding-top: 10px; border-top: 1px solid var(--line); }
/* «Плата не на связи» — три ответа на главный вопрос экрана: что будет с
   командой, есть ли путь в обход интернета и с какого номера плата слушает СМС. */
.ks-off { align-items: start; gap: 10px; margin-bottom: 12px; }
.ks-off .small { color: var(--ink-2); margin-top: 3px; }

html[data-theme="light"] .ks-lr { background: var(--panel); }

@media (max-width: 860px) {
  .ks-lr { grid-template-columns: 1fr; }
  .ks-lr-a { justify-content: flex-start; }
}

@media (max-width: 860px) {
  /* Карточная раскладка отменяет фиксированные ширины: в карточке ячейка —
     это строка «подпись → значение», и ширина ей задаётся макетом карточки. */
  .au-t { table-layout: auto; }
  .au-t th:nth-child(1), .au-t td:nth-child(1),
  .au-t th:nth-child(2), .au-t td:nth-child(2),
  .au-t th:nth-child(3), .au-t td:nth-child(3),
  .au-t th:nth-child(4), .au-t td:nth-child(4) { width: auto; }
  .au-det { -webkit-line-clamp: 4; }
  .acc-act { width: auto; }
  .acc-btns { justify-content: flex-start; }
  /* В карточной раскладке «⋮» стоит слева, и список, отсчитанный от кнопки,
     уезжает за край экрана в любую сторону. Здесь он меряется не кнопкой, а
     ячейкой: во всю её ширину, вниз. */
  .acc-act { position: relative; }
  .acc-menu { position: static; }
  .acc-menu-box {
    inset-inline-start: var(--sp-4); inset-inline-end: var(--sp-4);
    top: calc(100% - var(--sp-2)); min-width: 0;
  }
  /* Длинное имя действия в чипе («отменено взятие аварии в работу», 272 px)
     уезжало за правый край карточки на 390 px. Перенос разрешён только этим
     двум таблицам — общий компонент .chip остаётся однострочным. */
  .au-t .chip, .acc-t .chip { white-space: normal; }
  /* Кнопка выгрузки в ряду фильтров прижималась к самому краю экрана. */
  .au-flt2 .spacer { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * ПРИЁМКА ВОЛНЫ 6 (фиксер). Правки по находкам приёмки, которые касаются
 * общих блоков выше. Собраны здесь одним регионом, а не вписаны в чужие
 * секции: так видно, что именно изменила приёмка, и правка не встречается в
 * одной строке с правкой владельца страницы.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Ссылка «перейти к содержимому» ───────────────────────────────────────
   Первая остановка Tab на любой странице. До неё восемнадцать нажатий уходили
   на марку, колокольчик, кнопку сворачивания рейки, четырнадцать пунктов меню
   и «Выйти» — и только девятнадцатое попадало в содержимое (WCAG 2.4.1).
   Со экрана убрана сдвигом, а не display:none и не нулевым размером: скрытая
   так ссылка остаётся в порядке обхода и возвращается по фокусу. */
.skip {
  position: fixed; inset-inline-start: 12px; top: 12px; z-index: 300;
  transform: translateY(-200%);
  display: inline-flex; align-items: center; min-height: var(--ctl-h-lg);
  padding: 10px 14px; border-radius: var(--r);
  background: var(--panel); color: var(--ink); border: 1px solid var(--edge);
  font: 600 var(--fs-body) var(--sans); text-decoration: none;
  box-shadow: var(--shadow);
}
/* И :focus, и :focus-visible. Ссылка получает фокус ТОЛЬКО с клавиатуры (мышью
   по ней не попасть — она за экраном), а полагаться на одну эвристику
   :focus-visible нельзя: она не срабатывает, когда фокус ставят программно. */
.skip:focus, .skip:focus-visible { transform: none; }
@media (prefers-reduced-motion: no-preference) { .skip { transition: transform var(--dur-2); } }
/* Фокус на <main> ставится программно (tabindex="-1") — своё кольцо ему не
   нужно, иначе после перехода по ссылке обводится вся рабочая область. */
.main:focus { outline: none; }

/* ── Порядок кнопок окна на телефоне ──────────────────────────────────────
   Было column-reverse: главная кнопка рисовалась СВЕРХУ, «Отмена» снизу, а по
   Tab (порядок разметки) человек попадал сначала на нижнюю, потом на верхнюю.
   Это WCAG 2.4.3 и 1.3.2, и касалось оно всех окон продукта разом, включая
   опасные подтверждения. Разметка окна — «Отмена», затем главная кнопка;
   обычный column делает порядок обхода тем же, что и порядок на экране, и
   заодно кладёт главную кнопку под большой палец. */
@media (max-width: 860px) {
  .modal .row { flex-direction: column; }
}

/* ── Карточка канала (faceplate) ──────────────────────────────────────────
   1) Панель живёт внутри общей подложки .modal-back — её и поднимает
      наблюдатель ui.js до контракта диалога (role, ловушка фокуса, возврат
      фокуса). Модификатор снимает у подложки затемнение, блюр и центрирующую
      сетку: faceplate — боковая панель у схемы, а не затемняющее окно, и
      рассматривать схему через блюр было бы хуже, чем без него.
   2) Привязка к краю — ЛОГИЧЕСКАЯ. Физический right: 22px оставлял панель у
      правого края и в арабской раскладке, где строки начинаются справа: она
      накрывала ровно то, на что человек смотрит. */
.modal-back.face-back {
  display: block; padding: 0; z-index: 80;
  background: transparent; backdrop-filter: none; animation: none;
}
.face { right: auto; inset-inline-end: 22px; }
/* Появление растёт из того угла, откуда панель приходит: в RTL это левый
   верхний. Правило именное (html[dir=rtl]), чтобы не перебить нижний лист
   телефона, который растёт снизу по центру в обеих раскладках. */
html[dir="rtl"] .face { transform-origin: top left; }
@media (max-width: 860px) {
  /* Нижний лист прибит к обоим краям — логическая привязка ему не нужна. */
  .face { inset-inline-end: 0; }
  html[dir="rtl"] .face { transform-origin: bottom center; }
}

/* ── Панель действий объекта на телефоне ──────────────────────────────────
   На 390 px в ряд помещались два действия из четырёх: «Вернуть автоматике · N»
   и «Ещё» уезжали за экран, а единственным признаком этого была 28-пиксельная
   растушёвка правого края — на кадре она читается как обрезанная рамка кнопки,
   а не как «тяните вбок». Ровно на этой же странице у мнемосхемы есть ЯВНАЯ
   строка о прокрутке, то есть внутри одного экрана было два разных ответа на
   один вопрос. Решаем в пользу более простого: кнопки переносятся по строкам,
   прокрутки нет вовсе, и прятать нечего. Растушёвка при этом обязана уйти —
   на многострочном ряду она гасила бы нижнюю правую кнопку. */
@media (max-width: 860px) {
  .oa-row {
    flex-wrap: wrap; overflow-x: visible;
    -webkit-mask-image: none; mask-image: none;
  }
  html[dir="rtl"] .oa-row { -webkit-mask-image: none; mask-image: none; }
}

/* ── «Показать ещё» в журнале ─────────────────────────────────────────────
   Журнал показывает записи порциями (audit.js): 648 карточек на 390 px давали
   159 000 px непрерывной прокрутки. Подпись «показано X из Y» стоит рядом с
   кнопкой — тот же вид, что у ленты истории объекта. */
.au-more {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px; border-top: 1px solid var(--line);
}
