/* ui.css - библиотека примитивов Controller32. Значения перенесены из прототипа,
   все цвета/радиусы через токены base.css. Классы - единственный способ стилить экраны. */

/* ---- 1. Кнопки: 3 типа x состояния + загрузка ---- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-3);padding:10px 16px;border-radius:var(--r-input);font:600 13px var(--f-ui);cursor:pointer;white-space:nowrap;transition:background var(--tr-tap),border-color var(--tr-tap),color var(--tr-tap)}
/* ОСНОВНАЯ КНОПКА БЕРЁТ ЛЕСЕНКУ АКЦЕНТА, А НЕ ОДНУ ЗАЛИВКУ. Она была плоским зелёным
   прямоугольником: покой, наведение и нажатие - три несвязанных значения одного цвета, и
   единственное, чем кнопка отличалась от наклейки, это скругление. Теперь свет падает сверху,
   как на карточке: заливка идёт с 09 на 08, снизу тёмная кромка 06. Середина градиента - тот
   самый --accent логотипа (08 это он байт в байт), так что цвет продукта не поехал, у него
   появилась глубина. Наведение поднимает обе точки на ступень (10 -> 09), нажатие гасит
   градиент целиком и садится на 06: кнопка выступает под курсором и проваливается под пальцем.
   Подпись --on-accent проверена на всех трёх состояниях: 9.55 / 8.02 / 4.57. */
.btn--primary{border:0;background-color:var(--accent-08);background-image:linear-gradient(var(--accent-09),var(--accent-08));color:var(--on-accent);font-weight:700;box-shadow:inset 0 -1px 0 var(--accent-06)}
.btn--primary:hover{background-color:var(--accent-09);background-image:linear-gradient(var(--accent-10),var(--accent-09))}
.btn--primary:active{background-color:var(--accent-06);background-image:none;box-shadow:none}
.btn--secondary{border:1px solid var(--line-btn);background:var(--btn);color:var(--text-2);box-shadow:var(--edge-light)}
/* Наведение и нажатие БЫЛИ НЕРАЗЛИЧИМЫ: --btn это L* 14.2, а наведение вело на --surface-4
   (15.8) и нажатие на --surface-3 (14.3) - то есть нажатая кнопка возвращалась ровно туда,
   откуда пришла, и «нажалось или нет» приходилось угадывать. Теперь наведение уходит на ступень
   вверх (--surface-5, 17.7), а нажатие на ступень ВНИЗ (--btn-2, 11.3): кнопка светлеет под
   курсором и проваливается под пальцем. */
.btn--secondary:hover{border-color:var(--line-hover);background:var(--surface-5);color:var(--text)}
.btn--secondary:active{background:var(--btn-2);color:var(--text)}
.btn--danger{border:1px solid var(--risk-line-strong);background:var(--risk-ghost);color:var(--risk-text)}
.btn--danger:hover{background:var(--risk-ghost-hover)}
.btn--danger:active{border-color:var(--risk);background:var(--risk-bg)}
.btn:disabled,.btn.is-disabled{border:1px solid var(--line);background:var(--disabled);color:var(--text-off);cursor:not-allowed}
.btn--primary:disabled,.btn--primary.is-disabled{border:0;background-image:none;box-shadow:none}
/* Погашенная кнопка не выступает: она не поверхность, а место, где поверхности нет. */
.btn:disabled,.btn.is-disabled{box-shadow:none}
.btn.is-loading{cursor:progress}
.btn__spin{display:block;width:12px;height:12px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;animation:c32spin .8s linear infinite;flex:0 0 12px}
/* Гашёная кнопка не исчезает: рядом плашка-объяснение, почему погашена */
.btn-reason{display:flex;align-items:center;gap:var(--s-3);padding:11px 13px;border-radius:var(--r-input);border:1px solid var(--line-strong);background:var(--surface-2);box-shadow:var(--edge-light)}
.btn-reason .d{width:7px;height:7px;border-radius:50%;background:var(--text-faint);flex:0 0 7px}
.btn-reason .t{font:500 12.5px var(--f-ui);color:var(--text-muted)}

/* ГНЕЗДО КНОПКИ: кнопка плюс причина, по которой она погашена, ПРЯМО ПОД НЕЙ.
   Плашка-объяснение сверху экрана (.btn-reason) отвечает на вопрос «почему тут вообще ничего
   нельзя», но не отвечает на вопрос «почему нельзя ВОТ ЭТУ». Когда на экране четыре кнопки и
   погашены две, общая плашка врёт про обе. Поэтому причина живёт у каждой кнопки и появляется
   ровно тогда, когда кнопка гаснет: `gate.sync('почему нельзя')` - и то, и другое одним вызовом.
   Собирается через UI.Gate (ui.js), руками этот класс верстать не надо. */
.gate{display:inline-flex;flex-direction:column;align-items:stretch;gap:var(--s-2);min-width:0;max-width:100%}
.gate--wide{display:flex}
.gate--wide .btn{width:100%}
.gate__why{display:flex;align-items:flex-start;gap:var(--s-2);max-width:280px;font:500 var(--t-meta)/1.4 var(--f-ui);color:var(--nd-text);text-wrap:pretty}
.gate__why::before{content:"";width:5px;height:5px;margin-top:5px;border-radius:50%;background:var(--nd);flex:0 0 5px}
.gate__why[hidden]{display:none}
/* Причина «дело не в вас, а в правах или в лицензии» - жёлтая: это отказ, а не незнание. */
.gate--warn .gate__why{color:var(--warn-text)}
.gate--warn .gate__why::before{background:var(--warn)}

/* ---- 2. Карточка / секция ---- */
/* ГЛУБИНА, А НЕ ПЛОСКАЯ ЗАЛИВКА. Карточка и секция отличались от фона только рамкой, поэтому
   экран читался как одна серая плита с расчерченными клетками. Тонкая светлая линия по верхней
   кромке делает поверхность освещённой сверху - тот же приём, что у настоящих панелей приборов,
   и он стоит ноль внимания: заметить его нельзя, а без него всё выглядит наклеенным. */
/* Заливка карточки НЕ ПЛОСКАЯ, а с падением на полторы ступени сверху вниз. Плоский
   прямоугольник на тёмном фоне читается как вырезанная бумага; та же карточка со светом,
   падающим сверху, читается как поверхность прибора. Перепад маленький намеренно (L* 12.5 -> 11.0
   на всю высоту): его нельзя заметить как градиент, но без него светлая кромка сверху не имеет
   продолжения и висит сама по себе. Концы вынесены в переменные, чтобы вложенная карточка
   двигала обе точки разом и оставалась градиентом, а не плоской заплатой на градиенте. */
.card{--card-hi:var(--surface-2);--card-lo:var(--surface);border-radius:var(--r-card);border:1px solid var(--line);background-color:var(--card-lo);background-image:linear-gradient(var(--card-hi),var(--card-lo));padding:18px 20px;box-shadow:var(--edge-light)}
.section{--card-hi:var(--surface-2);--card-lo:var(--surface);border-radius:var(--r-card);border:1px solid var(--line);background-color:var(--card-lo);background-image:linear-gradient(var(--card-hi),var(--card-lo));overflow:hidden;box-shadow:var(--edge-light)}
/* Шапка секции чуть темнее тела и отбита снизу - так секция читается объектом с заголовком, а не
   двумя прямоугольниками подряд. */
/* Свет в шапке падает СВЕРХУ, как и на самой карточке: было наоборот, светлый конец стоял
   внизу, и шапка спорила со светлой кромкой над собой. Ступени те же, перевёрнут порядок. */
.section__head{padding:16px 16px 13px;border-bottom:1px solid var(--line-soft);background:linear-gradient(var(--surface-3),var(--surface-2))}
.section__title{margin:0 0 3px;font:700 16px var(--f-ui)}
.section__sub{margin:0;max-width:55ch;color:var(--text-muted);font-size:12.5px;text-wrap:pretty}
.section__body{padding:18px 20px}

/* ПРАВИЛО ОБЪЁМА: КРОМКА, А НЕ ТЕНЬ. Одно на весь файл, чтобы следующая поверхность не решала
   заново.

   В тёмной теме падающая тень не даёт объёма, она мылит край: чёрное по чёрному расплывается, и
   вместо ребра получается грязное пятно в два-три пикселя. Объём в тёмном интерфейсе даёт
   СВЕТ - тонкая светлая линия по ВЕРХНЕМУ краю, как на настоящей панели прибора.

   Поэтому в библиотеке ровно три состояния поверхности, и четвёртого не заводить:
     выступает   box-shadow:var(--edge-light)  карточка, секция, плашка, таблица, скелетон,
                                               вторичная кнопка, тост, модалка, знак-квадрат
     утоплена    box-shadow:var(--edge-inset)  жёлоб полосы, поле ввода, лог, терминал, гнездо
     ни то ни другое                           знак на заливке: чип, ярлык, точка, отрезок ленты
                                               (на такой площади кромку не видно, а шва добавляет)

   Падающая тень остаётся ровно у того, что действительно ВИСИТ НАД страницей и отбрасывает её
   по-настоящему: модалка, тост, всплывающая подпись графика, плавающая боковина. Там она идёт
   ВМЕСТЕ с кромкой (--shadow-modal,--edge-light), а не вместо неё. Всё остальное объёма из тени
   не получает. */

/* ---- 3. Чип-пилюля статуса ----
   Мелкий, сильно скруглённый, с цветной точкой перед словом. Точка тут не украшение: она
   красится состоянием и читается на периферии зрения раньше, чем слово. Пять слов на весь
   продукт задаёт UI.State - «в норме», «внимание», «снижено», «есть авария», «нет данных».
   Точка окрашена ОТДЕЛЬНЫМ токеном, а не currentColor: у «нет данных» надпись обязана быть
   тише точки, иначе незнание кричит наравне с фактом. */
/* Контур чипа даёт inset-тень, а не border: рамка сдвинула бы каждый чип в кабинете на пиксель.
   Без контура пилюля это пятно заливки, с ним - предмет: та же разница, что между наклейкой и
   кнопкой. Цвет контура берётся из линии своего состояния, поэтому пять чипов различимы даже
   там, где заливка почти не видна. */
.chip{display:inline-flex;align-items:center;gap:var(--s-2);padding:3px 9px 3px 8px;border-radius:var(--r-pill);font:600 var(--t-meta) var(--f-ui);white-space:nowrap;box-shadow:inset 0 0 0 1px var(--chip-line,transparent)}
.chip__dot{width:6px;height:6px;border-radius:50%;background:var(--chip-dot,currentColor);flex:0 0 6px}
/* Надпись чипа «в норме» берёт --ok-text, а не сам --accent. Четыре чипа из пяти уже брали свою
   светлую ступень (--warn-text, --risk-text, --info, --nd-text), и только «в норме» набиралась
   тем же зелёным, что кнопка и ссылка: состояние спорило по весу с действием. Заодно контраст
   поднялся с 5.64 до 7.36 на подложке чипа. */
.chip--ok{background:var(--ok-bg);color:var(--ok-text);--chip-dot:var(--ok);--chip-hot:var(--ok-hot);--chip-line:var(--ok-line)}
.chip--warn{background:var(--warn-bg);color:var(--warn-text);--chip-dot:var(--warn);--chip-hot:var(--warn-hot);--chip-line:var(--warn-line)}
.chip--risk{background:var(--risk-bg);color:var(--risk-text);--chip-dot:var(--risk);--chip-hot:var(--risk-hot);--chip-line:var(--risk-line)}
.chip--info{background:var(--info-bg);color:var(--info);--chip-dot:var(--info);--chip-hot:var(--info-hot);--chip-line:var(--info-line)}
/* «нет данных»: без заливки состояния, надпись тише, граница-хайрлайн вместо фона. */
.chip--nd{background:var(--nd-bg);color:var(--nd-text);--chip-dot:var(--nd);--chip-hot:var(--nd-hot);--chip-line:var(--nd-line)}
/* Чип-кнопка (фильтр, переключатель вкладки): та же пилюля, но с ответом на курсор. Без своей
   ступени наведения фильтр читается как надпись, и владелец не знает, что по нему можно нажать. */
.chip--go{cursor:pointer;border:0;font:600 var(--t-meta) var(--f-ui);transition:background var(--tr-tap)}
.chip--go:hover{background:var(--chip-hot,var(--surface-4))}

/* ---- 4. Таблица (десктоп-грид, липкая шапка; <900 карточки) ---- */
.tbl{border-radius:var(--r-card);border:1px solid var(--line);background:var(--surface);overflow:hidden;box-shadow:var(--edge-light)}
.tbl__scroll{overflow-x:auto}
.tbl__inner{min-width:1040px}
.tbl__head,.tbl__row{display:grid;grid-template-columns:34px 1.5fr 1fr 1.2fr 0.9fr 1.5fr 96px;gap:var(--s-6);align-items:center}
/* Шапка таблицы вторым акцентом: она объясняет, ЧТО в колонках, а не состояние строк, - и
   спокойный цвет отделяет её от данных лучше, чем ещё один оттенок серого. */
.tbl__head{position:sticky;top:0;z-index:5;padding:12px 18px;border-bottom:1px solid var(--line);background:var(--surface-2);font:600 11px var(--f-ui);letter-spacing:0.9px;text-transform:uppercase;color:var(--accent2-dim)}
.tbl__row{padding:14px 18px;border-bottom:1px solid var(--line-row);cursor:pointer;transition:background-color var(--tr-tap),border-color var(--tr-tap)}
/* background-COLOR, а не сокращённое background: сокращённое стирает подложку тона, и цветная
   строка под курсором становилась серой (разбор в разделе 13). */
.tbl__row:hover{background-color:var(--surface-3)}
.tbl__row:last-child{border-bottom:0}
.cell-name{font:600 13.5px var(--f-mono);color:var(--text)}
.cell-sub{font:500 11px var(--f-mono);color:var(--text-faint)}
.cell-txt{font:500 13px var(--f-ui);color:var(--text-3)}
.cell-mono{font:500 12.5px var(--f-mono);color:var(--text-muted)}
@media (max-width:900px){
  .tbl__inner{min-width:0}
  .tbl__head{display:none}
  .tbl__row{grid-template-columns:1fr;gap:var(--s-3);padding:14px;border:1px solid var(--line);border-radius:var(--r-plate-lg);background:var(--surface-2);margin:10px;box-shadow:var(--edge-light)}
  .tbl__row>*{display:flex;justify-content:space-between;gap:var(--s-4)}
  .tbl__row>*::before{content:attr(data-label);font:600 10.5px var(--f-ui);letter-spacing:0.6px;text-transform:uppercase;color:var(--text-faint)}
}

/* ---- 5. Модалка (720px, закреплённые шапка/подвал, 4 стадии) ---- */
.modal-overlay{position:fixed;inset:0;z-index:90;background:var(--overlay);backdrop-filter:blur(6px);display:flex;align-items:flex-start;justify-content:center;padding:5vh 18px 40px;overflow-y:auto}
.modal{width:100%;max-width:720px;border-radius:var(--r-modal);border:1px solid var(--line-btn);background:var(--surface);box-shadow:var(--shadow-modal),var(--edge-light);animation:c32sheet var(--dur-enter) var(--ease-enter);overflow:hidden}
.modal__head{padding:20px 22px 18px;border-bottom:1px solid var(--line);display:flex;align-items:flex-start;gap:var(--s-5)}
.modal__head .grow{flex:1;min-width:0}
.modal__badge{display:inline-flex;align-items:center;gap:var(--s-2);padding:4px 10px;border-radius:var(--r-pill);font:700 11px var(--f-ui);letter-spacing:0.5px;text-transform:uppercase;margin-bottom:9px}
.modal__badge .d{width:6px;height:6px;border-radius:50%;background:currentColor}
.modal__title{margin:0 0 3px;font:700 19px var(--f-ui);letter-spacing:-0.3px}
.modal__sub{margin:0;max-width:55ch;color:var(--text-muted);font-size:13px}
.modal__close{width:32px;height:32px;border-radius:var(--r-btn);border:1px solid var(--line-btn);background:var(--btn);box-shadow:var(--edge-light);color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;flex:0 0 32px}
.modal__close:hover{border-color:var(--line-hover);color:var(--text)}
.modal__close svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}
.modal__body{padding:18px 22px 4px;max-height:52vh;overflow-y:auto}
.modal__foot{padding:16px 22px 20px;border-top:1px solid var(--line);background:var(--surface-2);display:flex;align-items:center;gap:var(--s-4)}
.modal__foot .grow{flex:1;min-width:200px}
.m-block{margin-bottom:18px}
.m-block__label{font:600 10.5px var(--f-ui);letter-spacing:1.1px;text-transform:uppercase;color:var(--accent2-dim);margin-bottom:9px}
.m-will{display:flex;flex-direction:column;gap:var(--s-4)}
/* Мера чтения держится и в модалке: тело шире 70 знаков, а «что произойдёт» и «как откатить» -
   это абзацы, которые читают, а не машинный вывод. Разбор меры - в разделе 23. */
.m-will>div{font-size:13px;color:var(--text-2);max-width:55ch;text-wrap:pretty}
.m-will .arr{color:var(--accent)}
.m-term{border-radius:var(--r-plate);border:1px solid var(--line-mid);background:var(--terminal);box-shadow:var(--edge-inset);padding:12px 14px;font:500 12px var(--f-mono);line-height:1.8;overflow-x:auto}
.m-term div{white-space:pre;color:var(--text-3)}
.m-term .pf{color:var(--term-dim)}
.m-obj{padding:10px 13px;border-radius:var(--r-input);border:1px solid var(--line-mid);background:var(--surface-2);box-shadow:var(--edge-light);font:500 12.5px var(--f-mono);color:var(--text-2);margin-bottom:7px}
.m-rollback{padding:13px 15px;border-radius:var(--r-plate);border:1px solid var(--line-strong);background:var(--surface-2);box-shadow:var(--edge-light);font-size:12.5px;color:var(--text-muted);text-wrap:pretty}
.m-log{border-radius:var(--r-plate);border:1px solid var(--line-mid);background:var(--terminal);box-shadow:var(--edge-inset);padding:13px 15px;min-height:180px;font:500 12.5px var(--f-mono);line-height:1.85}
.m-log div{white-space:pre-wrap;color:var(--text-3)}
.m-log .pf{color:var(--term-dim)}
.m-log.fail{border-color:var(--risk-line-strong)}
.m-log .ok{color:var(--accent)}
.m-log .err{color:var(--risk-text)}
.m-stage-head{display:flex;align-items:center;gap:var(--s-4);margin-bottom:14px}
.m-stage-head .ttl{font:600 13.5px var(--f-ui)}
.m-icon{width:34px;height:34px;border-radius:var(--r-input);box-shadow:var(--edge-light);display:flex;align-items:center;justify-content:center;font:700 15px var(--f-ui);flex:0 0 34px}
.m-icon--ok{background:var(--ok-icon);border:1px solid var(--accent-04);color:var(--accent-09)}
.m-icon--fail{background:var(--risk-icon);border:1px solid var(--risk-line-strong);color:var(--risk-text)}
.m-ack{display:flex;align-items:center;gap:var(--s-4);width:100%;padding:0;border:0;background:transparent;cursor:pointer;text-align:left}
.m-ack .box{width:19px;height:19px;border-radius:var(--r-chip);border:1.5px solid var(--line-input);background:transparent;color:var(--on-accent);font:700 11px var(--f-ui);display:flex;align-items:center;justify-content:center;flex:0 0 19px}
.m-ack.on .box{border-color:var(--accent);background:var(--accent)}
.m-ack .t{font-size:12.5px;color:var(--text-2)}
.m-name-lbl{display:block;font:500 12.5px var(--f-ui);color:var(--text-muted);margin-bottom:7px}
.m-name-lbl .obj{font-family:var(--f-mono);color:var(--risk-text)}
.m-input{max-width:300px;width:100%;padding:10px 13px;border-radius:var(--r-input);border:1px solid var(--line-input);background:var(--terminal);box-shadow:var(--edge-inset);color:var(--text);font:500 13px var(--f-mono);outline:0;transition:border-color var(--tr-tap)}
.m-input:focus{border-color:var(--line-hover)}

/* ---- 6. Тост (3 уровня: получилось / не получилось без поломки / сбой) ---- */
.toast{position:fixed;right:20px;bottom:20px;z-index:95;display:flex;align-items:center;gap:var(--s-5);padding:14px 17px;border-radius:var(--r-plate-lg);background:var(--surface);box-shadow:var(--shadow-toast),var(--edge-light);animation:c32in var(--dur-enter) var(--ease-enter);max-width:420px}
.toast__icon{width:26px;height:26px;border-radius:var(--r-btn-sm);box-shadow:var(--edge-light);display:flex;align-items:center;justify-content:center;font:700 12px var(--f-ui);flex:0 0 26px}
.toast__text{font:500 13px var(--f-ui);color:var(--text)}
.toast__more{font:600 11.5px var(--f-ui);cursor:pointer;background:none;border:0;padding:0}
.toast--ok{border:1px solid var(--accent-04)}
.toast--ok .toast__icon{background:var(--ok-bg);color:var(--accent)}
/* warn - отказ без поломки (не заполнено поле, read-only, нет прав): те же токены, что у
   chip--warn и banner--warn, чтобы уровень читался одинаково по всему кабинету */
.toast--warn{border:1px solid var(--warn-line)}
.toast--warn .toast__icon{background:var(--warn-bg);color:var(--warn-text)}
.toast--warn .toast__more{color:var(--warn-text)}
.toast--err{border:1px solid var(--risk-line-strong)}
.toast--err .toast__icon{background:var(--risk-bg);color:var(--risk-text)}
.toast--err .toast__more{color:var(--risk-text)}

/* ---- 7. Пустые состояния ---- */
.empty{display:flex;flex-direction:column;align-items:center;gap:var(--s-6);padding:64px 24px;border-radius:var(--r-card);border:1px dashed var(--line-strong);background:var(--surface);box-shadow:var(--edge-light);text-align:center}
.empty__rail{display:flex;flex-direction:column;gap:var(--s-1);opacity:.55}
.empty__rail i{display:block;height:5px;border-radius:2px}
/* Знак пустого состояния - три полосы. Раньше все три были зелёными разной глубины, то есть по
   сути одним цветом: знак не читался как знак. Теперь он уходит из бренда во второй акцент, и это
   единственное место, где два акцента встречаются намеренно - маленький, но живой штрих там, где
   владельцу и так нечего смотреть. */
.empty__rail i:nth-child(1){width:38px;background:var(--accent)}
.empty__rail i:nth-child(2){width:24px;background:var(--accent2-deep)}
.empty__rail i:nth-child(3){width:13px;background:var(--accent2-line)}
.empty__glyph{width:44px;height:44px;border-radius:var(--r-plate-lg);border:1px solid var(--line-mid);background:var(--surface-2);box-shadow:var(--edge-light);display:flex;align-items:center;justify-content:center;color:var(--text-dim)}
.empty__glyph svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.empty__title{font:700 17px var(--f-ui)}
.empty__text{color:var(--text-muted);font-size:13px;max-width:55ch;text-wrap:pretty;margin:0}
.empty__src{font:500 11.5px var(--f-mono);color:var(--text-mono)}

/* ---- 8. Скелетоны ---- */
/* Волна медленная намеренно: скелетон это фон ожидания, а не событие. На 1.4 с линейно он
   мигал полосой и тянул взгляд к себе, хотя смотреть там нечего. Длительность и кривая -
   те же, что у живого замера (--dur-live, --ease-live): одно дыхание на весь продукт. */
.skel{background:linear-gradient(90deg,var(--surface-3),var(--surface-6),var(--surface-3));background-size:200% 100%;animation:c32shine var(--dur-live) var(--ease-live) infinite;border-radius:var(--r-chip)}
.skel--line{height:12px;border-radius:4px}
.skel-card{padding:16px 17px;border-radius:var(--r-card);border:1px solid var(--line);background:var(--surface);box-shadow:var(--edge-light);display:flex;flex-direction:column;gap:var(--s-5)}
.skel-rows{border-radius:var(--r-card);border:1px solid var(--line);background:var(--surface);box-shadow:var(--edge-light);overflow:hidden}
.skel-rows__head{padding:12px 18px;border-bottom:1px solid var(--line);background:var(--surface-2)}
.skel-rows__row{display:flex;align-items:center;gap:var(--s-6);padding:15px 18px;border-bottom:1px solid var(--line-row)}
.skel-rows__row .dot{width:8px;height:8px;border-radius:50%;background:var(--line-mid);flex:0 0 8px}
.skel-rows__row .btn-ph{width:86px;height:24px;border-radius:var(--r-btn-sm);background:var(--surface-3);flex:0 0 86px}
.skel__note{margin:var(--s-6) 0 0;font:500 var(--t-label) var(--f-ui);color:var(--text-mono)}
@media (prefers-reduced-motion:reduce){.skel{background:var(--surface-4);animation:none}}

/* ---- 9. Баннеры ---- */
.banner{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-4);padding:11px 22px;width:100%;border-bottom:1px solid transparent}
.banner__text{font:600 12.5px var(--f-ui);flex:1;min-width:200px}
.banner__note{font:500 11.5px var(--f-mono);color:var(--text-mono)}
.banner__btn{padding:7px 12px;border-radius:var(--r-btn-sm);background:transparent;font:600 12px var(--f-ui);cursor:pointer}
.banner__icon{display:flex}
.banner__icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.banner__dot{width:7px;height:7px;border-radius:50%;flex:0 0 7px}
.banner--offline{background:var(--risk-tint);border-bottom-color:var(--risk-line-strong)}
.banner--offline .banner__text,.banner--offline .banner__icon{color:var(--risk-text)}
.banner--offline .banner__btn{border:1px solid var(--risk-line-strong);color:var(--risk-text)}
.banner--warn{background:var(--warn-tint);border-bottom-color:var(--warn-line)}
.banner--warn .banner__text{color:var(--warn-text)}
.banner--warn .banner__dot{background:var(--warn)}
.banner--warn .banner__btn{border:1px solid var(--warn-line);color:var(--warn-text)}

/* Служебное для kitchen-sink */
.ks-grid{display:flex;flex-wrap:wrap;gap:var(--s-6);align-items:flex-start}
.ks-col{display:flex;flex-direction:column;gap:var(--s-4)}
/* Форс-классы состояний - только для витрины (сетка 5 состояний как в Спецификации) */
.btn.is-focus{outline:2px solid var(--accent);outline-offset:2px}
.btn--primary.is-hover{background-color:var(--accent-09);background-image:linear-gradient(var(--accent-10),var(--accent-09))}
.btn--primary.is-active{background-color:var(--accent-06);background-image:none;box-shadow:none}
.btn--secondary.is-hover{border-color:var(--line-hover);background:var(--surface-5);color:var(--text)}
.btn--secondary.is-active{background:var(--btn-2);color:var(--text)}
.btn--danger.is-hover{background:var(--risk-ghost-hover)}
.btn--danger.is-active{border-color:var(--risk);background:var(--risk-bg)}

/* РАСКЛАДКА «ГЛАВНОЕ + БОКОВАЯ ПАНЕЛЬ». Одна деталь вместо шести.

   Эту сетку собирали заново в шести экранах, вставляя стиль из JS прямо в head. Три из шести
   были байт в байт одинаковыми (.dash-grid, .geo-grid, .sec-grid), ещё три отличались на доли
   колонки. Хуже того, две вставки использовали ОДИН И ТОТ ЖЕ идентификатор dash-grid-css: кто
   отрисовался первым, тот и определял раскладку второму, а правка в одном файле молча не
   доезжала до другого.

   Соотношение колонок - это решение о том, что на экране главное. Ему место здесь, а не в шести
   строковых литералах. */
.split{display:grid;grid-template-columns:minmax(0,1.85fr) minmax(300px,1fr);gap:var(--s-7);align-items:start}
/* Боковая панель уже, главное шире: карточка ноды и настройки. */
.split--wide{grid-template-columns:minmax(0,2fr) minmax(280px,1fr);gap:var(--s-6)}
/* Боковая панель фиксированной ширины: мастер настройки. */
.split--fixed{grid-template-columns:minmax(0,1fr) 300px}
@media(max-width:900px){.split,.split--wide,.split--fixed{grid-template-columns:1fr}}

/* ---- 12. ОЧЕРЕДЬ РЕШЕНИЙ (Главная) ----

   Классами, а не атрибутом style, ровно по трём причинам: цветная рейка дела - псевдоэлемент,
   ширина колонок обязана меняться на узком окне (медиазапрос), а разделители полосы показателей
   держатся на :last-child. Ни одного из трёх строчный стиль не умеет, и попытка обойтись им
   кончилась бы либо резаной вёрсткой на ноутбуке, либо счётом «который я по счёту» в JS.

   Главная - единственный экран, который владелец открывает каждый день, поэтому её раскладка
   лежит в библиотеке, а не частной находкой одного файла. */

/* Полоса показателей: ОДНА строка вместо ряда одинаковых карточек. Показатель тут справка, а не
   предмет экрана; предмет ниже, в очереди. */
.pulse{display:flex;flex-wrap:wrap;border:1px solid var(--line);border-radius:var(--r-card);background-color:var(--surface);background-image:linear-gradient(var(--surface-2),var(--surface));overflow:hidden;box-shadow:var(--edge-light)}
/* Основа 155px, а не 185: у владельца с двумя каскадами измеренных колонок пять, и при
   прежней основе пятая вместе с колонкой «нет данных» не помещалась в строку - полоса
   переносилась, а справа от перенесённой части зияла пустота во всю ширину экрана. */
.pulse__cell{flex:1 1 155px;min-width:0;margin:0;padding:12px 16px 13px;border:0;border-right:1px solid var(--line-row);background:transparent;color:inherit;font:inherit;text-align:left}
.pulse__cell:last-child{border-right:0}
.pulse__cell--go{cursor:pointer}
.pulse__cell--go:hover{background:var(--surface-3)}
/* Незнание не имеет права весить столько же, сколько факт: неизмеренные показатели уходят в одну
   узкую колонку мелким набором, а не занимают по полосе каждый. */
/* Колонка «нет данных» ужимается вместе со всеми: она не должна выталкивать факты. */
.pulse__cell--nd{flex:0 1 210px;background:var(--surface-2)}
.pulse__lab{display:flex;align-items:center;gap:6px;margin-bottom:6px;font:600 var(--t-caps) var(--f-ui);letter-spacing:1px;text-transform:uppercase;color:var(--accent2-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pulse__dot{width:6px;height:6px;border-radius:50%;flex:0 0 6px}
.pulse__sub{margin-top:4px;font-size:11.5px;line-height:1.35;color:var(--text-dim)}
.pulse__nd{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:7px 12px;align-items:baseline}
/* «нет данных» переносом по слогам выглядит как поломка вёрстки, а не как честный ответ. */
.pulse__nd>div{white-space:nowrap;text-align:right}
/* Балл здоровья - тонкой полоской под колонкой нод: это оценка того же самого парка, и отдельной
   секции на весь экран он не стоит. */
.pulse__hp{display:flex;align-items:center;gap:8px;margin-top:9px}
.pulse__track{display:block;height:3px;flex:0 0 72px;border-radius:2px;background:var(--bar-track);overflow:hidden}
.pulse__fill{display:block;height:100%;border-radius:2px}
.pulse__hp span{font:500 var(--t-meta) var(--f-mono);color:var(--text-mono)}

/* Очередь дел. */
.queue{border:1px solid var(--line);border-radius:var(--r-section);background:var(--surface);overflow:hidden;box-shadow:var(--edge-light)}
.queue__head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-4);padding:13px 18px;border-bottom:1px solid var(--line-row);background:linear-gradient(var(--surface-3),var(--surface-2))}
.queue__title{font:700 15px var(--f-ui)}
.queue__sort{margin-left:auto;font:500 var(--t-label) var(--f-ui);color:var(--text-faint)}
.queue__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-4);padding:11px 18px;border-top:1px solid var(--line-row);background:var(--surface-2);font-size:12.5px;color:var(--text-dim)}
.tag{display:inline-flex;align-items:center;padding:3px 8px;border-radius:var(--r-chip);font:600 var(--t-meta) var(--f-ui);white-space:nowrap}
/* ЯРЛЫК ЗНАЕТ СВОИ ЦВЕТА САМ. Пять ярлыков очереди («Критично», «Предупреждение», «следствие
   дела 1») собирались в экране строкой style с парой цветов, подобранной на месте: библиотека
   давала форму, а цвет каждый раз изобретали заново, и мини-шкала состояния до ярлыка не
   доходила. Заливка берётся с самой плотной ступени (--X-bg, та же, что у чипа), надпись - своя
   светлая ступень состояния. Замерено: 7.36 / 7.05 / 6.07 / 5.88 / 5.43. */
.tag--ok{background:var(--ok-bg);color:var(--ok-text)}
.tag--warn{background:var(--warn-bg);color:var(--warn-text)}
.tag--risk{background:var(--risk-bg);color:var(--risk-text)}
.tag--info{background:var(--info-bg);color:var(--info-text)}
.tag--nd{background:var(--nd-bg);color:var(--nd-text)}
/* Ярлык не о состоянии, а о принадлежности продукту («рекомендовано», «по умолчанию»): вторая
   ступень лесенки под самой светлой. Точки нет намеренно - с точкой он читался бы как чип
   состояния «в норме», а это не состояние. Замер 11.09. */
.tag--accent{background:var(--accent-02);color:var(--accent-12)}

/* .task - ДЕЛО АБЗАЦЕМ - снят. Заголовок предложением, объяснение вторым предложением и колонка
   кнопок под ними давали четыре строки прозы на дело и двадцать строк на очередь из пяти.
   Владелец прочитал это и сказал, что читать по-прежнему нельзя. Дело теперь строка с
   постоянными колонками: раздел 24, .rows. Класс удалён целиком, чтобы его не позвали снова. */

/* Дел нет. Это результат проверки, а не пустая таблица, поэтому блок, а не серая заглушка. */
.calm{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--s-7);padding:28px 26px}
.calm__ico{width:46px;height:46px;border-radius:13px;background:var(--ok-icon);border:1px solid var(--accent-04);box-shadow:var(--edge-light);color:var(--accent-09);display:flex;align-items:center;justify-content:center;flex:0 0 46px}
.calm__ico svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.calm h2{margin:2px 0 6px;font:700 20px/1.2 var(--f-ui);letter-spacing:-0.3px}
.calm p{margin:0;max-width:55ch;font-size:13.5px;line-height:1.55;color:var(--text-muted);text-wrap:pretty}
@media(max-width:640px){.calm{grid-template-columns:minmax(0,1fr)}}

/* ---- 13. ТОН СОСТОЯНИЯ: КАРТОЧКА КРАСИТСЯ ЦЕЛИКОМ ----

   Главный приём всей системы. До него состояние жило только в цветном чипе размером с ноготь, а
   плашка под ним оставалась нейтральной при любой погоде. Экран с двумя авариями и экран, где всё
   в порядке, выглядели одинаково: чтобы понять, что горит, владелец обязан был ЧИТАТЬ. Он
   приходит сюда в двух случаях, и в первом («клиенты жалуются») читать некогда.

   Теперь тон разливается по всему блоку: подложка и граница берут цвет состояния, и экран
   читается пятнами до чтения текста. Красное пятно видно с другого конца комнаты, чип - нет.

   КАК УСТРОЕНО. Класс .tone-* не красит напрямую, он выставляет четыре переменные, а красит их
   одно общее правило. Из этого следует главное удобство: тон вешается на ЛЮБУЮ поверхность с
   рамкой - .card, .section, .queue, .pulse__cell, .task, строку таблицы, .nest, - и везде ведёт
   себя одинаково. Внутри блока можно взять цвет состояния, не зная, какой он: var(--tone-ink)
   для надписи, var(--tone-dot) для точки или рейки.

   ПОДЛОЖКА КЛАДЁТСЯ ПОВЕРХ --tone-base, А НЕ ВМЕСТО НЕЁ. Тинты - полупрозрачные; если поставить
   такой тинт просто фоном, он смешается не с поверхностью карточки, а с тем, что под карточкой,
   и карточка с аварией окажется ТЕМНЕЕ соседней спокойной. Поэтому тинт идёт вторым слоем
   background-image поверх непрозрачной базы: результат не зависит от того, на чём лежит блок.

   ОГОВОРКА ПРО ПОРЯДОК. Правило равно по весу тому, что задаёт фон примитиву (.card и прочие),
   поэтому этот раздел стоит последним в файле. Если экранный класс из shell.css красит тот же
   узел, тон надо ставить после него - иначе фон перебьют.

   Использование:
     h('div', { class: 'card tone-risk is-lit' }, ...)   авария, подсвечена изнутри
     h('div', { class: 'card tone-warn' }, ...)          предупреждение
     h('div', { class: 'card tone-nd' }, ...)            нет данных: легче нормы (см. ниже)
     без класса                                          норма: нейтрально, как было */
.tone-ok,.tone-warn,.tone-risk,.tone-info,.tone-nd{
  background-color:var(--tone-base);
  /* СВЕТ ГОРИТ САМ, ПОТОМУ ЧТО ЕГО СИЛА И ЕСТЬ СМЫСЛ. Раньше свечение включалось только руками,
     классом .is-lit, и почти нигде включено не было: тонированная плашка оставалась плоской
     заливкой с рамкой. Лестница силы уже посчитана в base.css и сама расставляет громкость -
     норма 0 (не светится ВОВСЕ), незнание 0, «к сведению» 0.06, «внимание» 0.10, «авария» 0.18.
     Гасить её целиком у всех, кроме одного блока, значит выбросить ту самую лестницу: экран с
     аварией и экран, где всё спокойно, снова выглядят одинаково плоско.
     Кто важнее, решает не факт свечения, а его СИЛА: рядом стоящие «внимание» и «авария»
     различаются вдвое, и красный угол виден раньше, чем прочитано слово.
     НО В ПОЛНУЮ СИЛУ СВЕТИТСЯ НЕ ВСЯКИЙ ТОН. Когда светилось всё подряд, лестница терялась
     обратно: замер на Главной дал два пятна 720x480 и 720x365, треть экрана залита, и очередь
     читалась одним мазком - граница между группой одной ноды и группой другой размывалась.
     Поэтому фоновый тон берёт ПОЛОВИННОЕ свечение (--tone-soft), а .is-lit возвращает полное
     (--tone-lit): им экран помечает ГЛАВНЫЙ блок, и тот снова виден раньше соседа.
     Разметку экранов трогать не надо: .is-lit уже стоит там, где надо. */
  --tone-glow:var(--tone-soft,var(--tone-lit));
  /* СЛОЙ СВЕЧЕНИЯ ПЕРЕСОБИРАЕТСЯ ЗДЕСЬ, И ЭТО НЕ ПРИХОТЬ, А ЕДИНСТВЕННЫЙ РАБОЧИЙ СПОСОБ.

     Готовый --glow-layer из base.css собран правильно и читает --tone-glow. Но var() ВНУТРИ
     значения пользовательского свойства подставляется там, где это свойство ОБЪЯВЛЕНО, а не
     там, где его позвали. --glow-layer объявлен на :root, где --tone-glow не существует, -
     значит ещё на :root он схлопывается в свой запасной вариант (transparent), и вниз по дереву
     наследуется уже прозрачный градиент. Цвет состояния до него не доезжает никогда.

     Это не рассуждение, а замер в браузере. У карточки `card tone-risk is-lit` на экране
     здоровья было так:
       --tone-glow      rgba(201,133,122,0.18)          <- цвет честно посчитан
       background-image radial-gradient(120% 150% at 82% -20%,
                        rgba(0,0,0,0), rgba(0,0,0,0) 62%)  <- и выброшен
     То есть свечения не было НИ У ОДНОГО блока кабинета, при живом токене и зелёной сборке.

     Объявление того же слоя ЗДЕСЬ, в одном блоке с --tone-glow, лечит ровно это: подстановка
     идёт на самом элементе, где к этому моменту известны и цвет состояния, и геометрия. Сам
     градиент не выдуман заново - он байт в байт повторяет --glow-layer и по-прежнему стоит на
     токенах геометрии, поэтому .glow-tl / .glow-br и переносы угла ниже продолжают работать.

     Правило на будущее: токен, внутри которого живёт var() на переменную состояния, обязан
     объявляться там, где эта переменная есть. На :root такой токен - всегда своё умолчание. */
  --glow-layer:radial-gradient(var(--glow-w) var(--glow-h) at var(--glow-x) var(--glow-y),var(--tone-glow,transparent),transparent var(--glow-fade));
  background-image:var(--glow-layer),
                   linear-gradient(var(--tone-tint),var(--tone-tint));
  border-color:var(--tone-line);
}
.tone-ok{--tone-base:var(--surface);--tone-tint:var(--ok-tint);--tone-wash:var(--ok-wash);--tone-hot:var(--ok-hot);--tone-line:var(--ok-line);--tone-line-hot:var(--ok-line-hot);--tone-lit:var(--ok-glow);--tone-soft:var(--ok-glow-soft);--tone-plate:var(--ok-plate);--tone-plate-hot:var(--ok-plate-hot);--tone-ink:var(--ok-text);--tone-dot:var(--ok)}
.tone-warn{--tone-base:var(--surface);--tone-tint:var(--warn-tint);--tone-wash:var(--warn-wash);--tone-hot:var(--warn-hot);--tone-line:var(--warn-line);--tone-line-hot:var(--warn-line-hot);--tone-lit:var(--warn-glow);--tone-soft:var(--warn-glow-soft);--tone-plate:var(--warn-plate);--tone-plate-hot:var(--warn-plate-hot);--tone-ink:var(--warn-text);--tone-dot:var(--warn)}
.tone-risk{--tone-base:var(--surface);--tone-tint:var(--risk-tint);--tone-wash:var(--risk-wash);--tone-hot:var(--risk-hot);--tone-line:var(--risk-line-strong);--tone-line-hot:var(--risk-line-hot);--tone-lit:var(--risk-glow);--tone-soft:var(--risk-glow-soft);--tone-plate:var(--risk-plate);--tone-plate-hot:var(--risk-plate-hot);--tone-ink:var(--risk-text);--tone-dot:var(--risk)}
/* ПЯТОЕ СОСТОЯНИЕ НАКОНЕЦ УМЕЕТ КРАСИТЬ БЛОК. «К сведению» было единственным из пяти, у кого
   был чип, но не было тона: карточку «это не поломка, просто знайте» приходилось красить жёлтым
   (врёт про тревогу) или не красить вовсе (тонет среди спокойных). Тон синеватый, из семьи
   второго акцента, и с жёлтым «вниманием» его не спутать даже боковым зрением. */
.tone-info{--tone-base:var(--surface);--tone-tint:var(--info-tint);--tone-wash:var(--info-wash);--tone-hot:var(--info-hot);--tone-line:var(--info-line);--tone-line-hot:var(--info-line-hot);--tone-lit:var(--info-glow);--tone-soft:var(--info-glow-soft);--tone-plate:var(--info-plate);--tone-plate-hot:var(--info-plate-hot);--tone-ink:var(--info-text);--tone-dot:var(--info)}
/* НЕЗНАНИЕ ЛЕГЧЕ ЖИВОГО. База уходит на уровень фона страницы, то есть блок ПРОВАЛИВАЕТСЯ, а не
   выступает; граница пунктирная - контур есть, объекта нет; своего цвета у состояния нет вовсе,
   поэтому спутать его с нормой или с отказом нечем. */
.tone-nd{--tone-base:var(--canvas);--tone-soft:transparent;--tone-tint:var(--nd-tint);--tone-wash:var(--nd-wash);--tone-hot:var(--nd-hot);--tone-line:var(--nd-line);--tone-line-hot:var(--nd-line-hot);--tone-lit:transparent;--tone-plate:var(--nd-plate);--tone-plate-hot:var(--nd-plate-hot);--tone-ink:var(--nd-text);--tone-dot:var(--nd);border-style:dashed}
/* ПЛАШКА ДЕЛА: СТРОКА НЕСЁТ СВОЙ ТОН ЦЕЛИКОМ.

   Здесь была ступень ВНИЗ: строка списка брала --X-wash (0.03) вместо подложки карточки, чтобы
   двадцать цветных строк не слились в сплошное поле. Рассуждение было верное, а результат -
   нет: строка отличалась от спокойной соседки в 1.06 раза, то есть не отличалась, и всю тяжесть
   дела несла рейка в 3px у левого края - 0.4% площади строки. Заметить её можно, только если
   заранее знаешь, куда смотреть.

   Владелец сказал про это дословно: «мне не нравятся все таблицы где просто слева красный жёлтый
   или зелёный, сделай более полноценно то есть прямо всю плашку цветом и более понятно что не
   так». Теперь ступень идёт ВВЕРХ, к --X-plate, и авария видна боковым зрением.

   СВЕТОФОРА НЕ ВЫХОДИТ, ПОТОМУ ЧТО ГРОМКОСТЬ РАВНА ТЯЖЕСТИ. Ступень --X-plate у каждого
   состояния своя (разбор и замеры в base.css): норма 0.05 и незнание 0.045 почти не красятся,
   авария 0.175 красится в полную силу. Экран, где всё спокойно, остаётся спокойным - иначе
   заливка съела бы ровно ту разницу, ради которой заведена.

   Заодно строка получает СВОЮ РАМКУ: --tone-line-hot вместо тихой --tone-line. Плашка обязана
   иметь край, иначе цвет растекается по ленте без границ между делами.

   Имя .tone--wash оставлено рабочим: им размечены живые экраны, и переименование ради честности
   слова стоило бы правки в каждом. Новый код пишет .tone--plate - то же правило, честное имя.
   Ставится вдогонку к тону: class="tbl__row tone-warn tone--plate". */
/* И СВЕТ ЗДЕСЬ ГАСНЕТ ЦЕЛИКОМ. Свечение - это про блок, который обязан быть замечен первым;
   строка в списке такой задачи не решает. Двадцать строк, каждая со своим источником света,
   складываются в сплошную цветную простыню, на которой не выделяется ни одна, - то есть в ровно
   ту беду, от которой заведён сам модификатор. Проверено на стенде ещё на прежней заливке: у
   строки таблицы с `tone-risk tone--wash` она была 0.035, а свечение осталось бы 0.18 - в пять
   раз громче собственной подложки строки, и модификатор переставал что-либо значить. Заливка с
   тех пор выросла до 0.175, но вывод не изменился: свет всё равно громче её. */
.tone--wash,.tone--plate{
  --tone-tint:var(--tone-plate,var(--tone-wash,var(--tone-tint)));
  --tone-line:var(--tone-line-hot,var(--tone-line));
  --tone-glow:transparent
}
/* НАВЕДЕНИЕ НА ПЛАШКУ ИДЁТ ПО СВОЕЙ ЛЕСТНИЦЕ. Общее правило ниже подставляет --tone-hot (0.09),
   а это ТИШЕ самой плашки аварии (0.175): курсор гасил бы тон вместо того, чтобы его усилить.
   Вес селектора здесь выше нарочно - двумя классами, а не !important. */
.tbl__row.tone--wash:hover,.tbl__row.tone--plate:hover,
.tone--wash.tone-go:hover,.tone--plate.tone-go:hover{background-color:var(--tone-base);--tone-tint:var(--tone-plate-hot,var(--tone-hot))}
/* ТАБЛИЦА ПОДЧИНЯЕТСЯ ТОМУ ЖЕ ПРАВИЛУ БЕЗ ЛИШНЕГО КЛАССА. Жалоба владельца была про «все
   таблицы», а модификатор стоял ровно на трёх экранах из двадцати трёх: остальные брали подложку
   КАРТОЧКИ (0.06-0.07) - одинаково тихую и у нормы, и у аварии. Строка таблицы с тоном - это и
   есть плашка дела, отдельного разрешения ей не нужно. */
.tbl__row.tone-ok,.tbl__row.tone-warn,.tbl__row.tone-risk,.tbl__row.tone-info,.tbl__row.tone-nd{
  --tone-tint:var(--tone-plate);--tone-line:var(--tone-line-hot);--tone-glow:transparent
}
/* И база под курсором остаётся своей. .tbl__row:hover подкладывает --surface-3; сложенная с
   заливкой аварии, эта ступень давала --text-dim 4.27:1 - единственную пару ниже порога во всём
   наборе. Курсор усиливает ТОН, а не подкладывает под него серую ступень. */
.tbl__row.tone-ok:hover,.tbl__row.tone-warn:hover,.tbl__row.tone-risk:hover,
.tbl__row.tone-info:hover,.tbl__row.tone-nd:hover{background-color:var(--tone-base);--tone-tint:var(--tone-plate-hot)}
/* НА ПЛАШКЕ САМАЯ ТИХАЯ КРАСКА ЭТО --text-dim (замер в base.css). Подпись под именем набрана
   --text-faint и на заливке аварии даёт 3.73:1 - ниже порога. Голос состояния читается лучше
   любого серого (5.5-8.2) и связывает строку в один объект. */
.tbl__row.tone-ok .cell-sub,.tbl__row.tone-warn .cell-sub,.tbl__row.tone-risk .cell-sub,
.tbl__row.tone-info .cell-sub,.tbl__row.tone-nd .cell-sub{color:var(--tone-ink)}
/* НАВЕДЕНИЕ НА ТОНИРОВАННЫЙ БЛОК. Раньше курсор ПОДМЕНЯЛ тон серым: .tbl__row:hover ставил
   background целиком, а сокращённое свойство стирает и подложку тона, и её цвет. Строка с
   аварией под курсором переставала быть красной ровно в тот момент, когда владелец в неё
   целится. Теперь наведение усиливает СВОЙ тон на ступень (--X-hot) и подсвечивает свою
   границу, а не заменяет их нейтралью. */
/* СВЕТ ИДЁТ ИЗ ТОГО УГЛА, ГДЕ МЕТКА, И В ЭТОМ КАБИНЕТЕ МЕТКА СЛЕВА.

   Умолчание base.css - правый верхний угол, и рассуждение под ним верное: свет обязан идти
   оттуда, где стоит метка состояния. Только метка тут стоит не там. Посчитано по живым экранам,
   а не по памяти: у всех тонированных блоков на «Здоровье» и «Безопасности» знак состояния -
   первым элементом СЛЕВА, и это ровно те четыре знака, которые даёт сама библиотека:
     .nest    гнездо с иконкой в начале карточки      (8 блоков из 8 на «Безопасности»)
     .tone-dot точка перед надписью показателя         (полоса «парк сейчас», события)
     .task::before цветная рейка дела                  (очередь на главной)
     .pulse__dot точка колонки                         (полоса показателей)
   Чипа в правом углу шапки в продукте нет ни одного. Значит умолчание для тона - левый угол,
   иначе свет каждый раз врёт про источник, а прежняя пара исключений (очередь и полоса) была
   не исключением, а правилом, написанным дважды.

   :where() НАМЕРЕННО: у него нулевой вес, поэтому .glow-tr / .glow-br из base.css по-прежнему
   перебивают это умолчание одним классом в разметке - у блока, который однажды поставит метку
   справа, способ сказать об этом остаётся. Обычный селектор из ui.css убил бы их молча: ui.css
   грузится после base.css, вес совпал бы, и выиграл бы файл, который ниже. */
:where(.tone-ok,.tone-warn,.tone-risk,.tone-info,.tone-nd){--glow-x:18%}
.tone-go{cursor:pointer;transition:background-color var(--tr-tap),border-color var(--tr-tap)}
.tbl__row:hover,.pulse__cell--go:hover,.tone-go:hover{--tone-tint:var(--tone-hot,var(--tone-tint));--tone-line:var(--tone-line-hot,var(--tone-line))}
/* .is-lit - полная сила свечения против половинной у фонового тона (см. выше). Класс стоит в
   семи экранах и означает «этот блок главный»: на Главной им помечена САМАЯ СРОЧНАЯ группа
   очереди, и без разницы в силе она переставала отличаться от соседней. */
.is-lit{--tone-glow:var(--tone-lit)}
/* Шапка секции под тоном не должна перебивать его своей заливкой. */
.tone-ok>.section__head,.tone-warn>.section__head,.tone-risk>.section__head,.tone-info>.section__head,.tone-nd>.section__head{background:none;border-bottom-color:var(--tone-line)}
/* НАДЗАГОЛОВОК В СВЕТЯЩЕМСЯ УГЛУ БЕРЁТ ГОЛОС СОСТОЯНИЯ. Это не вкус, а следствие включённого
   света, и оно посчитано.

   Свет идёт из того угла, где метка, а метка стоит рядом с надзаголовком - значит надзаголовок
   оказался ровно в самом ярком месте блока. Своя краска у него самая тихая из допустимых
   (--accent2-dim), и правило base.css про это сказано прямо: в светящемся углу текст не тише
   --text-dim. Замер по геометрии слоя на колонке полосы показателей (свет 0.18 в пике, до
   надзаголовка доходит 0.10):
     авария    --accent2-dim 4.21   --tone-ink (--risk-text) 5.72
     внимание  --accent2-dim 4.37   --tone-ink (--warn-text) 7.30
   Обе пары своей краской ниже порога 4.5, обе тоном - выше с запасом.

   ТОЛЬКО У ЭТИХ ДВУХ СОСТОЯНИЙ, потому что только они светятся настолько: «к сведению» даёт
   4.61, а «норма» и «незнание» не светятся вовсе (4.93 и 5.75). Перекрашивать их значило бы
   менять цвет там, где он ничего не чинит.

   Заодно это честнее по смыслу: надзаголовок в красном блоке, набранный синим вторым акцентом,
   спорит с блоком, в котором стоит. Второй акцент остаётся тем, чем был - краской СТРУКТУРЫ, -
   везде, где блок не заявляет тревогу. */
.tone-warn .plate__lab,.tone-warn .pulse__lab,
.tone-risk .plate__lab,.tone-risk .pulse__lab{color:var(--tone-ink)}
/* Готовые кисти для содержимого блока: цвет состояния, не зная, какое оно. */
.tone-ink{color:var(--tone-ink)}
.tone-rail{background:var(--tone-dot)}
/* ТОЧКА СОСТОЯНИЯ РАЗЛИЧАЕТСЯ ФОРМОЙ, А НЕ ТОЛЬКО ЦВЕТОМ.
   Замер в оттенках серого: норма 165.2, внимание 167.9, авария 146.7 из 255. Норма и внимание
   отличаются в 1.02 раза, то есть НИКАК: при дальтонизме, на плохом мониторе и в распечатке это
   один и тот же пиксель. Цвет - самый ненадёжный канал, и держать на нём единственный признак
   состояния нельзя.
   Поэтому у каждого состояния СВОЙ СИЛУЭТ, а цвет остаётся вторым признаком поверх формы:
     норма       круг, закрытый и спокойный
     внимание    треугольник вверх: форма сама показывает, что просит взгляда
     авария      квадрат: самый тяжёлый силуэт, виден боковым зрением
     к сведению  кольцо: контур без заливки, легче любого залитого
     незнание    пунктирное кольцо, тот же приём, что у рамки карточки
   Размер гнезда у всех один: строки не должны прыгать от смены состояния. */
.tone-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--tone-dot);flex:0 0 8px}
.tone-warn>.tone-dot,.tone-warn .tone-dot,.tone-dot--warn{
  border-radius:0;clip-path:polygon(50% 0,100% 100%,0 100%)}
.tone-risk>.tone-dot,.tone-risk .tone-dot,.tone-dot--risk{border-radius:1px}
.tone-info>.tone-dot,.tone-info .tone-dot,.tone-dot--info{
  background:transparent;border:1.6px solid var(--tone-dot)}
.tone-nd>.tone-dot,.tone-nd .tone-dot,.tone-dot--nd{
  background:transparent;border:1.4px dashed var(--tone-dot)}

/* ---- 14. ГНЕЗДО ПОД ИКОНКУ ----

   Иконка, лежащая прямо на поверхности карточки, читается как случайный значок и теряется среди
   текста. Та же иконка в квадрате со скруглением на ступень ТЕМНЕЕ карточки читается как знак
   раздела: у неё появляется место, а у карточки - точка входа для глаза.

   Три размера: 26 (в строке списка), 34 (в шапке карточки), 46 (в пустом состоянии и в крупной
   сводке). Гнездо принимает тон: `class="nest tone-risk"` - и подложка с рамкой станут
   красноватыми, а иконка внутри возьмёт цвет состояния. */
/* Гнездо на ступень ТЕМНЕЕ карточки, то есть это врезка, а не приподнятая плашка. Пара к
   правилу светлой кромки: поверхность либо выступает (--edge-light), либо утоплена
   (--edge-inset). Без тени сверху квадрат читался просто как более тёмное пятно. */
.nest{display:flex;align-items:center;justify-content:center;width:34px;height:34px;flex:0 0 34px;border-radius:var(--r-input);border:1px solid var(--nest-line);background-color:var(--nest);background-image:linear-gradient(var(--surface-0),var(--nest));box-shadow:var(--edge-inset);color:var(--text-dim)}
.nest svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.nest--sm{width:26px;height:26px;flex:0 0 26px;border-radius:var(--r-btn-sm)}
.nest--sm svg{width:14px;height:14px}
.nest--lg{width:46px;height:46px;flex:0 0 46px;border-radius:var(--r-plate-lg)}
.nest--lg svg{width:22px;height:22px;stroke-width:1.9}
/* Тон гнезда СТАВИТСЯ ЗДЕСЬ, а не наследуется из раздела 13. Причина в весе селекторов:
   .nest задаёт background и border СОКРАЩЁННЫМИ свойствами и стоит в файле ниже .tone-*, а
   сокращённое свойство сбрасывает и подложку, и цвет границы целиком. Вес у обоих правил
   одинаковый (один класс), выигрывает то, что ниже, - и «гнездо принимает тон» из комментария
   выше на деле не работало: краснела только иконка внутри. Правило из двух классов тяжелее и
   возвращает обещанное поведение. */
.nest.tone-ok,.nest.tone-warn,.nest.tone-risk,.nest.tone-info,.nest.tone-nd{
  color:var(--tone-dot);
  background-color:var(--tone-base);
  background-image:linear-gradient(var(--tone-tint),var(--tone-tint));
  border-color:var(--tone-line);
}
/* Гнездо со своим тоном берёт базой не --canvas, а поверхность: оно лежит НА карточке. */
.nest.tone-nd{--tone-base:var(--nest);border-style:solid}

/* ---- 15. ДРОБЬ: ЧИСЛИТЕЛЬ ЯРЧЕ ЗНАМЕНАТЕЛЯ ----

   19/28 - это не одно число, а ответ на вопрос «сколько из скольких». Набранные одинаково, обе
   половины весят поровну, и владелец каждый раз заново решает, какая из них важна. Важна левая:
   правая - это парк, он меняется раз в месяц. Поэтому числитель полной яркостью, косая черта и
   знаменатель приглушены на две ступени.

   Верстать руками не надо, есть UI.Frac(19, 28). Разделитель по умолчанию «/», для мест, где
   читается словом, - UI.Frac(5, 7, { sep: ' из ' }). */
.frac{display:inline-flex;align-items:baseline;font:700 var(--t-metric)/1.05 var(--f-mono);font-variant-numeric:tabular-nums;letter-spacing:-0.5px;color:var(--text);white-space:nowrap}
.frac__sep{margin:0 1px;font-weight:500;color:var(--text-faint)}
.frac__den{font-weight:600;color:var(--text-dim)}
.frac--sm{font-size:var(--t-title);letter-spacing:-0.2px}
.frac--lg{font-size:var(--t-hero);letter-spacing:-0.8px}
/* Словесный разделитель («5 из 7») набирается интерфейсным шрифтом и мельче: это слово, не знак. */
.frac__sep--word{font-family:var(--f-ui);font-size:var(--t-label);margin:0 4px 0 3px}

/* ---- 16. НЕЗНАНИЕ В НАБОРЕ ----

   Показатель, которого нет, набирается иначе, чем показатель, который есть: мельче, тише и без
   моноширинной «цифровой» подачи. Одна строка причины под ним обязательна - «нет данных» без
   объяснения владелец читает как поломку кабинета, а не как отсутствие источника.

   Разметка:
     h('div', { class: 'nd-val' }, 'нет данных')
     h('div', { class: 'nd-why' }, 'агент CrowdSec не подключён') */
.nd-stack{display:inline-flex;flex-direction:column;align-items:flex-start;min-width:0;max-width:100%}
.nd-val{font:600 var(--t-title) var(--f-ui);letter-spacing:0;color:var(--nd-text)}
.nd-why{margin-top:3px;max-width:55ch;font:500 var(--t-meta)/1.4 var(--f-ui);color:var(--text-off);text-wrap:pretty}

/* ---- 17. ПЛАШКА: ВЛОЖЕННОСТЬ, КОТОРУЮ ВИДНО ----

   Карточка в карточке в карточке рисовалась одним и тем же .card или инлайновым background, и
   три уровня вложенности сливались в один прямоугольник с лишними рамками внутри. Глубина в
   тёмном интерфейсе показывается светлотой, а не рамкой: рамка говорит «тут край», светлота
   говорит «это лежит НА том».

   Три роли, каждая на своей ступени лестницы из base.css:
     .card                 вложенность 1  --surface     карточка на фоне страницы
     .plate                вложенность 2  --surface-3   плашка внутри карточки
     .plate.plate--sub     вложенность 3  --surface-5   плашка внутри плашки
     .plate.plate--well    врезка         --surface-0   жёлоб, лог, поле: внутрь, а не наружу

   Шаг между соседними 3.3 по L*, это замер, а не подбор. Внутрь врезки светлая линия по кромке
   не ставится: она делает поверхность выпуклой, а врезка обязана быть вдавленной. */
.plate{border-radius:var(--r-plate);border:1px solid var(--line-soft);background-color:var(--surface-3);padding:12px 14px;box-shadow:var(--edge-light)}
.plate--sub{background-color:var(--surface-5);border-color:var(--line-mid)}
.plate--well{background-color:var(--surface-0);border-color:var(--line-soft);box-shadow:var(--edge-inset)}
.plate--go{cursor:pointer;transition:background-color var(--tr-tap),border-color var(--tr-tap)}
.plate--go:hover{background-color:var(--surface-5);border-color:var(--line-mid)}
.plate--sub.plate--go:hover{background-color:var(--surface-6)}
/* Надзаголовок плашки: тем же вторым акцентом, что шапка таблицы и подпись блока модалки. */
.plate__lab{margin-bottom:var(--s-2);font:600 var(--t-caps) var(--f-ui);letter-spacing:1px;text-transform:uppercase;color:var(--accent2-dim)}
/* Тон на плашке ставится правилом из двух классов по той же причине, что и у гнезда: .plate
   стоит в файле ниже раздела 13 и при равном весе перебивал бы подложку тона.
   СВЕЧЕНИЕ ПЛАШКА ПОЛУЧАЕТ ТОЖЕ, и это не украшение по инерции: плашка с тоном - это блок
   состояния, а не значок. Разбор происшествия на «Безопасности» и «что сломалось на уровне 2»
   на «Здоровье» свёрстаны именно плашкой, и без света они читались плоской заливкой рядом со
   светящейся карточкой - то есть выглядели МЕНЕЕ важными, чем сводка над ними. Лестница силы
   та же самая: норма и незнание не светятся вовсе. Гнездо (26-46 пикселей) света не получает
   намеренно - на такой площади градиент не даёт объёма, а даёт грязь. */
.plate.tone-ok,.plate.tone-warn,.plate.tone-risk,.plate.tone-info,.plate.tone-nd{
  background-color:var(--tone-base);
  background-image:var(--glow-layer),
                   linear-gradient(var(--tone-tint),var(--tone-tint));
  border-color:var(--tone-line);
}
/* Тон плашки берёт базой поверхность карточки, а не холст: плашка лежит НА карточке. */
.plate.tone-nd{--tone-base:var(--surface-2)}

/* ВЛОЖЕННОСТЬ БЕЗ ЕДИНОЙ ПРАВКИ В ЭКРАНАХ. Карточка внутри карточки и плашка внутри плашки
   встречаются в кабинете постоянно, и все они рисовались одной и той же заливкой: три рамки,
   одна светлота, разобрать вложенность нельзя. Правило само уводит каждый следующий уровень на
   ступень вверх, поэтому старая разметка становится глубже, ничего в ней не меняя.

   Тонированные блоки исключены поимённо: у них подложку задаёт состояние, и переписать её
   глубиной значит потерять цвет аварии на вложенной карточке. */
.card .card:not(.tone-ok):not(.tone-warn):not(.tone-risk):not(.tone-info):not(.tone-nd),
.section__body .card:not(.tone-ok):not(.tone-warn):not(.tone-risk):not(.tone-info):not(.tone-nd){
  --card-hi:var(--surface-4);--card-lo:var(--surface-3);border-color:var(--line-soft);
}
.card .card .card:not(.tone-ok):not(.tone-warn):not(.tone-risk):not(.tone-info):not(.tone-nd),
.section__body .card .card:not(.tone-ok):not(.tone-warn):not(.tone-risk):not(.tone-info):not(.tone-nd){
  --card-hi:var(--surface-6);--card-lo:var(--surface-5);border-color:var(--line-mid);
}
/* Врезка исключена намеренно. Правило вложенности весит (0,2,0) и перебивало .plate--well
   (0,1,0): жёлоб получал --surface-5 вместо --surface-0 и выглядел вдавленным И САМЫМ СВЕТЛЫМ
   одновременно. На «Безопасности» так было покрашено десять врезок из двадцати одной, и оттуда
   же пришла пара 4.45 к 1: --text-off поверх --surface-5 ниже порога, о чём base.css прямо
   предупреждает. Врезка всегда темнее родителя - в этом её смысл, и вложенность его не меняет. */
.plate .plate:not(.plate--well):not(.tone-ok):not(.tone-warn):not(.tone-risk):not(.tone-info):not(.tone-nd){
  background-color:var(--surface-5);border-color:var(--line-mid);
}

/* ---- 18. ПОЛОСЫ, ЛЕНТЫ И ЛЕГЕНДА ----

   Измерение это не вердикт. Полоса нагрузки, лента доступности и разложение балла собирались
   инлайном на каждом экране заново, брали --accent для заливки и --line-mid для жёлоба, и
   «сеть 40%» на карточке ноды оказывалась того же зелёного, что «всё в порядке» рядом.

   Здесь три разных инструмента, и путать их нельзя:
     .bar     ОДНА величина от нуля до предела. Цвет говорит, насколько горячо (--load-1..4).
     .strip   ряд отрезков времени. Цвет каждого - худший вердикт внутри отрезка.
     .meter   разложение целого на части. Цвет говорит, ЧТО это за часть (--chart-1..6),
              и ни одна часть не громче соседней: все шесть стоят на одной светлоте. */

/* Полоса нагрузки. Ширину заливки ставит экран, цвет выбирает по порогу. */
.bar{display:block;height:6px;border-radius:var(--r-pill);background:var(--bar-track);overflow:hidden;box-shadow:var(--edge-inset)}
.bar--thin{height:3px}
.bar--tall{height:9px}
/* Полоса НА СВЕТЛОЙ ПЛАШКЕ: жёлоб обязан стать врезкой, иначе он светлее подложки и полоса
   выглядит наклеенной поверх, а не утопленной в неё. */
.bar--deep{background:var(--bar-track-deep)}
.bar__fill{display:block;height:100%;border-radius:inherit;background:var(--load-1);transition:width var(--dur-enter) var(--ease-enter)}
.bar__fill--l1{background:var(--load-1)}
.bar__fill--l2{background:var(--load-2)}
.bar__fill--l3{background:var(--load-3)}
.bar__fill--l4{background:var(--load-4)}
/* Заливки нет вовсе - показатель не измерялся. Не ноль: ноль это тоже измерение. */
.bar__fill--nd{background:repeating-linear-gradient(90deg,var(--nd-bg) 0 3px,transparent 3px 6px)}

/* Лента отрезков времени: доступность за сутки, история проверок. */
.strip{display:flex;gap:2px;align-items:stretch}
.strip__cell{flex:1 1 0;min-width:3px;height:28px;box-sizing:border-box;border-radius:3px;background:var(--nd-bg);border:1px dashed var(--nd-line)}
.strip__cell--ok{background:var(--load-1);border:0}
.strip__cell--warn{background:var(--load-3);border:0}
.strip__cell--risk{background:var(--load-4);border:0}
.strip--sm .strip__cell{height:16px;border-radius:2px}
.strip__ends{display:flex;justify-content:space-between;margin-top:var(--s-2);font:500 var(--t-meta) var(--f-ui);color:var(--text-faint)}

/* Разложение целого на части. Сегменты идут подряд, ширину каждого ставит экран. */
.meter{display:flex;height:8px;border-radius:var(--r-pill);background:var(--bar-track);overflow:hidden;box-shadow:var(--edge-inset)}
.meter--tall{height:12px}
.meter__seg{height:100%;min-width:2px}
.meter__seg--1{background:var(--chart-1)}
.meter__seg--2{background:var(--chart-2)}
.meter__seg--3{background:var(--chart-3)}
.meter__seg--4{background:var(--chart-4)}
.meter__seg--5{background:var(--chart-5)}
.meter__seg--6{background:var(--chart-6)}

/* Легенда. Без неё разложение это разноцветная полоска, а не ответ на вопрос. */
.legend{display:flex;flex-wrap:wrap;gap:var(--s-2) var(--s-6);margin-top:var(--s-4)}
.legend__item{display:inline-flex;align-items:center;gap:var(--s-2);font:500 var(--t-meta) var(--f-ui);color:var(--text-muted);white-space:nowrap}
.legend__key{width:8px;height:8px;border-radius:2px;flex:0 0 8px}
.legend__key--1{background:var(--chart-1)}
.legend__key--2{background:var(--chart-2)}
.legend__key--3{background:var(--chart-3)}
.legend__key--4{background:var(--chart-4)}
.legend__key--5{background:var(--chart-5)}
.legend__key--6{background:var(--chart-6)}
.legend__val{color:var(--text-2);font-family:var(--f-mono);font-variant-numeric:tabular-nums}

/* ---- 19. ГРАФИК: СЕТКА, ОСЬ, ЛИНИЯ, ПЛОЩАДЬ ----

   Метрики ноды рисуются inline-SVG, и цвета в нём стояли атрибутами прямо в разметке:
   stroke="var(--accent)" для линии, stroke="var(--line)" для сетки, fill="var(--accent-ghost)"
   для площади. Отсюда две беды сразу. Первая: график «Сеть» выходил того же зелёного, что чип
   «в норме» рядом, и измерение читалось как вердикт. Вторая: сетка считалась от фона страницы,
   а график лежит на карточке - на плашке её контраст падал до 1.01, то есть сетки не было.

   Здесь и то и другое стоит на своих токенах: --chart-1..6 для рядов (все на одной светлоте:
   ни один ряд не громче соседа), --chart-N-fill для площади под линией, --chart-grid и
   --chart-axis пересчитаны от поверхностей карточки и плашки (1.23 и 1.49 к --surface).

   Ряд по умолчанию первый, поэтому одиночному графику хватает .chart__line без номера. */
.chart{display:block;width:100%;overflow:visible}
.chart__grid{stroke:var(--chart-grid);stroke-width:1;fill:none;shape-rendering:crispEdges}
.chart__axis{stroke:var(--chart-axis);stroke-width:1;fill:none;shape-rendering:crispEdges}
.chart__lab{fill:var(--text-faint);font:500 var(--t-meta) var(--f-mono);font-variant-numeric:tabular-nums}
.chart__line{fill:none;stroke:var(--chart-1);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.chart__area{stroke:none;fill:var(--chart-1-fill)}
.chart__line--2{stroke:var(--chart-2)}
.chart__line--3{stroke:var(--chart-3)}
.chart__line--4{stroke:var(--chart-4)}
.chart__line--5{stroke:var(--chart-5)}
.chart__line--6{stroke:var(--chart-6)}
.chart__area--2{fill:var(--chart-2-fill)}
.chart__area--3{fill:var(--chart-3-fill)}
.chart__area--4{fill:var(--chart-4-fill)}
.chart__area--5{fill:var(--chart-5-fill)}
.chart__area--6{fill:var(--chart-6-fill)}
/* Отметка под курсором: вертикаль по всей высоте и точка на линии. Обводка точки цветом
   поверхности, а не белым: точка обязана лежать НА графике, а не светиться над ним. */
.chart__cursor{stroke:var(--line-hover);stroke-width:1;shape-rendering:crispEdges}
.chart__dot{fill:var(--chart-1);stroke:var(--surface);stroke-width:2}
/* Всплывающая подпись значения. Плашка третьего уровня: она лежит НАД карточкой с графиком,
   поэтому светлее её на две ступени и с падающей тенью. */
.chart__tip{position:absolute;z-index:2;pointer-events:none;padding:8px 10px;border-radius:var(--r-plate);border:1px solid var(--line-mid);background:var(--surface-5);box-shadow:var(--shadow-float),var(--edge-light);font:500 var(--t-meta)/1.55 var(--f-mono);font-variant-numeric:tabular-nums;color:var(--text-2);white-space:nowrap}
.chart__tip b{color:var(--text);font-weight:600}

/* ---- 20. ДВИЖЕНИЕ: ОНО ВСЕГДА ЧТО-ТО ЗНАЧИТ ----

   Кабинет открывают, когда что-то сломалось. Анимация тут не украшение и не «оживление»: она
   отвечает на один вопрос - ЧТО ИМЕННО ИЗМЕНИЛОСЬ, пока владелец смотрел в другое место.
   Отсюда весь раздел и его границы.

   Пять работ, и каждая двигает ровно то, что изменилось:
     появилась запись      она въезжает сверху - откуда приходит новое в списке, отсортированном
                           по свежести. Остальные строки стоят на месте.
     обновилось число      подсвечивается ЦИФРА, а не карточка. Карточка не менялась.
     сменилась вкладка     едет подчёркивание. Содержимое подставляется без движения: оно и есть
                           то, ради чего вкладку нажали, и читать его начинают немедленно.
     идёт загрузка         медленная волна по скелетону (раздел 8), а не мигание.
     идёт живой замер      одна тихо дышащая точка (.live-dot).

   Чего тут нет и не заводить:
     - наведение не двигает НИЧЕГО. Ни подпрыгиваний, ни увеличений, ни теней, которые растут.
       Меняются только заливка и граница: на плотном экране в двадцать строк любое смещение под
       курсором превращает список в зыбь. Три интерактивные поверхности библиотеки (.tone-go,
       .plate--go, .tbl__row) сделаны ровно так, и четвёртая обязана.
     - пружин нет: перелёт за конечное значение читается дребезгом (разбор в base.css).
     - длительностей ровно три, все из base.css. Кратные вроде calc(var(--dur-enter)*3) считаются
       той же шкалой; новое число - нет.

   ГАСИТСЯ ВСЁ. Собранное на токенах замирает само (--dur-* уходят в ноль), keyframes добивает
   правило по звёздочке в base.css, а бесконечным анимациям тут вдобавок сказано animation:none:
   дыхание длиной .001мс - это дрожь, то есть ровно то, от чего человек защищается.
   Договор прежний: движение не имеет права быть ЕДИНСТВЕННЫМ носителем смысла. Живая точка
   остаётся цветной точкой, новая запись - записью, свежее число - числом. */

/* Вход сверху: так приходит новое в список, где самое свежее сверху. Снизу въезжала бы запись,
   которую только что дописали в конец, и направление врало бы о порядке.
   both: до старта запись уже невидима, после финиша остаётся на месте, без отката. */
@keyframes c32top{from{opacity:0;transform:translateY(-7px)}to{opacity:1;transform:none}}
.is-new{animation:c32top var(--dur-enter) var(--ease-enter) both}

/* ОБНОВИЛОСЬ ЧИСЛО - СВЕТИТСЯ ЧИСЛО. Мигать всей карточкой нельзя: карточка не менялась, а глаз
   всё равно уходит к самому крупному движению и ищет там новость, которой нет. Подсветка - тёплая
   вспышка позади цифр вторым акцентом (он несмысловой, тревоги не заявляет) и гаснет за три
   такта появления. Фон, а не цвет самой цифры: перекрасить цифру значит на полсекунды соврать
   про её состояние. */
@keyframes c32fresh{
  from{background-color:var(--accent2-ghost);box-shadow:0 0 0 4px var(--accent2-ghost)}
  to{background-color:transparent;box-shadow:0 0 0 4px transparent}
}
.is-fresh{border-radius:var(--r-key);animation:c32fresh calc(var(--dur-enter) * 3) var(--ease-enter) both}

/* СМЕНА ВКЛАДКИ ДВИГАЕТ ТОЛЬКО ПОДЧЁРКИВАНИЕ. Полоса жила ровно под активной вкладкой и
   возникала из ниоткуда: переключение читалось как перерисовка всей ленты. Теперь черта есть у
   КАЖДОЙ вкладки, просто сжата в ноль по ширине; активная разворачивает свою. Двигается два
   пикселя высоты, буквы и содержимое стоят.
   Правило живёт здесь, а не рядом с .sectabs в shell.css, потому что это раздел про движение:
   в shell.css остаётся вид полосы, тут - её поведение. ui.css грузится следом, вес тот же.

   ПЕРЕХОДА ОДНОГО НЕ ХВАТАЕТ, И ЭТО ПРОВЕРЕНО, А НЕ ПРЕДПОЛОЖЕНО. Лента вкладок собирается
   заново на каждый переход (screens.js:149 строит .sectabs целиком, app.js:160 подставляет
   экран), а `is-active` во всём кабинете ставится ровно один раз - в момент создания кнопки
   (screens.js:157), и больше не переключается нигде. Свежевставленный узел в переход не
   входит: он рождается уже в конечном значении. То есть transition ниже сам по себе не
   срабатывал НИ РАЗУ, и «едет подчёркивание» оставалось обещанием в комментарии.
   Поэтому черту разворачивает анимация входа - она играет на появлении узла, то есть ровно
   тогда, когда владелец нажал вкладку. Переход оставлен рядом: он подхватит случай, когда
   `is-active` однажды переключат на живом узле, и оба кончаются в одном и том же scaleX(1). */
@keyframes c32tab{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.sectab::after{content:"";position:absolute;left:var(--s-4);right:var(--s-4);bottom:0;height:2px;border-radius:2px 2px 0 0;background:var(--accent);transform:scaleX(0);transform-origin:center;transition:transform var(--tr-tap)}
.sectab.is-active::after{transform:scaleX(1);animation:c32tab var(--dur-enter) var(--ease-enter)}

/* ЖИВОЙ ЗАМЕР: ОДНА ТОЧКА НА ЭКРАН. Дыхание означает «данные идут прямо сейчас». Две дышащие
   точки на одном экране означают уже ничего: движение перестаёт быть новостью и становится
   фоном. Точка остаётся ЦВЕТНОЙ точкой при выключенном движении, то есть факт «живое» никуда
   не девается, уходит только дыхание.
   Соседей CSS погасить умеет, дальних родственников - нет: правило ниже страхует список, где
   точку по недосмотру поставили каждой строке, но «одна на экран» держится соглашением. */
.live-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--tone-dot,var(--accent));flex:0 0 7px;animation:c32pulse var(--dur-live) var(--ease-live) infinite}
.live-dot ~ .live-dot{animation:none;opacity:.55}

@media (prefers-reduced-motion:reduce){
  .is-new,.is-fresh{animation:none}
  .live-dot,.live-dot ~ .live-dot{animation:none}
  /* Черта остаётся на месте, просто встаёт сразу: подчёркивание - единственный признак того,
     какая вкладка открыта, и гасить его вместе с движением значит потерять факт. */
  .sectab::after{transition:none}
  .sectab.is-active::after{animation:none;transform:scaleX(1)}
}

/* ---- 21. ТИПОГРАФИКА: ВИДНО, КТО ГОВОРИТ ----

   Два шрифта в продукте различают не размер и не важность, а ГОВОРЯЩЕГО:

     моноширинный  всё, что сказала машина и что владелец может сверить с другим экраном:
                   адреса и домены, идентификаторы и UUID, версии, замеры и единицы, время,
                   коды ответов, пути ручек, имена файлов и нод.
     гротеск       всё, что продукт говорит владельцу: заголовки, объяснения, причины, подписи
                   кнопок, «уже 14 минут назад», «сначала срочные», «загружаем ноды».

   Правило простое и проверяемое: если строку можно скопировать и вставить в терминал или в
   поиск - она машинная. Если её можно только прочитать - она наша.

   До этого выбор делался на месте, в каждом вызове: `'font:600 var(--t-meta) ' + (o.mono ?
   'var(--f-mono)' : 'var(--f-ui)')`. Поэтому «уже 14 минут назад» и «сначала срочные» набирались
   как вывод команды, а домен внутри абзаца - как обычное слово. Теперь это два класса, и в
   спорном месте достаточно ответить на вопрос «кто это сказал».

   .mono несёт вместе со шрифтом и цифры по разряду: одинаковая ширина знака у чисел - половина
   смысла моноширинного набора, иначе столбец замеров всё равно пляшет.
   .say нужен внутри машинного блока: слово продукта посреди лога или адреса. */
.mono{font-family:var(--f-mono);font-variant-numeric:tabular-nums;letter-spacing:0}
.say{font-family:var(--f-ui)}

/* ---- 22. ЧИСЛА В ТАБЛИЦЕ: ПО ПРАВОМУ КРАЮ И ПО РАЗРЯДУ ----

   Число в таблице читают не по значению, а по ДЛИНЕ: 1014 и 98 сравнивают глазом по тому, где
   кончается столбик знаков. Прижатые влево, они начинают сравниваться по первой цифре, и 98
   выглядит больше 1014 ровно до того момента, как владелец вчитается.

   Поэтому три вещи сразу и всегда вместе: правый край, моноширинный набор, цифры равной ширины
   (tabular-nums). Порознь они не работают: правый край без равной ширины всё ещё пляшет, равная
   ширина без правого края не выстраивает разряды в столбик.

   На узком окне таблица становится карточками (раздел 4), подпись уходит влево, значение
   вправо - и правило продолжает выполняться само, без второго набора стилей.

   Заголовок числовой колонки берёт тот же класс: подпись, стоящая над столбцом слева, а сам
   столбец справа, разваливает колонку на две.
     h('div', { class: 'num' }, '1014')
     h('div', { class: 'num', 'data-label': 'Клиентов' }, '1014') */
.num{font-family:var(--f-mono);font-variant-numeric:tabular-nums;letter-spacing:0;text-align:right}
.tbl__head .num{text-align:right}
/* Моноширинные ячейки таблицы тоже выравнивают разряды: в них живут id, версии и замеры. */
.cell-name,.cell-sub,.cell-mono{font-variant-numeric:tabular-nums}

/* ---- 23. СТРОКА ОБЪЯСНЕНИЯ: ОКОЛО СЕМИДЕСЯТИ ЗНАКОВ ----

   Объяснение - это одна-две строки под заголовком, ради которых владелец и понимает, что
   случилось. На широком мониторе они растягивались во всю карточку: 120-140 знаков в строке,
   и глаз на обратном ходе теряет начало следующей. Читаемая мера - 60-75 знаков, дальше
   скорость чтения падает независимо от кегля.

   Мера задана в ch, потому что ch считается от СВОЕГО кегля: одна запись работает и на 13px
   объяснения карточки, и на 11px подписи под показателем.

   ПОЧЕМУ 55, А НЕ 70. ch - это ширина знака «0», а в Onest нуль шире среднего знака кириллицы
   ровно в 1.28 раза (замер в браузере: 8.95px против 7.02px при кегле 13.5px). Значит 70
   настоящих знаков - это 70 / 1.28 = 55ch. Записать сюда 70ch значило бы получить строку в
   девяносто знаков, то есть ровно ту, от которой уходим.

   Класс - для нового кода; готовые объяснения библиотеки (.section__sub, .modal__sub,
   .empty__text, .task__cost, .calm p, .nd-why, .m-will>div) меру уже держат сами.

   ЧТО ПОД ПРАВИЛО НЕ ПОПАДАЕТ, чтобы следующий замер не начал «чинить» исправное: машинный
   вывод (.m-term, .m-log, ячейки таблицы), однострочные подписи (чип, ярлык, кнопка) и
   плашка с рамкой, у которой текст лежит прямо в ней (.m-rollback): обрезав ей ширину, мы
   обрежем и рамку, и коробка встанет вполовину модалки. Такой строке достаётся только
   text-wrap:pretty - последнее слово не уезжает на свою строку. */
.explain{max-width:55ch;text-wrap:pretty}

/* ---- 24. СТРОКА-ДЕЛО: ШЕСТЬ ПОСТОЯННЫХ КОЛОНОК ----

   БЕДА, РАДИ КОТОРОЙ ЭТО ЕСТЬ, И ОНА ЗАМЕРЕНА НА ЖИВОМ СНИМКЕ ГЛАВНОЙ. Дело очереди было
   абзацем: чип тяжести, возраст, заголовок ЦЕЛЫМ ПРЕДЛОЖЕНИЕМ («Клиентский порт ноды Нью-Йорк
   не отвечает»), объяснение ВТОРЫМ предложением («Клиенты через эту ноду не подключаются.») и
   ссылка на улику. Четыре строки на дело, пять дел - двадцать строк сплошного текста. Сведения
   в них были все до одного, а прочитать их было нельзя: у прозы нет колонок, и глаз обязан
   читать каждое дело целиком, чтобы сравнить его с соседним.

   ЧТО ИЗМЕНИЛОСЬ. Дело стало СТРОКОЙ С ПОСТОЯННЫМИ КОЛОНКАМИ, и колонки одни и те же на всех
   тяжёлых экранах продукта - очередь Главной, находки «Здоровья», происшествия «Безопасности»,
   список нод. Глаз бежит СВЕРХУ ВНИЗ по колонке, а не читает текст слева направо:

     ВАЖНОСТЬ   форма плюс цвет, без слова. Слово дублирует цвет и съедает колонку
     ГДЕ        объект коротко: «Нью-Йорк», а не «нода Нью-Йорк»
     ЧТО        три-четыре слова, НЕ предложение: «порт не отвечает»
     КОГО       ЧИСЛОМ: «143 клиента», а не «клиенты не подключаются»
     КОГДА      «14 мин», а не «уже 14 минут назад»
     ДЕЙСТВИЕ   одна кнопка или прочерк
     (жёлоб)    18px под уголок раскрытия. Это не колонка данных, а место под ручку

   ШИРИНЫ ФИКСИРОВАНЫ, И ЭТО ГЛАВНОЕ СВОЙСТВО. Колонка читается сверху вниз только тогда, когда
   у всех строк она начинается в одном и том же пикселе. Растяжимая ровно одна - «что»: у неё
   единственной длина текста заранее неизвестна. Ширины лежат в переменных на .rows, поэтому
   экран может подвинуть их ОДНОЙ записью, а не пересобирать сетку в каждой строке.

   ЗАКРЫТАЯ СТРОКА - ОДНА ВЫСОТА У ВСЕХ. Любой текст в «где» и «что» обрезается многоточием, а
   полностью живёт в атрибуте title и в раскрытии. Строка, которая переносится на вторую строчку
   от длинного имени ноды, ломает столбик соседей и возвращает ту самую прозу.

   ПОДРОБНОСТИ - ПОД РАСКРЫТИЕ. Строка отвечает «что и насколько плохо». Раскрытие отвечает
   «почему так решили и что делать». Ни одно слово из раскрытия не имеет права переехать в
   строку: так абзац и вырастает обратно. */
.rows{
  /* Ширины колонок ОДНОЙ записью. Экран, которому нужна своя раскладка, ставит их на .rows -
     и все строки внутри едут вместе, потому что читают эти же переменные. */
  --col-sev:40px; --col-where:176px; --col-who:124px; --col-when:74px;
  --col-act:140px; --col-open:18px; --col-gap:var(--s-6);
  /* ЛЕНТА МЕРЯЕТ СЕБЯ, А НЕ ОКНО. Ступени ширины ниже считаются от ЭТОГО блока: та же лента
     живёт и во всю страницу, и в колонке рядом с рельсом, и по окну её не различить. Пока
     ступени висели на @media, лента в колонке шириной 640 пикселей на широком мониторе честно
     считала себя широкой и давила колонку «что» до «З..» - то есть ровно в ту нечитаемость,
     ради которой строки и заводились. */
  container-type:inline-size;
  border:1px solid var(--line);border-radius:var(--r-section);background:var(--surface);
  overflow:hidden;box-shadow:var(--edge-light)
}
/* Шапка ленты: имя списка и то, что экран кладёт справа (режим, сортировка, счётчик). */
.rows__cap{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-4);padding:13px 16px;border-bottom:1px solid var(--line-row);background:linear-gradient(var(--surface-3),var(--surface-2))}
.rows__cap-t{font:700 15px var(--f-ui);color:var(--text)}
.rows__cap-r{margin-left:auto;display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-3)}
/* Подписи колонок. Вторым акцентом, как шапка таблицы и надзаголовок плашки: они объясняют, ЧТО
   в колонках, а не состояние строк. */
.rows__head{padding:8px 16px 7px;border-bottom:1px solid var(--line);background:var(--surface-2);font:600 var(--t-caps) var(--f-ui);letter-spacing:1px;text-transform:uppercase;color:var(--accent2-dim)}
.rows__head>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Сетка одна на подписи и на строку: иначе подпись стоит над чужой колонкой. */
.rows__head,.rows__line{
  display:grid;
  grid-template-columns:var(--col-sev,40px) var(--col-where,176px) minmax(0,1fr) var(--col-who,124px) var(--col-when,74px) var(--col-act,140px) var(--col-open,18px);
  grid-template-areas:"sev where what who when act open";
  gap:0 var(--col-gap,var(--s-6));align-items:center
}

/* ЗАГОЛОВОК ГРУППЫ: ТОНЬШЕ СТРОКИ, НО ВИДИМЕЕ РАЗДЕЛИТЕЛЯ.
   Группы уже были, но весили столько же, сколько строки под ними, - то есть не группировали
   ничего. Здесь три признака сразу, и каждый работает поодиночке: вертикальный размер вдвое
   меньше строки (вес), своя подложка на ступень выше (видимость), цветная рейка тона слева,
   которая ПРОДОЛЖАЕТСЯ в строках группы и связывает их в один объект. */
.rows__grp{position:relative;display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-3);padding:7px 16px 6px 19px;border-top:1px solid var(--line);border-bottom:1px solid var(--line-row);background-color:var(--surface-2)}
.rows__grp:first-child{border-top:0}
.rows__grp::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--tone-dot,var(--line-strong))}
.rows__grp-n{font:600 13px var(--f-ui);color:var(--text)}
.rows__grp-s{font:500 11px var(--f-mono);color:var(--text-mono)}
.rows__grp-c{font:500 var(--t-meta) var(--f-ui);color:var(--text-dim)}
.rows__grp-a{margin-left:auto;display:flex;align-items:center;gap:var(--s-3)}
/* Уточнение в шапке группы набрано --text-mono, а на заливке аварии оно даёт 4.39:1 - ниже
   порога. Правило то же, что у строк: тише --text-dim на плашке не пишут, берут голос состояния. */
.rows__grp.tone-ok .rows__grp-s,.rows__grp.tone-warn .rows__grp-s,.rows__grp.tone-risk .rows__grp-s,
.rows__grp.tone-info .rows__grp-s,.rows__grp.tone-nd .rows__grp-s{color:var(--tone-ink)}
/* Тон группы правилом из двух классов: .rows__grp задаёт подложку и стоит ниже раздела 13. */
.rows__grp.tone-ok,.rows__grp.tone-warn,.rows__grp.tone-risk,.rows__grp.tone-info,.rows__grp.tone-nd{
  background-color:var(--surface-2);
  /* Заголовок берёт ТУ ЖЕ ступень, что и строки под ним. Иначе группа распадается: шапка тихая
     (0.06), а её собственные строки громкие (0.175), и глаз читает их как чужие друг другу.
     Отличается заголовок не цветом, а весом - половинной высотой, своей подложкой и рамкой. */
  background-image:linear-gradient(var(--tone-plate),var(--tone-plate));
  border-bottom-color:var(--tone-line-hot);
  /* Группа - не карточка: свет тут погашен по той же причине, по какой он погашен у строки
     списка (раздел 13, .tone--wash). Двадцать светящихся полосок не выделяют ни одной. */
  --tone-glow:transparent
}

/* Обёртка «строка плюс её раскрытие». Тон ставится НА НЕЁ, а не на строку: раскрытие обязано
   знать цвет состояния своего дела. */
.rows__item{display:block}
.rows__line{position:relative;padding:11px 16px 11px 19px;border-bottom:1px solid var(--line-row);background-color:var(--surface);background-image:linear-gradient(var(--row-wash,transparent),var(--row-wash,transparent));transition:background-color var(--tr-tap)}
.rows__item:last-child>.rows__line{border-bottom:0}
.rows__line::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--tone-dot,transparent)}
/* Строка, которая раскрывается, ведёт себя как ручка: курсор, ответ на наведение, фокус с
   клавиатуры. Строка без подробностей не притворяется ручкой. */
.rows__line--go{cursor:pointer}
.rows__line--go:hover{background-color:var(--surface-3)}
.rows__line--go:focus-visible{outline:2px solid var(--accent-05);outline-offset:-2px}
/* СТРОКА НЕСЁТ СВОЙ ТОН ЦЕЛИКОМ: ЗАЛИВКА, РАМКА, ГОЛОС ТИХИХ НАДПИСЕЙ.

   Было: --tone-wash (0.03) и рейка 3px у левого края. Рейка - это 0.4% площади строки, и всё
   дело держалось на ней; сама строка отличалась от спокойной соседки в 1.06 раза, то есть на
   глаз не отличалась вовсе. Владелец прислал снимок этой самой очереди со словами «прямо всю
   плашку цветом и более понятно что не так».

   Стало: --tone-plate, ступень которой равна ТЯЖЕСТИ дела (замеры и разбор в base.css). Авария
   отличается от спокойной строки в 1.310 раза - сильнее, чем самая глубокая плашка кабинета
   отличается от карточки. Норма 1.085 и незнание 1.063 остались почти неокрашенными: иначе
   двадцать одинаково громких строк не выделяли бы ни одной, и вышел бы светофор.

   Рейка ОСТАЁТСЯ, и не по инерции: она продолжает рейку заголовка группы и связывает строки
   одной ноды в объект. Просто теперь она второй признак, а не единственный. */
.rows__item.tone-ok>.rows__line,.rows__item.tone-warn>.rows__line,.rows__item.tone-risk>.rows__line,
.rows__item.tone-info>.rows__line,.rows__item.tone-nd>.rows__line{
  background-color:var(--tone-base);--row-wash:var(--tone-plate);border-bottom-color:var(--tone-line-hot)
}
/* Наведение на цветную строку УСИЛИВАЕТ её тон, а не подменяет его серым (раздел 13). Ступень
   наведения тоже своя у каждого состояния: общая --tone-hot (0.09) ТИШЕ плашки аварии (0.175),
   и курсор гасил бы тон ровно там, где владелец в него целится. */
.rows__item.tone-ok>.rows__line--go:hover,.rows__item.tone-warn>.rows__line--go:hover,
.rows__item.tone-risk>.rows__line--go:hover,.rows__item.tone-info>.rows__line--go:hover,
.rows__item.tone-nd>.rows__line--go:hover{--row-wash:var(--tone-plate-hot)}
/* Незнание внутри ленты берёт базой поверхность ленты, а не холст: строка лежит НА ней. */
.rows__item.tone-nd{--tone-base:var(--surface)}
/* НА ПЛАШКЕ САМАЯ ТИХАЯ КРАСКА ЭТО --text-dim, И ЭТО ЗАМЕР, А НЕ ВКУС. На заливке аварии под
   курсором --text-mono даёт 4.24:1, --text-faint 3.73 - оба ниже порога 4.5. Поэтому всё, что
   тише --text-dim, на цветной строке берёт голос состояния --tone-ink: он и читается с запасом
   (5.5-8.2), и делает строку одним цветным объектом, а не набором серых ячеек.

   Что переезжает: номер по очереди и уголок раскрытия (были --text-faint), уточнение под именем
   и само имя в повторной строке группы (были --text-mono), «давно» в колонке возраста
   (было --text-faint). Заголовок дела и имя объекта не трогаем - они и так самые громкие
   (10.6:1 и 8.8:1), и красить их состоянием значило бы отнимать у них контраст. */
.rows__item.tone-ok>.rows__line .rows__rank,.rows__item.tone-warn>.rows__line .rows__rank,
.rows__item.tone-risk>.rows__line .rows__rank,.rows__item.tone-info>.rows__line .rows__rank,
.rows__item.tone-nd>.rows__line .rows__rank,
.rows__item.tone-ok>.rows__line .rows__open,.rows__item.tone-warn>.rows__line .rows__open,
.rows__item.tone-risk>.rows__line .rows__open,.rows__item.tone-info>.rows__line .rows__open,
.rows__item.tone-nd>.rows__line .rows__open{color:var(--tone-ink)}
.rows__item.tone-ok>.rows__line .rows__where-s,.rows__item.tone-warn>.rows__line .rows__where-s,
.rows__item.tone-risk>.rows__line .rows__where-s,.rows__item.tone-info>.rows__line .rows__where-s,
.rows__item.tone-nd>.rows__line .rows__where-s,
.rows__item.tone-ok>.rows__line .rows__where--same .rows__where-n,
.rows__item.tone-warn>.rows__line .rows__where--same .rows__where-n,
.rows__item.tone-risk>.rows__line .rows__where--same .rows__where-n,
.rows__item.tone-info>.rows__line .rows__where--same .rows__where-n,
.rows__item.tone-nd>.rows__line .rows__where--same .rows__where-n,
.rows__item.tone-ok>.rows__line .age--old,.rows__item.tone-warn>.rows__line .age--old,
.rows__item.tone-risk>.rows__line .age--old,.rows__item.tone-info>.rows__line .age--old,
.rows__item.tone-nd>.rows__line .age--old{color:var(--text-dim)}
/* Прочерк - это линия, а не текст, и на цветной заливке серая --line-strong пропадает. Берёт
   ту же границу, что и сама плашка: видно, что ячейка пуста намеренно. */
.rows__item.tone-ok>.rows__line .rows__none,.rows__item.tone-warn>.rows__line .rows__none,
.rows__item.tone-risk>.rows__line .rows__none,.rows__item.tone-info>.rows__line .rows__none,
.rows__item.tone-nd>.rows__line .rows__none{background:var(--tone-line-hot)}
/* РАСКРЫТИЕ ОСТАЁТСЯ ПРИ СВОЁМ ДЕЛЕ. Подложка врезки не красится в полную силу - там лежит
   техническая улика, которую читают буквами, - но и нейтральной быть не может: раскрытая строка
   иначе распадается на цветную шапку и чужую серую врезку под ней. Берём тихую ступень. */
.rows__item.tone-ok>.rows__det>.rows__body,.rows__item.tone-warn>.rows__det>.rows__body,
.rows__item.tone-risk>.rows__det>.rows__body,.rows__item.tone-info>.rows__det>.rows__body,
.rows__item.tone-nd>.rows__det>.rows__body{
  background-image:linear-gradient(var(--tone-wash),var(--tone-wash));border-bottom-color:var(--tone-line-hot)
}

/* Колонка ВАЖНОСТЬ: силуэт состояния (раздел 13) плюс номер по очереди. Слова нет намеренно -
   оно повторяет цвет и забирает у «что» четверть ширины. */
.rows__sev{grid-area:sev;display:flex;align-items:center;gap:7px;min-width:0}
.rows__rank{font:500 11px var(--f-mono);font-variant-numeric:tabular-nums;color:var(--text-faint)}
.rows__sev .tone-dot{width:10px;height:10px;flex:0 0 10px}
/* Силуэт, позванный сам по себе, обязан знать свой цвет: вне блока с тоном --tone-dot пуст. */
.tone-dot--ok{--tone-dot:var(--ok)}
.tone-dot--warn{--tone-dot:var(--warn)}
.tone-dot--risk{--tone-dot:var(--risk)}
.tone-dot--info{--tone-dot:var(--info)}
.tone-dot--nd{--tone-dot:var(--nd)}

/* Колонка ГДЕ: имя объекта машинным шрифтом (раздел 21 - его копируют и ищут), регион тише. */
.rows__where{grid-area:where;display:flex;align-items:baseline;gap:6px;min-width:0;white-space:nowrap;overflow:hidden}
.rows__where-n{font:600 13px var(--f-mono);font-variant-numeric:tabular-nums;color:var(--text-2);overflow:hidden;text-overflow:ellipsis}
/* Имя, УЖЕ НАЗВАННОЕ ШАПКОЙ ГРУППЫ (UI.Row({sameWhere:true})). Из колонки оно не убирается:
   пустая ячейка рвёт чтение столбца сверху вниз, а строка, вырванная фильтром из группы,
   теряет место совсем. Оно становится тише имени, названного впервые, - столбец читается как
   «всё ещё Нью-Йорк», а не как шесть раз выкрикнутое имя. */
.rows__where--same .rows__where-n{font-weight:500;color:var(--text-mono)}
.rows__where-s{font:500 11px var(--f-mono);color:var(--text-mono);overflow:hidden;text-overflow:ellipsis}

/* Колонка ЧТО: три-четыре слова продукта, гротеском. Одна строка и многоточие - это и держит
   одинаковую высоту у всех строк. */
.rows__what{grid-area:what;display:flex;align-items:center;gap:var(--s-3);min-width:0;white-space:nowrap;overflow:hidden}
.rows__what-t{flex:0 1 auto;min-width:7ch;font:600 13.5px var(--f-ui);color:var(--text);overflow:hidden;text-overflow:ellipsis}
/* ТЕСНОТУ ДЕЛЯТ ПО ПРАВИЛУ, А НЕ КАК ВЫЙДЕТ.
   Ярлык («следствие дела 1») не переносится и не сокращается: обрезанный ярлык - это «с..», то
   есть мусор на месте сведения. Сокращается текст дела, но не ниже девяти знаков: ниже он
   перестаёт быть фразой и тоже становится мусором («З..» - ровно так это и выглядело). Оба
   правила простые, и вместе они дают предсказуемое «дело обрезано, ярлык цел». */
.rows__what>:not(.rows__what-t){flex:0 0 auto;min-width:0}

/* Колонка КОГО и колонка КОГДА выровнены ПО ПРАВОМУ КРАЮ (раздел 22): числа сравнивают по
   длине столбика знаков, а прижатые влево они сравниваются по первой цифре. */
.rows__who{grid-area:who;justify-self:end;min-width:0;text-align:right}
.rows__when{grid-area:when;justify-self:end;min-width:0;text-align:right}
.rows__act{grid-area:act;justify-self:end;display:flex;justify-content:flex-end;align-items:center;min-width:0;max-width:100%}
.rows__act .btn{padding:7px 12px;font-size:12px;max-width:100%}
/* ПРОЧЕРК, А НЕ ЗНАК ПРЕПИНАНИЯ. Пустая ячейка действия читается как недогруженная вёрстка,
   а любой дефис в ней - как часть соседнего слова. Короткая линия не читается ни тем, ни
   другим: она просто говорит «тут ничего нет». */
.rows__none{display:inline-block;width:14px;height:1px;background:var(--line-strong)}
.rows__open{grid-area:open;display:flex;align-items:center;justify-content:center;color:var(--text-faint);transition:transform var(--tr-tap),color var(--tr-tap)}
/* Уголок раскрытия - фигура, а не иконка: он живёт в ЛЮБОЙ строке, а inline-SVG на каждую
   строку списка это лишний разбор строки в узел на ровном месте. */
.rows__open i{display:block;width:7px;height:9px;background:currentColor;clip-path:polygon(0 0,100% 50%,0 100%)}
.rows__item.is-open>.rows__line>.rows__open{transform:rotate(90deg);color:var(--text-dim)}

/* РАСКРЫТИЕ НЕ СДВИГАЕТ СОСЕДЕЙ РЫВКОМ.
   Высота едет через grid-template-rows 0fr -> 1fr, а не через display:none: браузер знает обе
   границы и ведёт соседей плавно. max-height тут не годится - её пришлось бы задать числом,
   и любое раскрытие длиннее числа обрезалось бы молча.
   ВИДИМОСТЬ ГАСИТСЯ ОТДЕЛЬНО. Схлопнутая панель имеет нулевую высоту, но её кнопки остаются в
   обходе табом: владелец жмёт Tab и фокус проваливается в невидимое. visibility:hidden убирает
   их из обхода, и снимается она только после того, как панель доехала. */
.rows__det{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--dur-enter) var(--ease-enter)}
.rows__item.is-open>.rows__det{grid-template-rows:1fr}
.rows__body{overflow:hidden;min-height:0;visibility:hidden;transition:visibility 0s linear var(--dur-enter);padding:0 16px 0 19px;border-bottom:1px solid var(--line-row);background-color:var(--surface-0);box-shadow:var(--edge-inset)}
.rows__item.is-open>.rows__det>.rows__body{visibility:visible;transition-delay:0s}
.rows__item:last-child>.rows__det>.rows__body{border-bottom:0}
/* Отступы внутри клипа, а не на нём: у схлопнутой панели padding тоже обязан схлопнуться. */
.rows__pad{padding:13px 0 15px}
/* Пустая лента: ответ, а не таблица без строк. Подписей колонок над пустотой тут нет (см.
   UI.Rows), поэтому блоку нужны собственные поля. */
.rows__empty{padding:var(--s-7) var(--s-6)}
.rows__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-4);padding:10px 16px;border-top:1px solid var(--line-row);background:var(--surface-2);font:500 12.5px var(--f-ui);color:var(--text-dim)}
/* Признание о том, чего кабинет НЕ ЗНАЕТ, живёт в подвале ленты, а не в каждой строке. В
   столбце «кого задевает» стоит «нет данных», и объяснение почему нужно ровно один раз: в
   строках оно превратилось бы обратно в прозу, ради которой всё и переделывалось. */
.rows__foot-nd{color:var(--nd-text);border-bottom:1px dashed var(--nd-line)}

/* Раскрытие: подписанные куски, а не абзац. «Что увидела проверка», «почему так решили», «что
   делать» - каждый со своим надзаголовком, чтобы владелец брал нужный, не читая остальные. */
.rowdet{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:var(--s-3) var(--s-6);align-items:baseline}
.rowdet__lab{font:700 var(--t-caps) var(--f-ui);letter-spacing:1px;text-transform:uppercase;color:var(--accent2-dim);white-space:nowrap}
.rowdet__txt{font:500 var(--t-body)/1.5 var(--f-ui);color:var(--text-2);max-width:55ch;text-wrap:pretty}
.rowdet__mono{font:500 12px/1.65 var(--f-mono);color:var(--text-mono);white-space:pre-wrap;overflow-x:auto}
.rowdet__acts{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:var(--s-3);margin-top:var(--s-2)}

/* УЗКОЕ ОКНО: КОЛОНКИ СКЛАДЫВАЮТСЯ ПРЕДСКАЗУЕМО, А НЕ КАШЕЙ.
   Две ступени. Первая только ужимает ширины - шесть колонок остаются шестью. Вторая
   складывает их в три строки с ЗАКРЕПЛЁННЫМИ местами: важность держит левый столбец на всю
   высоту, дальше сверху вниз «где и когда», «что и кого», «действие». Порядок тот же, что на
   широком окне, поэтому владелец не переучивается. */
@container (max-width:1000px){
  .rows{--col-where:140px;--col-who:104px;--col-when:66px;--col-act:118px;--col-gap:var(--s-5)}
}
/* Третья ступень: колонок по-прежнему шесть, но всё, кроме «что», ужато до предела читаемости.
   «Что» отдавать нельзя - это единственная колонка, длина которой заранее неизвестна, и именно
   она отвечает на вопрос владельца. */
@container (max-width:860px){
  .rows{--col-sev:32px;--col-where:116px;--col-who:92px;--col-when:56px;--col-act:110px;--col-gap:var(--s-4)}
}
@container (max-width:760px){
  .rows{--col-gap:var(--s-4)}
  .rows__head{display:none}
  .rows__line{grid-template-columns:26px minmax(0,1fr) auto;grid-template-areas:"sev where when" "sev what who" "sev act act";align-items:start;row-gap:4px;padding:11px 14px 12px 17px}
  .rows__sev{align-self:start;padding-top:2px}
  .rows__who,.rows__when,.rows__act{justify-self:end}
  .rows__act{grid-area:act;justify-content:flex-start;justify-self:start;margin-top:var(--s-2)}
  /* Сложенная строка отдаёт действию ЦЕЛУЮ строку высоты. Отдавать её прочерку незачем: в
     столбик колонки уже не читается, а смысл «действия тут нет» несёт пустое место. */
  .rows__act--none{display:none}
  .rows__open{position:absolute;right:8px;bottom:10px}
  .rowdet{grid-template-columns:minmax(0,1fr);gap:var(--s-2) 0}
  .rowdet__lab{margin-top:var(--s-3)}
}
/* Браузер без поддержки контейнерных запросов (их держат все, куда кабинет доезжает, но
   вёрстка не имеет права зависеть от этого) получает те же две ступени по окну. Ширины при
   этом совпадают, поэтому два набора правил друг другу не противоречат. */
@supports not (container-type:inline-size){
  @media(max-width:1180px){
    .rows{--col-where:140px;--col-who:104px;--col-when:66px;--col-act:118px;--col-gap:var(--s-5)}
  }
  @media(max-width:820px){
    .rows{--col-gap:var(--s-4)}
    .rows__head{display:none}
    .rows__line{grid-template-columns:26px minmax(0,1fr) auto;grid-template-areas:"sev where when" "sev what who" "sev act act";align-items:start;row-gap:4px;padding:11px 14px 12px 17px}
    .rows__sev{align-self:start;padding-top:2px}
    .rows__who,.rows__when,.rows__act{justify-self:end}
    .rows__act{grid-area:act;justify-content:flex-start;justify-self:start;margin-top:var(--s-2)}
    .rows__act--none{display:none}
    .rows__open{position:absolute;right:8px;bottom:10px}
    .rowdet{grid-template-columns:minmax(0,1fr);gap:var(--s-2) 0}
    .rowdet__lab{margin-top:var(--s-3)}
  }
}

/* ---- 25. ЯЧЕЙКА «КОГО ЗАДЕВАЕТ»: НОЛЬ И НЕЗНАНИЕ - РАЗНЫЕ ВЕЩИ ----

   «Клиенты через эту ноду не подключаются» - это не ответ. Ответ - число: сколько именно
   клиентов сейчас без связи. По нему владелец решает, бежать ему или дочитать до конца.

   ТРИ ВИДА, И ОНИ ОБЯЗАНЫ РАЗЛИЧАТЬСЯ РАСФОКУСИРОВАННЫМ ВЗГЛЯДОМ:
     число      крупно, моноширинно, единица рядом и тише: «143 клиента»
     ноль       то же число. Ноль - это ИЗМЕРЕННЫЙ факт «никого не задевает», и врать про него
                незнанием так же нельзя, как наоборот
     незнание   не число вовсе: мельче, тише, гротеском и с пунктирным подчёркиванием. Именно
                поэтому его нельзя спутать с нулём даже боковым зрением - у них разная ФОРМА,
                а не только цвет

   ВЫДУМАННОЕ ЧИСЛО ТУТ ХУЖЕ ОТСУТСТВУЮЩЕГО: по нему примут решение. Если сервер не сказал,
   скольких задевает, ячейка говорит «нет данных» и молчит дальше. */
.affect{display:inline-flex;align-items:baseline;justify-content:flex-end;gap:4px;min-width:0;white-space:nowrap}
.affect__n{font:700 16px var(--f-mono);font-variant-numeric:tabular-nums;letter-spacing:-0.3px;color:var(--text)}
.affect__u{font:500 11px var(--f-ui);color:var(--text-dim)}
/* Ноль набран тише живого числа, но ЧИСЛОМ: это факт, а не его отсутствие. */
.affect--zero .affect__n{color:var(--text-dim)}
/* Незнание: другой шрифт, другой кегль, пунктир под словом. Три признака вместо одного цвета. */
.affect--nd{font:500 12px var(--f-ui);color:var(--nd-text);border-bottom:1px dashed var(--nd-line);padding-bottom:2px}

/* ---- 26. ЯЧЕЙКА «КОГДА»: СВЕЖЕЕ ЗАМЕТНЕЕ СТАРОГО ----

   «уже 14 минут назад» - это шесть слов там, где нужно два знака. В колонке из пяти строк такие
   подписи невозможно сравнить: они разной длины и начинаются с одного и того же слова «уже».

   Короткая форма: «14 мин», «2 ч», «вчера», «3 дн», «08.09». Выравнена по правому краю и набрана
   цифрами равной ширины - столбик времени читается так же, как столбик чисел.

   ГРОМКОСТЬ НЕСЁТ СМЫСЛ. Случившееся час назад важнее случившегося позавчера, и это видно до
   чтения: свежее ярче и жирнее, старое тише. Ступеней три, а не пять: разницу между «40 минут»
   и «50 минут» глазом всё равно не поймать, а лишний оттенок только шумит. */
.age{font:500 12px var(--f-mono);font-variant-numeric:tabular-nums;color:var(--text-dim);white-space:nowrap}
.age--now{font-weight:600;color:var(--text-2)}
.age--old{color:var(--text-faint)}
/* Времени не назвали вовсе. Не ноль и не «давно»: гротеском и тише любого срока, чтобы столбик
   времени не начал читаться как измеренный там, где измерения не было. */
.age--nd{font:500 11px var(--f-ui);color:var(--nd-text)}

/* ---- 27. ТРИ ВЕСА БЛОКОВ, А НЕ ТРИ КОПИИ ОДНОЙ КАРТОЧКИ ----

   Корневая жалоба владельца на этот кабинет звучала как «все карточки одинаковые». Она про вес:
   когда очередь дел, полоса показателей и справка про раздел оформлены одним и тем же
   прямоугольником с рамкой, экран читается как сплошная серая плита, и решать, что тут главное,
   приходится ЧТЕНИЕМ. Лечится это не цветом и не скруглением, а РАЗНЫМИ ТИПАМИ блоков.

   Весов три, и разница между соседними обязана читаться расфокусированным взглядом:

     ТЯЖЁЛЫЙ    .rows           предмет экрана: то, ради чего владелец зашёл. Своя поверхность,
                                полная рамка, цветные рейки состояния, крупный набор
     ЛЁГКИЙ     .ribbon         справка о фоне: показатели парка. Поверхности НЕТ вовсе, только
                                две волосяные линии сверху и снизу и разделители между ячейками
     ЛЕГЧАЙШИЙ  .note           сноска: откуда данные, что сюда не попадает, куда идти дальше.
                                Ни рамки, ни подложки - только рейка слева и тихий текст

   Проверка простая: прищурьтесь. Тяжёлый блок остаётся прямоугольником, лёгкий - парой линий,
   легчайший - серым пятном текста. Если после прищура их не различить, вес выбран неверно. */
.ribbon{display:flex;flex-wrap:wrap;align-items:stretch;border-top:1px solid var(--line-soft);border-bottom:1px solid var(--line-soft);background:transparent}
.ribbon__cell{flex:1 1 150px;min-width:0;margin:0;padding:11px 16px 12px;border:0;border-right:1px solid var(--line-row);background:transparent;color:inherit;font:inherit;text-align:left}
.ribbon__cell:last-child{border-right:0}
.ribbon__cell--go{cursor:pointer;transition:background-color var(--tr-tap)}
.ribbon__cell--go:hover{background-color:var(--surface-2)}
/* Неизмеренное не имеет права весить столько же, сколько измеренное: оно уходит в одну узкую
   ячейку на своей подложке, а не занимает по колонке каждое. */
.ribbon__cell--nd{flex:0 1 210px;background-color:var(--surface-2)}
.ribbon__lab{display:flex;align-items:center;gap:6px;margin-bottom:5px;font:600 var(--t-caps) var(--f-ui);letter-spacing:1px;text-transform:uppercase;color:var(--accent2-dim);white-space:nowrap;min-width:0;overflow:hidden}
/* Многоточие ставится на САМ ТЕКСТ: у flex-контейнера text-overflow не работает, и подпись
   просто обрубалась на полубукве («агенты на связ»). */
.ribbon__lab>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
.ribbon__sub{margin-top:3px;font:500 11.5px/1.35 var(--f-ui);color:var(--text-dim)}
.ribbon__nd{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:6px 12px;align-items:baseline}
/* «нет данных» переносом по слогам выглядит как поломка вёрстки, а не как честный ответ. */
.ribbon__nd>div{white-space:nowrap;text-align:right}
.note{border-left:2px solid var(--line-mid);padding:1px 0 1px 12px;background:transparent}
.note__lab{margin-bottom:3px;font:600 var(--t-caps) var(--f-ui);letter-spacing:1px;text-transform:uppercase;color:var(--accent2-dim)}
.note__t{font:500 12.5px/1.5 var(--f-ui);color:var(--text-dim);max-width:55ch;text-wrap:pretty}

/* ---- Навигация по подразделам экрана (ui.js: UI.SectionNav) ----
   Вид выбирает ЧИСЛО кусков, а не автор экрана: 3-5 полоса сверху, 6 и больше рельс сбоку.
   Оба варианта - одно оглавление, поэтому набраны одним весом и одним цветом: разными они
   читались бы как два разных механизма. */
.secnav-wrap--strip{display:flex;flex-direction:column;min-width:0}
.secnav-wrap--rail{display:grid;grid-template-columns:200px minmax(0,1fr);gap:var(--s-8);align-items:start}
.secnav-body{min-width:0}
.secnav{display:flex;min-width:0;margin:0 0 var(--s-8)}
.secnav--strip{flex-wrap:wrap;gap:var(--s-1);border-bottom:1px solid var(--line)}
/* Рельс липнет к верху окна: владелец уходит вниз по двенадцати блокам, а оглавление остаётся
   на глазах. Своя прокрутка - на случай, когда кусков больше, чем влезает в окно. */
.secnav--rail{position:sticky;top:var(--s-7);flex-direction:column;gap:1px;max-height:calc(100vh - var(--s-10));overflow-y:auto;scrollbar-width:none}
.secnav--rail::-webkit-scrollbar{width:0}
/* Кнопка оглавления. Геометрия одна на все состояния: меняются только цвет, фон и полоска
   текущего - иначе строка уезжает под курсором (nojump_test.go). */
.secnav__i{position:relative;flex:0 0 auto;display:flex;align-items:center;min-width:0;max-width:100%;padding:var(--s-3) var(--s-5);border:0;background:transparent;color:var(--text-muted);font:600 var(--t-body) var(--f-ui);line-height:1.2;text-align:left;cursor:pointer;transition:color var(--tr-tap),background-color var(--tr-tap)}
.secnav__lbl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* В рельсе подпись переносится, а не обрезается: обрезанное имя куска владелец прочитать не
   может, а угадывать по половине слова, куда он попадёт, он не обязан. В полосе перенос
   некуда деть - там строка, и длинное имя уезжает на второй ряд целым пунктом. */
.secnav--rail .secnav__lbl{overflow:visible;text-overflow:clip;white-space:normal}
.secnav__i:hover{color:var(--text-2);background-color:var(--surface-2)}
.secnav__i.is-on{color:var(--text)}
.secnav__i:focus-visible{outline-offset:-3px;border-radius:var(--r-btn)}
/* Текущий кусок помечен ЛИНИЕЙ, а не жирным начертанием: начертание меняет ширину слова, и
   оглавление дёргалось бы при каждой прокрутке. Линия выключена из потока и не двигает соседей. */
.secnav--strip .secnav__i.is-on::after{content:"";position:absolute;left:var(--s-4);right:var(--s-4);bottom:0;height:2px;border-radius:2px 2px 0 0;background:var(--accent)}
.secnav--rail .secnav__i{border-radius:var(--r-btn)}
.secnav--rail .secnav__i.is-on{background-color:var(--accent-ghost)}
.secnav--rail .secnav__i.is-on::before{content:"";position:absolute;left:0;top:var(--s-2);bottom:var(--s-2);width:2px;border-radius:var(--r-key);background:var(--accent)}
/* Якорь не должен уезжать под шапку: прокрутка к куску оставляет над ним воздух. */
[data-sec]{scroll-margin-top:var(--s-8)}
/* Узкий экран: рельс становится обычной полосой сверху. Столбец в 184 пикселя рядом с
   содержимым на телефоне не оставляет содержимому ничего. Правило стоит ПОСЛЕ базовых: вес у
   селекторов одинаковый, и порядок тут решает всё. */
@media(max-width:900px){
  .secnav-wrap--rail{grid-template-columns:minmax(0,1fr)}
  .secnav--rail .secnav__lbl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .secnav--rail{position:static;flex-direction:row;flex-wrap:wrap;gap:var(--s-1);max-height:none;border-bottom:1px solid var(--line)}
  .secnav--rail .secnav__i.is-on{background-color:transparent}
  .secnav--rail .secnav__i.is-on::before{left:var(--s-4);right:var(--s-4);top:auto;bottom:0;width:auto;height:2px;border-radius:2px 2px 0 0}
}
