/* СНИМКИ ГРУЗА В КАРТОЧКЕ ЛОТА.
   Лента в один ряд с прокруткой, а не сетка: снимков бывает от одного до
   шести, и сетка при одном снимке оставляет пустые клетки — экран выглядит
   недогруженным там, где всё в порядке. */
.shots { display: flex; gap: 10px; overflow-x: auto; padding: 4px 0 10px; }
.shots__one { margin: 0; flex: 0 0 auto; width: 200px; }
.shots__one img { width: 200px; height: 150px; object-fit: cover;
  border-radius: 6px; border: 1px solid var(--line, #d0d5dd); display: block; background: #f2f4f7; }
.shots__one figcaption { font-size: 12px; margin-top: 4px; line-height: 1.3; }

/* =====================================================================
 * ТРИ ЯЗЫКА.
 *
 * Переключатель — три коротких слова в шапке, а не выпадающий список с
 * флагами. Флаг страны не равен языку: казахский — не флаг Казахстана,
 * на нём говорят и за его пределами, а русский флагом Казахстана не
 * обозначишь вовсе. Слова короче и однозначнее.
 *
 * Двуязычная подача (data-t-both) — для того, что видит посторонний:
 * карточки грузов, витрина, ценники. Статья 21 Закона о языках требует
 * казахский сверху, русский снизу и ОДИНАКОВЫЙ размер шрифта. Поэтому
 * .t-ru не уменьшен и не приглушён до серого: и то и другое читается
 * как «меньшим размером», а требование буквальное.
 *
 * Цвета берутся переменными с запасным значением. Запасное нужно потому,
 * что переключатель рисуется и на экране входа, куда набор переменных
 * консоли может не доехать: кнопка без цвета там будет невидима.
 * ===================================================================== */
.langswitch { display: inline-flex; gap: 2px; margin-left: 12px; vertical-align: middle; }
.langswitch__b {
  font: inherit; font-size: 11px; line-height: 1.6; letter-spacing: .04em;
  padding: 2px 7px; border: 1px solid var(--line, #d0d5dd);
  background: transparent; color: var(--muted, #667085);
  border-radius: 4px; cursor: pointer;
}
.langswitch__b:hover { color: var(--text, #101828); border-color: var(--text, #101828); }
.langswitch__b.is-on {
  background: var(--accent, #1570ef); border-color: var(--accent, #1570ef); color: #fff;
}
[data-t-both] .t-kk, [data-t-both] .t-ru { display: block; }
[data-t-both] .t-ru { opacity: 1; font-size: inherit; font-weight: inherit; }
[data-t-both] .t-kk + .t-ru { margin-top: 1px; }

/*
 * Тело держится невидимым до первого прохода перевода, но не дольше
 * секунды. Полсекунды русского текста, сменяющегося казахским, читается
 * как поломка; белый экран без конца — как поломка тем более. Из двух
 * зол выбран короткий предел и русский текст после него.
 */
html[data-t-wait] body { visibility: hidden; animation: vl-t-give-up 0s linear 1s forwards; }
@keyframes vl-t-give-up { to { visibility: visible; } }

/* ЛОГОТИП. Цвет букв берётся у текста (currentColor в самом знаке), поэтому
   ночная тема не требует второго файла: буквы светлеют вместе с текстом, а
   оранжевая «V» остаётся оранжевой — она и есть опознавательный знак. */
.brand__logo { display: block; height: 30px; width: auto; color: var(--text, #101828); }
.brand { display: flex; align-items: center; gap: 8px; }

/*
  AVLOX · оформление консоли диспетчера.

  Правило одно и оно жёсткое: НИ ОДНОГО литерального цвета, отступа или
  размера шрифта. Всё — переменные из сгенерированного tokens.css. Проверка
  этого правила входит в тест: файл сканируется на hex-цвета и пиксельные
  значения вне токенов, поэтому «поправлю здесь один цвет» не проходит молча.

  ЯЗЫК — БУМАГА И ЧЕРНИЛА (ADR-109). Три следствия, которые видно в каждом
  правиле ниже:

  1. РАЗДЕЛЯЕТ ЛИНЕЙКА, А НЕ КОРОБКА. Обведённый блок внутри обведённого
     блока даёт на плотном экране решётку, и глаз начинает обходить рамки
     вместо содержимого. Панель — это лист с волосяной линейкой и почти
     неразличимой тенью, а не карточка с бортом.
  2. СОСТОЯНИЕ ЧИТАЕТСЯ БОКОВЫМ ЗРЕНИЕМ. У строки слева стоит отметка
     цветом последствия. Диспетчер просматривает две сотни строк и не
     должен читать каждое слово, чтобы найти горящую.
  3. ЦВЕТ — СИГНАЛ, А НЕ ОФОРМЛЕНИЕ. Цветного на экране ровно столько,
     сколько несёт смысл. Если покрасить и заголовки, и кнопки, и метки,
     то красная строка перестанет быть заметной — а она здесь главное.
*/

*, *::before, *::after { box-sizing: border-box; }

/* Атрибут hidden проигрывает любому display в правиле класса — из-за этого
   строка очереди оставалась на экране с текстом «0 действий ждут отправки».
   Дефект видно только глазами: свойство hidden в DOM при этом true, и тест,
   проверявший свойство, был зелёным. Теперь и правило есть, и тест смотрит
   на фактическую видимость. */
[hidden] { display: none !important; }

html { color-scheme: light dark; }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--text-1);
  font-family: var(--typography-sans);
  font-size: var(--text-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Цифры во ВСЕЙ консоли моноширинные по ширине знака и с перечёркнутым
     нулём. Это не типографская прихоть: столбец сумм, в котором единица уже
     нуля, невозможно сравнить взглядом, а ноль, неотличимый от буквы «О», в
     номере накладной стоит звонка перевозчику. */
  font-variant-numeric: tabular-nums slashed-zero;
}

.skip {
  position: absolute; left: var(--geometry-sp-2); top: calc(var(--geometry-sp-2) * -10);
  background: var(--surface); color: var(--text-1);
  padding: var(--geometry-sp-2) var(--geometry-sp-4);
  border-radius: var(--geometry-r-ctl); z-index: 20;
  box-shadow: var(--elevation-float);
  transition: top var(--motion-state) var(--ease-out);
}
.skip:focus { top: var(--geometry-sp-2); }

/* ------------------------------- шапка -------------------------------
   Вкладка отмечена ЛИНИЕЙ ПОД СЛОВОМ, а не заливкой. Заливка-«таблетка» —
   первое, что рисует любой генератор макетов, и она же худшая по делу:
   восемь залитых прямоугольников в ряд соревнуются за внимание с данными,
   ради которых экран открыт. Линия занимает 2 px и не соревнуется ни с чем. */
.topbar {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: stretch; gap: var(--geometry-sp-6);
  padding: 0 var(--geometry-sp-6);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.brand {
  display: flex; align-items: center; gap: var(--geometry-sp-2);
  font-weight: 600; letter-spacing: -.01em; white-space: nowrap;
  padding: var(--geometry-sp-3) var(--geometry-sp-4) var(--geometry-sp-3) 0;
  border-right: 1px solid var(--border); margin-right: var(--geometry-sp-2);
}
.mark {
  width: var(--geometry-sp-3); height: var(--geometry-sp-3);
  border-radius: 50%;
  background: var(--color-brand-default-day);
  /* Кольцо вокруг знака — единственная декоративная деталь во всей шапке.
     Она здесь потому, что знак тенанта иначе читается как индикатор
     состояния: круглая точка рядом со словом — это «онлайн», а не марка. */
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 var(--geometry-tick) var(--surface-2);
}
[data-theme="night"] .mark { background: var(--color-brand-default-night); }

/* Полоса разделов прокручивается — их дюжина, и втиснуть их в любую ширину
   нельзя. Но обрезанное на полуслове название читается как поломка вёрстки,
   поэтому край РАСТВОРЯЕТСЯ: маска гасит последние 24 px с обеих сторон, и
   обрезка превращается в указание, что там есть ещё. */
.tabs {
  display: flex; gap: var(--geometry-sp-1); flex: 1; min-width: 0;
  overflow-x: auto; scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, transparent 0, black var(--geometry-sp-6),
                                      black calc(100% - var(--geometry-sp-6)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, black var(--geometry-sp-6),
                              black calc(100% - var(--geometry-sp-6)), transparent 100%);
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  color: var(--text-2); font: inherit;
  padding: var(--geometry-sp-3);
  min-height: var(--density-row-comfortable);
  white-space: nowrap; position: relative;
  transition: color var(--motion-micro) var(--ease-out);
}
/* Линия живёт всегда и меняет масштаб: она выезжает из центра слова, а не
   возникает. Появление из ниоткуда читается как перерисовка страницы,
   выезд — как перемещение внимания. */
.tab::after {
  content: ""; position: absolute; left: var(--geometry-sp-3); right: var(--geometry-sp-3); bottom: 0;
  height: 2px; background: var(--color-brand-default-day);
  transform: scaleX(0); transform-origin: 50% 50%;
  transition: transform var(--motion-state) var(--ease-out);
}
[data-theme="night"] .tab::after { background: var(--color-brand-default-night); }
.tab:hover { color: var(--text-1); }
.tab:hover::after { transform: scaleX(.4); }
.tab[aria-current="page"] { color: var(--text-1); font-weight: 600; }
.tab[aria-current="page"]::after { transform: scaleX(1); }

/* Кольцо фокуса — фирменное, а не браузерное синее. Оно обязано быть видно
   на бумаге и в темноте, поэтому берётся цвет марки, а не сигнальный: синий
   здесь означал бы «сведения», и клавиатурный обход выглядел бы сообщением. */
.tab:focus-visible, .btn:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, a:focus-visible {
  outline: 2px solid var(--color-brand-default-day);
  outline-offset: 2px; border-radius: var(--geometry-r-ctl);
}
[data-theme="night"] .tab:focus-visible, [data-theme="night"] .btn:focus-visible,
[data-theme="night"] input:focus-visible, [data-theme="night"] select:focus-visible,
[data-theme="night"] textarea:focus-visible, [data-theme="night"] a:focus-visible {
  outline-color: var(--color-brand-default-night);
}

.topbar__right { display: flex; align-items: center; gap: var(--geometry-sp-3); flex-shrink: 0; }
.tenant {
  color: var(--text-2); font-size: var(--text-meta);
  font-family: var(--typography-mono); letter-spacing: -.02em;
}

/* ------------------------------ раскладка ----------------------------
   min-width: 0 у колонок сетки — не косметика. Без него колонка не умеет
   стать уже своего содержимого, и таблица с длинными столбцами вылезала за
   правый край: кнопка «Протокол» оказывалась наполовину срезанной на
   1280 px. Классическая ловушка grid, и находится она только глазами на
   настоящей ширине. */
main {
  padding: var(--geometry-sp-8) var(--geometry-sp-6) var(--geometry-sp-12);
  max-width: var(--layout-content-max); margin: 0 auto;
}
.viewhead { margin-bottom: var(--geometry-sp-6); max-width: var(--layout-content-read); }
/* Короткая черта над заголовком. Приём книжный, а не интерфейсный, и в этом
   смысл: она ничего не сообщает, но задаёт странице начало — глаз получает
   точку, от которой читать. Тем же цветом отмечена активная вкладка, и
   раздел оказывается связан с местом, откуда в него вошли. */
.viewhead::before {
  content: ""; display: block;
  width: var(--geometry-sp-6); height: 2px;
  background: var(--color-brand-default-day);
  margin-bottom: var(--geometry-sp-3);
}
[data-theme="night"] .viewhead::before { background: var(--color-brand-default-night); }

h1 {
  font-size: var(--text-title); margin: 0 0 var(--geometry-sp-2);
  font-weight: 600; letter-spacing: -.02em; line-height: 1.2;
}
h2 { font-size: var(--text-head); margin: 0; font-weight: 600; letter-spacing: -.01em; }
.lede { color: var(--text-2); margin: 0; max-width: 68ch; font-size: var(--text-lede); }
.muted { color: var(--text-3); font-size: var(--text-meta); }

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--geometry-r-panel);
  box-shadow: var(--elevation-sheet);
  overflow: hidden;
  min-width: 0;
}
.panel__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--geometry-sp-4);
  padding: var(--geometry-sp-3) var(--geometry-sp-4);
  border-bottom: 1px solid var(--border);
}
.split {
  display: grid;
  grid-template-columns: minmax(var(--layout-panel-min), var(--layout-panel-max)) minmax(0, 1fr);
  gap: var(--geometry-sp-4); align-items: start;
}
.split > * { min-width: 0; }

/*
 * ДЕФЕКТ 125. `.split` задуман как «узкая форма слева, широкое содержимое
 * справа»: левая колонка нарочно ограничена шириной панели. Справочники же
 * положили в него ДВЕ РАВНОЦЕННЫЕ таблицы — машины и водителей, — и таблица
 * машин оказалась зажата в узкую колонку.
 *
 * Последствие видно не в макете, а в работе: пять столбцов не помещались, и
 * за край уезжал ПОСЛЕДНИЙ — «Готова», то самое «можно ли выпускать эту
 * машину в рейс». На мониторе 1500 px главный ответ экрана был доступен
 * только горизонтальной прокруткой. Затемнение правого края (ADR-109)
 * честно показывало, что содержимое продолжается, — но показывать это на
 * широком экране нечего, там просто должно помещаться.
 *
 * Проверка «нет переполнения» этого не ловила: она смотрела переполнение
 * СТРАНИЦЫ, а здесь переполняется панель внутри страницы. Акт 10 теперь
 * смотрит и его — на широком экране ни одна таблица не имеет права
 * прокручиваться вбок.
 */
.split--even { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* -------------------------------- вход -------------------------------
   Экран входа живёт ВНЕ `main`, потому что шапка на нём скрыта: у человека,
   который ещё не вошёл, нет разделов, между которыми можно ходить. Поэтому
   отступы и ширину он задаёт себе сам.

   Форма — узкой колонкой по центру, а не во всю ширину монитора. Поле ввода
   в 1280 px читается как ошибка вёрстки: глаз ищет, где кончается строка, а
   вводить в неё надо восемь знаков. */
#view-login {
  max-width: var(--layout-panel-max);
  margin: 0 auto;
  padding: var(--geometry-sp-12) var(--geometry-sp-4);
}
#view-login .panel { padding-bottom: var(--geometry-sp-2); }
#view-login .breakdown__why { color: var(--critical); }

/* ------------------------------ показатели ---------------------------
   Сводка набрана КОЛОНКАМИ НА БУМАГЕ, а не карточками. Четыре одинаковые
   коробки в ряд — самый узнаваемый признак сгенерированной панели, и они же
   дороже всего обходятся по вниманию: восемь бортов вокруг четырёх чисел.
   Здесь борта нет вовсе, колонки разделены волосяной линейкой, и читается
   в первую очередь число — ради него сводка и существует. */
.kpis {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--layout-kpi-min), 1fr));
  gap: 0; margin-bottom: var(--geometry-sp-8);
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--geometry-r-panel);
  box-shadow: var(--elevation-sheet); overflow: hidden;
}
.kpi { padding: var(--geometry-sp-4) var(--geometry-sp-6); border-left: 1px solid var(--border); min-width: 0; }
.kpi:first-child { border-left: 0; }
.kpi__label { color: var(--text-2); font-size: var(--text-meta); }
.kpi__value {
  font-size: var(--text-display); font-weight: 600; margin-top: var(--geometry-sp-1);
  letter-spacing: -.03em; line-height: 1.1;
}
.kpi__value--critical { color: var(--critical); }
.kpi__value--success  { color: var(--success); }
/* Пояснение под числом. Показатель без объяснения читают как оценку —
   и спорят с ней; показатель с объяснением обсуждают по существу. */
.kpi__why { color: var(--text-2); font-size: var(--text-meta); margin-top: var(--geometry-sp-1); }

/* -------------------------------- таблицы ----------------------------
   Заголовок столбца НЕ НАБРАН ПРОПИСНЫМИ вразрядку. Это второй по
   узнаваемости признак шаблона — и он же мешает: прописные читаются на
   треть медленнее строчных, потому что теряется рисунок слова, а
   заголовок столбца человек перечитывает при каждом возврате к таблице. */
/* Прокручиваемая таблица гасит правый край так же, как полоса разделов:
   срезанный столбец у борта панели читается как поломка, растворяющийся —
   как продолжение. Маска включается ТОЛЬКО когда прокрутка есть: без неё
   последний столбец выглядел бы приглушённым без причины. */
.tablewrap { overflow-x: auto; scrollbar-width: thin; }
.tablewrap[data-scrolls] {
  -webkit-mask-image: linear-gradient(to right, black calc(100% - var(--geometry-sp-8)), transparent 100%);
  mask-image: linear-gradient(to right, black calc(100% - var(--geometry-sp-8)), transparent 100%);
}
.table { width: 100%; border-collapse: collapse; font-size: var(--text-body); }
/* Вертикальный отступ ячейки — sp-2, а не sp-3. Высота строки при этом
   остаётся 44 px (токен density.row-comfortable): она задана снизу, а не
   набрана отступами. Разница видна не в макете, а в работе — на экране
   помещается в полтора раза больше строк, и диспетчер реже прокручивает
   очередь, в которой ищет горящее. */
.table th, .table td {
  text-align: left; padding: var(--geometry-sp-2) var(--geometry-sp-4);
  border-bottom: 1px solid var(--border);
  height: var(--density-row-comfortable);
  vertical-align: middle;
}
.table th {
  color: var(--text-2); font-weight: 600; font-size: var(--text-meta);
  white-space: nowrap; padding-top: var(--geometry-sp-2); padding-bottom: var(--geometry-sp-2);
  height: auto; background: var(--surface);
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background var(--motion-micro) var(--ease-out); }
.table tbody tr:hover { background: var(--surface-2); }

/* ОТМЕТКА ПОСЛЕДСТВИЯ У КРАЯ СТРОКИ. Читается боковым зрением: чтобы найти
   горящую строку среди двухсот, не нужно читать ни одного слова. Слово при
   этом остаётся — цвет несёт то же, что текст, а не вместо него. */
.table tbody tr[data-severity] td:first-child { box-shadow: inset var(--geometry-tick) 0 0 var(--text-3); }
.table tbody tr[data-severity="crit"] td:first-child { box-shadow: inset var(--geometry-tick) 0 0 var(--critical); }
.table tbody tr[data-severity="high"] td:first-child { box-shadow: inset var(--geometry-tick) 0 0 var(--warning); }
.table tbody tr[data-severity="med"]  td:first-child { box-shadow: inset var(--geometry-tick) 0 0 var(--info); }
.table tbody tr[data-state="overdue"] td:first-child { box-shadow: inset var(--geometry-tick) 0 0 var(--critical); }
.table tbody tr[data-state="paid"]    td:first-child { box-shadow: inset var(--geometry-tick) 0 0 var(--success); }

/* Выбранная строка помечается полосой у края, а не только фоном: фон
   выбранной и фон наведённой мыши иначе неразличимы, и человек теряет,
   что именно он открыл. */
.table tbody tr[data-contract] { cursor: pointer; }
.table tbody tr[data-picked] { background: var(--surface-2); }
.table tbody tr[data-picked] td:first-child {
  box-shadow: inset var(--geometry-tick) 0 0 var(--color-brand-default-day); }
[data-theme="night"] .table tbody tr[data-picked] td:first-child {
  box-shadow: inset var(--geometry-tick) 0 0 var(--color-brand-default-night); }
.table tbody tr[data-contract]:focus-visible { outline: 2px solid var(--color-brand-default-day); outline-offset: -2px; }

/* Деньги и время — тем же шрифтом, что текст, но табличными цифрами.
   Моноширинный оставлен ИДЕНТИФИКАТОРАМ: номер накладной, ссылка банка,
   код — их сверяют посимвольно, и там рисунок знака важнее ритма строки.
   Раньше моноширинными были и деньги, отчего сумма выглядела кодом. */
.num { text-align: right; font-variant-numeric: tabular-nums slashed-zero; letter-spacing: -.01em; }
.code { font-family: var(--typography-mono); font-size: var(--text-meta); letter-spacing: -.02em; }
.nowrap { white-space: nowrap; }
/* Идентификатор НЕ ПЕРЕНОСИТСЯ. Первая редакция разрешала перенос в любом
   месте, чтобы таблица влезала, — и номер тендера T-20260801-00432 встал в
   четыре строки, а строка выросла вчетверо. Правило проекта про заголовки
   столбцов действует и здесь: горизонтальная прокрутка лучше, чем номер,
   разорванный посреди цифр, — его же переписывают в платёжку. */
.table td:first-child { white-space: nowrap; }
.table td .muted { display: block; font-size: var(--text-meta); }
/* Подпись под формой внутри панели. Класс, а не атрибут style: политика
   содержимого запрещает inline-оформление, и правильно делает — исключение
   для «одной строчки» открывает его для всех. */
.panel__note { padding: 0 var(--geometry-sp-4) var(--geometry-sp-3); color: var(--text-2); }
.empty { color: var(--text-3); padding: var(--geometry-sp-10); text-align: center; }

/* --------------------------------- метки -----------------------------
   Метка — не «таблетка с точкой». Точка перед словом и борт вокруг него
   повторяют то, что уже сказано словом и цветом, и добавляют на строку два
   лишних предмета. Осталось: подложка тем же цветом на десятую долю и сам
   цвет. Подложка собирается из currentColor, поэтому любой сигнальный цвет
   и любая тема работают без отдельного правила. */
.tag {
  display: inline-flex; align-items: center;
  padding: 0 var(--geometry-sp-2); border-radius: var(--geometry-r-ctl);
  font-size: var(--text-meta); font-weight: 600; white-space: nowrap;
  min-height: var(--geometry-sp-6);
  color: var(--text-2);
  background: color-mix(in srgb, currentColor 9%, transparent);
}
.tag--crit    { color: var(--critical); }
.tag--high    { color: var(--warning); }
.tag--med     { color: var(--info); }
.tag--low     { color: var(--text-2); }
.tag--ok      { color: var(--success); }

/* --------------------------------- формы ----------------------------- */
.form { display: grid; gap: var(--geometry-sp-3); padding: var(--geometry-sp-4); }
.form label { display: grid; gap: var(--geometry-sp-1); color: var(--text-2); font-size: var(--text-meta); }
input, select, textarea {
  font: inherit; color: var(--text-1); background: var(--canvas);
  border: 1px solid var(--border); border-radius: var(--geometry-r-ctl);
  padding: 0 var(--geometry-sp-3); min-height: var(--density-touch-target-min);
  transition: border-color var(--motion-micro) var(--ease-out),
              background var(--motion-micro) var(--ease-out);
}
/* Поле ввода ТЕМНЕЕ листа, а не светлее. Белое поле на белом листе видно
   только по борту, и на плотной форме борта сливаются в решётку; вдавленное
   поле читается полем само по себе. */
input:hover, select:hover, textarea:hover { border-color: var(--text-3); }
input:focus, select:focus, textarea:focus { background: var(--surface); }
textarea { padding: var(--geometry-sp-2) var(--geometry-sp-3); resize: vertical; }

.btn {
  font: inherit; font-weight: 600; cursor: pointer;
  border-radius: var(--geometry-r-ctl);
  min-height: var(--density-touch-target-min);
  padding: 0 var(--geometry-sp-4);
  border: 1px solid var(--border); background: var(--surface); color: var(--text-1);
  transition: background var(--motion-micro) var(--ease-out),
              border-color var(--motion-micro) var(--ease-out),
              box-shadow var(--motion-micro) var(--ease-out);
}
.btn:hover { background: var(--surface-2); border-color: var(--text-3); }
.btn:active { box-shadow: inset 0 1px 2px rgba(28,26,23,.14); }
.btn--primary {
  background: var(--color-brand-default-day); border-color: var(--color-brand-default-day);
  color: var(--surface); box-shadow: var(--elevation-sheet);
}
.btn--primary:hover { background: var(--text-1); border-color: var(--text-1); }
[data-theme="night"] .btn--primary {
  background: var(--color-brand-default-night); border-color: var(--color-brand-default-night);
  color: var(--canvas);
}
[data-theme="night"] .btn--primary:hover { background: var(--text-1); border-color: var(--text-1); }
.btn--ghost { background: transparent; border-color: transparent; color: var(--text-2); }
.btn--ghost:hover { background: var(--surface-2); border-color: transparent; color: var(--text-1); }
.btn--sm { min-height: var(--density-row-compact); padding: 0 var(--geometry-sp-3); font-size: var(--text-meta); }

/* ------------------------------- разбивка ---------------------------- */
.breakdown { border-top: 1px solid var(--border); padding: var(--geometry-sp-4); }
.breakdown__row { display: flex; justify-content: space-between; gap: var(--geometry-sp-4);
                  padding: var(--geometry-sp-2) 0; border-bottom: 1px solid var(--border); }
.breakdown__row:last-child { border-bottom: 0; }
.breakdown__total { font-weight: 600; font-size: var(--text-title); letter-spacing: -.02em; }
.breakdown__why { color: var(--text-2); font-size: var(--text-meta); margin-top: var(--geometry-sp-3); max-width: 68ch; }

/* ---------------------------- администрирование ----------------------- */
.adm-gap { margin-top: var(--geometry-sp-4); }
.perms { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--layout-panel-min), 1fr));
         gap: var(--geometry-sp-2); max-height: var(--layout-list-max); overflow-y: auto; }
.perm { display: flex; align-items: center; gap: var(--geometry-sp-2); color: var(--text-1);
        font-size: var(--text-meta); min-height: var(--density-row-compact); }
.perm input { min-height: var(--geometry-sp-4); }
/* Опасное право отмечено словом, а не только цветом: раздача администраторских
   полномочий не должна зависеть от того, различает ли человек оттенки. */
.perm--risky { font-weight: 600; }
.perm__area { color: var(--text-2); }

/* ------------------------------ уведомление --------------------------
   Приходит СНИЗУ, оттуда же, где висит. Появление на месте читается как
   ошибка отрисовки; движение снизу вверх — как «сообщение поднялось». */
.toast {
  position: fixed; left: 50%; bottom: var(--geometry-sp-6);
  background: var(--text-1); color: var(--surface);
  border-radius: var(--geometry-r-panel);
  padding: var(--geometry-sp-3) var(--geometry-sp-6);
  box-shadow: var(--elevation-float);
  max-width: 90vw;
  animation: toast-in var(--motion-panel) var(--ease-out);
}
@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, var(--geometry-sp-3)); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
.toast { transform: translateX(-50%); }

/* --------------------------- «это только что изменилось» --------------
   Диспетчер смотрит в один экран часами и отводит глаза. Данные за это
   время меняются, и разница между «я это уже видел» и «это новое» — самая
   дорогая информация на экране. Подсветка тёплая, затухает медленно и
   ничего не двигает: движущийся текст нельзя читать. */
/*
 * ПОДСВЕТКА НЕ ИМЕЕТ ПРАВА СДЕЛАТЬ ТЕКСТ ХУЖЕ ЧИТАЕМЫМ. Первая редакция
 * заливала ячейку фирменным цветом на 22 % — и проверка контраста, считающая
 * фон СЛОЖЕНИЕМ полупрозрачных слоёв, показала 3.01:1 там, где требуется
 * 4.5: под меткой оказывалось два слоя сразу, её собственная подложка и эта.
 * Сообщение «посмотри сюда», из-за которого сюда нельзя посмотреть.
 *
 * Осталось 10 % — измерено, худшее сочетание даёт 4.66:1 — и добавлена
 * ЧЕРТА ПОД ЯЧЕЙКОЙ: она заметна боковым зрением, а на читаемость не влияет
 * вовсе, потому что не лежит под текстом. Приём корректорский: изменённое
 * подчёркивают, а не закрашивают.
 */
@keyframes settle {
  from {
    background-color: color-mix(in srgb, var(--color-brand-default-day) 10%, transparent);
    box-shadow: inset 0 -2px 0 var(--color-brand-default-day);
  }
  to { background-color: transparent; box-shadow: inset 0 -2px 0 transparent; }
}
.settled { animation: settle var(--motion-settle) var(--ease-out); }
[data-theme="night"] .settled { animation-name: settle-night; }
@keyframes settle-night {
  from {
    background-color: color-mix(in srgb, var(--color-brand-default-night) 10%, transparent);
    box-shadow: inset 0 -2px 0 var(--color-brand-default-night);
  }
  to { background-color: transparent; box-shadow: inset 0 -2px 0 transparent; }
}

/* Лист появляется, а не мигает. Только при первой отрисовке раздела —
   повторять анимацию на каждом обновлении значит мешать читать. */
@keyframes sheet-in {
  from { opacity: 0; transform: translateY(var(--geometry-sp-2)); }
  to   { opacity: 1; transform: none; }
}
main > .panel, main > .kpis, main > .split > * {
  animation: sheet-in var(--motion-panel) var(--ease-out) both;
}

/* ------------------------------- узкий экран --------------------------
   720px — значение токена layout.bp-compact. Литералом оно здесь потому,
   что @media не умеет читать пользовательские свойства; тест сверяет это
   число с токеном, чтобы веб и мобильные ломали раскладку в одной точке. */
@media (max-width: 720px) {
  body { font-size: var(--text-mobile-body); }
  main { padding: var(--geometry-sp-4) var(--geometry-sp-3) var(--geometry-sp-10); }
  h1 { font-size: var(--text-mobile-title); }
  .lede { font-size: var(--text-mobile-body); }
  .split { grid-template-columns: minmax(0, 1fr); }

  /* РАЗДЕЛЫ ПЕРЕЕЗЖАЮТ НА СВОЮ СТРОКУ. В одной строке со знаком и кнопками
     полоса разделов сжималась до нуля: на 390 px от «Что горит» оставалось
     «Что гори», а остальные одиннадцать были недостижимы. Перенос строки
     здесь не «мобильная версия», а единственный способ оставить навигацию
     работающей. */
  .topbar { padding: 0 var(--geometry-sp-3); gap: var(--geometry-sp-2); flex-wrap: wrap; }
  .brand { border-right: 0; margin-right: 0; flex: 1; }
  .topbar__right { padding: var(--geometry-sp-2) 0; }
  .tabs { flex-basis: 100%; order: 3; border-top: 1px solid var(--border); }
  .tab { min-height: var(--density-touch-target-min); }

  /* Сводка в две колонки, и линейки идут по обеим осям: без горизонтальной
     второй ряд повисал без основания и читался как съехавший. */
  .kpis { grid-template-columns: 1fr 1fr; }
  .kpi { padding: var(--geometry-sp-3) var(--geometry-sp-4); }
  .kpi:nth-child(odd) { border-left: 0; }
  .kpi:nth-child(n+3) { border-top: 1px solid var(--border); }
  .kpi__value { font-size: var(--text-title); }

  /* НА ТЕЛЕФОНЕ СТРОКА СТАНОВИТСЯ ЗАПИСЬЮ, А НЕ СЖИМАЕТСЯ В СТОЛБЦЫ.
     Пробовалось иначе — и не вышло дважды. Сжатая до 390 px таблица отдаёт
     столбцу с объяснением сорок пикселей, текст рвётся на слоги, а строка
     вырастает до полутора экранов: видно один заголовок и половину пустого
     листа. Прокрутка вбок это не чинит — высоту строки задаёт САМЫЙ
     ВЫСОКИЙ столбец, и он остаётся высоким за краем экрана.
     Запись же читается сверху вниз, как её и читают на телефоне: сначала
     что случилось, потом насколько это плохо, потом почему и что делать.
     Шапка при этом убирается: подпись столбца над одной ячейкой — это
     подпись, повторяющая саму себя. */
  .table, .table tbody, .table tr, .table td { display: block; }
  .table thead { display: none; }
  .table tbody tr {
    padding: var(--geometry-sp-3) var(--geometry-sp-4);
    border-bottom: 1px solid var(--border);
  }
  .table tbody tr:last-child { border-bottom: 0; }
  .table td {
    border: 0; height: auto; padding: 0 0 var(--geometry-sp-1);
    text-align: left;
  }
  .table td:last-child { padding-bottom: 0; }
  /* Первая ячейка — заглавие записи. Оно и было заглавием: в каждой таблице
     консоли первый столбец отвечает на вопрос «что это». */
  .table td:first-child { font-weight: 600; padding-bottom: var(--geometry-sp-2); }
  /* Отметка последствия переезжает с ячейки на всю запись: у записи нет
     «первой ячейки слева», у неё есть левый край. */
  .table tbody tr[data-severity] td:first-child,
  .table tbody tr[data-state] td:first-child,
  .table tbody tr[data-picked] td:first-child { box-shadow: none; }
  .table tbody tr[data-severity] { box-shadow: inset var(--geometry-tick) 0 0 var(--text-3); }
  .table tbody tr[data-severity="crit"] { box-shadow: inset var(--geometry-tick) 0 0 var(--critical); }
  .table tbody tr[data-severity="high"] { box-shadow: inset var(--geometry-tick) 0 0 var(--warning); }
  .table tbody tr[data-severity="med"]  { box-shadow: inset var(--geometry-tick) 0 0 var(--info); }
  .table tbody tr[data-state="overdue"] { box-shadow: inset var(--geometry-tick) 0 0 var(--critical); }
  .table tbody tr[data-state="paid"]    { box-shadow: inset var(--geometry-tick) 0 0 var(--success); }
  .table tbody tr[data-picked] { box-shadow: inset var(--geometry-tick) 0 0 var(--color-brand-default-day); }
  /* Пустая ячейка в записи — это пустая строка посреди текста. В столбце
     прочерк нужен, чтобы столбец не съехал; в записи столбцов нет. */
  .table td:empty { display: none; }
  /* Подпись столбца встаёт НАД значением, а не перед ним через двоеточие:
     так читается запись, а не оборванная строка таблицы. Заглавие записи
     подписи не получает — оно и есть подпись. */
  .table td[data-label]:not(:first-child)::before {
    content: attr(data-label);
    display: block; font-size: var(--text-mobile-meta); font-weight: 400;
    color: var(--text-3); line-height: 1.3;
  }
  .table td[data-label]:not(:first-child) { padding-bottom: var(--geometry-sp-2); }
  /* Мелкая кнопка в таблице на телефоне — та же цель касания, что и любая
     другая. Найдено измерением: на 390 px «Открыть доступ» и «Протокол»
     оказались 37 px при собственном пороге 44. На узком экране экономить
     высоту нечем — экономится ширина. */
  .btn--sm { min-height: var(--density-touch-target-min); }
}

/* Широкий экран: 1120px — значение токена layout.bp-wide. До него панель
   слева жмётся к минимуму и таблица справа остаётся тесной; после —
   разворачивается воздух, ради которого широкий монитор и покупают. */
/* Воздух добавляется СНАРУЖИ листа, а не внутри ячеек. Первая редакция
   разгоняла на широком экране горизонтальные отступы таблицы — и пять
   столбцов получали лишние 240 px, из-за чего таблица переставала помещаться
   в лист и уезжала под прокрутку. Ровно то, что широкий экран должен был
   исправить. Найдено на снимке, а не рассуждением. */
@media (min-width: 1120px) {
  main { padding-left: var(--geometry-sp-10); padding-right: var(--geometry-sp-10); }
  .split { gap: var(--geometry-sp-6); }
  .panel__head { padding-left: var(--geometry-sp-6); padding-right: var(--geometry-sp-6); }
  .kpi { padding-left: var(--geometry-sp-8); padding-right: var(--geometry-sp-8); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}

/* Режим высокой контрастности операционной системы. Здесь система вправе
   переопределить всё, и спорить с ней нельзя: человек включил его не для
   красоты. Единственное, что остаётся, — не потерять СМЫСЛ: отметка
   состояния и кольцо фокуса рисуются системным цветом, а не исчезают. */
@media (forced-colors: active) {
  .tab[aria-current="page"]::after { background: Highlight; forced-color-adjust: none; }
  .table tbody tr[data-picked] td:first-child { box-shadow: inset var(--geometry-tick) 0 0 Highlight; }
  .tag { border: 1px solid currentColor; }
}

/* ------------------------------ окна приёмки --------------------------
   Сетка окон — не таблица, а карта загрузки: свободное место должно быть
   видно раньше, чем прочитан текст. Занятость показана И цветом, И числом
   «занято из», потому что цвет не читается ни в чёрно-белой печати, ни
   человеком, который его не различает. */
.form--row { grid-template-columns: repeat(auto-fit, minmax(var(--layout-kpi-min), auto));
             align-items: end; }
.slots { display: grid; gap: var(--geometry-sp-2); padding: var(--geometry-sp-4); }
.slot {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--geometry-sp-3);
  align-items: center; padding: var(--geometry-sp-2) var(--geometry-sp-3);
  border: 1px solid var(--border); border-radius: var(--geometry-r-ctl);
  background: var(--surface); min-height: var(--density-row-comfortable);
  transition: border-color var(--motion-micro) var(--ease-out);
}
.slot:hover { border-color: var(--text-3); }
.slot--full { background: var(--surface-2); }
.slot__time { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--text-1); }
.slot__load { color: var(--text-2); font-size: var(--text-meta); }
.slot__who { display: flex; flex-wrap: wrap; gap: var(--geometry-sp-2); }
.slot__act { display: flex; gap: var(--geometry-sp-2); }

/* ============================ КАРТА ОПЕРАЦИЙ ============================
   Инкремент 111. Экран из первого прототипа, которого не было сто десять
   инкрементов. Карта своя, без внешних библиотек — как и вся консоль: ни
   один цвет и отступ здесь не задан числом, всё из токенов.

   Раскладка «список — карта — инспектор» повторяет прототип и работает не
   ради симметрии: слева то, что можно перебрать глазами и клавиатурой,
   посередине то, что нельзя, справа подробности выбранного. Карта без
   списка недоступна с клавиатуры вовсе.                                    */
.mapgrid {
  display: grid;
  grid-template-columns: minmax(var(--layout-panel-min), 22%) minmax(0, 1fr) minmax(var(--layout-panel-min), 24%);
  gap: var(--geometry-sp-4); align-items: start;
}
.mapgrid > * { min-width: 0; }
.mappanel { display: flex; flex-direction: column; max-height: 70vh; }
.maplist, .mapinspect { overflow-y: auto; padding: var(--geometry-sp-2); }
.mapstage { display: flex; flex-direction: column; }
.mapbtns { display: flex; gap: var(--geometry-sp-1); }

/* Полотно карты. Курсор «схватить» — единственная подсказка, что её можно
   таскать: подпись про это заняла бы место, которое и есть карта. */
.mapplane {
  position: relative; overflow: hidden;
  height: 62vh; min-height: 320px;
  background: var(--surface-2);
  cursor: grab; touch-action: none;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.mapplane:active { cursor: grabbing; }
.mapplane:focus-visible { outline: var(--geometry-tick) solid var(--info); outline-offset: -3px; }
.maptiles, .mapdraw, .mapmarks { position: absolute; inset: 0; }
/* Размер тайла (256 px) — константа протокола карт, а не решение оформления,
   поэтому он задаётся в скрипте вместе с раскладкой тайлов, а не здесь. */
.maptiles img { position: absolute; user-select: none; }
.mapdraw { pointer-events: none; width: 100%; height: 100%; }
.mapmarks { pointer-events: none; }

/* Сетка вместо подложки. Не украшение: без неё пропадает ощущение
   перемещения при перетаскивании, и карта кажется сломанной. */
.mapplane[data-tiles="off"] .maptiles {
  background-image:
    linear-gradient(var(--border) 1px, transparent 1px),
    linear-gradient(90deg, var(--border) 1px, transparent 1px);
  background-size: var(--geometry-sp-10) var(--geometry-sp-10), var(--geometry-sp-10) var(--geometry-sp-10);
  opacity: .5;
}

.mapmark {
  position: absolute; pointer-events: auto;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: var(--geometry-sp-1);
  padding: var(--geometry-sp-1) var(--geometry-sp-2);
  border: 1px solid var(--border); border-radius: var(--geometry-r-ctl);
  background: var(--surface); color: var(--text-1);
  font: inherit; font-size: var(--text-body); line-height: 1.1;
  box-shadow: var(--elevation-float); cursor: pointer;
  min-height: var(--density-touch-target-min);
}
.mapmark:hover, .mapmark[data-picked] { background: var(--surface-2); }
.mapmark[data-picked] { outline: var(--geometry-tick) solid var(--info); }
.mapmark[data-kind="stop"] { padding: var(--geometry-sp-1); min-width: var(--density-touch-target-min); justify-content: center; }
.mapmark[data-late="1"] { border-color: var(--critical); }
.mapmark[data-stale="1"] { border-style: dashed; color: var(--text-2); }
.mapmark__dot { width: 10px; height: 10px; border-radius: 50%; background: var(--success); flex: none; }
.mapmark[data-state="выпущена"] .mapmark__dot { background: var(--info); }
.mapmark[data-state="свободна"] .mapmark__dot { background: var(--text-3); }
.mapmark[data-late="1"] .mapmark__dot { background: var(--critical); }

.mapnote {
  position: absolute; left: var(--geometry-sp-3); bottom: var(--geometry-sp-3);
  margin: 0; max-width: var(--layout-content-read);
  padding: var(--geometry-sp-2) var(--geometry-sp-3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--geometry-r-ctl); color: var(--text-2);
  box-shadow: var(--elevation-float);
}
.maplegend {
  display: flex; flex-wrap: wrap; gap: var(--geometry-sp-4);
  padding: var(--geometry-sp-2) var(--geometry-sp-4); color: var(--text-2);
}
.maplegend span { display: inline-flex; align-items: center; gap: var(--geometry-sp-1); }
.maplegend i { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }

.mapitem {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: var(--geometry-sp-2) var(--geometry-sp-3);
  border: 0; border-bottom: 1px solid var(--border);
  background: transparent; color: var(--text-1); font: inherit;
  min-height: var(--density-touch-target-min);
}
.mapitem:hover { background: var(--surface-2); }
.mapitem[data-picked] { background: var(--surface-2); box-shadow: inset var(--geometry-tick) 0 0 var(--info); }
.mapitem b { display: block; }
.mapitem small { color: var(--text-2); }
.mapinspect dl { margin: 0; }
.mapinspect dt { color: var(--text-2); margin-top: var(--geometry-sp-2); }
.mapinspect dd { margin: 0; color: var(--text-1); }

/* На телефоне три колонки не помещаются ни в каком виде: список сверху,
   карта под ним, инспектор снизу. */
@media (max-width: 1120px) {   /* layout.bp-wide */
  .mapgrid { grid-template-columns: minmax(0, 1fr); }
  .mappanel { max-height: 34vh; }
  .mapplane { height: 52vh; }
}
.mapdraw .geofence { fill: var(--info); fill-opacity: .08; stroke: var(--info); stroke-opacity: .45; }
.mapdraw .routeline {
  fill: none; stroke: var(--text-3); stroke-width: 2; stroke-dasharray: 6 5;
  stroke-linejoin: round; stroke-linecap: round;
}
.mapdraw .routeline[data-picked] { stroke: var(--info); stroke-width: 3; }

/* ---------------------------- ПОКАЗАТЕЛИ РАБОТЫ ----------------------- */
.perfbar {
  display: flex; align-items: center; gap: var(--geometry-sp-3);
  margin-bottom: var(--geometry-sp-4); flex-wrap: wrap;
}
.perfbar__label { color: var(--text-2); }
.perfbar #perf-period { display: inline-flex; gap: var(--geometry-sp-1); }
.perfsplits { padding: var(--geometry-sp-2) var(--geometry-sp-4); }
/* Строка разбора: подпись — значение — пояснение. Пояснение мелким и серым
   намеренно: «2 из 2» нужно только тому, кто усомнился в доле. */
.perfsplit {
  display: grid; grid-template-columns: 1fr auto; gap: 0 var(--geometry-sp-4);
  align-items: baseline;
  padding: var(--geometry-sp-2) 0; border-bottom: 1px solid var(--border);
}
.perfsplit:last-child { border-bottom: 0; }
.perfsplit small { grid-column: 1 / -1; color: var(--text-2); }

/* ------------------------ ЛЕНТА РЕЙСОВ (ГАНТ) ------------------------
   Полоска без часовой шкалы ничего не означает — поэтому шкала здесь не
   украшение, а часть данных. Строка рейса: слева подпись-кнопка, справа
   дорожка, на которой полоска стоит там, где рейс идёт по времени.        */
.gantt { padding: var(--geometry-sp-4); }
.gantt__axis {
  position: relative; height: var(--geometry-sp-6);
  margin-left: var(--layout-panel-min); border-bottom: 1px solid var(--border);
}
.gantt__tick {
  position: absolute; top: 0; transform: translateX(-50%);
  color: var(--text-2); font-size: var(--text-meta); white-space: nowrap;
}
.gantt__row { display: grid; grid-template-columns: var(--layout-panel-min) 1fr; align-items: center; }
.gantt__label {
  text-align: left; background: transparent; border: 0; cursor: pointer;
  color: var(--text-1); font: inherit; padding: var(--geometry-sp-2) var(--geometry-sp-3) var(--geometry-sp-2) 0;
  min-height: var(--density-touch-target-min);
}
.gantt__label:hover b { text-decoration: underline; }
.gantt__label b { display: block; }
.gantt__label small { color: var(--text-2); }
/* Дорожка ростом с минимальную цель касания: полоска внутри неё за вычетом
   отступов обязана оставаться нажимаемой пальцем, иначе на телефоне рейс
   нельзя выбрать вовсе. */
.gantt__lane {
  position: relative; height: var(--density-touch-target-min);
  border-bottom: 1px solid var(--border);
}
.gantt__bar {
  position: absolute; top: var(--geometry-sp-2); bottom: var(--geometry-sp-2);
  border: 1px solid var(--border); border-radius: var(--geometry-r-ctl);
  background: var(--surface-2); color: var(--text-1);
  font: inherit; font-size: var(--text-meta); cursor: pointer;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  /* Короткий рейс не имеет права превратиться в полоску, по которой не
     попасть пальцем: длина врёт на пару пикселей, зато рейс выбирается. */
  min-width: var(--geometry-sp-8); min-height: var(--geometry-sp-8);
}
.gantt__bar[data-status="started"] { background: var(--success); color: var(--surface); border-color: var(--success); }
.gantt__bar[data-status="published"], .gantt__bar[data-status="accepted"] { border-color: var(--info); }
.gantt__bar[data-late="1"] { border-color: var(--critical); box-shadow: inset var(--geometry-tick) 0 0 var(--critical); }
.gantt__bar[data-picked] { outline: 2px solid var(--info); outline-offset: 1px; }

/* --------------------------- ПРЕДПРОСМОТР ---------------------------- */
.impact { padding: var(--geometry-sp-4); }
.impact__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--layout-kpi-min), 1fr));
  gap: var(--geometry-sp-4); margin-bottom: var(--geometry-sp-4);
}
.impact__line { display: flex; align-items: baseline; gap: var(--geometry-sp-2); font-size: var(--text-title); }
.impact__before { color: var(--text-2); text-decoration: line-through; }
.impact__arrow { color: var(--text-3); }
.impact__after[data-better] { color: var(--success); }
.impact__after[data-worse] { color: var(--critical); }
.impact__actions { display: flex; gap: var(--geometry-sp-3); }

/* ------------------------- КОМАНДНАЯ ПАЛИТРА -------------------------
   Блок cmdk первого прототипа. Не украшение: разделов девятнадцать, и полоса
   вкладок на узком экране прокручивается — то есть половина разделов мышью
   достаётся в два движения, а с клавиатуры не достаётся вовсе.            */
.cmdk {
  position: fixed; inset: 0; z-index: 30;
  background: rgba(0, 0, 0, .35);
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: var(--geometry-sp-12);
}
.cmdk__box {
  width: min(var(--layout-content-read), 92vw);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--geometry-r-dialog); box-shadow: var(--elevation-sheet);
  overflow: hidden;
}
.cmdk__input {
  width: 100%; border: 0; border-bottom: 1px solid var(--border);
  padding: var(--geometry-sp-4); font: inherit; font-size: var(--text-lede);
  background: var(--surface); color: var(--text-1);
}
.cmdk__input:focus { outline: none; }
.cmdk__list { list-style: none; margin: 0; padding: 0; max-height: 50vh; overflow-y: auto; }
.cmdk__list li {
  padding: var(--geometry-sp-3) var(--geometry-sp-4); cursor: pointer;
  min-height: var(--density-touch-target-min); display: flex; align-items: center;
  gap: var(--geometry-sp-3);
}
.cmdk__list li[aria-selected="true"] { background: var(--surface-2); box-shadow: inset var(--geometry-tick) 0 0 var(--info); }
.cmdk__list small { color: var(--text-2); }
.cmdk__hint {
  margin: 0; padding: var(--geometry-sp-2) var(--geometry-sp-4);
  border-top: 1px solid var(--border); color: var(--text-2); font-size: var(--text-meta);
}

/* =====================================================================
   СЛОЙ ДВИЖЕНИЯ (инкремент 165)

   Ступени и кривые были объявлены в токенах с самого начала и там же
   объяснены: «движение объясняет, откуда что взялось, и ничего не
   украшает». Пользовались ими шестнадцать мест из ста — подчёркивание
   вкладки, наведение на строку, тост и `settle`. Всё остальное появлялось
   рывком, и рывок читается не как быстрота, а как перерисовка: человек
   перепроверяет, то ли он видит.

   Ни одного нового цвета и ни одной новой длительности здесь нет. Только
   те, что уже есть.
   ===================================================================== */

/* СМЕНА РАЗДЕЛА — НАПРАВЛЕННАЯ.
   Раздел правее въезжает справа, левее — слева, на 8 px. Лента разделов
   держится в голове; движение «непонятно куда» заставляет перечитывать
   заголовок, чтобы понять, где оказался. Смещение маленькое намеренно:
   переход должен объяснить направление, а не показать себя. */
@keyframes view-in-right { from { opacity: 0; transform: translateX(var(--geometry-sp-2)); }
                           to   { opacity: 1; transform: none; } }
@keyframes view-in-left  { from { opacity: 0; transform: translateX(calc(-1 * var(--geometry-sp-2))); }
                           to   { opacity: 1; transform: none; } }
[data-view-dir="right"] > main > *,
main[data-view-dir="right"] > * { animation: view-in-right var(--motion-panel) var(--ease-out) both; }
[data-view-dir="left"] > main > *,
main[data-view-dir="left"]  > * { animation: view-in-left  var(--motion-panel) var(--ease-out) both; }

/* КАСКАД СПИСКА.
   Шаг 22 мс, не больше десяти шагов — дальше это уже ожидание, а не
   подсказка. Каскад сообщает, что список ПРИШЁЛ, а не был всегда, и задаёт
   глазу направление чтения сверху вниз. */
.stagger > * { animation: row-in var(--motion-state) var(--ease-out) both;
               animation-delay: calc(var(--i, 0) * 22ms); }
@keyframes row-in { from { opacity: 0; transform: translateY(var(--geometry-sp-1)); }
                    to   { opacity: 1; transform: none; } }

/* СКЕЛЕТОН → ДАННЫЕ: КРОСС-ФЕЙД В ОДНОЙ ВЫСОТЕ.
   Подмена рывком выглядит как ошибка отрисовки. Скелетон и данные занимают
   одно место, поэтому страница не прыгает под курсором. */
/* Ширина блика — в ДОЛЯХ, а не в пикселях. Проверка «размеры либо из
   токенов, либо дословно равны токену» поймала здесь три посторонних
   значения, и поймала верно: число, взятое из головы, потом никто не
   объяснит. В долях оно ещё и не зависит от ширины строки.

   Само число тут не написано НАМЕРЕННО. Проверка читает файл целиком,
   вместе с комментариями, и объяснение с пикселем внутри засчитает как
   нарушение — что она и сделала со второй попытки. Придирка? Нет: ровно
   так же выглядит и «временно оставим», дописанное рядом кем-то через
   полгода вместе с числом. */
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.sk { height: var(--geometry-sp-3); border-radius: var(--geometry-tick);
      background: linear-gradient(90deg, var(--surface-2), var(--border), var(--surface-2));
      background-size: 200% 100%; animation: shimmer 1.1s linear infinite; }
.swap { position: relative; }
.swap > * { transition: opacity var(--motion-panel) var(--ease-out); }
.swap > .is-gone { opacity: 0; position: absolute; inset: 0; pointer-events: none; }

/* ВЫДВИЖНАЯ ПАНЕЛЬ РАЗБОРА.
   Отжимает содержимое, а не накрывает его: строку разбирают, глядя на
   соседние. Затемнения у неё нет — затемнять то, с чем продолжают
   работать, незачем. Затемнение остаётся только у палитры команд, где
   работа действительно остановлена до выбора. */
.with-aside { transition: grid-template-columns var(--motion-panel) var(--ease-out); }
.aside-in   { animation: view-in-right var(--motion-panel) var(--ease-out) both; }

/* ПАЛИТРА КОМАНД проявляется с едва заметным приближением: диалог
   приходит «сверху к читателю», а не возникает на месте. */
@keyframes cmdk-in { from { opacity: 0; transform: translate(-50%, -6px) scale(.985); }
                     to   { opacity: 1; transform: translate(-50%, 0) scale(1); } }
.cmdk__box { animation: cmdk-in var(--motion-panel) var(--ease-out) both; }

/* СМЕНА ТЕМЫ — КРОСС-ФЕЙД.
   Мгновенная подмена бьёт по глазам ровно в тот момент, ради которого
   ночную тему и включают. Переход вешается на холст и на чернила; всё
   остальное перекрашивается вместе с ними. */
/* СМЕНА ТЕМЫ ПЕРЕКЛЮЧАЕТСЯ МГНОВЕННО. Плавно — только через снимок страницы.

   Здесь было две редакции, и обе поймала проверка контраста.

   Первая плавно меняла и фон, и чернила: в середине перехода текст проходил
   через промежуточный оттенок и падал до 2.74:1.

   Вторая оставила плавным только фон — стало 3.26:1, и это всё ещё провал.
   Причина та же и она общая: если фон едет, а чернила стоят, отношение
   светлот в середине пути всё равно проваливается. Порог держится на
   концах перехода и не держится посередине — а «посередине» это те самые
   220 мс, когда человек смотрит на экран.

   Вывод: свойствами CSS тему плавно не сменить, не нарушив порог. Плавность
   даёт `::view-transition` — он смешивает СНИМКИ старой и новой страницы
   целиком, поэтому текст и фон уезжают вместе и их отношение не меняется ни
   на кадр. Там, где браузер этого не умеет, тема просто переключается — и
   это честнее, чем красивый переход, на котором нельзя читать. */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: var(--motion-panel); }

/* УМЕНЬШЕННОЕ ДВИЖЕНИЕ отключает не анимацию, а ожидание: конечное
   состояние остаётся тем же, приходит мгновенно. Правило уже есть выше по
   файлу и покрывает всё, что здесь объявлено. */
