/* Шрифты дизайн-системы (Onest + Fira Mono, самохост, OFL).
   ⚠️ Эти @font-face НЕ дубль, а ПЕРЕБИВКА и удалять их нельзя: подключаемая первой копия
   дизайн-системы (/static/mayak-ds.css) объявляет те же семейства с путями assets/fonts/, а в
   бинаре панели файлы лежат в fonts/. Пропадут эти объявления — панель тихо останется на системном
   шрифте. Сторож — ds_shared_test.go.
   🔴 ПОПРАВКА 24-08. Здесь стояло «и за assets/fonts браузер не ходит вовсе» — неправда, и это
   видно по бою: обход панели браузером показал два 404 на /static/assets/fonts/onest-vf-*.woff2 на
   каждый свежий заход. Правило «побеждает последнее объявление» решает, КАКОЙ шрифт применится, но
   не мешает браузеру сходить и за первым. Чинится не здесь: панель отдаёт те же файлы по обоим
   путям (псевдоним в Routes, internal/admin/server.go), потому что копия дизайн-системы обязана
   оставаться побайтной. */
@font-face{font-family:'Fira Mono';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/firamono-400-cyrillic.woff2') format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}
@font-face{font-family:'Fira Mono';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/firamono-400-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
/* Шрифт дизайн-системы — Onest (самохост, OFL; woff2 взяты с fonts.gstatic.com / Google Fonts).
   Файл ВАРИАТИВНЫЙ: одна пара woff2 (латиница+кириллица) несёт ось wght, объявленную 400 700 —
   и 400/500/600/700 берутся из неё точно, и 800/900 из разметки матчатся на верхнюю границу (700)
   по правилам CSS, как раньше на статический жир. Отдельных файлов на вес больше нет. */
@font-face{font-family:'Onest';font-style:normal;font-weight:400 700;font-display:swap;src:url('fonts/onest-vf-cyrillic.woff2') format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}
@font-face{font-family:'Onest';font-style:normal;font-weight:400 700;font-display:swap;src:url('fonts/onest-vf-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}

:root{
  /* ДИЗАЙН-СИСТЕМА Маяк (docs/DESIGN-SYSTEM.md). С 09-08 панель ГРУЗИТ общий файл дизайн-системы —
     /static/mayak-ds.css, ПОБАЙТНАЯ копия web/mayak-ds.css (сверяет architecture-check.sh, как у
     кабинета), — поэтому значения здесь больше НЕ дублируются: свои имена панели (--bg, --txt,
     --primary…) стали ССЫЛКАМИ на токены системы. Имена оставлены намеренно — их знает весь этот
     файл, а переименование ради переименования дало бы диф, который нечем проверить. Пары
     «имя панели → токен системы» по-прежнему сверяет architecture-check.sh (правило 6б): теперь
     оно ловит того, кто вернёт сюда разъехавшийся литерал. */
  --bg:var(--ds-bg);              /* dark-7 — фон контента (как body у Mantine dark) */
  --bg-deep:var(--ds-bg-deep);    /* dark-8 — старт градиента сайдбара / «глубокий» фон */
  --panel:var(--ds-surface);      /* dark-6 — сплошные контролы (инпуты, кнопки-subtle, нейтральные бейджи) */
  --panel2:var(--ds-surface-2);   /* dark-8 — «утопленный» фон */
  --line:var(--ds-border);        /* dark-5 — линии/границы */
  --card:rgba(255,255,255,.02);   /* карточки — полупрозрачная плёнка (Card override); у системы аналога нет */
  /* Тот же цвет, что даёт плёнка --card поверх --bg, но НЕПРОЗРАЧНЫЙ: #161b22 + 2 % белого.
     Нужен там, где что-то стоит ПОВЕРХ содержимого и обязано его закрывать — закреплённая шапка
     таблицы. С полупрозрачной --card первая строка данных просвечивала сквозь шапку насквозь
     (снимок 08-08: «EMAIL / ЛОГИН» поверх «user9@example.com»). Менять только вместе с --bg/--card. */
  --card-solid:#1b2026;
  --card-bd:var(--ds-border-soft);/* рамка карточки/сайдбара */
  --txt:var(--ds-text); --muted:var(--ds-dimmed); --accent:var(--cyan-5); --accent2:var(--cyan-6);
  /* Заливка ГЛАВНОЙ кнопки — тот же токен, что у сайта и кабинета: cyan-9 #0b7285, белый текст на
     нём 5.59 (панель до 02-08 красила кнопки в cyan-6 — 2.79, грубый провал AA на 12 экранах).
     Почему ховер НЕ из шкалы cyan — у --ds-primary в mayak-ds.css. `--accent2` остаётся жив как
     АКЦЕНТНЫЙ ТОН (текст, точки-легенды на тёмном фоне) — на заливку под белый текст он не годится. */
  --primary:var(--ds-primary); --primary-hover:var(--ds-primary-hover); --primary-ink:var(--ds-primary-ink);
  /* --ok и --bad приходят из дизайн-системы КАК ЕСТЬ (#40c057/#fa5252 — свои дубли сняты 09-08).
     --warn панели — это `--st-warn` системы (#f59e0b, янтарь статусов), а НЕ её же `--warn` #d29922:
     все подложки rgba(245,158,11,…) в этом файле посчитаны под янтарь. */
  --warn:var(--st-warn);
  /* --bad-ink — красный ТЕКСТ на красной подложке (бейдж «недоступна», плашка отказа). Сам --bad
     годится для рамок, точек и заливок, но как текст поверх собственной 15 %-подложки даёт 4.0–4.4
     при пороге AA 4.5 (замер 02-08: бейдж st-retired 4.21 на #3c272c, .alert 4.39 на #35252b).
     Светлее ровно настолько, чтобы пройти: 5.77 и 5.46 на тех же фонах. У системы аналога нет. */
  --bad-ink:#ff7b72;
  /* Токены движения (--dur/--ease-out/--ease-in-out) приходят из mayak-ds.css — дубли сняты 09-08. */
}
/* Появление карточек и плиток — var(--ease-out), а не linear: линейная прозрачность на глаз
   «включается» в конце, из-за чего страница как будто запаздывает; ease-out отдаёт большую часть
   изменения сразу и ощущается отзывчивее (та же кривая на тех же карточках в кабинете). */
@keyframes ds-fadein{from{opacity:0}to{opacity:1}}
/* Уважаем системную настройку «меньше движения». В панели она была не учтена вовсе, хотя каждая
   карточка и плитка появляются через ds-fadein, а таблицы и кнопки живут на transition: человеку с
   вестибулярными нарушениями это ровно то, от чего он настройку и включил. Лендинг и кабинет такое
   правило имеют с самого начала — панель просто забыли. Гасим длительности, а не сами свойства:
   состояние остаётся конечным, элементы не исчезают. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;
    transition-duration:.001ms !important;scroll-behavior:auto !important}
  /* `*` не матчит ::backdrop — подложка окна подтверждения гасится отдельной строкой. */
  dialog.ask::backdrop{animation-duration:.001ms !important}
  /* Настройка значит «меньше движения», а не «убрать признаки работы»: колёсико на активном шаге
     провижна (заявка на ноду, визард) — единственное, что отличает «идёт» от «зависло», глобальная
     строка выше замораживала его в неподвижную дугу. Вращение возвращаем; пульс .livedot не трогаем —
     без анимации он остаётся зелёной точкой, информация «лог живой» не теряется. */
  .steps li.active .ic,.steps2 li.active .ic{animation-duration:.8s !important;animation-iteration-count:infinite !important}
}
/* Кастомный скроллбар (teal→cyan) — 1:1 из remnawave global.css */
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:rgba(22,27,35,.6);border-radius:4px}
::-webkit-scrollbar-thumb{background:linear-gradient(180deg,#0d9488 0%,#06b6d4 100%);border-radius:4px;border:1px solid rgba(22,27,35,.8)}
::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,#14b8a6 0%,#22d3ee 100%)}
/* Те же правила С ПРИСТАВКОЙ .ds — перебить одноимённые в mayak-ds.css (там бегунок плоский dark-5):
   `.ds ::-webkit-scrollbar-*` весит (0-1-0) и побеждал бы универсальные правила выше, а панель
   держит teal→cyan градиент 1:1 из Remnawave. Вес тот же, файл позже — побеждает панель.
   Универсальные правила выше НЕ удалять: до скроллбара самого окна селектор-потомок .ds не достаёт. */
.ds ::-webkit-scrollbar{width:8px;height:8px}
.ds ::-webkit-scrollbar-track{background:rgba(22,27,35,.6);border-radius:4px}
.ds ::-webkit-scrollbar-thumb{background:linear-gradient(180deg,#0d9488 0%,#06b6d4 100%);border-radius:4px;border:1px solid rgba(22,27,35,.8)}
.ds ::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,#14b8a6 0%,#22d3ee 100%)}
*{scrollbar-width:thin;scrollbar-color:#0d9488 rgba(22,27,35,.6)}
*{box-sizing:border-box}
/* Прокручивает страницу САМ ДОКУМЕНТ — это и есть «область содержимого» каркаса: боковое меню
   (position:sticky) и нижняя панель телефона (position:fixed) при прокрутке стоят на месте, едет
   только содержимое. Отдельный прокручиваемый .content-area с фиксированной высотой пробовали и
   отвергли по ЗАМЕРУ 08-08: (1) закреплённую шапку таблицы он НЕ чинит — она уезжала ровно так же
   (225 px → −156 px на /users), потому что дело не в том, кто прокручивается, а в лишней
   прокручиваемой коробке ВНУТРИ (см. .tablewrap.fits-x ниже); (2) он ломает клавиатуру: с фокусом
   на body PageDown не прокручивает НИЧЕГО (замер: 0 px против 381 px у документа) — до содержимого
   пришлось бы сначала дотабиться.
   scroll-padding-top — под закреплённую шапку таблицы: без него Tab по ссылкам в длинном списке
   доводит строку РОВНО ПОД шапку, и элемент в фокусе не виден. То же снизу на телефоне: там
   приколочена нижняя панель разделов, и без запаса фокус уезжает под неё. */
html{scroll-padding-top:52px}
/* body.ds в селекторе — не украшение: дизайн-система задаёт `body.ds` кегль 16px/1.55 весом (0-1-1),
   и голый body (0-0-1) ей проигрывал бы — вся панель выросла бы на кегль. Вес тот же, файл позже —
   побеждает панель с её 15px (плотность рабочего инструмента, а не витрины). */
body,body.ds{margin:0;background:var(--bg);color:var(--txt);
  font:15px/1.5 'Onest',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
/* Строки заголовков выравниваются по длине, у абзацев не остаётся «сироты» — одного слова на
   последней строке. В панели это заметно на узких карточках и на телефоне, где заголовок вроде
   «Заявки на ноды» ломался как «Заявки на / ноды». Старый браузер свойство просто игнорирует. */
/* Вес заголовков — 600, как велит дизайн-система (docs/DESIGN-SYSTEM.md) и как на сайте и в
   кабинете. Без этой строки браузер берёт свой bold (700), и панель набрана жирнее остальных
   поверхностей — на одном экране с кабинетом это читается как другой продукт. */
h1,h2,h3{text-wrap:balance;font-weight:600}
p,li{text-wrap:pretty}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
.muted{color:var(--muted)}
/* overflow-wrap — чтобы длинная строка БЕЗ ПРОБЕЛОВ рвалась, а не распирала страницу. В .mono
   лежит ровно такое: ключ AWG (44 символа), адрес IPv6, список seed-адресов через запятую, текст
   отказа провижна (в нём приезжают чужие пути и base64) — пробелов там может не быть ни одного.
   Обход панели 30-08 (состояние `long`) показал, что защиты от этого нет вовсе: карточка ноды,
   отчёт подготовки и «Оригинал AWG» растягивались до 616px при экране 390, то есть панель с
   телефона пришлось бы таскать вбок.
   🔴 `anywhere`, А НЕ `break-word` — И ЭТО ПРОВЕРЕНО ЗАМЕРОМ, а не выбрано по вкусу. Сперва тут
   стоял осторожный `break-word` (он не трогает расчёт min-content, то есть заведомо не может
   сузить колонки таблиц). Обход показал, что он не лечит: страница «Оригинал AWG» на 390 так и
   уезжала до 631px. Причина — в карточном виде таблиц ячейка `table.cards tbody td` это ФЛЕКС с
   унаследованным `overflow-wrap:anywhere` (см. ниже), а флекс-элемент внутри не может стать уже
   своего min-content. Пока у .mono своего значения не было, span наследовал `anywhere` и честно
   рвался; объявленный `break-word` наследование ПЕРЕБИЛ и сделал хуже, чем было. Правило,
   написанное «на всякий случай осторожнее», отняло работавшее.
   ⚖️ Чем платим: в таблице, которая не влезает в контейнер, браузер теперь предпочтёт сузить
   колонку и разорвать значение, а не отдать горизонтальную прокрутку. Для IP и версий это чуть
   хуже читается — зато экран не ездит вбок. Тексты, которые рвать нельзя, защищены точечно:
   .acctnum и .groups td .mono стоят с white-space:nowrap, а при nowrap перенос не делается вовсе. */
.mono{font-family:'Fira Mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;
  overflow-wrap:anywhere}
/* Номер аккаунта (миграция 0110) — то, что поддержка сверяет с человеком и вставляет в переписку.
   user-select:all даёт копирование одним кликом БЕЗ кнопки и без JS: двойной клик по «472-918-563»
   выделяет только один блок из трёх, и в переписку уезжает треть номера. white-space:nowrap —
   номер не переносится по дефисам (перенесённый номер уже не продиктовать).
   tabular-nums: цифры одной ширины, столбик номеров читается как столбик, а не как рваный текст. */
.acctnum{user-select:all;-webkit-user-select:all;white-space:nowrap;font-variant-numeric:tabular-nums}

/* ── Каркас: левый сайдбар + контент (1:1 remnawave layout.module.css .sidebarWrapper) ── */
/* Высота окна — dvh, со ЗАПАСНОЙ строкой в vh для браузеров постарше. На iOS/Safari 100vh считается
   по РАЗВЁРНУТОМУ окну (без адресной строки), поэтому сайдбар и каркас на телефоне оказывались выше
   видимой области, а «Выйти» — под панелью браузера. Порядок строк важен: vh первым, dvh перебивает. */
.layout{display:flex;min-height:100vh;min-height:100dvh;background:var(--bg)}
/* «Плавающая» карточка сайдбара: диагональный градиент dark-8→dark-7, радиус lg, слоёная тень,
   в углу — радужное cyan-свечение (::before, z-index -1 → над градиентом, под контентом). */
.sidebar{width:236px;flex:0 0 236px;position:sticky;top:12px;z-index:200;
  margin:12px 0 12px 12px;height:calc(100vh - 24px);height:calc(100dvh - 24px);
  background:linear-gradient(135deg,var(--bg-deep) 0%,var(--bg) 100%);
  border:1px solid var(--card-bd);border-radius:16px;
  box-shadow:0 1px 1px rgba(0,0,0,.075),0 2px 2px rgba(0,0,0,.075),0 4px 4px rgba(0,0,0,.075),0 8px 8px rgba(0,0,0,.075),0 16px 16px rgba(0,0,0,.075);
  display:flex;flex-direction:column;overflow:hidden}
.sidebar::before{content:'';position:absolute;inset:0;border-radius:16px;pointer-events:none;z-index:-1;
  background:radial-gradient(circle at 20% 20%,rgba(6,182,212,.1) 0%,transparent 50%)}
.side-brand{display:flex;align-items:center;gap:10px;padding:18px 20px 14px;font-weight:600;font-size:17px}
.side-brand .logo{width:26px;height:26px;filter:drop-shadow(0 0 8px rgba(34,184,207,.4))}
/* Прокручивается СПИСОК пунктов, а не сайдбар целиком: «Выйти» обязан быть виден всегда. На 1440×900
   меню не помещалось — скрыто было 307 px, за краем оставались «Домены», «Аудит», «Админы», «2FA» и
   сам выход, и понять, что список листается, было неоткуда: скроллбар в браузере НАКЛАДНОЙ (проверено
   на живой странице — offsetWidth-clientWidth = 0), то есть до первой прокрутки его не видно вовсе
   (аудит админки 31-07, C1). */
.side-nav{display:flex;flex-direction:column;flex:1;min-height:0;padding:4px 12px 12px;position:relative}
.nav-scroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}
/* Класс вешает admin.js РОВНО когда снизу есть непоказанные пункты: затухание и стрелка говорят
   «листай» и исчезают, когда докрутил. Постоянная тень врала бы на высоких экранах, где всё влезает. */
.nav-scroll.more-below{
  -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 26px),transparent 100%);
  mask-image:linear-gradient(to bottom,#000 calc(100% - 26px),transparent 100%)}
.nav-scroll.more-below+.logout-form::before{content:"⌄";position:absolute;left:0;right:0;margin-top:-26px;
  text-align:center;font-size:15px;line-height:1;color:var(--muted);pointer-events:none}
.nav-group{margin-bottom:6px}
/* Заголовок группы — он же кнопка «свернуть/развернуть» (<summary>). Стрелка своя: браузерный
   треугольник у <summary> рисуется шрифтом ОС и выбивается из строя (та же причина, по которой
   в разделе «Ноды» убрали эмодзи-иконки). Кликабельная область — вся строка. */
/* 12px, а не 11: наша же дизайн-система объявляет xs=12px самым мелким размером (docs/DESIGN-SYSTEM.md,
   «Типографика»), и заголовки групп были единственным местом панели, которое из него выпадало
   (осмотр глазами 04-09). Прописными и с разрядкой 11 px читается ещё хуже обычных 11. */
.nav-lbl{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:10px 10px 4px;
  display:flex;align-items:center;gap:6px;cursor:pointer;list-style:none;user-select:none;border-radius:8px}
.nav-lbl::-webkit-details-marker{display:none}
.nav-lbl:hover{color:var(--txt)}
.nav-lbl::after{content:"⌃";margin-left:auto;font-size:12px;line-height:1;opacity:.6;
  transform:translateY(2px);transition:transform .15s var(--ease-out)}
.nav-group:not([open])>.nav-lbl::after{transform:rotate(180deg) translateY(1px)}
/* Свёрнутая группа с текущей страницей внутри не должна теряться: точка показывает, что искомое там. */
.nav-group:not([open])>.nav-lbl.has-active{color:var(--accent)}
.nav-group:not([open])>.nav-lbl.has-active::before{content:"";width:6px;height:6px;border-radius:50%;
  background:var(--accent);flex:0 0 auto}
.side-nav a{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;color:var(--txt);font-size:14px;font-weight:500;margin-bottom:1px}
.side-nav a svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:0 0 auto;opacity:.8}
.side-nav a:hover{background:rgba(255,255,255,.04);text-decoration:none}
.side-nav a.active{background:rgba(34,184,207,.13);color:var(--accent)}
.side-nav a.active svg{opacity:1}
/* Красная точка у «Бэкапы» (v2, 29-09): с копиями что-то не так. Справа в строке меню, цвет — токен
   «плохо». Имя сверено с style.css и css/*.css — совпадений нет. */
.side-nav a .bk-alarm{margin-left:auto;width:8px;height:8px;border-radius:50%;background:var(--bad);flex:0 0 auto}
/* «Выйти» приколочен к низу сайдбара и отчёркнут — он больше не уезжает вместе со списком. */
.logout-form{margin-top:0;padding-top:10px;border-top:1px solid var(--card-bd)}
.side-logout{width:100%;text-align:left;padding:8px 10px;border-radius:8px;background:none;border:0;
  color:var(--bad);font:inherit;font-size:14px;font-weight:500;cursor:pointer}
.side-logout:hover{background:rgba(248,81,73,.10)}
/* «Выйти везде» — второй, приглушённый: нужен редко (увели сессию), поэтому не должен спорить за
   внимание с обычным выходом, но и прятать его в подменю незачем — в такой момент ищут глазами. */
.side-logout-all{color:var(--muted);font-size:13px;font-weight:400}
.side-logout-all:hover{background:rgba(248,81,73,.10);color:var(--bad)}
.content-area{flex:1;min-width:0;display:flex;flex-direction:column}
/* Кнопка меню — компактная слева. Была flex-элементом колонки .content-area и растягивалась во всю
   ширину экрана широкой полосой (видно на мобильном обзоре). */
.side-toggle{display:none;position:sticky;top:0;z-index:5;align-self:flex-start;margin:10px;
  padding:6px 12px;border-radius:8px;
  background:var(--panel);border:1px solid var(--line);color:var(--txt);font-size:18px;cursor:pointer}
@media(max-width:900px){
  /* ⚠️ Закрытое меню обязано быть СКРЫТО, а не «уехавшим». Пока оно пряталось одним transform, панель
     оставалась видимой для браузера: на 390×844 клавиатура делала 18–20 нажатий Tab по невидимым
     пунктам, прежде чем добраться до страницы (замер 02-08: «/» — 20 остановок «в никуда», «/alerts»
     — 18), и скринридер читал всё меню на каждой странице. visibility:hidden убирает панель из
     обхода табом и из дерева доступности; задержка 0s .2s даёт доиграть выезду при закрытии. */
  /* Кривая выезда — var(--ease-out): панель приезжает из-за края, это появление, и стартовать она
     должна быстро (дефолтный ease тормозил в начале и панель казалась тяжёлой). */
  .sidebar{position:fixed;left:0;top:0;z-index:20;margin:0;height:100vh;height:100dvh;border-radius:0;
    transform:translateX(-100%);visibility:hidden;transition:transform .2s var(--ease-out),visibility 0s .2s}
  .sidebar.open{transform:translateX(0);visibility:visible;transition:transform .2s var(--ease-out),visibility 0s}
  /* 44 px кнопке меню — ЗДЕСЬ, а не в блоке ≤560: с появлением нижней панели телефона (см. ниже)
     ☰ живёт только в полосе 601–900 px, и норма попадания нужна именно там. В ≤560 это правило
     стало бы мёртвым — оно бы там красило кнопку, которой нет. */
  .side-toggle{display:inline-block;min-width:44px;min-height:44px;text-align:center}
}

/* ── НИЖНЕЕ МЕНЮ ТЕЛЕФОНА (≤600px) ────────────────────────────────────────────────────────────
   Разметка и обоснование — в base.html. Здесь только про раскладку.
   Порог 600, а не 700/900: Material 3 window size class «compact» (до 600 dp) — там навигация
   нижняя; ресёрч эталона называет ту же границу разумной точкой переключения.

   Панель ПРИКОЛОЧЕНА к низу: у телефона большой палец достаёт низ экрана, а не верх, и до неё не
   надо прокручивать страницу обратно вверх. z-index 18 — НИЖЕ подложки (19) и самого сайдбара (20):
   когда меню открыто, тап мимо обязан его закрывать, а не попадать в нижнюю панель под ним.
   padding-bottom с safe-area — под «домашнюю полоску» iPhone, иначе последний ряд под ней. */
.mobilenav{display:none}
@media(max-width:600px){
  .mobilenav{display:flex;position:fixed;left:0;right:0;bottom:0;z-index:18;
    background:var(--bg-deep);border-top:1px solid var(--card-bd);
    padding-bottom:env(safe-area-inset-bottom,0px)}
  /* 🔴 При 200 % зума (это 195 CSS-пикселей ширины) шесть пунктов по 44 px не влезают в строку,
     и нижнее меню растягивало ВСЮ СТРАНИЦУ вбок: замер 22-08 дал 243–303 px при экране 195.
     Горизонтальная прокрутка на странице — это кнопки, наполовину уехавшие за край.
     Пункты не ужимаем (44 px — норма попадания, а меню открывают большим пальцем): вбок едет
     САМО МЕНЮ, как полоса разделов в кабинете. `flex:0 0 auto` обязателен — с `1 1 0` пункты
     продолжали бы делить недостающую ширину и сжиматься ниже нормы. */
  .mobilenav{overflow-x:auto;scrollbar-width:none}
  .mobilenav::-webkit-scrollbar{display:none}
  .mobilenav a,.mobilenav button{flex:1 1 0;min-width:44px;min-height:52px;
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
    padding:6px 2px;background:none;border:0;cursor:pointer;text-decoration:none;
    color:var(--muted);font:inherit;font-size:10.5px;font-weight:600;line-height:1.1;
    text-align:center;white-space:nowrap}
  .mobilenav svg{width:21px;height:21px;stroke:currentColor;fill:none;stroke-width:1.8;
    stroke-linecap:round;stroke-linejoin:round;flex:0 0 auto}
  .mobilenav a:hover,.mobilenav button:hover{color:var(--txt);text-decoration:none}
  .mobilenav a.active{color:var(--accent)}
  @media(max-width:340px){ .mobilenav a,.mobilenav button{flex:0 0 auto;padding-inline:10px} }
  /* Длинная подпись кнопки («Обновить агента на всех нодах · 3 Заявки») на 195 px не ужимается
     и тянет страницу — разрешаем ей переноситься. Только на совсем узком: на 390 px строка
     кнопок и так укладывается, а перенос там сделал бы кнопки разной высоты. */
  /* Перенос длинных подписей кнопок на узком экране раньше стоял ЗДЕСЬ и не работал: база
     `.btn,.btn-primary,.btn-ghost{white-space:nowrap}` объявлена ниже по файлу (строка ~325), вес
     у селекторов одинаковый, и при равном весе побеждает тот, кто позже. Медиазапрос веса не
     добавляет. Правило переехало в САМЫЙ КОНЕЦ файла — там его уже никто не перебивает. */
  /* Кольцо фокуса — ВНУТРЬ: у нижнего ряда снаружи экран кончается, и outline-offset:2px из общего
     правила обрезался бы краем окна. Беречь :focus-visible обязательно — у эталона фокусное кольцо
     выключено во всей теме (`focusRing:'never'`), и вот это копировать нельзя. */
  .mobilenav a:focus-visible,.mobilenav button:focus-visible{outline-offset:-3px;border-radius:0}
  /* Кнопка ☰ сверху на этой ширине не нужна: «Ещё» в нижней панели открывает тот же сайдбар. */
  .side-toggle{display:none}
  /* Панель приколочена и накрывает низ страницы — освобождаем под неё место, иначе последняя
     строка таблицы и футер остаются под ней (52 px ряд + запас на safe-area). */
  .content-area{padding-bottom:76px}
  /* Отступ КОНЦА страницы спасает только последнюю строку. Прокрутка к элементу (Tab, :target,
     scrollIntoView) посреди длинного списка доводит его к нижней кромке окна — то есть ровно под
     приколоченную панель. Запас снизу отдаём тот же, что и место под неё. */
  html{scroll-padding-bottom:84px}
  /* Само меню на телефоне — ПЛОСКОЕ. На десктопе группы сворачиваются, потому что 19 пунктов не
     влезают в высоту сайдбара; на телефоне сайдбар во весь экран и уже прокручивается, а вот
     лишний уровень («сначала разверни группу, потом ищи пункт») стоит тапа на каждом заходе.
     Эталон на телефоне поступает так же — глубина принесена в жертву длине списка. Поэтому
     заголовок группы здесь не кнопка, а подпись раздела: ни стрелки, ни нажатия. Открытыми
     группы держит admin.js — <details> средствами CSS не раскрыть. */
  .nav-lbl{pointer-events:none}
  .nav-lbl::after{content:none}
  /* Пункты меню — под палец: 37 px строки были ниже нормы попадания 44 px, которую панель на узком
     экране держит для кнопок, полей и «Выйти» (блок ≤560 ниже). Меню телефона открывается большим
     пальцем с нижней панели — промах по соседнему разделу здесь та же цена, что промах по кнопке.
     Замер 09-08: все 29 пунктов были 210×37. Список станет длиннее — он и так прокручивается,
     а подсказка «⌄ листается» про это сообщает. */
  .side-nav a{min-height:44px}
}

/* «К содержимому» — первая остановка таба. Меню стоит в разметке ПЕРЕД страницей, поэтому на
   десктопе до первого элемента самой страницы приходилось 25 нажатий Tab, и так на КАЖДОЙ странице.
   Ссылка не видна, пока её не выбрали клавиатурой (мышью до неё не добраться — и не надо). */
/* Цвета — тёмный фон + акцентный текст (7.95), а не заливка под белый текст: на момент появления
   ссылки кнопки красились в cyan #15aabf и белым по нему выходило 2.79 (теперь заливка — `--primary`,
   5.59, но переделывать работающую ссылку незачем). */
/* Прячется сдвигом вверх (translateY −100% и 8px запаса), а не заездом на left:-9999px: телепорт
   с парковки за экраном не анимируется в принципе, а появление по фокусу должно быть переходом —
   иначе ссылка «вспыхивает» из ниоткуда и первый Tab выглядит сбоем. transform не трогает раскладку,
   и в покое ссылка, как и раньше, целиком скрыта за верхним краем (как в кабинете, .skip-link). */
/* min-height 44 — ссылка мерилась 144×43, промах по норме попадания на ОДИН пиксель (замер 23-08).
   Клавиатурой её и так «нажимают» Enter'ом, но открывают её и с планшета пальцем, а один пиксель —
   тот самый разряд промахов, который живёт годами именно потому, что его не видно глазом. */
.skiplink{position:absolute;left:0;top:0;z-index:300;transform:translateY(calc(-100% - 8px));
  transition:transform .15s var(--ease-out);
  display:inline-flex;align-items:center;min-height:44px;
  background:var(--panel2);color:var(--accent);border:1px solid var(--accent);
  padding:10px 16px;border-radius:0 0 8px 0;font-size:14px;font-weight:600}
.skiplink:focus{transform:translateY(0);text-decoration:none}

/* ── ШИРИНА СТРАНИЦЫ: ТАБЛИЦАМ — ЭКРАН, ТЕКСТУ — СТРОКА (владелец 27-09) ─────────────────────
   Было `max-width:1180px`: на мониторе 1920 при сайдбаре 248 px содержимое занимало 1180 из 1672,
   по бокам пустовало ~490 px, а в списке людей колонки «Тариф»/«Туннель» переносились в три строки.
   Панель — это прежде всего таблицы, и им ширина нужна; ограничивать надо не страницу, а длину
   СТРОКИ ТЕКСТА (норма чтения 45–80 знаков, WCAG 1.4.8 — не больше 80). Поэтому:
   • .wrap тянется по экрану до 1760 px — на 1920 это вся ширина рядом с сайдбаром, а на 2560+
     потолок остаётся: строку таблицы шириной в два метра глаз уже не ведёт без потери ряда;
   • абзацы, подсказки и формы держат свою ширину сами — правило ниже. */
.wrap{max-width:1760px;margin:0 auto;padding:28px;width:100%}
/* Длина строки у ТЕКСТА — ~80 знаков, независимо от ширины страницы. Только простые абзацы и
   подсказки: плашки (.alert/.notice/p.ok/.verdict) — рамки во всю карточку, их не трогаем.
   `:where()` обнуляет вес селектора: у любого правила с классом (у .pagesub свои 45rem, у
   .billing .rowacts свои 32ch) max-width по-прежнему свой — здесь только умолчание.
   🔴 43em, а НЕ 80ch — по двум причинам. (1) `ch` — ширина ЦИФРЫ «0», а она у шрифта панели шире
   средней кириллической буквы: замер на стенде 28-09 — при 80ch (800 px) в строку абзаца входило
   104–111 знаков, при 600 px — около 80. (2) `ch` меряется ТЕКУЩИМ шрифтом, а Onest грузится со
   `swap`: до подмены ширина считается по фолбэку и строки потом переносятся иначе (см. .pagesub —
   CLS на /nodes 02-08). `em` — это кегль, от гарнитуры не зависит: 43em = 602 px при 14 px и 559 px
   при 13 px, то есть ~80 знаков при любом кегле абзаца.
   Туда же — ОДИНОЧНОЕ поле формы (.field вне .form-grid): на /settings поле «Рублей на баланс» с
   числом 100 тянулось на всю карточку, 1370 px на 1920. Поле в сетке не трогаем: его ширину задаёт
   дорожка сетки, и поле `.full` под рядом из трёх колонок обязано совпадать с рядом по краю. */
:where(.wrap) :where(p:not([class]),p.muted,p.sm,p.warntext,p.triage,p.err,p.danger,.field .hint,
  .field:not(.form-grid > *)){max-width:43em}
.foot{text-align:center;padding:24px;font-size:12px}

/* ── ОДИН ЛЕВЫЙ КРАЙ (проход по UX 11-08) ────────────────────────────────────────────────────
   Замер на живой панели 1440×900: на каждой странице внутри карточки было ТРИ левых края сразу —
   заголовок h2 на 307 px (свой отступ 4px), обычное содержимое на 303 (край подложки карточки),
   текст в ячейках таблицы на 313 (padding 10px у th/td). На /users/14 это видно на «Устройствах»:
   «Устройства · 1/3» начинается левее, чем «МЕТКА» и сами метки под ним. На уровне страницы то же
   самое: h1 и карточки на 282, а описание страницы (.pagesub) — на 286.
   Правило одно и простое: ЛЕВЫЙ КРАЙ ТЕКСТА = КРАЙ ПОДЛОЖКИ (край .wrap на уровне страницы, край
   поля карточки внутри неё). Поэтому ниже снимаются горизонтальные отступы 4px у всего, что стояло
   «чуть правее», и обнуляется padding первой/последней ячейки таблиц. Дышать содержимому даёт
   padding самой карточки (20px), второй отступ поверх него — это не воздух, а раздрай. */
.pagehead{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px}
/* Длинный e-mail в заголовке карточки пользователя не переносился и вылезал за экран. */
.pagehead h1{margin:0;font-size:24px;overflow-wrap:anywhere;min-width:0}
/* Заголовок карточки человека: главным стоит НОМЕР аккаунта, почта — подзаголовком (решение
   владельца 11-08). Обёртка нужна, чтобы длинный адрес не распирал шапку, а .mono внутри h1 не
   ужимал заголовок до 13px — оба правила живут здесь, а не инлайном: свой стиль в разметке ловит
   сторож TestКаждаяСтраницаПанелиЖивётВОбщемКаркасе, и правильно делает. */
.pagehead .whobox{min-width:0;overflow-wrap:anywhere}
.pagehead h1 .acctnum{font-size:inherit}
.pagehead{gap:12px;flex-wrap:wrap}
/* ── СЕМЕЙСТВО КНОПОК ────────────────────────────────────────────────────────
   Одинаковая геометрия у <a> и <button>: раньше .btn-primary был только для ссылок (без
   border/box-sizing), и рядом с обычной кнопкой он «прыгал» по высоте.
   ⚠️ ПОРЯДОК ВНУТРИ БЛОКА ВАЖЕН и потому блок один. Нейтральная заливка `.btn` раньше стояла
   на 140 строк НИЖЕ вариантов, и при одинаковой специфичности побеждала она: `class="btn btn-primary"`
   рисовался фоном #0d1117 вместо заливки `--primary` — то есть ГЛАВНАЯ кнопка страницы выглядела
   второстепенной. Замер на проде 02-08: «Создать» (/plans), «Включить» (/payment-rails),
   «Добавить домен» (/domains) — bg rgb(13,17,23) при эталоне заливки; «btn btn-ghost»
   (5 кнопок визарда) вместо прозрачного получал ту же заливку. Вариант обязан идти ПОСЛЕ базы. */
.btn,.btn-primary,.btn-ghost{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  padding:9px 16px;border-radius:8px;font:inherit;font-size:14px;font-weight:600;line-height:1.25;
  cursor:pointer;border:1px solid transparent;text-decoration:none;white-space:nowrap}
/* нейтральная (по умолчанию) */
.btn{border-color:var(--line);background:var(--panel2);color:var(--txt)}
.btn:hover{border-color:var(--accent)}
/* главная */
.btn-primary{background:var(--primary);color:var(--primary-ink);border-color:var(--primary)}
.btn-primary:hover,.btn.btn-primary:hover{background:var(--primary-hover);border-color:var(--primary-hover);text-decoration:none}
.btn-primary:disabled,.btn-primary[disabled]{opacity:.5;cursor:not-allowed}
/* прозрачная */
.btn-ghost{background:transparent;border-color:var(--line);color:var(--txt)}
.btn-ghost:hover,.btn.btn-ghost:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
/* ⚠️ ССЫЛКИ-КНОПКИ против дизайн-системы. `.ds a{color:var(--cyan-5)}` весит (0-1-1) и перебивала
   бы одноклассовые (0-1-0) правила выше: <a class="btn"> становился голубым, а `.ds a:hover` —
   подчёркнутым (та же арифметика у плиток-ссылок, бейджей и красных ссылок ниже). Возвращаем цвет
   тем же весом (0-1-1), файл позже — побеждает панель. Список — по фактическим классам <a> в
   шаблонах (grep 09-08): btn / btn-primary / btn-ghost / tile-link / muted / danger / badge /
   nodeRow (карточка «Нод», стили в её шаблоне слабее и сами не отбились бы). */
a.btn,a.btn-ghost{color:var(--txt)}
a.btn-primary{color:var(--primary-ink)}
a.btn-danger{color:var(--bad-ink)}
a.tile-link{color:inherit}
a.muted{color:var(--muted)}
a.danger{color:var(--bad)}
a.badge{color:var(--muted)}
a.btn:hover,a.btn-primary:hover,a.btn-ghost:hover,a.btn-danger:hover,
a.tile-link:hover,a.nodeRow:hover{text-decoration:none}
/* Отклик на нажатие (как у кнопок кабинета): менялся только цвет мгновенным скачком, и на телефоне
   человек не понимал, сработала ли кнопка. Это :active, а не ховер-движение — в покое и при
   наведении кнопки панели не двигаются. Цветовые переходы — теми же токенами, что в кабинете,
   чтобы обе поверхности ощущались одним продуктом. Отключённые кнопки исключены: кнопка, которая
   «кивнула», но ничего не сделала, врёт сильнее, чем кнопка без отклика. */
.btn,.btn-primary,.btn-ghost,button.danger,.btn-danger-solid,.login button,.search button,
.rangesw button,.side-logout,.side-toggle{
  transition:background var(--dur) ease,border-color var(--dur) ease,color var(--dur) ease,
    transform .16s var(--ease-out)}
.btn:active:not(:disabled):not([disabled]),.btn-primary:active:not(:disabled):not([disabled]),
.btn-ghost:active:not(:disabled):not([disabled]),button.danger:active:not(:disabled),
.btn-danger-solid:active:not(:disabled),.login button:active,.search button:active,
.rangesw button:active,.side-logout:active,.side-toggle:active{transform:scale(.97)}

/* Плитки: было жёстко 4 колонки — на телефоне каждая сжималась до ~80px и цифры не читались.
   auto-fit даёт те же 4 на десктопе и 1–2 на узком экране. */
/* auto-FILL, а не auto-fit. Разница видна на обзоре: там три группы плиток подряд — 5, 4 и 4 штуки.
   auto-fit схлопывает пустые дорожки и растягивает то, что есть, поэтому ряд из пяти получал колонки
   по 212px, а ряды из четырёх — по 265px, и вертикальные края карточек не совпадали ни в одном месте
   (замер 1440: колонки 283/511/739/967/1195 против 283/568/853/1137). auto-fill дорожки сохраняет —
   ряды встают на одну сетку, а неполный ряд просто оставляет хвост пустым, как и положено. */
/* auto-FIT, а не auto-fill: ряд из четырёх плиток при пяти колонках оставлял пустую колонку, и
   правый край сводки рвался — «Деньги» кончались на 1178 px там, где верхние ряды доходили до 1406
   (проход по UX 11-08). auto-fit растягивает плитки на всю ширину, и все ряды кончаются на одной
   линии. */
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px;margin-bottom:24px}
/* …НО у auto-fit есть цена, и её платят две страницы (замер 11-08, 1440×900).
   1. ДВА РЯДА ПЛИТОК ПОДРЯД (на /payments — 5 штук и 4 штуки; их же склеивает `.tiles+.tiles`
      отрицательным отступом, то есть читаются они как ОДНА сетка). auto-fit считает дорожки
      отдельно для каждого блока: верхний ряд получал колонки по 212 px (края 282/510/738/966/1194),
      нижний — по 269 (282/567/852/1137). Совпадала одна вертикаль из пяти.
   2. ЕДИНСТВЕННАЯ ПЛИТКА (на /online — фильтр по направлению, пока страна одна): auto-fit
      схлопывает пустые дорожки, и плитка с числом «3» растягивалась на всю карточку — 1082 px.
      Ровно это уже описано у .sys-grid абзацем ниже.
   Обоим случаям нужна ФИКСИРОВАННАЯ СЕТКА (auto-fill): ряды встают на одни вертикали, одинокая
   плитка остаётся плиткой. Неполный ряд при этом кончается раньше правого края — так и должна
   выглядеть сетка, в которой не хватает элементов.
   ⚠️ Только шире 560 px: на телефоне ширину плиток задаёт свой блок (@media max-width:560px), а он
   стоит НИЖЕ и слабее по весу (0-1-0 против 0-2-0) — без этой рамки правило перебило бы телефон. */
@media(min-width:561px){
  .tiles+.tiles,.tiles:has(+.tiles),.online .tiles{
    grid-template-columns:repeat(auto-fill,minmax(210px,1fr))}
}
/* stretch, а не center: содержимое плитки растягивается на её высоту, и число уезжает вниз (см.
   .tile-body) — иначе при подписи разной длины числа съезжали друг относительно друга, и ряд
   переставал читаться как ряд, хотя ради сравнения чисел он и нужен. */
.tile{display:flex;align-items:stretch;gap:14px;background:var(--card);
  border:1px solid var(--card-bd);border-radius:8px;padding:16px 18px;animation:ds-fadein .2s var(--ease-out) both}
/* иконка в ЗАКРУГЛЁННОМ тинт-боксе (как на «Главной» Remnawave) */
.tile-ic{flex:0 0 auto;align-self:flex-start;width:40px;height:40px;border-radius:8px;background:rgba(34,184,207,.12);
  color:var(--accent);display:flex;align-items:center;justify-content:center}
.tile-ic svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
/* Число прижато к НИЗУ плитки, подпись — к верху. Плитки в ряду одной высоты (grid), поэтому числа
   встают на одну линию при любой длине подписи — а подписи разной высоты на страницах, где второй
   факт всё ещё живёт внутри подписи («Устройств на нодах · сумма по машинам»). */
.tile-body{min-width:0;display:flex;flex-direction:column;justify-content:space-between;flex:1 1 auto}
/* Плитки со строкой сравнения (.tiles-kpi) считаются СВЕРХУ: подпись — число — сравнение.
   Правило, на котором это держится: ПОДПИСЬ ВСЕГДА ОДНОСТРОЧНАЯ. Тогда числа в ряду стоят на одной
   линии, а строка сравнения висит ниже и ни на что не влияет — её длину мы не выбираем, там данные
   («вчера за сутки 8,1 ГБ» переносится, «снято 1» нет).
   🔴 Обратная привязка (число к низу) тут не годится, и это замерено, а не предположено: 15-08 на
   живой панели плитка с ПЕРЕНЕСЁННОЙ строкой сравнения поднимала своё число на 15 px выше соседей.
   Значит подписи длиннее строки — не «некрасиво», а поломка ряда: сокращать, объяснение в title. */
.tiles-kpi .tile-body{justify-content:flex-start}
/* Под подпись отведены ДВЕ строки — ровно столько, сколько нужно самой длинной («Ни разу не
   подключались» в плитке 168 px). Однострочные подписи добирают пустотой, и числа в ряду стоят
   на одной линии. Без этого приходилось бы резать подписи до неузнаваемости ради вёрстки. */
.tiles-kpi .tile .lbl{min-height:2.5em}
.tile .lbl{color:var(--muted);font-size:12px;line-height:1.25}
.tile .num{font-size:26px;font-weight:700;line-height:1.15;margin-top:2px}
/* Третья строка плитки: факт для сравнения — «вчера за сутки 8,1 ГБ», «снято 1», «+9 служебных».
   🔴 Зачем она появилась (15-08). Раньше такой факт вписывали ВНУТРЬ подписи через «·»:
   «Трафик сегодня · вчера за сутки 8,1 ГБ», а под ним «5,2 ГБ». Читается это плохо по двум
   причинам сразу: глаз встречает ЧУЖОЕ число раньше главного (и «8,1» успевает прочитаться как
   значение плитки), а сама подпись разрастается на две-три строки и ряд перестаёт выглядеть рядом.
   Теперь порядок обычный для сводок: имя — значение — сравнение. Подписи стали однострочными,
   плитки одной высоты. */
.tile .sub{color:var(--muted);font-size:11.5px;line-height:1.3;margin-top:3px}
/* Уточнение в скобках («4 (2 онлайн)») переносилось ПОСРЕДИ себя: в плитке 212px выходило «4 (2» /
   «онлайн)» — число читалось как «4 (2». Держим скобку целиком и уводим её на свою строку, если не
   влезла (замер 03-08 на карточке ноды). */
.tile .num .of{font-size:15px;color:var(--muted);font-weight:500;white-space:nowrap}

/* Карточка сама даёт горизонтальную прокрутку широкому содержимому (таблицам). Раньше это делала
   обёртка .tablewrap, но её ставили не везде: на мобилке страница пользователя расползалась до 551px
   при экране 390 (аудит 2026-07-25, P1-2). Оборачивать 20 шаблонов вручную — заведомо снова забыть. */
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:8px;
  padding:8px 20px 16px;margin-bottom:24px;animation:ds-fadein .2s var(--ease-out) both;overflow-x:auto}
.card h2{font-size:16px;margin:16px 0}
/* h3 — ПОДзаголовок внутри карточки, и он обязан быть мельче её h2. Без этой строки браузер брал
   свой размер (1.17em → 17.55 px), то есть «Отозванные выдачи» и «За чей счёт живут устройства» на
   карточке человека и три подзаголовка на карточке ноды рисовались КРУПНЕЕ названия самой карточки
   (16 px) — иерархия читалась вверх ногами. Класс `sm` на них не работал: `.sm` объявлен только
   внутри .support и .groups. Отступ сверху больше, чем снизу, — подзаголовок принадлежит тому, что
   под ним. Частные правила (.prep h3 13px, .gbranch h3 14px) весят столько же и стоят ниже — они
   остаются в силе. */
.card h3,.wrap h3{font-size:14px;margin:18px 0 8px}
/* «Опасная зона» — отдельная карточка в красной рамке для НЕОБРАТИМОГО. Была только у ноды, и то
   инлайновым стилем с хардкодом цвета; на карточке человека «выдать конфиг» и «удалить навсегда»
   лежали в одном свитке через <hr> (аудит 05-08). Один класс на оба места, цвет — из токенов.
   --bad-ink, а не --bad: красный текст на собственной подложке не добирает контраст (см. :root). */
.card.dangerzone{border-color:rgba(248,81,73,.35)}
.card.dangerzone h2{color:var(--bad-ink)}
/* Поля страницы на ТЕЛЕФОНЕ. 28 px у .wrap заводились под десктоп; на 390 px они вместе с полем
   карточки (20) съедали 98 px из 390 — четверть экрана, — и на текст оставалось 292 px (замер
   11-08 на /users/14). Отсюда трёхстрочные заголовки карточек и разорванные пополам строки таблиц.
   Отдаём телефону 16 и 14: на текст становится 328 px (+12 %), а карточка человека на 390 px
   укоротилась с 9616 до 8583 px прокрутки (вместе с правкой h3 ниже). Порог 601, потому что ровно
   там панель переключается на нижнее меню, — своей границы не заводим. */
@media(max-width:600px){
  .wrap{padding:18px 16px}
  .card{padding:8px 14px 14px}
}
table{width:100%;border-collapse:collapse}
th{text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.04em;
  color:var(--muted);font-weight:600;padding:8px 10px;border-bottom:1px solid var(--line)}
/* tabular-nums: в пропорциональном шрифте цифры разной ширины, и колонки чисел (трафик, счётчики,
   даты) визуально «пляшут» — сравнивать значения в столбик тяжело. */
td{padding:10px;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}
/* Крайние ячейки — БЕЗ бокового поля: 10 px у них не разделяют колонки (соседа с той стороны нет),
   а сдвигают весь текст таблицы вправо от заголовка карточки. Это и была третья вертикаль из
   комментария «ОДИН ЛЕВЫЙ КРАЙ» выше: «Устройства · 1/3» на 303, «МЕТКА» на 313. Внутренние поля
   (10 px с каждой стороны, то есть 20 между колонками) не трогаем — они работают. Правая сторона
   зеркально: последняя колонка (обычно «Действия») встаёт на правое поле карточки.
   Вес (0-1-1) — тот же, что у частных `.domains td`/`.alerts td`; там, где у таблицы своё поле,
   побеждает то правило, что ниже по файлу, и это правильно (см. .alertlog — ему поле нужно). */
th:first-child,td:first-child{padding-left:0}
th:last-child,td:last-child{padding-right:0}
/* ── ЗАКРЕПЛЁННАЯ ШАПКА ТАБЛИЦЫ ───────────────────────────────────────────────────────────────
   Прокрутил список на полсотни строк — и уже не помнишь, что за колонка. В панели 25 разделов и
   почти все — длинные таблицы (замер на стенде: /users отдаёт 50 строк, документ 3247 px при окне
   900 — шапка уезжает на 2122 px ВВЕРХ от экрана).

   ⚠️ Одной строкой `position:sticky` это НЕ чинится, и вот почему (замер 08-08 на живой странице).
   Sticky приклеивается к «скроллпорту» БЛИЖАЙШЕГО прокручиваемого предка, а у нас их два:
   `.card{overflow-x:auto}` и `.tablewrap{overflow-x:auto}`. Достаточно ОДНОЙ оси: по спецификации
   CSS Overflow, если `overflow-x` не `visible`, то `overflow-y:visible` вычисляется в `auto` —
   getComputedStyle на проде подтвердил `overflow-y: auto` у обоих. То есть контейнер вертикально
   прокручиваемый, но прокручивать в нём нечего (scrollHeight == clientHeight == 2646), и шапка
   приклеивается к границе, которая сама уезжает вместе со страницей. Видно это только В ПРОКРУТКЕ:
   на статичном снимке всё выглядит правильно.

   Значит нужна ПЕРЕСТРОЙКА: вертикальную прокрутку надо ОТДАТЬ той же обёртке, что уже даёт
   горизонтальную, ограничив ей высоту (см. `.tablewrap` ниже). Тогда у шапки появляется настоящая
   граница, к которой липнуть, а колесо над таблицей крутит саму таблицу — верх обёртки (и шапка
   вместе с ним) остаётся на экране.

   Фон обязателен и обязан быть НЕПРОЗРАЧНЫМ (--card-solid, а не --card): у карточек фон — плёнка
   в 2 % белого, и сквозь неё первая строка данных читалась ПОВЕРХ названий колонок. Линия — box-shadow,
   а НЕ border: при `border-collapse:collapse` рамку рисует таблица, а не ячейка, и у приклеенной
   шапки она уезжает вниз вместе с содержимым. Тень принадлежит самой ячейке и едет с ней. */
thead th{position:sticky;top:0;z-index:2;background:var(--card-solid);
  box-shadow:inset 0 -1px 0 var(--line)}
/* Планшетная полоса 601–900: сверху висит закреплённая кнопка ☰ (position:sticky, 44 px + отступы),
   и шапка, приклеенная к нулю, уходила бы ПОД неё — накрытой оказывалась первая колонка, самая
   нужная. Опускаем шапку ровно на высоту кнопки. Ниже 601 кнопки нет вовсе (её заменяет нижняя
   панель), выше 900 сайдбар снова сбоку — там правило не действует.
   ⚠️ Блок обязан стоять ПОСЛЕ `thead th{top:0}`: вес тот же, побеждает порядок. Стоял выше —
   и молча не работал (проверено getComputedStyle: top=0px на 800 px). */
@media(min-width:601px) and (max-width:900px){
  thead th{top:64px}
  /* …но только когда шапка липнет к ОКНУ (обёртка распущена классом fits-x). В КОРОБКЕ — обёртке,
     оставшейся прокручиваемой ради широкой таблицы, — шапка липнет к верху самой коробки, а он и
     так ниже кнопки ☰. Офсет 64px там оставлял над шапкой полосу, в которой строки ехали ПОВЕРХ
     названий колонок (замер 09-08, /audit на 800px: строка «28:17 claude user.block» поверх
     «АДМИН / ДЕЙСТВИЕ»). Вес (0-2-2) выше базового (0-0-2) — порядок не важен, но держим рядом. */
  .tablewrap:not(.fits-x) thead th{top:0}
  /* Запас прокрутки к элементу — под ОБА закреплённых слоя (кнопка 64 + шапка ~40). */
  html{scroll-padding-top:104px}
}
tbody tr:hover{background:rgba(255,255,255,.03)}
tbody tr:last-child td{border-bottom:0}
td.empty{text-align:center;color:var(--muted);padding:26px}
.keys span{margin-right:4px}

/* inline-block + nowrap: у бейджа с пробелом в подписи («не удалось», «разведка идёт») строка
   переносилась по словам, и цветная плашка рисовалась ДВУМЯ смещёнными прямоугольниками —
   на каждой упавшей заявке (аудит 05-08). Бейдж — это ярлык, он не переносится. */
.badge{display:inline-block;white-space:nowrap;font-size:12px;padding:3px 9px;border-radius:8px;font-weight:600;
  background:var(--panel);color:var(--muted)}
.st-active{background:rgba(63,185,80,.15);color:var(--ok)}
.st-provisioning,.st-creating,.st-bootstrapping,.st-registering{background:rgba(210,153,34,.15);color:var(--warn)}
.st-prepared{background:rgba(88,166,255,.16);color:#79c0ff}  /* пул: готова, ждёт назначения (SPEC-0033) */
.st-unreachable,.st-retired,.st-degraded,.st-draining{background:rgba(248,81,73,.15);color:var(--bad-ink)}
.role{font-size:12px;padding:3px 9px;border-radius:8px;font-weight:600;background:var(--panel)}
.role-relay{color:#79c0ff}.role-exit{color:#d2a8ff}.role-direct{color:#7ee787}
.role-pool{color:var(--muted);font-style:italic}  /* нода в пуле — роль не назначена */
.flag{font-weight:600;text-transform:uppercase;font-size:12px;color:var(--accent)}

.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}
.dot.ok{background:var(--ok)}.dot.bad{background:var(--bad)}

.login{max-width:340px;margin:60px auto;background:var(--panel);
  border:1px solid var(--line);border-radius:12px;padding:28px}
.login h1{font-size:20px;margin:0 0 18px}
.login label{display:block;font-size:13px;color:var(--muted);margin-bottom:14px}
/* min-height 44 — норма попадания на телефоне, та же, что панель держит для кнопок и пунктов
   меню. Замер 22-08 на 390 px: поле было 41 px, кнопка «Войти» 39 — единственный экран, который
   открывает КАЖДЫЙ заход в панель, и он же был ниже нормы. Высота добирается min-height, а не
   кеглем: буквы остаются прежними. */
.login input{display:block;width:100%;margin-top:6px;padding:10px;border-radius:8px;min-height:44px;
  border:1px solid var(--line);background:var(--bg);color:var(--txt);font-size:15px}
.login button{width:100%;margin-top:8px;padding:11px;border:0;border-radius:8px;min-height:44px;
  background:var(--primary);color:var(--primary-ink);font-weight:700;font-size:15px;cursor:pointer}
.login button:hover{background:var(--primary-hover)}
.alert{background:rgba(248,81,73,.12);color:var(--bad-ink);border:1px solid rgba(248,81,73,.3);
  padding:10px 12px;border-radius:8px;margin-bottom:16px;font-size:14px}
/* Успех — ЗЕРКАЛО отказа. Класса `.ok` в стилях не существовало вовсе, хотя <p class="ok"> стоит на
   восьми страницах: «✔ Сохранено.» на /settings, /alerts, /domains, /plans, /payments, /2fa
   печаталось обычным серым текстом (отказ рядом при этом — красной плашкой), а на /nodes/new
   `class="alert ok"` давало хорошей новости КРАСНУЮ коробку. Та же геометрия, что у .alert. */
p.ok,.ok-alert{background:rgba(63,185,80,.12);color:var(--ok);border:1px solid rgba(63,185,80,.3);
  padding:10px 12px;border-radius:8px;margin-bottom:16px;font-size:14px}

/* Штатное состояние, о котором надо знать, — НЕ отказ. Учётки без почты у нас основные (SPEC-0046),
   и красная плашка «✖ у обращения нет адреса почты» висела над формой ответа у каждого второго
   обращения: поддержка привыкает к кресту и перестаёт отличать его от настоящего сбоя. Та же
   геометрия, что у .alert и p.ok, но спокойный цвет. */
.note-plain{background:rgba(139,148,158,.10);color:var(--txt);border:1px solid var(--card-bd);
  padding:10px 12px;border-radius:8px;margin-bottom:16px;font-size:14px}

/* --- админ-панель: пользователи/тарифы/действия --- */
.bad-num{color:var(--bad)}
.warn-num{color:var(--warn)}
/* Зеркало .bad-num для числа, которое ХОРОШЕЕ (доля подключений, уложившихся в порог SLO). Без него
   «88,0 %» красным и «97,2 %» обычным серым читались бы как «плохо» и «никак», хотя второе — это
   выполненное обещание. Класс, а не <p class="ok">: та плашка — про сообщение, а тут число в
   ячейке таблицы. */
.ok-num{color:var(--ok)}
/* Вердикт обзора — одна строка «всё ли хорошо прямо сейчас» над плитками. Плашка, а не плитка:
   она не про число, а про состояние, и должна читаться раньше всех цифр. */
.verdict{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding:11px 14px;border-radius:8px;margin:0 0 20px;font-size:14px;font-weight:600;
  border:1px solid var(--card-bd);background:var(--card);color:var(--txt)}
.verdict-ok{border-color:rgba(63,185,80,.28);background:rgba(63,185,80,.09);color:var(--ok)}
.verdict-bad{border-color:rgba(248,81,73,.4);background:rgba(248,81,73,.10);color:var(--bad)}
a.verdict-bad:hover{border-color:var(--bad);text-decoration:none}
.verdict-idle{color:var(--muted)}
/* «Не могу утверждать»: не зелёное и не красное — отдельный цвет для «проверить не удалось».
   Раньше такого состояния не было вовсе, и любая непроверенная вещь молча попадала в зелёное. */
.verdict-warn{border-color:rgba(210,153,34,.4);background:rgba(210,153,34,.10);color:var(--warn)}
.verdict .muted{font-weight:500}
/* Список фактов под вердиктом: из чего сложился вывод. Одна строка = одна проверка, ссылка ведёт
   туда, где смотреть/чинить. Плашка-вердикт отвечает «да/нет», список — «почему». */
.verdict{margin-bottom:10px}
.verdict-list{list-style:none;margin:0 0 16px;padding:0;font-size:13.5px;display:grid;gap:6px}
.verdict-list li{padding:8px 12px;border:1px solid var(--card-bd);border-radius:8px;background:var(--card)}
/* Строка вердикта — ЦЕЛЬ НАЖАТИЯ целиком. Отступы стояли у <li>, а ссылка внутри была ровно по
   тексту: замер 22-08 на 390 px дал 332×20 у однострочных строк. Шесть таких строк идут стопкой
   с зазором 6 px — промах по соседней уводит не туда, куда человек шёл, и это первое, что он
   трогает на телефоне, когда пришла тревога. Отступы переезжают НА ССЫЛКУ (тогда они часть цели),
   пол высоты 44 px. У строк без ссылки всё как было — отсюда :has(). */
.verdict-list li:has(> a){padding:0}
/* 🔴 display:block, а НЕ flex. Флексом я это сделал в тот же день 22-08 — и при 200 % зума
   (195 px ширины) строка вердикта поехала за край: у флекса `<b>Выходы:</b>` и текст рядом с ним
   становятся ДВУМЯ элементами, и ни один не умеет сжаться ниже своего min-content. Блок с
   min-height даёт ту же цель нажатия 44 px и переносится нормально. */
.verdict-list a{color:var(--txt);text-decoration:none;display:block;min-height:44px;padding:8px 12px}
.verdict-list a:hover{text-decoration:underline}
.verdict-list li.vc-bad{border-color:rgba(248,81,73,.4)}
.verdict-list li.vc-bad b{color:var(--bad)}
.verdict-list li.vc-unknown{border-color:rgba(210,153,34,.35)}
.verdict-list li.vc-unknown b{color:var(--warn)}
.verdict-list li.vc-ok b{color:var(--ok)}
.verdict-why{margin:0 0 16px}
.verdict-why>summary{cursor:pointer;color:var(--muted);font-size:13px;padding:2px 0}
.verdict-why[open]>summary{margin-bottom:8px}
.verdict-noise{font-size:13px;margin:-6px 0 16px}
/* Горящие тревоги под вердиктом (обзор, 08-08). Раньше это была таблица в середине страницы, ПОСЛЕ
   тринадцати плиток: вердикт говорил «горит 1: нода молчит», а какая нода и с какого часа —
   надо было доскроллить. Строка составная (бейдж + ссылка на объект + текст + время), поэтому
   ссылка внутри неё не должна быть блоком во всю строку, как у строк-проверок выше. */
.firing li{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px}
.firing a{display:inline}
/* …и отступы у такой строки остаются на <li>, а не на ссылке (25-09). Правило строк-проверок
   `li:has(> a){padding:0}` + `a{padding:8px 12px}` срабатывало и здесь: поля получала ссылка «нода
   2222…» посреди строки, а бейдж, «— описание» и «горит с…» стояли вплотную к рамке — на 390 это
   две строки текста, прилипшие к левому краю карточки. Цель нажатия у этой ссылки — строка текста
   (исключение WCAG 2.5.8 для ссылок в тексте), своей высоты ей не нужно. Вес тот же, что у
   правил строк-проверок, — побеждает порядок, поэтому блок стоит ниже них. */
.firing li:has(> a){padding:8px 12px}
.firing a{padding:0;min-height:0}
/* «С чего начать» — карточка вместо пустых таблиц с нулями. Показывается, только пока в системе
   нет ни одного устройства: нули на таком обзоре законны, и человеку нужно не сообщение о пустоте,
   а следующий шаг (NN/g: состояние + объяснение + путь к действию). */
.startup-steps{margin:0 0 6px;padding-left:22px;line-height:1.65}
.startup-steps li{margin-bottom:8px}
/* Разбор журнала оповещений: из чего состоит «неразобранное» — что ждёт человека, а что остыло. */
.triage{font-size:13.5px;margin:10px 0}
.triage .danger{font-weight:600}

.tiles+.tiles{margin-top:-8px}
/* «Пульс» обзора: ежедневные цифры (онлайн, трафик, истекающие) — акцентная линия слева, чтобы
   ряд читался как сводка состояния, а не как продолжение справочных счётчиков под ним. */
.pulse .tile{border-left:2px solid rgba(34,184,207,.5)}
/* Второй ярус плиток — «Справочно» (замер владельца 03-08: 13 плиток одного веса, без иерархии;
   то, что реально требует внимания — «пульс» выше и вердикт над ним, — читалось наравне с чистыми
   итогами вроде «Направлений»). Тише: мельче цифра, мельче иконка-бокс, без akcent-рамки пульса —
   это подробность по клику, а не то, ради чего открыли обзор. */
/* Отступ СВЕРХУ у названия группы больше, чем зазор между плитками (16px): иначе «Люди и
   устройства», «Флот» и «Деньги» стояли на том же расстоянии, что и плитки внутри группы, и группы
   не читались как группы (проход по UX 11-08). */
/* Кнопки фильтра держатся ПАРОЙ: в гибкой строке они переносились порознь — «Показать» оставалась
   в конце первой строки, «Сбросить» уезжала в начало второй, и две кнопки одной формы расходились
   по разным углам (проход по UX 11-08). */
.filter-actions{display:inline-flex;gap:8px;align-items:center;flex:0 0 auto}

.tiles-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
  font-weight:700;margin:26px 0 10px}
.tiles-quiet .tile{padding:11px 14px}
.tiles-quiet .tile-ic{width:30px;height:30px;background:rgba(255,255,255,.04);color:var(--muted)}
.tiles-quiet .tile-ic svg{width:16px;height:16px}
.tiles-quiet .tile .num{font-size:19px}
.tiles-quiet .tile .lbl{font-size:11.5px}
.tiles-quiet .tile .sub{font-size:11px}
/* Ссылка «весь список →» в заголовке карточки: справа, не оттягивает на себя внимание от цифр. */
.card h2 .seeall{float:right;font-size:13px;font-weight:500}
/* «топ за 7 дней →» и соседи стояли 17 px в высоту — они не внутри предложения, а сами по себе,
   значит и цель должна быть под палец (замер 22-08). Кегль не трогаем, растёт только область. */
@media(max-width:700px){ .card h2 .seeall a{display:inline-flex;align-items:center;min-height:44px} }
/* 🔴 `.seeall` бывает ДВУХ форм, и правило выше берёт только одну. Обёртка —
   `<span class="seeall"><a>…</a></span>` (обзор), сама ссылка — `<a class="seeall">` (карточка
   человека ×4: «вся его ветка →», «весь лог →», «все платежи →», «все его приглашения →», плюс
   /lines). Селектор `.seeall a` ищет ссылку ВНУТРИ `.seeall` и во второй форме не совпадает ни с
   чем — замер 25-08 на 390 px честно показал у них 21 px при норме 44.
   Сторож TestMobile_SeeAllLinkIs44 всё это время зелёный: он сверяет ТЕКСТ правила в style.css, а
   не то, что правило кого-то нашло. Поэтому строку выше НЕ трогаем (её и проверяет сторож), а
   вторую форму покрываем отдельным правилом. */
@media(max-width:700px){ .card h2 a.seeall{display:inline-flex;align-items:center;min-height:44px} }
/* График суточного трафика: высота фиксирована, ширина тянется — столбиков всегда 14. */
.daybars{padding:4px 0 0}
.daybars .bars{display:block;width:100%;height:64px}
.daybars-axis{display:flex;justify-content:space-between;color:var(--muted);font-size:12px;padding:6px 0 10px}
/* Подпись-шкала под столбиками. Высота столбика относительна максимуму, и без этой строки график
   ничего не сообщает о величинах: на десктопе спасало наведение, на телефоне его нет. */
.daybars-scale{margin:0 0 10px;font-size:12px;line-height:1.5}
/* Подложка КАЖДЫХ суток (нулевые дни включая) рисуется в metrics.go (barsSVG) с фиксированной
   прозрачностью — литерал `opacity=".06"` в SVG-атрибуте, отсюда селектор по значению. На тёмном
   фоне такая подложка почти не видна, и день с нулевым трафиком читается как дыра в графике, а не
   как «мы измерили — там ноль» (замер владельца 03-08: «первые пять столбцов пустые, подписей и
   чисел нет вообще»). Поднимаем видимость ТОЛЬКО подложки и даём ей край — столбики с реальными
   данными (своя переменная прозрачность 0.55–1.00) не трогаем.
   ⚠️ Это подпорка, не полный фикс: у суток с Bytes=0 сам рект-с-данными рисуется высотой 0 и БЕЗ
   <title> вообще — навести мышью не на что, значение узнать нельзя даже так. Настоящий фикс —
   в metrics.go (см. docs/research/2026-08-03-admin-design.md), это Go и не в зоне этого агента. */
.daybars svg rect[opacity=".06"]{opacity:.18;stroke:rgba(255,255,255,.14);stroke-width:1}

.search{display:flex;gap:8px}
.search input{padding:8px 12px;border-radius:8px;border:1px solid var(--line);
  background:var(--bg);color:var(--txt);font-size:14px;min-width:240px}
.search button{padding:8px 16px;border:0;border-radius:8px;background:var(--primary);
  color:var(--primary-ink);font-weight:700;cursor:pointer}
.search button:hover{background:var(--primary-hover)}

/* Постраничность. Ссылки «← назад» / «вперёд →» — ЕДИНСТВЕННЫЙ способ дочитать длинный журнал, и
   мерились они 72×23 при норме попадания 44 (замер 23-08 на /users, 390 px). Промах по ним стоит
   дороже обычного: страница прокручена в самый низ, и человек не понимает, нажалось или нет.
   Делаем целью саму ссылку, а не строку, — иначе «назад» и «вперёд» слипаются в одну область. */
.pager{display:flex;gap:18px;align-items:center;justify-content:center;padding:8px 0 24px}
.pager a,.pager .pg-off{display:inline-flex;align-items:center;justify-content:center;min-height:44px;min-width:44px;padding:0 10px}
/* Неактивная сторона — на месте и серым: пропадающая кнопка сдвигает соседнюю под палец, и второе
   нажатие «вперёд» попадает в «назад» (та же ловушка, что у исчезающих кнопок в мастере ноды). */
.pager .pg-off{color:var(--muted);opacity:.45}
.pager .pg-range{white-space:nowrap}
@media(max-width:520px){ .pager{flex-wrap:wrap;gap:8px 12px} .pager .pg-range{display:block} }
/* Низ списка людей: «показано 1–N из M» и выбор «на странице» — виден и на одной странице (владелец 27-09). */
.users-foot{display:flex;flex-wrap:wrap;gap:4px 24px;align-items:center;justify-content:center;padding:12px 0 0}
.users-foot .perpage a,.users-foot .perpage b{display:inline-flex;align-items:center;justify-content:center;min-height:44px;min-width:44px;padding:0 6px}
th.rownum,td.rownum{width:1%;white-space:nowrap;text-align:right}
/* Список людей, порядок 27-09 (владелец: «приведи в порядок страницу»). Вкладки срезов с числами
   вместо выпадающего списка из 15 пунктов; редкие срезы — под «Ещё». Цели нажатия — 44 px. */
.srezy{display:flex;flex-wrap:wrap;gap:4px 6px;align-items:center;margin:0 0 14px;padding-bottom:10px;
  border-bottom:1px solid var(--line)}
.srezy .srez{display:inline-flex;align-items:center;gap:6px;min-height:44px;padding:0 12px;border-radius:8px;
  color:var(--muted);font-weight:600;font-size:14px;white-space:nowrap}
.srezy .srez:hover{color:var(--txt);background:var(--panel2)}
.srezy .srez.on{color:var(--txt);background:rgba(34,184,207,.13);box-shadow:inset 0 -2px 0 var(--accent)}
.srezy .srez-n{font-variant-numeric:tabular-nums;font-weight:500;color:var(--muted)}
.srezy .srez.on .srez-n{color:var(--accent)}
.srezy-eshche{position:relative}
.srezy-eshche>summary{display:inline-flex;align-items:center;min-height:44px;padding:0 12px;border-radius:8px;
  color:var(--muted);font-weight:600;font-size:14px;cursor:pointer;list-style:none;white-space:nowrap}
.srezy-eshche>summary::-webkit-details-marker{display:none}
.srezy-eshche>summary::after{content:"▾";margin-left:6px;font-size:11px}
.srezy-eshche.on>summary{color:var(--txt);background:rgba(34,184,207,.13);box-shadow:inset 0 -2px 0 var(--accent)}
.srezy-menu{position:absolute;z-index:5;top:calc(100% + 4px);right:0;min-width:340px;display:flex;flex-direction:column;
  padding:6px;border:1px solid var(--line);border-radius:10px;background:var(--panel2);box-shadow:0 8px 24px rgba(0,0,0,.35)}
.srezy-menu .srez{justify-content:space-between}
/* Строка — ссылка на карточку: видно по курсору и подсветке (без них щелчок по строке не открыть глазами). */
.users tbody tr[data-href]{cursor:pointer}
.users tbody tr[data-href]:hover>td{background:rgba(34,184,207,.05)}
.users th.num{text-align:right}
.users .devs{display:block;font-size:12px;color:var(--muted);white-space:nowrap}
/* Десять колонок в 1280 px: поля тоньше, длинная почта переносится в любом месте, а не распирает таблицу. */
.users th,.users td{padding-left:8px;padding-right:8px}
.users td[data-l="Email / логин"]{overflow-wrap:anywhere;min-width:21ch;max-width:26ch}
.users .sub-left{display:block;font-size:12px;color:var(--muted)}
.users .sub-left.soon{color:var(--warn-ink)}
.users .appver{display:block;font-size:12px;color:var(--muted)}
.notemark.warnmark{color:var(--warn-ink);border-color:currentColor}
.users-foot .csvlink{display:inline-flex;align-items:center;min-height:44px}
/* Панель массовых действий ПРИЛИПАЕТ к низу окна, пока что-то отмечено: на странице в 100 строк
   иначе отметил наверху — и листай вниз искать кнопку. */
.bulkbar.on{position:sticky;bottom:8px;z-index:4;box-shadow:0 -6px 20px rgba(0,0,0,.35)}
@media(max-width:700px){
  .srezy .srez,.srezy-eshche>summary{padding:0 10px}
  .srezy-menu{left:0;right:auto;min-width:min(300px,calc(100vw - 48px))}
  .users .sub-left,.users .appver,.users .devs{display:inline;margin-left:6px}
  .users td[data-l="Email / логин"]{min-width:0;max-width:none}
  .users tbody td.rownum{display:none}
}

table.kv{width:auto}
table.kv th{text-transform:none;letter-spacing:0;font-size:14px;color:var(--muted);
  padding:8px 24px 8px 0;border:0;vertical-align:top}
table.kv td{border:0;padding:8px 0}

/* ── ЕДИНЫЙ СЛОЙ ФОРМ (2026-07-25) ───────────────────────────────────────────
   Раньше каждая страница красила поля сама: .fld в визарде, .plan-form в тарифах, .alerts input,
   .billing .filterbar… Из-за этого одинаковые по смыслу поля имели разную высоту, отступы и радиус —
   панель выглядела собранной из кусков («криво-косо», замечание владельца 2026-07-25).
   Здесь базовый вид ЛЮБОГО поля ввода + три примитива: .field (подпись+контрол+подсказка),
   .form-grid (адаптивные колонки) и .form-actions (ряд кнопок). Частные правила ниже постепенно
   вытесняются этим слоем. */
/* ⚠️ `input:not([type])` и `date` в списке — не педантизм. Атрибутный селектор НЕ матчит поле без
   type, хотя браузер считает его текстовым: из-за этого на визарде («Добавить ноду» — первый экран,
   через который заводят машину) поля «Публичный IPv4», «Имя ноды» и «SSH-логин» рисовались БЕЛЫМИ
   системными инпутами прямо рядом с тёмным «SSH-пароль» на той же форме, а фильтр дат на /audit —
   белым системным календарём. Заодно высота: 21 px против 38 у остальных полей. Замер 02-08. */
input:not([type]),input[type=text],input[type=password],input[type=email],input[type=number],
input[type=search],input[type=url],input[type=tel],input[type=date],input[type=datetime-local],
input[type=time],select,textarea{
  padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);
  color:var(--txt);font:inherit;font-size:14px;width:100%;max-width:100%;
  transition:border-color .15s ease,box-shadow .15s ease}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);
  box-shadow:0 0 0 3px rgba(34,184,207,.18)}
/* У полей фокус свой (рамка + подсветка), а ссылки, кнопки и раскрывашки полагались на кольцо
   БРАУЗЕРА. В Chrome оно на тёмном фоне видно, в других — по-разному (в Firefox тонкий пунктир,
   который на #161b22 почти не читается). Рисуем своё: тот же акцент, что у полей, — тогда фокус
   выглядит одинаково везде и совпадает с дизайн-системой. Только :focus-visible, чтобы кольцо не
   появлялось от мыши. */
a:focus-visible,button:focus-visible,summary:focus-visible,label:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
/* #6e7681 (dark-3) на тёмном фоне даёт 4.12 при пороге AA 4.5 — правильный токен приглушённого
   текста один: --muted (#8b949e), он же 6.15 на фоне поля. */
input::placeholder,textarea::placeholder{color:var(--muted)}
/* 🔴 ОТКЛЮЧЁННОЕ ПОЛЕ ГАСИЛОСЬ opacity:.5 — и текст в нём падал до контраста 3.68 при пороге AA
   4.5 (ревью панели 29-08, п.16). Прозрачность не разбирает, что она гасит: она умножает и рамку,
   и фон, и БУКВЫ. А disabled в панели чаще всего значит не «кнопка не нажимается», а «у вашей роли
   нет права правки» — то есть роль «только чтение» хуже всего читала ровно те данные, ради которых
   её и завели (тарифы, каналы тревог, домены, настройки).
   Лечим правильным способом: «нельзя трогать» показываем ФОНОМ, рамкой и курсором, а буквы красим
   штатным приглушённым токеном --muted (#8b949e — 6.15 на фоне поля, см. комментарий выше).
   ⚠️ Где значение можно НЕ показывать полем — правильнее печатать его текстом, а не отключать
   контрол (так сделано в plans.html): disabled — состояние контрола, а не способ вывода данных.
   Это правило остаётся для тех экранов, где поле по делу (форма, часть которой закрыта ролью). */
input:disabled,select:disabled,textarea:disabled{
  cursor:not-allowed;color:var(--muted);border-style:dashed}
textarea{resize:vertical;min-height:88px}
select{appearance:none;
  background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
  background-position:calc(100% - 17px) 50%,calc(100% - 12px) 50%;background-size:5px 5px,5px 5px;
  background-repeat:no-repeat;padding-right:34px}
input[type=checkbox],input[type=radio]{width:auto;accent-color:var(--accent);cursor:pointer}

/* Поле = подпись + контрол + подсказка. Подсказка не двигает соседние поля: у строки формы
   выравнивание по верху, а не растягивание. */
.field{display:flex;flex-direction:column;gap:6px;margin-bottom:16px;min-width:0}
.field>.cap,.field>label{font-size:12.5px;font-weight:600;color:var(--muted)}
.field .hint{font-size:12px;color:var(--muted);line-height:1.45}
.field .err{font-size:12px;color:var(--bad)}
.field.invalid input,.field.invalid select,.field.invalid textarea{border-color:var(--bad)}
.field .req{color:var(--accent)}
/* Чекбокс/радио живут в строку с подписью. */
.field-inline{display:flex;align-items:center;gap:9px;margin-bottom:16px;font-size:14px}
.field-inline label{font-size:14px;font-weight:500;color:var(--txt)}

/* minmax(min(260px,100%),1fr), а не minmax(260px,1fr): дорожка с жёстким минимумом 260 px не
   может стать уже него НИКОГДА — при 200 % зума (экран 195 px) сетка оставалась 260 px шириной и
   тянула вбок всю страницу с формой (замер 22-08 на /users: документ 217 при окне 195). `min()`
   снимает минимум ровно тогда, когда он больше самого контейнера; на широких экранах поведение
   прежнее — колонки по 260 px и больше. */
.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:0 18px;align-items:start}
/* .two — ровно две колонки: поля, которые читаются парами (логин/пароль, порт/интерфейс), не должны
   разъезжаться по трём колонкам просто потому, что карточка широкая. */
.form-grid.two{grid-template-columns:repeat(2,minmax(0,1fr))}
@media(max-width:700px){.form-grid.two{grid-template-columns:1fr}}
.form-grid .full{grid-column:1 / -1}
.form-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:4px}
.form-actions .spacer{flex:1}

.actions{display:flex;gap:10px;flex-wrap:wrap;margin:4px 0 16px}
.actions form{margin:0}
/* Недоступная кнопка должна ВЫГЛЯДЕТЬ недоступной: раньше disabled отличался только поведением, и
   «Удалить» у занятого тарифа выглядела рабочей, пока не нажмёшь (title объясняет причину). */
.btn:disabled,.btn[disabled]{opacity:.45;cursor:not-allowed}
.btn:disabled:hover,.btn[disabled]:hover{border-color:var(--line)}
.btn.sm{padding:5px 11px;font-size:13px}
.btn-danger{color:var(--bad-ink);border-color:rgba(248,81,73,.4)}
.btn-danger:hover{background:rgba(248,81,73,.12);border-color:var(--bad)}
/* Недостающие классы (аудит 2026-07-12): раньше рендерились голыми. */
.filterbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:4px 0 16px}
.notice{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:12px 14px;color:var(--muted);font-size:14px;margin-bottom:16px}
/* `.danger` значил ДВЕ РАЗНЫЕ вещи сразу — «кнопка опасного действия» и «красный текст», — и
   оформление кнопки доставалось обоим: <span class="danger">не дошло в telegram</span> получал
   рамку и паддинг 8×16 прямо посреди строки таблицы, <p class="danger">⚠️ …</p> — вид коробки-кнопки.
   Ровно этой подменой 01-08 объяснялась «красная коробка посреди таблицы админов» (N7): там её
   вылечили точечно, здесь — в корне. Теперь: вид кнопки — только у <button>/.btn, всё остальное —
   просто красный текст; настоящие предупреждения-плашки размечены классом .alert. */
button.danger,.btn.danger{border-color:rgba(248,81,73,.4);background:var(--panel2);color:var(--bad-ink)}
button.danger{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:9px 16px;
  border:1px solid rgba(248,81,73,.4);border-radius:8px;font:inherit;font-size:14px;font-weight:600;
  line-height:1.25;cursor:pointer;white-space:nowrap}
button.danger:hover,.btn.danger:hover{background:rgba(248,81,73,.12);border-color:var(--bad)}
.danger{color:var(--bad)}

.plan-form{display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap;margin:4px 0}
.plan-form label{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--muted)}
.plan-form select,.plan-form input{padding:8px 10px;border-radius:8px;border:1px solid var(--line);
  background:var(--bg);color:var(--txt);font-size:14px}
.plan-form input[type=number]{width:120px}
/* Ряд «одно поле + кнопка» — самая частая форма панели, и на карточке человека их идёт ТРИ ПОДРЯД
   («Лимит устройств», «Набор направлений», «Конфиг роутера/ТВ»). Ширину каждому полю задавало его
   содержимое, поэтому кнопки вставали на 577, 580 и 565 px (замер 11-08, 1440×900) — не в ряд и не
   вразнобой, а «почти в ряд», что читается как неаккуратность. Одна ширина колонки — кнопки на
   одной вертикали (579). Формы с ДВУМЯ и более полями (смена тарифа тут же, «Новый тариф» на
   /plans) под правило не попадают: `label:first-of-type:last-of-type` — это «ровно один label»,
   у них своя раскладка и своя сумма ширин.
   align-items:flex-start — колонке отдана ширина, но не содержимому: поле «Лимит устройств»
   остаётся числовым на 120 px и не раздувается до 260 ради выравнивания. */
.plan-form>label:first-of-type:last-of-type{flex:0 0 260px;align-items:flex-start}

/* --- страница «Алерты» (alerts.html): каналы доставки + галочки категорий --- */
/* background-COLOR, а не сокращение `background`: сокращение сбрасывает background-image, которым
   у select нарисована стрелка «это список» (правило select{} выше). На «Алертах» и в «Журнале
   алертов» селекты из-за этого выглядели обычными текстовыми полями и были на 2 px ниже соседних
   (замер 05-08). Тот же класс ловушки, что и с `background` у тостов кабинета. */
.alerts input[type=text],.alerts input[type=password],.alerts select{padding:8px 10px;border-radius:8px;
  border:1px solid var(--line);background-color:var(--bg);color:var(--txt);font-size:14px}
/* ⚠️ `overflow` здесь БОЛЬШЕ НЕ ЗАДАЁТСЯ (и в .telemetry/.downloads/.fbsec тоже): его задаёт общее
   правило .tablewrap ниже, а частный дубль с тем же весом (0-2-0), стоящий ПОСЛЕ `.tablewrap.fits-x`,
   молча возвращал контейнеру прокрутку — и открепление шапки переставало работать ровно на этих
   страницах. Замер 08-08: /telemetry-stats и /downloads после правки теряли шапку у ДВУХ таблиц
   вместо одной, то есть стало хуже, чем было. Сторож на этот случай — TestНиктоНеВозвращаетОбёрткеПрокруткуПослеОткрепления. */
.alerts .tablewrap{-webkit-overflow-scrolling:touch}
/* Подсказка «таблица шире карточки, её можно листать вбок» — строкой ПОД таблицей. Ставит и убирает
   admin.js по факту непоказанной ширины. Почему не затемнение у правого края: псевдоэлемент внутри
   контейнера с overflow ЕЗДИТ ВМЕСТЕ С СОДЕРЖИМЫМ, то есть рисуется у правого края прокрученных 1736 px,
   а не у видимой границы — проверено глазами, подсказка оказалась не там, где нужна.
   Зачем вообще: скроллбар накладной и до первого движения не виден, поэтому колонок за краем для
   человека просто не существует — на 1440 телеметрия прячет «Активных дней» и «Бикон», «Выдачи» —
   «Кто выдал» и «Срок жизни» (аудит 31-07, A12; там про телефон, но то же ловится и на десктопе). */
.scrollhint{margin:6px 0 0;font-size:12px;color:var(--muted);user-select:none}
/* Прокручиваемую вбок область admin.js делает достижимой с клавиатуры (tabindex=0). Раз она попадает
   в обход табом — её обязано быть ВИДНО в фокусе: рамка внутрь (outline-offset отрицательный), иначе
   на карточке она обрезалась бы её собственным overflow. */
.tablewrap:focus-visible,.card:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.alerts table{min-width:640px}
.alerts table input[type=text]{width:100%;min-width:130px}
.alerts .filterbar>label{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--muted)}
/* Галочка в фильтре — В СТРОКУ с подписью: общее правило ставит подпись НАД полем (так надо селектам),
   и галочка «только неразобранные» уезжала на строку выше своего текста. */
.alerts .filterbar>label.chk{flex-direction:row;align-items:center;gap:8px}
.alerts .inline{display:inline-block;margin:0 6px 4px 0;vertical-align:middle}
.alerts .cats{display:inline-flex;gap:14px;align-items:center;flex-wrap:wrap}
.catbox{display:inline-flex;align-items:center;gap:5px;font-size:13px;color:var(--muted);
  white-space:nowrap;cursor:pointer;margin:0 10px 4px 0}
.catbox input{accent-color:var(--accent)}

/* --- страница «Домены» (реестр, миграция 0089) --- */
/* Колонки узкие по умолчанию (наследуются от .alerts), и домен с ролью обрезались до «cabinet.mayakvpı»
   и «API — a,». Задаём минимумы по смыслу поля: домен длинный, приоритет и код страны — короткие. */
/* Таблица должна ВЛЕЗАТЬ в карточку на десктопе: горизонтальная прокрутка прячет правую колонку с
   кнопками, а именно на неё и нужно нажать (эту грабку уже проходили на «Алертах»). Поэтому минимумы
   скромные, кнопки в столбик, а лишнюю ширину забирает «Заметка». */
.domains table { min-width: 0; }
.domains table input[type=text] { min-width: 120px; }
/* 🔴 Минимум под САМОЕ ДЛИННОЕ НАШЕ ИМЯ, а не «на глазок». При 175 px поле показывало
   «cabinet.mayaknetworks.c» — домен, то есть ОПОЗНАВАТЕЛЬНЫЙ ЗНАК строки, был обрезан на боевой
   странице (замер 25-08: видно 189 px, нужно 208). Имена доменов у нас ограничены по длине и
   известны, поэтому минимум ставится по ним и больше не гадается. */
.domains td[data-l="Домен"] input { min-width: 215px; }
/* «Заметка» должна ЧИТАТЬСЯ, а не только храниться: комментарий выше обещал ей «остаток ширины»,
   но `min-width:0` означает, что при тесноте она отдаёт всё и остаётся 109 px. Замер 25-08: три
   заметки из четырёх обрезаны («основной адрес ядра для приложения» — 279 px, «почта: SPF/DKIM/
   DMARC, отправка и приём» — 318, «личный кабинет» — 127). Дальше таблица уезжает в свою
   горизонтальную прокрутку — это честнее, чем показывать обрубок и молчать. */
.domains td[data-l="Заметка"] input { min-width: 160px; width: 100%; }
.domains td[data-l="Приоритет"] input { width: 74px; min-width: 0; }
.domains td[data-l="Где заблокирован"] input { width: 64px; min-width: 0; }
/* Только В ТАБЛИЦЕ (20-08). Без `table` это правило цепляло и ФОРМУ «Добавить домен» на той же
   странице: «Роль» и «Статус» становились узкими инлайн-селектами, а их подсказки втекали в ту же
   строку — форма выглядела сломанной, но только на широком экране (ниже 700px её чинило правило
   `width:100%` в медиа-блоке). */
.domains table select { min-width: 108px; width: auto; }
.domains th, .domains td { padding-left: 6px; padding-right: 6px; }
.domains td[data-l="Действия"] .btn { width: 100%; }
@media(max-width:700px){
  .domains table { min-width: 0; }
  .domains td[data-l="Приоритет"] input,
  .domains td[data-l="Где заблокирован"] input { width: 100%; }
  .domains table select { width: 100%; }
}

/* Каналы на телефоне: строка таблицы разворачивается в карточку. Горизонтального скролла на 390px
   формально хватало (обёртка .tablewrap), но таблица шире 800px, и «Сохранить»/«Тест» оказывались
   за краем — владелец правит chat_id с телефона и до кнопки не доходил (жалоба 2026-07-26).
   Подписи колонок берутся из data-l у ячеек, поэтому шапку прячем. */
@media(max-width:700px){
  .alerts table{min-width:0}
  .alerts thead{display:none}
  .alerts tbody,.alerts tbody tr,.alerts tbody td{display:block;width:100%}
  .alerts tbody tr{border:1px solid var(--card-bd);border-radius:8px;padding:6px 12px 12px;margin-bottom:14px}
  .alerts tbody td{border:0;padding:8px 0}
  .alerts tbody td::before{content:attr(data-l);display:block;margin-bottom:4px;
    font-size:12px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--muted)}
  .alerts tbody td[data-l="Вкл"]{display:flex;align-items:center;gap:10px}
  .alerts tbody td[data-l="Вкл"]::before{margin:0}
  .alerts tbody td .catbox{display:flex;margin:0 0 6px}
  /* Кнопки строки — в один ряд на всю ширину: на телефоне нужен палец, а не пиксель. */
  .alerts tbody td[data-l="Действия"]{display:flex;flex-wrap:wrap;gap:8px}
  .alerts tbody td[data-l="Действия"]::before{flex:1 0 100%}
  .alerts tbody td[data-l="Действия"] .inline{margin:0;flex:1 1 auto}
  .alerts tbody td[data-l="Действия"] .btn{width:100%}
  .alerts tbody td.empty::before{content:none}
}

/* /alerts на телефоне (25-09, чек-лист 23-09 №31): страница занимала около десяти экранов. Правило
   выше делало из КАЖДОЙ строки карточку «подпись над значением», то есть любое поле стоило двух
   строк: пятнадцать последних оповещений по четыре поля — это уже четыре экрана, таблица сторожа
   из девяти строк «ключ — значение» становилась девятью карточками с подписью, повторяющей жирное
   имя строки, а шесть галочек категорий у каждого канала вставали столбиком по одной.
   Здесь — тот же приём, что у списка людей: подпись и значение в ОДНУ строку, шапка карточки
   без подписей (бейдж «что» и время говорят сами), сторож остаётся таблицей, галочки ложатся в
   ряд с переносом. Ничего не спрятано под «подробнее»: это страница настроек, и все поля на ней
   главные (SITE-BACKLOG §2). Класс .alerts-page — только эта страница: .alerts носят ещё журнал
   тревог и «Домены». */
@media(max-width:700px){
  .alerts-page .alertlog tbody tr{display:grid;grid-template-columns:minmax(0,1fr) auto;
    column-gap:12px;padding:8px 12px 10px;margin-bottom:10px}
  .alerts-page .alertlog tbody td{grid-column:1 / -1;display:flex;flex-wrap:wrap;
    justify-content:space-between;align-items:baseline;gap:2px 12px;padding:3px 0;overflow-wrap:anywhere}
  .alerts-page .alertlog tbody td::before{display:inline;margin:0;flex:0 0 auto}
  /* Шапка карточки: «что» слева, «когда / горит с» справа, без подписей. */
  .alerts-page .alertlog tbody td[data-l="Что"]{grid-column:1;grid-row:1;display:block}
  .alerts-page .alertlog tbody td:is([data-l="Когда"],[data-l="Горит с"]){grid-column:2;grid-row:1;
    display:block;text-align:right;white-space:nowrap}
  .alerts-page .alertlog tbody td:is([data-l="Что"],[data-l="Когда"],[data-l="Горит с"],[data-l="Сообщение"],[data-l="Проблема"])::before{content:none}
  /* Текст тревоги — во всю ширину, отдельным абзацем: это то, ради чего карточку открыли. */
  .alerts-page .alertlog tbody td:is([data-l="Сообщение"],[data-l="Проблема"]){display:block}

  /* Сторож — таблица «ключ — значение» и на телефоне: две колонки влезают в 390 px. */
  .alerts-page table.alertkv{display:table;width:100%}
  .alerts-page table.alertkv tbody{display:table-row-group}
  .alerts-page table.alertkv tbody tr{display:table-row;border:0;padding:0;margin:0}
  .alerts-page table.alertkv tbody td{display:table-cell;width:auto;padding:6px 10px 6px 0;
    border-bottom:1px solid var(--card-bd);overflow-wrap:anywhere}
  .alerts-page table.alertkv tbody td:first-child{width:42%;padding-left:0}
  .alerts-page table.alertkv tbody td::before{content:none}

  /* Каналы: шапка карточки — тип слева, «вкл» справа, одной строкой; галочки категорий — рядом, с
     переносом; у кнопок подпись «Действия» не нужна — они говорят сами. */
  .alerts-page #kanaly tbody tr{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:12px}
  .alerts-page #kanaly tbody td{grid-column:1 / -1}
  .alerts-page #kanaly tbody td[data-l="Тип"]{grid-column:1;grid-row:1;padding:6px 0;align-self:center}
  .alerts-page #kanaly tbody td[data-l="Вкл"]{grid-column:2;grid-row:1;display:flex;align-items:center;
    gap:10px;padding:6px 0}
  .alerts-page #kanaly tbody td:is([data-l="Тип"],[data-l="Действия"])::before{content:none}
  /* Кнопки — в ряд на всю ширину. Явно: таблица получает ещё и общий класс .cards (admin.js), а его
     правило «ячейка с формой — блоком» сильнее старого .alerts-правила про «Действия». */
  .alerts-page #kanaly tbody td[data-l="Действия"]{display:flex;flex-wrap:wrap;gap:8px;padding-top:8px}
  .alerts-page #kanaly tbody td[data-l="Вкл"]::before{margin:0}
  .alerts-page #kanaly tbody td.empty{grid-column:1 / -1}
  .alerts-page #kanaly tbody td[data-l="Категории"]{display:flex;flex-wrap:wrap;gap:0 14px}
  .alerts-page #kanaly tbody td[data-l="Категории"]::before{flex:1 0 100%}
  .alerts-page #kanaly tbody td .catbox{display:inline-flex;min-height:32px;margin:0;white-space:normal}
}

/* Журнал оповещений (аудит админки 31-07, A3). Тело алерта приходит с переносами и бэктиками из
   алертера — показываем как есть, но не даём ему разорвать таблицу по ширине. */
.evbody{white-space:pre-wrap;word-break:break-word;margin:6px 0 0;padding:8px 10px;
  background:var(--bg);border:1px solid var(--card-bd);border-radius:6px;font-size:12.5px;line-height:1.45;
  max-height:260px;overflow:auto}
/* Маркер «это раскрывается» — тот же треугольник, что у пояснений `.explain` по всей панели.
   Без него «подробности» выглядели обычной серой подписью (класс .muted), хотя это ЕДИНСТВЕННЫЙ
   способ увидеть детали алерта: курсор менялся, но об этом узнаёшь, только наведя (аудит 05-08). */
.alertlog details summary{cursor:pointer;font-size:12px;color:var(--accent)}
.alertlog details summary::before{content:"▸ ";color:var(--accent)}
.alertlog details[open] summary::before{content:"▾ "}
.alertlog td{vertical-align:top}
/* Неразобранная проблема помечена полосой слева: в ленте из полусотни строк глаз должен находить
   «требует меня» без чтения каждой. Восстановления не подсвечиваем — это хорошая новость. */
.alertlog tr.ev-open td:first-child{border-left:3px solid var(--bad)}
.alertlog tr.ev-ok td:first-child{border-left:3px solid var(--ok)}
/* Исключение из правила «крайняя ячейка без поля» (см. th:first-child выше): у ЭТОЙ таблицы первая
   колонка несёт цветную полосу состояния, и без поля текст лёг бы к ней вплотную. Поле держим у
   всей колонки, включая шапку, — иначе полоса сдвигала бы только строки, и колонка съезжала бы
   относительно своего же названия. */
.alertlog th:first-child,.alertlog td:first-child{padding-left:10px}
@media(max-width:700px){
  .alertlog tr.ev-open td:first-child,.alertlog tr.ev-ok td:first-child{border-left:0}
  .alerts .alertlog tbody tr.ev-open{border-left:3px solid var(--bad)}
  .alerts .alertlog tbody tr.ev-ok{border-left:3px solid var(--ok)}
}

/* Общее правило для .tablewrap: широкая таблица скроллится ВНУТРИ карточки, а не растягивает всю
   страницу. Раньше это дублировалось для каждой секции (.alerts/.telemetry/.downloads/.fbsec), и
   стоило добавить обёртку в новом месте — она молча не работала: так 2026-07-25 таблица «Привязанные
   ноды» тянула документ до 558px при экране 390. Частные правила ниже оставлены — там свои min-width. */
/* `overflow:auto` (обе оси), а не только `overflow-x` — обёртка обязана быть прокручиваемой и по
   вертикали, иначе закреплённая шапка (`thead th{position:sticky}` выше) липнуть НЕ К ЧЕМУ.
   Горизонтальная прокрутка при этом никуда не девается — она тут не от лени, широкие таблицы
   листаются вбок на телефоне и на узком десктопе.

   max-height — то, ради чего всё и затевалось: без ограничения высоты обёртка ровно такая же, как
   таблица внутри (scrollHeight == clientHeight), прокручивать в ней нечего, и sticky молчит.
   Число: верх обёртки на страницах-со-списком лежит на 200–300 px от начала документа (замер
   стенда: /users — 225 px до шапки при окне 900), значит на 100dvh минус ~220 px коробка целиком
   помещается в экран, и её нижний край с горизонтальной полосой виден. Пол в 320 px — чтобы на
   низком окне (или в развёрнутой панели разработчика) таблица не схлопнулась в щель.
   dvh, а не vh: на телефоне vh считается по РАЗВЁРНУТОМУ окну, и коробка вылезала бы под адресную
   строку. Строкой выше — запасное значение для браузеров без dvh.
   scroll-padding-top — под высоту шапки: без него Tab по ссылкам внутри таблицы доводит строку
   ровно ПОД закреплённую шапку, и элемент в фокусе не виден. */
.tablewrap{overflow:auto;-webkit-overflow-scrolling:touch;scroll-padding-top:44px}
.tablewrap:has(thead th){max-height:max(320px,calc(100vh - 220px))}
.tablewrap:has(thead th){max-height:max(320px,calc(100dvh - 220px))}

/* ── КОРОБКА РАСПУСКАЕТСЯ, КОГДА ЛИСТАТЬ ВБОК НЕЧЕГО ─────────────────────────────────────────
   Коробка выше (max-height + своя прокрутка) держит шапку, ПОКА КРУТЯТ ЕЁ. Стоит человеку
   прокрутить саму СТРАНИЦУ — уезжает вся коробка вместе с шапкой: замер 08-08 на /users, окно
   1440×900, документ 1281 px → прокрутка страницы на 381 px уносила «EMAIL / ЛОГИН» с 225 px на
   −156 px, то есть за верхний край. На 1440 так вело себя 14 таблиц на 12 разделах.

   Почему не «сделать .content-area своим скроллером» (каркас app-shell). Пробовал и ЗАМЕРИЛ: шапка
   уезжала с теми же 225 → −156 — прокручиваемая коробка внутри осталась, а sticky липнет к
   БЛИЖАЙШЕМУ прокручиваемому предку, а не к окну. Плюс с фокусом на body PageDown переставал
   прокручивать что-либо (0 px против 381 px). То есть лечит не «кто прокручивается», а СНЯТИЕ
   лишнего скроллпорта между шапкой и окном.

   `overflow-x:clip` вместо `auto` — единственный способ снять его, не потеряв обрезку: по
   спецификации CSS Overflow контейнер с `clip` НЕ является прокручиваемым (а `hidden` — является,
   и sticky бы к нему прилипал так же). Пара обязана быть `clip` + `visible`: при `overflow-x:auto`
   значение `overflow-y:visible` вычисляется в `auto`, и коробка возвращается сама собой.

   Класс вешает admin.js ПО ЗАМЕРУ — только тем контейнерам, у которых содержимое реально влезло по
   ширине (тот же приём, что у карточек вместо таблиц и у подсказки «листается вбок»). Широкой
   таблице клип означал бы отрезанные колонки, поэтому ей остаётся прокрутка вбок — и вместе с ней
   коробка со своей шапкой, как было. Обещание человеку в обоих случаях одно: названия колонок
   видно, пока листаешь список. Сломается JS — всё вернётся к коробке, то есть к рабочему поведению,
   а не к поломке. */
.card.fits-x,.tablewrap.fits-x{overflow-x:clip;overflow-y:visible}
/* Селектор ВЕСОМ выше базового `.tablewrap:has(thead th)` (0-1-2), иначе max-height не снялся бы. */
.tablewrap.fits-x:has(thead th){max-height:none}

/* --- телеметрия аппа (широкая таблица активных устройств скроллится на мобилке) --- */
.telemetry .tablewrap{-webkit-overflow-scrolling:touch}
.telemetry .tablewrap table{min-width:760px}
/* «Куда смотреть» (24-09, слово владельца «куда смотреть?»): три коротких пункта под плитками. Кегль и
   интерлиньяж — как у пояснений карточек (.explain p), чтобы блок читался текстом, а не списком ссылок. */
.kuda-smotret{margin:0 0 6px;padding-left:20px;font-size:13.5px;line-height:1.55}
.kuda-smotret li{margin-bottom:8px}
.downloads .tablewrap{-webkit-overflow-scrolling:touch}
.downloads .tablewrap table{min-width:640px}

/* --- запасной канал линии (SPEC-0039): строка = линия + форма «адрес+токен», таблица широкая,
       поэтому на узком экране скроллится ВНУТРИ карточки, а не растягивает страницу --- */
.fbsec .tablewrap{-webkit-overflow-scrolling:touch}
.fbsec .tablewrap table{min-width:720px}
.fbsec .filterbar{margin:0}
.fbsec .filterbar>label{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--muted)}
.fbsec .inline{display:inline-block;margin:6px 6px 0 0;vertical-align:middle}

/* --- мониторинг нод (графики uPlot) --- */
.metrics-bar{display:flex;justify-content:space-between;align-items:center;
  gap:16px;flex-wrap:wrap;margin:6px 0 16px}
.rangesw{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
.rangesw button{background:var(--panel2);color:var(--muted);border:0;
  padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer}
.rangesw button+button{border-left:1px solid var(--line)}
.rangesw button:hover{color:var(--txt)}
.rangesw button.active{background:var(--primary);color:var(--primary-ink)}
.chart{margin:8px 0 18px}
.chart .u-title{color:var(--muted);font-size:13px;font-weight:600}
/* uPlot легенда/значения под тёмную тему */
.uplot,.uplot *{color:var(--txt)}
.uplot .u-legend{color:var(--muted);font-size:12px}
/* спарклайны на дашборде/в списке нод */
.spark{display:inline-block;vertical-align:middle}
.bw-now{font-variant-numeric:tabular-nums;color:var(--muted);font-size:12px}
td a.nodelink{color:var(--accent)}

/* --- аптайм / простои нод (SPEC-0004) --- */
.uptime-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px;margin:4px 0 18px}
.uptime-grid .tile .num{font-size:26px}
.timeline-wrap{margin:0 0 8px}
.timeline{display:block;width:100%;border-radius:6px;border:1px solid var(--line)}
/* Флаг рядом с названием страны — картинка из /static/flags (29-08): эмодзи-флаг не рисуется на
   Windows, поэтому во всей панели флаг только такой. Класса самой картинке не даём — правило по
   селектору, чтобы у карточки ноды и у отчёта подготовки не завелась своя копия оформления (у
   списка нод она в static/css/nodes.css, .nflag, и своя ей нужна: там флаг живёт во flex-строке).
   inline-flex + nowrap — не украшение: на 390 px заголовок переносится, и без них картинка
   оставалась в конце первой строки, а слово «Нидерланды» уезжало на вторую — флаг оказывался
   подписан именем НОДЫ (проверено глазами 29-08). */
.flag-lbl{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;
  font-size:13px;color:var(--muted);margin-left:6px;font-weight:500}
.flag-lbl img{border-radius:2px;flex:0 0 auto}

/* --- денежный раздел (SPEC-0040): тарифы/платежи/рельсы --- */
.billing .tablewrap table{min-width:820px}
.billing table input[type=text]{width:100%;min-width:130px}
.billing table input[type=number]{width:88px}
/* `nowrap` тут значил «кнопки не рвутся на две строки», но заодно запрещал перенос ЛЮБОМУ тексту
   внутри ячейки — и объяснение «нет секрета у ядра: задайте его в окружении client-api и
   перезапустите сервис» вытягивалось в одну строку на 880 px. Из-за него таблица «Рельсы» на
   десктопе 1440 занимала 1742 px при карточке 1082 (замер 02-08): 660 px, то есть 38 %, были за
   краем, и обрезалась ровно колонка «Действия» — та, на которую надо нажимать. Не даём рваться
   кнопкам, а не абзацам. */
.billing .rowacts{white-space:normal}
.billing .rowacts .btn,.billing .rowacts button{white-space:nowrap}
.billing .rowacts .muted,.billing .rowacts .danger{display:block;max-width:32ch;margin-top:6px;line-height:1.4}
.billing .inline{display:inline-block;margin:0 6px 4px 0;vertical-align:middle}
.billing .filterbar{display:flex;gap:14px;align-items:flex-end;flex-wrap:wrap;margin:10px 0 4px}
.billing .filterbar>label{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--muted)}
.billing .filterbar label.chk{flex-direction:row;align-items:center;gap:8px;padding-bottom:9px}
.billing .tile .num{font-size:26px}
.billing .danger{font-size:12px;margin-top:4px}

/* --- карточка ноды: подзаголовок, панель действий, чекбоксы в ряд, «нет данных» в плитке --- */
/* Строка-описание под заголовком страницы (ресёрч по юзабилити 02-08, §4 и §8 — приём page-header
   c `description` из Remnawave). Голый <h1> не отличал «Наборы» от «Направлений», а «Добавить ноду»
   визарда — от «Добавить ноду» заказа VPS: одинаковые имена в меню, одинаковые заголовки на странице.
   Ширину держим в ~95 знаках, но в rem, а НЕ в ch: единица ch — ширина «0» ТЕКУЩЕГО шрифта, а
   Onest грузится со `swap`, поэтому до его подмены строка меряется системным фолбэком и после
   пересчёта переносится иначе — всё ниже прыгает. Замер 02-08 на /nodes: CLS 0.0424, сдвиг −19 px
   (ровно строка) у .nodes-wrap и подвала. 95ch у Onest 13px ≈ 45rem. */
.pagesub{margin:-14px 0 18px;font-size:13px;line-height:1.5;max-width:45rem}
.node-toolbar{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin:0 0 18px}
.chk{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--txt);cursor:pointer}
.chk input{margin:0}
.chk-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 4px}
.tile .num .nodata{font-size:14px;font-weight:500;color:var(--muted)}
.assign-form{margin-top:10px}

/* --- список направлений: строка-форма --- */
.dirs .tablewrap table{min-width:940px}
/* 220 и 210 были подобраны под экран 1440, и на ноутбучном 1366 таблица направлений вылезала за
   карточку на 11 px — ровно настолько, чтобы спрятать край колонки действий. 190 хватает обеим
   (замер 02-08: 1366 → 0 за краем, 1280 → 97 вместо 47; шире 1440 ничего не меняется). */
.dirs .cell-name{min-width:190px}
.dirs .cell-name input{width:100%;margin-bottom:4px}
.dirs .cell-name input:last-child{margin-bottom:0}
.dirs .cell-flag{width:5em;text-transform:uppercase}
.dirs .cell-num{width:5em}
.dirs .rowacts{white-space:normal;min-width:190px}
.dirs .inline{display:inline-block;margin:0 4px 4px 0}
/* Формы-кнопки в ячейке таблицы — В СТРОКУ. `.inline` до сих пор объявляли отдельно под каждую
   страницу (.alerts/.billing/.dirs/.fbsec), и там, где забыли — на «Сплит-пресетах» — «Выключить»
   и «Удалить» вставали друг под друга и удваивали высоту строки: 210 строк давали 21 480 px на
   десктопе и 51 308 px на телефоне (замер 05-08). Правило теперь общее — по месту (форма прямо в
   ячейке), а не по названию страницы, поэтому следующая таблица получит его даром. */
td > form.inline{display:inline-block;margin:0 6px 4px 0;vertical-align:middle}
.dirs .rowacts .btn{margin:0 4px 4px 0}
/* 🔴 Кнопка ВНУТРИ формы не должна нести собственный нижний отступ — его уже несёт сама форма
   (`.dirs .inline{margin:0 4px 4px 0}` строкой выше). Двойной отступ ничего не сдвигал вбок, но
   делал КОРОБКУ формы 32 px при кнопке 28 (замер 25-08: <form> h=32, кнопка внутри h=28), а
   `td > form.inline{vertical-align:middle}` центрирует именно коробку. Итог виден глазами в каждой
   строке: «Включить»/«Выключить» стояло на 4 px ВЫШЕ соседних «Открыть», «Править» и «Удалить»
   (top 317 против 321, и так во всех четырёх направлениях).
   Почему не лечили выравниванием: `vertical-align` тут ни при чём — при РАЗНОЙ высоте коробок
   любое общее значение (baseline/middle/top) даёт свой перекос. Лечится равенство высот. */
.dirs .rowacts form.inline > .btn{margin-bottom:0}
/* Второй половиной той же правки — ОДИН способ выравнивания на всю ячейку. Общее правило
   `td > form.inline` ставит формам `vertical-align:middle`, а соседние ссылки-кнопки стоят по
   базовой линии: пока рядом стоят два РАЗНЫХ способа, коробки сходятся только случайно (после
   уравнивания высот оставалось ещё 2–3 px — middle считает по МАРГИН-коробке, а у формы есть свой
   нижний отступ 4 px на перенос). Ставим форму на базовую линию, как её соседей: у inline-block
   базовая линия — это базовая линия кнопки внутри, то есть ровно то же, по чему стоят ссылки. */
.dirs .rowacts form.inline{vertical-align:baseline}
.btn.sm{padding:5px 11px;font-size:13px}

/* --- «Почему может не работать» (карточка человека, reason.go, 03-08) ---
   Список причин: цвет левой полосы + текста несёт вывод (problem/warn/ok/info), не дублируя цвет
   badge'ей статусов ниже по карточке — те же переменные (--ok/--warn/--bad-ink/--muted), другая форма
   (список с полосой, а не плашка), чтобы визуально не путались с .badge/.alert. */
.reasons{list-style:none;margin:0 0 10px;padding:0;display:flex;flex-direction:column;gap:8px}
.reasons li{padding:8px 12px;border-radius:8px;border-left:3px solid var(--muted);
  background:var(--panel2);font-size:14px;line-height:1.5}
.reason-problem{border-left-color:var(--bad-ink)!important;color:var(--bad-ink)}
.reason-warn{border-left-color:var(--warn)!important;color:var(--warn)}
.reason-ok{border-left-color:var(--ok)!important;color:var(--ok)}
.reason-info{border-left-color:var(--muted)!important;color:var(--txt)}
.reason-device{margin-top:14px}
.reason-device>b{display:block;margin-bottom:6px;font-size:13px;color:var(--muted)}

/* --- сворачиваемое пояснение к таблице («Как читать эту таблицу») --- */
.explain{margin:6px 0 14px;font-size:13px}
.explain summary{cursor:pointer;color:var(--muted);font-weight:600;list-style:none}
.explain summary::-webkit-details-marker{display:none}
.explain summary::before{content:"▸ ";color:var(--accent)}
.explain[open] summary::before{content:"▾ "}
.explain p{margin:8px 0 0;line-height:1.55}

/* --- Свёрнутый РАЗДЕЛ карточки (`details.foldable`, 30-08) ---
   Повод — замер: карточка человека на 1440 выросла до 5885 px, владелец дословно «читать
   невозможно». Свёрнуты только РЕДКИЕ разделы; частые (действия, профиль, устройства, конфиги,
   диагностика, деньги) остались открытыми — там свёртка добавила бы клик к каждому заходу.

   Отличается от `.explain` НАЗНАЧЕНИЕМ, поэтому и класс отдельный: `.explain` — мелкая серая
   приписка «как читать эту таблицу» ВНУТРИ раздела, а здесь свёрнут сам раздел, и его подпись
   обязана выглядеть ровно как заголовок открытого соседа. Иначе полстраницы читается как набор
   сносок: заголовки то 16 px белые, то 13 px серые.

   Заголовок лежит ВНУТРИ <summary> (`<summary><h2>…`) — так по спецификации можно (summary
   принимает один элемент заголовка), и это единственный способ сделать кликабельной всю строку
   названия, не изобретая кнопку. Ссылки внутрь summary НЕ кладём: вложенный в раскрывашку
   интерактив читалка обходит стороной, а мышью он даёт два действия на один клик — поэтому
   «вся его ветка →» и её родня переехали в тело блока (`.foldlink`). */
details.foldable{margin:0}
details.foldable>summary{cursor:pointer;list-style:none}
details.foldable>summary::-webkit-details-marker{display:none}
/* Маркер — тот же треугольник, что у `.explain` и у журнала тревог: один язык на всю панель.
   Висит на ЗАГОЛОВКЕ, а не на самой раскрывашке, и это не мелочь: <summary> у нас flex с
   выравниванием по центру, и маркер-флекс-элемент на телефоне вставал посередине двухстрочного
   названия — отдельно от первой буквы, к которой относится (замер 390 px). Внутри заголовка он
   обычный inline-текст и всегда стоит перед первым словом, сколько бы строк ни заняло название. */
details.foldable>summary>h2{margin:16px 0}
details.foldable>summary>h2::before{content:"▸ ";color:var(--accent);font-weight:400}
details.foldable[open]>summary>h2::before{content:"▾ "}
details.foldable>summary:hover>h2{color:var(--link)}
/* Ссылка «весь список →», выселенная из заголовка. На телефоне она обязана оставаться целью под
   палец — ровно то же требование, что у `.card h2 a.seeall` (замер 25-08: 21 px при норме 44). */
.foldlink{margin:0 0 10px;font-size:13px}
@media(max-width:700px){ .foldlink a{display:inline-flex;align-items:center;min-height:44px} }

/* 🔴 ЯКОРЬ ОБЯЗАН РАСКРЫВАТЬ РАЗДЕЛ. Полоса разделов вверху карточки — это ссылки «#gruppa»,
   «#trafik» и прочие, и раздел, у которого свёрнуто тело, по ссылке открыться сам не может:
   `open` — атрибут, а не стиль. Механизма два, и они независимы нарочно.
   1) ЭТО ПРАВИЛО — без единой строки JS. `::details-content` — теневая обёртка тела <details>,
      у закрытого она `content-visibility:hidden`; вернув ей `visible` у раздела под `:target`,
      получаем раскрытие по чистому CSS. Браузер, который псевдоэлемента не знает, правило просто
      выбросит — отсюда и второй механизм.
   2) admin.js — открывает `open` по-настоящему (клик по чипу, hashchange, прямая ссылка с хешем).
   Треугольник переключаем вместе с телом: маркер, спорящий с содержимым, хуже отсутствующего. */
.card:target>details.foldable::details-content{content-visibility:visible}
.card:target>details.foldable>summary>h2::before{content:"▾ "}
/* .card сам прокручивает — обёртка не нужна, важно лишь не дать таблице сжаться в кашу. */
/* Было min-width:1180px — БОЛЬШЕ, чем карточка на 1440 (1122 px): таблица гарантированно не влезала
   и прятала две последние колонки на любом экране. Ширину теперь задаёт содержимое. */
.issuances table{min-width:0}

/* .wrapcell — ячейке РАЗРЕШЕНО рвать длинное слово. Нужно там, где одна колонка (почта, URL,
   модель устройства) работает распоркой и выталкивает последние колонки за край карточки: на
   десктопе 1440 это «Выдачи» 1233 px при 1122, «Диагностика» 1218, телеметрия 1193 (замер 02-08).
   ⚠️ Почему НЕ на все `td` разом: `overflow-wrap:anywhere` меняет min-content ячейки на один
   символ, и раскладка таблицы начинает жать ВСЁ подряд — проверено на проде поверх живой страницы:
   «Выдачи» стали читаться как «активе/н», «10.40.0./3», «24.07.203/6», а номер выдачи вставал
   столбиком по цифре. Хуже, чем спрятанная колонка. Поэтому — только тем колонкам, которые правда
   длинные. */
.wrapcell{overflow-wrap:anywhere}
/* .ipcell — колонка с АДРЕСОМ. Отдельно от .wrapcell из-за IPv6: он ровно втрое длиннее IPv4
   (39 знаков против 15) и в моноширинном шрифте занимает ~330 px, то есть в одиночку выталкивает
   таблицу за край окна. Замер на стенде 24-08 (окно 1280): «Аудит» — правый край ячейки 1297 px,
   «Диагностика» — 1396 px. Страница при этом не ломается (обе таблицы лежат в .tablewrap со своей
   прокруткой), но колонка с адресом оказывается ЗА КРАЕМ и читается только боковой прокруткой —
   то есть по факту её нет.
   min-width:15ch — прямой ответ на грабли, описанные у .wrapcell выше: `overflow-wrap:anywhere`
   делает min-content ячейки в один символ, и раскладка таблицы может ужать колонку в ленту по
   цифре в строку. 15 знаков — ровно ширина IPv4, ниже опускаться незачем: IPv4 не переносится
   никогда, IPv6 ложится в две-три строки вместо одной длинной.
   max-width:22ch — без него разрешение переносить НИЧЕГО не меняет: раскладка таблицы `auto`
   раздаёт ширину пропорционально max-content, и колонка с адресом просто забирает свои 39 знаков.
   Замер на стенде: /audit — «Что произошло» 160 px, а «IP» 384 px, то есть адрес был самой широкой
   колонкой страницы. Потолок в 22 знака заставляет длинный адрес лечь в две строки.
   ⚠️ + 20px к обоим (25-09): у панели `*{box-sizing:border-box}`, и пол с потолком ячейки
   раскладка меряет ВМЕСТЕ с полями td (10 px с каждой стороны). Голые 15ch давали тексту ~12,5
   знака — «10.33.117.254» рвалось (замер стенда на /online, там колонку увели в nowrap), а
   потолок в 22ch пускал IPv6 в три строки вместо двух. Поля прибавлены явно, а не спрятаны в ch. */
.ipcell{overflow-wrap:anywhere;min-width:calc(15ch + 20px);max-width:calc(22ch + 20px)}
/* .crashcell — «Что упало» на странице «Падения» (23-09). Тот же приём, что у .ipcell, с другой
   стороны: колонка несёт класс исключения и верхний кадр, а это 30–60 знаков моноширинным шрифтом.
   С одним .wrapcell раскладка ужимала её до min-content — замер на бою 23-09 при окне 1280: колонка
   ~200 px, «java.lang.RuntimeExceptio/n» рвалось посреди слова, а «Модели» забирали втрое больше.
   min-width:34ch — длина обычного класса исключения, рвутся теперь только кадры длиннее этого.
   25-09 → 29ch. `ch` берётся от шрифта ЯЧЕЙКИ (Onest 15 px, 10 px на знак), пол раскладка меряет
   вместе с полями ячейки, а класс исключения набран моноширинным 13 px (~7.8 px): 34ch давали
   320 px текста — 41 моноширинный знак, заметно больше задуманного. Когда «Версии» и «Модели»
   перестали рваться посреди слова (они держат по слову), таблица стала 964 px при карточке 934 и
   уехала вбок. 29ch — 270 px текста, 34 моноширинных знака: «java.lang.IllegalStateException»
   (31) и «…IllegalArgumentException» (34) по-прежнему в строку, таблица снова влезает. */
.crashcell{overflow-wrap:anywhere;min-width:29ch}
/* .modelcell — «Модели» там же. Модель переносится по «_» (шаблон ставит <wbr>), пробелам и
   дефисам, но её ширину задаёт ПОЛ, а не самое длинное слово в данных: без `anywhere` имя
   вроде «23078PND5G» распирало бы таблицу, которой при 1280 и так тесно (замер 25-09: наименьшая
   ширина таблицы — 920 из 934). 10.3ch — наименьший пол, при котором кусок «gphone64_» (эмулятор,
   он и стоит на бою) не рвётся и в самой узкой раскладке: при 10ch «_» отрывался на следующую строку. */
.modelcell{overflow-wrap:anywhere;min-width:10.3ch}
/* .bouncecell — «Адрес» и «Отказ сервера» на странице «Вернувшиеся письма» (SPEC-0061). Тот же приём,
   что у .crashcell, и сразу на ОБЕ длинные колонки: замер на стенде 24-09 при окне 1280 — с одним
   .wrapcell раскладка ужимала адрес до ~80 px («user5-novyy@exam/ple.com» рвалось посреди слова), а
   с минимумом только у адреса точно так же в ленту по слову сжимался ответ чужого сервера. 22 знака
   у обеих — обычный адрес и первая фраза ответа; длиннее переносится. Больше нельзя: при 24 знаках
   таблица из шести колонок вылезала за край окна 1280 на 6 px (940 при 934, замер тот же). На телефоне адрес становится
   шапкой карточки, и там правило якоря (min-width:0) сильнее этого. */
.bouncecell{overflow-wrap:anywhere;min-width:22ch}
/* Телеметрия: колонки помечены позиционно — 1 «Аккаунт» (номер + почта), 6 «Источник». Порядок
   колонок в шаблоне держит этот номер (там же оговорка). Промах по номеру безвреден: колонке просто
   разрешат перенос. */
.telemetry td:nth-child(1),.telemetry td:nth-child(6){overflow-wrap:anywhere}
.assign-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:0}
.assign-row select{min-width:220px;width:auto}
.users .filterbar{align-items:flex-end}
.users .filterbar>label{display:flex;flex-direction:column;gap:6px;font-size:12.5px;font-weight:600;color:var(--muted)}
.users .filterbar select{min-width:230px;width:auto}
.users .filterbar input[type=search]{min-width:240px;width:auto}

/* --- Массовые операции над списком пользователей (шаблон users.html + admin.js) ---
   Панель приглушена, пока ничего не отмечено: набор кнопок «Продлить/Заблокировать/Удалить» под
   списком не должен читаться как действие над ВСЕМ списком. Колонка галочек узкая и не кликается
   мимо: клик по ячейке (а не строго по квадратику) — самый частый промах в таких таблицах. */
.selcol{width:34px;padding-right:0;text-align:center;cursor:pointer}
.selcol input[type=checkbox]{width:16px;height:16px;vertical-align:middle}
/* Схлопнута, пока не отмечено ничего (находка владельца + агента по /users, 03-08): раньше бар был
   ВСЕГДА в полный рост — шесть полей и пять кнопок занимали больше места, чем сама таблица из 4-5
   строк, хотя все были disabled. Выключенные поля/кнопки прячем (`display:none`), остаётся только
   счётчик «Отмечено: 0» и подсказка-приглашение — компактной строкой.
   ⚠️ Сознательно БЕЗ анимации высоты (max-height): число полей у бара переменное — до девяти строк
   в столбик на телефоне, если разрешены и биллинг, и запись, и удаление (у CanBilling/CanWrite/
   CanDeleteUser своя комбинация прав). Угадать общий безопасный max-height нельзя, не обрезав кому-то
   реальный контент, а измерить `scrollHeight` в рантайме — уже не «просто CSS». Дискретный показ/
   прятанье через `display:none` — раскладку это МЕНЯЕТ, но не «дёргает»: сдвиг случается СИНХРОННО
   с кликом человека по чекбоксу, а такие сдвиги Web Vitals CLS не засчитывает (recent-input exclusion,
   500мс). Padding из transition убран 08-08: переход по нему — анимация раскладки (каждый кадр
   пересчитывает и двигает таблицу под баром), а скачок на 3px, синхронный с кликом, глазу не
   виден — те же основания, что у display выше.
   🔴 «ПРИГЛУШЁН» ПОКАЗЫВАЕТ РАМКА, А НЕ ПРОЗРАЧНОСТЬ (29-08, замер вслед за п.16 ревью). Здесь
   стояло `opacity:.5` на ВСЁМ баре — ровно та же ошибка, которую в этот же вечер убрали у
   отключённых полей, и по той же причине: прозрачность не разбирает, что она гасит, а гасит она и
   рамку, и фон, и БУКВЫ. Замер в браузере на /users: «Отмечено:» давало 2.43, счётчик — 3.84 при
   пороге AA 4.5, и точно так же гасла подсказка-приглашение в ::after — то есть хуже всего читалась
   ровно та строка, которая ЗОВЁТ человека воспользоваться массовыми действиями. Приглушённое
   состояние теперь показывает пунктирная рамка (как у полей, которые нельзя трогать), а буквы
   остаются штатным --muted: 6.15 на этом фоне. Анимируем border-color — она так же безопасна для
   любой высоты, как была opacity. */
.bulkbar{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;margin:14px 0 0;padding:12px 14px;
  border:1px solid var(--line);border-radius:10px;background:var(--panel2);
  transition:border-color .12s ease}
.bulkbar:not(.on){border-style:dashed}
.bulkbar.on{border-color:rgba(34,184,207,.45)}
.bulkbar:not(.on){padding-top:9px;padding-bottom:9px}
.bulkbar:not(.on)>*:not(.bulkhead){display:none}
.bulkbar:not(.on) .bulkhead::after{content:" — отметьте строки в таблице, чтобы обработать сразу несколько";
  color:var(--muted);font-weight:400}
/* Пояснение под баром — про поля, которых в схлопнутом виде не видно; прячем вместе с ними. */
.bulkbar:not(.on)+.bulkhint{display:none}
.bulkbar .bulkhead{font-size:13px;color:var(--muted);align-self:center;margin-right:4px}
.bulkbar .bulkhead b{color:var(--txt)}
.bulkbar .bulkfield{display:flex;flex-direction:column;gap:6px;font-size:12.5px;font-weight:600;color:var(--muted)}
.bulkbar .bulkfield input[type=number]{width:88px}
.bulkbar .bulkfield select{min-width:200px;width:auto}
/* Поле «впишите число отмеченных» уже своё: 6em из разметки (было атрибутом style до 20-08).
   Общая ширина полей бар'а (88px строкой выше) весит столько же, сколько простой класс, и
   перебила бы его — поэтому селектор здесь ровно на класс специфичнее, а не !important. */
.bulkbar .bulkfield input[type=number].confirm-count{width:6em}
.bulkbar .bulkfield .unit{display:none}
.bulkhint{margin:8px 0 0;font-size:13px;line-height:1.5}
@media(max-width:700px){
  .bulkbar{gap:8px}
  .bulkbar .bulkfield,.bulkbar .bulkfield select,.bulkbar .btn{width:100%}
  /* На телефоне подсказка-приглашение из ::after не влезает рядом со счётчиком в 390px — переносим
     под него, а не рвём посреди слова.
     И говорим ПРО ТО, ЧТО НА ЭКРАНЕ: на 390 px таблицы нет — строки развёрнуты в карточки, а
     галочка стоит в правом верхнем углу карточки. Подпись «отметьте строки в таблице» звала туда,
     чего человек не видит (осмотр глазами 04-09). */
  .bulkbar:not(.on) .bulkhead{display:block}
  .bulkbar:not(.on) .bulkhead::after{display:block;margin-top:3px;
    content:" — отметьте карточки галочкой справа, чтобы обработать сразу несколько"}
}

/* --- Журнал аудита: фильтры, «было → стало» --- */
.nowrap{white-space:nowrap}
.audit .filterbar{align-items:flex-end}
.audit .filterbar>label{display:flex;flex-direction:column;gap:6px;font-size:12.5px;font-weight:600;color:var(--muted)}
.audit .filterbar select{min-width:200px;width:auto}
.audit .filterbar input[type=search],.audit .filterbar input[type=text]{min-width:150px;width:auto}
.audit .filterbar input[type=date]{width:auto}
.audit table{min-width:1000px}
/* Diff читается как «поле: было → стало»; старое значение приглушено, новое — акцентом, иначе
   в списке из десятка пар глаз не цепляется за то, что изменилось. */
.audit .diff{list-style:none;margin:6px 0 0;padding:0;font-size:12.5px;line-height:1.6}
.audit .diff li{color:var(--muted)}
.audit .diff .f{color:var(--txt);font-weight:600}
.audit .diff .was{text-decoration:line-through;opacity:.75}
.audit .diff .now{color:var(--accent)}

/* --- «Онлайн сейчас»: плитки-фильтры по направлениям --- */
.tile-link{text-decoration:none;color:inherit}
/* В новых плитках подпись и число — инлайновые span'ы (в отличие от див-разметки «Обзора»),
   без этого они слипаются в «Нидерланды5» / «Всего за период126 КБ». */
.tile .tile-body>span{display:block}
.tile-link:hover{text-decoration:none;border-color:rgba(34,184,207,.45)}
.tile-link.on{border-color:var(--accent);background:rgba(34,184,207,.07)}
.online .tile .num{font-size:22px}
.online table{min-width:900px}
.audit .dates{display:flex;gap:8px;align-items:center}
/* Метка «есть заметка» в списке юзеров: не сам текст (он бывает длинным), а только сигнал. */
.notemark{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:999px;font-size:11px;
  color:var(--muted);border:1px solid var(--line);vertical-align:middle}


/* ─────────────────────────────────────────────────────────────────────────────────────────────
   Правки по аудиту админки 2026-07-31 (docs/AUDIT-admin-2026-07-31.md)
   ───────────────────────────────────────────────────────────────────────────────────────────── */

/* A8. Ядро в списке нод. Метка должна читаться как «эту машину не трогать», а не как ещё один
   серый бейдж роли: цвет предупреждения, а не опасности — само по себе это не авария. */
.badge-core{background:rgba(210,153,34,.16);color:#e3b341;border:1px solid rgba(210,153,34,.45);
  letter-spacing:.04em}
.corebox{border:1px solid rgba(210,153,34,.45)}
.corebox h2{color:#e3b341}

/* A7. Страница отказа. Код — крупной приглушённой цифрой слева от заголовка: он нужен для
   переписки и логов, но читать первым должны словами. */
.errpage{max-width:640px}
.errhead{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin:8px 0 4px}
.errcode{font-family:'Fira Mono',ui-monospace,monospace;font-size:34px;font-weight:700;color:var(--muted);opacity:.6}
.errpage h1{margin:0;font-size:22px}
.errmsg{font-size:15px;margin:6px 0 10px}
.erracts{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}

/* C2. Мобильное меню закрывается. Подложка перекрывает контент и ловит тап мимо панели; крестик
   в шапке меню виден всегда (сама кнопка ☰ остаётся под панелью — её не вернуть). */
.side-close{display:none;margin-left:auto;background:none;border:0;color:var(--muted);
  font-size:20px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:6px}
.side-close:hover{color:var(--txt);background:rgba(255,255,255,.06)}
.side-backdrop{display:none}
@media(max-width:900px){
  /* Крестик закрытия меню — единственная кнопка выхода из открытого сайдбара, а был 24 px
     (padding 2px 6px вокруг символа). Промах по нему оставляет меню открытым поверх страницы. */
  .side-close{display:flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;padding:0}
  .side-backdrop{display:block;position:fixed;inset:0;z-index:19;background:rgba(0,0,0,.5)}
  .side-backdrop[hidden]{display:none}
}

/* A12. Таблицы на телефоне. Прокрутка вбок была, но о ней ничто не сообщало: на 390px из семи
   колонок видна одна, и экран выглядит как сломанный. Список пользователей разворачиваем в
   карточки (тот же приём, что уже работает на «Алертах»: подписи берутся из data-l у ячеек). */
@media(max-width:700px){
  /* `overflow`, а не `overflow-x`: у обёртки теперь обе оси `auto` (см. .tablewrap), и правка
     одной оси оставила бы вторую прокручиваемой — а по спецификации это возвращает и первую. */
  .users .tablewrap{overflow:visible}
  .users table{min-width:0}
  .users thead{display:none}
  .users tbody,.users tbody tr,.users tbody td{display:block;width:100%}
  .users tbody tr{border:1px solid var(--card-bd);border-radius:8px;padding:8px 12px 12px;margin-bottom:12px;
    position:relative}
  .users tbody td{border:0;padding:6px 0;display:flex;justify-content:space-between;gap:12px;align-items:baseline}
  .users tbody td::before{content:attr(data-l);flex:0 0 auto;font-size:12px;font-weight:700;
    letter-spacing:.02em;text-transform:uppercase;color:var(--muted)}
  /* Заголовок карточки — НОМЕР АККАУНТА: без подписи и на всю ширину (решение владельца 11-08,
     якорь человека — номер, а не почта). Раньше заголовком стояла почта; после перестановки колонок
     номер получил подпись «АККАУНТ» и уехал вправо, под абсолютно позиционированную галочку —
     метка «заметка» рядом с ним обрезалась. Почта теперь обычная строка с подписью, как остальные. */
  .users tbody td[data-l="Аккаунт"]{display:block;font-size:15px;font-weight:600;padding-top:0}
  .users tbody td[data-l="Аккаунт"]::before{content:none}
  /* Проход панели с телефона 03-08: галочка «отметить» в карточке пользователя мерилась Playwright'ом
     как 18×18 — сама галочка не выросла бы до нормы (44 px) без того, чтобы визуально раздуться до
     нелепого квадрата. Вместо этого целью нажатия становится вся ЯЧЕЙКА (44×44, флекс-центровка
     держит галочку по центру), а клик мимо самой галочки, но внутри ячейки, ловит admin.js — тот же
     приём, что desktop-комментарий у .selcol называл целью, но не реализовывал («клик по ячейке, а не
     строго по квадратику»). top/right сдвинуты компенсировать выросшую ячейку, угол карточки тот же. */
  .users tbody td.selcol{position:absolute;right:2px;top:0;width:44px;height:44px;padding:0;
    display:flex;align-items:center;justify-content:center;cursor:pointer}
  .users tbody td.selcol::before{content:none}
  /* Галочка лежит АБСОЛЮТОМ в правом верхнем углу карточки — значит верхняя строка (номер слева,
     почта справа) обязана оставить ей место. Пока свёртка её прятала, места хватало; как только
     галочку вернули (04-09), длинная почта поехала ПОД неё, и палец, метясь в почту, отмечал
     строку. 44 px ячейки + 4 px воздуха. */
  .users tbody tr:has(td.selcol)>td[data-sub]{padding-right:48px}
  .users tbody td.empty{display:block;text-align:center}
  .users tbody td.empty::before{content:none}
}

/* A12, вторая половина (2026-08-01). Приём выше работал ровно на двух страницах, а замер на проде
   при 390px показал, что широких таблиц полтора десятка: «Выдачи» 1220px в окне 388 (32 % ширины),
   телеметрия 1190, журнал аудита 1000, «Онлайн» 900, направления 1061, рельсы оплаты 1736 (20 %).
   Подсказка «листается вбок» честная, но читать журнал в щель шириной в одну колонку нельзя.
   Поэтому: КАЖДАЯ таблица, которая на телефоне не влезает в свой контейнер, разворачивается в
   карточки. Класс `cards` вешает admin.js — только тем таблицам, которым он реально нужен (мерит
   ширину), поэтому узкие таблицы остаются таблицами, а страницы с ручной мобильной вёрсткой
   (`.users`, `.alerts`, `.domains`) под правило не попадают: они уже влезают.
   min-width перебиваем ДВУМЯ селекторами — частные правила выше живут и как `.audit table`
   (одинаковая специфичность, побеждает порядок), и как `.telemetry .tablewrap table` (специфичнее). */
@media(max-width:700px){
  /* Снимаем ИСКУССТВЕННЫЕ min-width, которыми таблицы держали ширину ради десктопа: на телефоне они
     врали замеру — двухколоночная «Версии приложения» растягивалась до 760px и уезжала в карточки
     наравне с двенадцатиколоночными «Выдачами». Пусть решает настоящая ширина содержимого. */
  .telemetry .tablewrap table,.downloads .tablewrap table,.fbsec .tablewrap table,
  .billing .tablewrap table,.dirs .tablewrap table,.issuances table,.audit table,
  .online table{min-width:0}
  /* display:block — не украшение: пока элемент остаётся таблицей, он не может стать уже своего
     min-content, и одна неразрывная строка (email, overlay-IP) распирала карточку за край экрана. */
  table.cards,.tablewrap table.cards{display:block;min-width:0;width:100%}
  .tablewrap:has(table.cards){overflow:visible}
  /* 🔴 На телефоне коробки с ограниченной высотой НЕТ ВОВСЕ — и это решение, а не недоделка.
     Широкая таблица здесь разворачивается в карточки, шапки у неё нет совсем (`thead{display:none}`),
     подпись каждого поля стоит рядом со значением — закреплять нечего. А узкие таблицы, которые
     карточками не становятся, влезают целиком, и колонок у них две-три. Оставить коробку значило бы
     подарить телефону вложенную прокрутку внутри прокрутки — ровно то, за что ругают мобильные
     таблицы (NN/g, Mobile Tables). Правило общее, а не списком исключений (.alerts, .domains,
     .users верстают карточки СВОИМИ правилами и класс `cards` не получают — перечислять их
     поимённо значило бы забыть следующего).
     Селектор ТОТ ЖЕ, что у базового правила: `.tablewrap:has(thead th)` весит 0-1-2, и простой
     `.tablewrap` (0-1-0) его бы не перебил — max-height молча остался бы. */
  .tablewrap:has(thead th){max-height:none}
  table.cards thead{display:none}
  table.cards tbody,table.cards tbody tr,table.cards tbody td{display:block;width:100%}
  table.cards tbody tr{border:1px solid var(--card-bd);border-radius:8px;padding:8px 12px 12px;
    margin-bottom:12px;background:var(--card)}
  /* flex-wrap — чтобы длинное значение (почта, overlay-IP) уходило на свою строку ЦЕЛИКОМ, а не
     рвалось посреди слова рядом с подписью: короткие значения при этом остаются в одну строку. */
  /* text-align НЕ ставим вправо: значение и так уходит вправо флексом (space-between), пока живёт с
     подписью на одной строке. А вот длинное значение переносится на СВОЮ строку, и там правое
     выравнивание давало рваный ЛЕВЫЙ край — так на телефоне выглядели самые важные для чтения тексты:
     «Что произошло» в журнале аудита и «Ошибка» в заявках на ноду (замер 04-08, 390px). Слева
     короткие значения не двигаются (их держит флекс), а абзацы читаются нормально. */
  table.cards tbody td{border:0;padding:6px 0;display:flex;flex-wrap:wrap;justify-content:space-between;
    gap:2px 12px;align-items:baseline;text-align:left;overflow-wrap:anywhere}
  table.cards tbody td::before{content:attr(data-l);flex:0 0 auto;text-align:left;font-size:12px;
    font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--muted)}
  /* Заголовок карточки. Раньше им становилась ПЕРВАЯ ячейка — то есть первая колонка таблицы,
     какой бы она ни была: на «Выдачах» карточку возглавлял внутренний номер строки, на «Платежах» —
     дата. Шапкой обязан быть ЯКОРЬ (номер аккаунта, тема обращения), и назначаться он должен явно,
     атрибутом `data-head`, а не порядком колонок в вёрстке (замер 23-08: перестановка колонок
     молча меняла шапку карточки). Правило ниже остаётся для таблиц, где `data-head` не проставлен:
     их в панели больше половины, и терять у них шапку карточки нельзя. */
  table.cards tbody tr:not(:has(td[data-head])) td:first-child{display:block;text-align:left;font-size:15px;font-weight:600;padding-top:0}
  table.cards tbody tr:not(:has(td[data-head])) td:first-child::before{content:none}
  /* Ячейки с управлением (формы, кнопки, поля) в две колонки не ложатся — подпись сверху,
     содержимое во всю ширину и на палец, а не на пиксель. */
  table.cards tbody td:has(form,button,select,input,textarea,.btn){display:block;text-align:left}
  table.cards tbody td:has(form,button,select,input,textarea,.btn)::before{display:block;margin-bottom:6px}
  table.cards tbody td .btn{width:100%}
  table.cards tbody td input[type=text],table.cards tbody td input[type=number],
  table.cards tbody td select{width:100%}
  /* «Пусто», colspan и прочие ячейки без подписи — обычным блоком. */
  table.cards tbody td:not([data-l]){display:block;text-align:left}
  table.cards tbody td:not([data-l])::before{content:none}
  table.cards tbody td.empty{text-align:center}

  /* Карточка пользователя и лог диагностики сделаны таблицей «ключ-значение» (подпись в <th>) —
     шапки у них нет, поэтому в карточки они не разворачиваются и не должны. Но на 390px они
     вылезали на 10px, и обрезалось именно значение: почта показывалась как «…@gmail.c», тариф —
     без лимита устройств. Отступ подписи ужимаем, длинные значения переносим. */
  table.kv{width:100%}
  table.kv th{padding-right:12px}
  table.kv td{overflow-wrap:anywhere}
}

/* ─────────────────────────────────────────────────────────────────────────────────────────────
   ЯКОРЬ + ДВА ПОЛЯ + «Подробнее» — карточка журнала на телефоне (замер 23-08).

   Что мерили. На 390 px журналы уходили в бесконечный свиток, потому что в мобильную карточку
   выкладывались ВСЕ столбцы подряд: /issuances 24 510 px (29 экранов), /payments 24 042,
   /diag-logs 21 803, /connect-log 17 543, /alerts/history 17 152, /users 17 618. Одна строка
   стоила 350–440 px — то есть пол-экрана на запись, которую пролистывают не читая.

   Что сделано. Карточка показывает ровно три вещи: ЯКОРЬ (`data-head` — номер аккаунта ссылкой,
   тема обращения), приписку справа мелким (`data-sub` — обычно время) и РОВНО ДВА поля, ради
   которых журнал и открывают (`data-key`): «что случилось» и «где». Всё остальное свёрнуто и
   раскрывается кнопкой «Подробнее» (её ставит admin.js, цель нажатия 44 px).

   Почему не «показать первые N ячеек». Полезные два поля у каждого журнала СВОИ (у платежей —
   сумма и статус, у журнала подключений — ступень и время), и порядок колонок в таблице задан
   десктопом. Разметка обязана называть их явно, иначе перестановка колонок молча меняет то, что
   видно на телефоне.

   Свёртка живёт ТОЛЬКО у строк с `data-head`: остальные таблицы панели (их больше половины)
   ведут себя как раньше. Без JS свёртки нет вовсе — как нет и самих карточек (класс `cards`
   вешает admin.js), то есть спрятать содержимое от человека без скриптов невозможно.
   ───────────────────────────────────────────────────────────────────────────────────────────── */
@media(max-width:700px){
  /* Строка-карточка — флекс: якорь и приписка встают на ОДНУ строку, остальные поля идут своими.
     Селекторы намеренно длинные и ОДИНАКОВОЙ формы: правило «каждое поле во всю ширину» и правила
     якоря/приписки обязаны иметь равную вес, иначе первое перебивает вторые по специфичности и
     якорь съезжает на свою строку — карточка становится на 24 px выше без всякой пользы
     (поймано замером 23-08, а не глазами: на вид разница неотличима от межстрочного интервала).
     ⚠️ `width:auto` у якоря и приписки — не украшение: выше у ячеек стоит `width:100%`, а у
     флекс-элемента базой становится именно `width`, то есть каждая ячейка требовала себе всю
     строку и переносилась. Уберёшь — они снова разъедутся по разным строкам. */
  table.cards tbody tr:has(td[data-head])>td,.users tbody tr:has(td[data-head])>td{flex:0 0 100%}
  table.cards tbody tr:has(td[data-head]),.users tbody tr:has(td[data-head]){
    display:flex;flex-wrap:wrap;align-items:baseline}

  /* order:-1 — якорь идёт ПЕРВЫМ в карточке, даже если в таблице его колонка вторая (на «Платежах»
     первая колонка — дата). Порядок в разметке при этом не трогаем: он задан десктопом и читается
     диктором. */
  table.cards tbody tr:has(td[data-head])>td[data-head],.users tbody tr:has(td[data-head])>td[data-head]{
    order:-1;flex:1 1 auto;width:auto;display:block;text-align:left;font-size:17px;font-weight:600;
    padding-top:0;min-width:0}
  table.cards tbody td[data-head]::before,.users tbody td[data-head]::before{content:none}
  /* Вторая строка якоря — почта/логин: она ПОДПИСЬ к номеру, а не второй заголовок. Без этого она
     наследовала 17px/600 от ячейки и спорила с самим номером за внимание (замер 23-08, скриншот). */
  table.cards tbody td[data-head] .sm,.users tbody td[data-head] .sm,
  table.cards tbody td[data-head] .muted,.users tbody td[data-head] .muted{
    font-size:13px;font-weight:400;line-height:1.35}

  /* Приписка — время. Справа от якоря, мелкая и приглушённая: она отвечает на «когда», а не
     соревнуется с якорём за внимание. nowrap, иначе дата ломается посреди числа. */
  table.cards tbody tr:has(td[data-head])>td[data-sub],.users tbody tr:has(td[data-head])>td[data-sub]{
    flex:0 0 auto;width:auto;margin-left:auto;display:block;padding-top:0;text-align:right;
    font-size:12px;color:var(--muted);white-space:nowrap}
  table.cards tbody td[data-sub]::before,.users tbody td[data-sub]::before{content:none}

  /* Два поля «ради чего открывают». Подпись остаётся: без неё бейдж «Польша» рядом с бейджем
     «активен» читается как один набор слов. */
  table.cards tbody td[data-key],.users tbody td[data-key]{padding:4px 0}

  /* Свёрнуто. Разворачивает кнопка (admin.js ставит .open на строку).
     ⚠️ `:not(.selcol)` — не мелочь, а починка (осмотр глазами 04-09). Ячейка с галочкой «отметить»
     не помечена ни `data-head`, ни `data-sub`, ни `data-key`, и свёртка 23-08 её ПРЯТАЛА: на 390 px
     в разметке галочки были, на экране — ни одной. При этом плашка массовых действий внизу
     оставалась видимой и звала «отметьте строки» — то есть панель просила сделать то, чего сделать
     нельзя. Ровно тот класс, что уже ловили: правку (44×44 у `.users td.selcol` выше, делегированный
     клик по ячейке в admin.js) донесли не во все двери, и следующее правило её отменило. */
  table.cards tbody tr:has(td[data-head]):not(.open)>td:not([data-head]):not([data-sub]):not([data-key]):not(.cardmore):not(.selcol),
  .users tbody tr:has(td[data-head]):not(.open)>td:not([data-head]):not([data-sub]):not([data-key]):not(.cardmore):not(.selcol){
    display:none}
  /* Свёрнутая карточка прячет и «вторые строки» ВНУТРИ показанных полей — но только те, что
     помечены явно (`.cardfold`) или являются раскрывашкой <details>. Пробовали по классу `.sm`
     («всё мелкое») — и снесли кнопку «Разобрался» в журнале тревог: она `class="btn sm"`. Мелкий
     размер не значит «подробность»; помечать надо смысл, а не кегль (замер 23-08, скриншот). */
  table.cards tbody tr:has(td[data-head]):not(.open)>td[data-head] details,
  .users tbody tr:has(td[data-head]):not(.open)>td[data-head] details,
  table.cards tbody tr:has(td[data-head]):not(.open) .cardfold,
  .users tbody tr:has(td[data-head]):not(.open) .cardfold{display:none}

  /* Кнопка идёт последней и забирает себе нижний отступ карточки: 44 px цели нажатия плюс ещё
     12 px пустоты — это 56 px на каждую запись, а записей в журнале сотни. */
  table.cards tbody tr:has(td.cardmore),.users tbody tr:has(td.cardmore){padding-bottom:0}
  table.cards tbody tr:has(td[data-head])>td.cardmore,.users tbody tr:has(td[data-head])>td.cardmore{
    order:99;display:block;padding:0}
  .cardmore-btn{display:inline-flex;align-items:center;gap:6px;min-height:44px;padding:0 4px;
    background:none;border:0;color:var(--link);font:inherit;font-size:13px;cursor:pointer}
  .cardmore-btn::after{content:"⌄";font-size:15px;line-height:1;transform:translateY(-1px)}
  .cardmore-btn[aria-expanded="true"]::after{content:"⌃"}
  .cardmore-btn:hover{text-decoration:underline}
}
/* Кнопку «Подробнее» ставит скрипт и только на телефоне, но человек может растянуть окно —
   на десктопе лишней ячейки в таблице быть не должно ни при каких обстоятельствах. */
td.cardmore{display:none}

/* ─────────────────────────────────────────────────────────────────────────────────────────────
   Правки по замеру юзабилити 2026-08-02 (docs/research/2026-08-02-ux-admin.md).
   Всё ниже — не вкусовое: каждое число снято Playwright'ом на 1366×768 и 390×844.
   ───────────────────────────────────────────────────────────────────────────────────────────── */

/* Ссылка ВНУТРИ фразы отличалась от соседнего текста ТОЛЬКО цветом: #22b8cf против #8b949e — это
   контраст 1.29 при пороге 3:1 (WCAG 1.4.1). Подбором цвета порог тут не взять в принципе (ярче
   ссылка → хуже её собственный контраст с фоном), поэтому единственный правильный ход —
   подчёркивание. Замер 02-08: 7 таких ссылок на /settings, /alerts, /alerts/history, /admins,
   /directions/{id}. Ссылки-ячейки таблиц, кнопки, плитки и меню НЕ трогаем: они не «внутри фразы»,
   их отличает положение, а сплошное подчёркивание сделало бы списки рябыми. */
p a,li a,.hint a,.notice a,.explain a,.pagesub a,.field .hint a,td p a,td.empty a{
  text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:from-font}
p a.btn,p a.btn-primary,p a.btn-ghost,li a.btn,li a.btn-primary,li a.btn-ghost{text-decoration:none}

/* Зоны нажатия. На 390×844 галочки рисовались браузерным размером 13×13 (в фильтре алертов — 5 шт.,
   в визарде — 4), при норме 24×24 (WCAG 2.5.8). Увеличиваем сам квадрат и, главное, делаем целью
   ВСЮ строку с подписью: попасть пальцем в текст проще, чем в квадратик. */
/* flex-shrink:0 не для красоты: замер 03-08 нашёл ДЛИННОЕ направление («Германия-Франкфурт-…»),
   которое внутри `label:has(>input)` ниже (тоже flex) сжимало сам квадрат до 13×18 — контейнер
   с длинным текстом-соседом теснит первый флекс-элемент, если ему не запрещено ужиматься. */
input[type=checkbox],input[type=radio]{width:18px;height:18px;flex-shrink:0}
.selcol input[type=checkbox]{width:18px;height:18px}
.catbox,.chk,.field-inline,.filterbar label.chk{min-height:24px}

/* Тот же порог 24 px (WCAG 2.5.8), но без опоры на класс `.chk` — замер 03-08 нашёл его НЕ везде:
   на /direction-sets/{id} галочки «активен» и часть направлений в .checkgrid клика-область была
   19 px (короткая подпись не тянула строку), хотя рядом другие галочки того же блока получали 47 px
   просто из-за более длинного текста — непоследовательность, а не решение. Правим ЛЮБОЙ label,
   который прямо оборачивает чекбокс/радио, а не только помеченные `.chk`: страница карточки набора
   вне периметра этой задачи (шаблон не в списке разрешённых), CSS-only правка её не трогает. */
label:has(>input[type=checkbox]),label:has(>input[type=radio]){min-height:24px;display:inline-flex;align-items:center;gap:8px}
/* Галочка-предохранитель РЯДОМ С РАЗРУШИТЕЛЬНОЙ кнопкой — по тем же 44 px, что и сама кнопка.
   24 px хватает по WCAG 2.5.8 AA, но соседство решает: замер 05-08 показал «Оборвать активных
   клиентов» целью 227×24 вплотную к кнопке 44 px. Промах по такой галочке — это либо не сделанное
   опасное действие, либо (что хуже) сделанное без осознанного подтверждения. */
form:has(.btn-danger) label:has(>input[type=checkbox]),
form:has(button.danger) label:has(>input[type=checkbox]),
form:has(.btn-danger-solid) label:has(>input[type=checkbox]){min-height:44px}
/* Раскрывашки-пояснения («Как это работает», «Как читать эту таблицу», «Создать пользователя»)
   были высотой 19.5 px во всю ширину карточки — по ширине попасть легко, по высоте нет. */
/* gap — не украшение, а починка ПРОБЕЛА. Маркер раскрывашки рисуется как `content:"▸ "`, и пока
   summary был обычной строкой, пробел в конце содержимого работал. С `display:flex` ::before стал
   отдельным флекс-элементом, а концевой пробел в нём отбрасывается — на экране выходило
   «▸Создать пользователя» и «▸Как читать эту таблицу» слитно (замер 11-08: край текста 313.5 px
   вплотную к маркеру шириной 6.5). Возвращаем расстояние тем, чем оно задаётся во флексе.
   Там, где gap уже задан своим правилом (.nav-lbl), побеждает оно — вес выше. */
.explain summary,details>summary{min-height:24px;display:flex;align-items:center;gap:6px}
.nav-lbl,.alertlog details summary{display:flex}

/* Кнопка «Удалить ноду» в «Опасной зоне»: белый на #f85149 = 3.35 при пороге 4.5 — самая
   разрушительная кнопка панели читалась хуже обычного текста. Тот же красный, но темнее
   (Mantine red-9): белый на нём 5.45. Заливку оставляем — она тут намеренная. */
/* Размеры — как у остальных кнопок (padding 9/16, шрифт 14): своя пара значений делала самую
   разрушительную кнопку панели МЕЛЬЧЕ соседних, хотя промахиваться по ней опаснее всего. */
.btn-danger-solid{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  background:#c92a2a;border:1px solid #c92a2a;color:#fff;border-radius:8px;padding:9px 16px;
  cursor:pointer;font:inherit;font-size:14px;font-weight:600;line-height:1.25;white-space:nowrap}
.btn-danger-solid:hover{background:#b02525;border-color:#b02525}

/* Плитки на телефоне. На 390 px одна плитка занимала всю ширину, и 13 плиток обзора давали 715 px
   вертикали — почти экран прокрутки до первой таблицы. Цифра в плитке короткая, ей столько места
   не нужно: на узком экране ставим их по две. Порог 210px→150px меняет ТОЛЬКО телефон (на 1366
   колонок как было — 4). */
@media(max-width:560px){
  /* Поля фильтров с фиксированной минимальной шириной торчали за экран: селект «Действие» на
     /audit рисовался 397–399 px при окне 390 (замер 2026-08-07). min-width заводили под десктоп,
     где рядом стоят пять полей в ряд; на телефоне ряд всё равно разворачивается в столбик, и
     держать минимум незачем — пусть каждое поле занимает строку целиком. */
  /* Ширину самой .filterbar НЕ трогаем: у неё горизонтальные поля 4px (см. .filterbar выше), и
     width:100% давало ровно 8px переполнения — а сторож прокрутки честно рисовал под каждой такой
     карточкой подсказку «таблица шире экрана», хотя никакой широкой таблицы там нет. Сама поймал
     этой же ночью, добавив правило часом раньше. Растягиваем только ПОЛЯ внутри. */
  .filterbar>label{width:100%}
  .filterbar select,.filterbar input[type=search],.filterbar input[type=text],.filterbar input[type=date]{
    min-width:0;width:100%;max-width:100%}
  .tiles{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
  .tile{padding:12px;gap:10px}
  /* На 390 px auto-fill давал ДВЕ колонки, и на плитку оставалось 92 px под текст — меньше, чем
     нужно неразрывной приписке вроде «· 62 с арендой» (138 px) или «· +1 служ.». Приписка вылезала
     за край плитки и растягивала страницу: замер 08-08 показывал scrollWidth 393 при экране 390,
     то есть горизонтальную прокрутку всего обзора из-за одной подписи. Ниже 480 px — одна колонка
     (число колонок задано явно, а не auto-fill: та же рекомендация, что в разборе эталона §1.3).
     Страница становится длиннее на ~250 px, зато ничего не обрезано и не вылезает. */
  @media(max-width:480px){
    .tiles{grid-template-columns:1fr}
    /* ...а обзору одна колонка больше НЕ нужна: длинных неразрывных приписок там не осталось —
       второй факт уехал из подписи в свою строку (.sub, 15-08). Замер на 390 px: две колонки,
       ширина плитки 174 px, ни у одной содержимое не шире её самой (scrollWidth == clientWidth),
       страница не расползается (scrollWidth документа = 390). Зато обзор с телефона стал вдвое
       короче — а владелец смотрит панель именно с телефона. */
    .tiles-kpi{grid-template-columns:1fr 1fr}
    /* 🔴 …но замер 15-08 делался ТОЛЬКО на обзоре, а класс .tiles-kpi носит ещё и страница
       «Версии приложения» — и там приписки длинные («из 62 назвавших», «боевая сейчас 0.3.23»).
       Что происходило (замер 16-08, 390 px): дорожка `1fr` не может стать уже min-content своего
       содержимого, а `white-space:nowrap` у приписки делает это min-content равным 208 px. Две
       такие дорожки с зазором — 422 px при экране 390, то есть горизонтальная прокрутка ВСЕЙ
       страницы; правый столбец плиток обрезан по краю экрана, и числа в нём не дочитать.
       Лечим причину, а не следствие: разрешаем плитке быть уже своего содержимого (min-width:0 —
       у grid-элемента он по умолчанию auto) и разрешаем длинной приписке переноситься. Неразрывной
       она нужна на десктопе, где рядом с числом стоит короткое «/62»; на телефоне цена неразрывности
       — обрезанная страница. После правки: документ 390 px, прокрутки нет. */
    .tile{min-width:0}
    .tile .num .of{white-space:normal}
  }
  .tile-ic{width:32px;height:32px}
  .tile-ic svg{width:18px;height:18px}
  .tile .num{font-size:22px}
  .uptime-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
  /* Пальцем, а не мышью. На десктопе поля и кнопки панели 41 и 38 px — с мышью это нормально, но
     владелец смотрит панель с телефона, а там норма попадания 44 px (Apple HIG; WCAG 2.5.8 требует
     24, мы уже проходим, но 38 px под большим пальцем — это промахи по «Сохранить» в таблице
     тарифов). Растим только на узком экране: десктопная плотность не меняется. */
  /* Список ТИПОВ здесь пропускал поля БЕЗ атрибута type: `<input name="code">` ведёт себя как
     text, но `input[type=text]` его не выбирает. БАЗОВОЕ правило вида (строка ~702) это давно
     учитывает — там первым стоит `input:not([type])`, — а мобильное про норму попадания не
     учитывало. Замер на бою: поле кода второго фактора на /2fa — 328×41 при норме 44 (остальные
     поля без type, в мастере и форме ноды, до 44 дотягивались отступами базового правила, то есть
     случайно). Перечисляем не типы, а ИСКЛЮЧЕНИЯ: галочки, переключатели, скрытое, кнопки-инпуты. */
  input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=submit]):not([type=button]):not([type=file]):not([type=image]),
  select,textarea{min-height:44px}
  /* button.danger (25-09): «Отвязать» на карточке направления мерилась 38 px — у неё своя геометрия
     (строка ~848), а в этом списке её не было. */
  .btn,.btn-primary,.btn-ghost,.btn-danger-solid,button.danger,.search button,.rangesw button{min-height:44px}
  /* Проход панели с телефона 03-08: короткая подпись «1ч» в переключателе диапазона (карточка ноды)
     давала 43×44 — на 1 px уже нормы по ШИРИНЕ. min-height был, min-width — нет; та же причина, что
     у гамбургера чуть выше. */
  .rangesw button{min-width:44px}
  /* Выход из сайдбара — 44 px: «Выйти» был 37. По нему промахиваться обиднее всего, он на каждом
     экране. ⚠️ Кнопка ☰ отсюда УЕХАЛА в блок ≤900: ниже 600 её больше нет вовсе (её работу делает
     «Ещё» в нижней панели), и правило про её размер здесь просто некому было бы применить. */
  .side-logout{min-height:44px;display:flex;align-items:center}
}

/* Пустая таблица (единственная строка — объяснение «данных пока нет») шапку не показывает:
   десять названий колонок над одной фразой — визуальный мусор, в котором тонет сама фраза.
   Класс ставит admin.js по наличию `td.empty`; без JS всё остаётся как было. */
table.is-empty thead{display:none}
table.is-empty td.empty{padding:22px 12px}

/* --- Карточки фактов о машине (.sys-grid/.sys-sub) и отчёт подготовки (.prep) ---
   Порт node-system-card из remnawave/frontend: тинт-подсекции + statLabel/statValue. Жили в <style>
   карточки ноды, пока их не понадобилось ДВА экрана (карточка + страница полного отчёта подготовки,
   SPEC-0042 T7): копия в шаблоне обязательно разъезжается с оригиналом, поэтому паттерн переехал
   сюда — в общий лист, как и требует дизайн-система («новый компонент — в общий набор»).
   auto-FILL, а не auto-fit: auto-fit растягивает единственную карточку на всю ширину строки, и
   сетка ломается на неполном ряду (разбор 02-08). */
/* Тот же приём «сетка плиток», что .tiles и .uptime-grid, — значит и зазор тот же. Стояло 10 px
   против 16 у соседей на ОДНОМ экране (карточка ноды): одинаковые по смыслу блоки дышали
   по-разному, и это видно глазом. */
.sys-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:16px;margin-top:6px}
.sys-sub{padding:9px 12px;border-radius:6px;border:1px solid rgba(255,255,255,.05);background:rgba(255,255,255,.02);min-width:0}
.sys-sub.mem{background:rgba(45,212,191,.06);border-color:rgba(45,212,191,.12)}
.sys-sub.net{background:rgba(34,211,238,.06);border-color:rgba(34,211,238,.12)}
.sys-sub .sl{color:#909296;font-size:12px;font-weight:500;line-height:1.3}
.sys-sub .sv{font-weight:600;font-size:13px;font-family:'Fira Mono',ui-monospace,monospace;color:#e8eaed;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sys-sub .sl2{margin-top:7px}
/* Адреса (IPv4/IPv6 на карточке ноды) — переносятся, а не режутся многоточием: обрезанный адрес
   нельзя ни прочитать, ни выделить мышью, а это единственное место в панели, где его берут (29-08,
   ревью панели, п.18). anywhere, а не break-word: у IPv6 нет пробелов и «слово» тут одно на всю
   строку. Остальные .sv по-прежнему в одну строку — там значения короткие. */
.sys-sub .sv-addr{white-space:normal;overflow:visible;text-overflow:clip;overflow-wrap:anywhere}
/* Отчёт подготовки: значения — ФРАЗЫ («закрыт — сертификат не выпустится»), а не короткие токены,
   поэтому строка переносится, а не режется многоточием, и печатается обычным шрифтом; моно
   остаётся идентификаторам (версия ядра, адрес IPv6). */
.prep .sv{white-space:normal;overflow:visible;text-overflow:clip;font-family:inherit;font-size:13.5px}
.prep .sv.mono,.prep .sl.mono{font-family:'Fira Mono',ui-monospace,SFMono-Regular,Menlo,monospace}
.prep .sv.ok{color:var(--ok)}.prep .sv.warn{color:var(--warn)}.prep .sv.bad{color:var(--bad-ink)}
.prep h3{font-size:13px;font-weight:600;color:var(--muted);margin:16px 0 0;text-transform:uppercase;letter-spacing:.03em}
.prep .port-pills{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
.prep .port-pill{background:var(--panel2);border:1px solid var(--line);border-radius:999px;color:var(--muted);
  font-size:12px;padding:3px 10px;font-family:'Fira Mono',ui-monospace,monospace}
.prep .port-pill.hot{background:rgba(248,81,73,.14);border-color:rgba(248,81,73,.4);color:var(--bad-ink)}
.prep .raw{max-height:220px;overflow:auto;background:var(--panel2);border:1px solid var(--line);
  border-radius:8px;padding:10px 12px;font-size:12px;white-space:pre-wrap;word-break:break-all}
/* Короткие результаты проверок бейджами — в СПИСКЕ нод (nodes.html). Тон несёт вывод, а не просто
   красит: серый — «проверка была, результата нет», зелёный/жёлтый — итог проверки. */
.checkchip{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;
  font-size:12px;font-weight:600;border:1px solid var(--line);color:var(--muted);white-space:nowrap}
/* На телефоне тот же nowrap ЛОМАЛ СТРАНИЦУ: результат проверки процессора приходит фразой
   («Процессор 4 457.63 оп/с · сосед берёт 0.09980039920159679 %»), в строку это 403 px, и весь
   /nodes на 390 px ехал вбок — scrollWidth 448 при экране 390 (замер 11-08). Неразрывность нужна
   коротким чипам («IPv6 в обе стороны»), а не абзацу: на узком экране разрешаем перенос.
   ⚠️ Само 17-значное число — это формат в Go (node_prepare.go, prepareShortNum), не раскладка;
   вёрстка может только не дать ему сломать экран. */
@media(max-width:700px){.checkchip{white-space:normal}}
.checkchip b{font-weight:600;color:var(--txt)}
.checkchip.ok{border-color:rgba(45,212,191,.4);color:#38d9a9}
.checkchip.ok b{color:#38d9a9}
.checkchip.warn{border-color:rgba(245,158,11,.4);color:var(--warn)}
.checkchip.warn b{color:var(--warn)}
.checkchip.bad{border-color:rgba(248,81,73,.4);color:var(--bad-ink)}
.checkchip.bad b{color:var(--bad-ink)}

/* ── Складные карточки обзора — УБРАНО 2026-08-08 ─────────────────────────────────────────────
   Механизм (data-fold + .fold-*) был подпоркой под обзор из семи таблиц: на 390 px страница тянулась
   на 3714 px, и карточки на узком экране складывались, чтобы простыню можно было пролистать. Таблицы
   с обзора унесены в свои разделы, складывать стало нечего, и data-fold не осталось ни в одном
   шаблоне — вместе с ним ушли эти стили и обработчик из admin.js. Понадобится снова — это признак,
   что на страницу опять сложили лишнее. */

/* ── Окно подтверждения (admin.js, window.mayakAsk) ──────────────────────────────────────────────
   Заменяет системный confirm(): тот не оформляется, показывает на телефоне адрес сайта в шапке и
   даёт кнопки «ОК/Отмена», которые не называют действие. Геометрия и цвета — те же токены, что у
   карточек и кнопок панели, чтобы окно не выглядело чужим. */
dialog.ask{border:1px solid var(--card-bd);border-radius:12px;background:var(--bg);color:var(--txt);
  padding:22px 24px 18px;max-width:min(520px,calc(100vw - 32px));width:100%;
  box-shadow:0 18px 48px rgba(0,0,0,.55)}
dialog.ask::backdrop{background:rgba(1,4,9,.66)}
/* Появление окна — короткий переход вместо телепорта: окно, возникшее без движения, читается как
   сбой страницы, а не как ответ на нажатие. Keyframe на [open], а не transition: showModal() ставит
   атрибут заново при каждом открытии, и анимация честно переигрывается. Закрытие остаётся мгновенным
   СОЗНАТЕЛЬНО: close() убирает окно из дерева сразу, а откладывать его таймером значило бы влезать в
   цепочку done()/resolve в admin.js — риск для механики подтверждения ради малозаметного выигрыша.
   При prefers-reduced-motion длительности гасятся глобальным правилом (и отдельной строкой для
   ::backdrop) — окно просто появляется, конечное состояние то же. */
dialog.ask[open]{animation:ask-in .2s var(--ease-out)}
dialog.ask[open]::backdrop{animation:ds-fadein .2s var(--ease-out)}
@keyframes ask-in{from{opacity:0;transform:translateY(8px) scale(.98)}}
dialog.ask .ask-title{margin:0 0 10px;font-size:18px;font-weight:600;line-height:1.35;
  overflow-wrap:anywhere}
/* Цена действия НЕ должна читаться как продолжение вопроса — иначе окно снова нажимают не глядя. */
dialog.ask .ask-detail{margin:0 0 18px;color:var(--muted);font-size:14px;line-height:1.5}
dialog.ask .ask-detail p{margin:0 0 8px}
dialog.ask .ask-detail p:last-child{margin-bottom:0}
dialog.ask .ask-actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}
/* На телефоне кнопки в столбик и во всю ширину: две кнопки по 44 px рядом на 390 px не расходятся,
   и промах по «Удалить» вместо «Отмена» — ровно та ошибка, которой окно и должно мешать. */
@media (max-width:520px){
  dialog.ask .ask-actions{flex-direction:column-reverse}
  dialog.ask .ask-actions .btn{width:100%}
}

/* ─────────────────────────────────────────────────────────────────────────────────────────────
   ОБРАЩЕНИЯ В ПОДДЕРЖКУ (SPEC-0043, 08-08). Раздел «Обращения»: список и карточка с ниткой.

   Три вещи, которые здесь делает ИМЕННО оформление, а не текст:
     1. Ответ человеку и внутренняя заметка обязаны отличаться С ПЕРВОГО ВЗГЛЯДА. Если они выглядят
        одинаково, однажды в кабинет уедет то, что писалось для себя, — и отозвать это уже нельзя.
        Отсюда жёлтая полоса и другой фон у заметки, зелёная — у нашего ответа, серая — у человека.
        Цвет ВТОРОЙ сигнал: рядом стоит слово («внутренняя заметка» / «ответ человеку»).
     2. Текст человека печатается в <pre> — переносы строк в жалобе значимы (список шагов, вывод
        ошибки), а схлопнутый в абзац текст читается вдвое хуже. `white-space:pre-wrap` + перенос
        длинных слов, иначе одна ссылка в жалобе растянула бы карточку и дала прокрутку вбок.
     3. Состояния обращения — свои цвета бейджей: «новое» акцентом (мяч у нас), «ждём ответа» —
        приглушённо (мяч у человека), «закрыто» — совсем тихо. */
.st-new{background:rgba(34,184,207,.16);color:var(--accent)}
.st-in_progress{background:rgba(210,153,34,.15);color:var(--warn)}
.st-waiting{background:rgba(88,166,255,.16);color:#79c0ff}
.st-closed{background:var(--panel);color:var(--muted)}
.st-note{background:rgba(210,153,34,.15);color:var(--warn)}
/* Пометка «похоже на самоприглашение» — ТЕКСТ, а не бейдж. Причина бывает длинной («один адрес при
   регистрации, регистрации в пределах часа»), а у .badge стоит white-space:nowrap: одна такая
   строка растягивала таблицу приглашений шире окна и включала прокрутку вбок на 1440×900 (замер
   15-08). Цвет остаётся предупреждающим — по нему строку и находят глазами. */
.suspicion{color:var(--warn);display:inline-block;max-width:34ch;line-height:1.35}
/* Плитка состояния, на которое смотрят: «новые» и «в работе» — это работа, а не статистика. */
.tile-accent{border-left:2px solid rgba(34,184,207,.5)}
.support .sm{font-size:12.5px}
.support .filterbar{margin-bottom:8px}

.thread{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}
.thread .msg{border:1px solid var(--line);border-left:3px solid var(--line);border-radius:10px;
  padding:10px 14px;background:var(--panel2)}
.thread .msg-them{border-left-color:var(--muted)}
.thread .msg-mine{border-left-color:var(--ok);background:rgba(63,185,80,.05)}
.thread .msg-note{border-left-color:var(--warn);background:rgba(210,153,34,.07)}
.thread .msg-head{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:6px}
.thread .msg-body{margin:0;font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.55}
.thread .msg-err{margin:8px 0 0}
/* Оговорка «разобрано из письма» стоит у самого сообщения (см. support_ticket.html): цитаты и
   отсутствие вложений — свойство ЭТОГО текста, а не всего раздела. */
.thread .msg-mail{margin:8px 0 0;line-height:1.45}
/* 📎 Вложения письма (миграция 0277): превью картинок плиткой, файлы и несохранённое — строками.
   Высота превью ограничена: скриншот телефона высокий, и без потолка одно вложение занимало бы
   экран. Полный размер — по нажатию (картинка открывается отдельной вкладкой). */
.thread .msg-atts{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:10px}
.thread .msg-atts .att-skip,.thread .msg-atts .att-file{flex-basis:100%}
.thread .msg-atts .att-img{display:flex;flex-direction:column;gap:4px;max-width:220px}
.thread .msg-atts .att-img img{display:block;max-width:220px;max-height:260px;border:1px solid var(--line);
  border-radius:8px;background:var(--panel);object-fit:contain}
.thread .msg-atts .att-img span{overflow-wrap:anywhere}
/* Телефон: полей меньше, полоса слева остаётся — она и есть признак «чьё сообщение». */
@media(max-width:560px){
  .thread .msg{padding:9px 11px}
  .thread .msg-atts .att-img,.thread .msg-atts .att-img img{max-width:calc(50vw - 40px)}
  /* Цели нажатия в этом разделе — те же 44 px, что и везде: владелец смотрит панель с телефона. */
  .support .btn,.support .btn-primary,.support .btn-ghost{min-height:44px}
  .support .filterbar select,.support .filterbar input[type=search]{min-height:44px}
}

/* ─────────────────────────────────────────────────────────────────────────────────────────────
   ГРУППЫ (SPEC-0044 T5, 08-08). Раздел «Группы»: вся иерархия делегирования слотов и ветка одного
   человека деревом. Блок дописан В КОНЕЦ файла намеренно — так правка не пересекается с чужими
   разделами при слиянии нескольких агентов.

   Что здесь делает именно оформление, а не текст:
     1. НОМЕР АККАУНТА — главный якорь этого экрана: его диктуют голосом в поддержку. Поэтому он
        крупнее строки, моноширинный и с разрядкой — «472-918-563» должно читаться с одного взгляда
        и не путаться с числами слотов рядом.
     2. ДЕРЕВО ЧИТАЕТСЯ ОТСТУПОМ И ЛИНИЕЙ, а не только отступом: связь «кто кому выдал» — это ровно
        то, зачем экран открывают, и терять её на узком окне нельзя. На телефоне отступ уменьшается,
        но линия остаётся — дерево перестраивается в ленту, а не «сжимается».
     3. ЗА ЧЕЙ СЧЁТ ЖИВЁТ УСТРОЙСТВО подписано у каждого устройства и выделено цветом ТОЛЬКО вместе
        со словом («слот от 472-918-563»): цвет — второй сигнал, а не первый (доступность).
     4. Полоска «занято N из M» всегда идёт СО СТРОКОЙ ЧИСЕЛ. Полоска без подписи — украшение:
        по ней нельзя ни назвать число, ни проверить его. */
.groups .sm{font-size:12.5px}
.groups .ok{color:var(--ok)}
.groups .accent{color:var(--accent2)}
/* Номер аккаунта: то, что человек диктует голосом. nowrap обязателен — в узкой колонке он ломался
   на три строки по дефисам («092-\n372-\n159», замер 1440), и продиктовать его стало нельзя. */
.groups .acct{font-size:15.5px;font-weight:700;letter-spacing:.03em}
.groups .acct,.groups td .mono,.gchain .mono{white-space:nowrap}
.groups .tile-hint{display:block;font-size:11px;margin-top:4px;line-height:1.3}
.groups .chips{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
.groups .chip{border:1px solid var(--line);border-radius:7px;padding:1px 7px;font-size:12px;
  color:var(--muted);background:rgba(255,255,255,.03)}
/* Найденный по номеру человек подсвечен строкой, а не «где-то в таблице». */
.groups tr.row-focus td{background:rgba(34,184,207,.10)}
/* Логин/почта в таблице участников ОГРАНИЧЕНЫ по ширине. Иначе ширина таблицы зависит от того, какой
   длины у человека почта: на одном наборе данных девять столбцов влезали в 1440, на другом — уже
   нет (+32 px), а не влезающая таблица остаётся коробкой, которую страница увозит вместе с шапкой.
   Полное значение не теряется: оно в title и в карточке человека. */
.groups table.people .ell{display:inline-block;max-width:11em;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;vertical-align:bottom}
.groups table.people td{max-width:14em}
/* 1280 (25-09). Карточка там 934 px, а таблица даже в САМОМ УЗКОМ виде (каждая колонка сжата до
   своего min-content) была 937 — с живым «Откуда» 1031. Обёртка становилась коробкой с прокруткой
   вбок, «Откуда» уезжало за край, а «Связь» — единственная колонка, которой было куда сжиматься, —
   вставала в 63 px и пять строк. Лечится только тем, что НАИМЕНЬШАЯ ширина каждой колонки задана
   нами, а не тем, какой длины слово попалось в данных (замер стенда 1280, стало 908 из 934):
     • «Свободно» — второй строкой под «Получено» (шаблон), −94 px;
     • почта ВЫДАВШЕГО — вторая строка под его номером, и ширину колонке задаёт номер (88 px), а не
       она: 7em ≈ номер, полное значение в title, −50 px; своя почта человека — 10em, −15 px;
     • «Откуда» — самое длинное слово там домен лендинга, «mayaknetworks.com»: 146 px в строке
       таблицы, 120 px шрифтом подписи (он же у даты под ним). Пол 11em вмещает его целиком — пол
       раскладка таблицы меряет ВМЕСТЕ с полем ячейки, и при 10em домен рвался (замер при ширине
       min-content). Всё, что длиннее (utm-метка без пробела, адрес referrer'а), рвётся внутри
       пола, а не распирает таблицу: `anywhere` без пола сжал бы колонку в букву (см. .wrapcell). */
.groups table.people td.pby .ell{max-width:7em}
.groups table.people a.ell{max-width:10em}
.groups table.people td.pfrom{font-size:12.5px;overflow-wrap:anywhere;min-width:11em}

/* ── дерево ─────────────────────────────────────────────────────────────────────────────────── */
.gtree{list-style:none;margin:0;padding:0}
.gtree .gtree{margin:8px 0 0 14px;padding-left:18px;border-left:1px solid var(--line)}
.gnode{position:relative;margin:0 0 10px}
/* Скобка от линии предка к карточке: связь видна и когда у ребёнка длинная карточка. */
.gtree .gtree>.gnode::before{content:"";position:absolute;left:-18px;top:20px;width:14px;
  border-top:1px solid var(--line)}
.gcard{border:1px solid var(--line);border-radius:10px;padding:10px 13px;background:var(--panel2)}
.gnode-root>.gcard{border-left:3px solid var(--accent)}
.ghead{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:2px}
.gslots{margin-top:9px}
.gbar{height:6px;border-radius:4px;background:rgba(255,255,255,.07);overflow:hidden;margin-bottom:5px}
.gbar-fill{display:block;height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent))}
.gdevs{list-style:none;margin:9px 0 0;padding:0;display:flex;flex-direction:column;gap:4px}
.gdev{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;padding:4px 9px;border-radius:8px;
  background:rgba(255,255,255,.03)}
.gdev-name{font-weight:600}
/* Погашенное устройство тише живого, но НЕ спрятано: именно про него и спрашивают. */
.gdev-off{opacity:.7}
.gfrom{margin:8px 0 0}
.gchain{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 10px}
.garrow{color:var(--muted)}
.gbranch h2{margin-top:0}
.gbranch h3{font-size:14px;margin:18px 0 8px}

/* Телефон (владелец смотрит панель с телефона): отступ дерева меньше, линия остаётся, цели
   нажатия — те же 44 px, что и во всей панели. */
@media(max-width:700px){
  .gtree .gtree{margin-left:6px;padding-left:12px}
  .gtree .gtree>.gnode::before{left:-12px;width:9px}
  .gcard{padding:9px 11px}
  /* На телефоне таблица разворачивается в карточки — обрезать там нечего и незачем: ширину задаёт
     экран, а не соседние столбцы. Многоточие вместо почты в карточке было бы потерей без причины. */
  /* Селекторы — те же, что у потолков выше (вес 0-4-2 у td.pby .ell): простой `.ell` их не перебил бы,
     и почта выдавшего осталась бы с многоточием и на телефоне. */
  .groups table.people .ell,.groups table.people td.pby .ell,.groups table.people a.ell{max-width:none;
    white-space:normal;overflow:visible;display:inline}
  .groups table.people td{max-width:none}
  /* Ячейка с числом и строкой под ним («1» + «дальше 3», «3» + «свободно 1») — это ТРИ флекс-элемента
     вместе с подписью, и space-between ставил число посреди карточки, отдельно и от подписи, и от
     своей второй строки. Подпись забирает свободное место себе — число и приписка встают вместе справа. */
  .groups table.people td::before{margin-right:auto}
  .groups .btn,.groups .btn-ghost{min-height:44px;display:inline-flex;align-items:center}
  .groups .filterbar select,.groups .filterbar input[type=search]{min-height:44px}
}

/* ─── Полоса разделов длинной карточки (14-08) ───────────────────────────────────────────────────
   Ставится скриптом (admin.js) из секций с data-nav — см. мотивацию там же: карточка человека на
   боевом это 5126 px, 5.7 экрана, 14 разделов и ноль навигации.

   Почему z-index 3, а не меньше. Внутри карточки живут таблицы с ЗАКРЕПЛЁННЫМИ шапками
   (thead th{position:sticky;top:0;z-index:2}). Двум закреплённым слоям на одной высоте нужен явный
   порядок, иначе шапка таблицы всплывает поверх полосы. Полоса главнее: она про всю страницу.
   Сайдбар (z-index 200) остаётся выше обоих — он перекрывает содержимое по замыслу каркаса. */
.cardnav{position:sticky;top:0;z-index:3;display:flex;gap:6px;overflow-x:auto;
  margin:0 0 14px;padding:8px 2px;background:var(--bg);
  border-bottom:1px solid var(--line);scrollbar-width:none}
.cardnav::-webkit-scrollbar{display:none}
/* Подсказка «полоса продолжается вправо» (20-08). Замер на боевом при ширине окна 1440: у карточки
   человека scrollWidth 1166 против clientWidth 1124 — чип «Трафик» разрезан пополам ровно по кромке.
   Листалось это и раньше, но полосу прокрутки мы прячем (строка выше, и правильно — она бы прыгала
   и съедала высоту закреплённого слоя), поэтому обрезанный чип читался как «конец списка», а не как
   «есть ещё». Разделов там 14, и последние из них человек просто не находил.

   Делаем затенением у правой кромки, а не переносом на вторую строку: полоса ЗАКРЕПЛЁННАЯ, её высота
   зашита в три соседних правила (scroll-padding-top:60px и `thead th{top:46px}` здесь же, плюс их
   версии для 900/600px). Вторая строка сдвинула бы всё это молча — якоря снова начали бы ставить
   раздел под полосу, ровно тот дефект, который лечили 14-08.

   Механика: ::after у flex-контейнера — сам flex-элемент. Он стоит ПОСЛЕ последнего чипа и прижат к
   правой кромке видимой части ленты (`position:sticky;right:0`), поэтому подсказка сама появляется и
   сама исчезает, без JS и без счёта ширин:
     · листать нечего — элемент уезжает за последний чип на пустой фон, градиент «прозрачный → фон»
       на фоне не виден;
     · есть что листать — он висит у кромки поверх обрезанного чипа и растворяет его;
     · долистали до конца — он снова оказывается на своём месте за последним чипом, то есть на пустом
       фоне. Это и причина НЕ давать ему отрицательный margin: так он экономил бы 32 px ширины, но у
       конца ленты продолжал бы гасить чип «Трафик» — тот самый, ради которого человек и листал.
   pointer-events:none — иначе он перехватывал бы клик по последнему чипу. */
.cardnav::after{content:"";position:sticky;right:0;flex:0 0 32px;align-self:stretch;
  pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,0),var(--bg) 78%)}
.cardnav a{flex:0 0 auto;padding:6px 12px;border-radius:999px;font-size:13px;line-height:1;
  color:var(--muted);border:1px solid transparent;white-space:nowrap}
.cardnav a:hover{color:var(--txt);border-color:var(--line)}
/* Текущий раздел (считает admin.js прямо по положению — IntersectionObserver там осознанно
   отвергнут, см. комментарий у mark()). Заливка слабая, а не сплошная: полоса — указатель
   места, а не главное действие страницы; сплошной cyan спорил бы с кнопками «Продлить»/«Заблокировать». */
.cardnav a.on{color:var(--cyan-4);border-color:var(--cyan-7);background:rgba(21,170,191,.10)}
/* Якорь обязан доводить раздел ПОД полосу. Двигаем ОДИН рычаг — scroll-padding-top прокручиваемого
   элемента; собственный scroll-margin разделам НЕ даём. Первая версия давала оба, и замер показал,
   что они складываются (52 + 60 = 112): раздел вставал на 68 px ниже полосы вместо 16.
   60px = полоса (44) + воздух. Базовое значение 52px (под шапку таблиц) остаётся для всех остальных
   страниц — оно строкой выше. */
html.has-cardnav{scroll-padding-top:60px}
/* Шапки таблиц внутри карточки липнут НИЖЕ полосы — но ТОЛЬКО у обёрток с .fits-x.
   🔴 Селектор был перевёрнут в первой версии (`:not(.fits-x)`), и это возвращало дефект 09-08.
   Разница принципиальна: `.fits-x` — обёртка, которой нечего листать вбок (`overflow-x:clip`,
   `max-height:none`), её шапка липнет к ОКНУ и потому уезжает под нашу полосу — ей офсет нужен.
   А `.tablewrap` БЕЗ `.fits-x` осталась прокручиваемой коробкой ради широкой таблицы: её шапка
   липнет к верху самой коробки, который и так ниже полосы. Офсет там создаёт полосу, в которой
   строки едут ПОВЕРХ названий колонок — ровно то, что замерили 09-08 на /audit («28:17 claude
   user.block» поверх «АДМИН / ДЕЙСТВИЕ»), см. правило `.tablewrap:not(.fits-x) thead th{top:0}`. */
html.has-cardnav .tablewrap.fits-x thead th{top:46px}
@media (max-width:900px){
  /* На планшете сверху уже висит кнопка ☰ (sticky, 44px + отступы) — полоса встаёт под неё, иначе
     два закреплённых слоя перекрываются. Запас якорям увеличиваем на ту же высоту. */
  .cardnav{top:56px}
  html.has-cardnav{scroll-padding-top:112px}
  html.has-cardnav .tablewrap.fits-x thead th{top:102px}
}
@media (max-width:600px){
  /* Телефон: полоса остаётся у верхней кромки (кнопка ☰ там не закреплена), но кегль мельче —
     чипы должны читаться, не съедая экран. Рекомендация по закреплённым панелям: не больше
     20–30% высоты экрана на всё закреплённое, а снизу уже стоит панель разделов (52px). */
  .cardnav{top:0;padding:6px 2px}
  .cardnav a{padding:5px 10px;font-size:12px}
  html.has-cardnav{scroll-padding-top:56px}
}

/* ── БЫСТРЫЙ ПЕРЕХОД (Cmd/Ctrl+K) ────────────────────────────────────────────────────────────
   Кнопка в сайдбаре + окно поиска по разделам и по человеку. Логика — static/palette.js.
   Цвета берём токенами дизайн-системы: своих значений тут нет ни одного. */
/* min-height 44: на телефоне сайдбар открывается кнопкой «Ещё» снизу, и «Быстрый переход» —
   такая же цель под палец, как пункты меню под ним (те давно 44). Замер 22-08 дал 208×39. */
.side-cmdk{display:flex;align-items:center;gap:.5rem;width:calc(100% - 1.6rem);margin:.2rem .8rem .6rem;
  padding:.5rem .6rem;min-height:44px;border:1px solid var(--line);border-radius:10px;background:var(--bg-deep, var(--ds-bg-deep));
  color:var(--muted);font:inherit;font-size:.86rem;cursor:pointer;text-align:left}
.side-cmdk:hover{border-color:var(--primary);color:var(--txt)}
.side-cmdk svg{width:15px;height:15px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-width:2;
  stroke-linecap:round}
.side-cmdk span{flex:1 1 auto}
.side-cmdk kbd{font-family:var(--font-mono, ui-monospace, monospace);font-size:.72rem;border:1px solid var(--line);
  border-radius:6px;padding:.05rem .3rem;color:var(--muted)}

.cmdk{position:fixed;inset:0;z-index:1000}
.cmdk[hidden]{display:none}
.cmdk-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.55)}
.cmdk-win{position:relative;max-width:560px;margin:10vh auto 0;background:var(--card-solid,#21262d);
  border:1px solid var(--line);border-radius:14px;box-shadow:0 24px 60px -20px rgba(0,0,0,.7);
  overflow:hidden}
.cmdk-input{width:100%;padding:.85rem 1rem;border:0;border-bottom:1px solid var(--line);
  background:transparent;color:var(--txt);font:inherit;font-size:1rem;outline:none}
.cmdk-list{list-style:none;margin:0;padding:.3rem;max-height:52vh;overflow:auto}
.cmdk-item{display:flex;align-items:center;justify-content:space-between;gap:.8rem;
  padding:.5rem .7rem;border-radius:9px;cursor:pointer}
.cmdk-item.is-cur{background:rgba(34,184,207,.14)}
.cmdk-title{color:var(--txt)}
.cmdk-group{font-size:.78rem}
.cmdk-empty{padding:.7rem;font-size:.86rem}
.cmdk-foot{padding:.5rem .8rem;border-top:1px solid var(--line);font-size:.76rem}
@media (max-width:600px){
  .cmdk-win{margin:4vh .6rem 0}
  .cmdk-foot{display:none} /* на телефоне подсказка про клавиши бесполезна */
}

/* ══ Утилиты вместо атрибутов style= (20-08) ═══════════════════════════════════════════════════
   Заведены, когда из шаблонов панели убирали 81 атрибут style="…". Причина та же, что 19-08 у
   скриптов: политика ужимается до `style-src 'self'`, а атрибут style — ровно то, что такая
   политика запрещает (style-src-attr наследует значение от style-src, отдельно его не задаём).
   Пока разметка носила стили на себе, CSP была обязана разрешать 'unsafe-inline', и внедрённый в
   страницу style работал наравне с нашим — а через него на экране, где видны ПДн людей, рисуется
   и фальшивая форма, и накладка поверх кнопки.
   Значения перенесены из разметки КАК БЫЛИ: ни одно не «заодно поправлено», иначе визуальную
   регрессию потом не отличить от переноса. Новое сюда добавлять только если правило правда
   повторяется на нескольких экранах; разовое — в правила своей страницы (static/css/<страница>.css).

   ⚠️ Префикс `mu-` (mayak utility), а НЕ короткий `u-`: короткий занят вендорной библиотекой
   графиков — /static/uplot/uPlot.min.css объявляет .u-wrap, .u-axis, .u-legend, .u-off… и среди них
   `.u-inline{display:block}`. На карточке ноды uPlot подключается ПОСЛЕ style.css, то есть при
   равном весе побеждал бы он: заголовок с нашим `.u-inline` остался бы блочным, а `.u-inline *`
   вдобавок перекрасил бы всё внутри. Поймано сверкой вычисленных стилей в браузере.

   ⚠️ Порядок при выкате: СНАЧАЛА новый бинарь панели (шаблоны и статика вшиты в него go:embed),
   ПОТОМ ужатый заголовок CSP в Caddy. Наоборот — панель останется без своих стилей. */

/* Скрытие. !important здесь НЕ «чтобы наверняка», а по устройству загрузки: style.css подключается
   ДО файла страницы (base.html), поэтому у равных по весу селекторов побеждает страничный — и
   `.steps2{display:flex}` перебил бы обычный `.mu-hide`. Обратная сторона: показывать элемент
   присваиванием el.style.display='' больше нельзя (инлайн проигрывает !important) — JS снимает
   и вешает сам класс (classList), см. static/js/node-*.js. */
.mu-hide{display:none!important}
.mu-inline{display:inline}
/* Ряд кнопок/полей в одну строку с переносом — шапки разделов и подтверждения опасных действий. */
.mu-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.mu-m0{margin:0}
.mu-mb6{margin:0 0 6px 0} /* только нижний зазор, остальные поля обнулены — как было в разметке */
.mu-mt6{margin-top:6px}
.mu-mt8{margin-top:8px}
.mu-mt10{margin-top:10px}
.mu-mt12{margin-top:12px}
.mu-mt16{margin-top:16px}
.mu-mt24{margin-top:24px}
.mu-ml8{margin-left:8px}
.mu-fs12{font-size:12px}
.mu-fs13{font-size:13px}
.mu-nowrap{white-space:nowrap}
.mu-right{float:right}
/* Ширины полей ввода. Тип элемента в селекторе (`input.mu-w22em`), а не просто класс, — НЕ прихоть:
   общий блок правил для полей выше (`input[type=text],…{width:100%;max-width:100%;font:inherit}`)
   бьёт по АТРИБУТУ, а атрибут в CSS весит ровно столько же, сколько класс, — простой класс ему
   проигрывает. Проверено вычисленными стилями в браузере: 22em превращались обратно в 100%.
   Инлайновый style, стоявший здесь до 20-08, выигрывал у всего этого сам собой, без оговорок. */
input.mu-w9em{width:9em}
input.mu-w22em{width:22em}
.mu-minw16em{min-width:16em}

/* Приписка к заголовку раздела («Система — самоотчёт ноды»): тот же размер, что у текста, и без
   жира — иначе она читается как часть самого заголовка. Встречается на карточке ноды и в отчёте
   подготовки, поэтому правило общее, а не страничное. */
.h2note{font-size:13px;font-weight:400}
/* Предупреждающий текст янтарём (устаревший агент, «доступ ещё N дней» перед удалением). */
.warntext{color:#d29922}

/* Подписи-пояснения в отчёте подготовки ноды. Боковой отступ 4px выравнивает их по содержимому
   плиток, у которых свой padding. Один и тот же блок живёт на двух экранах — карточка ноды
   («Как заводили эту ноду») и /nodes/prepare/{id}, — поэтому правила здесь, а не в node.css. */
.prep-note{margin:8px 4px 0;font-size:12px}
.prep-note-top{margin:10px 4px 0;font-size:12px}
.prep-note-plain{margin:0 4px}
.prep-lead{margin:6px 4px 6px}
.prep-more{margin:12px 4px 0}

/* Привязка ноды из пула (/directions/{id}) — форма «выбрать ноду + кнопка» в одну строку. */
.attachform{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0}
/* Форма возврата на карточке платежа: отступ сверху — чтобы её подпись не налезала на строку-
   подсказку прокрутки таблицы заявок над ней (замер на стенде 11-08, объяснение — в шаблоне). */
.refund-form{margin-top:1.5rem}
/* Подзаголовки на «Настройках» стоят ближе к своей таблице, чем общие `.card h3` (18px сверху):
   между заголовком и таблицей нет вводного текста. Элемент в селекторе — чтобы весить столько же,
   сколько `.card h3` выше, плюс свой класс, иначе общее правило побеждает. */
.card h3.h3-tight{margin-top:14px}
/* Поле «причина» в строке отзыва устройства: тянется по ширине ячейки, но не разъезжается. */
input.reason-inp{width:100%;max-width:16em;min-width:9em}
/* Обёртка голой галочки в ячейке: сама подпись пустая, её работа — быть целью нажатия. */
.cellchk{display:inline-flex;align-items:center;justify-content:center;cursor:pointer}

/* ─────────────────────────────────────────────────────────────────────────────
   200 % ЗУМА (экран ~195 px). Правила ниже обязаны стоять В КОНЦЕ файла: они
   перебивают базовые `.btn…{white-space:nowrap}` и `.mu-row`, объявленные выше,
   а при равном весе селекторов выигрывает последний. Проверено 22-08: до правки
   вычисленный white-space кнопки «Обновить агента на всех нодах» на 195 px был
   nowrap, хотя правило «переносить» было написано тем же утром и считалось
   действующим (страница /nodes всё равно ехала вбок на 108 px).
   ───────────────────────────────────────────────────────────────────────────── */
@media(max-width:340px){
  /* Длинная подпись кнопки не ужимается и тянет страницу. Перенос лучше горизонтальной
     прокрутки: кнопка станет выше, зато целиком на экране. */
  .btn,.btn-primary,.btn-ghost,.btn-danger-solid{white-space:normal}
  /* Ряд кнопок — элемент флекса у .pagehead, а у элемента флекса min-width по умолчанию auto:
     уже своего содержимого он не станет, даже когда содержимому перенос разрешён. */
  .mu-row{min-width:0}
  /* Плитки KPI держат две колонки с 480 px и ниже — на 390 px это верно (замер 15-08: обзор стал
     вдвое короче). Но на 195 px колонка выходит ~90 px, а в плитке ещё значок 32 px и поля: под
     приписку («0 с арендой», «данных нет: версию не назвал никто») остаётся меньше её самого
     короткого слова, и плитка перестаёт ужиматься — вбок едет вся страница. Ниже 340 px
     возвращаем одну колонку: лучше длиннее, чем обрезано по краю. */
  .tiles-kpi{grid-template-columns:1fr}
  /* Строка вердикта — элемент грида (.verdict-list), а у него min-width по умолчанию auto: уже
     своего самого длинного слова строка не станет. В вердиктах попадаются имена нод и доменов,
     которые переносить негде, — поэтому разрешаем рвать их в любом месте. Остаток прокрутки на
     обзоре был +2 px, но это ПЕРВЫЙ экран панели, и «почти не едет» на нём читается как «едет». */
  .verdict-list li{min-width:0;overflow-wrap:anywhere}
}

/* ─────────────────────────────────────────────────────────────────────────────
   ЦЕЛИ НАЖАТИЯ В ТАБЛИЦАХ НА ТЕЛЕФОНЕ. Замер боевой панели 22-08 на 390 px: ссылка
   в ячейке — 88×20 (номер аккаунта), 16×15 (код направления), 18×18 (номер записи
   диагностики). Это ГЛАВНЫЙ вход внутрь: по номеру аккаунта открывают человека, по
   коду — направление. Всего таких целей нашлось 174 на шести страницах.
   Расширяем область нажатия НЕВИДИМЫМ слоем, а не отступами: страницы панели и так
   по 20–40 экранов (/issuances 28, /diag-logs 27), и +24 px на каждую строку удлинили
   бы их ещё в полтора раза. Запас до соседней цели мерен на бою — минимум 26 px при
   половине слоя 22 px, то есть на соседа слой не залезает (на телефоне строки таблиц
   раскладываются карточками, соседние ссылки стоят в 92–407 px друг от друга).
   Блок стоит в конце файла сознательно — см. ловушку каскада выше.
   ───────────────────────────────────────────────────────────────────────────── */
/* 🔴 700, а НЕ 600 — и это не округление. Карточка журнала раскладывается при `max-width:700px`
   (блок «ЯКОРЬ + ДВА ПОЛЯ» выше), а слой попадания жил на 600. В зазоре 601–700 px панель
   показывала телефонную вёрстку — карточки со ссылкой-якорем — с целью нажатия 22 px, ровно
   вдвое ниже нормы. Замер 23-08 (elementFromPoint со скроллом к элементу, /issuances):

     390 px  .cards=true   коробка 26  попадание 44   ok
     560 px  .cards=true   коробка 26  попадание 44   ok
     601 px  .cards=true   коробка 21  попадание 22   МАЛО
     700 px  .cards=true   коробка 21  попадание 22   МАЛО

   Глазами такое не видно вовсе: слой невидим по определению, и «мимо» читается как дрожь руки.
   Два числа обязаны совпадать, поэтому они и разъехались молча — за этим следит
   TestЦелиНажатияПокрываютВсюШиринуКарточек (mobile_ui_test.go).

   Почему не растим сами ссылки отступами: страницы панели и так по 20–40 экранов, и +24 px на
   строку удлинили бы их в полтора раза. Почему не 44 px кеглем: якорь — это номер аккаунта, он
   и так самый крупный в карточке; ещё крупнее он спорил бы с заголовком страницы. */
@media(max-width:700px){
  /* z-index обязателен: без него слой перекрывают ячейки СЛЕДУЮЩИХ строк — они рисуются позже,
     и нижняя половина области нажатия пропадала. Поймано замером «куда попадает палец»
     (elementFromPoint) на /directions: коробка стала 44 по ширине, а по высоте так и осталась 20. */
  td a{position:relative;z-index:1;display:inline-block;min-width:44px}
  td a::after{content:"";position:absolute;left:0;right:0;top:50%;height:44px;
    transform:translateY(-50%)}
}
@media(max-width:600px){
  /* Раскрывашка («Создать пользователя», «Как читать эту таблицу», «Что при этом в порядке») —
     24 px высотой. Здесь растим ЧЕСТНО, а не невидимым слоем: слой в 44 px накрыл бы первые
     20 px раскрытого содержимого, а там бывают ссылки. Раскрывашек на странице одна-две,
     +20 px к каждой на фоне 20–40 экранов не считаются. */
  /* :not(.nav-lbl) — обязательно. Заголовки групп в боковом меню («Люди», «Ноды», …) это тоже
     details>summary, но на телефоне они pointer-events:none, то есть подписи, а не кнопки.
     Без исключения правило растило все шесть, и ящик меню становился на 81 px длиннее ни за что
     (поймано поэлементным сравнением геометрии, глазами такое в закрытом ящике не увидеть). */
  .explain summary,details>summary:not(.nav-lbl){min-height:44px}
  /* Чип полосы разделов — 24 px при высоте самой полосы 37: по 6 px сверху и снизу принадлежат
     ПОЛОСЕ, и нажатие туда не срабатывает, хотя выглядит как часть чипа. Отдаём эти отступы
     самому чипу и добираем до нормы попадания. Полоса подрастает с 37 до 45 px; вместе с нижней
     панелью разделов (52) закреплено 11 % экрана — порог «не больше 20–30 %» соблюдён.
     🔴 scroll-padding-top двигаем ТОЙ ЖЕ правкой: якорь обязан доводить раздел ПОД полосу, и
     рассинхрон этих двух чисел — ровно дефект, который лечили 14-08. */
  .cardnav{padding-block:0}
  .cardnav a{padding-block:16px}
  html.has-cardnav{scroll-padding-top:64px}
  /* Ссылка-строка под таблицей («Все приглашения, включая сгоревшие →») мерилась 293×43 — на ОДИН
     пиксель ниже нормы. Такие промахи по одному пикселю мы за день ловили трижды (гамбургер,
     переключатель диапазона, «К содержимому»), поэтому лечим не поштучно, а правилом: ссылка,
     стоящая одна в своём абзаце, — это кнопка по смыслу, и цель у неё как у кнопки. */
  p > a:only-child{display:inline-flex;align-items:center;min-height:44px}
  /* Бейдж-ССЫЛКА («2 остывших без отметки» на /alerts) — это кнопка: по ней уходят разбирать
     тревоги. Сам по себе .badge 24 px, и как подпись это нормально, но как цель — нет.
     Растим только те бейджи, которые ссылки; подписи-бейджи рядом остаются прежними. */
  a.badge{display:inline-flex;align-items:center;min-height:44px}
  /* Галочки ФИЛЬТРОВ (категории тревог, «только неразобранные», «Активен», «показывать входы»)
     мерились 24 px при пальце 48×23 (замер 23-08: /alerts, /alerts/history, /plans, /audit).
     24 px — порог WCAG 2.5.8 AA, а норма владельца для телефона 44.
     🔴 Селектор ОБЯЗАН нести `:has(>input…)`, и это та же грабля, что расписана у td>label.cellchk
     ниже: выше по файлу стоит пол `label:has(>input[type=checkbox]){min-height:24px}`, у `:has()`
     вес считается вместе с аргументом, и лёгкое `.catbox{min-height:44px}` он перебивает. Первая
     версия правки была именно лёгкой — замер честно показал прежние 24 px на /alerts. */
  label.catbox:has(>input[type=checkbox]),
  label.chk:has(>input[type=checkbox]),
  .filterbar label.chk:has(>input[type=checkbox]){min-height:44px}
  /* Голая галочка в ячейке («канал включён», «тариф активен») — 18×18 без подписи. Обёртка
     <label class="cellchk"> делает целью 44×44: нажатие по подписи переключает галочку.
     🔴 Это правило СНАЧАЛА попало в блок ≤340 px — то есть не работало на обычном телефоне
     (390 px), и замер на бою честно показал 44×21 вместо 44×44. Цели нажатия живут ЗДЕСЬ. */
  /* 🔴 Селектор `td > label.cellchk`, и это не украшательство. Выше по файлу стоит осознанный пол
     24 px — `label:has(> input[type=checkbox])`; у `:has()` вес считается вместе с аргументом, и
     это правило оказывается ТЯЖЕЛЕЕ, чем `.cellchk` и даже чем `label.cellchk`. Обе лёгкие версии
     я выкатывал и обе замер на бою показал как 44×21 при написанном 44×44. Победивший вариант
     выбран не рассуждением о весах, а прогоном трёх кандидатов на стенде. */
  td > label.cellchk{min-width:44px;min-height:44px}
}


/* Кнопка «Наверх» на длинных страницах (23-08). Ставит её admin.js и только там, где страница
   длиннее шести экранов, — см. комментарий у него. Правый нижний угол: левый на телефоне занят
   большим пальцем при листании, а середина перекрывает содержимое.
   ⚠️ Отступ снизу на телефоне считает нижнее меню (.mobilenav, 56 px + safe-area): без него кнопка
   ложится ровно на «Ещё» и первый же промах уводит человека в меню вместо начала страницы. */
.totop{position:fixed;right:16px;bottom:24px;z-index:17;width:44px;height:44px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  background:var(--surface);color:var(--txt);border:1px solid var(--line);
  box-shadow:0 2px 10px rgba(0,0,0,.45);font-size:18px;line-height:1}
.totop:hover{border-color:var(--cyan-7);color:var(--cyan-4)}
.totop[hidden]{display:none}
@media(max-width:600px){
  .totop{bottom:calc(68px + env(safe-area-inset-bottom,0px))}
}

/* .taplink — ссылка ВНУТРИ фразы, которую нажимают как кнопку («смотреть журнал →»). Обычная
   строчная ссылка получает высоту строки (на 390 px это 43 px — промах по норме 44 на один
   пиксель, замер 23-08 на /admins). Отдельный класс, а не общее правило на все ссылки в абзацах:
   раздутая строка внутри текста рвёт абзац, а таких ссылок в панели десятки. */
.taplink{display:inline-flex;align-items:center;min-height:44px}

/* .jumpline — СТРОКА ССЫЛОК-ПЕРЕХОДОВ («Подробнее: устройства (2) · выдачи (42) · платежи (0) ·
   трафик · группа · источник»). Это не текст со ссылками, а оглавление: им прыгают по карточке
   человека, самой длинной странице панели (12 951 px на десктопе, 27 024 px на телефоне —
   замер 25-08). Область нажатия у них была 20–21 px при норме 44.
   Почему не `.taplink` на каждую: `.taplink` действует на ЛЮБОЙ ширине, а на десктопе эти абзацы
   попадают мышью и без запаса — 44 px там только разогнали бы и без того длинную страницу.
   Почему не общее правило на ссылки в абзаце: `p > a:only-child` (оно есть ниже) берёт ссылку,
   стоящую в абзаце ОДНУ, а здесь их шесть. */
@media(max-width:600px){
  .jumpline a{display:inline-flex;align-items:center;min-height:44px}
}

/* Числа — по ПРАВОМУ краю и табличными цифрами. Столбик сумм и счётчиков сравнивают глазом, а при
   левом выравнивании «199 ₽» и «1 299 ₽» начинаются в одной точке, и разряды друг под другом не
   стоят: чтобы понять, что больше, приходится читать (ревью панели 23-08, п. 11).
   На телефоне правило не нужно и не действует: в карточке значение и так уходит вправо флексом, а
   правило `table.cards tbody td{text-align:left}` тяжелее по весу — проверено. */
th.num,td.num{text-align:right;font-variant-numeric:tabular-nums}

/* ИНФРАБИЛЛИНГ НОД (карточка /nodes/{id}, миграция 0247): полоса «сколько квоты хостера съедено».
   Рисуется SVG из Go (quotaBarSVG), а не инлайновым style="width:…": на бою стоит CSP
   `style-src 'self'`, и такой стиль браузер молча отбрасывает — полоса при любом расходе осталась
   бы пустой, то есть блок врал бы в самую опасную сторону.
   preserveAspectRatio="none" в разметке: тянем прямоугольники по ширине карточки, пропорции тут
   не нужны — это шкала, а не рисунок. */
.quota-bar{display:block;width:100%;height:10px;border-radius:5px;overflow:hidden;margin:2px 0 10px}
.qb-bg{fill:var(--panel2)}
.qb-fill.lvl-ok{fill:var(--ok)}
.qb-fill.lvl-warn{fill:var(--warn)}
.qb-fill.lvl-over{fill:var(--bad)}
/* Засечка порога тревоги: без неё «сколько ещё можно» приходится считать в уме. */
.qb-mark{fill:var(--txt);opacity:.55}
.quota-sum{margin:0 0 12px}
.quota-sum .lvl-ok{color:var(--ok)}
.quota-sum .lvl-warn{color:var(--warn)}
.quota-sum .lvl-over{color:var(--bad-ink)}
/* Форма квоты стоит В ТОЙ ЖЕ карточке, что показ: разделитель, чтобы «что есть» не слипалось с
   «что поменять». */
.quota-form{border-top:1px solid var(--line);margin-top:14px;padding-top:12px}
/* Приписка рядом с процентом («квота выбрана целиком», «порог 80 %»). НЕ `.alert`: тот рисует
   плашку-коробку, и внутри строки она наезжает на соседний текст (стенд 21-09). */
.quota-note{font-weight:500}
.quota-note.lvl-over{color:var(--bad-ink)}
/* .nobr — одно слово, которое сверяют глазами целиком: версия Xray (vX.Y.Z) в разделе «X-ray» и
   подпись падения (16 hex) на «Падениях». Общее правило .mono (`overflow-wrap:anywhere`, см. выше)
   в тесной таблице @1280 рвало его пополам: «v26.9» / «.9», «5eed000000000» / «001» (снимки стенда
   24-09) — читалось как два разных значения. Оба короче 17 знаков: на 390 в карточке влезают и без
   переноса. */
.mono.nobr{white-space:nowrap}

/* МЕТКИ ЛЮДЕЙ (SPEC-0063). Цвет — ТОЛЬКО по ключу палитры (cprepo.LabelColors, CHECK миграции 0258):
   класс собирается из ключа, произвольная строка из формы до разметки не доходит. Пилюля, а не
   прямоугольник .badge (и не .lbl — это подпись плитки, 27-09 столкновение имён растянуло плитки на 390 px): бейдж в панели значит СОСТОЯНИЕ (в работе/выключен), метка — принадлежность,
   и путать одно с другим на одной строке списка нельзя. Контраст текста к фону — ≥ 4.5 на тёмной теме. */
.metka{display:inline-block;white-space:nowrap;font-size:12px;line-height:1.5;padding:1px 9px;border-radius:999px;
  font-weight:600;border:1px solid currentColor;background:var(--panel)}
.metka-gray{color:#b1bac4}.metka-blue{color:#79c0ff}.metka-green{color:#7ee787}.metka-yellow{color:#e3b341}
.metka-orange{color:#ffa657}.metka-red{color:#ff7b72}.metka-purple{color:#d2a8ff}.metka-teal{color:#5eead4}
.metki{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}
.metka-chip{display:inline-flex;align-items:center;gap:2px}
.metka-chip form{display:inline}
.metka-x{background:none;border:0;color:var(--muted);cursor:pointer;font-size:15px;line-height:1;padding:0 4px;min-width:28px;min-height:28px}
.metka-x:hover{color:var(--bad-ink)}
.label-edit{display:inline-block}
.label-edit[open]{display:block;margin-top:6px}
.label-edit form{margin-top:8px}
.labels-actions{text-align:right}
@media (max-width:720px){
  .metka-x{min-width:44px;min-height:44px}
  .labels-actions{text-align:left}
}
.metki-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px}
.metki-add{display:inline-flex;gap:6px;align-items:center}
.metki-add select{min-width:9em}

/* ТЕПЛОВАЯ КАРТА ВЫХОДА В СЕТЬ (/connect-heatmap, 02-10). Сетка «день недели × час»: клетка красится
   пятью ступенями одного цвета — тем же cyan-9, что у главной кнопки (--primary), разной
   прозрачностью поверх тёмной карточки. Ступень выбирает КОД (heatLevel в connectheatmap.go), а не
   инлайновый style: на бою CSP `style-src 'self'`, инлайновый стиль там молча не применяется.
   Контраст: на ступенях 1–3 светлый текст на почти тёмной подложке (6+), на 4–5 — белый на
   насыщенном cyan (5,6 и выше). Имена с приставкой hm-: до 02-10 в файле такой не было (в
   прошлый раз `.lbl` вдруг совпал с подписью плитки — свободное имя проверено grep'ом). */
table.hm{width:auto;border-collapse:separate;border-spacing:3px;margin:0}
table.hm th,table.hm td{padding:0;border:0;text-align:center}
table.hm thead th{position:static;background:none;box-shadow:none;text-transform:none;letter-spacing:0;
  font-size:11px;font-weight:500;color:var(--muted);padding:0 0 2px}
table.hm th.hm-day{text-align:left;padding-right:8px;white-space:nowrap;font-size:13px;
  font-weight:500;text-transform:none;letter-spacing:0;color:var(--txt)}
table.hm thead th.hm-day{font-size:11px;color:var(--muted)}
table.hm tbody tr:hover{background:none}
table.hm td.hm-c{min-width:28px;height:30px;border-radius:4px;font-size:12px;
  font-variant-numeric:tabular-nums;color:var(--txt);background:rgba(255,255,255,.04)}
table.hm td.hm-sum,table.hm th.hm-sum{min-width:34px;padding-left:6px;font-size:12px;color:var(--muted);
  font-variant-numeric:tabular-nums;text-align:right}
table.hm tbody tr:last-child td{border-bottom:0}
.hm-l1,table.hm td.hm-l1{background:rgba(11,114,133,.22)}
.hm-l2,table.hm td.hm-l2{background:rgba(11,114,133,.40)}
.hm-l3,table.hm td.hm-l3{background:rgba(11,114,133,.60)}
.hm-l4,table.hm td.hm-l4{background:rgba(11,114,133,.82)}
.hm-l5,table.hm td.hm-l5{background:var(--primary)}
table.hm td.hm-l4,table.hm td.hm-l5{color:#fff}
.hm-legend{display:flex;align-items:center;gap:4px;margin:10px 0 0}
.hm-legend span:first-child{margin-right:4px}
.hm-legend span:last-child{margin-left:4px}
.hm-key{display:inline-block;width:18px;height:18px;border-radius:4px;border:1px solid var(--line)}
/* Строка таблицы, на которую смотрят в первую очередь (транзит РФ): тонкая подсветка слева. */
tr.hm-rowhl td:first-child{box-shadow:inset 3px 0 0 var(--warn);padding-left:10px}
@media (max-width:720px){
  table.hm{border-spacing:2px}
  table.hm td.hm-c{min-width:26px;height:28px}
}
