
    /* Тёмная тема Telegram-палитры. Без сборки, без внешних CDN (CSP витрины строгий). */
    :root {
      --bg: #17212b;
      --card: #232e3c;
      --card2: #1c2733;
      --text: #f5f5f5;
      --hint: #7d8e9e;
      --accent: #64b5ef;
      --sep: #303c49;
      --ok: #4dcb5d;
      --discuss: #e8b04b;
      --escalated: #ec6a5e;
      /* WP130: состояние клавиатуры и безопасной зоны живёт в переменных корня.
         Их правит ОДИН обработчик в конце скрипта, вёрстка только ссылается —
         поэтому подъём переживает перерисовку карточки (render пересоздаёт
         .approvebar, и пер-элементный style на нём протух бы на первом же
         пересчёте). --kb = высота клавиатуры, --kbbar = высота полосы «Готово»,
         --sab = нижний инсет (при открытой клавиатуре домашнего индикатора нет,
         обработчик обнуляет — иначе лишний отступ, грабля §4б исследования). */
      --kb: 0px;
      --kbbar: 0px;
      --sab: env(safe-area-inset-bottom, 0px);
      /* WP139: полоса, занятая нижней панелью и плавающей 💬. Значение здесь —
         стартовое (до первого замера syncBarClearance): панель 136px + 💬 над
         ней. Дальше его правит ЗАМЕР — у лобби-токена панель выше на кнопку
         «‹ Все заказы», и константа на всех случаях не бывает верной. */
      --barh: 224px;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      background: var(--bg);
      color: var(--text);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      -webkit-font-smoothing: antialiased;
      font-size: 15px;
      line-height: 1.4;
    }
    /* fab-clearance: под липкой панелью и плавающей 💬 не должно оказываться
       интерактивных элементов — иначе человек, метящий в «0» ряда количества,
       попадает в «Все заказы» и уходит с экрана (дефект UI-прогона 21.07: FAB
       перекрывал кнопку убирания на 375px; замер 26.07: панель проезжает по ряду
       количества до 44pt). Прежние 156px были константой, посчитанной под панель
       из одной кнопки; панель с тех пор выросла до 136px (WP85 «＋ Добавить») и
       до 188px у лобби-токена («‹ Все заказы»). Теперь высоту меряет
       syncBarClearance и кладёт в --barh, здесь остаётся только зазор. */
    .wrap { max-width: 640px; margin: 0 auto; padding: 16px 14px calc(var(--barh) + 16px); }
    header { padding: 6px 2px 14px; }
    header h1 { font-size: 18px; margin: 0 0 4px; font-weight: 600; }
    header .sub { color: var(--hint); font-size: 13px; }
    .statusline {
      margin: 8px 0 2px; font-size: 13px; color: var(--accent);
      font-variant-numeric: tabular-nums;
    }
    /* WP1112: состав корзины — «под потребность N ₽ · добивка M ₽». Слова
       считает ядро (cart_core.basket_composition) и приезжают ГОТОВЫМИ в
       пейлоаде (cart.composition.words); разметка только печатает (ADR-030).
       Тихая строка под шапкой: это число, из которого решают «пропустить или
       заказать», а не герой экрана. Ключа нет (движок ни одной строки не
       считал) — строка молчит: молчание отличимо от нуля. */
    .composition {
      margin: 2px 0 0; font-size: 13px; color: var(--hint);
      font-variant-numeric: tabular-nums;
    }
    .cutoff {
      margin: 10px 0 16px; padding: 9px 12px; border-radius: 10px;
      background: var(--card2); color: var(--accent); font-size: 13px;
    }
    /* WP167: та же полоса про УЖЕ УЕХАВШИЙ заказ. Цвет «сделано», а не «внимание»:
       отправка — не авария, но это факт, а не обещание, и читаться он обязан иначе,
       чем строка про отсечку. */
    .cutoff.cutoff-sent {
      background: rgba(76,201,164,.13); color: var(--ok); font-weight: 600;
    }
    /* WP167 (B4): застейдженное и не отправленное. Жёлтое, не красное: это не
       поломка, это ждущий человек — но молчать ему нельзя. */
    .pending-send, .lobby-pending {
      margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
      background: rgba(240,180,80,.13); border: 1px solid rgba(240,180,80,.4);
      color: #f0b450; font-size: 13px; line-height: 1.45;
    }
    /* WP167: блок отправки на листе итога. Кнопка ОДНА и названа своим делом. */
    .sendbox { margin: 14px 0 10px; }
    .sendbox .sendbtn { width: 100%; margin-top: 10px; font-size: 16px; padding: 14px; }
    .sendbox .sendnote {
      margin-top: 8px; font-size: 13px; color: var(--accent); line-height: 1.4;
    }
    /* WP283: дозаказ — отдельная работа, не вторая отправка того же заказа.
       Своя рамка, чтобы блок не читался как «отправить ещё раз». */
    .addonbox {
      margin-top: 10px; padding: 10px 12px; border-radius: 10px;
      border: 1px dashed rgba(120,190,140,.45);
    }
    /* WP288: отзыв — соседняя развилка дозаказа и куда более тяжёлая (поставщику
       уйдёт «предыдущий не исполнять»). Рамка тревожного цвета: это не вторая
       кнопка отправки и не второй дозаказ. */
    .recallbox {
      margin-top: 10px; padding: 10px 12px; border-radius: 10px;
      border: 1px dashed rgba(232,120,100,.5);
    }
    .recallbox .mbtn.recallbtn {
      width: 100%; margin-top: 8px; font-size: 15px; padding: 12px;
      background: transparent; color: var(--escalated);
      border: 1px solid var(--escalated);
    }
    /* Второе нажатие — та же кнопка, но уже с составом на экране: заливка
       говорит, что следующее нажатие исполняет. */
    .recallbox .mbtn.recallbtn.armed { background: rgba(232,120,100,.15); }
    /* WP288: ЛЕНТА заказа. Читается как история, а не как тревога — серым, с
       временем в моноширинной колонке. Свидетель каждой строки — рядом с ней:
       строка без свидетеля была бы выдуманным этапом. */
    .timeline {
      margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
      background: var(--card); font-size: 13px; line-height: 1.45;
    }
    .timeline h3 {
      margin: 0 0 8px; font-size: 13px; color: var(--hint); font-weight: 600;
    }
    .tl-row { display: flex; gap: 8px; padding: 5px 0; border-bottom: 1px solid var(--sep); }
    .tl-row:last-of-type { border-bottom: none; }
    .tl-at {
      min-width: 46px; color: var(--accent); font-variant-numeric: tabular-nums;
      font-weight: 600;
    }
    .tl-body { flex: 1; }
    .tl-src { display: block; color: var(--hint); font-size: 12px; margin-top: 2px; }
    .tl-letter { margin-top: 6px; }
    .tl-letter summary { color: var(--accent); cursor: pointer; font-size: 12px; }
    .tl-letter pre {
      white-space: pre-wrap; word-break: break-word; margin: 6px 0 0;
      font-size: 12px; line-height: 1.4; color: var(--text);
      background: rgba(255,255,255,.04); padding: 8px; border-radius: 8px;
    }
    .tl-horizon { color: var(--hint); font-size: 12px; margin-top: 8px; line-height: 1.4; }
    .pos {
      position: relative; background: var(--card); border-radius: 12px;
      padding: 12px 14px 12px 18px; margin-bottom: 9px; overflow: hidden;
    }
    .pos::before {
      content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
    }
    .pos.ok::before { background: var(--ok); }
    .pos.discuss::before { background: var(--discuss); }
    .pos.escalated::before { background: var(--escalated); }
    .pos .row1 { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
    .pos .name { font-weight: 600; font-size: 15px; }
    /* ── WP165: лучшее место — тому, что человек ищет ────────────────────────
       Счёт произнесённого вслух за 28 минут встречи 27.07: остаток 33 · тара 29 ·
       количество 26 · расход 25 · страховой 15 · прогноз 11 · «на сколько
       хватит» 9 · ЦЕНА И СУММА 2 · книга iiko 2 · срок годности 0.
       До этой правки справа от названия стояла ЖИРНАЯ сумма в рублях, а первой
       строкой под ним — цена за килограмм: два самых редких понятия занимали
       лучшее место, два самых частых — мелкий серый блок.
       Теперь справа от названия — СКОЛЬКО БЕРЁМ, крупно и в таре; деньги ушли
       вниз одной тихой строкой (совсем убирать нельзя: по цене изредка ловят
       ошибку прайса, решение основателя). */
    .pos .lead { text-align: right; line-height: 1.15; white-space: nowrap; }
    .pos .lead-big {
      display: block; font-size: 23px; font-weight: 700; letter-spacing: -.02em;
      font-variant-numeric: tabular-nums;
    }
    .pos .lead-sml {
      display: block; font-size: 12px; color: var(--hint); margin-top: 2px;
      font-variant-numeric: tabular-nums;
    }
    /* WP441 (TD-309): состояние ЧИСЛА — рядом с числом, а не только в сноске.
       Дробное число без маркера прочтётся как квантованное, а оно квантованным
       не является: фасовка не закреплена, округлять нечем. Слова — из пейлоада
       (словарь cart_core.ENTRY_STATE_LEGEND), поверхность только печатает
       (ADR-030). Пара с пином маркера не несёт — шапка прежняя. */
    .pos .lead-state {
      display: block; font-size: 11px; font-weight: 600; color: var(--discuss);
      margin-top: 2px;
    }
    /* WP498: невозможная фасовка — шапка НЕ печатает выведенных из неё чисел
       (ни «упаковок», ни пересчёта в базовые), а слова правила. Ярко: это не
       справка, а сигнал «чинить в supplier_pack.csv». Цвет — discuss (жёлтый):
       требует внимания, но не красный «не уедет». */
    .pos .lead-sml.pack-sanity {
      font-size: 13px; font-weight: 600; color: var(--discuss);
      white-space: normal;
    }
    .pos .money {
      display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
      margin-top: 9px; padding-top: 8px; border-top: 1px solid var(--sep);
      color: var(--hint); font-size: 12px; font-variant-numeric: tabular-nums;
    }
    .pos .sum { white-space: nowrap; }
    /* WP165: остаток — 33 упоминания, и почти все в форме СПОРА: система говорит
       одно, шеф видит другое. Это точка входа в разговор, а выглядела как справка
       в мелком сером блоке. Второй по величине после количества, и рядом —
       приглашение поправить (та же ссылка, что открывала сверку из фактов). */
    .pos-stock {
      display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 8px; margin-top: 9px;
    }
    .pos-stock .k {
      font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--hint);
    }
    .pos-stock .v { font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; }
    .pos-stock .src { font-size: 12px; color: var(--hint); }
    /* WP130: ссылка-действие в строке текста — паддинг растит мишень пальца,
       отрицательный отступ гасит рост строки (тот же приём, что .facts-recheck).
       Разбор 14.09: пунктир — подчёркиванием ТЕКСТА, а не нижней рамкой. Рамка
       рисуется по краю выросшей мишени, на 11px ниже слов, и на карточке креветок
       легла зачёркиванием через первую строку слагаемых под ссылкой. */
    .stock-fix {
      color: var(--accent); font-size: 12px; cursor: pointer;
      text-decoration: underline dotted currentColor; text-underline-offset: 3px;
      -webkit-tap-highlight-color: transparent;
      display: inline-block; padding: 11px 4px; margin: -11px -4px;
      touch-action: manipulation;
    }
    .stock-fix:active { color: var(--text); }
    /* WP165: «хватит до четверга, приедет во вторник» — девять раз за встречу это
       считали в уме из остатка и расхода. Вместе две даты отвечают на
       единственный вопрос человека: дотянем или нет. */
    .pos-life { font-size: 13px; margin-top: 5px; line-height: 1.45; }
    .pos-life b { font-weight: 600; }
    /* WP195: полка и поставка — РАЗНЫЕ мысли, и на экране между ними пауза.
       Через запятую («Хватит до понедельника, приедет в четверг») они читались
       как одно предложение, и человек спрашивал вслух, к какой дате оно
       относится. Отступ маленький: это по-прежнему один ответ на один вопрос
       «дотянем или нет», просто в две строки. */
    .pos-life .life-arrival { margin-top: 2px; }
    /* WP10033: вторая вилка — С едущим. Строка ПОД «Хватит до …» (полка без
       едущего) и ДО «Приедет …»: «если приедут 5 кг — до вторника». Это
       УСЛОВНАЯ вилка (едущее может не доехать), поэтому тон тихой справки —
       не герой карточки, а ответ на «а если оно приедет?». Числа и слова
       приезжают готовыми из ядра (f.life_on_way). */
    .pos-life .life-onway {
      color: var(--hint); font-size: 12px; margin-top: 2px;
    }
    .pos-life .life-onway b { font-weight: 600; color: var(--text); }
    /* WP1287: ВЫВОД о заказе — своя строка сразу под «хватит до …/приедет …».
       Две строки выше отвечают «дотянем или нет», эта — «почему заказываем»:
       конец окна против нормы. Тон обычного текста, не тихой справки: это
       ответ на вопрос, ради которого карточку и открыли, — но и не герой
       карточки, поэтому без выделения. Числа приезжают готовыми из ядра. */
    .pos-verdict {
      font-size: 13px; margin-top: 4px; line-height: 1.45;
      font-variant-numeric: tabular-nums;
    }
    /* WP165: расход и страховой называют часто (25 и 15), но всегда как
       ОБОСНОВАНИЕ уже принятого решения, а не как то, ради чего открыли
       карточку. Одна тихая строка. */
    .pos-quiet {
      color: var(--hint); font-size: 12px; margin-top: 3px;
      font-variant-numeric: tabular-nums; line-height: 1.5;
    }
    /* WP9710: разложение крупного числа «на утро 40 = 25 + 15» — строка между
       ведущим числом и подчинёнными строками: человек не складывает глазами,
       сумма напечатана, а слагаемые — строкой ниже. Тон обычного текста: это
       ответ, а не тихая справка, но и не герой карточки. */
    .pos-breakdown {
      font-size: 12px; margin-top: 2px; line-height: 1.4;
      font-variant-numeric: tabular-nums;
    }
    /* WP9706: подчинённые строки блока остатка (№5 «доступно», №1 «в книге»,
       №2 висяк) — одна тихая строка под ведущим числом: они не героя, а
       разбивка, по которой человек проверяет ведущее число. */
    .pos-parts {
      color: var(--hint); font-size: 12px; margin-top: 2px;
      font-variant-numeric: tabular-nums; line-height: 1.5;
    }
    /* Разбор 14.09 (вариант 1 владельца): слагаемые остатка на утро — столбиком,
       числа друг под другом, знак «+» у следующего; каждое называет своё
       происхождение. Прежняя строка «40 = 25 — … + 15 — …» читалась обрывками. */
    .pos-terms {
      display: grid; grid-template-columns: 12px auto 1fr; gap: 0 6px; align-items: baseline;
      color: var(--hint); font-size: 12px; margin-top: 2px; line-height: 1.5;
      font-variant-numeric: tabular-nums;
    }
    .pos-terms .op { text-align: right; }
    .pos-terms .v { color: var(--text); font-weight: 600; white-space: nowrap; }
    /* WP10120: блюда под «в работу» (элемент с sub: true, ядро
       cart_core.demand_split_words) — СВОЯ разметка: отступ слева, обычный
       вес числа (не 600), число БЕЗ единицы. Остальные строки столбика —
       как сейчас (.v/.lb без .sub). */
    .pos-terms .v.sub { font-weight: 400; padding-left: 10px; }
    .facts-pending {
      color: var(--discuss); font-size: 12px; margin-top: 2px;
      font-weight: 600;
    }
    /* WP568: разлёт книги с последним физпересчётом — слова готовы из ядра
       (cart_core.stock_drift_words), строка печатает их рядом с уровнем №3:
       человек решает по позиции ТАМ, где видит остаток. Тон — предупреждение
       (тот же --discuss, что у «приехало, не проведено»), не ошибка: заказ
       при этом НЕ останавливается, карточка лишь не даёт пройти молча. */
    .pos-drift {
      color: var(--discuss); font-size: 12px; margin-top: 3px;
      font-variant-numeric: tabular-nums; line-height: 1.5;
    }
    /* WP10009: разбивка висяка по документам — по строке на документ (поставщик,
       номер, Д1), до трёх, дальше «и ещё N». Строки готовые из ядра
       (cart_core.stock_block, `pending_docs`); тон тот же, что у «приехало,
       не проведено» (--discuss): строка №2 называет, ЧТО приехало и ОТКУДА. */
    .pos-pending-docs {
      color: var(--discuss); font-size: 12px; margin-top: 2px;
      font-variant-numeric: tabular-nums; line-height: 1.5;
    }
    .pos-pending-doc { padding-left: 12px; text-indent: -12px; }
    /* WP9711 + разбор 13.09: «подробности заказа» по мокапу «Заказ на таймлайне»
       (11.09, одобрен владельцем). Раскрывашка тихая — под блоком остатка, акцентным
       цветом, со своим треугольником; рисунок на всю ширину карточки, легенда и
       прогноз расхода — под рисунком, текст вывода — подписью. Цвета линий заданы в
       разметке графика (палитра мокапа, проверена на тёмной карточке); здесь — текст
       рисунка, раскрывашка и появление линий при раскрытии. */
    .order-details { margin-top: 9px; border-top: 1px solid var(--sep); padding-top: 9px; }
    .order-details > summary {
      list-style: none; cursor: pointer; font-size: 12.5px; color: var(--accent);
      display: flex; align-items: center; gap: 7px; user-select: none;
      -webkit-tap-highlight-color: transparent;
    }
    .order-details > summary::-webkit-details-marker { display: none; }
    .order-details > summary::before {
      content: '▸'; display: inline-block; transition: transform .18s ease;
    }
    .order-details[open] > summary::before { transform: rotate(90deg); }
    .order-details > summary:focus-visible {
      outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px;
    }
    .order-chart-box { padding-top: 12px; }
    .order-chart-box svg { display: block; width: 100%; height: auto; overflow: visible; }
    .oc-ax, .oc-ev {
      font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 10.5px;
    }
    .oc-ax { fill: #8c9aad; }
    .oc-ev { fill: #e0a458; }
    .oc-lbl { font-size: 11.5px; fill: #f0f4fa; font-variant-numeric: tabular-nums; }
    .oc-lbl.b { font-weight: 700; }
    .oc-legend {
      display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center;
      margin-top: 10px; font-size: 12px; color: var(--hint);
    }
    .oc-legend i {
      display: inline-block; width: 16px; height: 3px; border-radius: 2px;
      vertical-align: middle; margin-right: 6px;
    }
    .oc-legend .oc-rate {
      font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
      color: var(--text); font-weight: 600;
    }
    /* WP10342: подпись «до пересчёта» над рисунком — график на записанном
       остатке, серверный пересчёт ещё не был (слова несёт правило
       window.__rung.recordedWords, разметка печатает). */
    .oc-recorded {
      margin-bottom: 6px; font-size: 12px; color: #e0a458;
    }
    .order-verdict-caption {
      font-size: 12.5px; color: #c3ccd7; margin-top: 10px; line-height: 1.5;
    }
    /* Появление — ОТ видимого состояния покоя: ключевые кадры задают только начало.
       Не запустится анимация (свёрнутая вкладка, веб-вью мессенджера) — линии и
       подписи всё равно на месте, а не остаются прозрачными. Штрих 1400 длиннее
       любой ломаной рисунка 620 × 262: разрыва в линии не бывает. */
    @media (prefers-reduced-motion: no-preference) {
      .order-details[open] .oc-draw {
        stroke-dasharray: 1400; animation: oc-draw 1s .05s ease-out backwards;
      }
      .order-details[open] .oc-fade { animation: oc-fade .45s .75s ease-out backwards; }
    }
    @keyframes oc-draw { from { stroke-dashoffset: 1400; } }
    @keyframes oc-fade { from { opacity: 0; } }
    /* WP365 (21.08): строка ИТОГА ушла с карточки (WP9710): сумму везёт ядро
       cart_core.stock_block — ведущее число №3 «на утро» и напечатанные
       слагаемые terms. Отдельная строка «итог» была второй копией сложения
       (класс WP198/WP205/WP225), и с ней ушли её стили .facts-total. */
    /* WP1290: подтверждённый заказ, который ещё едет. Стоит ПОД итогом полки и
       тише его: на полке этого товара нет, и спутать одно с другим нельзя.
       Просрочка («ждали сегодня или раньше, прихода нет») — своя строка тоном
       предупреждения (тот же --discuss, что у «приехало, не проведено»): это
       уже не «едет», а повод разобраться, но заказ она не останавливает. */
    .facts-onway {
      color: var(--text); font-size: 12px; margin-top: 4px;
      font-variant-numeric: tabular-nums; line-height: 1.45;
    }
    .facts-onway-hint { color: var(--hint); font-size: 12px; }
    .facts-onway-late { color: var(--discuss); font-size: 12px; margin-top: 2px; }
    /* ── Указатель по семейству .whatif-* (рычаги предпросчёта, WP121) ────────
       Четыре рычага: остаток по факту, прогноз, страховой запас, квант. Первые три
       живут в зоне ступени (.zone-rung) и раскрываются ПО ОДНОМУ — лестницей
       WP126; квант вне лестницы, он в справочной зоне (.zone-ref) внизу карточки.
       WP588: справочная зона несёт органы редактора (select [data-wh-pk-select],
       поле [data-wh-pk-num], чипсы [data-pk-cand]) и на строке БЕЗ лестницы —
       цикл .whatif их там не вяжет, их вяжет отдельный проход по .zone-ref.
       Строка под рычагом — последствие: цифра ПРЕДВАРИТЕЛЬНАЯ, итог даёт пересчёт
       корзины движком. Отдельной пометкой идёт добивка до минимального заказа.
       Кнопка «сброс» возвращает рычаги к расчёту движка. Всё это рисуется, только
       когда у позиции есть входы формулы (need_qty_raw): без них считать нечего.
       WP1015: это ПОДСКАЗКА, не поле (div, фокуса нет) — но гейт check_wp127
       меряет ей шрифт: его regex видит в этом правиле слово «select» из
       комментария выше и требует ≥16px. Шрифт поднят до 16px ЯВНО (не снять
       фокусируемость — её и так нет, а гейт смотрит на шрифт, не на tabindex). */
    .whatif-anchor-hint {
      color: var(--hint); font-size: 16px; margin: 2px 0 4px;
    }
    /* WP127: селектор шёл по [type=number], а поле стало текстовым (айфон съедал
       запятую) — правило чинится на data-атрибут, иначе тёмная тема с него
       слетает. Шрифт 16px: меньше — айфон зумит страницу на фокусе, и человек
       теряет место в списке позиций. */
    /* WP130: min-height 44px — поле не только читают, в него ЦЕЛЯТСЯ пальцем. */
    /* WP133: прогноз стал таким же дробным ПОЛЕМ, как остаток (был ползунок со
       шкалой множителя 0,25…3, молча зажимавший 5,3 кг/д в «3»). Правило одно на
       оба поля: одинаковые поля обязаны выглядеть одинаково, а порознь они
       разъезжаются — 16px (автозум айфона) и 44px (WP130) пришлось бы держать
       дважды, и вторая копия однажды отстанет. */
    /* WP196: величина страхового — такое же ПОЛЕ (коридор 0…100,8 л ползунком не
       набирается), и выглядеть оно обязано как соседние поля, а не как третий вид
       ввода на одной карточке. */
    .whatif-row input[data-wh="an"],
    .whatif-row input[data-wh="an-use"],
    .whatif-row input[data-wh="fc"],
    .whatif-row input[data-ss-mode="units"] {
      width: 100%; padding: 6px 8px; border-radius: 8px; border: 1px solid var(--line, #2b3a4a);
      background: var(--card2); color: inherit; font-size: 16px;
      min-height: 44px;
    }
    /* WP366: второе поле «в работе» — компактнее главного (оно в расчёт не идёт),
       но той же формы, чтобы не был «третий вид ввода на карточке». */
    .whatif-anuse { margin: 8px 0 0 0; }
    /* WP160: «И тут вот бегунок, который почти не видно» — дословно, 27.07. Был
       голый браузерный range: кружок без дорожки, без подписи значения и без
       границ шкалы. Дорожку, заполнение и мишень пальца (WP130: 44px) рисуем сами;
       значение и границы стоят СНАРУЖИ ползунка (.ss-val / .ss-ticks). */
    .whatif-row input[type="range"][data-wh="ss"] {
      -webkit-appearance: none; appearance: none;
      width: 100%; height: 44px; margin: 0; background: transparent;
      touch-action: manipulation; cursor: pointer;
    }
    .whatif-row input[type="range"][data-wh="ss"]::-webkit-slider-runnable-track {
      height: 6px; border-radius: 3px;
      background: linear-gradient(to right, var(--accent) 0 var(--fill, 0%),
                                  var(--sep) var(--fill, 0%) 100%);
    }
    .whatif-row input[type="range"][data-wh="ss"]::-moz-range-track {
      height: 6px; border-radius: 3px; background: var(--sep);
    }
    .whatif-row input[type="range"][data-wh="ss"]::-moz-range-progress {
      height: 6px; border-radius: 3px; background: var(--accent);
    }
    .whatif-row input[type="range"][data-wh="ss"]::-webkit-slider-thumb {
      -webkit-appearance: none; appearance: none;
      width: 22px; height: 22px; border-radius: 50%; margin-top: -8px;
      background: var(--accent); border: 3px solid var(--card);
      box-shadow: 0 0 0 1px var(--accent);
    }
    .whatif-row input[type="range"][data-wh="ss"]::-moz-range-thumb {
      width: 22px; height: 22px; border-radius: 50%; border: 3px solid var(--card);
      background: var(--accent); box-shadow: 0 0 0 1px var(--accent);
    }
    /* Значение — КРУПНО и в двух мерах сразу: люди думают в килограммах. */
    .ss-val { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
    .ss-val em { font-style: normal; color: var(--hint); font-weight: 400; font-size: 13px; }
    .ss-ticks {
      display: flex; justify-content: space-between; color: var(--hint);
      font-size: 11px; margin-top: 2px; font-variant-numeric: tabular-nums;
    }
    /* Быстрая правка прогноза: −10% / +10% / ×2. Мишени пальца — 44px и
       touch-action по WP130. */
    .fc-quick {
      display: flex; gap: 6px; margin-top: 6px;
    }
    .fc-quick-btn {
      flex: 1; min-height: 44px; touch-action: manipulation;
      background: var(--card); color: var(--text);
      border: 1px solid var(--sep); border-radius: 6px;
      font-size: 15px; font-weight: 600; cursor: pointer;
    }
    .fc-quick-btn:active { background: var(--card2); }
    .applied-note {
      color: var(--ok, #6ab04c); font-size: 12px; margin-top: 6px; font-weight: 600;
    }
    .facts-warn {
      color: var(--escalated); font-size: 12px; margin-top: 2px;
    }
    /* WP128: «сверить остаток» в строке неучтённой накладной — единственное
       осмысленное действие по ней. Открывает ту же сверку, что ссылка WP126. */
    /* WP130: ссылка-действие внутри строки текста. Полноценные 44px ей не дать —
       она стоит в середине предложения. Приём: вертикальный паддинг растит зону
       касания, отрицательный внешний отступ на ту же величину гасит рост строки —
       высота строки не меняется, а промахнуться пальцем стало вдвое труднее. */
    .facts-recheck {
      cursor: pointer; -webkit-tap-highlight-color: transparent;
      border-bottom: 1px dotted currentColor;
      display: inline-block; padding: 11px 4px; margin: -11px -4px;
      touch-action: manipulation;
    }
    .facts-recheck:active { color: var(--text); }
    /* WP130: «подробности» — отдельная строка, поэтому честные 44px по высоте.
       :active добавлен не для красоты: у этой пары снят tap-highlight, и без
       него тап не даёт вообще никакого отклика (§4 исследования). */
    .facts-toggle {
      color: var(--hint); font-size: 12px; margin-top: 4px; cursor: pointer;
      -webkit-tap-highlight-color: transparent;
      display: flex; align-items: center; min-height: 44px;
      touch-action: manipulation;
    }
    .facts-toggle:active { color: var(--accent); }
    .facts-body {
      color: var(--hint); font-size: 12px; margin-top: 4px;
      font-variant-numeric: tabular-nums; line-height: 1.6;
      padding: 6px 8px; background: var(--card2); border-radius: 8px;
    }
    /* WP151: три справки блока — каждая своим абзацем. Слитые в один поток, они
       читаются как прежняя стена цифр: цель у каждой своя, и пауза между ними
       делает половину работы. */
    .facts-brief { margin: 0 0 8px 0; }
    .facts-brief:last-child { margin-bottom: 0; }
    /* WP192: разложение заказа — ОДНА строка чисел, не таблица: карточку читают
       с телефона. Итог отбит и выделен — он обязан совпасть с суммой, которую
       человек складывает глазами. flex-wrap: на 375 px части ПЕРЕНОСЯТСЯ, а не
       выдавливают карточку вбок. Разделителя между частями НЕТ намеренно: точка
       посреди flex-строки на переносе уезжает в начало следующей («· 3,224 на
       округление» — видно глазами на 375 px), а разделяют здесь и без неё
       промежуток и цвет — число белое, подпись серая. */
    .dcmp-row {
      display: flex; flex-wrap: wrap; align-items: baseline;
      column-gap: 12px; row-gap: 2px; margin-bottom: 4px;
    }
    .dcmp-row b, .dcmp-total { color: var(--text); font-weight: 600; }
    /* WP619: причина добивки — под строкой чисел, рядом со слагаемой добивки,
       приглушённым тоном: это объяснение, а не число. */
    .dcmp-reason { font-size: 12px; color: var(--hint); line-height: 1.35; margin-top: 2px; }
    .badge {
      display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 20px;
      margin-top: 7px; font-weight: 600;
    }
    .badge.ok { background: rgba(77,203,93,.16); color: var(--ok); }
    .badge.discuss { background: rgba(232,176,75,.16); color: var(--discuss); }
    .badge.escalated { background: rgba(236,106,94,.16); color: var(--escalated); }
    /* WP162: решение человека не прочитано — ярлык красный, как эскалация */
    .badge.unknown { background: rgba(236,106,94,.16); color: var(--escalated); }
    .badge.accepted { background: rgba(100,181,239,.16); color: var(--accent); }
    /* WP136: исход разговора. converged — спор закрыт (цифра человека достигнута
       или перекрыта); pending — параметр записан, движок ещё не считал. */
    .badge.converged { background: rgba(100,181,239,.16); color: var(--accent); }
    .badge.pending { background: rgba(232,176,75,.16); color: var(--discuss); }
    /* WP220: объяснение цвета — тихой строкой под ярлыком. Тапом ничего не
       является (новых целей не заводим — WP127 про размеры тапов), просто
       читается: «чё ж оно такое жёлтое» обязано закрываться взглядом. */
    .badge-why {
      color: var(--hint); font-size: 11px; line-height: 1.35; margin-top: 4px;
    }
    /* WP256: запрет — своё СОСТОЯНИЕ карточки, а не значок поверх чужого: полоса
       и ярлык красные, как у эскалации, потому что строка НЕ уедет. Слова под
       ярлыком печатает ядро (cart_core.barred_state). */
    .badge.barred { background: rgba(236,106,94,.16); color: var(--escalated); }
    .pos.barred::before { background: var(--escalated); }
    .barred-why {
      color: var(--escalated); font-size: 11px; line-height: 1.35; margin-top: 4px;
    }
    /* WP285: «эта строка НЕ уедет» — словами и там, где человек смотрит на
       количество. 14.08 он зачитал вслух «Масло фритюрное, восемь упаковок» по
       позиции, которую удерживал незакрытый спор: от согласованной она на экране
       ничем не отличалась. Красный — тот же язык, что у запрета (предмет один:
       строка не уедет), второго цвета на одно утверждение не заводим. */
    .ships-no {
      margin: 0 0 8px; padding: 6px 9px; border-radius: 8px;
      background: rgba(236,106,94,.12); border: 1px solid rgba(236,106,94,.35);
      color: var(--escalated); font-size: 12px; font-weight: 600; line-height: 1.4;
    }
    /* Тот же предмет на листе подтверждения и в блоке отправки — но ИМЕНАМИ:
       счётчик там уже есть (WP144), а вслух человек читает имя позиции. */
    .ships-not-list .snl-row { margin-top: 5px; font-weight: 400; }
    /* WP81: у принятой правки — расчётная цифра серым рядом с количеством */
    .calc { color: var(--hint); font-size: 12px; margin-left: 8px; font-weight: 400; }
    /* WP291: степпера количества в окне добавления больше НЕТ — число называет
       расчёт, а поднимают его лестницей в карточке. Правила `.stepper/.stepbtn/
       .qtybox` ушли вместе с разметкой: живой класс, которым никто не рисует,
       читается как «где-то это ещё есть» и переживает не одну волну правок. */
    .edit { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
    /* ── WP129: ряд разлёта количества вместо степпера ────────────────────────
       Человек называл цифру ДВАЖДЫ: щёлкал степпером, на втором клике его
       обрывали вопросами и просили дощёлкать заново. Теперь он называет её ОДИН
       раз кнопкой ряда, и эта цифра едет через все ступени. Цвет зоны виден ДО
       нажатия: зелёный — расчёт, жёлтый — ±1 упаковка (свободно), красный —
       отсюда начнётся разговор. НОЛЬ КРАСНЫЙ ВСЕГДА: «не нужно вообще» — не шаг,
       а отказ (решение основателя 26.07). Размеры ≥44×44 — Apple HIG. */
    .qtyrow { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
    /* WP132: подпись зоны переехала ВНУТРЬ кнопки (flex-column: число сверху,
       слово под ним). Отдельная строка легенды стояла на КАЖДОЙ карточке и гоняла
       глаз от цвета к легенде и обратно; теперь цвет и слово там же, где палец. */
    .qty-chip {
      min-width: 44px; min-height: 44px; padding: 0 10px;
      border-radius: 10px; border: 1px solid var(--sep);
      background: var(--card2); color: var(--text);
      font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums;
      cursor: pointer; touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
      display: inline-flex; flex-direction: column;
      align-items: center; justify-content: center; line-height: 1.15;
    }
    .qc-cap { font-size: 10px; font-weight: 400; opacity: .75; letter-spacing: .2px; }
    .qty-chip.z-calc { border-color: var(--ok); color: var(--ok); }
    .qty-chip.z-free { border-color: var(--discuss); color: var(--discuss); }
    .qty-chip.z-ask { border-color: var(--escalated); color: var(--escalated); }
    .qty-chip.sel { box-shadow: inset 0 0 0 2px currentColor; }
    .qty-chip.sel.z-calc { background: rgba(77,203,93,.16); }
    .qty-chip.sel.z-free { background: rgba(232,176,75,.16); }
    .qty-chip.sel.z-ask { background: rgba(236,106,94,.16); }
    /* WP132: строка легенды цветов удалена — подписи стоят внутри кнопок ряда
       (.qc-cap выше). Одна строка на каждой из 16 карточек — это 16 строк текста,
       которые ничего не решают. */
    .qty-own-wrap { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
    /* 16px — порог автозума айфона (WP127), ниже опускать нельзя */
    .qty-own-wrap input {
      flex: 1; min-height: 44px; padding: 6px 10px; border-radius: 10px;
      border: 1px solid var(--sep); background: var(--card2); color: var(--text);
      font-size: 16px;
    }
    .qty-own-wrap input.wh-bad {
      border-color: var(--escalated); background: rgba(236,106,94,.10);
    }
    .qty-own-wrap .qty-own-go {
      min-height: 44px; min-width: 56px; border-radius: 10px; border: none;
      background: var(--accent); color: #06263f; font-size: 15px; font-weight: 600;
      cursor: pointer; touch-action: manipulation;
    }
    .qty-own-msg { color: var(--escalated); font-size: 12px; margin-top: 4px; font-weight: 600; }
    /* WP297: подпись поля «своё» — в чём оно и сколько это упаковок. Тише
       сообщения об ошибке (оно красное и жирное): подпись не тревога, а опора. */
    .qty-own-hint { color: var(--hint); font-size: 12px; margin-top: 4px; line-height: 1.4; }
    /* WP553: кнопка «своё» на ступени страхового (дни) — та же идиома, что у
       количества (.qty-own). Кнопка-плашка + тихая подпись справа: человек видит
       дверь «больше 14» ДО того, как упрётся в потолок бегунка. */
    .ss-own-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
    .ss-own-row .qty-chip { min-width: 44px; min-height: 44px; }
    .ss-own-cap { color: var(--hint); font-size: 12px; line-height: 1.3; }
    /* Разговор живёт ПОД рядом и НЕ блокирует его: человек продолжает менять
       количество, вопрос едет за ним (модалка перехватывала клик — обход 26.07).
       WP132: отдельной коробки «панель» больше нет — разговор И ЕСТЬ зона ступени
       (.zone-rung), третья сверху вниз. Одна коробка вместо двух вложенных. */
    .qty-objection {
      margin-top: 6px; padding: 8px 10px; border-radius: 8px;
      background: rgba(232,176,75,.12); color: var(--discuss); font-size: 12px;
      line-height: 1.5;
    }
    .qty-exits { display: flex; flex-direction: column; gap: 7px; margin-top: 8px; }
    /* WP193: две стороны разговора — про ПАРАМЕТР ступени и про КОЛИЧЕСТВО
       заказа — стояли вперемешку как равные, и их путали именно поэтому. Подпись
       стороны и строка порядка стоят на границе; заливки у них нет — это не
       действия, а указатели, и спорить видом с кнопками им незачем. */
    .exits-cap {
      color: var(--hint); font-size: 11px; font-weight: 600;
      text-transform: uppercase; letter-spacing: .04em; margin-bottom: 1px;
    }
    .step-order {
      margin-top: 8px; padding: 7px 10px; border-radius: 8px;
      background: rgba(255,255,255,.04); border-left: 3px solid var(--sep);
      color: var(--hint); font-size: 12px; line-height: 1.45;
    }
    /* Ярлык страхового: 44px, как у остальных действий карточки (WP130) — за ним
       переход на ступень, где норму меняют. Вид «ссылки с рамкой», а не заливки:
       главное действие ступени — запись, и спорить с ней ярлык не должен. */
    .step-ss {
      display: inline-flex; align-items: center; min-height: 44px; margin-top: 6px;
      padding: 5px 12px; border-radius: 8px; border: 1px dashed var(--sep);
      background: transparent; color: var(--hint); font-size: 12px;
      font-weight: 600; cursor: pointer; touch-action: manipulation;
      text-align: left;
    }
    .step-ss:active { filter: brightness(.92); }
    .qexit {
      min-height: 44px; min-width: 44px; border-radius: 10px; font-size: 14px;
      font-weight: 600; cursor: pointer; touch-action: manipulation; padding: 0 12px;
    }
    .qexit-main { border: none; background: var(--accent); color: #06263f; }
    .qexit-agree { border: 1px solid var(--ok); background: transparent; color: var(--ok); }
    .qexit-keep { border: 1px solid var(--sep); background: transparent; color: var(--hint); }
    /* WP160: выход из ЗАКОНЧЕННОГО разговора. 27.07 его не было вовсе — панель
       выходов пряталась навсегда, и масло с гребешком закрывали правкой файла. */
    .qexit-own { border: none; background: var(--ok); color: #04231a; }
    /* WP217: взять число, которое расчёт даёт НА ПОПРАВКЕ человека. Заливкой
       акцента — это ДЕЙСТВИЕ ступени прогноза, а не запасной выход: 31.07 его
       искали вслух и не нашли. От «согласен на расчёт» (обводка) отличается видом
       так же, как по смыслу: там наша цифра, тут — наша арифметика на его цифре. */
    .qexit-oncorr { border: none; background: var(--accent); color: #06263f; }
    /* WP296: соседняя упаковка. Обводкой «в порядке» — это ЗАКРЫВАЮЩИЙ выход без
       спора, и от согласия с расчётом он отличается только тем, чьё число едет. */
    .qexit-pack { border: 1px solid var(--ok); background: transparent; color: var(--ok); }
    /* WP299: разовая закупка. Обводкой, а не заливкой: это НЕ спор и не главное
       действие ступени — это законный третий путь, о котором человек должен знать,
       но который не должен спорить видом с «согласен на расчёт». Цветом отличается
       от «оставить свою цифру» (серое) — там спор, тут его нет. */
    .qexit-oneoff {
      border: 1px dashed var(--discuss); background: transparent; color: var(--discuss);
    }
    /* WP298: «записать остаток, заказ оставить прежним». Обводкой «в порядке», как
       у соседней упаковки: это тоже ЗАКРЫВАЮЩИЙ выход без спора — заказ остаётся
       расчётным, спорить не о чем, а ценность нажатия в том, что факт с кухни
       доезжает до якоря. Заливку акцента не берём: главное действие ступени
       по-прежнему запись со ступенью дальше, и спорить с ней видом нечем. */
    .qexit-keep-order { border: 1px solid var(--ok); background: transparent; color: var(--ok); }
    /* WP371: ДВЕРЬ НАЗАД ПО ЛЕСТНИЦЕ. Обводкой, как «оставить свою цифру» — это
       путь, а не решение, и цветом с главными действиями ступени не спорит;
       стрелка говорит направление без легенды. 44px по WP130, как у всех
       действий панели. */
    .qexit-back { border: 1px solid var(--sep); background: transparent; color: var(--text); }
    .qexit:active { filter: brightness(.92); }
    /* Окно комментария разовой закупки. Во всю ширину панели и под кнопками —
       читается как продолжение решения, а не как отдельная форма. */
    .oneoff-note {
      width: 100%; box-sizing: border-box; margin-top: 6px; padding: 8px 10px;
      border-radius: 8px; border: 1px solid var(--sep); background: rgba(255,255,255,.04);
      color: var(--text); font: inherit; font-size: 13px; line-height: 1.45; resize: vertical;
    }
    /* WP160: полоса сходимости — ПЕРВОЙ строкой ступени. «Сошлось или нет? Где он
       там пишет, что сошлось?» прозвучало трижды подряд: состояние было в тексте,
       но терялось в плотности. Полоса занимает место прежней строки «Вам нужно N ·
       расчёт даёт M» — она называет те же два числа, поэтому экран не уплотняется. */
    .lband {
      border-radius: 8px; padding: 8px 10px; margin: 0 0 6px;
      font-size: 13px; line-height: 1.45; font-weight: 600;
      font-variant-numeric: tabular-nums;
    }
    .lband.hit { background: rgba(77,203,93,.12); color: var(--ok);
                 border: 1px solid rgba(77,203,93,.3); }
    .lband.miss { background: rgba(232,176,75,.12); color: var(--discuss);
                  border: 1px solid rgba(232,176,75,.3); }
    .total {
      margin-top: 16px; padding: 14px 16px; background: var(--card);
      border-radius: 12px; display: flex; justify-content: space-between;
      align-items: baseline; font-size: 16px;
    }
    .total .lbl { color: var(--hint); }
    .total .val { font-weight: 700; font-size: 19px; }
    .totalnote { color: var(--hint); font-size: 12px; margin: 6px 2px 0; text-align: right; }
    /* WP120: подпись шага степпера */
    .steplabel {
      display: block; text-align: center; font-size: 12px;
      color: var(--hint); margin-top: 4px; letter-spacing: .2px;
    }
    /* WP120: полоса минимального заказа */
    .minzo {
      margin: 8px auto 0; max-width: 640px; font-size: 13px; padding: 0 10px;
    }
    .minzo-ok { color: var(--ok); text-align: right; }
    .minzo-warn { color: var(--discuss); text-align: right; }
    /* WP153: заказывать нечего — минимум назван справочно. Тон подсказки, а не
       предупреждения: цвет --warn на пустой корзине и был половиной ложной тревоги. */
    .minzo-idle { color: var(--hint); text-align: right; }
    .minzo-bar {
      height: 6px; border-radius: 3px; background: rgba(255,255,255,.1);
      margin-bottom: 6px; overflow: hidden;
    }
    .minzo-fill {
      height: 100%; border-radius: 3px;
      background: linear-gradient(90deg, var(--discuss), var(--ok));
    }
    /* WP121: блок «что если» — предпросчёт строки заказа */
    .whatif {
      margin-top: 8px; padding: 8px 10px; background: var(--card2);
      border-radius: 8px; font-size: 12px;
    }
    /* WP132: шапка «Что если» и подпись под ней удалены. Это наследие песочницы —
       сейчас блок не прикидка «а что если», а РАЗГОВОР о конкретном числе, и его
       заголовок — «шаг N из 3», а не название инструмента. */
    .whatif-row { margin-bottom: 6px; }
    .whatif-label {
      display: flex; justify-content: space-between; align-items: baseline;
      margin-bottom: 2px;
    }
    .whatif-lbl { color: var(--hint); }
    .whatif-val { font-weight: 600; font-variant-numeric: tabular-nums; }
    /* WP130: ползунок.
       (1) touch-action: pan-y — ОБЯЗАТЕЛЬНО: без него палец, начатый на ползунке,
           не прокручивает страницу, и список позиций «залипает» в зоне рычага.
       (2) Зона касания: дорожка была 4px высотой — в неё нечем попасть. Паддинг
           растит зону до 28px, background-clip: content-box держит фон только на
           дорожке, так что визуально она осталась той же тонкой полосой.
       (3) Палец-мишень 16 → 22px.
       До 44px зона не доведена сознательно: это стоило бы +40px высоты на каждый
       ползунок, а исследование §2 рекомендует страховой запас с ползунка вообще
       снять (кнопки-сегменты) — тратить вёрстку на уходящий элемент незачем. */
    .whatif input[type="range"] {
      width: 100%; accent-color: var(--accent);
      -webkit-appearance: none; appearance: none;
      height: 4px; border-radius: 2px; background: var(--sep); outline: none;
      padding: 12px 0; background-clip: content-box;
      touch-action: pan-y;
    }
    .whatif input[type="range"]::-webkit-slider-thumb {
      -webkit-appearance: none; appearance: none;
      width: 22px; height: 22px; border-radius: 50%;
      background: var(--accent); cursor: pointer;
    }
    /* Строка последствия под рычагом: «СЗ 5 → 7 дн: заказ 4 упак (+10 кг, +12 000 ₽)».
       Цифра в ней ПРЕДВАРИТЕЛЬНАЯ — это ярус 1, предпросчёт на фронте; итог даёт
       пересчёт корзины движком (ADR-030: считает движок, фронт показывает). */
    .whatif-conseq {
      margin-top: 2px; padding-left: 4px; border-left: 2px solid var(--accent);
      color: var(--text); line-height: 1.4;
    }
    .whatif-warn {
      color: var(--escalated); font-weight: 600; margin-top: 2px;
    }
    /* Пометка добивки: часть цифры заказа — это добивка до минимального заказа
       поставщика, и после пересчёта она изменится. WP132: стоит под рядом, в зоне
       «сколько нужно», — она объясняет ЦИФРУ, а не параметр. */
    .whatif-pull {
      color: var(--discuss); font-size: 11px; margin-top: 2px;
    }
    /* WP10173: источник нормы СЗ — ЧЬЁ это число («поставил admin 30.09 из
       корзины», «зона «unclassified» точки — …», «умолчание системы…»). Приглушён
       как справка (не предупреждение — предупреждение рядом, ss-review-due),
       стоит там же, где норма читается и правится. Слова — из ядра
       (cart_core.ss_norm_source_words), фронт печатает готовое (ADR-030). */
    .ss-norm-src {
      color: var(--hint); font-size: 11px; margin-top: 2px;
    }
    /* WP10278: лента применённых правок нормы («23.09 · 3 дн → 6 дн · admin»,
       «и ещё 2 раньше») — под числом дней, там же, где норма читается и
       правится. Приглушена как справка: это история, не предупреждение
       (ss-review-due рядом). Строки собирает ядро
       (cart_core.ss_edit_ledger_map), фронт печатает готовое (ADR-030). */
    .ss-edit-ledger {
      color: var(--hint); font-size: 11px; margin-top: 2px;
    }
    .ss-edit-ledger-head {
      color: var(--hint); font-size: 11px;
    }
    /* WP310: срок пересмотра нормы СЗ — рядом с рычагом, там же, где норма
       редактируется; слова — из ядра (cart_core.ss_review_words). Вес подчёркивания
       как у добивки: не ошибка, но человек решает по норме, которую просили
       пересмотреть, — слово у места решения стоит. */
    .ss-review-due {
      color: var(--discuss); font-size: 11px; margin-top: 2px;
    }
    /* WP127: 12px → 16px (порог зума айфона на фокусе) */
    /* WP130: + min-height 44px и touch-action (шрифт 16px — защита WP127, не трогать) */
    /* WP132: редактор кванта переехал в справочную зону — правила ведут его туда
       ЦЕЛИКОМ. Иначе селектор потерял бы и 16px (автозум айфона), и 44px (WP130):
       гейты читают CSS-текст и остались бы зелёными, а на телефоне защита исчезла. */
    .whatif select, .zone-ref select {
      background: var(--card); color: var(--text); border: 1px solid var(--sep);
      border-radius: 4px; padding: 2px 4px; font-size: 16px;
      min-height: 44px; touch-action: manipulation;
    }
    /* WP127: селектор был [type="number"] и перекрывал собой правило поля остатка;
       поля стали текстовыми — правило адресуется «своему кванту» по data-атрибуту.
       Ширина 60 → 74px: 16px-шрифт в 60px не помещал «12,5». */
    /* WP130: + min-height 44px (поле «свой квант» — тоже мишень пальца) */
    .whatif input[data-wh-pk-num], .zone-ref input[data-wh-pk-num] {
      width: 74px; background: var(--card); color: var(--text);
      border: 1px solid var(--sep); border-radius: 4px;
      padding: 2px 4px; font-size: 16px; text-align: right;
      min-height: 44px;
    }
    /* WP131: сверка фасовки по накладной. Имя SKU печатаем ЦЕЛИКОМ (перенос строк
       разрешён): в нём и лежит ответ — «…, в/у, 1 кг, 5 пач/кор». Текстовые строки
       высоту не задают (это не мишени пальца); кнопка распознанного — задаёт, как
       все мишени после WP130. */
    /* WP1240: .pack-now из селектора ушла — эмитента у класса не было (мёртвая
       половина правила); pack-nom в той же строке жив (строка 4420). */
    .whatif .pack-nom,
    .zone-ref .pack-nom {
      font-size: 12px; color: var(--hint); line-height: 1.35;
      margin: 2px 0; overflow-wrap: anywhere;
    }
    /* WP220: та же строка стоит теперь У КОЛИЧЕСТВА, а не в свёрнутых
       подробностях, — значит и правило вида ей нужно своё: зоны .whatif/.zone-ref
       её больше не содержат. Тихий серый обязателен: строка видна на КАЖДОЙ
       карточке, и кричать ей не о чем — она отвечает на вопрос, а не задаёт его. */
    .pos .pack-nom {
      font-size: 12px; color: var(--hint); line-height: 1.35;
      margin: 2px 0 0; overflow-wrap: anywhere; text-align: center;
    }
    /* WP225: расхождение закреплённого SKU с последней накладной. Тише
       предупреждения об ошибке (чинить нечего — пин правит оператор), но громче
       серого: это сигнал «подменили товар либо пин устарел», и он не должен
       теряться в строке происхождения. */
    .pos .pack-diverged, .whatif .pack-diverged, .zone-ref .pack-diverged,
    .assrow .anom.pack-diverged {
      color: var(--discuss);
    }
    .whatif .pack-cands, .zone-ref .pack-cands {
      display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
      font-size: 12px; color: var(--hint); margin: 4px 0;
    }
    .whatif .pack-cand, .zone-ref .pack-cand {
      min-height: 44px; touch-action: manipulation;
      padding: 4px 12px; font-size: 15px; font-weight: 600;
      background: var(--card); color: var(--text);
      border: 1px solid var(--sep); border-radius: 6px; cursor: pointer;
    }
    /* WP9990: чипы фасовки — РЯД (packRowHtml), не раскрываш. Число и слово —
       ДВЕ строки внутри чипа: длинное слово переносится ВНУТРИ своего чипа,
       а не раздувает ряд (max-width:100% + overflow-wrap:anywhere). 44px/16px
       мишени не тронуты (WP127/WP130). */
    .zone-ref .pack-cand {
      display: inline-flex; flex-direction: column; align-items: flex-start;
      max-width: 100%; overflow-wrap: anywhere; text-align: left;
      line-height: 1.25;
    }
    .zone-ref .pack-cand .pk-big { font-size: 15px; font-weight: 600; }
    .zone-ref .pack-cand .pk-sub { font-size: 11px; font-weight: 400; color: var(--hint); }
    /* WP9990: подсветка ОТВЕЧАЮЩЕГО чипа (window.__pack.chipOn) — акцент. */
    .zone-ref .pack-cand.pk-on {
      border-color: var(--accent); background: rgba(100,181,239,.12);
    }
    /* WP9954: подсказка «сделать упаковкой» в ряду — пунктир (не выбор, а
       предложение ядра). */
    .zone-ref .pack-cand.pk-hint { border-style: dashed; color: var(--accent); }
    /* WP9990: ряд чипов — flex, wrap, выравнивание по верху (у чипов две
       строки, у «своё…» одна). Заголовок «Упаковка» — тихий, не растёт. */
    .zone-ref .pk-row {
      display: flex; flex-wrap: wrap; align-items: flex-start; gap: 6px;
    }
    .zone-ref .pk-hd {
      color: var(--hint); font-size: 12px; padding-top: 12px; flex: 0 0 auto;
    }
    .zone-ref .pk-chips {
      display: flex; flex-wrap: wrap; gap: 6px; flex: 1 1 0; min-width: 0;
    }
    /* Подпись выбора и строка последствия — под рядом во всю ширину, а не
       ещё одним элементом рядом с чипами (строка .pk-row — flex, wrap). */
    .zone-ref .pk-row > .pack-lever,
    .zone-ref .pk-row > .whatif-conseq { flex: 1 1 100%; }
    /* WP9990: подпись выбора (pack-lever) — теперь ТИХАЯ: чипы сами по себе
       читаются как кнопки, акцент был нужен (WP372), потому что <select> не
       выглядел рычагом. */
    .zone-ref .pack-lever {
      font-size: 12px; font-weight: 400; color: var(--hint);
      margin: 4px 0 2px; line-height: 1.35;
    }
    /* WP372: подпись выбора фасовки — РЫЧАГ, а не справка. Справочные строки
       зоны (pack-nom, pack-cands) — приглушённые (--muted/--hint): отвечают на
       вопрос, не задают его. Эта строка делает обратное — зовёт к действию,
       поэтому цвет у неё АКЦЕНТНЫЙ: человек, читающий происхождение фасовки
       у количества, обязан опознать соседний выбор как «здесь можно менять».
       Слова не печатаем — их даёт ядро (p.pack_lever, ADR-030). */
    /* WP9990: зона фасовки из правила вышла — её подпись тихая (правило выше),
       иначе это правило, стоящее ниже, перебивало его каскадом. */
    .whatif .pack-lever {
      font-size: 13px; font-weight: 600; color: var(--accent);
      margin: 4px 0 2px; line-height: 1.35;
    }
    /* WP127: поле, в котором число не разобралось. Немой пропуск — худший исход:
       человек жмёт «применить» и не понимает, почему ничего не произошло.
       Правило стоит ПОСЛЕ обоих правил полей осознанно: специфичность у них
       одинаковая (0,2,1), и раньше по каскаду выигрывало бы поле, а не пометка —
       на «своём кванте» красная рамка не появлялась (поймано живым обходом). */
    .whatif input.wh-bad, .zone-ref input.wh-bad {
      border-color: var(--escalated); background: rgba(236,106,94,.10);
    }
    .whatif-conseq.wh-bad-msg {
      color: var(--escalated); border-left-color: var(--escalated); font-weight: 600;
    }
    /* ── WP132: одна вертикаль карточки ──────────────────────────────────────
       Карточку собирали слоями (факты WP128 → ряд WP129 → лестница WP126) и целиком
       не пересобирали ни разу. Вышли три коробки, спорящие за внимание: вопрос в
       одной, поле для ответа двумя блоками ниже, между ними чужая шапка и ссылка,
       дублирующая уже открытую ступень. Вердикт основателя на стенде 26.07:
       «миллион цифр, интуитивно непонятно куда жать, нет последовательности».
       Стало: три тихих заголовка и справочная зона внизу —
       что мы знаем → сколько нужно → шаг N из 3 → справочное.
       Порядок зон размечен ЯВНО (data-zone), это контракт, а не догадка по тексту. */
    .card-zone { margin-top: 10px; }
    .zone-rung { font-size: 13px; }
    .zone-hd {
      color: var(--hint); font-size: 11px; font-weight: 600;
      letter-spacing: .8px; text-transform: uppercase; margin-bottom: 3px;
    }
    .zone-hd.on { color: var(--accent); }
    /* Пройденная ступень — СТРОКА-ИТОГ, а не действие: на третьей ступени на
       экране одновременно жили три живые кнопки подтверждения, и человек не мог
       понять, какая из них текущая (обход архитектора 26.07). */
    .rung-done { color: var(--ok); font-size: 12px; font-weight: 600; margin: 3px 0; }
    /* Справочная зона: квант, его подробности и сброс. Это не участники разговора —
       к ним обращаются изредка, поэтому одной строкой ВНИЗУ, после ступени. */
    .zone-ref {
      margin-top: 10px; padding: 8px 10px; background: var(--card2);
      border-radius: 8px; font-size: 12px;
    }
    .zone-ref .ref-line {
      display: flex; flex-wrap: wrap; align-items: center; gap: 0 12px;
    }
    .zone-ref .ref-val { color: var(--hint); font-variant-numeric: tabular-nums; }
    /* WP588: зона фасовки несёт органы редактора (select [data-wh-pk-select],
       поле [data-wh-pk-num], чипсы [data-pk-cand]) и на строке БЕЗ лестницы —
       цикл .whatif их там не вяжет, их вяжет отдельный проход по .zone-ref. */
    /* WP374 (TD-466): редактор кванта живёт в ВИДИМОЙ строке — и обязан читаться
       как РЫЧАГ, а не как ещё один серый факт. Строка занимает всю ширину
       (flex-basis 100%): select + «свой квант» + подпись не давят «квант 10,8 л»
       и «Сброс» в одну тесную линию, а подпись WP372 (акцентная, .pack-lever)
       стоит над выбором, как и задумано. Зоны касания 44px/16px селектора и
       поля не тронуты — правила .zone-ref select / input[data-wh-pk-num]
       адресуют их по-прежнему. */
    .zone-ref .ref-line [data-wh-row="pk"] {
      flex: 1 1 100%; margin: 2px 0;
    }
    /* WP130 для новых мишеней: честные 44px и гашение двойного-тап-зума. */
    .ref-toggle {
      display: inline-flex; align-items: center; min-height: 44px;
      color: var(--hint); cursor: pointer; -webkit-tap-highlight-color: transparent;
      border-bottom: 1px dotted currentColor; touch-action: manipulation;
    }
    .ref-toggle:active { color: var(--accent); }
    /* WP126: лестница эскалации — раскрытие по одной ступени.
       Карточка вываливала четыре рычага сразу, и страховым можно было подогнать
       любую цифру, не сходив к холодильнику. Теперь видно ровно то, что уместно
       на текущей ступени спора. */
    .ladder-head {
      margin: 2px 0 6px; padding: 6px 8px; border-radius: 6px;
      background: rgba(100,181,239,.10); line-height: 1.45;
    }
    .ladder-head .lstep { color: var(--accent); font-weight: 600; }
    .ladder-head.ladder-done {
      background: rgba(77,203,93,.14); color: var(--ok); font-weight: 600;
    }
    /* WP130: ссылка стоит на своей строке — даём честные 44px и отклик на тап
       (tap-highlight снят, :active не было — палец жал в пустоту, §4). */
    .ladder-check {
      display: inline-flex; align-items: center; min-height: 44px;
      margin: 0 0 6px; color: var(--accent);
      cursor: pointer; -webkit-tap-highlight-color: transparent;
      border-bottom: 1px dotted var(--accent);
      touch-action: manipulation;
    }
    .ladder-check:active { color: var(--text); border-bottom-color: var(--text); }
    /* WP130: 12px-шрифт в паддинге 4px давал ~21px высоты → min-height 44px */
    .ladder-ok {
      margin-top: 5px; padding: 4px 14px; border-radius: 8px; min-height: 44px;
      border: 1px solid var(--sep); background: transparent; color: var(--ok);
      font-size: 12px; cursor: pointer; -webkit-tap-highlight-color: transparent;
      touch-action: manipulation;
    }
    .ladder-ok:active { background: rgba(77,203,93,.12); }
    /* WP10208: ответ на вопрос о приходе того же дня — две кнопки под полем
       ступени 1. Выбранный ответ обязан быть ВИДЕН (иначе человек не знает,
       что он уже ответил, и ответ уедет меткой в журнал без его ведома). */
    .whatif-arrival-btns { display: flex; gap: 8px; flex-wrap: wrap; }
    .ladder-ok.on { background: rgba(77,203,93,.18); border-color: var(--ok); }
    .ladder-note { color: var(--hint); margin: 4px 0 2px; }
    /* WP130: 11px-шрифт в паддинге 3px давал ~19px высоты → min-height 44px */
    .whatif-reset {
      display: inline-flex; align-items: center; min-height: 44px;
      margin-top: 4px; padding: 3px 14px;
      border-radius: 4px; border: 1px solid var(--sep); background: transparent;
      color: var(--hint); font-size: 11px; cursor: pointer;
      touch-action: manipulation;
    }
    /* WP122: apply-lever — блок применения рычагов с записью в справочники.
       Предохранитель: кнопка «Применить» заблокирована (disabled) без выбора причины.
       Модалка подтверждения (подтверд) показывает: что меняем, с чего на что,
       как изменится заказ (цифры предпросчёта), куда запишется, до какого числа вернуться.
       Кнопка в модалке называет действие: «Записать страховой 6 дн», «Записать квант 2 кг».
       Коридор нормы: СЗ вне 0..14 — отклоняется (верхняя граница 14 дней).
       Потолок ОСГ: СЗ > ОСГ — блокируется с числами («ОСГ N дн: X дн запаса — Y дн уедут»).
       Журнал: отказ (declined/reject) в модалке тоже пишется в ops_feedback_log.
       Права: рычаг не по роли НЕ показывается (role-based visibility, проверять роль).
       Якорь — шеф/Юля/Виктор, СЗ — Юля/Виктор (шеф с пометкой), квант — все.
       WP135: сорсинг ИСПОЛНЯЕТСЯ из модалки переключения (рычаг 'sourcing' →
       запись в order-guide → предложение пересчёта), вердикта больше не ждёт.
       Прогноз никуда в движок не пишется — только ops_feedback_log. */
    .apply-lever {
      margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--sep);
    }
    /* WP127: 11px → 16px. Это был худший случай на странице: выбор причины —
       обязательный шаг применения, и айфон зумил ровно на нём. */
    /* WP130: + min-height 44px. Выбор причины — обязательный шаг применения:
       промах по нему стоит человеку целого захода в рычаг. */
    .apply-lever select, select.why {
      background: var(--card); color: var(--text); border: 1px solid var(--sep);
      border-radius: 4px; padding: 3px 6px; font-size: 16px; width: 100%;
      margin-bottom: 4px; min-height: 44px; touch-action: manipulation;
    }
    /* WP218: причина решения о КОЛИЧЕСТВЕ — рядом с выходами разговора, где
       позицию и закрывают. Вид тот же, что у причины рычага (одна привычка на
       обе), но выбор НЕ обязателен и ничью кнопку не блокирует: «любые
       согласования это палка в колёса нашего велосипеда». Оттого и подпись
       говорит «если есть, что сказать», а не «выбери, чтобы применить». */
    select.why { margin-bottom: 2px; }
    /* WP130: было ~27px (12px шрифт + 5px паддинг) — самая мелкая кнопка страницы
       при том, что за ней запись в справочник. inline-flex, чтобы надпись стояла
       по центру выросшей до 44px кнопки, а не прижималась к верху. */
    .apply-btn {
      display: inline-flex; align-items: center; justify-content: center;
      min-height: 44px; margin-top: 2px; padding: 5px 18px;
      border-radius: 6px; border: none;
      background: var(--ok); color: #0b2410; font-size: 12px; font-weight: 600;
      cursor: pointer; touch-action: manipulation;
    }
    .apply-btn:disabled { opacity: .4; cursor: not-allowed; }
    .apply-btn:active:not(:disabled) { filter: brightness(.92); }
    /* модалка подтверждения применения */
    .apply-modal-title { font-size: 15px; margin: 0 0 6px; }
    .apply-modal-detail { font-size: 13px; line-height: 1.5; }
    .apply-modal-detail b { color: var(--accent); }
    /* WP160: ПОСЛЕДСТВИЕ — отдельной строкой и до кнопки. 27.07 оно стояло справкой
       посреди справок («Заказ: 3 → 1 кг (−2 кг)») и было прочитано никем: запись
       остатка срезала желаемые 3 кг гребешка до 1, и позиция уехала бы единицей. */
    /* WP10341 (встреча 06.10): подпись ГРУППЫ над двумя дверями перешага —
       «Записать остаток и выбрать, что уедет». 27.07 окно лечили одной кнопкой,
       потому что две похожие кнопки сбивали; здесь двери названы разными
       числами и стоят под одной подписью — человек выбирает, а не гадал,
       «вот в чём разница». */
    .apply-modal-group {
      margin: 10px 0 2px; font-size: 13px; font-weight: 600;
      color: var(--discuss); line-height: 1.4;
    }
    .apply-modal-conseq {
      margin: 9px 0 2px; padding: 9px 11px; border-radius: 8px;
      background: rgba(100,181,239,.10); border: 1px solid rgba(100,181,239,.28);
      font-size: 14px; line-height: 1.45; font-weight: 600;
      font-variant-numeric: tabular-nums;
    }
    .apply-modal-warn {
      background: rgba(232,176,75,.12); color: var(--discuss);
      border-radius: 8px; padding: 8px 10px; font-size: 13px;
      margin: 10px 0;
    }
    /* WP299: «сделать нормой» — отдельное осознанное действие. Галочка снята по
       умолчанию, поэтому она обязана ЧИТАТЬСЯ, а не прятаться: цель ровно
       обратная — чтобы постоянную норму ставили нарочно, а не проглядели. */
    .ss-scope {
      display: flex; align-items: center; gap: 8px; min-height: 44px;
      margin: 10px 0 0; font-size: 14px; font-weight: 600; cursor: pointer;
      touch-action: manipulation;
    }
    .ss-scope input { width: 20px; height: 20px; }
    .ss-scope-hint {
      color: var(--hint); font-size: 12px; line-height: 1.45; margin: 0 0 4px;
    }
    .apply-confirm-btn {
      min-height: 48px; border-radius: 12px; border: none; font-size: 14px;
      font-weight: 600; cursor: pointer; background: var(--ok); color: #0b2410;
      touch-action: manipulation; /* WP130 */
    }
    .apply-confirm-btn:disabled { opacity: .35; cursor: not-allowed; }
    /* WP121: рычаги предпросчёта рендерятся динамически в render().
       Содержимое: страховой запас (дн, 0..15), прогноз (поле в единицах ТОВАРА в
       день, WP133 — шкалы множителя больше нет), квант (закреплённый + ручной
       ввод). Под каждым рычагом — строка последствия, обновляемая на каждый ввод.
       Цифра последствия ПРЕДВАРИТЕЛЬНАЯ: итог даёт пересчёт корзины движком.
       WP132: рычаги ступеней живут в зоне ступени (.zone-rung), квант со «сбросом» —
       в справочной зоне (.zone-ref) внизу карточки; отдельной шапки у них нет,
       заголовок зоны — «шаг N из 3».
       У позиции с dcmp_pullforward > 0 пишется:
       «в этой цифре N ед добивки до минимального заказа — после пересчёта она изменится».
       Кнопка «сброс» возвращает рычаги к расчёту движка.
       Рычаги скрыты, когда нет need_qty_raw (входы формулы не публикуются на Mac). */
    /* WP123: кнопка «Пересчитать корзину» + штамп времени + блок «изменилось не по вашей правке».
       Кнопка появляется после применённого рычага (или правки количества).
       На время прогона — спиннер и блокировка кнопки (2 сек, прогресс-бар не нужен).
       После: штамп «пересчитано в ЧЧ:ММ» в шапке корзины.
       Блок диффа сверху списка, если by_engine непуст:
       «Изменилось не по вашей правке — добивка до минимального заказа»
       с пояснением одной строкой: «эти позиции движок подтянул, чтобы корзина добрала минимальный заказ поставщика».
       Если by_engine пуст — так и написать: «кроме вашей правки ничего не изменилось». */
    .recalc-bar {
      margin: 8px 0; display: flex; align-items: center; gap: 8px;
    }
    /* WP130: 40 → 44px */
    .recalc-btn {
      flex: 1; min-height: 44px; border-radius: 10px; border: none;
      background: var(--accent); color: #06263f; font-size: 14px;
      font-weight: 600; cursor: pointer; touch-action: manipulation;
    }
    .recalc-btn:disabled { opacity: .45; cursor: not-allowed; }
    .recalc-btn:active:not(:disabled) { filter: brightness(.92); }
    .recalc-stamp {
      font-size: 12px; color: var(--ok); font-weight: 600;
    }
    .diff-block {
      margin: 6px 0 12px; padding: 10px 12px; border-radius: 10px;
      background: var(--card); font-size: 13px; line-height: 1.5;
    }
    .diff-block .diff-title {
      font-weight: 600; margin-bottom: 4px; color: var(--discuss);
    }
    .diff-block .diff-note {
      color: var(--hint); font-size: 12px; margin-top: 4px;
    }
    .diff-block .diff-item {
      padding: 2px 0; display: flex; justify-content: space-between;
    }
    .diff-block .diff-item .dn { font-weight: 600; }
    .diff-block .diff-item .dv { color: var(--discuss); font-variant-numeric: tabular-nums; }
    /* WP123: кнопки Пересчитать корзину / штамп пересчитано в ЧЧ:ММ;
       блок «Изменилось не по вашей правке — добивка до минимального заказа».
       пояснение: эти позиции движок подтянул, чтобы корзина добрала минимальный заказ поставщика.
       если по вашей правке — «кроме вашей правки ничего не изменилось». */
    .msg { text-align: center; color: var(--hint); padding: 60px 20px; font-size: 15px; }
    .msg.err { color: var(--escalated); }
    /* WP570: занятая база — «подождите», а не «сломалось». Цвет ожидания (жёлтый),
       не цвет ошибки (красный): человек видит, что это пересчёт, а не авария. */
    .msg .dbbusy-title { color: var(--discuss); font-size: 16px; font-weight: 600; line-height: 1.4; }
    .msg .dbbusy-sub { color: var(--hint); font-size: 13px; margin-top: 6px; }
    .msg .dbbusy-retry {
      margin-top: 16px; min-height: 44px; padding: 0 20px; border-radius: 10px;
      border: 1px solid var(--discuss); background: transparent; color: var(--discuss);
      font-size: 15px; font-weight: 600; cursor: pointer; touch-action: manipulation;
    }
    .msg .dbbusy-retry:active { background: rgba(232,176,75,.12); }
    /* WP162: реестр решений не читается. Полоса стоит ПЕРВОЙ и красная: под ней
       чистый расчёт движка, а не результат разговора, и человек обязан это знать
       ДО того, как посмотрит на цифры (27.07 он видел «✓ без вопросов» на всех
       позициях и сумму 52 734,99 ₽ — ни одного признака, что решений уже нет). */
    .regerr {
      margin: 0 0 10px; padding: 12px 14px; border-radius: 12px;
      background: rgba(244,102,102,.14); border: 1px solid var(--escalated);
      color: var(--escalated); font-size: 14px; line-height: 1.4; font-weight: 600;
    }
    /* нижняя кнопка «Согласовать» */
    /* WP130: липкий футер.
       (1) Безопасная зона: viewport-fit=cover стоял с самого начала, а env() не
           применялся ни разу — кнопка «Согласовать» лезла под домашний индикатор.
       (2) Клавиатура ресайзит ТОЛЬКО визуальный вьюпорт, а position: fixed висит
           на layout-вьюпорте — футер уезжал под клавиатуру. VirtualKeyboard API в
           Safari нет, interactive-widget не реализован; единственный путь —
           visualViewport (обработчик в конце скрипта пишет --kb и --kbbar).
           Поднимаем на высоту клавиатуры ПЛЮС высоту полосы «Готово», чтобы
           футер встал над ней, а не поверх.
       (3) transform, а не переанкеровка на top: без JS (и на десктопе) футер
           остаётся ровно там, где был — сломать себя нечем. */
    .approvebar {
      position: fixed; left: 0; right: 0; bottom: 0;
      padding: 12px 14px calc(20px + var(--sab));
      background: linear-gradient(to top, var(--bg) 70%, rgba(23,33,43,0));
      transform: translateY(calc(-1 * (var(--kb) + var(--kbbar))));
    }
    .approvebar .inner { max-width: 640px; margin: 0 auto; }
    /* WP564: полоса «страница упала сама» — сверху, поверх всего, пока не обновят.
       Не молчание: экран не притворяется живым, когда отрисовка не дошла до конца. */
    .crashbar {
      position: fixed; left: 0; right: 0; top: 0; z-index: 9999;
      padding: 10px 14px;
      background: #b3261e; color: #fff; font-size: 14px; line-height: 1.4;
    }
    .crashbar .inner { max-width: 640px; margin: 0 auto; }
    .crashbar .way { display: block; margin-top: 4px; font-weight: 700; }
    .approvebtn {
      width: 100%; min-height: 50px; border: none; border-radius: 12px;
      background: var(--ok); color: #0b2410; font-size: 16px; font-weight: 700;
      cursor: pointer; touch-action: manipulation; /* WP130 */
    }
    .approvebtn:active { filter: brightness(.92); }
    /* модалка возражения */
    .backdrop {
      position: fixed; inset: 0; background: rgba(0,0,0,.55);
      /* z-index ВЫШЕ панели ассортимента (72) и виджета (70): модалка-возражение
         из панели добавления открывалась ПОД ней — «кнопка ничего не делает»
         (живой тест 22.07; та же причина у «невидимой модалки» хартии C) */
      display: flex; align-items: flex-end; justify-content: center; z-index: 95;
    }
    .modal {
      width: 100%; max-width: 640px; background: var(--card); border-radius: 16px 16px 0 0;
      padding: 20px 18px 26px; animation: slideup .18s ease-out;
    }
    @keyframes slideup { from { transform: translateY(40px); opacity: .4; } to { transform: none; opacity: 1; } }
    .modal h2 { font-size: 17px; margin: 0 0 4px; }
    .modal .mhint { color: var(--hint); font-size: 13px; margin-bottom: 14px; }
    /* WP441 (TD-309): пометка состояния числа в шапке модалки — рядом с
       «Расчёт даёт N», а не только в сноске ниже. Слова — из пейлоада
       (ADR-030); пара с пином пометки не несёт. */
    .modal .mhint .entry-state {
      color: var(--discuss); font-weight: 600; white-space: nowrap;
    }
    .facts { background: var(--card2); border-radius: 10px; padding: 4px 12px; margin-bottom: 12px; }
    .factrow {
      display: flex; justify-content: space-between; padding: 8px 0;
      border-bottom: 1px solid var(--sep); font-size: 14px;
    }
    .factrow:last-child { border-bottom: none; }
    .factrow .fl { color: var(--hint); }
    .factrow .fv { font-weight: 600; font-variant-numeric: tabular-nums; }
    .risk {
      background: rgba(232,176,75,.12); color: var(--discuss); border-radius: 10px;
      padding: 10px 12px; font-size: 14px; margin-bottom: 16px;
    }
    .mbtns { display: flex; flex-direction: column; gap: 9px; }
    /* WP130: height → min-height (48 ≥ 44) + touch-action */
    .mbtn {
      min-height: 48px; border-radius: 12px; border: none; font-size: 15px;
      font-weight: 600; cursor: pointer; touch-action: manipulation;
    }
    .mbtn.keep { background: var(--accent); color: #06263f; }
    /* WP139: возврат с экрана итога. Кнопка живёт В ЛЕНТЕ (не в модалке и не в
       липком футере — футера на этом экране нет), поэтому ей нужна своя ширина. */
    .backrow { display: flex; margin: 4px 0 8px; }
    .backrow .mbtn { width: 100%; }
    .mbtn.insist { background: transparent; color: var(--discuss); border: 1px solid var(--discuss); }
    /* WP304: выходы разговора в окне возражения — тем же ВИДОМ, что в панели
       ступени: один и тот же выход не должен читаться на двух экранах как два
       разных. Цвета взяты у панельных близнецов (.qexit-own / .qexit-pack /
       .qexit-oneoff); сами панельные классы сюда не годятся — .mbtn стоит в
       файле ниже и стёр бы им обводку своим `border: none`. */
    .mbtn.own { background: var(--ok); color: #04231a; }
    .mbtn.pack { background: transparent; color: var(--ok); border: 1px solid var(--ok); }
    .mbtn.oneoff {
      background: transparent; color: var(--discuss); border: 1px dashed var(--discuss);
    }
    /* ── WP88: мотивировка переключения сорсинга (заявка Виктору только с причиной) ── */
    /* WP127: 14px → 16px. Седьмое фокусируемое поле страницы — textarea причины
       в модалке «настаиваю». В шести правилах карточки его нет, потому что имя
       класса не содержит слова textarea (гейт ищет по селектору) — но айфон
       зумит по ФАКТУ шрифта, а не по имени класса. */
    .switch-reason {
      width: 100%; box-sizing: border-box; min-height: 54px; resize: vertical;
      border-radius: 10px; border: 1px solid var(--sep); background: var(--card2);
      color: var(--text); padding: 10px 12px; font-size: 16px; margin-bottom: 14px;
      font-family: inherit; line-height: 1.35;
    }
    .switch-reason.needs { border-color: var(--escalated); }
    /* ── WP135: закрытый список причин переключения сорсинга ── */
    /* Причина здесь не согласование, а поле журнала: один тап на редком действии.
       16px — защита от автозума iOS (WP127), 44px — палец (WP130). */
    .switch-reason-pick {
      width: 100%; box-sizing: border-box; min-height: 44px; margin-bottom: 8px;
      border-radius: 10px; border: 1px solid var(--sep); background: var(--card2);
      color: var(--text); padding: 8px 12px; font-size: 16px;
      font-family: inherit; touch-action: manipulation;
    }
    .switch-reason-pick.needs { border-color: var(--escalated); }
    /* Цены ОБЕИХ сторон в модалке: показ последствия перед переключением */
    .switch-prices {
      color: var(--discuss); font-size: 13px; font-weight: 600;
      margin: -4px 0 12px;
    }
    .mbtn.insist:disabled { opacity: .45; cursor: not-allowed; }
    /* ── WP10283: «взять здесь один раз» — ВТОРАЯ дорога модалки переключения ──
       Разделитель обязан быть ВИДЕН: две дороги у одного решения, и человек
       выбирает между ними (03.10 креветка уехала «насовсем» именно потому, что
       дороги была одна). Кнопка — видом близнец «разового выхода» панели ступени
       (.mbtn.oneoff WP304): разовое действие читается одним видом на двух
       экранах. */
    .once-here { border-top: 1px solid var(--sep); margin-top: 14px; padding-top: 14px; }
    .mbtn.oneoff:disabled { opacity: .45; cursor: not-allowed; }
    /* WP160: отказ от действия в окне писателя. Не заливка: единственная заливка
       окна — у действия, иначе повторяется 27.07 (жали ту кнопку, что ярче и ниже). */
    .mbtn.cancel { background: transparent; color: var(--hint); border: 1px solid var(--sep); }
    /* ── WP138: добавить и убрать позицию ── */
    /* «Добавить» стало ДЕЙСТВИЕМ (кладёт строку в корзину), поэтому у него свой
       вид — зелёный, как у согласия; заявка осталась второстепенным исходом. */
    .mbtn.go { background: var(--ok); color: #04231a; }
    .mbtn.go:disabled { opacity: .45; cursor: not-allowed; }
    .mbtn.stop { background: transparent; color: var(--escalated);
      border: 1px solid var(--escalated); }
    .mbtn.stop:disabled { opacity: .45; cursor: not-allowed; }
    /* Что будет ДО нажатия: последствие называем словами, а не после факта */
    .add-note, .drop-note {
      color: var(--hint); font-size: 13px; line-height: 1.4; margin: -4px 0 12px;
    }
    /* Пометка «своя строка» на карточке: расчёт её не предлагал */
    .facts-hand {
      color: var(--discuss); font-size: 13px; font-weight: 600;
      line-height: 1.35; margin-bottom: 6px;
    }
    .pos[data-by-hand] { border-left-color: var(--discuss); }
    /* WP154: согласие дали под ДРУГОЙ расчёт. Тот же вес, что у «✋ добавлено
       вручную»: это не украшение строки, а причина, по которой цифра на экране
       отличается от той, о которой договаривались. */
    .facts-stale {
      color: var(--discuss); font-size: 13px; font-weight: 600;
      line-height: 1.35; margin-bottom: 6px;
    }
    .pos[data-stale] { border-left-color: var(--discuss); }
    /* WP10002: строка отложенного переключения — постоянная строка на карточке
       позиции (новый поставщик, switch_from, первый приход, прежний поставщик).
       Слова приходят готовыми из ядра (cart_core.deferred_switch_line); тон —
       акцент (синий): это справка о будущем, а не предупреждение (жёлтый
       discuss у «✋ добавлено вручную» / «↺ протухшее согласие»). */
    .facts-switch {
      color: var(--accent); font-size: 13px; font-weight: 600;
      line-height: 1.35; margin-bottom: 6px;
    }
    /* Вход в снятие позиции у поставщика — ссылкой, не кнопкой: действие редкое
       и опасное, соседства с рядом количества ему хватает (WP130: 44px пальцу) */
    .droprow { margin-top: 10px; }
    /* ── WP1128: кубики двух имён — ЧЕТЫРЕ состояния, ЧЕТЫРЕ класса ─────────
       Цвета — решение владельца 02.09. Классы различимы в DOM, не только
       цветом (гейт читает имена). Свёрнуто по умолчанию: кубики — третий
       уровень карточки, не первый. Разметка ПЕЧАТАЕТ пришедшее (verdict и
       words из пейлоада, WP1126/ADR-030), ничего не считает. */
    .cubes { margin: 6px 0 2px; font-size: 13px; }
    .cubes summary {
      cursor: pointer; color: var(--hint); font-size: 12px;
      min-height: 32px; line-height: 32px; touch-action: manipulation;
    }
    .cubes-list { margin: 4px 0 6px; }
    .cube {
      display: block; padding: 3px 8px; margin: 2px 0; border-radius: 4px;
      border-left: 3px solid var(--hint); line-height: 1.35;
    }
    /* совпало — зелёный */
    .cube-same { border-left-color: var(--ok); color: var(--ok); }
    /* разошлось — янтарный (решать человеку) */
    .cube-diff { border-left-color: var(--discuss); color: var(--discuss); }
    /* только у одного — синий (асимметрия знания, НЕ расхождение) */
    .cube-only { border-left-color: var(--accent); color: var(--accent); }
    /* не назван — серый (граница нашего разбора) */
    .cube-unknown { border-left-color: var(--hint); color: var(--hint); }
    /* ── WP9707: сравнение кандидата — ТАБЛИЦА в три колонки ─────────────────
       Список фраз (WP1128) читался как девять расхождений: у 52 % кубиков по
       сети молчит одна сторона, а у 118 пар из 183 конфликтов ноль. Таблица
       признак · сейчас · кандидат даёт сравнить глазом и проверить по именам.
       Порядок строк — КАК ДАЛА витрина (WP1292): экран не сортирует.
       «не сказано» (cubes-none) — приглушённый: это отсутствие сведений,
       а не различие, и набирается иначе, чем значение. */
    .cubes-name {
      margin: 4px 0 2px; font-size: 13px; font-weight: 600;
      font-variant-numeric: tabular-nums;
    }
    .cubes-name-now { color: var(--hint); font-weight: 400; }
    .cubes-name-cand { color: var(--text); }
    .cubes-conclusion {
      margin: 2px 0 4px; font-size: 13px; color: var(--text);
      line-height: 1.4;
    }
    .cubes-table {
      width: 100%; border-collapse: collapse; margin: 2px 0 6px;
      font-size: 13px; line-height: 1.35;
    }
    .cubes-table th {
      text-align: left; color: var(--hint); font-weight: 600; font-size: 11px;
      text-transform: uppercase; letter-spacing: .5px;
      padding: 3px 6px 3px 0; border-bottom: 1px solid var(--sep);
    }
    .cubes-table td {
      padding: 4px 6px 4px 0; border-bottom: 1px solid var(--sep);
      vertical-align: top; word-break: break-word;
    }
    .cubes-table tr:last-child td { border-bottom: none; }
    .cubes-table .cubes-attr { color: var(--hint); min-width: 70px; }
    .cubes-table .cubes-now, .cubes-table .cubes-cand {
      font-variant-numeric: tabular-nums;
    }
    /* «не сказано» — приглушённый: отсутствие сведений, а не различие. */
    .cubes-none { color: var(--hint); font-style: italic; }
    /* Строки-расхождения — своим цветом (тот же язык, что у кубиков):
       diff — discuss (жёлтый), same — ok (зелёный), only — accent (синий).
       Цвет строки, а не значения: сравнение видно по строке целиком. */
    .cubes-row.cube-diff td { color: var(--discuss); }
    .cubes-row.cube-same td { color: var(--ok); }
    .cubes-row.cube-only td { color: var(--accent); }
    .cubes-row.cube-unknown td { color: var(--hint); }
    /* ── WP9810: раскрывашка «откуда эта цена» — шаги ядра ──────────────────
       Разметка печатает пришедшее (basis из ядра, WP9809), не считает.
       Свёрнута по умолчанию: третий уровень карточки, как кубики. Две
       стороны — две колонки; на узком экране одна. Значения — tabular-nums,
       как деньги в .sl-num: цифры шагов читаются в столбик. */
    .basis { margin: 6px 0 2px; font-size: 13px; }
    .basis summary {
      cursor: pointer; color: var(--hint); font-size: 12px;
      min-height: 32px; line-height: 32px; touch-action: manipulation;
    }
    .basis-sides {
      display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
      margin: 4px 0 6px;
    }
    @media (max-width: 480px) {
      .basis-sides { grid-template-columns: 1fr; }
    }
    .basis-side {
      padding: 6px 8px; border-radius: 6px; line-height: 1.4;
      background: var(--card2); border: 1px solid var(--sep);
    }
    .basis-side-head { font-weight: 600; font-size: 13px; }
    .basis-side-nom { color: var(--hint); font-size: 12px; margin-top: 1px; }
    .basis-side-src { color: var(--hint); font-size: 11px; margin-top: 1px; }
    .basis-step {
      display: flex; justify-content: space-between; gap: 8px;
      margin-top: 3px; font-size: 13px;
      font-variant-numeric: tabular-nums;
    }
    .basis-step-label { color: var(--hint); }
    .basis-step-value { font-weight: 600; white-space: nowrap; }
    /* итог стороны/выкладки — жирнее, линия сверху */
    .basis-out {
      border-top: 1px solid var(--sep); margin-top: 6px; padding-top: 4px;
      font-weight: 700;
    }
    .basis-calc { margin: 4px 0 6px; }
    .basis-calc-head {
      color: var(--hint); font-size: 11px; font-weight: 600;
      text-transform: uppercase; letter-spacing: .5px; margin-bottom: 2px;
    }
    /* расхождение выкладки с витриной — предупреждение, не сноска */
    .basis-check {
      color: var(--discuss); font-size: 13px; font-weight: 600;
      margin-top: 4px; line-height: 1.35;
    }
    .droplink {
      display: inline-block; min-height: 44px; line-height: 44px;
      color: var(--escalated); font-size: 13px; cursor: pointer;
      touch-action: manipulation;
    }
    /* ── WP1127: список кандидатов на замену — ВНУТРИ карточки ───────────────
       Решение владельца 02.09: показывать ВСЕХ, а не одного «лучшего». По
       умолчанию строка одна (свёрнутая, `swapLinkHtml`): карточка и так
       перегружена. Нажатие раскрывает список МЕЖДУ рядом количества и
       судьбами (drop / меню / дверь) — там, где его ставит макет. Каждая
       строка: поставщик · цена · ОДНО число · мелкая строка разложения ·
       основа (вердикт судьи или честное «по карточке»). Слова — из ядра
       (CART.swap_words), разметка только печатает (ADR-030). */
    .swaplist {
      margin: 8px 0 2px; padding: 8px 10px; border-radius: 10px;
      background: var(--card2); border: 1px solid var(--sep);
    }
    .swaplist[hidden] { display: none; }
    .swaplist-title {
      color: var(--hint); font-size: 12px; font-weight: 600;
      margin-bottom: 6px; line-height: 1.35;
    }
    .swaplist-row { padding: 7px 0; border-top: 1px solid var(--sep); }
    .swaplist-row:first-of-type { border-top: none; }
    .swaplist-row .sl-top {
      display: flex; justify-content: space-between; align-items: baseline;
      gap: 8px;
    }
    .swaplist-row .sl-name { font-weight: 600; font-size: 13px; }
    .swaplist-row .sl-num {
      font-weight: 700; font-size: 14px; white-space: nowrap;
      font-variant-numeric: tabular-nums;
    }
    .swaplist-row .sl-num.neg { color: var(--escalated); }
    .swaplist-row .sl-break {
      color: var(--hint); font-size: 12px; margin-top: 2px; line-height: 1.4;
      font-variant-numeric: tabular-nums;
    }
    /* WP1143: сомнение о карточке (aggregate) — предупреждение ПЕРЕД деньгами:
       у агрегата разницу цен финансирует подмена товара, а не поставщик, и
       читать её как выгоду нельзя (контракт §«Сомнение о карточке»). СВОЙ
       класс и СВОЙ вес — не подстрочник цвета подсказки (.sl-break), каким
       набраны три числа: предупреждение, набранное как сноска, предупреждением
       не работает. Цвет — --discuss (янтарный, цвет состояния «обсуждается»
       в палитре): сомнение, а не провал — --escalated (красный) в этой
       поверхности зарезервирован за отрицательным числом (.sl-num.neg).
       13px/700 — между деньгами (14/700) и сносками (12/400): выше сноска
       по весу, ниже денег по размеру. */
    .swaplist-row .sl-warn {
      color: var(--discuss); font-size: 13px; font-weight: 700;
      margin-bottom: 2px; line-height: 1.4;
    }
    .swaplist-row .sl-basis {
      display: inline-block; font-size: 11px; font-weight: 600; margin-top: 3px;
      padding: 1px 7px; border-radius: 20px;
      background: rgba(100,181,239,.14); color: var(--accent);
    }
    /* ── WP75: чат-виджет (второй транспорт к ядру бота) ── */
    .chatfab {
      position: fixed; right: 16px; bottom: 84px; z-index: 60;
      width: 56px; height: 56px; border-radius: 50%; border: none;
      background: var(--accent); color: #06263f; font-size: 26px; cursor: pointer;
      box-shadow: 0 4px 14px rgba(0,0,0,.4);
      touch-action: manipulation; /* WP130 */
    }
    /* На узком экране нижняя панель выше (кнопки встают в столбик), и пузырь
       чата накрывал правый край кнопки «Добавить из ассортимента»: тап в этой
       зоне открывал чат вместо поиска (обход 25.07). Правило идёт ПОСЛЕ базового
       — иначе одинаковая специфичность и побеждает более поздний bottom. */
    @media (max-width: 520px) { .chatfab { bottom: 168px; } }
    .chatfab:active { filter: brightness(.92); }
    .chatfab .cbadge {
      position: absolute; top: -3px; right: -3px; min-width: 20px; height: 20px;
      padding: 0 5px; border-radius: 10px; background: var(--escalated); color: #fff;
      font-size: 12px; font-weight: 700; line-height: 20px; text-align: center;
      display: none;
    }
    .chatpanel {
      position: fixed; inset: 0; z-index: 70; background: var(--bg);
      display: flex; flex-direction: column;
    }
    .chatpanel.hidden { display: none; }
    .chathead {
      display: flex; align-items: center; gap: 10px; padding: 14px 16px;
      border-bottom: 1px solid var(--sep); background: var(--card);
    }
    .chathead h2 { font-size: 16px; margin: 0; font-weight: 600; flex: 1; }
    /* WP130: 34×34 → 44×44 (крестик закрытия чата и панели ассортимента) */
    .chathead .closebtn {
      min-width: 44px; min-height: 44px; border: none; border-radius: 8px; cursor: pointer;
      background: var(--card2); color: var(--text); font-size: 18px;
      touch-action: manipulation;
    }
    .chatbody { flex: 1; overflow-y: auto; padding: 14px; }
    .cmsg { margin-bottom: 10px; max-width: 82%; }
    .cmsg.user { margin-left: auto; }
    .cmsg .bubble {
      display: inline-block; padding: 9px 12px; border-radius: 14px;
      font-size: 14px; line-height: 1.4; white-space: pre-wrap; word-break: break-word;
    }
    .cmsg.bot .bubble { background: var(--card); border-bottom-left-radius: 4px; }
    .cmsg.user .bubble { background: var(--accent); color: #06263f; border-bottom-right-radius: 4px; }
    .cacts { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 7px; }
    /* WP130: чипы быстрых действий чата — 44px по высоте (было ~31px) */
    .cact {
      display: inline-flex; align-items: center; min-height: 44px;
      padding: 7px 14px; border-radius: 22px; border: 1px solid var(--sep);
      background: var(--card2); color: var(--accent); font-size: 13px; cursor: pointer;
      touch-action: manipulation;
    }
    .cact:active { background: var(--sep); }
    .ctyping { color: var(--hint); font-size: 13px; padding: 4px 4px 10px; font-style: italic; }
    .ctyping.hidden { display: none; }
    /* WP130: строка ввода чата — нижняя кромка полноэкранной панели, значит ей
       и безопасная зона, и подъём над клавиатурой (тот же --kb). Полосу «Готово»
       чат НЕ получает: у буквенной клавиатуры своя клавиша возврата есть, беда
       только у цифровой панели — иначе две полосы налезли бы друг на друга. */
    .chatinput {
      display: flex; gap: 8px; padding: 10px 12px calc(18px + var(--sab));
      border-top: 1px solid var(--sep); background: var(--card);
      transform: translateY(calc(-1 * var(--kb)));
    }
    /* WP127: 15px → 16px — порог, ниже которого айфон зумит страницу на фокусе */
    /* WP130: 42 → 44px по высоте */
    .chatinput input {
      flex: 1; min-height: 44px; border-radius: 12px; border: 1px solid var(--sep);
      background: var(--card2); color: var(--text); padding: 0 14px; font-size: 16px;
    }
    .chatinput button {
      min-width: 46px; min-height: 44px; border: none; border-radius: 12px;
      background: var(--accent); color: #06263f; font-size: 20px; cursor: pointer;
      touch-action: manipulation; /* WP130 */
    }
    .chatinput button:disabled { opacity: .5; }
    /* ── WP79: тост «Заказ пересчитан» после фонового пересчёта ── */
    .toast {
      position: fixed; left: 50%; bottom: 92px; transform: translateX(-50%);
      z-index: 90; max-width: 90%; padding: 11px 16px; border-radius: 12px;
      background: var(--accent); color: #06263f; font-size: 14px; font-weight: 600;
      box-shadow: 0 4px 14px rgba(0,0,0,.4); opacity: 0; pointer-events: none;
      transition: opacity .2s ease; text-align: center;
    }
    .toast.show { opacity: 1; }
    /* ── WP85: панель добавления позиции из ассортимента ── */
    .assbtn {
      width: 100%; min-height: 44px; margin-top: 10px; border-radius: 12px;
      border: 1px dashed var(--sep); background: transparent; color: var(--accent);
      font-size: 15px; font-weight: 600; cursor: pointer;
      touch-action: manipulation; /* WP130 */
    }
    .assbtn:active { background: var(--card2); }
    .asspanel {
      position: fixed; inset: 0; z-index: 72; background: var(--bg);
      display: flex; flex-direction: column;
    }
    .asspanel.hidden { display: none; }
    .asssearch {
      display: flex; gap: 8px; padding: 12px 14px;
      border-bottom: 1px solid var(--sep); background: var(--card);
    }
    /* WP127: 15px → 16px — порог, ниже которого айфон зумит страницу на фокусе */
    /* WP130: 42 → 44px по высоте */
    .asssearch input {
      flex: 1; min-height: 44px; border-radius: 12px; border: 1px solid var(--sep);
      background: var(--card2); color: var(--text); padding: 0 14px; font-size: 16px;
    }
    /* WP130: список ассортимента упирается в нижнюю кромку экрана — безопасная зона */
    .assbody { flex: 1; overflow-y: auto; padding: 12px 14px calc(24px + var(--sab)); }
    .assrow {
      background: var(--card); border-radius: 12px; padding: 12px 14px;
      margin-bottom: 9px;
    }
    .assrow.foreign { opacity: .62; }
    /* WP445: «едим, но не покупаем» — приглушена как foreign (добавить нельзя),
       но акцент тревожный: точка расходует, а цепочки нет — это не чужая
       корзина, а кончающийся молча товар. */
    .assrow.no_chain { opacity: .62; border-left: 5px solid var(--escalated); }
    /* WP86: switchable — НЕ приглушена (добавить можно), но с акцентом «переключение» */
    .assrow.switchable { border-left: 5px solid var(--discuss); }
    .assrow .aname { font-weight: 600; font-size: 15px; }
    .assrow .anom { color: var(--hint); font-size: 13px; margin-top: 3px; }
    .assrow .aprice { color: var(--hint); font-size: 13px; margin-top: 3px; }
    /* WP291: фасовка — ПОКАЗ, а не выбор: число заказа называет расчёт */
    .assrow .apack { color: var(--hint); font-size: 13px; margin-top: 3px; }
    .assrow .aforeign {
      color: var(--discuss); font-size: 13px; margin-top: 7px; font-weight: 600;
    }
    .assrow .aswitch {
      color: var(--discuss); font-size: 13px; margin-top: 7px; font-weight: 600;
    }
    /* WP86: кнопка переключения сорсинга — контурная, отличима от обычного «Добавить» */
    .assrow .addbtn.sourcing-switch {
      background: transparent; color: var(--discuss);
      border: 1px solid var(--discuss);
    }
    .assrow .aadd { display: flex; align-items: center; gap: 8px; margin-top: 11px; }
    /* WP130: 34 → 44px по высоте */
    .assrow .addbtn {
      margin-left: auto; min-height: 44px; padding: 0 16px; border-radius: 8px;
      border: none; background: var(--accent); color: #06263f; font-size: 14px;
      font-weight: 600; cursor: pointer; touch-action: manipulation;
    }
    .assrow .addbtn:active { filter: brightness(.92); }
    .asshint { color: var(--hint); text-align: center; padding: 30px 16px; font-size: 14px; }
    /* WP85: строка-заявка добавленной позиции (в разборе, системная цифра 0) */
    .reqrow {
      background: var(--card2); border-left: 5px solid var(--discuss);
      border-radius: 10px; padding: 11px 14px; margin-bottom: 9px;
      color: var(--discuss); font-size: 14px; font-weight: 600;
    }
    /* WP135: след ПЕРЕКЛЮЧЁННОЙ позиции — не заявка (вердикта нет): сорсинг уже
       переключён, в корзину позиция придёт с пересчётом. Цвет «сделано», не «ждёт». */
    .reqrow.switched { border-left-color: var(--ok); color: var(--ok); }
    /* ── WP100: экран-лобби корзин ── */
    .lobby-hidden { display: none !important; }
    .lobby-header { padding: 6px 2px 10px; }
    .lobby-header h1 { font-size: 18px; margin: 0 0 2px; font-weight: 600; }
    .lobby-header .sub { color: var(--hint); font-size: 13px; }
    .lobby-summary {
      margin: 4px 0 14px; font-size: 13px; color: var(--accent);
      font-variant-numeric: tabular-nums;
    }
    /* WP9840: полоска точек над списком. Кнопка на точку — имя и два числа
       (всего · ждут); выбранная подсвечена. Точка одна — полоски нет вовсе. */
    .venue-strip {
      display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 12px;
    }
    .venue-strip .venue-btn {
      min-height: 40px; padding: 4px 12px; border-radius: 10px;
      border: 1px solid var(--sep); background: var(--card2);
      color: var(--text); font-size: 13px; cursor: pointer;
      touch-action: manipulation; font-variant-numeric: tabular-nums;
      -webkit-tap-highlight-color: transparent;
    }
    .venue-strip .venue-btn.venue-sel {
      background: rgba(100,181,239,.16); border-color: var(--accent);
      color: var(--accent); font-weight: 600;
    }
    .basket {
      background: var(--card); border-radius: 12px; padding: 12px 14px 12px 18px;
      margin-bottom: 9px; position: relative; cursor: pointer; overflow: hidden;
      min-height: 44px; touch-action: manipulation; /* WP130 */
    }
    .basket::before {
      content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
      background: var(--ok);
    }
    .basket.status-discussing::before { background: var(--discuss); }
    .basket.status-sent::before { background: var(--ok); }
    .basket.status-reapprove::before { background: var(--escalated); }
    .basket .brow1 {
      display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
    }
    .basket .bsup { font-weight: 600; font-size: 15px; }
    .basket .bsum { font-weight: 600; white-space: nowrap; }
    .basket .bmeta { color: var(--hint); font-size: 13px; margin-top: 3px; }
    .basket .bdiffs { margin-top: 6px; font-size: 12px; color: var(--discuss); }
    .basket .bdiffs div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .basket .bbadge-reapprove {
      display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 20px;
      margin-top: 6px; font-weight: 600;
      background: rgba(232,176,75,.16); color: var(--discuss);
    }
    .orphans-strip {
      margin: 12px 0; padding: 10px 14px; border-radius: 10px;
      background: var(--card2); color: var(--hint); font-size: 13px;
    }
    /* WP189: кучка «нужен поставщик». Громче приютной полосы намеренно: там
       позиция перелетела к другому поставщику (её кто-то везёт), здесь — везти
       НЕКОМУ, и пока человек не выберет, её не будет ни в одном заказе. */
    .needs-supplier-strip {
      margin: 12px 0; padding: 10px 14px; border-radius: 10px;
      background: rgba(229,115,115,.10); border: 1px solid rgba(229,115,115,.42);
      color: var(--text); font-size: 13px; line-height: 1.5;
    }
    .needs-supplier-strip .nhead {
      font-weight: 600; color: var(--escalated); margin-bottom: 4px;
    }
    .needs-supplier-strip .nsince { color: var(--hint); }
    .needs-supplier-strip .nfoot {
      margin-top: 6px; color: var(--hint); font-size: 12px;
    }
    /* WP189: выбор замены прямо в окне снятия — по кнопке на поставщика */
    .mbtn.stop-alt {
      display: block; width: 100%; margin: 0 0 8px; text-align: left;
    }
    .lobby-footer {
      margin: 10px 0; padding: 8px 0; color: var(--hint); font-size: 12px;
      border-top: 1px solid var(--sep);
    }
    /* WP130: возврат в лобби был ~22px высотой — min-height 44px */
    .back-btn {
      display: inline-flex; align-items: center; min-height: 44px;
      margin-bottom: 8px; padding: 4px 8px;
      border: none; background: transparent; color: var(--accent);
      font-size: 14px; cursor: pointer; touch-action: manipulation;
    }
    .back-btn:active { background: var(--card2); border-radius: 6px; }
    /* ── WP130: своя полоса «Готово» над клавиатурой ────────────────────────────
       У цифровой панели iOS клавиши возврата НЕТ — это решение Apple, и в вебе
       её взять неоткуда (в нативе для этого есть inputAccessoryView, в вебе
       такого API не существует). Человек набирает дробные килограммы и остаётся
       с клавиатурой на пол-экрана — поверх ровно той строки последствия, ради
       которой он в поле и полез. Полоса встаёт НАД клавиатурой (сдвиг на --kb,
       который считает обработчик по visualViewport) и закрывает её по тапу.
       Показывается только у полей с цифровой панелью: у буквенной клавиатуры
       своя клавиша возврата есть, и вторая полоса там была бы шумом.
       z-index выше модалки (95) — поля есть и внутри модалок. */
    .kbdone {
      position: fixed; left: 0; right: 0; bottom: 0; z-index: 100;
      display: none; align-items: center; justify-content: space-between;
      gap: 10px; padding: 6px 14px;
      background: var(--card); border-top: 1px solid var(--sep);
      box-shadow: 0 -3px 12px rgba(0,0,0,.35);
      transform: translateY(calc(-1 * var(--kb)));
    }
    .kbdone.kbdone-show { display: flex; }
    .kbdone-hint { color: var(--hint); font-size: 13px; }
    .kbdone-btn {
      min-height: 44px; min-width: 96px; padding: 0 18px;
      border: none; border-radius: 10px;
      background: var(--accent); color: #06263f; font-size: 15px; font-weight: 700;
      cursor: pointer; touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
    }
    .kbdone-btn:active { filter: brightness(.92); }
  
    /* ── НАВИГАЦИЯ ПО КОРЗИНЕ: СПИСОК СТРОК (вариант «А», решение владельца 23.09) ──
       Встреча с Москвой 23.09: 27+ позиций, высокие карточки, «вверх-вниз скроллить
       очень долго». Строка на позицию, нажатие раскрывает ПОД ней прежнюю карточку.
       Узкий экран: список и карточки — одна колонка, место карточки задаёт `order`
       (правило window.__nav.openCss), разметка карточек не двигается. Широкий
       (≥1000px): список колонкой слева со своей прокруткой, открытая карточка справа. */
    .navbar { display: flex; flex-direction: column; gap: 8px; margin: 0 0 12px; }
    .navmode { display: inline-flex; align-self: flex-start; border: 1px solid var(--sep);
      border-radius: 10px; overflow: hidden; }
    .navmode-btn { background: transparent; color: var(--hint); border: 0; font: inherit;
      font-size: 13px; padding: 6px 11px; cursor: pointer; }
    .navmode-btn.on { background: var(--card); color: var(--text); font-weight: 600; }
    .navchips { display: flex; gap: 6px; flex-wrap: wrap; }
    .navchip { flex: none; border: 1px solid var(--sep); border-radius: 999px; background: var(--card2);
      color: var(--text); font: inherit; font-size: 13px; padding: 5px 11px; cursor: pointer;
      font-variant-numeric: tabular-nums; }
    .navchip.on { background: var(--accent); border-color: var(--accent); color: #06263f; font-weight: 600; }
    .navwrap { display: flex; flex-direction: column; min-width: 0; }
    .navlist, .navcards { display: contents; }
    .navsearch { order: -2; width: 100%; margin: 0 0 8px; padding: 8px 11px; border-radius: 10px;
      border: 1px solid var(--sep); background: var(--card2); color: var(--text); font: inherit;
      font-size: 14px; }
    .navnone { order: -1; color: var(--hint); font-size: 13px; padding: 8px 2px; }
    /* WP10041: дверь в надписи пустого списка — «Показать все» / «Сбросить поиск».
       Ссылка-действие в строке текста: приём .stock-fix (паддинг растит мишень,
       отрицательный отступ гасит рост строки), акцентный цвет — это выход, а не
       тихая справка. */
    .navnone .navnonebtn {
      color: var(--accent); font-size: 13px; font-weight: 600; cursor: pointer;
      background: none; border: none; padding: 11px 4px; margin: -11px -4px;
      text-decoration: underline dotted currentColor; text-underline-offset: 3px;
      -webkit-tap-highlight-color: transparent; touch-action: manipulation;
    }
    .navnone .navnonebtn:active { color: var(--text); }
    .navempty { display: none; }
    .navcards > .pos { display: none; }
    /* Строка — flex с переносом, а не сетка «1fr auto»: правая часть nowrap и в
       колонке 280 px съедала название до одной буквы в строке (26.09). Название
       держит минимум, количество не влезло — уходит под него, прижатое вправо. */
    .navrow { position: relative; display: flex; flex-wrap: wrap; column-gap: 10px; row-gap: 2px;
      align-items: center; background: var(--card); border-radius: 10px; padding: 9px 12px 9px 16px;
      margin-bottom: 5px; cursor: pointer; overflow: hidden; -webkit-tap-highlight-color: transparent; }
    .navrow .nr-bar { position: absolute; left: 0; top: 0; bottom: 0; width: 5px; }
    .navrow.ok .nr-bar { background: var(--ok); }
    .navrow.discuss .nr-bar { background: var(--discuss); }
    .navrow.escalated .nr-bar { background: var(--escalated); }
    .navrow .nr-name { flex: 1 1 9em; min-width: 9em; font-size: 14px; line-height: 1.25;
      overflow-wrap: break-word; }
    .navrow .nr-q { flex: 0 1 auto; max-width: 100%; margin-left: auto; text-align: right; font-size: 13px;
      font-variant-numeric: tabular-nums; }
    .navrow .nr-calc { color: var(--hint); }
    .navrow .nr-calc, .navrow .nr-yours, .navrow .nr-sum { white-space: nowrap; }
    .navrow .nr-sum { display: block; color: var(--hint); font-size: 12px; }
    .navrow.nav-open { outline: 2px solid var(--accent); outline-offset: -2px; margin-bottom: 4px; }
    .navrow.nav-hide { display: none !important; }
    .navrow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
    @media (min-width: 1000px) {
      #screen-cart:has(.navwrap) { max-width: 1180px; }
      .navwrap { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 16px;
        align-items: start; }
      .navlist { display: block; position: sticky; top: 8px; overflow: auto;
        max-height: calc(100vh - var(--barh) - 16px); padding-right: 4px; }
      .navcards { display: block; min-width: 0; }
      .navempty { display: block; color: var(--hint); font-size: 14px; padding: 24px 4px; }
      .navwrap.nav-has-open .navempty { display: none; }
      .navrow { padding: 7px 10px 7px 14px; }
      .navrow .nr-name { font-size: 13px; }
    }
