/* ════════════════════════════════════════════════════════════════════════════
   SuiteScore — Civic Editorial Design System
   ops.css v2 · Staff portal (surface 4) — направление B «Консоль»
   (виза Виктора 2026-07-22; канон вёрстки = docs/mockups/ops-redesign-v2/).
   Регистр: мониторинговая консоль (Datadog/Grafana-класс). Светлая тема =
   дефолт, тёмная = html[data-ops-theme="dark"] (стамп до отрисовки,
   localStorage 'ops-theme'). Шрифты Inter + JetBrains Mono — САМОХОСТ
   (fonts/web/, staff-поверхность: приватность/зависимость, NOTES v2 §этап 2);
   пути @font-face несут ?v=<sha256[:10] файла> — равенство хэшей держит
   детерминированный гейт check_ops_surface.py (иначе precache-ключ SW
   разошёлся бы с runtime-запросом, 084 п.2а).
   САМОДОСТАТОЧНЫЙ файл (волна 5): токены --ops-* обеих тем живут ЗДЕСЬ —
   /ops/ НЕ грузит colors_and_type.css (публичная элементная типографика и
   Google-CDN @import staff-поверхности не нужны; canon-поправка №2 — словарь
   на своих токенах). Стек любой ops-страницы = ops.css (+ page head_extra).
   Каскад = @layer ops-reset < ops-vocab (#44 §3, введён волной 5 после ухода
   легаси): ресет проигрывает словарю СЛОЕМ, а не порядком/спецификой; prose-
   набор :where() внутри vocab проигрывает любому классу словаря, но бьёт ресет
   (до волны 5 ресет глушил отступы markdown-прозы — латентный баг спецификой
   (0,1,0) против (0,0,1), закрыт слоями).
   Нормы: RESEARCH-2026.md (строки 28px · 4px-сетка · hairline+тон, не тени ·
   цвет только за смысл · tabular-nums везде). Кегль mono-caps-меток: пол
   9.5px (виза v2 п.2).

   СЛОВАРЬ (закрытый; новый класс = сначала строка здесь):
   shell    ops-shell(.drawer-open) ops-side(__brand .wm .tag __grp __nav .n(.unread);
            a.active) ops-scrim ops-main ops-top(__search .k .spacer)
            ops-burger ops-crumb wrap ops-badge(--city --demo --env) ops-who
            ops-theme ops-out ops-back(span) ops-h-row(.when .scenario)
            ops-lock(__btn __icon __text __left __until __pop __form __label __code
            __go __err __hint; [data-ops-lock=open|closed])
   signal   ops-sig(__cell __lab __val .ok .warn .bad .neu)
            ops-dot(--ok --warn --bad --neu)
   kpi      ops-tiles ops-tile(--warn --bad __k __v __s)
   panels   ops-cols(--entity) ops-col ops-panel(__head __title __note __pad)
            ops-rule-top
   table    ops-table(--wrap --fixed --cards; th td .r .aux .when __id __age __act __sub(--warn)
            __lede __fold __q .what) — __lede = первая строка ячейки, главная фраза строки;
            __fold = раскрывашка ВНУТРИ ячейки (машинная подробность, нужная редко);
            --cards = строка читается КАРТОЧКОЙ на узком контейнере, __q — подпись
            вопроса внутри ячейки (видна только в карточном виде)
            ops-row--warn ops-row--bad ops-scroll (несёт скролл-тени краёв —
            обрезанная колонка обязана показывать, что её можно доскроллить)
            ops-cards — ОБЁРТКА, объявляющая контейнер для --cards (запрос
            адресуется предку, поэтому носитель порога — обёртка, не таблица)
   tags     ops-tag(--ok --warn --bad --neu)
   facts    ops-facts(__k __v(--wrap) .ok .warn .bad)
   feed     ops-feed(.ts .ev)
   funnel   ops-funnel(__step .fk .fv .fc .ar) ops-meas
   viz      ops-spark(.bad) ops-uptime(i .w .b .n)
   worklist ops-filters(.on) ops-empty(--clear --none --unavail)
   form     ops-fld(label input textarea .hint) ops-check ops-actions
            ops-btn(--outline) ops-linkbtn ops-steptag
            ops-rowset(>legend) — одна запись повторяющейся группы (095/03 § 3.3)
   confirm  ops-diff(.old .arr .new) ops-warncall ops-confirm(__ask)
   doc      ops-doc(__rail __toc(.t a.on) __kicker __prose __sec
            __callout(.boundary .ct) .lead)
   gate     ops-gatepage(__panel .wm .tag .note .why)
   misc     ops-qa ops-foot num
   ── дополнения волны 1 (миграция 12 app-shell страниц; каждый класс = строка
      здесь ДО использования — словарь остаётся закрытым) ──
   note     ops-note(--ok --warn --bad) — фактовая строка в теле панели
            (муфлеры, легенды, провенанс, flash)
   kv       ops-kv(__k __v) — verbatim key-value пары C3-карточек
   seg      ops-seg(a.on) — segmented-переключатель scope/view
   pager    ops-pager
   dim      ops-dim(--wrap) — мелкий приглушённый мон-текст (state/mask/seen);
            --wrap = pre-wrap (deficiency verbatim)
   flag     ops-flag — юридически значимая warn-пометка ≥10.5px
   table+   td.neg (bad-число, отрицательная маржа) · tr.ytot (итоговая строка)
   kpi+     ops-tile__v--text (текстовое значение тайла) · ops-tile__s--wrap
            (юр-подпись без обрезки)
   form+    ops-form (grid инлайн-полей в панели; ops-fld--wide = вся ширина)
            ops-fld select · ops-fld--code (крупный ввод TOTP)
            ops-btn--danger (деструктив: emergency revoke)
   mix      ops-mix — доли-бар work mix (сегменты data-driven inline width)
   sha      ops-sha — fingerprint конфирм-церемонии
   cal      ops-cal(__day .dh __slots __empty)
            ops-slot(--open --booked --done .t .ctx .st .ack) — календарь слотов
   mx       ops-mx(.blk .bn .frozen .lock .legal-chan .mintier(.off) .depth)
            ops-cellbtn(.off) ops-cell-ro(.off) — конфиг-матрица
   qr       ops-qr(img .manual) — 2FA enrollment
   ── дополнения волны 2 (worklist/config с легаси; каждый класс = строка здесь
      ДО использования — словарь остаётся закрытым) ──
   event    ops-event(--review .e-head .e-kind .e-when .e-title .e-sum .e-meta)
            — контент-лента regulatory watch; --review = warn-кромка pending
   pre      ops-pre — verbatim diff/machine-текст (pre-wrap, мон)
   rowform  ops-rowform — инлайн-форма в ячейке таблицы (release hold,
            tier-тумблер): flex, компактные поля
   kv+      ops-kv__v.warn — warn-акцент значения kv-пары (нечитаемый email
            rtbf-карточки); по образцу ops-facts__v.ok/.warn/.bad
   btn+     .ops-btn:disabled — видимая недоступная кнопка (честная подсказка
            рядом = ops-dim); [hidden] побеждает display компонентов
   charts   серверные SVG-классы ops-ysc(-bar -val -yr -src) ops-ifp(-chart
            -bar -label -val) ops-mkt(-chart -bar -label -disc -val -foot .fn)
            ops-coh(-meter -track -fill) svg.ops-tl — переименованы волной 5
            вместе с serving-кодом (были ysc-/ifp-/mkt-/coh-/tl); + ops-tl-head
   ── дополнения волны 3 (reading C4 на ops_base_doc; каждый класс = строка здесь
      ДО использования — словарь остаётся закрытым; prose-набор и этот словарь
      дополняет ТОЛЬКО оркестратор, писателям ops.css закрыт) ──
   doc+     ops-doc__prose элементный набор (h2 = близнец __sec, h3, h4, ul/ol/li,
            blockquote, hr, strong, a-underline, code, pre) — markdown-рендер
            staff-статей (kb_portal, raw HTML выключен)
   doc+     ops-doc__find — широкая поисковая строка reading-входов (kb, handbook)
   doc+     ops-doc__tech(summary) — «technical details» disclosure глав handbook
   doc+     ops-doc__tablewrap ops-doc__table(td.cm td.cl) — derived-таблицы
            ref-страниц; широкая матрица скроллится в контейнере, не рвёт колонку
   index    ops-index(__row(--warn --on) __head __t __meta __sum) — hairline-указатель
            (дерево тем kb, карточки/группы handbook, номера-указатель, модули
            learn, очередь повторения); --warn = warn-кромка (locked/needs_revision)
   quiz     ops-quiz(__kind __prompt label input .sds(.phrase) __fb(--ok --bad .st))
            — practice/exam формы learn (radio/checkbox/numeric/say-dont-say)
   fcard    ops-fcard(__head(.n .key) __val(.closed) __claim __when __src __quotes
            __hist(.date .name .desc)) — карточка факта kb-статьи (значение из
            fact store, badge двух дат F.4, history-окна); амбер = ops-warncall
   rowform+ input[type=date] — as-of time-travel селектор kb-статьи
   fact     ops-fact(--ok --warn) ops-factref — метки {{fact:…}}-токенов в
            markdown-прозе (эмитит kb_portal-рендерер; волна 5 переименовала
            бывш. fact-inline/fact-ref; md-tablewrap/md-table слиты в
            ops-doc__tablewrap/ops-doc__table — один словарный класс)
   ── дополнения волны K1 knowledge-v2 (kb; канон = docs/mockups/ops-knowledge-v2/,
      виза Виктора 2026-07-23; каждый класс = строка здесь ДО использования —
      словарь остаётся закрытым, дополняет ТОЛЬКО оркестратор) ──
   suggest  ops-suggest(__row(.on) __grp .t .k) — дропдаун живого поиска под
            строкой (htmx GET-фрагмент, keyword-FTS only; деградация = form
            submit); __grp = строка-заголовок группы (B5 scheduled — отдельно)
   copy     ops-copy(.ok) — копи-кнопка вербатим-текста (kb say-фраза; далее
            handbook формулировка, runbook команда); .ok = состояние Copied
            ПОСЛЕ резолва clipboard-промиса, не оптимистично
   say      ops-say(--dont __lab __txt) — вербатим say/don't-say строка; текст
            только из fact store / handbook content, рендер без пересказа;
            усечению не подлежит: целиком или только заголовок
   ── дополнения волны K2 knowledge-v2 (handbook; канон = docs/mockups/
      ops-knowledge-v2/handbook-*.html, виза Виктора 2026-07-23; словарь
      дополняет ТОЛЬКО оркестратор) ──
   verd     ops-verd(a(.on)) — ряд 5 вердикт-кнопок ДОСЛОВНО (Likely / Possible /
            Risky / Unlikely / Needs Review — юр-контракт #22); нейтральная
            окраска: это фильтр/ветвление, НЕ статус; .on = активная
   split    ops-split(--rail __left __right) — split-view сценария handbook;
            --rail = рельса-указатель (каталог писем): узкий список + один предмет
            «что видит клиент | что говорить»; ≤900px say-панель ПЕРВОЙ
   preview  ops-preview(__cap __frame(--letter)) — окошко живого прод-превью фрагмента
            отчёта: iframe на /ops/handbook/preview/… (sandbox_render-цепочка);
            подложка = токен бумаги отчёта, светлая в обеих темах
   sr       ops-sr — screen-reader-only утилита (aria-live регион копи-острова;
            хвост K1: замена inline-clip рецепта)
   ── дополнения волны K3 knowledge-v2 (runbook; канон = docs/mockups/
      ops-knowledge-v2/runbook-*.html, виза Виктора 2026-07-23; словарь
      дополняет ТОЛЬКО оркестратор) ──
   symptom  ops-symptom(a .t .s) — крупные кнопки «What happened?» входа
            runbook; touch-высота ≥52px (стресс/ночь/телефон)
   inc      ops-inc(.ref .t) — шапка инцидента (реф · sev · pii · возраст)
   sev      ops-sev(--1 --2 --3) — severity-бейдж, 3 уровня не 5; текст =
            имя уровня Critical/Urgent/Routine (Lex Т-4: без юр-квалификации)
   pii      ops-pii(--confirmed) — бейдж «Personal data involved — legal track
            active» (Lex Т-3): амбер suspected, красный confirmed; виден на
            шапке и в таймлайне НЕЗАВИСИМО от severity
   step     ops-step(__meta(.n .next) __q) — экран одного шага: «Step X of Y»,
            next-превью, вопрос-развилка; действия = ops-actions + ops-btn--big
   cmd      ops-cmd — обвязка «крупная команда + Copy» (reuse ops-pre + ops-copy)
   btn+     ops-btn--big — 44px touch-таргет (Yes/No шага, стресс-режим)
   print    @media print — печатный fallback «всё лежит» (#44): shell-хром
            (сайдбар/топбар/крошка/кнопки форм) скрыт, контент во всю ширину
   choice   ops-choice(input .t .s) — крупная radio-карточка выбора (severity /
            PII-вопрос severity-экрана): label-обёртка, touch ≥52px (стресс),
            титул + дим-дескриптор; образец плотности = ops-symptom
   ── дополнения волны K4 knowledge-v2 (learn; канон = docs/mockups/
      ops-knowledge-v2/learn-*.html, виза Виктора 2026-07-23; словарь
      дополняет ТОЛЬКО оркестратор) ──
   review   ops-review(.n __bar i) — шапка due-ритуала: честный счётчик
            «N of M left» + тонкий прогресс; НИКАКИХ streak/таймеров (Tier 1);
            и тонкая полоска ориентации урока (module i of n · path ▸)
   path     ops-path(__step(--done --now --next) .m .t .k) — дорога онбординга
            «ты здесь / что дальше»; --now = location-state (не вердикт)
   focus    ops-focus — центрированная одноколоночная фокус-колонка (карточка
            ритуала learn; НЕ ops-doc: фокус-экран без TOC-рельса)
   ── дополнения волны K5 knowledge-v2 (единый поиск консоли; виза 6
      брейншторма 2026-07-23: ОДИН вход заказы+знания, строгие группы;
      словарь дополняет ТОЛЬКО оркестратор) ──
   topsuggest ops-top__suggest — абсолютный контейнер дропдауна единого поиска
            под топбаром (внутри — обычный ops-suggest; фрагмент
            POST /ops/search/suggest; пустой контейнер невидим)
   ── дополнения кусков 6 «Generate» + 7 «Map» (стенд прогонов и карта участков;
      виза Виктора 2026-07-26; словарь дополняет ТОЛЬКО оркестратор) ──
   run      ops-run(__stage(--done --now --fail) .t .s) — чеклист РЕАЛЬНЫХ стадий
            живого прогона (job_store.StageTracker); прогресс-полосы нет: доля
            выполненного неизвестна, нарисованная полоса врала бы
   map      ops-map__wrap + ops-map(__canvas __side __bar) — карта участков + боковая
            панель; @container объявлен на ОБЁРТКЕ (запрос адресуется предку, поэтому
            раскладка не может спросить саму себя) + запасное правило по ширине окна
   legend   ops-legend(__row .sw) — легенда слоя вердиктов: пять слов ДОСЛОВНО
            (#22) + отдельная строка «not computed» контуром без заливки
   fold     ops-fold(summary .n) — раскрывашка секции панели участка; содержимое
            приходит ОДНИМ ответом, свёрнутость = состояние показа, не запрос;
            маркер выключен, шеврон в summary::after (идиома аккордеона панели)
   shot     ops-shot(img __cap) — спутник / вид с улицы панели; показывается
            ТОЛЬКО по явному нажатию (платный вызов провайдера), подпись несёт
            и атрибуцию, и это предупреждение
   токены   --ops-map-likely/-possible/-risky/-unlikely/-review/-none — заливки слоя
            вердиктов; ОДНИ в обеих темах, пока подложка карты одна (тёмного стиля
            карты у нас нет). --ops-map-line — контур квадратика легенды, он живёт на
            панели консоли и тему панели наследует. MapLibre читает всё это из JS через
            getComputedStyle (сырых цветов в скриптах не бывает, #44 §2)
   ── дополнения волны 4 (периметр C7 + PWA site-visit C6; канон =
      ops-perimeter/ops-form-tool мокапы; словарь дополняет ТОЛЬКО
      оркестратор) ──
   gate+    ops-gatepage__panel--wide (широкая панель break-glass, текст от
            начала строки) · .err (mono-строка ошибки в панели) · __code (pre-wrap
            mono-блок challenge/команд) · ol в панели (нумерованные шаги) ·
            h1 в панели (заголовок verify/break-glass — экраны без
            мокап-прецедента, login/403 живут без h1);
            формы внутри панели = ops-fld/ops-actions/ops-btn без дубля
   btn+     ops-btn--block — кнопка на всю ширину (канон мокапа
            ops-perimeter: .gate-signin{width:100%})
   acc      details.ops-panel(+summary) — панель-аккордеон формы C6: summary
            без маркера, шеврон в __title, закрытая панель без нижней черты
   check+   label внутри .ops-check (radio-группа Egress-канона) — сам
            полноценная touch-цель ≥44
   touch    ops-touch — планшетная поверхность замерщика: инпуты 16px
            (iOS no-zoom) и цели ≥44px
   unit     ops-unitrow(.unit) — ряд «ввод + единица измерения»
   syncbar  ops-syncbar(--attention) — sticky сводка офлайн-очереди на
            site-visit поверхностях (id sv-queue-banner = хук JS); кромка =
            состоянию (решение Виктора 07-24, 080 п.2): neutral, --attention
            при несинхроне · ops-offline-note — dashed офлайн-плашка
            (id sv-offline-notice)
   offq     ops-offq(__kicker __who __lede __status __row(--draft) __empty
            __act(--discard) __sync __out __foot) — офлайн-мини-страница
            очереди (без shell/nav — офлайн-надёжность; строки рендерит JS
            из IndexedDB, #40)
   ── дополнения волны 5 (cleanup: decoupling + хвосты 080/081/082/084;
      словарь дополняет ТОЛЬКО оркестратор) ──
   num      ops-num — tabular/lining цифры (бывш. публичная утилита .num;
            консоль децуплирована — утилита своя)
   mark     ops-mark(--on --off) — текст-глиф ✓/— матрицы тиров вместо emoji
            ✅/❌ (080 п.4, NIT Prod): цвет за смысл (on = ok-x, off = mute),
            глиф в шаблоне (не ::before) — доступное имя у кнопки остаётся
   diff+    ops-diff--state — модификатор конфирма для STATE-слов (on/off,
            visible/hidden): .new чернилами --ops-ink; зелёный .new остаётся
            только числовым значениям (цены) — решение Prod, 081 п.2
   doc+     ops-doc__tablewrap скролл-тени краёв (082 п.2б): обрезанная колонка
            на 390 получает видимую подсказку скролла (background-attachment
            local, цвета color-mix токенов)
   doc+     markdown-заголовки прозы несут id-якоря (082 п.2а, рендерер
            kb_portal) — scroll-margin в prose-наборе, рельс «On this page»
            строит шаблон из заголовков; ops-doc__toc a.l3 = ступень h3
   gate+    .ops-gatepage__panel code — инлайн-code периметра на ops-токенах
            (пилюля challenge.txt; бывшая протечка публичного листа)
   ── дополнения куска 8 (доводка стенда прогонов и карты; план утверждён Виктором
      2026-07-26; словарь дополняет ТОЛЬКО оркестратор, писателям ops.css закрыт) ──
   fill     ops-shell--fill + ops-fill — страница-инструмент во весь экран: высота
            приходит от вьюпорта ЦЕПОЧКОЙ (shell → main → wrap → ops-fill), без
            calc с высотой шапки. Магическое число протухает от любой правки хрома
            и молча оставляет либо серое поле, либо вторую полосу прокрутки
   map+     ops-map__foot — легенда СТРОКОЙ под холстом (была правой колонкой).
            Правая колонка отдана панели участка целиком и несёт СВОЙ скролл:
            иначе рост панели после клика тянет высоту грид-строки, а MapLibre
            авто-ресайзится по контейнеру — отсюда «карта дёргается при клике»
   sig+     ops-sig--panel — полоса сигналов В ПАНЕЛИ участка (320px): снимает
            отброс последней ячейки вправо, из-за которого при переносе подпись
            «наследие» отрывалась от своего ряда
   gen+     ops-gen__bar + ops-gen__result — стенд прогонов перевёрнут: ввод
            компактной полосой сверху, лист отчёта занимает остаток экрана.
            Поля в полосе не растягиваются (max-inline-size), иначе шесть коротких
            полей размазываются на 1280 и полоса перестаёт быть полосой
   reset    body:has(>.ops-shell|gatepage|offq) margin/фон · kill-switch
            reduced-motion (зеркало motion.css — сам motion.css ушёл)
   ── дополнения куска 9 (боковое меню стенда прогонов; указание Виктора 2026-07-27;
      словарь дополняет ТОЛЬКО оркестратор, писателям ops.css закрыт) ──
   gen9+    правки по гейтам куска 9: полоса — СОБСТВЕННЫЙ контейнер (@container
            420px), потому что вся узкая форма консоли стояла на ОКОННЫХ порогах и
            внутри полосы в 300px на мониторе 1440 не срабатывала ни разу · строка
            грида задана явно (высота = свойство раскладки, не побочный эффект чужого
            overflow) · свёрнутая ширина вынесена во второй токен · язычок попал в
            сенсорный блок 44px · печать разворачивает свёрнутое меню ·
            ops-map__foot>.ops-note--warn — тревожная строка своим рядом
   gen9     ops-gen + ops-gen__rail(__body __railtoggle .t) — меню ВЕРТИКАЛЬНОЙ
            полосой слева, лист отчёта во всю высоту справа. Свёрнутость живёт
            атрибутом html[data-ops-rail="collapsed"] и переживает перезагрузку
            (стамп в <head> до отрисовки — как тема портала). Разворот решения
            куска 8 «полоса сверху»: довод «колонка отрезает у листа 300px
            НАВСЕГДА» снимается сворачиванием, а не вкусом. Токен ширины
            --ops-gen-rail. Полоса несёт СВОЙ скролл: без него раскрытый журнал
            прогонов растянул бы строку грида и снова отнял бы высоту у листа
   ── дополнения витрины ресурсов /ops/infra (серверные SVG задачи 8; словарь
      дополняет ТОЛЬКО оркестратор, писателям ops.css закрыт) ──
   lane     ops-lane(__grid __wide) — РАСКЛАДКА витрины ресурсов: сетка дорожек
            фиксированной ширины (--ops-lane), панель занимает целое число
            дорожек. Карточка сервиса — одну, панель с картинкой разворота
            (__wide) — две. Правка 2026-07-31: до неё страница была столбцом
            панелей во всю ширину окна и на 2560 не читалась вовсе — подпись
            уезжала от значения, картинка висела по центру пустой полосы, а
            высота доходила до шести тысяч пикселей. Запрос ширины объявлен на
            ОБЁРТКЕ ops-lane, сама сетка — ops-lane__grid внутри: контейнер не
            может спросить собственную ширину (та же причина, что у ops-map);
            запасное правило — по ширине окна для браузера без @container
   lane     ops-lane(__floor(__t) __pack) — ЭТАЖИ витрины (правка 2026-08-01 по
            замечанию Виктора «расположение блоков не нравится»). Одна сетка на
            всю страницу ставила рядом панели несопоставимой высоты — короткая
            таблица подписок против карточки с двумя графиками, полоса вердиктов
            в 10px против журнала на пол-экрана, — и разницу высот было видно как
            дыру в фоне. Мера не в выравнивании (пустота уезжает внутрь рамки), а
            в РАЗДЕЛЕНИИ МЕХАНИК: узкое пакуется колонками (__pack, дыр нет по
            устройству — упаковка не оставляет хвоста), широкое стоит парами в
            сетке дорожек (__grid), и пары подобраны по смыслу И по высоте.
            __floor — подпись этажа: mono-caps + волосяная линия во всю ширину;
            структура называет содержание, а не украшает его
   infra    ops-ic(--card --wide) — КАДР картинки объявляет разметка, потолок
            ширины ставит лист: без объявления лист не знает, до какой ширины
            картинке позволено расти. Имена == _FRAME_CLASS генератора
   infra    ops-ic(-axis -line -band -bar -track -fill -label -tick -empty) — три формы
            серверного SVG (ряд по времени · столбики по дням · полоса запаса).
            Ни одного статусного цвета намеренно: порога «много» у нас нет, и
            покрашенная полоса читалась бы как вердикт. Имена == CLASS_VOCABULARY
            в pipeline/serving/infra_chart.py, равенство сверяет тест
            ops-allow — СТРОКА таблицы запаса, несущая полосу своего соседа
            сверху на всю ширину (задача 10; почему не столбец — у правила)
            ops-strip(__cell(--ok --warn --bad --neu)) — полоса вердиктов по
            часам (задача 13). Не график, а ПЕРЕЧИСЛЕНИЕ состояний, поэтому
            статусные тона здесь уместны: величина сама и есть статус. Почему
            не родственная ops-uptime — у правила
   ── дополнение /ops/alerts (страница-разбор; класс = строка здесь ДО
      использования — словарь остаётся закрытым) ──
   alerts   ops-ah(-day(--hit --quiet) -tick(--end) -empty) — полоса «сколько
            суток это длится», серверный SVG. Клетка БЕЗ модификатора не
            выпускается рисовальщиком вовсе: безымянная клетка — это тон по
            умолчанию, то есть незнание, покрашенное чем-то одним. Два тона =
            два разных факта (доставлялось · не доставлялось). Имена ==
            CLASS_VOCABULARY в pipeline/serving/alert_chart.py, равенство
            сверяет тест
   ════════════════════════════════════════════════════════════════════════════ */

/* ── self-hosted fonts (Inter variable · JetBrains Mono 400/500/600) ───────
      ?v=<sha256[:10] файла> — тот же контракт, что asset_v() у CSS/JS: смена
      файла = новый URL (immutable-кэш год), а precache SW собирает ТОТ ЖЕ
      версионированный URL через asset_v — офлайн-планшет держит шрифты
      (084 п.2а). Равенство хэша и файла сверяет check_ops_surface.py. ─────── */
@font-face{font-family:'Inter';src:url('fonts/web/InterVariable.woff2?v=693b77d4f3') format('woff2');font-weight:100 900;font-style:normal;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('fonts/web/JetBrainsMono-Regular.woff2?v=a9cb1cd823') format('woff2');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('fonts/web/JetBrainsMono-Medium.woff2?v=086c48dfbe') format('woff2');font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('fonts/web/JetBrainsMono-SemiBold.woff2?v=918edad542') format('woff2');font-weight:600;font-style:normal;font-display:swap}

/* ── токены (обе темы; canon-поправка №2 — консоль на СВОИХ токенах).
      Переехали сюда из colors_and_type.css волной 5: /ops/ больше не грузит
      публичный лист, стек самодостаточен. ──────────────────────────────────── */
/* Токены живут ВНЕ каскадных слоёв намеренно, и это не забытая обёртка. Слой
   влияет на то, чьё ОБЪЯВЛЕНИЕ побеждает при равной специфике; у пользовательских
   свойств победитель нужен ровно обратный — самый поздний и самый конкретный
   (тема переопределяет базу). Заверни токены в ops-vocab, и переопределение темы
   пришлось бы вносить в тот же слой, то есть тема перестала бы быть надстройкой. */
/* stylelint-disable-next-line rule-nesting-at-rule-required-list */
:root{
  --ops-canvas:#eef1f4; --ops-panel:#ffffff;
  --ops-line:#dde3e9; --ops-line-soft:#e9edf1;
  --ops-ink:#101820; --ops-text:#333e4b; --ops-mute:#5a6879;
  --ops-accent:#0e6e50;
  --ops-ok:#158a52;  --ops-ok-t:#e2f3e9;  --ops-ok-x:#0f6b40;
  --ops-warn:#b57a00; --ops-warn-t:#fdf2d7; --ops-warn-x:#805206;
  --ops-bad:#d13636; --ops-bad-t:#fbe7e7; --ops-bad-x:#a02222;
  --ops-neu:#78848f; --ops-neu-t:#eceff2; --ops-neu-x:#4f5b66;
  --ops-btn-ink:#ffffff; --ops-scrim-bg:rgba(8,10,14,.45);
  --ops-font-sans:'Inter',system-ui,sans-serif;
  --ops-font-mono:'JetBrains Mono',monospace;
  --ops-dur:130ms;  /* hover-tint консоли (канон v2; ≠ --dur-micro сайта 150ms) */
  /* Ширина боковой полосы меню стенда прогонов (кусок 9). Токен, а не литерал в
     правиле: этой же мерой пользуются и раскладка, и узкая ветка, и расчёт того,
     что остаётся листу. 300px — минимум, на котором таблица журнала прогонов и
     поле адреса ещё читаются столбиком; шире полоса начинает отбирать у листа
     больше, чем экономит оператору кликов. Свёрнутая ширина — ВТОРОЙ токен, а не
     литерал в правиле (гейт Leon куска 9, ⚪-14): мера, записанная в двух местах,
     заводит второе описание одной величины ровно так же, как второй список слоёв. */
  --ops-gen-rail:300px;
  --ops-gen-rail-min:34px;
  /* МЕРА ШИРИНЫ витрины ресурсов (правка вёрстки 2026-07-31). Страница разложена
     ДОРОЖКАМИ фиксированной ширины: карточка сервиса занимает одну, панель с
     картинкой разворота — две. До этого панели были блоками во всю ширину окна, и
     на мониторе 2560 подпись отрывалась от своего значения на две трети экрана, а
     картинка кадром 1140 висела по центру пустой полосы. Токен, а не литерал:
     этой же мерой пользуются и сетка, и потолки картинок, и узкая ветка.
     480px — минимум, на котором ряд по времени с двумя подписями шкалы и строка
     фактов «ключ — значение» ещё читаются в одной колонке; ниже подпись и значение
     начинают переноситься, и колонка перестаёт быть колонкой.
     Кадры картинок — ВТОРЫЕ токены, а не «дорожка минус поля»: их числа обязаны
     дословно совпадать с FRAME_CARD / FRAME_WIDE генератора (pipeline/serving/
     infra_chart.py), и равенство сверяет tests/serving/test_infra_chart.py. Кадр
     чуть шире дорожки НАМЕРЕННО: колонка сетки резиновая (1fr), и картинка,
     упёртая в свой потолок раньше края колонки, оставляла бы справа поле. */
  --ops-lane:480px;
  --ops-frame-card:560px;
  --ops-frame-wide:1140px;
  /* Подложка окошка прод-превью отчёта (K2 handbook split-view): = --paper-outer
     публичного листа. ЛИТЕРАЛ осознанно: colors_and_type.css на /ops/ не грузится,
     равенство значений держит check_ops_surface.py, не var()-ссылка. Не
     переопределяется тёмной темой — превью светлое в ОБЕИХ темах, это правда
     продукта (NOTES K2). */
  --ops-report-paper:#EFEBE0;
  /* Заливки слоя вердиктов карты участков (кусок 7, виза Виктора 2026-07-26).
     Вердикт ПОРЯДКОВЫЙ (Likely → Unlikely), поэтому и шкала последовательная:
     зелёный → янтарь → красный. «Needs Review» — не точка этой шкалы, а признание
     незнания, поэтому нейтральный серый; «не считали» — БЕЗ заливки (только контур):
     пустое знание не имеет права выглядеть результатом. Слой рисует MapLibre из JS,
     поэтому значения читаются им через getComputedStyle этих токенов — сырых цветов
     в скриптах не бывает (#44 §2, три слоя токенов). */
  --ops-map-likely:#158a52;
  --ops-map-possible:#7fa93c;
  --ops-map-risky:#b57a00;
  --ops-map-unlikely:#d13636;
  --ops-map-review:#78848f;
  --ops-map-none:transparent;
  --ops-map-line:#5a6879;
}
/* Тёмная тема v2 (переключатель, НЕ дефолт; ink-синий, не чёрный). Атрибут
   ставит инлайн-скрипт _ops_theme_stamp до первой отрисовки (localStorage).
   Вне слоя по той же причине, что и база токенов выше. */
/* stylelint-disable-next-line rule-nesting-at-rule-required-list */
html[data-ops-theme="dark"]{
  --ops-canvas:#0c0f16; --ops-panel:#141824;
  --ops-line:#262c3c; --ops-line-soft:#1e2431;
  --ops-ink:#e7ebf2; --ops-text:#bcc5d2; --ops-mute:#8792a3;
  --ops-accent:#3bbd85;
  --ops-ok:#37b872;  --ops-ok-t:#11291c;  --ops-ok-x:#5fd598;
  --ops-warn:#e5a93d; --ops-warn-t:#2c2310; --ops-warn-x:#eebd60;
  --ops-bad:#e25a5a; --ops-bad-t:#331717; --ops-bad-x:#f28080;
  --ops-neu:#78848f; --ops-neu-t:#222836; --ops-neu-x:#9aa5b4;
  --ops-btn-ink:#0c0f16; --ops-scrim-bg:rgba(2,3,5,.6);
  /* ЗАЛИВОК КАРТЫ ЗДЕСЬ НЕТ НАМЕРЕННО (гейт Prod куска 6, MAJOR-6). Прежняя редакция
     осветляла их «потому что подложка плиток темнее» — тогда это было неправдой:
     подложка была ОДНА на обе темы. Кусок 8 завёл вторую (map-style-dark.json), и
     довод устарел — но вывод остался прежним по ДРУГОЙ причине: заливка означает
     право на сюиту, и один и тот же статус обязан читаться одним цветом в обеих
     темах, иначе оператор сверяет карту с панелью участка и видит расхождение.
     Значения наследуются из :root осознанно.
     Исключение — линия контура: квадратик легенды живёт на ПАНЕЛИ консоли (её фон в
     тёмной теме действительно тёмный), поэтому контур берёт тему панели. */
  --ops-map-line:#8792a3;
}

/* ── каскад: ресет проигрывает словарю СЛОЕМ (#44 §3; волна 5 после ухода
      легаси). До слоёв `.ops-shell *` (0,1,0) глушил элементные правила
      prose-набора (0,0,1) — отступы markdown-прозы рендерились нулями. ────── */
@layer ops-reset,ops-vocab;

@layer ops-reset{
html{color-scheme:light}
html[data-ops-theme=dark]{color-scheme:dark}
/* smooth для TOC-якорей reading-страниц — под kill-switch reduced-motion
   (паритет: до децуплинга давал colors_and_type, но без kill-switch) */
@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}
/* body-фикс ВСЕХ трёх поверхностей (Leon CRITICAL волны 5): html,body{margin:0}
   жил в публичном листе — без этого правила UA-марджин 8px давал белый кант
   вокруг каждой /ops/-страницы; offq-вариант существовал с волны 4, shell и
   gatepage добраны фикс-раундом. Фон на body — чтобы канва шла в край. */
body:has(>.ops-shell),body:has(>.ops-gatepage),body:has(>.ops-offq){margin:0;padding:0;background:var(--ops-canvas)}
.ops-shell,.ops-shell *,.ops-gatepage,.ops-gatepage *,.ops-offq,.ops-offq *{margin:0;padding:0;box-sizing:border-box}
/* font-smoothing/text-rendering — сохранение вида: до децуплинга наследовались
   с body публичного листа; без них весь портал стал бы визуально жирнее */
.ops-shell,.ops-gatepage,.ops-offq{font:400 12.5px/1.4 var(--ops-font-sans);background:var(--ops-canvas);color:var(--ops-text);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
/* UA-нормализация (щитом от публичного листа больше не является — лист не
   грузится): ссылки/кнопки без UA-синего и подчёркиваний. :where() — ноль
   специфичности (фикс волны 1): любой класс словаря сильнее ресета. */
:where(.ops-shell,.ops-gatepage,.ops-offq) a{color:inherit;text-decoration:none}
:where(.ops-shell,.ops-gatepage,.ops-offq) button{font:inherit;cursor:pointer;border:0;background:none;color:inherit}
:where(.ops-shell,.ops-gatepage,.ops-offq) ::selection{background:var(--ops-accent);color:var(--ops-btn-ink)}
/* Reduced-motion kill-switch (Prod MAJOR волны 5): глобальный килл жил в
   motion.css и ушёл с децуплингом — drawer-слайд и hover-tint оставались
   живыми для motion-sensitive операторов. Скоуп-зеркало motion.css:58-70;
   !important в НИЖНЕМ слое побеждает верхние (инверсия важности в @layer) —
   килл сильнее любого словарного transition. */
@media (prefers-reduced-motion:reduce){
  :where(.ops-shell,.ops-gatepage,.ops-offq),
  :where(.ops-shell,.ops-gatepage,.ops-offq) :is(*,*::before,*::after){
    /* ЕДИНСТВЕННЫЙ !important на поверхности, и он здесь обязателен. Рубильник
       движения обязан бить ЛЮБУЮ анимацию, включая объявленную в более позднем
       слое и с большей спецификой, — а такой победы не даёт ни слой, ни селектор.
       Запрет из #44 §3 направлен против !important как способа переспорить
       порядок слоёв; здесь он не спорит с порядком, а исполняет системную
       настройку пользователя, отменять которую нам нечем.
       Все три объявления стоят ОДНОЙ строкой — исключение накрывает ровно её. */
    /* stylelint-disable-next-line declaration-no-important */
    animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important
  }
}
}
/* Ресет `:is(p,h1..h6)` волны 2 удалён волной 5: серифная протечка публичного
   листа исчезла вместе с самим листом (децуплинг), а prose-набор reading-
   страниц теперь честно бьёт box-ресет слоем ops-vocab. */

@layer ops-vocab{

/* ── shell ───────────────────────────────────────────────────────────────── */
.ops-shell{display:grid;grid-template-columns:210px 1fr;min-height:100vh}
.ops-side{background:var(--ops-panel);border-inline-end:1px solid var(--ops-line);padding:12px 0 8px;display:flex;flex-direction:column}
.ops-side__brand{padding:0 14px 10px}
.ops-side__brand .wm{font:700 14px var(--ops-font-sans);color:var(--ops-ink);letter-spacing:-.01em}
.ops-side__brand .wm em{font-style:normal;color:var(--ops-accent)}
.ops-side__brand .tag{display:block;font:500 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute);margin-block-start:2px}
.ops-side__nav{flex:1;overflow:auto}
.ops-side__grp{font:600 9.5px var(--ops-font-mono);letter-spacing:.11em;text-transform:uppercase;color:var(--ops-mute);padding:11px 14px 3px}
/* min-height 24px = SC 2.5.8 target-size (axe wcag22aa); мокап 23.8px — +0.2px */
.ops-side__nav a{display:flex;justify-content:space-between;align-items:center;padding:3.5px 14px;min-height:24px;font-size:12px;color:var(--ops-text);transition:background var(--ops-dur),color var(--ops-dur)}
.ops-side__nav a:hover{color:var(--ops-ink);background:var(--ops-line-soft)}
.ops-side__nav a.active{color:var(--ops-ink);font-weight:600;background:var(--ops-ok-t);box-shadow:inset 2px 0 0 var(--ops-accent)}
.ops-side__nav .n{font:600 10px var(--ops-font-mono);background:var(--ops-warn-t);color:var(--ops-warn-x);border-radius:7px;padding:0 5px;min-width:16px;text-align:center}
/* «Не посчитано» — НЕ ноль и не число очереди: у него свой знак и свой тон, иначе
   отсутствие бейджа означало бы три разные вещи сразу (аудит 2026-08-02, К-19). */
.ops-side__nav .n.unread{background:var(--ops-line);color:var(--ops-mute)}
.ops-scrim{display:none}
.ops-main{display:flex;flex-direction:column;min-width:0}

/* ── topbar ──────────────────────────────────────────────────────────────── */
.ops-top{position:relative;height:42px;display:flex;align-items:center;gap:8px;padding:0 14px;background:var(--ops-panel);border-block-end:1px solid var(--ops-line)}
.ops-burger{display:none;font:600 10px var(--ops-font-mono);letter-spacing:.08em;border:1px solid var(--ops-line);background:var(--ops-panel);color:var(--ops-text);border-radius:4px;padding:5px 9px;min-height:24px}
.ops-top__search{flex:0 1 320px;display:flex;align-items:center;gap:7px;border:1px solid var(--ops-line);border-radius:4px;padding:4px 9px;background:var(--ops-canvas)}
.ops-top__search .k{border:1px solid var(--ops-line);border-radius:3px;padding:0 4px;font:400 10px var(--ops-font-mono);color:var(--ops-mute)}
.ops-top__search input{border:0;background:none;outline:none;width:100%;font:400 11.5px var(--ops-font-mono);color:var(--ops-text)}
.ops-top__search input::placeholder{color:var(--ops-mute)}
.ops-top .spacer{flex:1}
.ops-badge{font:600 9.5px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;border-radius:3px;padding:2.5px 7px;border:1px solid var(--ops-line);color:var(--ops-mute);white-space:nowrap}
.ops-badge--demo{background:var(--ops-warn-t);color:var(--ops-warn-x);border-color:transparent}
.ops-badge--env{background:var(--ops-ok-t);color:var(--ops-ok-x);border-color:transparent}
.ops-who{font-size:11.5px;color:var(--ops-mute);white-space:nowrap}
/* white-space:nowrap — чипы топбара живут в одну строку при любой тесноте: на
   390px «Sign out» ломался пополам и распирал полосу, как только рядом встал
   возврат на сайт. */
.ops-theme,.ops-out,.ops-back{font:600 11px var(--ops-font-sans);border:1px solid var(--ops-line);background:var(--ops-panel);color:var(--ops-text);border-radius:4px;padding:3.5px 9px;min-width:24px;min-height:24px;white-space:nowrap;transition:border-color var(--ops-dur)}
.ops-theme:hover,.ops-out:hover,.ops-back:hover{border-color:var(--ops-mute)}
/* Возврат на сайт — ссылка в хроме кнопок: тот же чип, но <a> нужен flex-центр
   (у кнопки он от UA) и запрет переноса «← Back to site». */
.ops-back{display:inline-flex;align-items:center;gap:5px;white-space:nowrap}

/* ── замок повышения (099 § 1 п.3) ────────────────────────────────────────────
   Состояние действий видно ДО заполнения формы, а не в момент отправки. Цвет здесь
   несёт СМЫСЛ (доступ открыт / закрыт), а не украшает — тем же токеном ok/bad, что
   сигнальная полоса. Отсчёт печатается моноширинным с табличными цифрами: иначе
   строка дёргается каждую секунду. Окно ввода живёт в той же полосе и не двигает
   соседей — ширина поля кода фиксирована по 6 знакам. */
.ops-lock__btn{display:inline-flex;align-items:center;gap:5px;font:600 11px var(--ops-font-sans);border:1px solid var(--ops-line);border-radius:4px;padding:3.5px 9px;min-height:24px;white-space:nowrap;transition:border-color var(--ops-dur)}
.ops-lock__btn:hover{border-color:var(--ops-mute)}
[data-ops-lock="open"] .ops-lock__btn{background:var(--ops-ok-t);color:var(--ops-ok-x);border-color:transparent}
[data-ops-lock="closed"] .ops-lock__btn{background:var(--ops-bad-t);color:var(--ops-bad-x);border-color:transparent}
.ops-lock__left{font:600 11px var(--ops-font-mono);font-variant-numeric:tabular-nums}
.ops-lock{position:relative;display:inline-flex}
/* Окно кода — НАД полосой, а не в ней: встроенная в строку форма раздвигала топбар и
   наезжала на соседние кнопки (проверено съёмкой 1280 и 390). Позиционируется от самого
   замка, поэтому полоса не меняет ширину ни на одном состоянии. */
.ops-lock__pop{position:absolute;inset-block-start:calc(100% + 5px);inset-inline-end:0;z-index:40;display:flex;flex-direction:column;gap:5px;inline-size:max-content;max-inline-size:min(320px,calc(100vw - 28px));background:var(--ops-panel);border:1px solid var(--ops-line);border-radius:5px;padding:9px 10px}
.ops-lock__form{display:flex;align-items:center;gap:5px}
.ops-lock__label{font:400 10px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ops-mute)}
.ops-lock__code{inline-size:calc(6ch + 6 * .12em + 16px);font:600 12px var(--ops-font-mono);font-variant-numeric:tabular-nums;letter-spacing:.12em;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-text);padding:3px 6px;min-height:24px}
.ops-lock__go{font:600 11px var(--ops-font-sans);border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-text);padding:3.5px 9px;min-height:24px}
.ops-lock__go:hover{border-color:var(--ops-mute)}
.ops-lock__err{font-size:11px;color:var(--ops-bad-x)}
.ops-lock__hint{font-size:10.5px;color:var(--ops-mute);max-inline-size:34ch}
/* Узкая полоса: замок ужимается до значка, а окно кода переносится под полосу и
   растягивается на её ширину. Замер до правки: страница ехала вбок на 91px (481 при
   экране 390) — новый узел в строке nowrap выдавливал полосу. Цвет остаётся, то есть
   состояние читается и без подписи; подпись живёт в aria-label ссылки. */
@media (max-width:700px){
  .ops-lock__text{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
  /* Узкая полоса: подпись и слово «until» уходят, остаётся цвет, значок и сами цифры
     (замер: со словом страница ехала с 390 до 432). Смысл держат значок и цвет. */
  .ops-lock__until{display:none}
  .ops-lock__left{font-size:10.5px}
  .ops-lock__pop{inset-inline-end:auto;inset-inline-start:50%;transform:translateX(-50%)}
  .ops-lock__hint{max-inline-size:none}
}

/* ── crumb + page frame ──────────────────────────────────────────────────── */
.ops-crumb{font:400 10px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ops-mute);padding:7px 14px 0}
.ops-crumb a:hover{color:var(--ops-ink)}
.ops-crumb b{font-weight:600;color:var(--ops-text)}
.ops-shell .wrap{padding:10px 14px 14px;display:flex;flex-direction:column;gap:8px}
/* ── кусок 8: страница-инструмент во весь экран (карта, стенд прогонов) ──────
   Высота приходит от вьюпорта ЦЕПОЧКОЙ: shell (100dvh) → main → wrap → .ops-fill.
   `calc(100dvh - высота шапки)` отвергнут: число протухает от любой правки топбара,
   крошек или заголовка, и протухает МОЛЧА — либо серое поле под холстом, либо вторая
   полоса прокрутки. `min-block-size:0` на каждом звене обязателен: flex-элемент иначе
   считает своим минимумом высоту содержимого и цепочка не сжимается.
   dvh, а не vh: на мобильных vh считается по РАЗВЁРНУТОМУ вьюпорту, и нижняя полоса
   браузера срезала бы легенду. */
.ops-shell--fill{block-size:100dvh}
.ops-shell--fill .ops-main{min-block-size:0}
.ops-shell--fill .wrap{flex:1 1 auto;min-block-size:0}
.ops-shell--fill .ops-fill{flex:1 1 auto;min-block-size:0;display:flex;flex-direction:column;gap:8px}
/* Внутри полноэкранной страницы растёт РОВНО ОДИН блок — тот, ради которого страница
   полноэкранная. Прежнее правило раздавало рост всем детям поровну, и на стенде прогонов
   заметки над рамкой отбирали высоту у листа: замер на 1280×800 дал листу 11-18% окна при
   242 px заметок. Соседи занимают столько, сколько им нужно по содержимому, и не больше. */
.ops-shell--fill .ops-fill>*{flex:0 0 auto;min-block-size:0}
.ops-shell--fill .ops-fill>.ops-gen,
.ops-shell--fill .ops-fill>.ops-gen__result,
.ops-shell--fill .ops-fill>.ops-map{flex:1 1 auto;min-block-size:0}
/* Узкий экран: полноэкранный режим снимается — там правильнее обычная прокрутка
   страницы (инструмент во весь экран имеет смысл на рабочем мониторе, не на телефоне). */
@media (max-width:820px){
  .ops-shell--fill{block-size:auto;min-block-size:100dvh}
  .ops-shell--fill .wrap,.ops-shell--fill .ops-fill,.ops-shell--fill .ops-fill>*{flex:0 1 auto;min-block-size:auto}
}
.ops-h-row{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.ops-h-row h1{font:700 15px var(--ops-font-sans);letter-spacing:-.01em;color:var(--ops-ink)}
.ops-h-row .when{font:400 10.5px var(--ops-font-mono);color:var(--ops-mute);margin-inline-start:auto}
.ops-h-row .scenario{font:600 10px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-warn-x)}

/* ── signal strip ────────────────────────────────────────────────────────── */
.ops-sig{display:flex;align-items:stretch;background:var(--ops-panel);border:1px solid var(--ops-line);border-radius:4px;overflow:hidden;flex-wrap:wrap}
.ops-sig__cell{display:flex;align-items:center;gap:8px;padding:7px 12px;border-inline-end:1px solid var(--ops-line-soft)}
.ops-sig__cell:last-child{border-inline-end:0;margin-inline-start:auto}
.ops-sig__lab{font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute)}
.ops-sig__val{font:700 12.5px var(--ops-font-sans);color:var(--ops-ink);display:flex;align-items:center;gap:6px;white-space:nowrap}
.ops-sig__val.ok{color:var(--ops-ok-x)}
.ops-sig__val.warn{color:var(--ops-warn-x)}
.ops-sig__val.bad{color:var(--ops-bad-x)}
.ops-sig__val.neu{color:var(--ops-neu-x)}
/* Кусок 8: та же полоса сигналов — но в панели участка шириной 320px. Трём ячейкам
   (затопление / шум аэропорта / наследие) нужно ~420px, значит они переносятся, и правило
   `.ops-sig__cell:last-child{margin-inline-start:auto}` выше отбрасывает третью ячейку в
   правый край отдельной строки — подпись отрывается от своего ряда. Модификатор снимает
   именно этот отброс и раздаёт ячейкам равные доли, чтобы перенос был ровным. */
.ops-sig--panel .ops-sig__cell{flex:1 1 auto;margin-inline-start:0}
.ops-sig--panel .ops-sig__cell:last-child{margin-inline-start:0}
.ops-dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--ops-neu);flex:none}
.ops-dot--ok{background:var(--ops-ok)}
.ops-dot--warn{background:var(--ops-warn)}
.ops-dot--bad{background:var(--ops-bad)}
.ops-dot--neu{background:var(--ops-neu)}

/* ── KPI tiles ───────────────────────────────────────────────────────────── */
.ops-tiles{display:grid;grid-template-columns:repeat(7,1fr);gap:7px}
.ops-tile{position:relative;background:var(--ops-panel);border:1px solid var(--ops-line);border-radius:4px;padding:7px 10px 6px;min-width:0;transition:border-color var(--ops-dur)}
.ops-tile:hover{border-color:var(--ops-mute)}
.ops-tile::before{content:"";position:absolute;inset:0 0 auto 0;height:2px;border-radius:4px 4px 0 0;background:transparent}
.ops-tile--warn::before{background:var(--ops-warn)}
.ops-tile--bad::before{background:var(--ops-bad)}
/* overflow-ellipsis — волна 1: длинные подписи (costs) резались о край тайла */
/* Ключ плитки переносится, а не режется: когда значение = «—», весь смысл несёт
   ИМЯ, и «NIGHTLY SYNC + HE…» не говорит ничего. Сетка тайлов — grid с равными
   колонками, вторая строка ключа её не ломает. */
.ops-tile__k{font:600 9.5px/1.35 var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);display:block;overflow-wrap:anywhere}
.ops-tile__v{font:700 21px/1.15 var(--ops-font-sans);letter-spacing:-.02em;color:var(--ops-ink);margin-block-start:1px}
.ops-tile--warn .ops-tile__v{color:var(--ops-warn-x)}
.ops-tile--bad .ops-tile__v{color:var(--ops-bad-x)}
.ops-tile__s{font:400 10px var(--ops-font-mono);color:var(--ops-mute);white-space:nowrap;display:block;overflow:hidden;text-overflow:ellipsis}
/* --wrap: юридически значимая подпись (оговорка достоверности) не обрезается */
.ops-tile__s--wrap{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.5}

/* ── panels ──────────────────────────────────────────────────────────────── */
.ops-cols{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:8px;align-items:start}
.ops-cols--entity{grid-template-columns:1fr 1.2fr}
.ops-col{display:flex;flex-direction:column;gap:8px;min-width:0}
.ops-panel{background:var(--ops-panel);border:1px solid var(--ops-line);border-radius:4px}
.ops-panel__head{display:flex;align-items:center;gap:8px;padding:7px 11px;border-block-end:1px solid var(--ops-line)}
.ops-panel__title{font:600 10px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-ink)}
.ops-panel__note{margin-inline-start:auto;font:400 10px var(--ops-font-mono);color:var(--ops-mute)}
a.ops-panel__note:hover{color:var(--ops-ink)}
.ops-panel__pad{padding:9px 11px}
.ops-rule-top{border-block-start:1px solid var(--ops-line-soft)}
.ops-scroll{overflow-x:auto}

/* ── tables ──────────────────────────────────────────────────────────────── */
.ops-table{width:100%;border-collapse:collapse}
.ops-table th{font:600 9.5px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);text-align:left;padding:4px 11px;border-block-end:1px solid var(--ops-line-soft)}
.ops-table td{padding:0 11px;height:28px;border-block-end:1px solid var(--ops-line-soft);font-size:12px;white-space:nowrap}
.ops-table tr:last-child td{border-block-end:0}
/* Длинные ЮР-СТРОКИ (карточка компании): перенос вместо горизонтального скролла.
   Обычная ops-table держит nowrap — она для коротких значений. Здесь ячейка несёт
   строку продавца и CASL-идентификацию целиком, и спрятать её хвост за скроллом
   значит дать оператору подтвердить то, чего он не видел (гейт Lex 2026-07-25,
   условие 7: предпросмотр показывает РЕНДЕРНЫЕ строки «до → после»). */
.ops-table--wrap td{white-space:normal;height:auto;padding-block:7px;line-height:1.5;overflow-wrap:anywhere}
/* Рабочий лист, разбитый на несколько панелей (очередь выездов: сегодня / другие дни):
   каждая группа — своя таблица, и при авто-раскладке одноимённые колонки соседних
   панелей встают на РАЗНЫХ отступах — глаз читает лист сверху вниз и спотыкается
   (гейт Prod куска 2). Фиксированная раскладка + именованная ширина колонки времени
   дают всем панелям один шаг колонок. Панель без времени колонку просто не печатает —
   её форма честно отличается, а не притворяется той же таблицей со сдвигом. */
.ops-table--fixed{table-layout:fixed}
.ops-table td.when,.ops-table th.when{width:132px}
/* Под фиксированной раскладкой колонка без ширины делит остаток поровну — служебная
   мета забирала бы половину листа у адреса, ради которого лист и существует. Мета
   получает треть, адрес — всё остальное. */
.ops-table--fixed td.aux,.ops-table--fixed th.aux{width:32%}
/* Вторая строка ячейки: то, что относится к предмету строки и обязано пережить
   узкий экран (тип suite под адресом выезда), в отличие от .aux — служебной меты,
   которая на телефоне уходит целиком. */
.ops-table__sub{display:block;margin-block-start:2px;font:400 11.5px var(--ops-font-sans);color:var(--ops-mute);white-space:normal}
/* Тот же шов, когда вторая строка ПРЕДУПРЕЖДАЕТ (два разных адреса у одного выезда):
   амбер = внимание, читается тремя каналами вместе с текстом самой фразы. Не bad:
   расхождение бывает законным (город перенумеровал участок) — это факт к проверке,
   а не авария. */
.ops-table__sub--warn{color:var(--ops-warn-x);font-size:11px;font-weight:600}
.ops-table tbody tr{transition:background var(--ops-dur)}
.ops-table tbody tr:hover{background:color-mix(in srgb,var(--ops-line-soft) 45%,transparent)}
.ops-table td.r,.ops-table th.r{text-align:right}
.ops-table td.what{max-width:0;width:56%;overflow:hidden;text-overflow:ellipsis}
.ops-table__id{font-family:var(--ops-font-mono);font-size:11.5px;color:var(--ops-ink)}
.ops-table__age{font-family:var(--ops-font-mono);font-size:11.5px;color:var(--ops-mute)}
.ops-table__act{font:600 11px var(--ops-font-sans);color:var(--ops-accent)}
.ops-table__act:hover{text-decoration:underline}
/* Первая строка ячейки — ЧТО СЛУЧИЛОСЬ: главная фраза строки, набранная крупнее
   служебного. Заведена для этажа «нужен человек», который печатал главным машинное
   имя события тем же кеглем, что и всё вокруг, — и читающий искал смысл глазами.
   white-space:normal обязателен: ячейка .what ужата до остатка ширины (max-width:0)
   и без него фраза резалась бы многоточием в одну строку. */
.ops-table__lede{display:block;font:600 13px/1.4 var(--ops-font-sans);color:var(--ops-ink);white-space:normal;overflow-wrap:anywhere}
/* Раскрывашка ВНУТРИ ячейки: машинная подробность нужна раз в месяц, а место
   занимала всегда. Идиома взята у ops-fold (маркер выключен явно, знак в
   summary::before) — второго рецепта раскрывашки в системе не заводим; отличие
   только в том, что здесь нет рамки и отступов секции: она стоит в строке таблицы. */
.ops-table__fold{margin-block-start:5px}
.ops-table__fold>summary{list-style:none;font:600 10px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ops-mute);cursor:pointer}
.ops-table__fold>summary::-webkit-details-marker{display:none}
.ops-table__fold>summary::before{content:"+";margin-inline-end:5px}
.ops-table__fold[open]>summary::before{content:"\2212"}
.ops-table__fold>summary:hover{color:var(--ops-ink)}
.ops-row--bad td:first-child{box-shadow:inset 2px 0 0 var(--ops-bad)}
.ops-row--warn td:first-child{box-shadow:inset 2px 0 0 var(--ops-warn)}

/* ── кусок З-1: строка ЧИТАЕТСЯ КАРТОЧКОЙ, когда колонок больше, чем ширины ──
      Носитель — /ops/health: шесть колонок, и в каждой не значение, а ГОТОВОЕ
      ПРЕДЛОЖЕНИЕ. На 390px это ~40px на колонку, то есть слова, рвущиеся
      посимвольно («datase / ts»), и высота в несколько экранов.

      ПОЧЕМУ НЕ ДВА ОЧЕВИДНЫХ ЛЕЧЕНИЯ. Кегль поменьше не меняет НИЧЕГО: узка
      колонка, а не буква, и семь символов в строке останутся семью. Горизонтальная
      прокрутка (ops-scroll, которая тут и так есть) честна для таблицы КОРОТКИХ
      значений — обрезанный столбец видно и его домотка стоит одного движения; для
      шести столбцов прозы она означает, что дежурный собирает одну строку из шести
      проездов туда-обратно, то есть работу переложили на него. Оба — перенос
      проблемы, а не её снятие.

      ПОЧЕМУ НЕ .aux (принятое в этом файле лечение узкого экрана). .aux прячет
      ВТОРОСТЕПЕННУЮ колонку. Здесь второстепенных нет: пять вопросов — закрытый
      домен модели, и спрятанный вопрос это не сэкономленное место, а молчание,
      неотличимое от «мы это измерили и там пусто». Ровно против такого молчания
      экран и построен.

      ЧТО СДЕЛАНО. Ниже порога контейнера строка раскладывается сверху вниз:
      имя конвейера — заголовок карточки, под ним пять ответов, каждый со своей
      подписью (шапка при этом убирается: подпись переезжает внутрь ячейки, и
      второй раз её печатать незачем). Ни одного слова не потеряно, прокрутка
      остаётся вертикальной — той, которой телефон и читается.

      ПОРОГ И ЕГО НОСИТЕЛЬ. 860px — это ширина ОБЁРТКИ, не окна (#44 §4:
      внутренняя адаптивность компонента живёт на @container). Меру видно на
      замере: при 1280px окна обёртка ~1042px и шесть колонок дают ~170px каждой —
      ширина, на которой лист утверждён; при 1024px (сайдбар уже ящик) ~996px,
      те же ~165px; ниже 860px на колонку остаётся меньше 140px, и предложение
      перестаёт быть предложением. Правило вешать на окно было бы неверно дважды:
      та же таблица в узкой колонке широкого экрана осталась бы раздавленной, а
      обёртка знает свою ширину сама.

      РОЛИ. display:block снимает с таблицы табличную семантику в дереве
      доступности — поэтому роли проставлены в РАЗМЕТКЕ постоянно (см. шаблон);
      лист их не подменяет и подменить не может. ───────────────────────────── */
.ops-cards{container-type:inline-size;container-name:ops-cards}
/* Подпись вопроса лежит в разметке ВСЕГДА, а показывается только в карточном
   виде: на широкой ширине её работу делает шапка, и два экземпляра одного слова
   на экране читались бы как два разных факта. */
.ops-table__q{display:none}
@container ops-cards (max-width:860px){
  .ops-table--cards,.ops-table--cards tbody,.ops-table--cards tr,.ops-table--cards td{display:block}
  .ops-table--cards thead{display:none}
  /* Карточка = строка. Отбивка сильной чертой (--ops-line, не --ops-line-soft):
     между ячейками ОДНОЙ карточки границ больше нет, и слабая черта перестала бы
     отличать «конец конвейера» от «следующий ответ того же конвейера». */
  .ops-table--cards tr{padding-block:9px;padding-inline:11px;border-block-end:1px solid var(--ops-line)}
  .ops-table--cards tr:last-child{border-block-end:0}
  .ops-table--cards td{height:auto;padding-inline:0;padding-block:0;margin-block-start:8px;border-block-end:0}
  .ops-table--cards td:first-child{margin-block-start:0}
  .ops-table--cards .ops-table__q{display:block;font:600 9.5px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);margin-block-end:2px}
  /* Имя конвейера — заголовок карточки, а не первая из шести равных клеток:
     карточку ищут глазами по имени, и в общем кегле его приходится выискивать. */
  .ops-table--cards td:first-child .ops-table__act,.ops-table--cards td:first-child .ops-table__id{font-size:13px}
  /* Амбер-марка переезжает со ПЕРВОЙ КЛЕТКИ на всю карточку: в карточном виде
     первая клетка — только заголовок, и полоса у неё отмечала бы имя, а не
     строку, которая требует взгляда. */
  .ops-table--cards tr.ops-row--warn td:first-child,.ops-table--cards tr.ops-row--bad td:first-child{box-shadow:none}
  .ops-table--cards tr.ops-row--warn{box-shadow:inset 2px 0 0 var(--ops-warn)}
  .ops-table--cards tr.ops-row--bad{box-shadow:inset 2px 0 0 var(--ops-bad)}
}

/* ── status tags (цвет + точка + текст = 3 канала) ───────────────────────── */
.ops-tag{display:inline-flex;align-items:center;gap:5px;font:600 10px var(--ops-font-mono);letter-spacing:.04em;text-transform:uppercase;white-space:nowrap}
.ops-tag--ok{color:var(--ops-ok-x)}
.ops-tag--warn{color:var(--ops-warn-x)}
.ops-tag--bad{color:var(--ops-bad-x)}
.ops-tag--neu{color:var(--ops-neu-x)}

/* ── facts ───────────────────────────────────────────────────────────────── */
.ops-facts>div{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:0 11px;height:25px;border-block-end:1px solid var(--ops-line-soft)}
.ops-facts>div:last-child{border-block-end:0}
.ops-facts__k{font-size:11.5px;color:var(--ops-mute);white-space:nowrap}
.ops-facts__v{font:500 11.5px var(--ops-font-mono);color:var(--ops-ink);text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* --wrap: значение-ОБЪЯСНЕНИЕ, а не число. Обрезка съедает ровно объяснение
   («2 scheduled · recording o…»), то есть ту часть, ради которой строку и писали;
   на сенсоре подсказки title нет. Тот же шов, что у ops-tile__s--wrap. */
.ops-facts__v--wrap{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.45}
/* Строка факта фиксирована по высоте (25px) — переносящееся значение вылезало бы
   поверх соседей. Строка с таким значением растёт и цепляется верхом, чтобы ключ
   и первая строка значения читались как пара, а не как две независимые надписи. */
.ops-facts>div:has(.ops-facts__v--wrap){height:auto;min-height:25px;padding-block:5px;align-items:flex-start}
.ops-facts__v.ok{color:var(--ops-ok-x)}
.ops-facts__v.warn{color:var(--ops-warn-x)}
/* .bad — дополнение словаря волны 0 (Leon): в мокапе Health свёрстан только
   GREEN-день; RED-день обязан красить и панель фактов, не только hero/тайл */
.ops-facts__v.bad{color:var(--ops-bad-x)}

/* ── feed ────────────────────────────────────────────────────────────────── */
.ops-feed{list-style:none}
.ops-feed li{display:flex;gap:9px;padding:0 11px;height:24px;align-items:center;font-size:11.5px}
.ops-feed .ts{font:400 10px var(--ops-font-mono);color:var(--ops-mute);flex:none}
.ops-feed .ev{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ops-feed .ev b{font-weight:600;color:var(--ops-ink)}
/* Строка ленты с карточкой за ней — акцентом, как остальные переходы словаря
   (ops-table__act / ops-note a): кликабельное обязано отличаться от текста без
   наведения, иначе на сенсоре разницы не видно (R-15). */
.ops-feed a.ev{color:var(--ops-accent)}
.ops-feed a.ev:hover{text-decoration:underline}

/* ── funnel ──────────────────────────────────────────────────────────────── */
.ops-funnel{display:flex;align-items:center;gap:7px;padding:9px 11px}
.ops-funnel__step{flex:1;min-width:0}
.ops-funnel .fk{font:600 9.5px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ops-mute);white-space:nowrap;display:block}
.ops-funnel .fv{font:700 16px var(--ops-font-sans);letter-spacing:-.02em;color:var(--ops-ink);display:inline-block}
.ops-funnel a.fv:hover{color:var(--ops-accent)}
.ops-funnel .fc{font:400 10px var(--ops-font-mono);color:var(--ops-mute);display:block}
.ops-funnel .ar{color:var(--ops-mute);font-size:12px}
.ops-meas{display:flex;justify-content:space-between;gap:12px;padding:5px 11px 7px;border-block-start:1px solid var(--ops-line-soft);font:400 10px var(--ops-font-mono);color:var(--ops-mute);flex-wrap:wrap}

/* ── sparkline + uptime bar ──────────────────────────────────────────────── */
.ops-spark polyline{fill:none;stroke:var(--ops-accent);stroke-width:2}
.ops-spark line{stroke:var(--ops-line);stroke-width:1}
.ops-spark.bad polyline{stroke:var(--ops-bad)}
/* uptime: severity кодируется цветом И высотой (grayscale-читаемо); n = не пробовали */
.ops-uptime{display:inline-flex;align-items:flex-end;gap:1px;height:16px}
.ops-uptime i{display:block;width:3px;height:11px;border-radius:1px;background:var(--ops-ok)}
.ops-uptime i.w{background:var(--ops-warn);height:14px}
.ops-uptime i.b{background:var(--ops-bad);height:16px}
.ops-uptime i.n{background:var(--ops-line);height:7px}

/* ── worklist filters + empty states ─────────────────────────────────────── */
.ops-filters{display:flex;align-items:center;gap:5px;padding:7px 11px;border-block-end:1px solid var(--ops-line);flex-wrap:wrap}
.ops-filters a{font:600 10.5px var(--ops-font-mono);letter-spacing:.04em;text-transform:uppercase;border:1px solid var(--ops-line);border-radius:4px;padding:3px 9px;color:var(--ops-mute);transition:border-color var(--ops-dur)}
.ops-filters a:hover{border-color:var(--ops-mute);color:var(--ops-ink)}
.ops-filters a.on{background:var(--ops-ok-t);color:var(--ops-ok-x);border-color:transparent}
.ops-filters .ops-panel__note{margin-inline-start:auto}
.ops-empty{padding:14px 11px;font:600 10.5px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase}
.ops-empty--clear{color:var(--ops-ok-x)}
.ops-empty--none{color:var(--ops-mute)}
.ops-empty--unavail{color:var(--ops-warn-x)}

/* ── forms + actions ─────────────────────────────────────────────────────── */
.ops-fld{display:flex;flex-direction:column;gap:4px;margin-block-end:10px;max-width:340px}
/* Подпись поля — это label БЕЗ класса. `:not([class])` вместо голого `label` (гейт Prod
   куска 6, MAJOR-2): идиома подписи (капс-моно с трекингом) наследуема, а селектор
   (0,1,1) бил любой компонент-label внутри поля — крупная radio-карточка `.ops-choice`
   молча превращалась в подпись, и целые предложения вместе с ЦЕНОЙ печатались капсом.
   Компонент внутри поля переопределял только `font`, поэтому капс и трекинг протекали
   насквозь. Сузили область, а не залатали компонент: иначе тот же дефект вернулся бы на
   следующем классифицированном label (`.ops-check` уже стоял в очереди). Все живые
   подписи полей — label с одним `for=`, ни одна правку не заметит. */
.ops-fld > label:not([class]){font:600 10px var(--ops-font-mono);letter-spacing:.07em;text-transform:uppercase;color:var(--ops-mute)}
.ops-fld input,.ops-fld textarea{border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-ink);font:500 13px var(--ops-font-mono);padding:7px 9px;min-height:32px;outline-offset:2px}
.ops-fld input:focus-visible,.ops-fld textarea:focus-visible{outline:2px solid var(--ops-accent)}
.ops-fld .hint{font-size:10.5px;color:var(--ops-mute)}
.ops-check{display:flex;align-items:center;gap:12px;min-height:44px;padding:4px 11px;font-size:12.5px;color:var(--ops-text);cursor:pointer;border-block-end:1px solid var(--ops-line-soft)}
.ops-check:last-child{border-block-end:0}
.ops-check input[type=checkbox],.ops-check input[type=radio]{width:22px;height:22px;flex:none;accent-color:var(--ops-accent)}
.ops-actions{display:flex;align-items:center;gap:12px;padding:9px 11px;flex-wrap:wrap}
/* Поле и выпадающий список несут СОБСТВЕННУЮ минимальную ширину от движка, и на
   узком экране она распирает строку таблицы наружу: перенос по flex-wrap спасает
   только пока каждый элемент по отдельности помещается. Потолок в 100% и есть то
   условие, при котором перенос начинает работать. */
.ops-actions input,.ops-actions select,.ops-actions .ops-fld{max-inline-size:100%}
/* rowset: одна запись повторяющейся группы формы визита (095/03 § 3.3) — рамка
   вокруг ячеек одной записи, чтобы «проём 2» не читался как продолжение «проёма 1». */
.ops-rowset{border:1px solid var(--ops-line);border-radius:4px;padding:8px 11px;margin-block-end:10px}
.ops-rowset>legend{font:600 10px var(--ops-font-mono);letter-spacing:.07em;text-transform:uppercase;color:var(--ops-mute);padding-inline:4px}
.ops-btn{display:inline-flex;align-items:center;gap:6px;font:600 11.5px var(--ops-font-sans);background:var(--ops-accent);color:var(--ops-btn-ink);border:1px solid transparent;border-radius:4px;padding:6px 12px;min-height:28px;transition:filter var(--ops-dur)}
.ops-btn:hover{filter:brightness(1.08)}
.ops-btn--outline{background:var(--ops-panel);color:var(--ops-accent);border-color:var(--ops-line)}
.ops-btn--outline:hover{border-color:var(--ops-accent);filter:none}
.ops-btn[aria-disabled=true]{opacity:.5;pointer-events:none}
.ops-linkbtn{border:0;background:none;font:600 11.5px var(--ops-font-sans);color:var(--ops-text);text-decoration:underline;text-underline-offset:2px;padding:4px 2px}
.ops-linkbtn:hover{color:var(--ops-ink)}
.ops-steptag{font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;background:var(--ops-neu-t);color:var(--ops-neu-x);border-radius:3px;padding:1px 5px}

/* ── config-confirm ceremony ─────────────────────────────────────────────── */
.ops-diff{display:inline-flex;align-items:center;gap:7px;font-family:var(--ops-font-mono)}
.ops-diff .old{color:var(--ops-mute);text-decoration:line-through}
.ops-diff .arr{color:var(--ops-mute)}
.ops-diff .new{color:var(--ops-ok-x);font-weight:600}
.ops-warncall{margin:9px 11px;padding:8px 10px;background:var(--ops-warn-t);color:var(--ops-warn-x);border-radius:4px;font-size:11.5px}
.ops-confirm__ask{font-size:12px;color:var(--ops-text);padding:0 11px 4px}

/* ── doc / reading (по центру, TOC-рельс справа) ─────────────────────────── */
.ops-doc{display:grid;grid-template-columns:minmax(0,720px) 190px;justify-content:center;gap:36px;padding-block-start:6px}
.ops-doc__rail{order:2}
.ops-doc__toc{position:sticky;top:14px;border-inline-start:2px solid var(--ops-line);padding-inline-start:12px;display:flex;flex-direction:column;gap:5px}
.ops-doc__toc .t{font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute);margin-block-end:2px}
.ops-doc__toc a{font-size:11.5px;color:var(--ops-mute)}
.ops-doc__toc a:hover,.ops-doc__toc a.on{color:var(--ops-ink)}
/* h3-якорь в рельсе — ступень вложенности (Prod minor волны 5) */
.ops-doc__toc a.l3{padding-inline-start:10px}
.ops-doc__kicker{font:600 10px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute);margin-block-end:8px}
:where(.ops-doc__prose) h1{font:700 24px/1.25 var(--ops-font-sans);letter-spacing:-.02em;color:var(--ops-ink);margin-block-end:10px}
:where(.ops-doc__prose) p{font-size:16px;line-height:1.75;margin-block-end:12px;max-width:70ch}
.ops-doc__prose .lead{font-size:17px;color:var(--ops-mute)}
/* markdown-h2 (kb_portal, raw HTML off — классов не эмитит) = близнец __sec (волна 3) */
.ops-doc__sec,:where(.ops-doc__prose) h2{font:700 15px var(--ops-font-sans);letter-spacing:-.01em;color:var(--ops-ink);margin-block:22px 8px;padding-block-start:14px;border-block-start:1px solid var(--ops-line-soft)}
.ops-doc__callout{margin-block:14px;padding:10px 12px;background:var(--ops-ok-t);border-radius:4px;font-size:13px;line-height:1.6}
.ops-doc__callout .ct{display:block;font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-ok-x);margin-block-end:4px}
.ops-doc__callout.boundary{background:var(--ops-warn-t)}
.ops-doc__callout.boundary .ct{color:var(--ops-warn-x)}

/* ── perimeter (bare) ────────────────────────────────────────────────────── */
.ops-gatepage{min-height:100vh;display:grid;place-items:center;padding:40px 16px}
.ops-gatepage__panel{width:min(340px,100%);background:var(--ops-panel);border:1px solid var(--ops-line);border-radius:6px;padding:28px 24px;display:flex;flex-direction:column;gap:10px;text-align:center}
.ops-gatepage__panel .wm{font:700 17px var(--ops-font-sans);color:var(--ops-ink)}
.ops-gatepage__panel .wm em{font-style:normal;color:var(--ops-accent)}
.ops-gatepage__panel .tag{font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute)}
.ops-gatepage__panel .note{font:400 10.5px var(--ops-font-mono);line-height:1.6;color:var(--ops-mute)}
.ops-gatepage__panel .why{font-size:13px;line-height:1.6;color:var(--ops-text)}

/* ── misc ────────────────────────────────────────────────────────────────── */
.ops-qa{display:flex;flex-wrap:wrap;gap:5px;padding:9px 11px}
.ops-qa a{font:600 10.5px var(--ops-font-sans);color:var(--ops-accent);border:1px solid var(--ops-line);border-radius:4px;padding:3.5px 9px;transition:border-color var(--ops-dur)}
.ops-qa a:hover{border-color:var(--ops-accent)}
.ops-foot{font:400 9.5px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute)}
.ops-shell :focus-visible,.ops-gatepage :focus-visible{outline:2px solid var(--ops-accent);outline-offset:2px}

/* ── волна 1: notes + kv + seg + pager + dim (словарь-дополнения выше) ────── */
.ops-note{padding:8px 11px;font:400 10.5px/1.7 var(--ops-font-mono);color:var(--ops-mute)}
.ops-note--ok{color:var(--ops-ok-x)}
.ops-note--warn{color:var(--ops-warn-x)}
.ops-note--bad{color:var(--ops-bad-x)}
.ops-note .ops-steptag{margin-inline-end:4px}
.ops-note a{color:var(--ops-accent)}
.ops-note a:hover{text-decoration:underline}
.ops-kv{padding:8px 11px}
.ops-kv>div{display:flex;gap:10px;padding:2.5px 0;font:400 11.5px/1.5 var(--ops-font-mono)}
.ops-kv__k{color:var(--ops-mute);flex:none;min-width:190px}
.ops-kv__v{color:var(--ops-text);word-break:break-word;min-width:0}
/* ПЕРЕКЛЮЧАТЕЛЬ ПЕРЕНОСИТСЯ (правка 2026-08-01). Пока у переключателя было два-три
   положения, строка влезала всюду; восемь окон витрины ресурсов на телефоне дают
   464px при экране 390 — и вбок ехала ВСЯ СТРАНИЦА, а не сам переключатель.
   Замеряется оракулом overflow-390 стенда съёмки: до правки 6 позиций из 8 давали
   scrollWidth 464. Прокрутка вместо переноса была бы хуже переноса: она прячет
   часть положений за краем, а спрятанный орган управления не выбирают.
   Строки разделяет ФОН контейнера в зазоре 1px — та же линия, что стоит между
   положениями по горизонтали, поэтому второго рисунка разделителя не заводится.
   При одной строке зазор не проявляется вовсе, и вид на всех прочих экранах
   консоли остаётся прежним. */
.ops-seg{display:inline-flex;flex-wrap:wrap;row-gap:1px;background:var(--ops-line);
  border:1px solid var(--ops-line);border-radius:4px;overflow:hidden;align-self:start}
.ops-seg a{background:var(--ops-panel);font:600 10.5px var(--ops-font-mono);letter-spacing:.04em;text-transform:uppercase;padding:5px 12px;color:var(--ops-mute);border-inline-end:1px solid var(--ops-line);transition:color var(--ops-dur)}
.ops-seg a:last-child{border-inline-end:0}
.ops-seg a:hover{color:var(--ops-ink)}
.ops-seg a.on{background:var(--ops-ok-t);color:var(--ops-ok-x)}
.ops-pager{display:flex;align-items:center;gap:12px;padding:9px 11px;font:400 11px var(--ops-font-mono);color:var(--ops-mute)}
.ops-pager a{color:var(--ops-accent);font-weight:600}
.ops-pager a:hover{text-decoration:underline}
.ops-dim{font:400 10px var(--ops-font-mono);color:var(--ops-mute)}
.ops-dim--wrap{white-space:pre-wrap;word-break:break-word;font-size:10.5px}
.ops-flag{font:400 10.5px var(--ops-font-mono);color:var(--ops-warn-x)}
.ops-sha{font:600 15px var(--ops-font-mono);letter-spacing:.08em;color:var(--ops-ink)}
.ops-mix{display:flex;height:12px;border-radius:3px;overflow:hidden;margin:9px 11px 0}
.ops-mix span{display:block}
.ops-table td.neg{color:var(--ops-bad-x)}
.ops-table tr.ytot td{border-block-start:1px solid var(--ops-line);font-weight:600}
.ops-tile__v--text{font-size:13px;line-height:1.5}

/* ── волна 1: формы в панелях ─────────────────────────────────────────────── */
.ops-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px 10px;align-items:end;padding:9px 11px}
.ops-form .ops-fld{margin-block-end:0;max-width:none}
.ops-form .ops-fld--wide{grid-column:1/-1}
.ops-form .ops-actions{grid-column:1/-1;padding:0}
.ops-form .ops-check{border-block-end:0;padding:0;min-height:28px}
.ops-fld select{border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-ink);font:500 12px var(--ops-font-mono);padding:6px 9px;min-height:32px;outline-offset:2px}
.ops-fld select:focus-visible{outline:2px solid var(--ops-accent)}
.ops-fld--code input{font-size:18px;letter-spacing:.3em}
.ops-btn--danger{background:var(--ops-panel);color:var(--ops-bad-x);border-color:var(--ops-bad)}
.ops-btn--danger:hover{filter:none;background:var(--ops-bad-t)}

/* ── волна 1: календарь слотов (ops_site_visit_calendar) ──────────────────── */
.ops-cal{display:grid;grid-template-columns:repeat(7,minmax(96px,1fr));gap:7px;padding:9px 11px;overflow-x:auto}
.ops-cal__day{border:1px solid var(--ops-line);border-radius:4px;display:flex;flex-direction:column;min-width:0}
.ops-cal__day .dh{padding:6px 8px;border-block-end:1px solid var(--ops-line-soft);font:600 9.5px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute)}
.ops-cal__day .dh b{display:block;color:var(--ops-ink);font-size:11px}
.ops-cal__slots{padding:6px;display:flex;flex-direction:column;gap:6px}
.ops-cal__empty{padding:6px 7px;font:400 9.5px var(--ops-font-mono);color:var(--ops-mute)}
.ops-slot{border:1px solid var(--ops-line);border-radius:4px;padding:6px 7px;font:400 10.5px var(--ops-font-mono)}
.ops-slot--open{box-shadow:inset 2px 0 0 var(--ops-ok)}
.ops-slot--booked{box-shadow:inset 2px 0 0 var(--ops-warn)}
.ops-slot--done{box-shadow:inset 2px 0 0 var(--ops-neu);opacity:.85}
.ops-slot .t{font:600 11px var(--ops-font-sans);color:var(--ops-ink)}
.ops-slot .ctx{color:var(--ops-mute);margin-block-start:3px;line-height:1.5}
.ops-slot .st{display:inline-block;font:600 9px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);margin-block-start:3px}
.ops-slot form{margin-block-start:5px;display:flex;flex-wrap:wrap;gap:4px;align-items:center}
.ops-slot .ack{display:flex;gap:4px;align-items:center;font-size:9px;color:var(--ops-mute)}

/* ── волна 1: конфиг-матрица (ops_config_matrix*) ─────────────────────────── */
.ops-mx{border-collapse:collapse;font:400 11px var(--ops-font-mono);margin:9px 11px}
.ops-mx th,.ops-mx td{border:1px solid var(--ops-line);padding:5px 8px;text-align:center}
.ops-mx th.blk,.ops-mx td.blk{text-align:left;white-space:nowrap}
.ops-mx thead th{font:600 9px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute)}
.ops-mx td.blk .bn{color:var(--ops-mute);margin-inline-end:6px}
.ops-mx tr.frozen td.blk{color:var(--ops-warn-x)}
.ops-mx .lock,.ops-mx .legal-chan{font:600 8px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-warn-x)}
.ops-mx .legal-chan{display:block;margin-block-start:2px}
.ops-mx .mintier,.ops-mx .depth{display:block;font:400 8px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);margin-block-start:2px}
.ops-mx .mintier.off{color:var(--ops-warn-x)}
.ops-mx form{margin:0}
.ops-cellbtn{font:400 13px var(--ops-font-mono);padding:2px 4px;color:var(--ops-ink)}
.ops-cell-ro{color:var(--ops-ink)}
.ops-cellbtn.off,.ops-cell-ro.off{color:var(--ops-mute)}

/* ── волна 1: 2FA enrollment QR ───────────────────────────────────────────── */
.ops-qr{text-align:center;padding:14px}
/* #fff НЕ токен намеренно: QR обязан лежать на белом в обеих темах (сканируемость) */
.ops-qr img{width:220px;height:220px;background:#fff;border-radius:4px;padding:8px}
.ops-qr .manual{font:400 12px var(--ops-font-mono);letter-spacing:.08em;color:var(--ops-text);word-break:break-all;margin-block-start:10px}

/* ── волна 2: контент-лента событий (ops_news) ────────────────────────────── */
.ops-event{padding:9px 11px;border-block-end:1px solid var(--ops-line-soft)}
.ops-event:last-child{border-block-end:0}
.ops-event--review{box-shadow:inset 2px 0 0 var(--ops-warn);background:color-mix(in srgb,var(--ops-warn-t) 45%,transparent)}
.ops-event .e-head{display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap}
.ops-event .e-kind{font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute)}
.ops-event--review .e-kind{color:var(--ops-warn-x)}
.ops-event .e-when{font:400 10px var(--ops-font-mono);color:var(--ops-mute);white-space:nowrap}
.ops-event .e-title{font:600 13px/1.4 var(--ops-font-sans);color:var(--ops-ink);margin-block:3px 2px}
.ops-event .e-sum{font-size:12px;line-height:1.6;color:var(--ops-text);max-width:80ch}
.ops-event .e-meta{font:400 10px var(--ops-font-mono);color:var(--ops-mute);margin-block-start:3px}
.ops-event .e-meta a{color:var(--ops-accent)}
.ops-event .e-meta a:hover{text-decoration:underline}
.ops-event details{margin-block-start:5px}
.ops-event summary{font:600 10px var(--ops-font-mono);letter-spacing:.06em;color:var(--ops-mute);cursor:pointer}
.ops-event summary:hover{color:var(--ops-ink)}
.ops-pre{font:400 11px/1.6 var(--ops-font-mono);background:var(--ops-line-soft);border-radius:4px;padding:8px 10px;margin-block-start:6px;white-space:pre-wrap;word-break:break-word;color:var(--ops-text)}

/* ── волна 2: инлайн-форма в ячейке таблицы + честный disabled ────────────── */
.ops-rowform{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding-block:4px}
.ops-rowform input[type=text],.ops-rowform input[type=date]{border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-ink);font:500 11.5px var(--ops-font-mono);padding:4px 8px;min-height:28px;width:20ch;outline-offset:2px}
/* date (as-of time-travel kb-статьи, волна 3): ширину задаёт нативный виджет */
.ops-rowform input[type=date]{width:auto}
.ops-rowform input[type=text]:focus-visible,.ops-rowform input[type=date]:focus-visible{outline:2px solid var(--ops-accent)}
.ops-rowform .ops-fld{margin-block-end:0}
.ops-kv__v.warn{color:var(--ops-warn-x)}
/* карточка отчёта: недоступное действие ВИДИМО disabled с честной подсказкой
   (спека 10 §2 «Состояния») — не спрятано */
.ops-btn:disabled{opacity:.5;cursor:not-allowed;filter:none}
/* [hidden] обязан побеждать display:inline-flex компонентов (sv-prepare-day
   появляется только когда JS снимает атрибут) — специфичность выше классовой */
.ops-shell [hidden],.ops-gatepage [hidden]{display:none}

/* ── волна 1: серверные SVG-чарты (волна 5: ops-неймспейс синхронно с
      serving-кодом — year_source_chart / inspection_facts_panel /
      inspections_market / cohort_chart; стили v2 без изменений) ───────────── */
.ops-ysc,.ops-ifp-chart{max-width:100%}
.ops-mkt-chart{max-width:none}
.ops-ysc-bar,.ops-ifp-bar,.ops-mkt-bar{fill:var(--ops-ink);opacity:.72}
.ops-ysc-val,.ops-ysc-yr,.ops-ysc-src,.ops-ifp-label,.ops-ifp-val{font-family:var(--ops-font-mono);fill:var(--ops-mute)}
.ops-ysc-val{font-size:9px;text-anchor:middle}
.ops-ysc-yr{font-size:9.5px;text-anchor:middle}
.ops-ysc-src{font-size:10px}
.ops-ifp-label{font-size:10px}
.ops-ifp-val{font-size:9.5px}
.ops-mkt-label{font-family:var(--ops-font-mono);fill:var(--ops-text);font-size:10.5px}
.ops-mkt-disc,.ops-mkt-val{font-family:var(--ops-font-mono);fill:var(--ops-mute);font-size:10.5px}
.ops-mkt-foot{padding:0 11px 8px;font:400 10.5px/1.7 var(--ops-font-mono);color:var(--ops-mute)}
.ops-mkt-foot .fn{color:var(--ops-warn-x);display:block}
.ops-coh-meter{vertical-align:middle}
.ops-coh-track{fill:var(--ops-line)}
.ops-coh-fill{fill:var(--ops-ink);opacity:.72}
svg.ops-tl{display:block}
.ops-tl-head{font:600 10px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ops-mute);margin-block-end:8px}

/* ── волна 3: prose-набор reading-страниц (markdown-рендер kb_portal). Элементные
      правила — в :where(.ops-doc__prose): нулевой класс-слой. Слой ops-vocab бьёт
      ресет (волна 5: до @layer ресет (0,1,0) глушил эти (0,0,1)-правила — отступы
      прозы рендерились нулями), а ЛЮБОЙ классовый компонент словаря, вложенный в
      prose (ops-index/ops-quiz/ops-fcard/ops-qa/панели), всегда сильнее прозы —
      протечки 16px-параграфа/подчёркивания в компоненты исключены каскадом. ──── */
/* якоря markdown-заголовков (082 п.2а): цель не прилипает к верхней кромке */
:where(.ops-doc__prose) :is(h2,h3){scroll-margin-block-start:14px}
:where(.ops-doc__prose) h3{font:700 13.5px var(--ops-font-sans);letter-spacing:-.01em;color:var(--ops-ink);margin-block:18px 6px}
:where(.ops-doc__prose) h4{font:600 10px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);margin-block:16px 5px}
:where(.ops-doc__prose) :is(ul,ol){margin-block-end:12px;padding-inline-start:22px;font-size:16px;line-height:1.75;max-width:70ch}
:where(.ops-doc__prose) li{margin-block-end:5px}
:where(.ops-doc__prose) li>p{margin-block-end:5px}
:where(.ops-doc__prose) blockquote{margin-block-end:12px;padding-block:2px;padding-inline-start:12px;border-inline-start:2px solid var(--ops-line);color:var(--ops-mute)}
:where(.ops-doc__prose) hr{border:0;border-block-start:1px solid var(--ops-line-soft);margin-block:20px}
:where(.ops-doc__prose) strong{color:var(--ops-ink)}
:where(.ops-doc__prose) a{color:inherit;text-decoration:underline;text-underline-offset:2px;text-decoration-color:var(--ops-mute)}
:where(.ops-doc__prose) a:hover{color:var(--ops-accent);text-decoration-color:var(--ops-accent)}
:where(.ops-doc__prose) code{font:500 13px var(--ops-font-mono);background:var(--ops-line-soft);border-radius:3px;padding:1px 4px;overflow-wrap:break-word;box-decoration-break:clone;-webkit-box-decoration-break:clone}
:where(.ops-doc__prose) pre{font:400 11.5px/1.6 var(--ops-font-mono);background:var(--ops-line-soft);border-radius:4px;padding:9px 11px;margin-block-end:12px;overflow-x:auto}
:where(.ops-doc__prose) pre code{background:none;padding:0;font:inherit}
/* ops-fact / ops-factref — метки {{fact:…}}-токенов kb_portal-рендерера
   (волна 5: бывш. fact-inline/fact-ref, переименованы синхронно с serving) */
.ops-doc__prose .ops-fact--ok{border-block-end:1px dotted var(--ops-ok)}
.ops-doc__prose .ops-fact--warn{border-block-end:1px dotted var(--ops-warn);color:var(--ops-warn-x)}
.ops-doc__prose .ops-factref{font:500 10px var(--ops-font-mono);vertical-align:super;margin-inline-start:2px;color:var(--ops-mute);text-decoration:none}
.ops-doc__prose .ops-factref:hover{color:var(--ops-accent)}

/* ── волна 3: doc-таблицы (derived-сводные ref + markdown-матрицы; волна 5:
      md-tablewrap/md-table рендерера слиты в единый словарный класс) ───────── */
.ops-doc__tablewrap{margin-block:6px 16px;overflow-x:auto}
.ops-doc__table{border-collapse:collapse;width:100%;font-size:12.5px;line-height:1.55}
.ops-doc__table th{font:600 9.5px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);text-align:left;padding:5px 10px;border-block-end:1px solid var(--ops-line);vertical-align:bottom}
.ops-doc__table td{padding:5px 10px;border-block-end:1px solid var(--ops-line-soft);vertical-align:top}
.ops-doc__table :is(td,th):first-child{padding-inline-start:0}
.ops-doc__table td.cm{text-align:center;font-family:var(--ops-font-mono);color:var(--ops-ink)}
.ops-doc__table td.cl{max-width:560px;font-size:11.5px;color:var(--ops-mute)}
.ops-doc__prose .ops-doc__table code{font-size:11px;white-space:normal}

/* ── волна 3: поисковая строка reading-входов ─────────────────────────────── */
.ops-doc__find{margin-block:12px 4px}
.ops-doc__find input{width:100%;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-ink);font:500 12.5px var(--ops-font-mono);padding:8px 11px;min-height:36px;outline-offset:2px}
.ops-doc__find input:focus-visible{outline:2px solid var(--ops-accent)}
.ops-doc__find input::placeholder{color:var(--ops-mute)}

/* ── волна 3: «technical details» disclosure глав handbook ────────────────── */
.ops-doc__tech{margin-block:12px;border:1px solid var(--ops-line);border-radius:4px}
.ops-doc__tech summary{list-style:none;display:flex;align-items:baseline;gap:8px;padding:8px 11px;font:600 10px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ops-mute);cursor:pointer}
.ops-doc__tech summary::-webkit-details-marker{display:none}
.ops-doc__tech summary::before{content:"+";font-size:12px;line-height:1}
.ops-doc__tech[open] summary::before{content:"\2212"}
.ops-doc__tech summary:hover{color:var(--ops-ink)}
.ops-doc__tech[open] summary{color:var(--ops-ink);border-block-end:1px solid var(--ops-line-soft)}
.ops-doc__tech>div{padding:10px 12px 2px}

/* ── волна 3: hairline-указатель (kb-дерево, handbook-карточки, learn-модули) ── */
.ops-index{min-width:0}
.ops-index__row{padding-block:9px;border-block-end:1px solid var(--ops-line-soft)}
.ops-index__row:last-child{border-block-end:0}
.ops-index__row--warn{box-shadow:inset 2px 0 0 var(--ops-warn);padding-inline-start:10px;background:color-mix(in srgb,var(--ops-warn-t) 45%,transparent)}
/* Кусок 1b: ВЫБРАННАЯ строка указателя. Кромка акцентом, как активная дверь меню
   (.ops-side__nav a.active) — амбер --warn занят смыслом «требует внимания», и выбор
   не должен читаться тревогой. */
.ops-index__row--on{box-shadow:inset 2px 0 0 var(--ops-accent);padding-inline-start:10px;background:var(--ops-ok-t)}
.ops-index__head{display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap}
.ops-index__t{font:600 13px/1.4 var(--ops-font-sans);color:var(--ops-ink)}
a.ops-index__t:hover,.ops-index__t a:hover{color:var(--ops-accent)}
.ops-index__meta{font:400 10px var(--ops-font-mono);color:var(--ops-mute)}
.ops-index__meta a{color:var(--ops-accent)}
.ops-index__meta a:hover{text-decoration:underline}
.ops-index__sum{font-size:12px;line-height:1.6;color:var(--ops-text);max-width:80ch;margin-block:2px 0}

/* ── волна 3: practice/exam квиз learn ────────────────────────────────────── */
.ops-quiz{padding-block:12px;border-block-start:1px solid var(--ops-line-soft)}
.ops-quiz__kind{font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute)}
.ops-quiz__prompt{font-size:13.5px;line-height:1.6;color:var(--ops-ink);margin-block:4px 8px;max-width:70ch}
.ops-quiz label{display:flex;align-items:center;gap:9px;font-size:12.5px;padding-block:3px;cursor:pointer;min-height:24px}
.ops-quiz input[type=radio],.ops-quiz input[type=checkbox]{width:16px;height:16px;flex:none;accent-color:var(--ops-accent)}
.ops-quiz input[type=text]{border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-ink);font:500 12.5px var(--ops-font-mono);padding:5px 9px;min-height:30px;max-width:200px;outline-offset:2px}
.ops-quiz input[type=text]:focus-visible{outline:2px solid var(--ops-accent)}
.ops-quiz .sds{display:grid;grid-template-columns:1fr auto auto;gap:8px;align-items:center;padding-block:3px}
.ops-quiz .sds .phrase{font-size:12.5px;color:var(--ops-ink)}
.ops-quiz .sds label{font:600 10px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;padding-block:0}
.ops-quiz .ops-actions{padding:8px 0 0}
.ops-quiz__fb{margin-block-start:9px;border:1px solid var(--ops-line);border-radius:4px;padding:8px 10px;font-size:12.5px;line-height:1.6}
.ops-quiz__fb--ok{box-shadow:inset 2px 0 0 var(--ops-ok)}
.ops-quiz__fb--bad{box-shadow:inset 2px 0 0 var(--ops-warn)}
.ops-quiz__fb .st{display:block;font:600 10px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase}
.ops-quiz__fb--ok .st{color:var(--ops-ok-x)}
.ops-quiz__fb--bad .st{color:var(--ops-warn-x)}
.ops-quiz__fb p{font-size:inherit;line-height:inherit;margin-block:5px 0;max-width:70ch}

/* ── волна 3: карточка факта kb-статьи (значение из fact store, F.4/F.5) ───── */
.ops-fcard{border:1px solid var(--ops-line);border-radius:4px;padding:9px 11px;margin-block-end:10px}
.ops-fcard__head{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
.ops-fcard__head .n{font:400 10px var(--ops-font-mono);color:var(--ops-mute)}
.ops-fcard__head .key{font:500 11.5px var(--ops-font-mono);color:var(--ops-ink)}
.ops-fcard__head .ops-tag{margin-inline-start:auto}
.ops-fcard__val{font:700 15px var(--ops-font-sans);letter-spacing:-.01em;color:var(--ops-ink);margin-block-start:5px}
.ops-fcard__val.closed{color:var(--ops-mute);font-weight:500}
.ops-fcard__claim{font-size:12.5px;line-height:1.6;color:var(--ops-text);margin-block:3px 5px;max-width:80ch}
.ops-fcard__when{font:400 10px var(--ops-font-mono);color:var(--ops-mute)}
.ops-fcard__when b{font-weight:600;color:var(--ops-text)}
.ops-fcard__src{font:400 10px var(--ops-font-mono);color:var(--ops-mute);margin-block-start:5px;overflow-wrap:anywhere}
.ops-fcard__src a{color:var(--ops-accent);word-break:break-all}
.ops-fcard__quotes{list-style:none;margin-block-start:6px;border-inline-start:2px solid var(--ops-line);padding-inline-start:10px}
.ops-fcard__quotes li{font-size:12px;line-height:1.6;color:var(--ops-text);padding-block:2px}
.ops-fcard .ops-warncall{margin:7px 0}
.ops-fcard details{margin-block-start:7px}
.ops-fcard summary{font:600 10px var(--ops-font-mono);letter-spacing:.06em;color:var(--ops-mute);cursor:pointer}
.ops-fcard summary:hover{color:var(--ops-ink)}
.ops-fcard__hist{margin-block-start:7px}
.ops-fcard__hist>div{display:grid;grid-template-columns:150px 1fr 1.2fr;gap:2px 12px;padding-block:2px;font:400 10.5px/1.5 var(--ops-font-mono)}
.ops-fcard__hist .date{color:var(--ops-mute);white-space:nowrap}
.ops-fcard__hist .name{color:var(--ops-ink)}
.ops-fcard__hist .desc{color:var(--ops-mute)}

/* ── волна K1: suggest (kb typeahead) ────────────────────────────────────── */
.ops-suggest{border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);margin-block-start:4px;overflow:hidden}
.ops-suggest__row{display:flex;align-items:center;gap:8px;padding:5.5px 10px;min-height:28px;border-block-end:1px solid var(--ops-line-soft);font-size:12px;color:var(--ops-text);transition:background var(--ops-dur)}
.ops-suggest__row:last-child{border-block-end:0}
.ops-suggest__row:hover,.ops-suggest__row.on{background:var(--ops-line-soft);color:var(--ops-ink)}
.ops-suggest .t b{font-weight:600;color:var(--ops-ink)}
.ops-suggest .k{margin-inline-start:auto;font:400 10px var(--ops-font-mono);color:var(--ops-mute);white-space:nowrap}
.ops-suggest__grp{padding:4px 10px;font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute);background:var(--ops-line-soft);border-block-end:1px solid var(--ops-line-soft)}

/* ── волна K1: copy (вербатим-текст; .ok после резолва clipboard-промиса) ── */
.ops-copy{flex-shrink:0;font:600 10px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-text);padding:3px 9px;min-height:24px;transition:border-color var(--ops-dur),color var(--ops-dur)}
.ops-copy:hover{border-color:var(--ops-mute);color:var(--ops-ink)}
.ops-copy.ok{border-color:var(--ops-ok);color:var(--ops-ok-x)}

/* ── волна K1: say / don't-say (вербатим из fact store; без пересказа) ───── */
.ops-say{display:flex;align-items:flex-start;gap:9px;border:1px solid var(--ops-line);border-inline-start:2px solid var(--ops-ok);border-radius:4px;background:var(--ops-panel);padding:7px 10px;margin-block-start:6px}
.ops-say--dont{border-inline-start-color:var(--ops-bad)}
.ops-say__lab{flex-shrink:0;font:600 9.5px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-ok-x);padding-block-start:2.5px}
.ops-say--dont .ops-say__lab{color:var(--ops-bad-x)}
.ops-say__txt{flex:1;font-size:12.5px;line-height:1.55;color:var(--ops-text)}

/* ── волна K2: verd (5 вердикт-кнопок ДОСЛОВНО — фильтр/ветвление, не статус) ── */
.ops-verd{display:flex;gap:6px;flex-wrap:wrap}
.ops-verd a{border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);padding:5px 11px;min-height:24px;font:500 11.5px var(--ops-font-sans);color:var(--ops-text);transition:border-color var(--ops-dur),color var(--ops-dur)}
.ops-verd a:hover{border-color:var(--ops-mute);color:var(--ops-ink)}
.ops-verd a.on{background:var(--ops-ok-t);border-color:var(--ops-accent);color:var(--ops-ink);font-weight:600}

/* ── волна K2: split (split-view сценария handbook) ──────────────────────────
   Брейкпоинт 900px — свой (канон мокапа handbook-scenario): двух-панельная
   сетка ломается раньше общего 640; say-панель складывается ПЕРВОЙ (виза 4
   брейншторма — «что говорить» важнее превью на узком экране). */
.ops-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px;align-items:start}
@media (max-width:900px){
  .ops-split{grid-template-columns:1fr}
  .ops-split__left{order:2}
  .ops-split__right{order:1}
}
/* Кусок 1b: РЕЛЬСА-указатель (каталог писем) — слева узкий список предметов, справа
   один выбранный целиком. Отличие от базового split ДВА и оба намеренные: колонки не
   равны (список — указатель, а не половина смысла), и на узком экране первым идёт
   СПИСОК, а не правая панель (у handbook наоборот: там «что говорить» важнее превью,
   здесь без списка не выбрать предмет — виза Виктора 2026-07-25). */
.ops-split--rail{grid-template-columns:minmax(0,270px) minmax(0,1fr)}
@media (max-width:900px){
  .ops-split--rail{grid-template-columns:1fr}
  .ops-split--rail .ops-split__left{order:1}
  .ops-split--rail .ops-split__right{order:2}
}

/* ── волна K2: preview (окошко живого прод-превью фрагмента отчёта) ───────── */
.ops-preview{border:1px solid var(--ops-line);border-radius:4px;overflow:hidden;background:var(--ops-panel)}
.ops-preview__cap{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font:600 9.5px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute);padding:5px 9px;border-block-end:1px solid var(--ops-line)}
.ops-preview__frame{display:block;width:100%;min-block-size:320px;border:0;background:var(--ops-report-paper)}
/* Кусок 1b: рамка ПИСЬМА выше базовой — лист письма ≈930px, и на 320px он читался
   огрызком. clamp: не ниже 560px даже на коротком окне, дальше по высоте окна, потолок
   940px (выше листа расти незачем). Прокрутка — ВНУТРИ рамки, страница не тянется. */
.ops-preview__frame--letter{block-size:clamp(560px,74vh,940px)}

/* ── волна K2: sr (screen-reader-only; хвост K1 — замена inline-clip) ─────── */
.ops-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ── волна K3: symptom (вход runbook «What happened?») ────────────────────── */
.ops-symptom{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.ops-symptom a{display:block;min-height:52px;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);padding:11px 14px;transition:border-color var(--ops-dur)}
.ops-symptom a:hover{border-color:var(--ops-mute)}
.ops-symptom .t{display:block;font:600 13px var(--ops-font-sans);color:var(--ops-ink)}
.ops-symptom .s{display:block;font-size:11.5px;line-height:1.5;color:var(--ops-mute);margin-block-start:3px}

/* ── волна K3: inc + sev + pii (шапка инцидента; pii поверх severity, Т-3) ── */
.ops-inc{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.ops-inc .ref{font:600 12px var(--ops-font-mono);color:var(--ops-ink)}
.ops-inc .t{font:400 10.5px var(--ops-font-mono);color:var(--ops-mute)}
.ops-sev{font:700 10px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;border-radius:3px;padding:2.5px 7px}
.ops-sev--1{background:var(--ops-bad-t);color:var(--ops-bad-x)}
.ops-sev--2{background:var(--ops-warn-t);color:var(--ops-warn-x)}
.ops-sev--3{background:var(--ops-neu-t);color:var(--ops-neu-x)}
.ops-pii{font:700 10px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;border-radius:3px;padding:2.5px 7px;background:var(--ops-warn-t);color:var(--ops-warn-x)}
.ops-pii--confirmed{background:var(--ops-bad-t);color:var(--ops-bad-x)}

/* ── волна K3: step + cmd (один шаг на экран) ─────────────────────────────── */
.ops-step{border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);padding:12px 14px}
.ops-step__meta{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;font:600 10px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute)}
.ops-step__meta .n{color:var(--ops-ink)}
.ops-step__meta .next{margin-inline-start:auto;font-weight:400;text-transform:none;letter-spacing:.02em}
.ops-step__q{font:600 14px/1.45 var(--ops-font-sans);color:var(--ops-ink);margin-block-start:10px}
.ops-cmd{display:flex;align-items:stretch;gap:6px;margin-block-start:8px}
.ops-cmd .ops-pre{flex:1;margin-block-start:0;font-size:12.5px;line-height:1.55}
.ops-cmd .ops-copy{align-self:stretch;min-width:56px}
.ops-btn--big{font-size:13px;padding:10px 18px;min-height:44px}

/* ── волна K3: choice (крупная radio-карточка severity-экрана) ────────────── */
.ops-choice{display:flex;align-items:flex-start;gap:10px;min-height:52px;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);padding:11px 14px;margin-block-start:6px;cursor:pointer;transition:border-color var(--ops-dur)}
.ops-choice:hover{border-color:var(--ops-mute)}
.ops-choice:has(input:checked){border-color:var(--ops-accent);background:var(--ops-ok-t)}
.ops-choice input{margin-block-start:2px;accent-color:var(--ops-accent)}
.ops-choice .t{display:block;font:600 13px var(--ops-font-sans);color:var(--ops-ink)}
.ops-choice .s{display:block;font-size:11.5px;line-height:1.5;color:var(--ops-mute);margin-block-start:3px}

/* ── волна K4: review (шапка due-ритуала; честный счётчик, без streak) ────── */
.ops-review{display:flex;align-items:center;gap:10px;font:600 10px var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ops-mute)}
.ops-review .n{color:var(--ops-ink)}
.ops-review__bar{flex:1;height:3px;border-radius:2px;background:var(--ops-line-soft);overflow:hidden}
.ops-review__bar i{display:block;height:100%;background:var(--ops-accent)}

/* ── волна K4: focus (одноколоночная фокус-колонка ритуала; без TOC-рельса) ── */
.ops-focus{inline-size:100%;max-inline-size:720px;margin-inline:auto}

/* ── волна K4: path (дорога онбординга «ты здесь / что дальше») ───────────── */
.ops-path{list-style:none}
.ops-path__step{display:flex;align-items:center;gap:10px;padding:6px 2px;min-height:28px;border-block-end:1px solid var(--ops-line-soft);font-size:12px;color:var(--ops-text)}
.ops-path__step:last-child{border-block-end:0}
.ops-path__step .m{flex:none;width:16px;text-align:center;font:600 10px var(--ops-font-mono);color:var(--ops-mute)}
.ops-path__step .t{flex:1;min-width:0}
.ops-path__step .k{font:400 10px var(--ops-font-mono);color:var(--ops-mute);white-space:nowrap}
.ops-path__step--done .m{color:var(--ops-ok-x)}
.ops-path__step--done .t{color:var(--ops-mute)}
.ops-path__step--now{box-shadow:inset 2px 0 0 var(--ops-accent);padding-inline-start:10px;background:var(--ops-ok-t)}
.ops-path__step--now .t{font-weight:600;color:var(--ops-ink)}
.ops-path__step--next .t,.ops-path__step--next .m{color:var(--ops-mute)}

/* ── волна K5: top__suggest (дропдаун единого поиска консоли под топбаром;
      якорь = весь топбар (не инпут): на узких ширинах инпут сжимается до
      120px — дропдаун от него переполнял бы 390; z-index над контентом,
      ниже drawer'а 1024 нет нужды — дропдаун живёт при закрытом drawer) ──── */
.ops-top__suggest{position:absolute;inset-block-start:100%;inset-inline:14px;z-index:30}
.ops-top__suggest .ops-suggest{box-shadow:0 8px 22px -12px var(--ops-scrim-bg)}

/* ── волна 4: gate+ (периметр C7 — break-glass/verify/ошибки; канон =
      ops-perimeter мокап; формы в панели переиспользуют ops-fld/ops-actions/
      ops-btn, дубля форм-стилей нет) ──────────────────────────────────────── */
.ops-gatepage__panel--wide{width:min(640px,100%);text-align:start}
.ops-gatepage__panel .err{font:500 10.5px var(--ops-font-mono);line-height:1.6;color:var(--ops-bad-x)}
.ops-gatepage__code{background:var(--ops-canvas);border:1px solid var(--ops-line);border-radius:4px;padding:10px 12px;font:400 10.5px/1.6 var(--ops-font-mono);color:var(--ops-text);white-space:pre-wrap;word-break:break-all;text-align:start}
.ops-gatepage__panel ol{padding-inline-start:18px;text-align:start;font-size:12.5px;line-height:1.6;color:var(--ops-text);display:flex;flex-direction:column;gap:4px}
.ops-gatepage__panel .ops-fld{max-width:none;text-align:start}
.ops-gatepage__panel .ops-actions{padding:0;justify-content:center}
.ops-gatepage__panel--wide .ops-actions{justify-content:flex-start}
.ops-gatepage__panel h1{font:600 15px var(--ops-font-sans);color:var(--ops-ink)}
/* инлайн-code периметра (break-glass «challenge.txt»): пилюлю раньше давал
   публичный лист (civic-green протечка); с децуплингом — своя, на ops-токенах
   (Leon MAJOR волны 5) */
.ops-gatepage__panel code{font:500 10.5px var(--ops-font-mono);background:var(--ops-line-soft);border-radius:3px;padding:1px 4px}
.ops-btn--block{width:100%;justify-content:center}

/* ── волна 4: acc (details.ops-panel — секции-аккордеоны формы C6; канон =
      ops-form-tool мокап: summary без маркера, шеврон в __title) ──────────── */
details.ops-panel>summary{cursor:pointer;list-style:none}
details.ops-panel>summary::-webkit-details-marker{display:none}
details.ops-panel>summary .ops-panel__title::after{content:" \2039";display:inline-block;transform:rotate(-90deg);color:var(--ops-mute)}
details.ops-panel[open]>summary .ops-panel__title::after{transform:rotate(90deg)}
details.ops-panel:not([open])>summary{border-block-end:0}

/* ── волна 4: touch (планшет замерщика — 16px против авто-зума iOS,
      интерактивные цели ≥44px; вешается на контейнер PWA-формы) ───────────── */
/* label внутри radio-ряда .ops-check (Egress-канон) — сам touch-цель ≥44
   (радио 22×22 по словарю; кликабельная зона = label, фикс-раунд M2 волны 4) */
.ops-check label{display:flex;align-items:center;gap:8px;min-block-size:44px;cursor:pointer}
.ops-touch .ops-fld input,.ops-touch .ops-fld textarea{font-size:16px;min-height:44px}
.ops-touch .ops-btn{min-height:44px;padding:10px 16px;font-size:13px}
.ops-touch .ops-linkbtn{min-height:44px;display:inline-flex;align-items:center}
.ops-touch .ops-check{min-height:52px}

/* ── волна 4: unit (ряд «ввод + единица измерения» замерочных полей) ───────── */
.ops-unitrow{display:flex;gap:10px;align-items:center}
.ops-unitrow input{flex:1;min-width:0}
.ops-unitrow .unit{font:500 11px var(--ops-font-mono);color:var(--ops-mute);white-space:nowrap}

/* ── волна 4: syncbar (sticky сводка офлайн-очереди на site-visit
      поверхностях; кромка = состоянию — решение Виктора 07-24 (080 п.2):
      несинхрон есть → --attention (amber warn), нет → нейтральная линия;
      тонирование на --ops-* токенах = обе темы из коробки) ────────────────── */
.ops-syncbar{position:sticky;inset-block-end:0;z-index:20;display:flex;gap:12px;align-items:center;justify-content:space-between;margin-block-start:24px;padding:12px 16px;font:500 11.5px var(--ops-font-mono);color:var(--ops-text);background:var(--ops-panel);border:1px solid var(--ops-line);border-inline-start:3px solid var(--ops-line);border-radius:4px}
/* [hidden]-контракт компонентно (Leon M1 волны 4): на офлайн-мини queue баннер —
   прямой ребёнок body ВНЕ скоупов общего правила [hidden], а авторский display:flex
   перебивает UA-правило — без этой строки «All observations synced» висел бы вечно. */
.ops-syncbar[hidden]{display:none}
.ops-syncbar--attention{border-inline-start-color:var(--ops-warn-x)}
.ops-syncbar a{text-decoration:underline;text-underline-offset:2px}
.ops-offline-note{font:500 11.5px var(--ops-font-mono);color:var(--ops-text);border:1px dashed var(--ops-line);border-radius:4px;padding:10px 14px;margin-block-end:16px}

/* ── волна 4: offq (офлайн-мини-страница очереди — без shell/nav, офлайн-
      надёжность; строки рендерит JS из IndexedDB, сервер очередь не видит,
      #40; body-фон/марджин — общее body:has-правило слоя ops-reset, волна 5) ── */
.ops-offq{max-inline-size:640px;min-height:100vh;margin-inline:auto;padding:6vh 20px 64px;font-family:var(--ops-font-mono)}
.ops-offq__kicker{font:600 9.5px var(--ops-font-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ops-mute);margin-block-end:12px}
.ops-offq h1{font:600 18px var(--ops-font-sans);color:var(--ops-ink);margin-block-end:6px}
.ops-offq__who{font-size:10.5px;color:var(--ops-mute);margin-block-end:20px}
.ops-offq__lede{font-size:12px;line-height:1.7;margin-block-end:18px}
.ops-offq__status{font-size:11px;color:var(--ops-mute);margin-block-end:14px;min-block-size:1em}
.ops-offq__row{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center;background:var(--ops-panel);border:1px solid var(--ops-line);border-radius:4px;padding:11px 13px;margin-block-end:8px;font-size:11.5px}
.ops-offq__row>div{flex:1 1 100%}
.ops-offq__row--draft{background:none;border-style:dashed;color:var(--ops-mute)}
.ops-offq__empty{font-size:12px;color:var(--ops-mute)}
.ops-offq__act{font:600 9px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-text);border:1px solid var(--ops-line);border-radius:4px;padding:8px 10px;min-height:32px;cursor:pointer;text-decoration:none}
.ops-offq__act:hover{border-color:var(--ops-mute)}
.ops-offq__act--discard{color:var(--ops-warn-x);border-color:var(--ops-warn-x)}
.ops-offq__sync{inline-size:100%;margin-block-start:18px;padding:14px 18px;font:600 11px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-btn-ink);background:var(--ops-accent);border-radius:4px;min-height:44px}
.ops-offq__sync:disabled{opacity:.6;cursor:default}
.ops-offq__out{margin-block-start:26px}
.ops-offq__out button{font:600 10px var(--ops-font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ops-mute);border:1px solid var(--ops-line);border-radius:4px;padding:10px 13px;min-height:40px}
.ops-offq__out button:hover{color:var(--ops-text);border-color:var(--ops-mute)}
.ops-offq__foot{margin-block-start:24px;font-size:10.5px}
.ops-offq__foot a{color:var(--ops-mute);text-decoration:underline;text-underline-offset:2px}
.ops-offq__foot a:hover{color:var(--ops-text)}

/* ── волна 5: num — tabular/lining цифры (бывш. публичная утилита .num;
      после децуплинга консоль держит свою — шаблоны пишут ops-num) ─────────── */
.ops-num{font-feature-settings:'tnum' 1,'lnum' 1}

/* ── волна 5: mark — глиф-марка матрицы тиров вместо emoji ✅/❌ (080 п.4,
      NIT Prod): текст-глиф ✓/— живёт в шаблоне (доступное имя кнопки цело),
      класс красит за смысл; off — mute, не bad (hidden ≠ авария) ───────────── */
.ops-mark{font:600 12px var(--ops-font-mono)}
.ops-mark--on{color:var(--ops-ok-x)}
.ops-mark--off{color:var(--ops-mute)}

/* ── волна 5: diff--state (081 п.2, решение Prod 07-24): STATE-слова конфирма
      (on/off, visible/hidden) — чернилами; зелёный .new остаётся только
      числовым значениям (цены): зелёное «off» читалось как «хорошо» ────────── */
.ops-diff--state .new{color:var(--ops-ink)}

/* ── волна 5: скролл-тени краёв широких doc-таблиц (082 п.2б): на 390 обрезанная
      колонка получает видимую подсказку скролла. Чистый CSS: «шторки» цвета
      канвы скроллятся с контентом (attachment local) и закрывают тени у краёв;
      сами тени неподвижны; цвета — color-mix токенов, обе темы из коробки ──── */
/* Тот же рецепт получает и обычный скролл-контейнер таблицы (ре-гейт Prod, MINOR-3):
   на 390 обрезанная колонка в один символ читалась как дефект рендера, а не как
   приглашение доскроллить. Одно правило на оба носителя — второй копии теней нет.
   Шторки берут цвет ПАНЕЛИ: `.ops-scroll` живёт внутри панели, а `.ops-doc__tablewrap` —
   на канве, поэтому фон у каждого свой, а геометрия общая. */
.ops-doc__tablewrap,.ops-scroll{
  background:
    linear-gradient(to right,var(--ops-canvas) 34%,transparent) left/40px 100%,
    linear-gradient(to left,var(--ops-canvas) 34%,transparent) right/40px 100%,
    radial-gradient(farthest-side at 0 50%,color-mix(in srgb,var(--ops-ink) 30%,transparent),transparent) left/16px 100%,
    radial-gradient(farthest-side at 100% 50%,color-mix(in srgb,var(--ops-ink) 30%,transparent),transparent) right/16px 100%;
  background-repeat:no-repeat;
  background-attachment:local,local,scroll,scroll;
}
.ops-scroll{
  background-image:
    linear-gradient(to right,var(--ops-panel) 34%,transparent),
    linear-gradient(to left,var(--ops-panel) 34%,transparent),
    radial-gradient(farthest-side at 0 50%,color-mix(in srgb,var(--ops-ink) 30%,transparent),transparent),
    radial-gradient(farthest-side at 100% 50%,color-mix(in srgb,var(--ops-ink) 30%,transparent),transparent);
}

/* ── кусок 6: run — чеклист стадий живого прогона (/ops/generate). Стадии
      РЕАЛЬНЫЕ (job_store.StageTracker), не таймер: --now = текущая, --done =
      пройденная, --fail = упавшая. Прогресс-полосы нет намеренно — доля
      выполненного нам неизвестна, а нарисованная полоса врала бы ───────────── */
.ops-run{display:flex;flex-direction:column;gap:2px;margin-block-start:6px}
.ops-run__stage{display:flex;align-items:baseline;gap:8px;padding:3px 0;min-height:22px;border-block-end:1px solid var(--ops-line-soft)}
.ops-run__stage:last-child{border-block-end:0}
.ops-run__stage .t{font-size:12px;color:var(--ops-mute)}
.ops-run__stage .s{font:400 10px var(--ops-font-mono);color:var(--ops-mute);margin-inline-start:auto;white-space:nowrap}
.ops-run__stage--done .t{color:var(--ops-text)}
.ops-run__stage--now .t{color:var(--ops-ink);font-weight:600}
.ops-run__stage--now .s{color:var(--ops-accent)}
.ops-run__stage--fail .t,.ops-run__stage--fail .s{color:var(--ops-bad-x)}

/* ── кусок 8: gen — стенд прогонов перевёрнут. Ввод компактной ПОЛОСОЙ сверху,
      лист отчёта занимает остаток экрана.
      Почему полоса, а не боковая колонка: результат — лист отчёта в рамке, у него
      книжная пропорция и он выигрывает от ширины; боковая колонка отрезала бы от листа
      280-320px НАВСЕГДА, тогда как полей ввода пять-шесть коротких и они помещаются в
      строку. Поля не растягиваются (max-inline-size): без потолка шесть коротких полей
      размазываются по 1280 и полоса перестаёт читаться как полоса ───────────────── */
.ops-gen__bar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:8px 11px;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel)}
.ops-gen__bar .ops-fld{max-inline-size:230px;flex:1 1 160px}
/* Адрес — главное поле полосы, ему шире; потолок остаётся (иначе на 1280 он съедает строку). */
.ops-gen__bar .ops-fld--wide{max-inline-size:420px;flex:2 1 260px}
.ops-gen__bar .ops-actions{margin-inline-start:auto}
/* Окно результата: рамка листа тянется на всю оставшуюся высоту. `min-block-size:0` —
   то же звено цепочки .ops-fill; без него iframe считает своей высотой атрибут, а не
   область, и лист снова оказывается «где-то ниже». */
/* `min-block-size:0` (звено цепочки .ops-fill) означает «можешь сжаться до нуля» — и на
   невысоком экране раскрытая соседняя панель сжимает именно окно листа, до нуля буквально
   (найдено на приёмке, вьюпорт 800px). Нижний порог возвращает листу право на место:
   ниже 320px рамка перестаёт быть рамкой листа. В узкой ветке свой порог (70vh) стоит там,
   где полноэкранный режим уже снят. */
.ops-gen__result{display:flex;flex-direction:column;min-block-size:320px;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);overflow:hidden}
.ops-gen__result>iframe{flex:1 1 auto;inline-size:100%;block-size:100%;min-block-size:0;border:0;background:var(--ops-canvas)}
@media (max-width:820px){
  .ops-gen__bar .ops-fld,.ops-gen__bar .ops-fld--wide{max-inline-size:none;flex:1 1 100%}
  /* Без полноэкранного режима рамке нужна собственная высота, иначе она схлопнется. */
  .ops-gen__result{min-block-size:70vh}
}

/* ── кусок 9: gen — меню ВЕРТИКАЛЬНОЙ ПОЛОСОЙ СЛЕВА, лист во всю высоту справа
      (указание Виктора 2026-07-27).

      ЭТО ОСОЗНАННЫЙ РАЗВОРОТ РЕШЕНИЯ КУСКА 8, и довод против боковой колонки был
      верным: она отрезает у листа 300px НАВСЕГДА. Отвечает на него не вкус, а
      сворачиваемость — свёрнутая полоса отдаёт листу всю ширину, и состояние
      переживает перезагрузку (атрибут data-ops-rail на корне, как тема портала).
      «Навсегда» перестаёт быть правдой, и довод снимается по существу.

      ПОЧЕМУ ПРЕЖНЯЯ ПОЧИНКА ВЫСОТЫ НЕ СРАБОТАЛА (замер этой сессии, не догадка):
      правило «растёт ровно один блок» (строки 470-476) раздаёт рост ВНУТРИ .ops-fill,
      а на стенде у .ops-fill РОВНО ОДИН ребёнок — само окно листа. Заметки и
      раскрывашки лежат этажом выше, соседями в .wrap, и лист забирает то, что от
      них осталось. Лечение било мимо уровня; лист так и получал 11-18% окна.
      Здесь уровень исправлен: всё меню уезжает ВНУТРЬ полосы, и в .wrap рядом с
      листом не остаётся ничего, кроме заголовка страницы.

      Полоса несёт СВОЙ скролл: раскрытая панель журнала прогонов иначе растянула бы
      строку грида и снова отняла бы высоту у листа — ровно та же болезнь, которой
      болела панель участка на карте (кусок 8, строки 1284-1290). ─────────────── */
/* `grid-template-rows` задан ЯВНО, а не оставлен на `auto` (гейт Leon куска 9, 🟡-11).
   Иначе высота листа держалась бы на незаписанном допущении: строка `auto` не рвёт
   контейнер только потому, что у полосы есть `overflow-y:auto` и оттого автоминимум 0.
   Снимет кто-нибудь скролл ради выпадающего списка — строка начнёт меряться по
   содержимому полосы, лист снова отдаст высоту, и НИ ОДИН тест не покраснеет, потому
   что вложенность не изменится. Это ровно тот механизм, которым кусок 8 оказался
   ложно-зелёным, этажом ниже. Теперь высота — свойство раскладки, а не побочный
   эффект чужого `overflow`. */
.ops-gen{display:grid;grid-template-columns:var(--ops-gen-rail) minmax(0,1fr);grid-template-rows:minmax(0,1fr);gap:8px;min-block-size:0}
/* Полоса скроллится внутри себя и НЕ двигает высоту грид-строки. `overscroll-behavior`
   не даёт докрученной до края полосе утащить за собой страницу под ней.
   `container-type:inline-size` — КОРЕНЬ, а не отделка (гейт Prod куска 9, MAJOR-3/4).
   Вся узкая форма консоли (`.ops-cols`, служебные колонки таблиц, потолки полей) стоит
   на ОКОННЫХ запросах 1024/640. Полоса шириной 300px существует при ЛЮБОЙ ширине окна:
   на мониторе 1440 ни один оконный порог не срабатывает, и внутри полосы панель подбора
   рисуется десктопной сеткой 1.4fr/1fr/1fr — колонки ≈106/76/76px, треть ширины пустая,
   кнопка не помещается в свой трек; журнал прогонов в девять колонок уезжает за край.
   Компонент обязан спрашивать СВОЮ ширину, а не ширину окна (инвариант #44 §4).
   Прецедент в этом же файле — `.ops-map__wrap`, заведён после того, как та же ошибка
   съела холст карты на 390. */
.ops-gen__rail{container-type:inline-size;container-name:ops-rail;display:flex;flex-direction:column;gap:8px;min-block-size:0;min-inline-size:0;overflow-y:auto;overscroll-behavior:contain;padding-inline-end:4px}
/* Узкая форма ВНУТРИ полосы: те же правила, что оконные 1024/640, но по ширине
   компонента. Порог 420px — та же граница, на которой две колонки перестают читаться;
   полоса в 300px проходит его всегда, широкая полоса (если её когда-нибудь расширят
   токеном) получит прежнюю сетку без правки этого блока. */
/* Контейнер ИМЕНОВАН, и это не педантизм (гейт Prod куска 9, ре-гейт NIT-a): безымянный
   `@container` адресуется БЛИЖАЙШЕМУ контейнеру, а на карте ближайший — `.ops-map__wrap`
   (он тоже `inline-size`), и на 390 он ~362px. Правило, написанное для полосы стенда,
   молча правило бы вторую поверхность: `max-inline-size:none` снимал бы там потолок
   поля, объявленный позже и потому побеждающий порядком. Имя запирает правило за своей
   поверхностью. */
@container ops-rail (max-width:420px){
  .ops-cols{grid-template-columns:1fr}
  .ops-table td.aux,.ops-table th.aux{display:none}
  .ops-fld,.ops-fld--wide{max-inline-size:none}
}
/* Всё, что прячется при сворачивании. Кнопка-язычок остаётся снаружи этого блока —
   иначе свёрнутую полосу было бы нечем развернуть. */
.ops-gen__body{display:flex;flex-direction:column;gap:8px;min-block-size:0}
.ops-gen__railtoggle{display:flex;align-items:center;justify-content:center;gap:6px;inline-size:100%;min-block-size:26px;padding:4px 6px;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel);color:var(--ops-mute);font:500 9.5px/1 var(--ops-font-mono);letter-spacing:.08em;text-transform:uppercase;transition:color var(--ops-dur) linear,border-color var(--ops-dur) linear}
.ops-gen__railtoggle:hover{color:var(--ops-ink);border-color:var(--ops-mute)}
/* Свёрнутое состояние: колонка ужимается до язычка, лист забирает всю ширину.
   Состояние живёт атрибутом на КОРНЕ, а не классом на компоненте, — его ставит
   синхронный скрипт в <head> до первой отрисовки, иначе полоса моргнёт раскрытой
   у того, кто её свернул (тот же приём, что у темы портала). */
html[data-ops-rail="collapsed"] .ops-gen{grid-template-columns:auto minmax(0,1fr)}
html[data-ops-rail="collapsed"] .ops-gen__body{display:none}
html[data-ops-rail="collapsed"] .ops-gen__rail{inline-size:var(--ops-gen-rail-min);overflow:visible}
/* В свёрнутом виде подпись язычка уехала бы за ширину язычка — остаётся один глиф. */
html[data-ops-rail="collapsed"] .ops-gen__railtoggle .t{display:none}
/* Внутри полосы поля идут В СТОЛБИК: потолки ширины куска 8 писались для строки и
   в 300px превратили бы каждое поле в обрезок.
   `flex-wrap:nowrap` здесь ОБЯЗАТЕЛЕН, и это не вкус: колонка С ПЕРЕНОСОМ меряет свою
   поперечную ось по max-content самого широкого ребёнка. Список видов прогона
   («Free report — customer pays nothing — $0.00») даёт 300px при доступных 257px,
   полоса получает горизонтальную прокрутку (её `overflow-x` вычисляется в auto из-за
   `overflow-y:auto`), и поля выглядят обрезанными. Замер на живом стенде: scrollWidth
   полосы 312→285 = clientWidth, поля 300→257px. */
.ops-gen__rail .ops-gen__bar{flex-direction:column;flex-wrap:nowrap;align-items:stretch}
.ops-gen__rail .ops-gen__bar .ops-fld,.ops-gen__rail .ops-gen__bar .ops-fld--wide{max-inline-size:none;flex:0 0 auto}
.ops-gen__rail .ops-gen__bar .ops-actions{margin-inline-start:0}
@media (max-width:820px){
  /* Полноэкранного режима здесь уже нет (строки 479-482): полоса возвращается
     НАВЕРХ обычным потоком, лист под ней. Боковая колонка в 300px на телефоне
     оставила бы листу ширину, на которой лист не читается. */
  .ops-gen{display:flex;flex-direction:column}
  .ops-gen__rail{overflow-y:visible;inline-size:auto}
  html[data-ops-rail="collapsed"] .ops-gen__rail{inline-size:auto}
}

/* ── кусок 7: map — холст + боковая панель участка. Раскладка на @container
      (компонент, не страница — инвариант #44 §4): панель уходит вниз, когда
      тесно САМОЙ раскладке, а не когда узок экран ──────────────────────────── */
/* Контейнер объявлен на ОБЁРТКЕ, а не на самой раскладке (гейт Prod куска 6, MAJOR-5):
   `@container` спрашивает ПРЕДКА, и `.ops-map` сам себе предком не является — правило
   смены колонок не срабатывало никогда, а срабатывало только правило для потомка. Итог
   в бою: на 390 холст сжимался в полоску ~48px (320px панели вычитались из ширины
   экрана), кнопки зума висели на полоске, карта была нечитаема — и машинный оракул
   перелива этого не видел, потому что у холста `overflow:hidden`. */
.ops-map__wrap{container-type:inline-size}
/* Кусок 8: карта занимает ОСТАТОК ЭКРАНА, а не 60vh с серым полем под ним, и клик по
   участку её больше не дёргает.
   ПРИЧИНА ДЁРГАНЬЯ (установлена разведкой, не догадка): холст и боковая колонка стояли в
   ОДНОЙ строке грида с `align-items:stretch`. После клика панель участка становится выше —
   строка растёт — растёт и холст, а MapLibre авто-ресайзится по контейнеру. Лечится
   раскладкой, а не запретом ресайза: строка холста фиксирована (`minmax(0,1fr)`), панель
   скроллится ВНУТРИ своей области и высоту строки больше не двигает.
   Области вместо колонок: полоса слоёв сверху во всю ширину, легенда строкой под холстом,
   правая колонка отдана панели участка целиком. */
.ops-map{
  display:grid;
  grid-template-columns:1fr 320px;
  grid-template-rows:auto minmax(0,1fr) auto;
  /* Легенда идёт ВО ВСЮ ШИРИНУ, а панель занимает ровно строку холста. Прежняя раскладка
     («foot side») спанила панель на две строки, и её содержимое всё ещё добавляло высоту
     гриду — замер на приёмке: клик по участку менял высоту холста с 439 на 454 px, то есть
     дёрганье уменьшилось, но не исчезло. Панель, ограниченная одной строкой `minmax(0,1fr)`,
     высоту грида не двигает вовсе. */
  grid-template-areas:"bar bar" "canvas side" "foot foot";
  gap:8px;
  /* Высота приходит от .ops-fill цепочкой; min-block-size — ЗАПАСНАЯ НОГА на случай, если
     страница не объявила ops-shell--fill: без неё карта схлопнулась бы в ноль молча. */
  block-size:100%;
  min-block-size:60vh;
}
.ops-map__bar{grid-area:bar;display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;padding:6px 11px;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel)}
.ops-map__canvas{grid-area:canvas;position:relative;min-block-size:0;border:1px solid var(--ops-line);border-radius:4px;overflow:hidden;background:var(--ops-panel)}
.ops-map__foot{grid-area:foot;display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;padding:6px 11px;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-panel)}
/* Тревожная строка подвала занимает СВОЙ ряд (гейт Prod куска 9, NIT-6): подвал —
   переносящийся флекс, и сообщение «часть слоёв не отрисовалась» иначе встаёт в один
   ряд с квадратиками легенды, где читается как ещё одна подпись легенды. Сообщение о
   неполноте карты не имеет права выглядеть частью того, о неполноте чего оно говорит. */
.ops-map__foot>.ops-note--warn{flex-basis:100%}
/* Панель участка несёт СВОЙ скролл: `min-block-size:0` обязателен — без него grid-элемент
   считает минимальной высотой высоту содержимого, `overflow` не включается, и панель снова
   растянет строку (тот же дефект, вид сбоку). */
/* `block-size:0` + `min-block-size:100%` — штатная идиома «скроллиться ВНУТРИ трека, не
   задавая его высоту». Без неё панель всё равно двигала грид: замер на приёмке дал остаточные
   15 px (холст 589 → 604 при клике), потому что грид считался авто-высотой и трек
   `minmax(0,1fr)` брал высоту от содержимого панели, несмотря на overflow и min-block-size:0. */
.ops-map__side{grid-area:side;display:flex;flex-direction:column;gap:8px;min-inline-size:0;block-size:0;min-block-size:100%;overflow-y:auto}
/* Фактовая строка внутри полосы уже стоит в её отступе — свой горизонтальный отступ
   сдвигал бы подпись легенды относительно ряда квадратиков, к которому она относится. */
/* Компоненты, рассчитанные на панель, стоят здесь в ОДНУ СТРОКУ — им нужен горизонтальный
   режим, иначе полоса и легенда съедают высоту у холста (замер на приёмке: 44% высоты
   карты). Отступы и разделители панели в строке лишние: у переключателя нижняя граница
   рисует подчёркивание под каждым слоем, у поля остаётся вертикальный отступ, а четыре
   фактовые строки дают 64 px чистого воздуха. */
.ops-map__bar .ops-note,.ops-map__foot .ops-note{padding-inline:0;padding-block:0}
.ops-map__bar .ops-check{border-block-end:0;min-height:32px}
.ops-map__bar .ops-fld{margin-block-end:0}
/* Список районов сам по себе шире экрана телефона: его минимальная ширина равна самому
   длинному имени («R-2M — Residential Low Density Multiple Dwelling»). Колонке грида уже
   разрешено сжиматься (minmax(0,1fr)), но поле внутри неё этим не связано — замер на
   приёмке показал остаточный перелив 394 px в вьюпорте 390. Имя района не теряется:
   собственный список остаётся полным, усекается только показ закрытого поля. */
.ops-map__bar .ops-fld select{max-inline-size:100%;min-inline-size:0}
/* Строке легенды запрещён перенос — это верно для ряда квадратиков с одним словом, но не
   для строки-объяснения выключенного слоя («No rule on record — we have no suite rule for
   this district»): на 390 она выходит за экран целиком (замер на приёмке: 415 px при
   вьюпорте 390). Перенос разрешается только там, где строка длинная по смыслу — в подвале
   карты; ряд квадратиков вердиктов рядом остаётся неразрывным. */
.ops-map__foot .ops-legend__row{white-space:normal}
/* Узко — раскладка разворачивается в один столбец и ПЕРЕСТАЁТ быть полноэкранной: на 390
   панель участка в 200px-окошке со своим скроллом нечитаема, там правильнее обычная
   прокрутка страницы. Порядок: полоса → холст → легенда → панель. */
@container (max-width:820px){
  /* minmax(0,1fr), а не 1fr: автоминимум колонки равен min-content, и список районов
     (самое длинное имя — «R-2M — Residential Low Density Multiple Dwelling») распирал
     раскладку за экран — замер на приёмке: 413 px в вьюпорте 390. */
  .ops-map{grid-template-columns:minmax(0,1fr);grid-template-areas:"bar" "canvas" "foot" "side";grid-template-rows:auto auto auto auto;block-size:auto}
  .ops-map__canvas{min-block-size:46vh}
  .ops-map__side{overflow-y:visible;min-block-size:auto}
}
/* Запасная нога на случай, если обёртка потеряется: раскладка обязана складываться и по
   ширине ОКНА. Два правила об одном факте — осознанная избыточность ровно там, где отказ
   молчалив (контейнерный запрос без контейнера просто не применяется, без ошибки). */
@media (max-width:820px){
  /* minmax(0,1fr), а не 1fr: автоминимум колонки равен min-content, и список районов
     (самое длинное имя — «R-2M — Residential Low Density Multiple Dwelling») распирал
     раскладку за экран — замер на приёмке: 413 px в вьюпорте 390. */
  .ops-map{grid-template-columns:minmax(0,1fr);grid-template-areas:"bar" "canvas" "foot" "side";grid-template-rows:auto auto auto auto;block-size:auto}
  .ops-map__canvas{min-block-size:46vh}
  .ops-map__side{overflow-y:visible;min-block-size:auto}
}

/* ── кусок 7: legend — легенда слоя вердиктов. Пять слов ДОСЛОВНО (#22); шестая
      строка «not computed» обязана отличаться от них не только цветом, но и
      контуром: незнание ≠ результат ───────────────────────────────────────── */
.ops-legend{display:flex;flex-wrap:wrap;gap:4px 12px}
.ops-legend__row{display:inline-flex;align-items:center;gap:6px;font:400 10.5px var(--ops-font-mono);color:var(--ops-mute);white-space:nowrap}
.ops-legend__row .sw{inline-size:11px;block-size:11px;border-radius:2px;border:1px solid var(--ops-map-line);flex:none}

/* ── кусок 7: fold — раскрывашка секции панели участка. Содержимое приходит ОДНИМ
      ответом (виза 07-26): свёрнутость это состояние показа, а не второй запрос,
      иначе оператор не отличит «факта нет» от «не догрузилось» ─────────────── */
/* Шеврон вместо маркера + отступ панели (ре-гейт Prod, MAJOR-2). Под `display:flex`
   браузер маркер не рисует вовсе, поэтому правило `::marker` было мёртвым: после того
   как счётчик строк справедливо ушёл (M-10), у нейтральной секции не осталось НИ ОДНОГО
   признака раскрываемости — голый заголовок читается как «здесь пусто», то есть ломается
   ровно тот контракт, ради которого содержимое приходит одним ответом. Идиома взята
   готовой у аккордеона панели выше (маркер выключен явно, шеврон в `::after`, поворот по
   [open]) — второго рецепта раскрывашки в системе не заводим. Горизонтальный отступ 11px
   равен отступу строк фактов: до правки заголовок стоял левее данных, а на 390 упирался
   в саму рамку. */
.ops-fold{border-block-start:1px solid var(--ops-line-soft)}
.ops-fold summary{display:flex;align-items:center;gap:7px;padding:6px 11px;min-height:28px;cursor:pointer;list-style:none;font:600 11px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ops-mute)}
.ops-fold summary::-webkit-details-marker{display:none}
.ops-fold summary::after{content:"\2039";display:inline-block;transform:rotate(-90deg);color:var(--ops-mute);margin-inline-start:auto}
.ops-fold[open] summary::after{transform:rotate(90deg)}
.ops-fold summary:hover{color:var(--ops-ink)}
.ops-fold .n{font-weight:400;color:var(--ops-mute);margin-inline-start:auto}
/* Сигнал секции и шеврон уезжают вправо ВМЕСТЕ: `auto`-отступ достаётся первому из них
   (тегу), второму пространства уже не остаётся — заголовок слева, «нужно внимание ›»
   справа. Отдельного правила на их пару не нужно. */
.ops-fold summary .ops-tag{margin-inline-start:auto}
.ops-fold[open] summary{color:var(--ops-ink)}

/* ── кусок 7: shot — спутник / вид с улицы в панели участка. Картинка приходит
      ТОЛЬКО по явному нажатию: каждый показ = платный вызов провайдера, и подпись
      обязана нести и атрибуцию, и это предупреждение ───────────────────────── */
.ops-shot{margin-block-start:8px}
.ops-shot img{display:block;inline-size:100%;block-size:auto;border:1px solid var(--ops-line);border-radius:4px;background:var(--ops-line-soft)}
.ops-shot__cap{font:400 10px var(--ops-font-mono);color:var(--ops-mute);margin-block-start:4px}

/* ── Раскладка витрины ресурсов (/ops/infra): СЕТКА ДОРОЖЕК ────────────────────
      Правка 2026-07-31 (жалоба Виктора «страница растянута на всю ширину и не
      читается», снимок на 2560). До неё панели были блоками во всю ширину окна, и
      на широком мониторе ломалось всё сразу: строка фактов растаскивала подпись и
      её значение на две трети экрана, картинка со своим кадром вставала по центру
      пустой полосы, а страница вытягивалась на шесть тысяч пикселей — семь
      карточек сервисов шли одна под другой.
      Мера — ДОРОЖКА (--ops-lane), и панель занимает целое число дорожек. Это и
      есть «блоки фиксированной ширины»: ширину задаёт сетка, а не окно.
      auto-fit сам решает, сколько дорожек влезло, поэтому точек перелома у
      раскладки нет ни одной — на 2560 выходит четыре колонки, на 1280 две, на
      телефоне одна, и ни одно из этих чисел не записано в правиле.
      min() внутри minmax обязателен: без него дорожка в 480px осталась бы 480px и
      на экране шириной 360, то есть страница потянула бы горизонтальную прокрутку
      именно там, где её терпят меньше всего. ────────────────────────────────── */
.ops-lane{container-type:inline-size;container-name:ops-lane;display:flex;flex-direction:column;gap:14px}
/* ── ЭТАЖИ (правка 2026-08-01). Страница читается сверху вниз четырьмя вопросами:
      видим ли мы вообще и есть ли запас · что с каждым сервисом · как оно шло во
      времени · что записано поимённо. Подпись этажа несёт волосяную линию во всю
      ширину — тот же приём, что у section-head сайта: линия отделяет этажи там,
      где раньше их разделяла только случайная разница высот. ─────────────────── */
.ops-lane__floor{display:flex;align-items:center;gap:10px;padding-block-end:2px}
.ops-lane__floor::after{content:"";flex:1;height:1px;background:var(--ops-line)}
.ops-lane__floor__t{font:600 9.5px var(--ops-font-mono);letter-spacing:.16em;
  text-transform:uppercase;color:var(--ops-mute);white-space:nowrap}
/* УЗКОЕ ПАКУЕТСЯ КОЛОНКАМИ, А НЕ СТРОКАМИ. В сетке строка равняется по самой
   высокой панели, поэтому короткая таблица подписок рядом с карточкой о двух
   графиках оставляла под собой четыреста пикселей фона. Колоночная упаковка
   раскладывает панели ПОТОКОМ: следующая встаёт сразу под предыдущей, и высота
   соседней колонки на это не влияет вовсе — дыра не может возникнуть в принципе,
   сколько бы графиков ни было у карточки и сколько бы строк ни пришло в таблицу.
   Мера колонки — та же дорожка, что у сетки: этажи обязаны стоять на одной мере,
   иначе край панели одного этажа не совпадёт с краем другого. */
.ops-lane__pack{columns:var(--ops-lane) auto;column-gap:8px}
.ops-lane__pack>.ops-panel{break-inside:avoid;margin-block-end:8px}
/* ПАНЕЛЬ ЗАНИМАЕТ РОВНО СВОЮ ВЫСОТУ (правка 2026-08-01 по замечанию Виктора
   «слишком много пустого места — оно никогда ничем не заполнится»).
   Правка 07-31 растягивала панели строки до общей высоты, чтобы убрать дыры между
   рамками. Обмен оказался плохим: пустота никуда не делась, а переехала ВНУТРЬ
   рамки, и панель вердиктов — тонкая полоска в 10 px — раздувалась до полутысячи
   пикселей белого поля, которое нечем заполнить никогда. Пустой фон между рамками
   читается как воздух раскладки, пустая рамка — как сломанный блок.
   Дыры при этом закрыты не выравниванием, а РАЗМЕРОМ: кадр графика вдвое ниже
   прежнего (infra_chart._H), поэтому разброс высот в строке перестал быть
   двукратным. */
.ops-lane__grid{display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(var(--ops-lane),100%),1fr));
  gap:8px;align-items:start}
/* Панель разворота занимает две дорожки — но ТОЛЬКО там, где двум дорожкам есть
   где встать. Без условия `span 2` при единственной колонке не клампится, а
   создаёт неявную вторую: строка становится шире контейнера, и узкий экран
   получает горизонтальную прокрутку. Порог — ровно две дорожки плюс зазор между
   ними, то есть та ширина, при которой вторая колонка гарантированно есть.
   Спрашивается ширина ОБЁРТКИ, а не окна: область содержимого зависит ещё и от
   того, развёрнуто ли меню консоли. */
.ops-lane__wide{grid-column:auto}
@container ops-lane (min-width:968px){
  .ops-lane__wide{grid-column:span 2}
}
/* Запасное правило для браузера без @container: та же мера, пересчитанная в
   ширину окна (полоса меню 210 + поля области содержимого 28). Ставится тем же
   значением, поэтому с запросом контейнера не спорит. */
@supports not (container-type:inline-size){
  @media (min-width:1210px){
    .ops-lane__wide{grid-column:span 2}
  }
}

/* ── Графики витрины ресурсов (/ops/infra, инварианты #40/#44).
      Словарь закрыт: имена ниже == CLASS_VOCABULARY в
      pipeline/serving/infra_chart.py, сверку в обе стороны держит
      tests/serving/test_infra_chart.py (имя без правила и правило без имени
      одинаково красные). Цвета берутся из токенов --ops-*, поэтому тёмная тема
      достаётся без второй таблицы правил.
      Статусных цветов здесь НЕТ намеренно: порога «много» у нас не существует,
      а покрашенная полоса читалась бы как вердикт. ─────────────────────────── */
/* Картинка занимает свою дорожку целиком, НО НЕ ШИРЕ СВОЕГО КАДРА. Оба правила
   нужны, и каждое закрывает свою беду. Без первого картинка стояла бы в
   натуральную величину и на узкой дорожке вылезала бы за панель. Без второго
   она растягивалась бы за кадр — а поскольку у svg отношение сторон держит
   preserveAspectRatio, растяжение по ширине браузер компенсирует масштабом по
   МЕНЬШЕЙ стороне: рисунок остаётся своей величины и встаёт ПО ЦЕНТРУ пустой
   полосы. Ровно это и было на боевом снимке 2026-07-31: подпись ряда прижата
   влево, сам ряд — посередине, между ними пустота в половину экрана.
   Кадр называет сама разметка (--card / --wide, _FRAME_CLASS генератора):
   лист не может вывести его из содержимого картинки. */
.ops-ic{display:block;overflow:visible;inline-size:100%}
.ops-ic--card{max-inline-size:var(--ops-frame-card)}
.ops-ic--wide{max-inline-size:var(--ops-frame-wide)}
.ops-ic-axis{stroke:var(--ops-line);stroke-width:1}
/* stroke-linecap:round — НЕ украшение: одиночный замер между дырами рисуется
   нулевым отрезком, и только круглый конец штриха делает его видимой точкой.
   С квадратным/плоским концом такой замер исчез бы с графика молча. */
/* fill:none обязателен: заливка пути по умолчанию ЧЁРНАЯ, и без этого правила
   ряд превратился бы в залитую кляксу. Живёт здесь, а не атрибутом в
   генераторе — генератор отдаёт структуру и числа, вид задаёт лист (#44 §1). */
.ops-ic-line{fill:none;stroke:var(--ops-ink);stroke-width:1.2;stroke-linejoin:round;stroke-linecap:round}
/* Полоса диапазона группы: между минимумом и максимумом интервала. Нейтральный
   тон И ТОЛЬКО ОН — величина статусом не является ни при каком значении, порога
   «много» у страницы нет, и крашеная полоса читалась бы как вердикт. Контура нет
   намеренно: обведённая полоса спорит по весу с линией середины, а главное на
   графике — именно середина. */
.ops-ic-band{fill:var(--ops-neu-t);stroke:none}
.ops-ic-bar{fill:var(--ops-neu)}
.ops-ic-track{fill:var(--ops-neu-t)}
.ops-ic-fill{fill:var(--ops-mute)}
/* Скругление концов полосы — то же самое: свойство вида, а не содержимого
   рисунка. SVG-геометрия задаётся из CSS, атрибута rx в генераторе нет. */
.ops-ic-track,.ops-ic-fill{rx:3px}
/* Подпись шкалы тише и мельче прежнего (задание Виктора 2026-08-01 «меньше и
   деликатнее»): график отвечает на «куда идёт», точное число берут из таблицы. */
.ops-ic-label{fill:var(--ops-mute);font:10px var(--ops-font-mono)}
/* Подпись слота центрируется относительно своего столбика — якорь задаёт лист,
   а не presentation-атрибут в генераторе (#44: вид задают только стили). */
.ops-ic-tick{fill:var(--ops-mute);font:10px var(--ops-font-mono);text-anchor:middle}
/* Пустое состояние говорит ТОЙ ЖЕ грамматикой, что .ops-empty панелей консоли
   (моно, прописные, разрядка .06em): ворота G8 заведены против того, чтобы
   «здесь ничего нет» выглядело по-разному на разных экранах, но смотрят только
   на разметку страницы и про SVG молчат — равенство держит тест. Отличие одно и
   вынужденное: у текста SVG цвет задаётся fill, а не color. */
.ops-ic-empty{fill:var(--ops-mute);font:600 10.5px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase}
/* Полоса запаса лежит ОТДЕЛЬНОЙ строкой под своими числами, а не шестым столбцом,
   и это не вкус вёрстки. Кадр картинки — 1140px канона графиков; в узком столбце
   браузер вписывает её по МЕНЬШЕЙ стороне (preserveAspectRatio по умолчанию), то
   есть масштабирует вместе с надписью на плашке — «включённое не сверено» в
   столбце на 300px превращается в нечитаемые 3px. Своя строка отдаёт полосе всю
   ширину таблицы, и надпись остаётся надписью.
   Разделитель уезжает на строку полосы: черта между числами и ИХ ЖЕ полосой
   разрывала бы одну запись пополам. */
.ops-table tr:has(+ tr.ops-allow) td{border-block-end:0}
.ops-table tr.ops-allow td{height:auto;padding-block:0 9px}

/* ── полоса вердиктов по часам (задача 13) ───────────────────────────────────
      Здесь статусные тона УМЕСТНЫ, в отличие от соседних картинок: полоса не
      рисует величину, она перечисляет состояния, а состояние само и есть статус.
      Порога при этом по-прежнему нет — красит не страница, а слово из книги
      снапшотов (домен колонки verdict: green | yellow | red) плюс метка часа
      без снапшота.
      УМОЛЧАНИЕ КЛЕТКИ — НЕЗНАНИЕ, не благополучие. Клетка без модификатора
      выглядит как пропуск, и это ровно то, ради чего полоса сделана: час, о
      котором мы ничего не знаем, не имеет права выглядеть спокойным. Отсюда же
      ответ, почему не взята родственная ops-uptime, форма у которой та же:
      её безымянная клетка зелёная, а сама она кодирует шкалу доступности, а
      не слово вердикта.
      Высота — второй канал поверх цвета (тот же приём, что у ops-uptime):
      полоса остаётся читаемой в оттенках серого и на печати.
      flex:1 1 0 вместо фиксированной ширины КЛЕТКИ: клеток около двух сотен, и
      любая ширина в пикселях либо вылезала бы за панель, либо оставляла её пустой
      на треть при другой глубине полосы.
      Пол ширины у полосы при этом ЕСТЬ: клетка тоньше 2px перестаёт быть видимой,
      значит семь суток по часам не сжимаются ниже ~525px. Узкий экран консоли
      (390px) полосу поэтому ПРОКРУЧИВАЕТ — обёрткой ops-scroll в шаблоне, той же,
      что у таблиц; min-inline-size:max-content даёт прокрутке точную ширину
      содержимого, иначе правый отступ полосы при прокрутке терялся бы.
      ──────────────────────────────────────────────────────────────────────── */
.ops-strip{display:flex;align-items:flex-end;gap:1px;padding:11px;min-inline-size:max-content}
.ops-strip__cell{flex:1 1 0;min-width:2px;height:6px;border-radius:1px;background:var(--ops-line)}
.ops-strip__cell--ok{height:10px;background:var(--ops-ok)}
.ops-strip__cell--warn{height:13px;background:var(--ops-warn)}
.ops-strip__cell--bad{height:16px;background:var(--ops-bad)}
/* --neu повторяет базовое правило намеренно: именованный тон обязан иметь СВОЁ
   правило. Шаблон печатает этот модификатор явно (час без снапшота), и молчаливая
   опора на умолчание означала бы, что смена базы перекрашивает незнание вместе с
   безымянными клетками, которых на странице быть не должно. */
.ops-strip__cell--neu{height:6px;background:var(--ops-line)}

/* ── полоса «сколько суток это длится» (/ops/alerts) ─────────────────────────
      Родственница ops-strip по форме и ПРОТИВОПОЛОЖНА ей по устройству: та —
      флекс из <div>, эта — серверный SVG (pipeline/serving/alert_chart), потому
      что клеток здесь бывает больше полутора сотен и они обязаны сжиматься вместе
      с кадром, а не переносом. Словарь классов закрыт в самом рисовальщике
      (CLASS_VOCABULARY), и тест сверяет его с этими правилами В ОБЕ СТОРОНЫ.
      УМОЛЧАНИЯ У КЛЕТКИ НЕТ, и это не стилистика: у класса .ops-ah-day СВОЕГО
      правила нет вовсе, а оба тона висят на составных селекторах «класс +
      модификатор» — то есть цвет физически недостижим без модификатора, а не
      обещан комментарием. Клетка без модификатора остаётся с заливкой браузера
      по умолчанию, а она ЧЁРНАЯ (тот же довод, что у .ops-ic-line выше): такого
      тона нет ни в светлой палитре, ни в тёмной, и клетка читается как поломка,
      а не как спокойные сутки — ровно та беда, из-за которой ops-uptime сюда не
      взята: её безымянная клетка зелёная.
      Держат это ДВА теста, а не внимание (tests/serving/test_alert_chart.py):
      один запрещает рисовальщику выпустить клетку без модификатора, второй
      запрещает листу вернуть тон селектору, который цепляет клетку без него.
      ДВА ТОНА = ДВА РАЗНЫХ ФАКТА. --hit: тревога в эти сутки доставлялась. --quiet:
      не доставлялась (НЕ «всё было хорошо» — книга пишет строку только при удавшейся
      отправке, и оговорку про это печатает страница рядом). Третьего тона («до этих
      суток выборка не дошла») нет намеренно: полоса начинается с первых прочитанных
      суток, поэтому клетка такого тона недостижима — про обрезку говорит страница.
      inline-size:100% + preserveAspectRatio="none" в разметке: полоса тянется по
      ширине панели, а высота остаётся натуральной — иначе клетки на узком экране
      сплющились бы вместе с подписями краёв. */
.ops-ah{display:block;inline-size:100%;block-size:30px;min-inline-size:220px}
.ops-ah-day.ops-ah-day--hit{fill:var(--ops-bad)}
.ops-ah-day.ops-ah-day--quiet{fill:var(--ops-line)}
.ops-ah-tick{fill:var(--ops-mute);font:500 9.5px var(--ops-font-mono);letter-spacing:.04em}
.ops-ah-tick--end{text-anchor:end}
/* Плашка «полосе нечего показать» — тем же кеглем и тем же регистром, что у плашек
   картинок витрины ресурсов: два разных пустых состояния на соседних экранах не
   должны выглядеть разными сообщениями. fill вместо color вынужденное: у текста SVG
   цвет задаётся fill. */
.ops-ah-empty{fill:var(--ops-mute);font:600 10.5px var(--ops-font-mono);letter-spacing:.06em;text-transform:uppercase}

/* ── волна K3: print — деградированный fallback «всё лежит» (#44) ─────────── */
@media print{
  .ops-side,.ops-scrim,.ops-top,.ops-crumb,.ops-burger{display:none}
  .ops-shell{display:block}
  .ops-main .wrap{max-inline-size:none;padding:0}
  .ops-step,.ops-panel,.ops-symptom a{break-inside:avoid}
  /* Карточка конвейера не рвётся между листами: на бумаге лист Letter даёт
     обёртке ~720px, то есть карточный вид (порог 860px) — и разорванная пополам
     карточка печатала бы часть ответов под чужим именем. Ширина листа тут не
     утверждается правилом, а измеряется контейнером: печать в альбом сама
     вернётся к таблице, и это верно — там колонкам снова хватает места. */
  .ops-table--cards tr{break-inside:avoid}
  .ops-shell .ops-actions,.ops-shell .ops-doc__find,.ops-shell form{display:none}
  /* Стенд прогонов печатается ЦЕЛИКОМ, даже если полоса свёрнута на экране
     (гейт Prod куска 9, NIT-4): канон печати — «всё лежит плашмя», а свёрнутость
     это состояние экрана, а не свойство документа. Иначе распечатка молча теряет
     и заметки про деньги, и журнал прогонов. */
  .ops-gen{display:block}
  html[data-ops-rail="collapsed"] .ops-gen__body{display:block}
  .ops-gen__railtoggle{display:none}
}

/* ── motion (body-fade 250ms; reveal-анимации панелей НЕТ — виза v2 п.3;
      общий kill-switch reduced-motion живёт в слое ops-reset этого файла —
      motion.css на /ops/ не грузится с волны 5) ─────────────────────────────── */
@media (prefers-reduced-motion: no-preference){
  body:has(> .ops-shell),body:has(> .ops-gatepage){animation:ops-fade .25s ease-out}
  @keyframes ops-fade{from{opacity:.7}to{opacity:1}}
}

/* ── responsive: 1024 drawer · 640 плотность/стек ────────────────────────── */
@media (max-width:1024px){
  .ops-shell{grid-template-columns:1fr}
  .ops-side{position:fixed;inset-block:0;inset-inline-start:0;width:240px;z-index:20;transform:translateX(-100%);visibility:hidden;transition:transform .2s ease,visibility .2s}
  .ops-shell.drawer-open .ops-side{transform:none;visibility:visible}
  .ops-shell.drawer-open .ops-scrim{display:block;position:fixed;inset:0;background:var(--ops-scrim-bg);z-index:15}
  .ops-burger{display:inline-flex;align-items:center}
  .ops-tiles{grid-template-columns:repeat(4,1fr)}
  .ops-cols{grid-template-columns:1fr 1fr}
  .ops-doc{grid-template-columns:minmax(0,720px);justify-content:center}
  .ops-doc__rail{display:none}
}
@media (max-width:640px){
  .ops-tiles{grid-template-columns:repeat(2,1fr)}
  .ops-cols{grid-template-columns:1fr}
  .ops-top__search{flex:1 1 120px}
  .ops-who,.ops-badge--city{display:none}
  /* Возврат на сайт сжимается до стрелки — на 390px строка топбара иначе
     съедает поиск; смысл ссылки держит aria-label. */
  .ops-back span{display:none}
  .ops-sig__cell{border-inline-end:0}
  .ops-sig__cell:last-child{margin-inline-start:0}
  /* волна 3: say/don't-say и history-окна складываются в один столбец */
  .ops-quiz .sds{grid-template-columns:1fr;gap:2px}
  .ops-fcard__hist>div{grid-template-columns:1fr;gap:1px;padding-block:4px}
  /* волна K2: вердикт-кнопки — столбцом (5 слов дословно не влезают рядом) */
  .ops-verd{flex-direction:column}
  .ops-verd a{text-align:center;min-height:32px;display:flex;align-items:center;justify-content:center}
  /* волна K3: симптом-кнопки — один столбец (телефон-ночью = главный кейс) */
  .ops-symptom{grid-template-columns:1fr}
  /* кусок 1b: второстепенная колонка таблицы на телефоне уходит целиком.
     Правило узкого экрана — не больше пяти колонок (эталон = очередь работ 1a):
     на шестой имя предмета усекается до нечитаемого огрызка, а колонка действия
     уезжает за край (найдено на приёмке 1b: «Invit…», «Op|»). Что именно
     второстепенно, знает ШАБЛОН и говорит это классом у себя — порядковый
     селектор здесь врал бы после любой вставки колонки. Деталь не теряется:
     она на карточке предмета. */
  .ops-table td.aux,.ops-table th.aux{display:none}
  /* Колонка времени на телефоне уже: дата переносится второй строкой, адрес получает
     остаток ширины. Замерщик читает лист именно с телефона — адрес там главный. */
  .ops-table td.when,.ops-table th.when{width:92px;white-space:normal}
}
@media (pointer:coarse){
  .ops-side__nav a{min-height:44px}
  .ops-filters a,.ops-table__act,.ops-btn,.ops-linkbtn{min-height:44px;display:inline-flex;align-items:center}
  .ops-theme,.ops-out,.ops-back,.ops-burger,.ops-gen__railtoggle{min-height:44px;min-width:44px;justify-content:center}
  /* Свёрнутая дорожка расширяется ВМЕСТЕ с язычком (ре-гейт NIT-b): 44px кнопка в 34px
     дорожке при `overflow:visible` вылезала бы на левый край рамки листа. Мера правится
     в ОДНОМ месте — токеном, а не вторым числом в правиле. */
  :root{--ops-gen-rail-min:44px}
  .ops-fld input,.ops-fld textarea,.ops-fld select{min-height:44px}
  .ops-rowform input[type=text],.ops-rowform input[type=date]{min-height:44px}
  .ops-seg a,.ops-pager a{min-height:44px;display:inline-flex;align-items:center}
  /* волна 3: квиз и поиск reading-входов */
  .ops-quiz label{min-height:44px}
  .ops-quiz input[type=text],.ops-doc__find input{min-height:44px}
  /* волна K1: копи-кнопка — полноценный touch-таргет (стресс-режим) */
  .ops-copy{min-height:44px;min-width:44px}
  /* волна K2: вердикт-кнопки */
  .ops-verd a{min-height:44px;display:inline-flex;align-items:center}
}
}
