/* Компоненты админки fbswb. Цвета, отступы и тайминги — только через
 * токены theme.css: собственный оттенок здесь означал бы, что правка
 * темы перестаёт менять этот экран.
 *
 * Экран рассчитан на ПК: оператор смотрит плотную таблицу заданий на
 * широком мониторе. Отсюда высокая плотность и залипающие заголовки —
 * ценность экрана измеряется тем, сколько заданий видно без прокрутки и
 * помнит ли человек, какая колонка перед ним, на трёхсотой строке.
 */

/* ============================================================ базовое */

/* Клавиатурный фокус виден ВСЕГДА и одинаково. Отдельным правилом и в
   самом начале — чтобы никакой компонент ниже не мог его отменить
   незаметно. :focus-visible, а не :focus: кольцо нужно тому, кто пришёл
   с Tab, а не тому, кто просто кликнул мышью по селекту. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Залипающая шапка + залипающий заголовок таблицы вместе образуют полосу
   высотой --top-h + строка thead. Без этого отступа Tab уводил бы фокус
   ПОД них: элемент получал фокус, браузер прокручивал к нему, и элемент
   оказывался накрыт (WCAG 2.2, 2.4.11 «фокус не перекрыт»). */
a,
button,
input,
select,
summary,
[tabindex] {
  scroll-margin-top: calc(var(--top-h) + 44px);
}

/* Скрыто от глаз, но НЕ от скринридера. Не display:none и не hidden —
   те убирают элемент из дерева доступности целиком, то есть отменяют
   ровно то, ради чего подпись и написана. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Ссылка для клавиатуры: первым Tab на странице — «к таблице», мимо
   переключателя контуров и фильтров. Видимой становится только при
   фокусе, мышью её не встретить. */
.skip {
  position: absolute;
  left: var(--sp-2);
  top: calc(-1 * var(--top-h) - var(--sp-4));
  z-index: calc(var(--z-header) + 1);
  padding: var(--sp-2) var(--sp-3);
  background: var(--bg-elevated);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  text-decoration: none;
  transition: top var(--transition);
}
.skip:focus {
  top: var(--sp-2);
}

/* =============================================================== шапка */

/* Раскладка экрана: шапка, заголовок, фильтры, счётчик и пагинатор
   стоят на месте, прокручивается ТОЛЬКО таблица. Это и есть плотный
   рабочий экран: на трёхсотой строке фильтры и «сколько всего» должны
   быть перед глазами, а не в четырёх экранах выше.
   Побочно это чинит и залипающий заголовок таблицы — почему, написано
   у .table-wrap. */
body {
  display: flex;
  flex-direction: column;
  /* height, а НЕ min-height. С min-height контейнер имеет право вырасти
     выше экрана — тогда сжимать flex-детям нечего, таблица разворачивается
     во всю длину, и пагинатор уходит под нижний край: замер показывал
     body 1197px при экране 963px и .pager с top=1153. Фиксированная
     высота делает свободное место конечным, и цепочка flex начинает
     работать так, как задумано. */
  height: 100dvh;
  overflow: hidden;
}

.top {
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  height: var(--top-h);
  padding: 0 var(--sp-4);
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--border);
}

.brand {
  font-weight: 650;
  letter-spacing: 0.01em;
}

/* Боевой кабинет виден боковым зрением, а не вычитывается из адресной
   строки. Три носителя разом, потому что цена ошибки — лишняя этикетка
   на живом складе: цветная полоса по всей ширине, красный бренд и слово
   «PROD» текстом. Одного цвета мало — правило «цвет не единственный
   носитель смысла» здесь не формальность, а страховка. */
.top--prod {
  border-bottom: 2px solid var(--err);
  box-shadow: inset 0 3px 0 0 var(--err);
}
.top--prod .brand {
  color: var(--err);
}
.brand-tag {
  margin-left: var(--sp-2);
  padding: 2px var(--sp-2);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--err);
  border: 1px solid var(--err);
  border-radius: var(--radius-sm);
}

/* Переключатель контуров — сегментированный блок, а не россыпь ссылок:
   так видно, что это ОДИН выбор из двух, а активный сегмент читается
   заливкой, а не только цветом текста. */
.contour {
  display: flex;
  gap: 2px;
  padding: 2px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.contour a {
  min-height: 26px;
  display: flex;
  align-items: center;
  padding: 0 var(--sp-3);
  color: var(--text-muted);
  font-size: 13px;
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition:
    background var(--transition),
    color var(--transition);
}
.contour a:hover {
  background: var(--bg-hover);
  color: var(--text);
}
.contour a.is-active {
  background: var(--bg-hover);
  color: var(--text);
  font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--border-strong);
}
/* Боевой сегмент подкрашен и в неактивном состоянии: человек должен
   видеть, КУДА он собирается переключиться, до клика. */
.contour a[data-contour="prod"] {
  color: var(--err);
}

/* Боковое меню разделов.

   Слева — просьба владельца и правило `adaptive-navigation` из
   ui-ux-pro-max §9 (>=1024px предпочитает колонку). Главный компромисс —
   ширина: таблица заданий уже шире экрана, и постоянная колонка отъедает
   ровно то, чего не хватает. Отсюда сворачивание в полосу с иконками. */
.shell {
  display: flex;
  flex: 1;
  min-height: 0;
}
.nav {
  flex: none;
  width: 13rem;
  padding: var(--sp-3) var(--sp-2);
  border-right: 1px solid var(--border);
  background: var(--bg);
  transition: width var(--transition);
}
:root.nav-collapsed .nav {
  width: 3.25rem;
}
:root.nav-collapsed .nav-label {
  display: none;
}
.nav ul {
  list-style: none;
  margin: var(--sp-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.nav a {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 34px;
  padding: 0 var(--sp-3);
  color: var(--text-muted);
  font-size: 13px;
  text-decoration: none;
  border-radius: var(--radius-sm);
  /* Полоса слева всегда занимает место, даже прозрачная: иначе активный
     пункт сдвигал бы текст соседей на два пикселя при каждом переходе. */
  border-left: 2px solid transparent;
  white-space: nowrap;
  transition:
    background var(--transition),
    color var(--transition);
}
.nav a:hover {
  background: var(--bg-hover);
  color: var(--text);
}
.nav a.is-active {
  background: var(--bg-hover);
  color: var(--text);
  font-weight: 600;
  border-left-color: var(--border-strong);
}
.nav-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 30px;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.nav-toggle:hover {
  background: var(--bg-hover);
  color: var(--text);
}

/* min-width: 0 обязателен. Без него широкая таблица распирает флекс-элемент
   до своей полной ширины, и горизонтальная прокрутка внутри .table-wrap,
   которая её и должна держать, просто не возникает — прокручивается вся
   страница вместе с шапкой и меню. */
.shell > main {
  flex: 1;
  min-width: 0;
}

/* Ячейка печати задания: кнопка, форма и результат сменяют друг друга здесь
   же. Минимальная ширина не даёт колонке прыгать при смене состояния —
   иначе вся таблица дёргается на каждый ответ. */
.print-cell {
  min-width: 15ch;
}
.task-print {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}
.task-print select,
.task-print input[type="text"] {
  font-size: 12px;
  max-width: 18ch;
}
.btn-print {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 var(--sp-2);
  color: var(--text-muted);
  font-size: 12px;
  text-decoration: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition:
    background var(--transition),
    color var(--transition);
}
.btn-print:hover {
  background: var(--bg-hover);
  color: var(--text);
}

/* Подробности записи журнала: JSON, развёрнутый в каждой строке, сделал бы
   таблицу нечитаемой, поэтому он живёт в <details>. */
.tasks details summary {
  cursor: pointer;
}
.tasks details pre {
  margin: var(--sp-2) 0 0;
  max-width: 40ch;
  max-height: 12em;
  overflow: auto;
  white-space: pre-wrap;
  font-size: 12px;
  color: var(--text-muted);
}

.spacer {
  flex: 1;
}
.user {
  color: var(--text-muted);
  font-size: 13px;
}
/* Часовой пояс всей системы — сказан один раз в шапке (см. base.html).
   Приглушён намеренно: это подпись, а не состояние, и спорить за внимание
   с пометкой боевого контура она не должна. */
.tz {
  color: var(--text-muted);
  font-size: 12px;
  white-space: nowrap;
}
/* Чем работает выбранный контур: кабинет WB, база CRM, наша база. Стоит
   вплотную к переключателю и приглушено так же, как часовой пояс: это
   подпись под переключателем, а не отдельное сообщение. Прячется первым на
   узком экране — на телефоне важнее сам переключатель, а вопрос «в какую
   базу это ушло» задают за большим экраном. */
.stack {
  display: inline-flex;
  gap: var(--sp-2);
  color: var(--text-muted);
  font-size: 12px;
  white-space: nowrap;
}
.stack-item {
  font-family: var(--font-mono, ui-monospace, monospace);
}
@media (max-width: 900px) {
  .stack {
    display: none;
  }
}
.logout {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  min-height: 26px;
  padding: 0 var(--sp-2);
  color: var(--text-muted);
  font-size: 13px;
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition:
    background var(--transition),
    color var(--transition);
}
.logout:hover {
  background: var(--bg-hover);
  color: var(--text);
}

/* ============================================================== страница */

/* min-height:0 обязателен на каждом звене цепочки: flex-элемент по
   умолчанию не сжимается меньше своего содержимого, и без этого
   .table-wrap не получил бы конечной высоты — прокручиваться было бы
   нечему, а таблица просто вытолкнула бы пагинатор за экран. */
main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: var(--sp-4) var(--sp-4) var(--sp-4);
  /* Предохранительный клапан. В обычной жизни не срабатывает: дети
     сжимаются сами. Включается, только если неподвижная часть экрана
     (шапка, фильтры в две строки, счётчик, пагинатор) уже не влезает в
     низкое окно — тогда лучше прокрутка, чем срезанный и недостижимый
     пагинатор. */
  overflow: auto;
}

/* Неподвижная часть экрана не сжимается: в flex-колонке высота — это
   главная ось, и без flex:none низкое окно начало бы плющить панель
   фильтров и пагинатор вместо того, чтобы отнять высоту у таблицы.
   Отдавать место обязана именно таблица — она единственная прокручиваемая. */
.page-head,
.filters,
.total,
.pager {
  flex: none;
}

.page-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-3);
}
.page-head h1 {
  margin: 0;
  font-size: 18px;
  font-weight: 650;
  letter-spacing: -0.01em;
}
/* Подзаголовок раздела: называет ГРАНИЦЫ того, что на странице. Для
   журнала это не украшение — без него пустая таблица читается как
   «ничего не происходило», хотя сюда попадают только мутации. */
.page-head .hint {
  margin: 0;
  max-width: 70ch;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.5;
}
/* Счётчик — часть заголовка страницы, но живёт в блоке результата: он
   меняется вместе со строками. Оформлен приглушённо, чтобы не спорить с
   заголовком за внимание. */
.total {
  margin: 0;
  color: var(--text-muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

/* =============================================================== фильтры */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-3);
  padding: var(--sp-3);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.filters label {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  color: var(--text-muted);
  font-size: 12px;
}
.filters .field-search {
  flex: 1 1 260px;
  max-width: 380px;
}
.filters .actions {
  display: flex;
  gap: var(--sp-2);
  margin-left: auto;
}

/* Поле с иконкой: иконка декоративная (aria-hidden в разметке) —
   смысл несёт видимая подпись «Поиск», а не лупа. */
.input-icon {
  position: relative;
  display: flex;
}
.input-icon .ico {
  position: absolute;
  left: var(--sp-2);
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
}
.input-icon input {
  width: 100%;
  padding-left: 30px;
}

select,
input,
button {
  min-height: 30px;
  background: var(--bg);
  color: var(--text);
  /* --border-strong, а не --border: это границы УПРАВЛЯЮЩИХ элементов,
     для них WCAG 1.4.11 требует 3:1 к фону. Замерено: 3.4 к холсту,
     3.0+ к любой нашей поверхности. */
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: var(--sp-1) var(--sp-2);
  font: inherit;
  font-size: 13px;
  transition:
    border-color var(--transition),
    background var(--transition);
}
select:hover,
input:hover {
  border-color: var(--text-muted);
}
input::placeholder {
  color: var(--text-muted);
  opacity: 1;
}

button {
  cursor: pointer;
  padding: var(--sp-1) var(--sp-3);
  font-weight: 550;
}
button:hover {
  background: var(--bg-hover);
}
/* Одна главная кнопка на экран; «Сбросить» рядом — заведомо
   второстепенная, поэтому остаётся обычной. */
button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
button.primary:hover {
  filter: brightness(1.08);
}
button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.btn-reset {
  display: inline-flex;
  align-items: center;
  min-height: 30px;
  padding: 0 var(--sp-3);
  color: var(--text-muted);
  font-size: 13px;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  transition:
    color var(--transition),
    border-color var(--transition);
}
.btn-reset:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

/* =============================================================== таблица */

/* Собственная область прокрутки таблицы — по обеим осям.
 *
 * Горизонтальная нужна затем, что десять колонок не обязаны влезать в
 * узкое окно, но и ломать страницу прокруткой всего документа не должны.
 *
 * Вертикальная — не украшение, а условие работы залипающего заголовка.
 * `overflow-x: auto` сам по себе делает элемент контейнером прокрутки по
 * ОБЕИМ осям (CSS: `visible` рядом с `auto` вычисляется в `auto`), и
 * `position: sticky` внутри начинает липнуть к ЭТОМУ контейнеру, а не к
 * окну. Пока у контейнера не было своей высоты, прокручиваться было
 * нечему — и `top: 48px` просто сдвигал строку заголовков на 48px вниз,
 * поверх первой строки данных. Ровно это и было видно на снимке.
 * Дав контейнеру конечную высоту и обнулив `top`, получаем ту самую
 * залипающую шапку, ради которой всё и затевалось. */
.table-wrap {
  flex: 1;
  min-height: 120px;
  overflow: auto;
  /* Докрутив таблицу до края, не утаскиваем за собой страницу целиком —
     иначе на трекпаде выдача «проскакивает» в прокрутку документа. */
  overscroll-behavior: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-surface);
}

table.tasks {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  font-size: 13px;
}

table.tasks th,
table.tasks td {
  text-align: left;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

/* Заголовок липнет под шапкой страницы. На трёхсотой строке человек уже
   не помнит, седьмая колонка — это баркод или склад; прокрутка вверх
   ради этого и есть та мелочь, из которой складывается «неудобно». */
table.tasks thead th {
  position: sticky;
  /* 0, а не высота шапки: липнем к верху СВОЕГО контейнера прокрутки
     (.table-wrap), который и так начинается под шапкой страницы. */
  top: 0;
  z-index: var(--z-sticky);
  background: var(--bg-elevated);
  color: var(--text-muted);
  font-weight: 600;
  font-size: 12px;
  padding: 0;
  border-bottom: 1px solid var(--border-strong);
}

/* Сортировка — ссылка во всю ячейку заголовка: цель клика равна видимой
   шапке колонки, а не подписи в ней (WCAG 2.2, 2.5.8 — минимум 24x24). */
table.tasks thead a {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  min-height: 32px;
  padding: var(--sp-1) var(--sp-3);
  color: inherit;
  text-decoration: none;
  transition:
    background var(--transition),
    color var(--transition);
}
table.tasks thead a:hover {
  background: var(--bg-hover);
  color: var(--text);
}
/* Стрелка направления видна всегда у активной колонки и проявляется по
   наведению у остальных — так видно, что колонка вообще сортируемая. */
table.tasks thead .ico-sort {
  opacity: 0;
  color: var(--text-muted);
  transition: opacity var(--transition);
}
table.tasks thead a:hover .ico-sort {
  opacity: 0.7;
}
table.tasks th[aria-sort] a {
  color: var(--text);
}
table.tasks th[aria-sort] .ico-sort {
  opacity: 1;
  color: var(--accent);
}
/* Активная колонка помечена не только стрелкой, но и полосой сверху:
   направление сортировки не должно держаться на одном мелком глифе. */
table.tasks th[aria-sort] {
  box-shadow: inset 0 2px 0 0 var(--accent);
}

/* Зебра приглушённая: её задача — вести глаз вдоль строки на десяти
   колонках, а не расчерчивать экран. */
table.tasks tbody tr:nth-child(even) td {
  background: color-mix(in oklab, var(--bg-hover) 55%, transparent);
}
table.tasks tbody tr:hover td {
  background: var(--bg-hover);
}
/* Наведение подтверждается ещё и полосой у левого края: на плотной
   таблице разница фонов в 1.4 заметна не всем и не на всяком мониторе. */
table.tasks tbody tr td:first-child {
  box-shadow: inset 2px 0 0 0 transparent;
  transition: box-shadow var(--transition);
}
table.tasks tbody tr:hover td:first-child {
  box-shadow: inset 2px 0 0 0 var(--accent);
}
table.tasks tbody tr:last-child td {
  border-bottom: none;
}

.mono {
  font-family: var(--font-mono);
  font-size: 12px;
}
/* Цифры фиксированной ширины — иначе колонка ID и колонка цены «дышат»
   при каждой смене страницы, и глазу не за что зацепиться. */
.num,
.mono {
  font-variant-numeric: tabular-nums;
}
/* `table.tasks td.num`, а не просто `.num`: базовое правило выше —
   `table.tasks th, table.tasks td` — весит больше одиночного класса, и
   короткая запись молча проигрывала ему выравнивание. Цена оставалась
   прижатой влево, а колонка чисел, выровненная влево, не читается
   столбиком вовсе. */
table.tasks td.num,
table.tasks th.num {
  text-align: right;
}
/* Подпись числовой колонки обязана стоять НАД своими цифрами. Правило
   выше выравнивает вправо и ячейку, и заголовок, но содержимое
   заголовка — ссылка сортировки с `display: flex`, а flex-контейнер
   `text-align` не слушает вовсе: подпись оставалась у левого края
   колонки, число — у правого, и на широком заголовке («Расчётный
   остаток», 166px) они расходились на всю его ширину. Замечание
   владельца 30.08.26: «названия столбцов не соответствуют значениям,
   цифры съезжают». */
table.tasks th.num a {
  justify-content: flex-end;
}
/* Стрелка сортировки уходит ВЛЕВО от подписи: стоя справа, она занимала
   20px правого края, и подпись всё равно не вставала над цифрами —
   ровно на её ширину. */
table.tasks th.num a .ico-sort {
  order: -1;
}
/* Заголовки салонов — не ссылки, а `span`, и отступа ячейки у них нет
   вовсе: их текст доходил до самого края и стоял на 12px правее чисел
   под собой. */
table.tasks thead th.num > span {
  display: block;
  padding: var(--sp-1) var(--sp-3);
}
/* Прочерк вместо пустоты, но приглушённый: «данных нет» не должно
   выглядеть так же весомо, как данные. */
.nil {
  color: var(--text-muted);
}

/* Артикул и имя склада — единственные колонки со свободным текстом.
   Ограничиваем ширину и обрезаем с многоточием, полное значение отдаём
   в title; nowrap на всей таблице иначе растянул бы её без предела. */
.text-cell {
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ============================================================== статусы */

/* Точка + текст, а не бейдж с заливкой: русские названия статусов WB
   длинные («Новое сборочное задание»), и заливка под ними превращает
   строку в цветную плашку. Точка ускоряет поиск глазами, текст несёт
   смысл — цвет здесь помощник, а не носитель информации. */
.status {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}
.status::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 0 2px color-mix(in oklab, currentColor 25%, transparent);
}
.status--ok {
  color: var(--ok);
}
.status--warn {
  color: var(--warn);
}
.status--err {
  color: var(--err);
}
.status--info {
  color: var(--info);
}
.status--idle {
  color: var(--text-muted);
}
/* Сам текст статуса остаётся основным цветом: цветной остаётся только
   точка, иначе десять цветных строк подряд снова превращаются в шум. */
.status span {
  color: var(--text);
}
.status--idle span {
  color: var(--text-muted);
}

/* ======================================================= пустое и ошибки */

/* Та же ловушка специфичности, что и у .num: без `table.tasks tbody`
   базовое правило ячейки перебивало и центрирование, и перенос строк —
   сообщение прижималось влево и не переносилось. */
table.tasks tbody tr.empty td {
  padding: var(--sp-8) var(--sp-3);
  text-align: center;
  white-space: normal;
}
.empty-box {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  color: var(--text-muted);
}
.empty-box .ico {
  color: var(--border-strong);
}
.empty-box strong {
  color: var(--text);
  font-weight: 600;
}
.empty-box .hint {
  font-size: 13px;
  max-width: 46ch;
}
/* Единственная «текстовая» ссылка в интерфейсе. Без своего правила она
   остаётся браузерно-синей — на тёмном фоне это и чужеродно, и плохо
   читается. */
.empty-box a {
  color: var(--accent);
  text-decoration-color: color-mix(in oklab, var(--accent) 45%, transparent);
  text-underline-offset: 2px;
}
.empty-box a:hover {
  text-decoration-color: currentColor;
}

/* =============================================================== пагинация */

.pager {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
  color: var(--text-muted);
  font-size: 13px;
}
.pager .range {
  font-variant-numeric: tabular-nums;
}
.pager a {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  min-height: 28px;
  padding: 0 var(--sp-3);
  color: var(--text);
  text-decoration: none;
  background: var(--bg-surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  transition:
    background var(--transition),
    border-color var(--transition);
}
.pager a:hover {
  background: var(--bg-hover);
  border-color: var(--text-muted);
}
.pager .spacer {
  flex: 1;
}

/* ========================================================== загрузка HTMX */

/* Ответ на действие обязан быть виден раньше, чем приедут данные. Полоса
   сверху блока результата + приглушение старой таблицы: человек видит,
   что цифры под курсором уже неактуальны и ответ в пути. */
#results {
  position: relative;
  /* Звено той же цепочки: блок результата тянется на всю оставшуюся
     высоту и отдаёт её .table-wrap, а пагинатор остаётся под ним. */
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
#results::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -2px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  opacity: 0;
  transition: opacity var(--transition);
}
#results.htmx-request::before {
  opacity: 1;
  animation: load-bar 900ms ease-in-out infinite;
}
#results.htmx-request .table-wrap {
  opacity: 0.45;
  transition: opacity var(--transition);
}
@keyframes load-bar {
  0% {
    transform: scaleX(0);
    transform-origin: left;
  }
  50% {
    transform: scaleX(1);
    transform-origin: left;
  }
  51% {
    transform: scaleX(1);
    transform-origin: right;
  }
  100% {
    transform: scaleX(0);
    transform-origin: right;
  }
}

/* ================================================================== иконки */

.ico {
  flex: none;
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ico--lg {
  width: 28px;
  height: 28px;
  stroke-width: 1.5;
}

/* ================================================================== вход */

.login {
  max-width: 340px;
  margin: 14vh auto 0;
  padding: var(--sp-6);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.login h1 {
  margin: 0 0 var(--sp-1);
  font-size: 20px;
  font-weight: 650;
}
.login .hint {
  margin: 0 0 var(--sp-4);
  color: var(--text-muted);
  font-size: 13px;
}
.login label {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  color: var(--text-muted);
  font-size: 12px;
}
.login input {
  min-height: 36px;
  font-size: 14px;
}
.login button {
  width: 100%;
  min-height: 36px;
  margin-top: var(--sp-4);
}
/* Ошибка входа: цвет плюс иконка плюс текст. Красным одним здесь
   обойтись нельзя — это ровно тот случай, ради которого правило
   «не только цветом» и написано. */
.login .error {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  color: var(--err);
  font-size: 13px;
  background: color-mix(in oklab, var(--err) 12%, transparent);
  border: 1px solid var(--err);
  border-radius: var(--radius);
}

/* ============================================================ узкий экран */

/* Экран сделан под ПК, но открыть его с планшета в цеху могут. Задача
   этих правил — не «сделать мобильную версию», а не рассыпаться:
   фильтры в колонку, шапка переносится, таблица уезжает в свою
   прокрутку (она уже в .table-wrap). */
@media (max-width: 900px) {
  /* Узкий экран возвращается к обычной прокрутке страницы: запирать
     таблицу в область высотой в пол-экрана там, где и так мало места, —
     это оставить видимыми три строки. */
  body {
    display: block;
    height: auto;
    overflow: visible;
  }
  main {
    display: block;
    overflow: visible;
  }
  #results {
    display: block;
  }
  .table-wrap {
    max-height: none;
  }
  .top {
    height: auto;
    min-height: var(--top-h);
    flex-wrap: wrap;
    padding: var(--sp-2) var(--sp-3);
    gap: var(--sp-2);
  }
  .filters .actions {
    margin-left: 0;
  }
  .filters .field-search {
    max-width: none;
  }
  /* 16px на полях ввода — иначе iOS/Safari при фокусе сам увеличивает
     страницу, и вёрстка «прыгает» под руками. */
  input,
  select {
    font-size: 16px;
  }
}

/* Раскрытие грузомест: строка-вставка ПОД строкой поставки. Владелец просил
   видеть короба «сразу», поэтому они приезжают сюда, а не на отдельную
   страницу. */
.korob-row > td {
  padding: 0;
  background: var(--bg);
}
.korob-box {
  padding: var(--sp-3) var(--sp-4);
  border-left: 2px solid var(--border-strong);
}
.korob-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-bottom: var(--sp-3);
}
.korob-all {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.korob-table {
  width: auto;
  min-width: 40rem;
  border-collapse: collapse;
  font-size: 12px;
}
.korob-table th,
.korob-table td {
  padding: 4px var(--sp-3);
  text-align: left;
  border-bottom: 1px solid var(--border);
}
.korob-table th.num,
.korob-table td.num {
  text-align: right;
}
.korob-box .hint {
  margin: var(--sp-3) 0 0;
  color: var(--text-muted);
  font-size: 12px;
}

/* Ячейка товара: миниатюра слева, название и идентификаторы справа.
   Заменила колонки nmID и Баркод — баркод переехал внутрь. */
.goods {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  min-width: 22rem;
}
.goods-photo {
  flex: none;
  display: block;
  width: 36px;
  height: 48px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
}
.goods-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Заглушка держит ту же ширину, что и фотография: иначе строки без карточки
   съезжали бы относительно соседних. */
.goods-photo--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  border: 1px dashed var(--border);
}
.goods-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.goods-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 24ch;
}
.goods-ids {
  display: flex;
  gap: var(--sp-3);
  flex-wrap: wrap;
  font-size: 11px;
  color: var(--text-muted);
}
.goods-nm {
  font-size: 11px;
  color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   Разделы «Сток» и «Синхронизация».

   Классы ниже добавлены после того, как таблица «Стока» разъехалась на
   первом же показе: разметка ссылалась на имена, которых в этом файле не
   было вовсе, и браузер рисовал голый <table>. Урок общий — новый раздел
   либо берёт существующие классы (`.goods`, `.num`, `.tasks`), либо
   заводит свои ЗДЕСЬ, а не только в шаблоне.
   -------------------------------------------------------------------------- */

/* Колонка с чекбоксом. Узкая и не растягивается: содержимое у неё
   фиксированное, а место нужно данным. */
table.tasks th.pick,
table.tasks td.pick {
  width: 1%;
  padding-right: 0;
  white-space: nowrap;
}

/* Расчётный остаток — то, ради чего таблицу открывают. Не цветом, а весом:
   цвет здесь означал бы статус, которого у числа нет. */
table.tasks td.num--strong {
  font-weight: 600;
}

/* Колонки салонов: детализация под итогом «в коробах». Приглушены, чтобы
   глаз шёл по итогам, а разбивку находил, когда она нужна. */
table.tasks td.num--muted {
  color: var(--text-muted);
}

/* Панель массового действия над таблицей. Липнет к верху вместе с
   заголовком: выделив строки в конце длинной страницы, человек не должен
   искать кнопку прокруткой вверх. */
.bulk-actions {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  margin: var(--sp-3) 0 var(--sp-2);
}

/* Статус прогона синхронизации. Цвет НИКОГДА не единственный носитель
   смысла (WCAG 1.4.1): внутри тега всегда текст — «идёт», «оборван»,
   число строк. */
.tag {
  display: inline-block;
  padding: 2px var(--sp-2);
  border-radius: var(--radius-sm);
  font-size: 11px;
  white-space: nowrap;
  border: 1px solid var(--border);
  color: var(--text-muted);
}
.tag--ok {
  color: var(--ok);
  border-color: color-mix(in oklch, var(--ok) 40%, transparent);
}
.tag--warn {
  color: var(--warn);
  border-color: color-mix(in oklch, var(--warn) 40%, transparent);
}
.tag--error {
  color: var(--err);
  border-color: color-mix(in oklch, var(--err) 40%, transparent);
  /* Текст отказа WB бывает длинным — рвём его, а не растягиваем колонку. */
  white-space: normal;
  max-width: 28ch;
}

/* Список методов WB в каталоге синхронизаций. Моноширинный и мелкий: это
   пути API, их читают глазами по буквам, а не сканируют. */
td.methods {
  white-space: normal;
  max-width: 30ch;
}
td.methods code {
  display: block;
  font-size: 11px;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.muted {
  color: var(--text-muted);
}

/* ------------------------------------------------------------------
   Карточка одного задания: /tasks/{order_id}. Страница про ОДИН объект,
   поэтому раскладка не табличная: пары «поле — значение» списком, и
   секции с заголовками вместо колонок.
   ------------------------------------------------------------------ */
.card-page {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}
.card-sec {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-4);
}
.card-sec h2 {
  margin: 0 0 var(--sp-2);
  font-size: 14px;
  font-weight: 600;
}
.card-sec > .hint {
  margin: 0 0 var(--sp-3);
  color: var(--text-muted);
  font-size: 12px;
}
/* Полоса действий над карточкой поставки. Отдельный вид от .card-sec
   намеренно: это не факты о предмете, а то, что с ним можно сделать, и одно
   из действий НЕОБРАТИМО. Читаться как ещё одна секция со справкой она не
   должна. */
.card-actions {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4);
}
.actions-row {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
/* Недоступная кнопка не прячется, а объясняет словами — и объяснение не
   должно спорить с живой кнопкой за внимание. */
.actions-row .nil {
  color: var(--text-muted);
  font-size: 12px;
  max-width: 42ch;
}
.card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
/* Две колонки: подпись поля узкая и приглушённая, значение занимает
   остаток. `grid`, а не таблица: строк немного, а вёрстка обязана
   складываться в одну колонку на узком экране (правило adaptive-layout). */
.card-facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-4);
  margin: 0;
  min-width: min(100%, 22rem);
  flex: 1;
}
.card-facts dt {
  color: var(--text-muted);
  font-size: 12px;
  align-self: center;
}
.card-facts dd {
  margin: 0;
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  overflow-wrap: anywhere;
}
@media (max-width: 40rem) {
  .card-facts {
    grid-template-columns: minmax(0, 1fr);
    gap: 0 0;
  }
  .card-facts dd {
    margin-bottom: var(--sp-2);
  }
}

/* Место CRM в карточке задания: группа, название, номер (см.
   app/ui/templates/_place.html). Группа приглушена, название несёт вес —
   человек ищет глазами «К-12», а группа отвечает на вопрос «что это за
   место», когда название ни о чём не говорит. */
.place {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
}
.place-group {
  color: var(--text-muted);
  font-size: 12px;
}
.place-name {
  font-weight: 500;
}
.place-id {
  color: var(--text-muted);
  font-size: 12px;
}
