/* app.css — раскладка поверх дизайн-системы (ds.css).
   Внешний вид — лицо программы (Archivo, красный #ec3013, нулевой радиус,
   набор компонентов) — задан в ds.css и здесь не меняется. Здесь только
   то, чего в системе нет: шкала кегля, сетка экранов, график и мелочи
   поведения. */

/* ═══════════════════════════════════════════════════════════════════════
   ШКАЛА КЕГЛЯ
   ═══════════════════════════════════════════════════════════════════════
   Замер 22.08.2026: во всей программе 19 разных кеглей и НИ ОДНОГО в
   rem — 13 в ds.css, 13 в app.css (в том числе половинные 12,5 и 13,5),
   10 в инлайновых стилях, 104 объявления, из них 66 — одно и то же
   «13px». На ОДНОМ экране отбора зондов одновременно жило 17 сочетаний
   кегль/насыщенность. Следствия два, и оба замерены:

     * ритма нет — размер выбирался на глаз в каждом месте отдельно, и
       два соседних блока расходились на полпикселя без причины;
     * настройка «крупный шрифт» в браузере и в системе НЕ МЕНЯЕТ
       НИЧЕГО: пиксель к ней не привязан вовсе. Для врача-лаборанта,
       который смотрит в этот экран весь день, это не мелочь.

   Ниже шесть ступеней текста плюс три плакатных из дизайн-системы
   (25/32/42 у h3/h2/h1). Значения — те же самые, что были; меняется
   ЕДИНИЦА. `rem` считается от КОРНЯ документа, а не от родителя,
   поэтому вложенность кегль не множит, а настройка браузера работает.

   ЭТО ДОПОЛНЕНИЕ, А НЕ ПОДМЕНА ds.css. Файл дизайн-системы не тронут;
   здесь объявлены токены и те же его правила переписаны в rem ТЕМ ЖЕ
   ВЕСОМ СЕЛЕКТОРА (`body` против `body`, `.btn` против `.btn`), а не
   `!important` и не более сильным селектором. При настройках браузера
   по умолчанию картинка не меняется — каждое значение подписано в
   пикселях, сверять глазами не нужно.
   ═══════════════════════════════════════════════════════════════════ */
:root {
  --fs-label:   0.6875rem;  /* 11 — капитель: шапка таблицы, тег, кикер */
  --fs-caption: 0.75rem;    /* 12 — подпись поля, приписка под значением */
  --fs-sm:  0.8125rem;  /* 13 — вспомогательный текст, плотные таблицы */
  --fs-body:   0.9375rem;  /* 15 — базовый: тело, кнопки, поля, таблицы */
  --fs-lg: 1.0625rem;  /* 17 — большая кнопка, заголовок карточки */
  --fs-section:  1.25rem;    /* 20 — заголовок раздела и диалога */

  /* Плакатные ступени — лицо, заданное дизайнером. Числа не тронуты. */
  --fs-h3: 1.5625rem;       /* 25 */
  --fs-h2: 2rem;            /* 32 */
  --fs-h1: 2.625rem;        /* 42 */
}

body { font-size: var(--fs-body); }
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); }
h4 { font-size: var(--fs-section); }
h5 { font-size: var(--fs-lg); }
h6 { font-size: var(--fs-sm); }
figcaption { font-size: var(--fs-label); }

/* Компоненты дизайн-системы — те же размеры, но в rem.
   `.btn`, `.input`, `.table`, `.radio`, `.dialog-body` стояли на 14px, а
   тело документа на 15px. Разницы в один пиксель не видит никто, а
   лишняя ступень в шкале от неё есть — и именно она разводила соседние
   блоки. Сведены к базовой.
   Комментарий в ds.css у `.btn` объясняет 14px тем, что «совпадает с
   .input» — то есть это правило ПАРНОСТИ, а не ступень шкалы. Парность
   сохранена: обе на базовой. */
.btn { font-size: var(--fs-body); }
.input { font-size: var(--fs-body); }
.radio { font-size: var(--fs-body); }
.table { font-size: var(--fs-body); }
.dialog-body { font-size: var(--fs-body); }
.nav a { font-size: var(--fs-body); }
.seg-opt { font-size: var(--fs-sm); }
.card-body { font-size: var(--fs-sm); }
.card-title { font-size: var(--fs-lg); }
.dialog-title { font-size: var(--fs-section); }
.field > label { font-size: var(--fs-caption); }
.table th { font-size: var(--fs-label); }
.tag { font-size: var(--fs-label); }
.card-meta { font-size: var(--fs-label); }
.card-kicker { font-size: var(--fs-label); }

.nav { padding: 18px 48px; gap: 32px; position: sticky; top: 0; z-index: 20;
       background: var(--color-bg); }
/* — знак программы в шапке —
   Знак стал ссылкой (был `div`), и на него теперь ЛОЖИТСЯ `.nav a` из
   ds.css весом (0,1,1), а прежнее `.nav-brand` весило (0,1,0): кегль
   молча уехал бы с 20 px на 15. Спорим тем же приёмом, что и везде в
   этом файле (ловушка про `.table th.num`) — берём селектор глубже,
   (0,2,1), ds.css не трогается. Кегль виден только в запасном виде,
   когда картинка не загрузилась и на её месте стоит подпись.

   `margin-right: auto` СЮДА НЕ ПЕРЕНОСИТСЯ. Он приходит из ds.css весом
   (0,1,0), и ровно тем же весом его снимает мобильный медиазапрос ниже.
   Объяви его здесь — (0,2,1) перебьёт (0,1,0), правая часть шапки на
   телефоне потеряет свою строку и полоса разъедется.

   Обводку фокуса даёт общее `:focus-visible` из ds.css (2 px акцентом,
   отступ 2 px). Своего правила здесь нет намеренно: дублировать нечего,
   а `.nav a { text-decoration: none }` обводку не снимает. */
.nav a.nav-brand { display: flex; align-items: center; flex-shrink: 0;
                   font-size: var(--fs-section); letter-spacing: -0.01em; }
/* `picture` по умолчанию строчный: без этого под картинкой остаётся
   щель под нижние выносные и шапка выше на пару пикселей. */
.nav-brand picture { display: flex; flex-shrink: 0; }
/* Высота задана, ширина считается — пропорции обоих файлов разные
   (900×192 и 384×353), и фиксировать надо ровно одну сторону.
   object-fit и max-width не дают логотипу сжиматься при нехватке места. */
.nav-brand .brand-logo { display: block; height: 32px; width: auto;
                         max-width: 168px; object-fit: contain;
                         flex-shrink: 0; }
.nav-tabs { display: flex; align-items: center; gap: var(--space-2);
            margin-left: calc(var(--space-3) * -1); flex-shrink: 0; }
.nav-tabs .btn[aria-current="page"] { color: var(--color-accent-700); }
.nav-tabs .btn:not([aria-current="page"]) { color: var(--color-neutral-700); }
/* ШАПКА ПЕРЕНОСИТСЯ ПО СТРОКАМ.
   Замер при окне 375 px: `.nav-right` требовала 438 px и переноса не
   имела — шесть элементов подряд без `flex-wrap`. Медиазапрос на 1120 px
   добавлял перенос только внешней `.nav`, а внутренняя полоса всё равно
   вылезала за экран: ширина прокрутки документа 462 при видимых 375.
   Заодно высота шапки была 163 px — пятая часть экрана телефона,
   занятая тем, ради чего сюда не заходят. */
.nav-right { display: flex; align-items: center; flex-wrap: wrap;
             justify-content: flex-end; gap: var(--space-2) var(--space-4); }
.nav-note { display: flex; align-items: center; gap: var(--space-2);
            font-size: var(--fs-sm); color: var(--color-neutral-700); }
.nav-note.bad { color: var(--color-accent-700); }
.nav-sep { height: 20px; width: 1px; background: var(--color-divider); }

main { padding: 40px 48px 64px; display: flex; flex-direction: column; gap: 28px;
       max-width: 1920px; margin: 0 auto; min-width: 0; }
.loading { color: var(--color-neutral-700); }

/* Вход / регистрация / восстановление: карточка по центру оставшейся
   высоты. Навбар липкий сверху и в поток входит — вычитаем его из
   min-height. Внутренний `.auth-stack` с `margin-block: auto` центрирует
   пачку, а если форма выше окна — отступы схлопываются, верх не обрезается. */
#screen:has(> .auth-screen) {
  padding-top: 12px;
  padding-bottom: 24px;
  gap: 0;
}
.auth-screen {
  box-sizing: border-box;
  width: 100%;
  min-height: calc(100dvh - 5.5rem);
  display: flex;
  flex-direction: column;
  align-items: center;
}
.auth-stack {
  margin-block: auto;
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.auth-consent {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  font-size: var(--fs-sm);
  color: var(--color-neutral-700);
  line-height: 1.4;
}
.auth-consent-label {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
}
.auth-consent input[type="checkbox"] {
  flex: none;
  width: 16px;
  height: 16px;
  margin-top: 3px;
}
.legal-block {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  font-size: var(--fs-sm);
  color: var(--color-neutral-700);
  padding-top: 8px;
  border-top: 1px solid var(--color-divider);
}
.legal-block-wide { max-width: 640px; }
.legal-kicker {
  margin-top: 10px;
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent);
}
.legal-kicker:first-child { margin-top: 0; }
.legal-links { margin-top: 10px; }
.legal-page {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 720px;
  width: 100%;
}
.legal-p {
  margin: 0;
  font-size: var(--fs-body);
  color: var(--color-neutral-700);
  line-height: 1.45;
}
.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 12px 20px;
  padding: 12px 24px calc(12px + env(safe-area-inset-bottom, 0px));
  background: var(--color-surface);
  border-top: 2px solid var(--color-divider);
  box-shadow: var(--shadow-sm);
}
.cookie-banner-text {
  font-size: var(--fs-sm);
  color: var(--color-neutral-700);
  max-width: 52rem;
}
body:has(.cookie-banner) { padding-bottom: 4.5rem; }

.head-row { display: flex; align-items: flex-end; justify-content: space-between;
            gap: var(--space-4) 40px; flex-wrap: wrap; }
.sub { font-size: var(--fs-body); color: var(--color-neutral-700); }
.row { display: flex; align-items: center; gap: var(--space-3); }
/* `min-width: 0` — чтобы широкое содержимое (таблица, график, длинная
   строка последовательности) распирало СВОЙ контейнер, а не страницу:
   у элемента сетки и флекса умолчание `min-width: auto`, то есть «не
   уже своего min-content». */
.col { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.spread { justify-content: space-between; }
.grow { flex: 1; }

/* Заголовок раздела внутри колонки или строки: отбивку задаёт раскладка
   (`gap`), а не поле у заголовка. Иначе они складываются — 8 px от
   ds.css плюс 12 px от `.col`, — и в шести местах это лечилось
   инлайновым `marginBottom: 0`, то есть по одному месту за раз. Вес
   (0,1,1) против (0,0,1) у ds.css: спорим весом, а не порядком. */
.col > h4, .row > h4 { margin: 0; }

/* — плашка «ждёт вашего решения» — */
.banner { display: flex; align-items: center; gap: var(--space-4);
          padding: 18px 22px; flex-wrap: wrap;
          background: var(--color-accent-100); border: 2px solid var(--color-accent); }
.banner-title { font-family: var(--font-heading); font-weight: 800;
                font-size: var(--fs-lg); color: var(--color-accent-800); }
.banner-body { font-size: var(--fs-body); color: var(--color-accent-800); opacity: .85; }

/* — таблица работ —
   Строка кликабельна целиком, а не только название: попасть в короткую
   ссылку мышью тяжело, и это была прямая жалоба. Вложенные кнопки
   останавливают всплытие, чтобы «Удалить» не открывало работу. */
.works td { padding: var(--space-4) 14px; }
.works th { font-size: var(--fs-caption); padding: var(--space-3) 14px; }
.works tbody tr { cursor: pointer; }
.works tbody tr:hover { background: color-mix(in srgb, var(--color-accent) 7%, transparent); }
.works tbody tr:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.works .name { font-size: var(--fs-lg); font-family: var(--font-heading);
               font-weight: 800; }
.works .rowlinks { display: flex; flex-wrap: wrap; gap: var(--space-1) 10px;
                   font-size: var(--fs-caption); color: var(--color-neutral-700); }

/* СТРОКА «ЖДЁТ ВАШЕГО РЕШЕНИЯ» ПОДСВЕЧЕНА КЛАССОМ, А НЕ ИНЛАЙНОМ.
   Фон стоял присвоением `tr.style.background`, а инлайновый стиль имеет
   вес (1,0,0,0) и выигрывает у ЛЮБОГО селектора. Итог: единственная
   строка, ради которой человек и открыл список, была единственной,
   которая не подсвечивалась под курсором, — то есть выглядела как
   ненажимаемая. Правило `.works tbody tr:hover` (0,2,1) и правило ниже
   (0,2,1) одного веса, поэтому наведение отдельно объявлено весом
   (0,3,1): равный вес разрешался бы порядком, а это не разрешение. */
.works tbody tr.important { background: var(--color-accent-100); }
.works tbody tr.important:hover {
  background: color-mix(in srgb, var(--color-accent) 16%, transparent);
}
/* — числа в таблицах —
   ЗАГОЛОВОК ОБЯЗАН СТОЯТЬ НАД СВОИМ СТОЛБЦОМ.
   `.table th` в дизайн-системе задаёт выравнивание влево, и по силе
   селектора (0,1,1) он побеждает одиночный `.num` (0,1,0). Из-за этого
   у КАЖДОГО числового столбца во всей программе подпись стояла слева, а
   числа — справа: глазом это читается как «числа съехали». Разбираться
   в таком столбце невозможно, потому что глаз ведёт от подписи вниз и
   попадает мимо.
   Пишем `.table th.num` (0,2,1) — этого хватает, чтобы перебить, и не
   приходится трогать дизайн-систему.
   `.mono` выравнивание НЕ задаёт намеренно: им помечены и числа, и
   текстовые ячейки вроде «зонд 5 · пара 0», которым правый край не
   нужен. Табличные цифры при этом нужны обоим: без них знаки разной
   ширины пляшут между строками. */
.num, .table th.num, .table td.num {
  text-align: right; font-variant-numeric: tabular-nums;
}
.mono { font-variant-numeric: tabular-nums; }

/* Прижать к правому краю то, что числом НЕ является: кнопку действия в
   конце строки. Через `.num` это делать нельзя — он включает табличные
   цифры и говорит читающему код, что в ячейке число. */
.right, .table th.right, .table td.right { text-align: right; }

/* — сортируемые заголовки таблицы (экран праймеров) —
   Кнопка занимает ячейку ЦЕЛИКОМ: щелчок мимо слова «Продукт», но внутри
   ячейки, обязан сортировать так же. Отбивки и рамка остаются на `th`
   (у него их задал ds.css), кнопка получает `padding: 0`.
   Стрелки: активная колонка несёт плотную «↑» / «↓» цвета акцента,
   остальные сортируемые — бледную «↕»: видно, что кликабельно, ещё до
   наведения. ds.css не трогается намеренно — он общий с макетами
   дизайнера. */
.table th.sortable { padding: 0; }
.th-sort {
  display: flex; align-items: baseline; gap: 5px; width: 100%;
  /* Отбивки повторяют `.table.compact th` (8 px / 10 px): шапка
     сортируемых столбцов встаёт ровно на одну линию с несортируемыми
     ячейками той же таблицы. */
  padding: var(--space-2) 10px;
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; text-align: inherit;
  background: none; border: 0; cursor: pointer;
}
.table th.num .th-sort { justify-content: flex-end; }
.th-sort:hover { color: var(--color-text); }
.th-sort:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.th-arrow { flex: none; font-size: 12px; line-height: 1; }
.th-arrow.idle { opacity: 0.35; }
.th-sort:hover .th-arrow.idle { opacity: 0.7; }
.th-arrow.on { color: var(--color-accent-700); font-weight: 700; }

/* — знак вопроса у непонятного (задача 10) —
   Маленький, но с нормальной областью нажатия: палец по 24-пиксельной
   кнопке не промахивается, а глазу она не спорит с числом рядом.
   Ставится в строку подписи показателя (`справка:` в ui.показатель). */
.hint-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; margin-left: 6px; padding: 0;
  font: inherit; font-size: 11px; font-weight: 700; line-height: 1;
  color: var(--color-neutral-700);
  background: none; border: 1px solid var(--color-divider);
  border-radius: 50%; cursor: pointer;
}
.hint-btn:hover { color: var(--color-accent-700); border-color: var(--color-accent); }
.hint-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* — экран отбора зондов —
   ЭЛЕМЕНТ СЕТКИ НЕ ИМЕЕТ ПРАВА БЫТЬ ШИРЕ СВОЕЙ ДОРОЖКИ.
   Замер на свежей загрузке `#/probes/met_1` при окне 520 px: дорожка
   сетки 549,875 px против самой сетки 492 px, ширина прокрутки
   документа 566 при видимых 520, график растянут в 1,118 раза. Мерил
   себя график правильно — раздувал дорожку блок «Что будет при других
   настройках», у которого min-content 550 px, и раздувал уже ПОСЛЕ
   замера. Причина общая: `1fr` это `minmax(auto, 1fr)`, а `auto` в
   минимуме означает `min-content`. `minmax(0, 1fr)` разрешает дорожке
   быть уже содержимого, а `min-width: 0` у `.chart-wrap` — тому, что
   внутри неё. Нужны оба: первое лечит дорожку, второе — её содержимое. */
.probes-grid { display: grid; grid-template-columns: 320px minmax(0, 1fr);
               gap: 28px; align-items: start; }
.knobs { display: flex; flex-direction: column; gap: 22px; position: sticky;
         top: 92px; min-width: 0; }
.knob-name { font-family: var(--font-heading); font-weight: 800;
             font-size: var(--fs-body); margin-bottom: var(--space-2); }
/* Название ручки — теперь настоящая ПОДПИСЬ поля (`поле()` выдаёт `for`
   и `id`), а не заголовок рядом с ним. Вид оставлен прежний, тот же, что
   у `.knob-name`.
   Селектор с ПРЯМЫМ потомком: у «Длины зонда» внутри лежат ещё два поля
   («от», «до»), и их подписи обязаны остаться мелкими — они называют
   часть ручки, а не ручку. Вес (0,2,1) против `.field > label` из
   дизайн-системы (0,1,1). */
.knobs > .field > label { font-family: var(--font-heading); font-weight: 800;
                          font-size: var(--fs-body); color: var(--color-text);
                          margin-bottom: var(--space-2); }
.knob-value { font-variant-numeric: tabular-nums; font-size: var(--fs-body);
              margin-top: 6px; }
.knob-auto { font-size: var(--fs-caption); color: var(--color-accent-700);
             margin-top: 6px; }
/* ОТСТУП МЕЖДУ СОСЕДЯМИ ЗАДАЁТ РАСКЛАДКА, А НЕ ПОЛЕ.
   Здесь была попытка отбить два поля «Длины зонда» («от» и «до») правилом
   `.knobs .field + .field { margin-top }`. Замер сразу показал, зачем это
   правило проекта: селектор попадал и в САМИ ручки, у которых просвет уже
   задан `gap: 22px` у `.knobs`, — получилось 30 px между ручками вместо 22
   и 22 перед первой. Сузив селектор до вложенных, получили то же удвоение
   этажом ниже: 6 px от `gap` плюс 8 px поля = 14.
   Правильный ответ — не поле, а `gap` у контейнера этих двух полей
   (`probes.js`, ручка «Длина зонда»). Правила здесь нет намеренно. */

.stats { display: flex; gap: 28px; flex-wrap: wrap; }
.stat-label { font-size: var(--fs-caption); color: var(--color-neutral-700); }
/* ОДИН ПОКАЗАТЕЛЬ — ОДИН ВИД.
   Плитка «подпись + значение» была собрана вручную на шести экранах и
   разошлась по кеглю: 15/400 на экране участка (класс `.stat-value` там
   не брался вовсе), 22/800 на геле (инлайновая перебивка), 26/800 у
   остальных пяти. Теперь плитку выдаёт `показатель()` из `ui.js`, а
   размер живёт здесь и только здесь. Ступень — плакатная h3: значение
   показателя и есть заголовок, только числом. */
.stat-value { font-family: var(--font-heading); font-weight: 800;
              font-size: var(--fs-h3);
              font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat-value.warn { color: var(--color-accent-700); }

/* — график —
   touch-action: none нужен, чтобы на тачпаде жест не уводил страницу.
   Само предотвращение прокрутки колесом делается в chart.js через
   addEventListener(..., {passive:false}); одним CSS его не добиться. */
.chart-wrap { background: var(--color-surface); border: 1px solid var(--color-divider);
              min-width: 0; }
.chart-bar { display: flex; align-items: center; gap: var(--space-2);
             padding: 10px 14px;
             border-bottom: 1px solid var(--color-divider); flex-wrap: wrap; }
.chart-hint { margin-left: auto; font-size: var(--fs-caption);
              color: var(--color-neutral-700); }
.chart-svg { display: block; width: 100%; touch-action: none; user-select: none;
             cursor: grab; }
.chart-svg.panning { cursor: grabbing; }
.chart-svg.adding { cursor: copy; }

.legend { display: flex; gap: 18px; flex-wrap: wrap; padding: 10px 14px;
          font-size: var(--fs-caption); color: var(--color-neutral-800);
          border-top: 1px solid var(--color-divider); }
.legend i { display: inline-block; width: 14px; height: 10px; margin-right: 6px;
            vertical-align: -1px; }

/* Разбор под курсором: место, число совпадений, зонд, экзон.
   Отдельной строкой, а не подсказкой поверх картинки: та закрывала бы
   как раз то, на что смотрят, и прыгала бы за мышью. Высота задана,
   чтобы график не дёргался, когда строка пустеет. */
.chart-readout { min-height: 34px; display: flex; align-items: center;
                 padding: var(--space-2) 14px; font-size: var(--fs-sm);
                 font-variant-numeric: tabular-nums;
                 color: var(--color-neutral-800);
                 border-top: 1px solid var(--color-divider); }

/* Пока экзоны не пришли (нет сети, участок задан координатами), не
   обещаем их в легенде. */
.chart-wrap.no-exons .legend-exons { display: none; }

/* — пробелы и таблицы последствий — */
.gap-item { padding: 10px 0; border-bottom: 1px solid var(--color-divider);
            font-size: var(--fs-sm); }
.gap-why { color: var(--color-neutral-700); font-size: var(--fs-caption); }
.table.compact td, .table.compact th { padding: var(--space-2) 10px;
                                       font-size: var(--fs-sm); }
/* Шапка плотной таблицы остаётся капителью: кегль у неё свой, и
   `.table.compact th` (0,2,1) его бы перебил. */
.table.compact th { font-size: var(--fs-label); }
.table tr.current { background: var(--color-accent-100); }

/* ШИРОКОЕ СОДЕРЖИМОЕ ЕЗДИТ ВБОК САМО, А НЕ ВЕЗЁТ С СОБОЙ СТРАНИЦУ.
   Таблица пар — семь столбцов и три сотни строк; таблица результатов —
   столько же. Раньше прокрутка внутри себя включалась только ниже
   900 px, а выше этого порога широкая таблица распирала `main` и
   уводила вбок весь экран вместе с шапкой. */
.table-wrap { overflow-x: auto; max-width: 100%; }

/* Длина праймера в подписи пары: она различает пары с одним номером,
   поэтому остаётся читаемой, но не спорит по яркости с самим номером.
   Вес (0,1,0) — `.table td` цвет не задаёт вовсе, спорить не с чем. */
.pair-len { color: var(--color-neutral-700); }

/* — ошибки: что случилось / почему / что делать — */
.problem { border-left: 4px solid var(--color-accent); padding: 14px 18px;
           background: var(--color-accent-100); }
.problem.warn { border-color: var(--color-accent-2); background: var(--color-accent-2-100); }
.problem.ok { border-color: var(--color-neutral-400); background: var(--color-neutral-100); }
.problem-what { font-family: var(--font-heading); font-weight: 800; font-size: var(--fs-body); }
.problem-why { font-size: var(--fs-sm); color: var(--color-neutral-800); }
.problem-todo { font-size: var(--fs-sm); }
details.tech { margin-top: 8px; font-size: var(--fs-caption); color: var(--color-neutral-700); }
details.tech pre { white-space: pre-wrap; word-break: break-word; margin: 6px 0 0; }

/* — уведомления — */
#toast-host { position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
              display: flex; flex-direction: column; gap: 8px; z-index: 50; }
.toast { display: flex; align-items: center; gap: 16px; padding: 12px 18px;
         background: var(--color-neutral-900); color: var(--color-neutral-100);
         font-size: var(--fs-body); box-shadow: var(--shadow-lg); }
.toast .btn { color: var(--color-accent-400); }

/* — пустые состояния — */
.empty { display: flex; flex-direction: column; align-items: flex-start; gap: 12px;
         padding: 56px 0; }
.empty-title { font-family: var(--font-heading); font-weight: 800; font-size: var(--fs-section); }

/* — просмотрщик ПЦР-продукта —
   ЧТО ПОПАДАЕТ В БУФЕР ПРИ ВЫДЕЛЕНИИ МЫШЬЮ.
   Выделение куска последовательности давало кашу: номера позиций слева и
   обратная цепь вперемешку с прямой. Вставить такое в BLAST или в заказ
   нельзя — надо чистить руками, строка за строкой.
   `user-select: none` исключает текст из копируемого выделения (тем же
   приёмом сделаны номера строк в GitHub). Номера и обратная цепь —
   производные: первое можно прочитать глазами, второе однозначно
   восстанавливается из прямой цепи. Обе целиком отдают кнопки в шапке
   панели, там же FASTA. */
.seq { font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
       font-size: var(--fs-sm); line-height: 1.45; white-space: pre; overflow-x: auto; }
.seq .pos { color: var(--color-neutral-600);
            user-select: none; -webkit-user-select: none; }
.seq .rev { color: var(--color-neutral-700);
            user-select: none; -webkit-user-select: none; }
.seq mark { background: var(--color-accent-200); color: inherit; }
.seq .rev mark { background: var(--color-accent-2-200); }

/* — полоса шагов —
   Экраны сами по себе были тупиками, поэтому путь работы виден всегда:
   сверху где мы, снизу что делать дальше. */
.steps { display: flex; align-items: center; gap: 0; flex-wrap: wrap;
         padding: 12px 0; }
.step { display: flex; align-items: center; gap: 8px; padding: 8px 14px;
        border: 1px solid var(--color-divider); background: var(--color-bg);
        font-family: var(--font-heading); font-weight: 800; font-size: var(--fs-sm);
        color: var(--color-neutral-700); cursor: default; }
button.step { cursor: pointer; }
button.step:hover:not(:disabled) { background: var(--color-accent-100); }
.step:disabled { opacity: .5; }
.step-mark { display: inline-grid; place-items: center; width: 20px; height: 20px;
             border: 1px solid currentColor; font-size: var(--fs-label); }
.step-done { color: var(--color-accent-700); border-color: var(--color-accent-700); }
.step-done .step-mark { background: var(--color-accent-700);
                        color: var(--color-bg); border-color: transparent; }
.step-now { color: var(--color-accent); border-color: var(--color-accent);
            border-width: 2px; background: var(--color-accent-100); }
.step-locked { color: var(--color-neutral-500); }
/* Шаг, на котором человек стоит прямо сейчас. Заливка, а не рамка: у
   законченной работы все пять шагов зелёные с галочками, и тонкая рамка
   среди них не читается. */
.step-here { background: var(--color-text); color: var(--color-bg);
             border-color: var(--color-text); cursor: default; }
.step-here .step-mark { background: var(--color-bg); color: var(--color-text);
                        border-color: transparent; }
.step-you { font-size: var(--fs-label); letter-spacing: 0.08em; text-transform: uppercase;
            opacity: .75; font-weight: 400; margin-left: 2px; }
.step-link { width: 18px; height: 1px; background: var(--color-divider); }

/* — кнопка следующего действия — */
.next-bar { display: flex; align-items: center; gap: 20px; padding: 18px 22px;
            border: 2px solid var(--color-accent);
            background: var(--color-accent-100); margin-top: 8px; }
.next-bar.done { border-color: var(--color-neutral-400);
                 background: var(--color-neutral-100); }
.next-title { font-family: var(--font-heading); font-weight: 800; font-size: var(--fs-lg);
              color: var(--color-accent-800); }
.next-bar.done .next-title { color: var(--color-text); }
.next-why { font-size: var(--fs-sm); color: var(--color-accent-800); opacity: .85; }
.next-bar.done .next-why { color: var(--color-neutral-800); }

/* — журнал и предупреждения под раскрытием — */
.logbox, .warnbox { border: 1px solid var(--color-divider);
                    background: var(--color-surface); }
.logbox > summary, .warnbox > summary {
  cursor: pointer; padding: 8px 12px; font-size: var(--fs-sm);
  font-family: var(--font-heading); font-weight: 800;
  color: var(--color-neutral-800); }
.logbox > summary:hover, .warnbox > summary:hover {
  background: color-mix(in srgb, var(--color-text) 6%, transparent); }
.warnbox > .problem { margin: 0 10px 10px; }
/* Кнопка возврата к концу журнала висит НАД ним, в правом нижнем углу:
   в потоке она сдвигала бы содержимое каждый раз, когда появляется и
   исчезает. Показывается только когда человек отлистал от конца. */
.joblog-wrap { position: relative; }
.joblog-end { position: absolute; right: 14px; bottom: 12px; font-size: var(--fs-caption);
              padding: 5px 10px; background: var(--color-bg);
              box-shadow: var(--shadow-lg); }

/* Высота ФИКСИРОВАННАЯ, а не max-height: иначе окно прыгает при каждой
   новой строке, и читать невозможно. */
.joblog { margin: 0; padding: 10px 12px; height: 340px; overflow: auto;
          font-family: ui-monospace, Consolas, monospace; font-size: var(--fs-caption);
          line-height: 1.45; white-space: pre-wrap; word-break: break-word;
          background: var(--color-neutral-900); color: var(--color-neutral-200); }

/* — полоска хода расчёта в шапке: видна на ЛЮБОЙ вкладке — */
.nav-job { display: flex; align-items: center; gap: 10px; font-size: var(--fs-sm);
           padding: 5px 12px; background: var(--color-accent-100);
           border: 1px solid var(--color-accent); cursor: pointer; }
.nav-job progress { width: 90px; height: 8px; }
.nav-job .nj-text { color: var(--color-accent-800); font-weight: 800;
                    font-family: var(--font-heading); }

/* — нагрузка компьютера в шапке (W4): «ОЗУ … % · Диск … %» —
   МИНИМАЛЬНАЯ ШИРИНА ПОСТОЯННА. Число обновляется раз в десять секунд, и
   без неё «9 %» ↔ «10 %» двигали бы соседей по шапке при каждом опросе.
   Ширина взята под самый широкий вид («ОЗУ 100 % · Диск 100 %» плюс
   значок тревоги), содержимое прижато к ПРАВОМУ краю узла: запас уходит
   влево, а не толкает разделитель.
   Цвета состояний спорят с `.nav-note` весом (0,2,0) против (0,1,0) — по
   правилу проекта (ловушка про `.table th.num`), ds.css не трогается.
   Обычное состояние остаётся цвета `.nav-note`; предупреждение — семья
   accent-2, как у плашки problem.warn; плохо — тот же тон, что у
   env-note в отказе (.nav-note.bad). */
.nav-load { min-width: 160px; justify-content: flex-end; white-space: nowrap;
            font-variant-numeric: tabular-nums; }
.nav-load.warn { color: var(--color-accent-2-700); }
.nav-load.bad { color: var(--color-accent-700); }

/* — кто вошёл, в шапке (фаза 2б): «логин · роль», ссылки «Админ-панель»
   и «Выйти из аккаунта» —
   Приём тот же, что у .nav-load: постоянная минимальная ширина и прижатие
   к правому краю, чтобы появление показателя не двигало соседей по шапке;
   `white-space: nowrap` держит строку «логин · роль» одной строкой.
   Цвет ссылок спорит с `.nav a` из ds.css весом (0,1,1) — селектор берётся
   глубже (0,2,1) по правилу проекта (ловушка про `.table th.num`),
   ds.css не трогается. */
.nav-right .nav-auth { min-width: 190px; justify-content: flex-end;
                       white-space: nowrap; gap: var(--space-3); }
.nav-right .nav-auth a { color: var(--color-accent-700); font-weight: 600; }

/* Узкие окна: длинные подписи в шапке сжимают логотип и ломают строки. */
@media (max-width: 1100px) {
  .nav-note.nav-load { display: none; }
  #env-note {
    max-width: 10rem; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
  }
}
@media (max-width: 900px) {
  .nav-note-text { display: none; }
  .nav-right .nav-auth { min-width: 0; }
}

/* — выдвижная панель ПЦР-продукта (макет, экран 05) —
   Раньше продукт разворачивался блоком внизу страницы: чтобы его увидеть,
   надо было прокрутить мимо всей таблицы, и связь «нажал вот на эту пару»
   терялась. Панель поверх экрана эту связь сохраняет. */
/* Панель — колонка флекса на всю высоту подложки. Без min-height:0 и
   overflow:hidden потомок .drawer-body с min-height:auto растёт выше
   вьюпорта, overflow:auto на нём не срабатывает, колесо крутит страницу.
   overscroll-behavior: contain — жест «доскроллил карточку» не уходит
   в документ (и ПЦР-панель, и профиль в админке — общие классы). */
.drawer-backdrop { position: fixed; inset: 0; z-index: 40; display: flex;
                   justify-content: flex-end; overscroll-behavior: contain;
                   background: color-mix(in srgb, var(--color-neutral-900) 45%, transparent); }
.drawer { width: min(1080px, 96vw); height: 100%; min-height: 0; overflow: hidden;
          background: var(--color-bg);
          box-shadow: var(--shadow-lg); display: flex; flex-direction: column; }
/* Панель принимает фокус (tabindex=-1), чтобы Escape и стрелки работали
   сразу после открытия, без предварительного щелчка внутри. Обводку при
   этом не рисуем: панель во весь экран, и рамка по её краю читается как
   сбой отрисовки, а не как отметка фокуса. Вес (0,2,0) против (0,1,0) у
   `:focus-visible` в ds.css — по правилу проекта спорим тем же весом. */
.drawer:focus-visible { outline: none; }
.drawer-head { display: flex; align-items: center; justify-content: space-between;
               gap: 20px; padding: 20px 28px; flex-shrink: 0;
               border-bottom: 2px solid var(--color-divider); }
.drawer-body { padding: 24px 28px 40px; display: flex; flex-direction: column;
               gap: 22px; flex: 1; min-height: 0; overflow: auto;
               overscroll-behavior: contain; }
.drawer-body .table th { font-size: var(--fs-label); padding: 10px; }
.drawer-body .table td { padding: 12px 10px; }
/* Заголовок панели: на ступень ниже h2 и выше карточки. Отбивку задаёт
   раскладка шапки, поэтому поля у него нет. */
.drawer-title { margin: 0 0 2px; font-size: var(--fs-h3); }
/* Последовательность праймера в таблице панели читают по буквам и
   сверяют с бланком заказа — ей нужна разрядка и размер побольше, чем у
   плотной таблицы. Вес (0,2,1) против `.table.compact td` (0,2,1),
   разрешается порядком — это правило ниже. */
.table td.seq-primer { font-size: var(--fs-body); letter-spacing: .06em; }

/* схема продукта: где садятся праймеры относительно всей длины */
.product-bar { display: flex; align-items: center; height: 54px; }
.pb-primer { display: flex; align-items: center; height: 26px;
             background: var(--color-accent-200); border: 1px solid var(--color-accent);
             padding: 0 8px; font-size: var(--fs-label); color: var(--color-accent-800);
             font-family: var(--font-heading); font-weight: 800; white-space: nowrap; }
.pb-body { flex: 1; height: 12px; background: var(--color-text); }

/* — выход из программы —
   Кнопка стоит в шапке справа: программа держит до 44 ГБ памяти, и
   отпустить её раньше можно было только через stop.bat в папке проекта.
   Красной её не делаем — это не «удалить», а «закрыть»; предупреждение
   про идущий расчёт даёт диалог. */
#exit-btn { font-size: var(--fs-sm); padding: 6px 14px; }
#exit-btn:hover { border-color: var(--color-accent);
                  color: var(--color-accent-700); }

/* Диалог, у которого есть цена ошибки, — с акцентной рамкой. */
.dialog.dialog-danger { border: 2px solid var(--color-accent); }
/* Кнопка необратимого действия. Не красная «вообще» — красная у нас уже
   основной акцент; отличается насыщенностью, чтобы взгляд задерживался. */
.btn-danger { background: var(--color-accent-700); }
.btn-danger:hover { background: var(--color-accent-800); }
.dialog-body.col { gap: 10px; }
.dialog-body .problem { margin-top: 4px; }

/* После выхода интерфейс мёртв: гасим шапку, чтобы кнопки не выглядели
   рабочими. Служба уже не ответит, и нажатие только запутает.
   Знак программы попал сюда вместе с остальными: он стал ссылкой на
   список работ, и без этого правила по нему можно было бы уйти на
   экран, который службе уже нечем наполнить. */
body.goodbye .nav-brand,
body.goodbye .nav-tabs,
body.goodbye .nav-right { opacity: .4; pointer-events: none; }

/* Гость: шапка кабинета («Работы / Корзина / диск / ⋯») скрыта.
   Знак программы и «Справка» остаются. auth-boot — пока /api/auth/mode
   не ответил, чтобы кабинет не мигнул на форме входа. */
.nav-guest-help { display: none; margin-left: auto; }
body.auth-boot .nav-tabs,
body.auth-boot .nav-right,
body.auth-gate .nav-tabs,
body.auth-gate .nav-right { display: none; }
body.auth-boot .nav-guest-help,
body.auth-gate .nav-guest-help { display: inline-flex; }
body.auth-waiting .nav-tabs,
body.auth-waiting #autosave,
body.auth-waiting #load-note,
body.auth-waiting #env-note,
body.auth-waiting #nav-job,
body.auth-waiting .nav-sep { display: none; }

/* ═══════════════════════════════════════════════════════════════════════
   ГЕОМЕТРИЯ КНОПОК
   ═══════════════════════════════════════════════════════════════════════
   Замер 22.08.2026: главная кнопка программы («Новая работа», «Создать
   первую работу», «Открыть отбор зондов», «Сохранить выбор зондов»,
   «Дальше…») имела ЧЕТЫРЕ геометрии — 19/12+22, 19/13+24, 17/11+20,
   14/8+14,4, — а на экране отбора зондов шесть разных `.btn` стояли
   одновременно. Задавались они инлайном, то есть весом (1,0,0,0):
   перебить такое классом нельзя в принципе, и каждая следующая правка
   заводила пятую геометрию.

   Ниже три размера, и других быть не должно:
     `.btn.btn-lg`   — главное действие экрана. Оно одно.
     `.btn.btn-sm`   — плотный ряд действий: панель графика, выгрузки.
     `.btn.btn-link` — кнопка, которая ведёт себя как текст в потоке.

   Вес (0,2,0) — на ступень выше `.btn` из дизайн-системы (0,1,0):
   спорим весом, а не порядком и не `!important`. */
.btn.btn-lg { font-size: var(--fs-lg);
              padding: var(--space-3) var(--space-6); }
.btn.btn-sm { font-size: var(--fs-sm);
              padding: var(--space-1) var(--space-3); }
/* `align-self` нужен внутри колонки-флекса, где кнопка иначе
   растягивается во всю ширину, и безвреден в обычном блоке — поэтому
   форма одна на оба случая. */
.btn.btn-link { padding: 0; align-self: flex-start; }

/* ═══════════════════════════════════════════════════════════════════════
   УТИЛИТЫ КЕГЛЯ
   ═══════════════════════════════════════════════════════════════════════
   Заменяют 104 инлайновых объявления. Порядок важен дважды:

   1. Они стоят ПОСЛЕ `.sub` и `.table.compact td` — правил того же веса,
      с которыми спорят. Равный вес разрешается порядком, и он здесь
      объявлен, а не получился.

   2. Ячейку таблицы утилита помечает через компонент —
      `.table td.t-sm` (0,2,1), а не одиночный `.t-sm` (0,1,0).
      Иначе её перебило бы `.table th` (0,1,1) из дизайн-системы: ровно
      этот механизм и дал находку §15, когда подпись числового столбца
      осталась слева, а числа ушли вправо. Механическая замена
      «инлайн → класс» без этой оговорки вернула бы часть узлов к своему
      размеру МОЛЧА. */
.t-caption, .table th.t-caption, .table td.t-caption { font-size: var(--fs-caption); }
.t-sm,  .table th.t-sm,  .table td.t-sm  { font-size: var(--fs-sm); }
.t-body,   .table th.t-body,   .table td.t-body   { font-size: var(--fs-body); }

/* Пояснение под полем ввода — ОДИН класс на всю программу.
   Их было три способа: `.knob-hint` на экране отбора, `.sub` плюс
   инлайновый кегль 12,5 в `поле()`, и просто `.sub` кое-где. Выглядели
   одинаково, назывались по-разному и расходились бы дальше.
   Отбивку задаёт само пояснение: `.field` — не флекс, `gap` в нём не
   работает. */
.field-hint { margin-top: 6px; font-size: var(--fs-caption);
              color: var(--color-neutral-700); }

/* Метка состояния работы. `.tag` в дизайн-системе — капитель в 11 px для
   короткого ярлыка вроде «new»; здесь в ней целая фраза («ждёт вашего
   решения»), и читать её капителью нельзя. Вес (0,2,0) против (0,1,0). */
.tag.tag-lg { font-size: var(--fs-sm); padding: 6px 12px; }

/* Роль участка: мишень — акцент, опорный — нейтральный. Не ds.css. */
.region-kind-list { gap: 8px; }
.region-kind-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid currentColor;
  font-size: var(--fs-sm);
  line-height: 1.2;
  text-decoration: none;
  color: inherit;
  background: transparent;
}
.region-kind-chip.is-current { font-weight: 700; }
.region-kind-dot {
  width: 8px; height: 8px;
  background: currentColor;
  flex: 0 0 auto;
}
.region-kind-target { color: var(--color-accent); }
.region-kind-reference { color: var(--color-neutral-800); }
.region-pick-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  padding: 10px 12px;
  border: 1px solid var(--color-divider);
}
.region-pick-row.region-kind-target {
  border-left: 3px solid var(--color-accent);
}
.region-pick-row.region-kind-reference {
  border-left: 3px solid var(--color-neutral-800);
}

/* Поле ввода, с которого экран начинается: единственное на экране, и в
   него надо попасть не глядя. Вес (0,2,0) против `.input` (0,1,0). */
.input.input-lg { font-size: var(--fs-lg); min-height: 48px; }

/* ─────────────────────────────────────────────────────────────────────
   Узкое окно
   ─────────────────────────────────────────────────────────────────────
   Программу открывают на разных машинах, и ноутбук 1366x768 с открытой
   боковой панелью даёт меньше 1000 px рабочей ширины. Колонки экрана
   отбора зондов при этом жёстко делились как «320 px + остаток», и
   остатка не хватало: график сжимался до пары сотен пикселей, а профиль
   на нём переставало быть видно. Ниже порога колонки становятся одна
   под другой — ручки сверху, график во всю ширину. */
@media (max-width: 1120px) {
  /* `minmax(0, 1fr)`, а не `1fr`: та же мина, что и в основном правиле.
     `1fr` = `minmax(auto, 1fr)`, и `auto` в минимуме — это min-content,
     то есть дорожка раздувается по самому широкому содержимому. */
  .probes-grid { grid-template-columns: minmax(0, 1fr); }
  .knobs { position: static; }
  main { padding: 32px 24px 56px; }
  .nav { padding: 14px 24px; gap: var(--space-2) 18px; flex-wrap: wrap; }
}

/* Шапка на телефоне.
   Замер при окне 375 px до правки: высота шапки 163 px — пятая часть
   экрана, — а `.nav-right` требовала 438 px и уводила вбок весь
   документ. Разделители при такой ширине только съедают место: строка
   и так переносится, и границу между кусками показывает перенос. */
@media (max-width: 700px) {
  .nav { padding: var(--space-3) var(--space-4); gap: var(--space-2) var(--space-3); }
  /* Вес (0,1,0) — ровно тот же, что у `.nav-brand` в ds.css, откуда
     приходит `margin-right: auto`. Иначе правая часть шапки не получит
     свою строку. */
  .nav-brand { margin-right: 0; }
  /* А кегль спорит с `.nav a` из ds.css и потому берётся глубже — как в
     основном правиле выше. */
  .nav a.nav-brand { font-size: var(--fs-lg); }
  /* Здесь <picture> в index.html уже подставил один знак вместо полного
     написания: порог тот же, 700 px. Знак почти квадратный, и по высоте
     ему нужно чуть больше, чем горизонтальному написанию, — иначе он
     читается мельче соседних кнопок. Строку шапки это не поднимает:
     кнопки в ней и так выше 34 px. */
  .nav-brand .brand-logo { height: 34px; max-width: 38px; }
  .nav-right { width: 100%; justify-content: flex-start;
               gap: var(--space-2) var(--space-3); }
  .nav-sep { display: none; }
  .nav-tabs { margin-left: 0; }
  .auth-screen { min-height: calc(100dvh - 9rem); }
}

/* Список работ: колонка владельца не раздувает строку на мобильных. */
.works .owner-cell { white-space: nowrap; max-width: 8rem;
                     overflow: hidden; text-overflow: ellipsis; }

/* Широкие таблицы (праймеры, зонды) прокручиваются внутри себя, а не
   растягивают страницу горизонтально. */
@media (max-width: 900px) {
  main { padding: 24px 14px 48px; }
  .table { display: block; overflow-x: auto; white-space: nowrap; }
  .stats { gap: 18px; }
}

/* — предсказанный гель (ТЗ, 5.9) —
   Тёмная подложка не украшение: полосы читаются как на снимке из
   гель-документации, и человек сразу понимает, на что смотрит.
   Картинка прокручивается внутри себя, а не растягивает страницу:
   дорожек может быть сколько угодно. */
.gel-wrap { overflow-x: auto; background: var(--color-neutral-900);
            border: 1px solid var(--color-divider); padding: 10px; }
.gel-svg { display: block; }

/* — миниатюра профиля на экране участка —
   Не замена большому графику: она отвечает на вопрос «как вообще
   выглядит участок» до того, как открывать отбор зондов. Поэтому без
   осей, подписей и взаимодействия — только форма. */
.mini-profile-wrap { background: var(--color-surface);
                     border: 1px solid var(--color-divider); padding: 8px; }
.mini-profile { display: block; width: 100%; height: 90px; }

/* — админ-панель (#/admin) — см. также admin.css (дубликат для надёжности) */
.admin-shell {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: 0;
  min-height: calc(100vh - 120px);
  margin: -8px -8px 0;
}
.admin-sidebar {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-4) var(--space-3);
  background: var(--color-surface);
  border-right: 2px solid var(--color-divider);
  position: sticky; top: 72px; align-self: start;
  max-height: calc(100vh - 88px);
}
.admin-sidebar-title {
  font-family: var(--font-heading); font-weight: 800;
  font-size: var(--fs-caption); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--color-neutral-600);
  padding: 0 var(--space-2) var(--space-2);
  border-bottom: 1px solid var(--color-divider);
  margin-bottom: var(--space-1);
}
.admin-nav-item {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); width: 100%; padding: 10px 12px;
  font: inherit; font-size: var(--fs-body); text-align: left;
  color: var(--color-text); background: none; border: 0;
  border-left: 3px solid transparent; cursor: pointer;
}
.admin-nav-item:hover {
  background: color-mix(in srgb, var(--color-text) 5%, transparent);
}
.admin-nav-item[aria-current="page"] {
  background: var(--color-accent-100);
  border-left-color: var(--color-accent);
  color: var(--color-accent-800); font-weight: 600;
}
.admin-nav-badge {
  font-size: var(--fs-label); font-weight: 800;
  min-width: 1.4em; padding: 2px 7px; text-align: center;
  background: var(--color-accent); color: #fff;
}
.admin-main {
  display: flex; flex-direction: column; gap: var(--space-6);
  padding: var(--space-2) 0 var(--space-6) var(--space-6); min-width: 0;
}
.admin-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
}
.admin-head h1 { margin: 0; font-size: var(--fs-h3); }
.admin-updated { font-size: var(--fs-sm); color: var(--color-neutral-600); }
.admin-kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--space-3);
}
.admin-kpi {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: var(--space-4); background: var(--color-surface);
  border: 1px solid var(--color-divider); min-height: 96px;
}
.admin-kpi-label {
  font-size: var(--fs-caption); color: var(--color-neutral-700);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.admin-kpi-value {
  font-family: var(--font-heading); font-weight: 800;
  font-size: 2rem; line-height: 1; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.admin-kpi-value.warn { color: var(--color-accent-700); }
.admin-kpi-value.ok { color: var(--color-neutral-800); }
.admin-kpi-hint { font-size: var(--fs-sm); color: var(--color-neutral-600); }
.admin-section { display: flex; flex-direction: column; gap: var(--space-3); }
.admin-section-title { margin: 0; font-size: var(--fs-section); }
.admin-card {
  padding: var(--space-4); background: var(--color-surface);
  border: 1px solid var(--color-divider);
}
.admin-card.warn {
  background: var(--color-accent-100); border-color: var(--color-accent-300);
}
.admin-meters {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-4);
}
.admin-meter-label {
  display: flex; justify-content: space-between;
  font-size: var(--fs-sm); margin-bottom: 6px;
}
.admin-meter-label span:last-child {
  font-variant-numeric: tabular-nums; font-weight: 600;
}
.admin-meter-bar {
  height: 8px; background: var(--color-neutral-200); overflow: hidden;
}
.admin-meter-fill {
  height: 100%; background: var(--color-neutral-700);
  transition: width 0.4s ease;
}
.admin-meter-fill.warn { background: var(--color-accent-500); }
.admin-meter-fill.bad { background: var(--color-accent-700); }
.admin-activity { display: flex; flex-direction: column; gap: 0; }
.admin-activity-item {
  display: grid; grid-template-columns: 88px 1fr auto;
  gap: var(--space-3); align-items: baseline;
  padding: 10px 0; border-bottom: 1px solid var(--color-divider);
  font-size: var(--fs-sm);
}
.admin-activity-item:last-child { border-bottom: 0; }
.admin-activity-time {
  font-variant-numeric: tabular-nums; color: var(--color-neutral-600);
}
.admin-activity-who { font-weight: 600; }
.admin-activity-action { color: var(--color-neutral-700); }
.admin-toolbar {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--space-3) var(--space-4);
}
.admin-toolbar .input, .admin-toolbar select.input {
  min-width: 160px; max-width: 240px;
}
.admin-user-cell { display: flex; align-items: center; gap: var(--space-2); }
.admin-avatar {
  width: 32px; height: 32px; display: grid; place-items: center;
  font-family: var(--font-heading); font-weight: 800;
  font-size: var(--fs-caption); background: var(--color-neutral-200);
  color: var(--color-neutral-800); flex-shrink: 0;
}
.admin-avatar.accent {
  background: var(--color-accent-200); color: var(--color-accent-800);
}
.admin-actions {
  display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end;
}
.admin-job-card {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4); background: var(--color-accent-100);
  border: 2px solid var(--color-accent-300);
}
.admin-job-progress {
  height: 6px; background: var(--color-accent-200); overflow: hidden;
}
.admin-job-progress-fill {
  height: 100%; background: var(--color-accent);
  transition: width 0.3s ease;
}
.admin-empty-hint {
  padding: var(--space-6); text-align: center;
  color: var(--color-neutral-600); font-size: var(--fs-sm);
}
.admin-panel { animation: admin-fade-in 0.22s ease-out; }
@keyframes admin-fade-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}
.admin-live-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--color-neutral-400); display: inline-block;
}
.admin-live-dot--busy {
  background: var(--color-accent);
  animation: admin-pulse 0.8s ease-in-out infinite;
}
@keyframes admin-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.5; transform: scale(0.85); }
}
.admin-link.btn-link {
  padding: 0; min-height: 0; font-weight: 600;
  text-align: left; height: auto;
}
/* Пометка служебной записи журнала (dev-режим, системные действия).
   Контурная и серая: она ОТДЕЛЯЕТ запись от людских, а не кричит. */
.admin-service-tag {
  font-size: var(--fs-label); font-weight: 600;
  padding: 1px 6px; margin: 0 4px; white-space: nowrap;
  border: 1px solid var(--color-divider);
  color: var(--color-neutral-600);
}
/* Логин-кнопка в таблицах: длинный логин сжимается в многоточие,
   а не разрывает колонку (D7). Целиком — в title. */
.admin-table-user {
  display: inline-block; max-width: 14rem; height: auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom; text-align: left;
}
.admin-drawer { width: min(720px, 96vw); min-height: 0; overflow: hidden; }
.admin-drawer-backdrop { z-index: 45; overscroll-behavior: contain; }

/* Строка пользователей открывает карточку; кнопки и ссылки свои. */
.admin-row-click tbody tr { cursor: pointer; }

.admin-profile-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.admin-quota-fields {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}

.admin-spend-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
}

/* ТЗ 04, вилка 700–1100: сайдбар уже, KPI в две колонки. Блок стоит
   ДО media 900px: на 900 и уже вступает горизонтальный сайдбар. */
@media (max-width: 1100px) {
  .admin-shell { grid-template-columns: 190px minmax(0, 1fr); }
  .admin-kpi-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 900px) {
  .admin-shell { grid-template-columns: 1fr; }
  .admin-sidebar {
    position: static; flex-direction: row; flex-wrap: wrap;
    max-height: none; border-right: 0;
    border-bottom: 2px solid var(--color-divider);
  }
  .admin-sidebar-title { width: 100%; }
  .admin-main { padding-left: 0; }
  .admin-activity-item { grid-template-columns: 1fr; gap: 2px; }
  .admin-quota-fields { grid-template-columns: 1fr; }
}
/* ТЗ 04, ≤700: KPI столбиком, числа мельче — ширины в 2rem не хватает. */
@media (max-width: 700px) {
  .admin-kpi-grid { grid-template-columns: 1fr; }
  .admin-kpi-value { font-size: 1.5rem; }
}

/* — меню «⋯» в шапке (v3) —
   Второстепенные действия (настройки, справка, кабинет, выход) собраны
   за кнопкой «⋯» в правом краю шапки: шапка перестала тянуть в ширину,
   а состав пунктов зависит от режима входа и строится в main.js.
   Обёртка — точка привязки: меню прижато к ПРАВОМУ краю кнопки и висит
   НАД содержимым (z-index выше sticky-шапки, у которой 20). Скруглений
   нет — лицо дизайн-системы; тень самая мелкая: меню лёгкое, это не
   диалог, а более тяжёлая тень читалась бы как модальное окно. */
.nav-menu-wrap { position: relative; display: flex; align-items: center; }
#nav-menu-btn { min-width: 36px; padding: 6px 10px;
                font-size: var(--fs-lg); line-height: 1; }
.nav-menu {
  position: absolute; right: 0; top: calc(100% + 6px);
  z-index: 25;
  display: flex; flex-direction: column; min-width: 250px;
  padding: var(--space-1) 0;
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  box-shadow: var(--shadow-sm);
}
/* `display: flex` перебивает скрытие по атрибуту: у [hidden] в браузере
   `display: none`, но ЛЮБОЙ авторский display его сильнее независимо от
   селектора. Тот же приём, что у скрытых плашек списка работ. */
.nav-menu[hidden] { display: none; }
/* Надпись «кто вошёл»: не кнопка — нажимать в ней нечего. После уборки
   шапки это единственное место, где видно, под кем работаешь. */
.nav-menu-caption {
  padding: var(--space-2) var(--space-4);
  margin-bottom: var(--space-1);
  border-bottom: 1px solid var(--color-divider);
  font-size: var(--fs-caption); color: var(--color-neutral-600);
  white-space: nowrap;
}
/* Пункты в стиле вкладок .nav: обычное состояние — серые, как вкладки
   без текущей, по наведению — акцент, как ссылки шапки. Селектор взят
   глубже (0,2,0), чем одиночный класс: из ds.css сюда дотягивается
   `.nav a` (0,1,1), и спорить с ним надо весом, а не порядком. */
.nav .nav-menu-item {
  display: block; width: 100%;
  padding: 10px var(--space-4);
  border: 0; background: none;
  font: inherit; text-align: left;
  color: var(--color-neutral-700);
  text-decoration: none; cursor: pointer;
}
.nav .nav-menu-item:hover { color: var(--color-accent); }

/* ═══════════════════════════════════════════════════════════════════════
   — таблицы dtable (v4) —
   Компонент web/ui/js/dtable.js. Блок обязан быть ИДЕНТИЧНЫМ в admin.css
   (он грузится позже и перебивает); синхронность набора классов держит
   tests/test_admin_css_sync.py. Вес селекторов — не выше (0,2,0), кроме
   пары `.dt th.dt-sort` (0,2,1): она спорит с `.table th` из ds.css
   (0,1,1) по правилу ловушки «.table th.num».
   ═══════════════════════════════════════════════════════════════════════ */

/* РАЗДЕЛЬНЫЕ ГРАНИЦЫ ДЛЯ ЛИПКОЙ ШАПКИ. При border-collapse граница
   рисуется частью таблицы: прокрутка уносит её прочь от прилипшей
   ячейки, и шапка остаётся без подчёркивания. */
.table.dt { border-collapse: separate; border-spacing: 0; }

/* Горизонтальную прокрутку держит ОБЁРТКА, а не сама таблица: так
   табличная модель жива, и шапка задаёт ширину своим столбцам. */
.dt-scroll { overflow-x: auto; max-width: 100%; }

/* высота 'page': таблица живёт в остатке экрана, шапка прилипает. */
.dt-page .dt-scroll { overflow: auto;
  max-height: max(240px, calc(100vh - 320px)); }
.dt-page th { position: sticky; top: 0; z-index: 1;
  background: var(--color-surface); }

/* Сортируемая шапка: кнопка занимает ячейку целиком, отбивки повторяют
   `.table.compact th` — как у .th-sort экрана праймеров. */
.dt th.dt-sort { padding: 0; }
.dt-sort-btn {
  display: flex; align-items: baseline; gap: 5px; width: 100%;
  padding: var(--space-2) 10px;
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; text-align: inherit;
  background: none; border: 0; cursor: pointer;
}
.dt th.num .dt-sort-btn { justify-content: flex-end; }
.dt-sort-btn:hover { color: var(--color-text); }
.dt-sort-btn:focus-visible {
  outline: 2px solid var(--color-accent); outline-offset: -2px; }
.dt-arrow {
  flex: none; font-size: 12px; line-height: 1;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.dt-arrow.quiet { opacity: 0.35; }
.dt-sort-btn:hover .dt-arrow.quiet { opacity: 0.7; }

/* Панель над таблицей: поиск, селекты, счётчик строк. */
.dt-panel {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--space-2) var(--space-3);
}
.dt-search { min-width: 160px; max-width: 240px; }
.dt-filter { min-width: 150px; max-width: 240px; }
.dt-count {
  margin-left: auto; align-self: center;
  font-size: var(--fs-sm); color: var(--color-neutral-600);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.dt-block { min-width: 0; }
.dt-pager {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
}
.dt-pager-size {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: var(--fs-sm); color: var(--color-neutral-700);
}
.dt-page-size {
  width: auto; min-width: 4.5rem; max-width: 6rem;
  height: 36px; min-height: 36px;
}
.dt-pager-hint {
  font-size: var(--fs-sm); color: var(--color-neutral-600);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Пустое состояние: человеческая фраза вместо скелета таблицы. */
.dt-empty {
  padding: var(--space-6); text-align: center;
  color: var(--color-neutral-600); font-size: var(--fs-sm);
  border: 1px dashed var(--color-divider);
}

/* Наведение строки мягко разгорается, а не мигает: цвет даёт ds.css. */
.dt tbody tr { transition: background-color 0.15s ease; }

/* — находка Этапа 1 №1 —
   Шапка и ячейки drawer-таблицы имели разную вертикальную геометрию:
   th 10px против td 12px. Сведены к td; дубль в admin.css обязателен. */
.drawer-body .table th { padding: 12px 10px; }
.drawer-body .table td { padding: 12px 10px; }

/* — находка Этапа 1 №2 —
   На узких окнах `.table { display: block; … }` выключал табличную
   коробку: внутренняя анонимная таблица сжималась по содержимому, и
   подписи переставали сидеть над своими столбцами. dtable-таблицам
   модель возвращена — прокрутка в .dt-scroll. */
@media (max-width: 900px) {
  .table.dt { display: table; white-space: normal; }
}

/* ═══════════════════════════════════════════════════════════════════════
   — мониторинг (v3): недостающий дубль admin.css —
   Найдено тестом синхронности дублей: блок v3 дописали только в
   admin.css. Правила идентичны; admin.css грузится позже и перебивает —
   вид не меняется, меняется только надёжность дубля.
   ═══════════════════════════════════════════════════════════════════════ */
.mon-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4);
}

.mon-kpi {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  min-width: 0;
}

.mon-kpi-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}

.mon-kpi-label {
  font-size: var(--fs-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-neutral-700);
}

.mon-kpi-value {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 1.5rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.mon-canvas {
  display: block;
  width: 100%;
  height: 120px;
}

.mon-kpi-scale {
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-sm);
  color: var(--color-neutral-600);
  font-variant-numeric: tabular-nums;
}

.mon-status {
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  color: var(--color-neutral-600);
  font-size: var(--fs-sm);
}

.mon-feed {
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
}

.mon-feed-item {
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-divider);
  font-size: var(--fs-sm);
  color: var(--color-accent-800);
}

.mon-feed-item:last-child { border-bottom: 0; }

.mon-note {
  font-size: var(--fs-sm);
  color: var(--color-neutral-600);
}

@media (max-width: 1099px) {
  .mon-grid {
    grid-template-columns: 1fr;
  }
}

/* Недостающий дубль admin.css: карточки в две колонки. */
.admin-two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
}
