/* ===================================================
   SEO Platform Design System
   All classes MUST use .ds-* prefix.
   Enforced by .claude/hooks/verify-design-system.sh
   =================================================== */

/* ─── Theme token overrides ─── */
/* Primary color: #5d87ff (Modernize blue), Error: #ef4444 (Modernize red) — overrides DaisyUI defaults.
   --wa override: DaisyUI "corporate" (light) warning is L=0.847 — unreadable as text on white;
   override to amber-600 (~#d97706) which reads well on light backgrounds.
   seo-qzm: accent chroma calibrated to ~0.12 (was --p 0.186, --wa 0.163, --in 0.191/0.1435,
   corporate --su 0.150, business --a 0.168; business --wa/--su + corporate --a already <=0.12) —
   accents ran 5-6x the desaturated surfaces (C~0.03) and read as "loud". Lightness+hue preserved
   so WCAG AA is unaffected (verified L1 machine both themes). --in overridden here (was DaisyUI
   default) so ds-callout-info calms too. Error (--er) kept loud — alarm semantics. Component-level
   accent fills (btn-primary/btn-warning/btn-outline/ta-menu-item.active/ds-page-active/ds-toc-nav)
   carry their own hardcoded chroma in component-overrides.css — calmed there in lockstep. */
[data-theme="corporate"] { --p: 64% 0.12 264; --pc: 100% 0 0; --er: 63.7% 0.223 27.3; --erc: 100% 0 0; --wa: 0.555 0.12 70; --in: 72.06% 0.12 231.6; --su: 64.8% 0.12 160; --text-muted-aa: oklch(var(--bc) / 0.68); --text-error-aa: oklch(0.45 0.11 27.3); --text-error-aa-hover: oklch(0.38 0.12 27.3); color-scheme: light; }
/* business: override DaisyUI default grey --b1/--b2/--b3 (24/22/20% achromatic) with
   chromatic navy ladder matching Modernize surface tokens. b1=#1c2536 (card/body),
   b2=#253043 (elevated/popover), b3=#1a2230 (sunken). Eliminates grey-on-navy chroma
   mismatch when any element falls through to DaisyUI ladder inside .ds-mc-card. */
[data-theme="business"]  { --b1: 26.43% 0.0345 262.71; --b2: 30.80% 0.0377 261.11; --b3: 25.10% 0.0291 261.39; --p: 64% 0.12 264; --pc: 100% 0 0; --er: 63.7% 0.223 27.3; --erc: 100% 0 0; --bc: 92% 0.01 258; --in: 62.6163% 0.12 240.033697; --a: 67.271% 0.12 35.791525; --text-muted-aa: oklch(var(--bc) / 0.60); --text-error-aa: oklch(0.70 0.12 27.3); --text-error-aa-hover: oklch(0.78 0.12 27.3); color-scheme: dark; }
/* --text-muted-aa (seo-2pu3): the one AA-safe muted-text token. Replaces ad-hoc
   text-base-content/40|50|55 on secondary prose/labels (sidebar section labels, version,
   brand subtitle, modal/help body) whose hierarchy comes from weight/uppercase/size — NOT
   from sub-AA contrast. 0.68 light → ~5.9:1, 0.60 dark → ~5.2:1 on the canonical surfaces. */
/* --text-error-aa (seo-1lgy chips row): AA-safe MUTED error-family TEXT ink for small
   red-tinted text actions at rest («Сбросить фильтры»). Full --er is wrong twice over:
   as 12px text on white it reads ~3.15:1 (below AA), and at C 0.19-0.22 it was measured
   the single most saturated ink in the frame — out-chroming the page's primary CTA
   (C 0.12) and colliding with the per-row delete red, the one genuinely destructive
   register (ensemble R2, 2026-08-11). Both themes cap chroma at ~the CTA's 0.12 and
   tune L for AA: corporate 0.45 L (measured 7.84:1 on white), business 0.70 L (5.47:1 on b1).
   --text-error-aa-hover (seo-zko1 R2): the paired HOVER ink. It used to jump to the raw
   --er, which is a FILL colour: as 12px text that measured 3.79:1 light / 4.05:1 dark —
   below AA and below its own rest state, so pointing at the control made its label HARDER
   to read (an inverted state ladder). The hover stays error-red and moves AWAY from the
   surface instead: corporate 0.38 L (10.61:1), business 0.78 L (7.35:1) — each ~1.35x its
   own rest, both still at the CTA's 0.12 chroma, so hovering creates no new loud accent. */

/* ── Design System tokens ── */
:root {
    /* Surfaces — aliased to DaisyUI base ladder (single source of truth).
       Light values resolve to white (--b1=100%) / light grey (--b2=93%). Border + tooltip
       are chrome (not surface elevation) and stay hardcoded. Mirrored in [business] below. */
    --surface-body:           oklch(var(--b1));
    --surface-card:           oklch(var(--b1));
    --surface-popover:        oklch(var(--b1));
    --surface-popover-border: #e5e7eb;
    --surface-sunken:         oklch(var(--b2) / 0.5);
    --tooltip-surface:        rgb(51, 65, 85);
    --tooltip-surface-text:   #e2e8f0;
    /* Radii */
    --radius-card:    1rem;
    --radius-popover: 0.5rem;
    --radius-tile:    0.5rem;
    --radius-control: 0.375rem;
    /* Icon/tile sizes */
    --icon-tile-sm: 2rem;
    --icon-tile-md: 2.25rem;
}
[data-theme="business"] {
    /* Surface tokens are now aliases of the DaisyUI ladder (overridden above to navy).
       Single source of truth: --b1/--b2/--b3. Anyone using either ladder gets the same
       chroma family. Keeps --surface-popover-border + --tooltip-surface hardcoded — they
       belong to chrome (borders, tooltip) that doesn't move with surface elevation. */
    --surface-body:           oklch(var(--b1));
    --surface-card:           oklch(var(--b1));
    --surface-popover:        oklch(var(--b2));
    --surface-popover-border: #333f55;
    --surface-sunken:         oklch(var(--b3));
    --tooltip-surface:        rgb(40, 52, 70);
}

/* Modernize shadow tokens — single source of truth for card / popover elevation.
   Values mirror the former inline shadows on .ds-mc-card and .ds-surface-popover.
   seo-mp3n: business now has 2-layer shadow matching light's structure (ring +
   drop). Without the drop layer, .ds-mc-card was visually flat against
   .ds-shell-bg in dark theme — same chroma family (post-seo-ojuq) + no depth
   cue = card boundary invisible. Drop layer rgba(0,0,0,0.4) gives subtle
   under-card darkening matching Modernize-Vue reference; ring layer
   rgba(145,158,171,0.3) preserved for upper-edge highlight. */
:root {
    --shadow-dark-md: rgba(145,158,171,0.2) 0px 0px 2px 0px, rgba(145,158,171,0.12) 0px 12px 24px -4px;
}
[data-theme="business"] {
    --shadow-dark-md: rgba(145,158,171,0.3) 0px 0px 2px 0px, rgba(0,0,0,0.4) 0px 8px 20px -4px;
}

/* ─── Scrollbar gutter (seo-e8ai) ─────────────────────────────────────────────
   While a modal is open the page is scroll-locked with `overflow:hidden` +
   `scrollbar-gutter: stable` on <html> — DaisyUI's own
   `html:has(.modal-open, .modal[open], …)` rule (daisyui.min.css; Alpine's
   x-trap.noscroll only adds overflow/padding-right) — but only while the modal
   is open. On a page that has NO scrollbar at rest (short /projects) that
   toggles a classic scrollbar's width in and out of the layout: the card's right
   edge moved 1896→1881 on open and back on close (measured with classic
   scrollbars; with overlay scrollbars the width is 0 and nothing moves). Reserve
   the gutter permanently so the content width is the same with and without a
   scrollbar, locked or not — every modal (form/confirm/bulk/picker) stops
   reflowing the page underneath it. */
html { scrollbar-gutter: stable; }

/* ─── Table row styles ─── */
.ds-tr-hover {
    transition: background-color 0.15s ease;
}
.ds-tr-hover:hover {
    background-color: oklch(var(--b2) / 0.4);
}
.ds-tr-clickable > td {
    padding: 0;
    /* Table-layout trick: `height: 1px` in table-cell context is interpreted
       by Chrome/Firefox/WebKit as «participate in row sizing but don't enforce
       a min». The child `.ds-tr-cell-link` with `height: 100%` then resolves
       to the actual row height (max of all cells in the row), not just its
       own content height. Without this, single-line cells like «—» / «0» end
       up shorter than multi-line neighbours, the inset focus ring is cropped
       at the bottom, and the click area is smaller than the visible cell.
       (seo-j4y2) */
    height: 1px;
}
.ds-tr-cell-link {
    /* flex column восстанавливает vertical centering, потерянное при row-fill
       из seo-j4y2 (`height: 100%` нейтрализует `vertical-align: middle` на td,
       а `display: block` не центрирует свой контент). Контракт: multi-part
       inline content на одной строке нужно обернуть в один child-блок,
       иначе flex column распределит сиблинги как вертикальный стек. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding: 0.75rem;
}
/* seo-vi6x — cell padding has exactly ONE owner: the th; the td of a plain row; the
   cell link of a clickable row (.ds-tr-clickable zeroes its td above). A plain row
   (row_link=None) still carries .ds-tr-cell-link anchors — the disabled ones every
   built-in cell emits, and the live link_project / link_user cross-links — and their
   own 0.75rem stacked on the td's: measured on /admin/logs, header ink at 12px, body
   ink at 24px in every built-in cell. Keyed on the ROW class, not the link class, so
   the live cross-links keep navigating and simply stop owning padding there. */
tr:not(.ds-tr-clickable) > td > .ds-tr-cell-link {
    padding: 0;
}
/* seo-vi6x — alignment-seam gutter. A right-aligned (number / date) column followed by
   a left-aligned one puts the two ink edges 24px apart — two 12px paddings and no
   vertical rule — while every other seam in the same row is 64px and more, so
   «6045 | freq-demo.ru» (/admin/tasks) and «— | —» (/tasks/{id}) read as one field.
   The LEFT-aligned neighbour opens the gutter on whichever element owns its padding
   (the three owners above), so header and body ink stay flush. `[class^="col-"]` =
   data cells only: the checkbox and actions cells carry other classes.
   `:not([hidden])`: data-table.js marks picker-hidden / auto-hidden cells `hidden`
   beside the inline display, so a hidden numeric column leaves no gutter on its
   visible neighbour; a hidden LEFT neighbour cannot be skipped by `+`, so the column
   after it keeps the plain seam while that neighbour is hidden (auto-hidden below its
   responsive tier, or unticked in the picker). Fixed-layout tables get it too: the 36px
   is part of the left neighbour's th_class width budget there (seo-f90m —
   /admin/data-sources «Комментарий» 138 → 174px). */
.ds-table-border th.text-right:not([hidden]) + th[class^="col-"]:not(.text-right),
.ds-table-border tr:not(.ds-tr-clickable) > td.text-right:not([hidden]) + td[class^="col-"]:not(.text-right),
.ds-table-border tr.ds-tr-clickable > td.text-right:not([hidden]) + td[class^="col-"]:not(.text-right) > .ds-tr-cell-link {
    padding-left: 3rem;
}
/* Inset focus ring — cell-link fills the entire cell, an outset offset would
   bleed into neighbouring cells and the table grid. (seo-3n0o) */
.ds-tr-cell-link:focus-visible {
    outline: 2px solid oklch(var(--p));
    outline-offset: -2px;
}
/* Non-clickable variant for tabs where row_link is suppressed (e.g. /projects
   «Удалённые» tab — clicking a soft-deleted project would 404). The anchor
   stays in the DOM (markup parity across tabs) but receives no clicks/focus. */
.ds-tr-cell-link-disabled {
    cursor: default;
    pointer-events: none;
}
.ds-tr-selected {
    background-color: oklch(var(--p) / 0.05);
}

/* seo-nn2: the `.ds-tr-loading.htmx-request { opacity:0.5 }` loading-dim rule was
   removed here — it was orphaned/dead since seo-tetx narrowed the HTMX swap +
   hx-indicator to the inner `#{panel}-table-results` div, so `.htmx-request`
   never lands on the `.ds-tr-loading` root and the rule never matched a live
   element (empirically verified + Codex-confirmed). It rendered no dim in
   practice, so its WCAG 1.4.3 loading-contrast concern was a phantom. The
   bare `.ds-tr-loading` at-rest lifts below still fire and are untouched.
   seo-h1t then restored loading feedback (a 2px ::before sweep) — removed in turn
   by owner decision, see below. */

/* seo-lo0z: the seo-h1t 2px loading sweep (a primary ::before bar on the
   .ds-tr-loading root, shown 170ms into every table request) was REMOVED by
   owner decision 2026-08-13 — painted on the root's top edge it sat above the
   toolbar, far from the rows that actually update, and read as a stray line
   rather than feedback. The `.ds-dt-busy` class + aria-busy toggle in
   data-table.js STAY: aria-busy is the SR loading signal, and the seo-0wrh
   height-reserve busy-check keys off the class to defer an unpin past an
   in-flight swap. Only the paint is gone. */

/* ─── Reduced-motion global kill-switch (seo-ape) ─── */
/* WCAG 2.3.3: when the OS requests reduced motion, neutralise ALL transitions
   and animations site-wide — Alpine x-transition (sidebar slide, dropdown/modal
   scale+opacity), Tailwind transition-* utilities, keyframe animations. The
   inline styles Alpine emits need !important to override. 0.01ms (not 0) keeps
   transitionend/animationend events firing so x-show/Alpine state stays correct.
   transition-DELAY is untouched (separate property) — that mattered for the
   seo-h1t loading sweep's 170ms delayed reveal while it existed (removed by
   owner decision, seo-lo0z above); the carve-out itself is kept as-is. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ─── Table wrapper ─── */
.ds-table-border {
    border: 1px solid var(--surface-popover-border);
    border-radius: var(--radius-tile);
    overflow: hidden;
}

/* seo-3wni: the last row's own border-bottom lands 1.5px above the wrapper's, so the
   two hairlines read as one 2px rule — heavier than every other divider in the table
   and against the checklist's "single 1px border-b per tr, no doubled-up dividers".
   The wrapper edge already closes the table, so the last row does not need its own.
   Note for the record: the reviewing lens blamed a border-t on the pagination footer;
   measured, the footer carries no top border at all and sits outside the wrapper —
   the second line is the wrapper's own bottom edge. */
.ds-table-border tbody tr:last-child { border-bottom: 0; }

/* Horizontal scroll wrapper for wide tables. `tabindex` is set CONDITIONALLY
   by Alpine x-init in data_table.html — 0 when scrollWidth > clientWidth
   (overflow → keyboard scroll needed, WCAG 2.1.1), -1 otherwise (no extra
   focus stop on wide viewports). When focused (overflow case), the inset
   focus ring rides the nested `.ds-tab-track` rounded-xl shape (0.75rem) so
   the rectangle doesn't read as a foreign element over the rounded tab pill.
   (seo-3n0o, radius + conditional from seo-rsia) */
.ds-table-scroll:focus-visible {
    outline: 2px solid oklch(var(--p));
    outline-offset: -2px;
    border-radius: 0.75rem;
}

/* ─── Card ─── */
.ds-mc-card {
    background: var(--surface-card);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-dark-md);
}

/* ─── Filter chips ─── */
.ds-filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0.625rem;
    border-radius: 0.375rem;
    font-size: 0.75rem;
    font-weight: 500;
    background: oklch(var(--bc) / 0.08);
    /* Alpha 0.55 gave only 3.18:1 on corporate (white body) — below WCAG AA
       for readable text. Raised to 0.84 (Codex math: limiting theme is
       corporate — bc at 26.8618% L; over composited chip-bg 0.9216, AA
       requires Y_text ≤ 0.1659 → α ≥ 0.8376). Business theme passes at
       α ≥ 0.65 so single rule works for both. */
    color: oklch(var(--bc) / 0.84);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
    border: none;
    line-height: 1.5;
}
.ds-filter-chip:hover {
    background: oklch(var(--er) / 0.12);
    color: oklch(var(--er));
}
[data-theme="business"] .ds-filter-chip:hover { background: oklch(var(--er) / 0.15); color: oklch(var(--er)); }

.ds-filter-chip-error {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0.625rem;
    border-radius: 0.375rem;
    font-size: 0.75rem;
    font-weight: 500;
    background: oklch(var(--er) / 0.12);
    color: oklch(var(--er));
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
    border: none;
    line-height: 1.5;
}
.ds-filter-chip-error:hover { background: oklch(var(--er) / 0.22); }

/* ─── Search input chips (toggle include ↔ exclude on click) ─── */
.ds-search-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0.625rem;
    border-radius: 0.375rem;
    font-size: 0.75rem;
    font-weight: 500;
    background: oklch(var(--p) / 0.10);
    color: oklch(var(--p));
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
    border: none;
    line-height: 1.5;
}
/* `color: oklch(var(--wa))` here is intentionally overridden by the per-theme
   .ds-search-chip rules later in the cascade (~line 333, same specificity).
   Result: amber-tint bg + primary text. Original amber/amber intent dropped
   because no contrast-safe amber-on-amber-tint token exists in the Modernize
   reference (--wa over amber/0.12 ≈ 4.48 corporate, fails AA). Locked by
   tests/browser/admin/tasks/test_shell_wcag_contrast.py. */
.ds-search-chip:hover {
    background: oklch(var(--wa) / 0.12);
    color: oklch(var(--wa));
}
.ds-search-chip:has(button:hover) {
    background: oklch(var(--er) / 0.12);
    color: oklch(var(--er));
}

.ds-search-chip-exclude {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0.625rem;
    border-radius: 0.375rem;
    font-size: 0.75rem;
    font-weight: 500;
    background: oklch(var(--wa) / 0.10);
    color: oklch(var(--wa));
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
    border: none;
    line-height: 1.5;
}
.ds-search-chip-exclude:hover {
    background: oklch(var(--p) / 0.12);
    color: oklch(var(--p));
}
.ds-search-chip-exclude:has(button:hover) {
    background: oklch(var(--er) / 0.12);
    color: oklch(var(--er));
}

/* Unified focus ring for all chip variants — primary outline matches
   the rest of the form/shell focus contract. Without this, chip buttons
   fall back to UA `outline: auto 1px rgb(16,16,16)` which `color-scheme:
   dark` inverts to white (visible artefact when Tab lands on a chip). */
.ds-filter-chip:focus-visible,
.ds-filter-chip-error:focus-visible,
.ds-search-chip:focus-visible,
.ds-search-chip-exclude:focus-visible {
    outline: 2px solid oklch(var(--p));
    outline-offset: 2px;
}

/* ─── Tab switcher ─── */
.ds-tab-track {
    background: var(--surface-sunken);
}
.ds-tab-active {
    background: var(--surface-card);
    box-shadow: 0 1px 2px 0 rgba(0,0,0,0.06);
}
[data-theme="business"] .ds-tab-active {
    background: var(--surface-popover);
    box-shadow: rgba(0,0,0,0.3) 0px 1px 4px 0px;
}
/* Inset focus ring — tab anchors live inside .ds-tab-track with internal
   padding, outset offset would clip on the track edge. (seo-3n0o)
   `:is(a, button)` so the keyword source-filter tabs (in-form <button>, not nav
   <a>) get the same ring (seo-jg9t.9 B). */
.ds-tab-track > :is(a, button):focus-visible {
    outline: 2px solid oklch(var(--p));
    outline-offset: -2px;
}

/* ─── Popover surface (dropdowns, col-picker, modal bg) ─── */
/* seo-a80g: a popover owns its text style — it must not inherit it from the
   element that hosts its trigger. The column-filter panel and the aggregate
   picker live inside <th>, and DaisyUI styles `.table thead` as 700 / bc 0.6 /
   nowrap: every item of the filter dropdown rendered BOLD in 60 % grey, which
   the owner read as «размытый шрифт» (measured: +22 % ink vs weight 400). The
   per-page menu, hosted outside the table, never had the problem. Same class
   outside tables: the domain-history popover on /projects/{id} sits in the
   meta-chips row (bc/0.6) and inherited that grey for its heading — now full
   ink too. Font-size is deliberately not reset here — each popover's content
   sets its own. */
.ds-surface-popover {
    background: var(--surface-popover);
    border: 1px solid var(--surface-popover-border);
    border-radius: var(--radius-popover);
    padding: 0.25rem;
    box-shadow: var(--shadow-dark-md);
    color: oklch(var(--bc));
    font-weight: 400;
    white-space: normal;
    text-transform: none;
    letter-spacing: normal;
}

/* ─── Control border (outlined inputs, dropdowns, inactive buttons) ─── */
/* seo-6zb: interactive control boundaries need >=3:1 (WCAG 1.4.11). The shared
   chrome token (--surface-popover-border) is divider-grade — 1.45:1 on dark b1,
   ~1.2:1 on white — fine for non-interactive 1px dividers (.ds-table-border,
   .ds-shell-divider, .ds-surface-popover keep it), too faint to mark a control
   hit area. Follow-up (user: bc/0.5 too prominent): per-theme alphas sit at the
   COMPLIANCE FLOOR, not above it — corporate /0.47 = 3.06:1 on white, business
   /0.40 = 3.16:1 on b1 (live canvas-composited). Do not raise without a design
   decision; do not lower below 3:1 (the seo-6zb defect returns). */
.ds-ctrl-border {
    border-color: oklch(var(--bc) / 0.47);
}
[data-theme="business"] .ds-ctrl-border {
    border-color: oklch(var(--bc) / 0.4);
}
/* Disabled control (e.g. the bulk «Применить» until a row is selected): the
   interactive-strength boundary would advertise an affordance the control
   doesn't have — drop to the divider-grade token (WCAG 1.4.11 exempts
   inactive controls; the muted label carries the state instead).
   seo-nd7e: fill + label moved here from the markup's Alpine branch, which was
   dead code. `bg-transparent` (0,1,0) lost to daisyui `.btn:disabled` (0,2,0),
   so the body was painted `--n/0.2` — a NEUTRAL paint, and neutral is a dark
   slate in both palettes: 1.52:1 over the white card but 1.00:1 over the navy
   one (rgb(29,37,52) on rgb(28,37,54) — the body vanished, and in light it
   outweighed the live select beside it). `text-base-content/30` was likewise
   overridden, by the seo-4cec .ds-tr-loading AA lift (!important), which paints
   it the same value as the neighbouring enabled select (exact in light: both
   land on --bc/0.68; near-identical in dark: /0.57 vs /0.60) — so the /30 the comment
   above once relied on never reached the screen. Both are now `--bc`-based, so
   they invert with the theme instead of against it; values are the canonical
   disabled paint of .ds-cta-disabled (seo-hlhd — a disabled control stays
   perceptible in dark). Deliberately NOT >=3:1: perceptible, not interactive. */
.ds-ctrl-border:disabled {
    border-color: var(--surface-popover-border);
    background-color: oklch(var(--bc) / 0.06);
    color: oklch(var(--bc) / 0.50);
}

/* seo-0o9j: a daisyui form control hosted INSIDE a popover — the funnel's «Поиск...»
   box and its от/до range row (every column_filter of every data_table), the
   keyword-add form_modal's textarea (modal-box carries this surface) — kept daisyui's
   `.input-bordered`/`.textarea-bordered`/`.select-bordered` bc/0.2: 1.52:1 on the light popover, 1.74:1 on the raised dark
   one (--b2) — a hairline where the checkbox strokes 20px below draw at ≥3.3:1
   (WCAG 1.4.11 non-text ≥3:1). Same compliance-floor policy as .ds-ctrl-border,
   re-measured on THIS surface: `.ds-ctrl-border`'s business /0.40 is tuned for b1
   and composites to 2.98:1 on the lighter popover (--b2 rgb(37,48,67)), so the dark
   alpha is its own value. Live canvas-composited: corporate /0.47 = 3.1:1 on white,
   business /0.42 = 3.14:1 on b2. Not raised further (the seo-6zb softening stands);
   do not lower below 3:1. Focus keeps the --p border (component-overrides.css
   `.input:focus` & co., same specificity, loads later). A callsite must NOT pin its
   own `border-base-content/N` utility on such a control: it LOSES to this (0,2,0)
   rule by specificity (Tailwind runs without `important`), so it is dead code that
   reads as the border's owner — the kw-add textarea carried a bc/0.10 one (Codex R1
   catch; mutation-verified: the utility changes nothing, the DS rule owns the border).
   One owner per surface. Pins: tests/test_design_system_
   popover_input_border.py + tests/browser/shared_ui/test_popover_input_border_contrast.py. */
.ds-surface-popover :is(.input, .textarea, .select) {
    border-color: oklch(var(--bc) / 0.47);
}
/* :where() keeps the dark rule at (0,2,0) — a plain [data-theme] prefix would be
   (0,3,0) and outrank component-overrides.css `.input:focus` & co., leaving the
   dark popover control on bc/0.42 while focused (Codex R2 catch). */
:where([data-theme="business"]) .ds-surface-popover :is(.input, .textarea, .select) {
    border-color: oklch(var(--bc) / 0.42);
}

/* ─── seo-3bnr: icon-only dismiss «×» ink (modal close, search close) ─────────
   The four canonical modal close buttons (confirm_modal ×2, bulk_confirm_modal,
   projects_panel delete modal) and the data_table search-bar «×» sat on raw
   `text-base-content/30`: 1.94:1 light /
   2.31:1 dark on the popover surface — below the 3:1 non-text floor in BOTH themes,
   and the × is the one dismiss affordance the overlay has. Ink moves to the
   AA-safe muted token (--text-muted-aa: bc/0.68 light, bc/0.60 dark — ≥4.5:1 on
   card/popover surfaces per seo-2pu3); hover lifts to full --bc as before.
   Symmetric by construction (the token is per-theme), so the L1 rest-state
   scan — which never opens overlays — is not the guard here; the pin is the
   ui-kit modal sections + this comment. */
.ds-dismiss-x { color: var(--text-muted-aa); }
.ds-dismiss-x:hover { color: oklch(var(--bc)); }

/* ─── Tooltip surface ─── */
/* Applied by tooltip.js to the #_tip element on creation. Background and
   text color come from tokens so palette rotation in :root and
   [data-theme="business"] above propagates automatically. */
.ds-tooltip-surface {
    background: var(--tooltip-surface);
    color: var(--tooltip-surface-text);
}

/* ─── Icon button (toolbar actions) ─── */
.ds-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--icon-tile-md);
    height: var(--icon-tile-md);
    border-radius: var(--radius-tile);
    /* seo-zyfg: rest was bc/0.5 — byte-identical to the DISABLED family's
       text alpha (.ds-cta-disabled / .ds-row-action-disabled), so the one
       LIVE control in a card header wore the product's own "can't press"
       paint. 0.7 clears the disabled rank with a visible margin in both
       themes (light ~7:1, dark ~6:1 vs card) while hover still jumps to
       full bc. Shared by the decorative settings-chevron span — conscious:
       it sits inside a real disclosure control. */
    color: oklch(var(--bc) / 0.7);
    transition: background 0.15s, color 0.15s;
    cursor: pointer;
    background: transparent;
    border: none;
    padding: 0;
}
.ds-icon-btn:hover {
    background: oklch(var(--bc) / 0.08);
    color: oklch(var(--bc));
}
.ds-icon-btn:focus-visible {
    outline: 2px solid oklch(var(--p));
    outline-offset: 2px;
}
/* seo-g74t: the disabled state this family never had. Rest is bc/0.7 precisely so a
   LIVE icon-button does not wear the disabled alpha (seo-zyfg) — so a blocked one has
   to say it explicitly, and it says it with the SAME PAIR as .ds-cta-disabled and
   .ds-row-action-disabled: bc/0.06 plate + bc/0.50 ink. The plate is not decoration —
   an icon-button at rest is a bare glyph on the card, so ink alpha alone would leave a
   dead trigger and a live one differing by one alpha step (0.7 vs 0.5) with no shape
   cue, and its own blocked siblings in the same title bar (the CTA, the row-action
   trash) would wear a plate it lacks: one state in two strengths, which is exactly what
   that pair exists to prevent. Hover pinned to the same values — a disabled control
   must not flash an interactive response. No `pointer-events: none` (seo-uyks): that is
   what had killed every disabled tooltip in this product, and the block reason rides in
   this button's `title`. */
.ds-icon-btn:disabled,
.ds-icon-btn[disabled],
.ds-icon-btn:disabled:hover,
.ds-icon-btn[disabled]:hover {
    background-color: oklch(var(--bc) / 0.06);
    color: oklch(var(--bc) / 0.50);
    cursor: not-allowed;
}
.ds-icon-btn--sm {
    width: var(--icon-tile-sm);
    height: var(--icon-tile-sm);
}
/* Icon-button that opens a MENU: auto width so a small dropdown caret fits beside
   the glyph. The bare square ds-icon-btn reads as a direct action; the caret
   signals "opens a choice" (seo-jg9t.6 — keyword export keeps CSV/XLSX×all/filtered
   while matching the etalon download glyph). */
.ds-icon-btn--menu {
    width: auto;
    padding-inline: 0.375rem;
    gap: 0.125rem;
}

/* ─── Disabled CTA — visible state в обеих темах (seo-hlhd) ───
   DaisyUI `.btn:disabled` default desaturates the variant so heavily that on
   the business (dark) theme the text + icon disappear into the card surface —
   the canonical /tasks/{id} «Запустить Вордстат-сбор» CTA is the trigger case.
   Apply ALONGSIDE the `disabled` attribute on <button>/<a> CTAs whose disabled
   reason comes from typed view-model (`cta.disabled`) or template-level
   `can_write_content` guards. Color targets WCAG AA in both themes against
   surface-card.
   seo-oek: weight calmed — border made transparent + text --bc/0.65→0.50 so the disabled
   CTA reads as inert and no longer dominates an enabled ghost button beside it (read-only
   toolbars). Fill kept (--bc/0.06) so it stays visible in dark (the seo-hlhd invariant).
   Disabled controls are WCAG-exempt, so the sub-AA text is intentional. */
.ds-cta-disabled,
.ds-cta-disabled[disabled],
.ds-cta-disabled:disabled {
    background-color: oklch(var(--bc) / 0.06);
    color: oklch(var(--bc) / 0.50);
    border: 1px solid transparent;
    cursor: not-allowed;
}
.ds-cta-disabled:hover,
.ds-cta-disabled[disabled]:hover {
    background-color: oklch(var(--bc) / 0.06);
    color: oklch(var(--bc) / 0.50);
    border-color: transparent;
}
/* seo-uyks (Codex review): every disabled CTA in this project explains itself
   through a native `title` — and none of those tooltips had ever appeared.
   DaisyUI's `.btn:disabled` sets `pointer-events: none`, so the element never
   receives the hover that a native tooltip needs; the ui-kit swatch labelled
   «Disabled (с tooltip)» was showing nothing. Re-enabling pointer events cannot
   make the control actionable: every callsite also carries the native `disabled`
   attribute, which blocks activation at the platform level regardless of this
   property (and `cursor: not-allowed` above still reads as «blocked»). The
   keyboard/AT path is separate and is served by folding the reason into the
   accessible NAME at the callsite, since native tooltips are hover-only. */
.ds-cta-disabled[disabled],
.ds-cta-disabled:disabled {
    pointer-events: auto;
}

/* ─── CTA busy state (seo-y8m7, part 2) ───
   A post_reload CTA starts a paid run with `hx-swap="none"`, so until the reload nothing
   on screen changes. Native `disabled` (hx-disabled-elt) paints no useful busy affordance
   here — the .btn-primary override keeps the full fill and DaisyUI only fades the ink (see
   below) — and dimming is ruled out by contrast (white
   on primary falls below AA 4.5 at any visible opacity). So: spinner + «Запускаем…» over
   RESERVED space. The label keeps its box and turns transparent (no width change, the
   neighbouring CTA does not shift, the accessible name stays in the tree); the busy box is
   absolutely centred over it and clipped to the button, so a label shorter than the busy
   word can never widen it. Shown while the button carries htmx's `.htmx-request` OR
   `aria-busy="true"` (seo-b35z): the post_reload CTA sets both for the same window, but
   form_modal's submit (the frequency picker) must stay visibly busy AFTER the response
   too — its success is a page reload, and between the 200 and the new document the
   button used to paint pristine and re-armed (a second click = a second paid run); there
   htmx has already dropped `.htmx-request`, and `aria-busy` — true until the page goes
   away — is what keeps the box on. */
.ds-cta-busy {
    position: relative;
}
.ds-cta-busy > .ds-cta-busy-state {
    display: none;
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    overflow: hidden;
    white-space: nowrap;
}
.ds-cta-busy.htmx-request > .ds-cta-busy-state,
.ds-cta-busy[aria-busy="true"] > .ds-cta-busy-state {
    display: flex;
}
.ds-cta-busy.htmx-request > .ds-cta-label,
.ds-cta-busy[aria-busy="true"] > .ds-cta-label {
    color: transparent;
}
/* --fit (seo-b35z): the same box, but the width is RESERVED FOR THE BUSY WORD at rest.
   The base contract clips a busy word longer than the label («a label shorter than the
   busy word can never widen it») because the zone CTA's neighbour must not shift; a
   form_modal submit («Дособрать» → «Дособираем…») has the opposite need — its footer has
   room, and the busy row overran the padding by 21px, the last «…» 1px from the fill edge
   (interactive-states + microcopy lenses). Grid-stacking both spans in one cell makes the
   button max(label, busy) wide from the first paint, so nothing moves when the swap
   happens; the hidden busy span keeps its box (visibility, not display). */
.ds-cta-busy--fit {
    display: inline-grid;
    grid-template-areas: "s";
    place-items: center;
}
.ds-cta-busy--fit > .ds-cta-label,
.ds-cta-busy--fit > .ds-cta-busy-state {
    grid-area: s;
}
.ds-cta-busy--fit > .ds-cta-busy-state {
    position: static;
    display: flex;
    visibility: hidden;
}
.ds-cta-busy--fit.htmx-request > .ds-cta-busy-state,
.ds-cta-busy--fit[aria-busy="true"] > .ds-cta-busy-state {
    visibility: visible;
}
/* Busy is NOT disabled-by-policy, so it keeps the rest ink. DaisyUI's `.btn:disabled`
   (0,2,0) repaints the ink to --bc/0.2 while the filled overrides in component-overrides.css
   keep the full primary/warning fill (they set background + border, not color) — measured on
   /tasks/228: white ink at rest, --bc/0.2 on the primary fill once in flight, i.e. an
   invisible busy box. Filled styles get their content ink back; neutral gets --bc on DaisyUI's
   muted disabled fill (where --bc/0.2 on --n/0.2 would vanish too); outline-primary keeps its
   per-theme canon ink from component-overrides.css (that rule already outranks .btn:disabled,
   so it is excluded here rather than overridden); outline-warning gets its --wa back. */
.ds-cta-busy.btn.htmx-request:disabled:not(.btn-primary):not(.btn-warning),
.ds-cta-busy.btn[aria-busy="true"]:disabled:not(.btn-primary):not(.btn-warning) {
    color: oklch(var(--bc));
}
.ds-cta-busy.btn-primary:not(.btn-outline).htmx-request:disabled,
.ds-cta-busy.btn-primary:not(.btn-outline)[aria-busy="true"]:disabled {
    color: oklch(var(--pc));
}
.ds-cta-busy.btn-warning:not(.btn-outline).htmx-request:disabled,
.ds-cta-busy.btn-warning:not(.btn-outline)[aria-busy="true"]:disabled {
    color: oklch(var(--wac));
}
.ds-cta-busy.btn-outline.btn-warning.htmx-request:disabled,
.ds-cta-busy.btn-outline.btn-warning[aria-busy="true"]:disabled {
    color: oklch(var(--wa));
}

/* ─── Stat tile (small tinted number tile) ─── */
.ds-stat-tile {
    border-radius: var(--radius-tile);
    padding: 0.5rem 0.75rem;
    text-align: center;
}
.ds-stat-tile--neutral { background: oklch(var(--b2)  / 0.50); }
.ds-stat-tile--primary { background: oklch(var(--p)   / 0.10); }
.ds-stat-tile--success { background: oklch(var(--su)  / 0.10); }
.ds-stat-tile--error   { background: oklch(var(--er)  / 0.10); }
.ds-stat-tile--warning { background: oklch(var(--wa)  / 0.10); }
.ds-stat-tile--info    { background: oklch(var(--in)  / 0.10); }
.ds-stat-tile--accent  { background: oklch(var(--a)   / 0.10); }

/* ─── Info callout (replaces raw DaisyUI `alert alert-info`) ───
   Muted info-tint surface (mirrors .ds-stat-tile--info) instead of the fully
   saturated DaisyUI `alert-info` fill, whose bg ran ~4.4× brighter than the card
   canvas in dark theme — the loudest element on the page (seo-mx98). Text inherits
   base-content (WCAG-safe in both themes); the icon keeps the info accent so the
   callout stays recognisable. Used by access-state banners on /tasks/{id},
   /projects/{id} and step_detail. */
.ds-callout-info {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-control);
    background: oklch(var(--in) / 0.10);
    color: oklch(var(--bc) / 0.85);
    font-size: 0.875rem;
    line-height: 1.25rem;
}
.ds-callout-info svg {
    color: oklch(var(--in));
    flex-shrink: 0;
}

/* ─── Selection bar (bulk action toolbar — «Выбрано: N» + actions) ───
   Muted primary-tint surface for the row-selection action bar. Distinct from
   .ds-callout-info (info notification): primary tint signals an active selection
   affordance, not an info message (seo-1bz0). Replaces raw DaisyUI `alert
   alert-info`, whose saturated cyan fill matched the access-banner loudness defect
   fixed in seo-mx98. Text inherits base-content (NOT primary-coloured → no WCAG
   lift needed); action buttons keep their own btn styling. Used by the keyword
   table bulk toolbar on /tasks/{id}. */
.ds-selection-bar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-control);
    background: oklch(var(--p) / 0.10);
    color: oklch(var(--bc) / 0.85);
    font-size: 0.875rem;
    line-height: 1.25rem;
}

/* ─── Tab counter badge ─── */
.ds-tab-counter {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 1.75rem;
    min-width: 1.75rem;
    padding: 0 0.5rem;
    border-radius: 9999px;
    font-size: 0.75rem;
    font-weight: 700;
    background: oklch(var(--bc) / 0.08);
    /* seo-v4m1.8 (rcp4 sweep): base counter lifted /0.5 → /0.68 so the STANDALONE
       .ds-tab-counter (page-title total pill, data_table.html:32) reaches WCAG AA in
       both themes (was dark 3.7:1 / light 3.08:1). rcp4's earlier fix only covered the
       in-`.ds-tab-track` counters; the title pill lives outside the track. The in-track
       inactive override is already /0.68; the active counter keeps its own override. */
    color: oklch(var(--bc) / 0.68);
    font-variant-numeric: tabular-nums;
}

/* ─── Brand logo square ─── */
.ds-brand-logo {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--icon-tile-md);
    height: var(--icon-tile-md);
    border-radius: var(--radius-tile);
    background: oklch(var(--p));
    color: oklch(var(--pc));
    font-weight: 700;
    font-size: 1.125rem;
    flex-shrink: 0;
    text-decoration: none;
}
.ds-brand-logo--sm {
    width: var(--icon-tile-sm);
    height: var(--icon-tile-sm);
    font-size: 0.875rem;
}

/* ─── Dropdown item (interactive row inside any dropdown/popover) ─── */
.ds-dropdown-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.375rem 0.5rem;
    font-size: 0.875rem;
    border-radius: var(--radius-control);
    transition: background 0.15s;
    cursor: pointer;
    color: inherit;
    text-align: left;
    border: none;
    background: transparent;
    text-decoration: none;
}
.ds-dropdown-item:hover {
    background-color: oklch(var(--p) / 0.12);
}
/* Inset focus ring — items live in a tight popover surface, outset offset
   would clip on the popover edge. Without this rule Chromium falls back to
   the UA default `outline: auto 1px rgb(16,16,16)` which `color-scheme: dark`
   auto-inverts to white over the dark popover — visible artefact reported
   on /projects column-filter dropdown in dark theme. (seo-2af8) */
.ds-dropdown-item:focus-visible {
    outline: 2px solid oklch(var(--p));
    outline-offset: -2px;
}
/* Disabled item: the hover plate is a «you can press this» signal, so a dead
   item must not paint it, and the cursor says so. pointer-events stay ON —
   that is what lets the native `title` reason render (seo-x1w8); the
   `opacity-50` ink comes from the callsite (canonical_components.md
   «Disabled item»). Before this rule every disabled ds-dropdown-item still
   lit up primary/0.12 under the mouse (Codex R2 on seo-j58t). */
.ds-dropdown-item:disabled,
.ds-dropdown-item[aria-disabled="true"],
.ds-dropdown-item:disabled:hover,
.ds-dropdown-item[aria-disabled="true"]:hover {
    background-color: transparent;
    cursor: not-allowed;
}
/* Destructive item: role colour ONLY while enabled — a disabled destructive
   item drops to the neutral disabled pattern like every other control
   (seo-chm8 rule, canonical_components.md). Per-theme AA for 14px text on
   the popover surface: raw --er measured ~4.0:1 on white and ~3.1:1 on --b2
   — sub-AA in both themes (seo-chm8 side-finding), so the modifier carries
   its own values: corporate 0.55/0.19/25 = 5.35:1 on white, business
   0.72/0.14/25 = 5.04:1 on b2. Hover keeps the error-tinted plate instead of
   the primary one (source order after .ds-dropdown-item:hover decides). */
.ds-dropdown-item--danger { color: oklch(0.55 0.19 25); }
.ds-dropdown-item--danger:hover { background-color: oklch(var(--er) / 0.10); }
[data-theme="business"] .ds-dropdown-item--danger { color: oklch(0.72 0.14 25); }
.ds-dropdown-item--active {
    background-color: oklch(var(--p) / 0.1);
    color: oklch(var(--p));
}
/* Active-filter hint in pagination_info ("(фильтр)") — primary, per-theme AA-lifted
   below (was text-primary/50 = 1.78:1 on white, hard sub-AA). seo-cetn. */
.ds-filter-hint {
    color: oklch(var(--p));
}
/* Once the user hovers any item in a popover, the active pill is dismissed
   for the remainder of this open cycle (reset on next open). Parent toggles
   .ds-dropdown-touched via Alpine on first @mouseenter of any item. */
.ds-dropdown-touched .ds-dropdown-item--active:not(:hover) {
    background-color: transparent;
    color: inherit;
}

/* Applied (pressed) state of a filter-PRESET control — seo-sn7w. The muted NEUTRAL
   pill the values chips already live in (.ds-filter-chip = bc/0.08) plus a hairline,
   deliberately not the primary tint of .ds-segmented-on / .ds-dropdown-item--active:
   on the keyword toolbar those two already mean «menu open» and «option selected»,
   and a saturated accent on a secondary utility is the loudness class the CLAUDE.md
   anchor warns about. The pill is measured at 1.23:1 (light) / 1.31:1 (dark) against
   the card — enough to read as a surface, quiet enough not to compete with the CTA;
   the bc/0.35 ring is the part that separates a PRESSED CONTROL from the borderless
   .ds-filter-chip tokens of the same family.

   Why a ds class instead of `bg-base-content/10 border-base-content/20` utilities:
   those live ONLY in the static daisyui build (the Play CDN cannot generate them —
   its config carries no DaisyUI colors), so on a host that declares its own
   `background` / `border` shorthand in THIS file (.ds-dropdown-item) they lose the
   cascade and the mobile preset item rendered unpainted.
   The ring is an INSET box-shadow, not a border: the two hosts disagree about border
   (.btn-ghost has 1px transparent, .ds-dropdown-item has none), so a real border
   would shift the menu item's box by 2px only in the applied state. Ring alpha 0.35,
   not the fill-family 0.20: at 0.20 it measured 1.28–1.7:1 against its own fill —
   sub-JND at 1px, so the pressed pill was indistinguishable from the borderless
   .ds-filter-chip tokens below it (ensemble R1, three lenses converged). */
.ds-preset-on {
    background-color: oklch(var(--bc) / 0.10);
    box-shadow: inset 0 0 0 1px oklch(var(--bc) / 0.35);
}
/* seo-uhf3: the STATE outranks the pointer. The .btn-ghost host's plain hover is
   bc/0.20 — twice the applied bc/0.10 fill — so hovering an OFF preset painted it
   stronger than an ON one at rest (measured 1.52:1 vs 1.22:1 light, 1.75 vs 1.31 dark;
   interactive-states lens of the seo-0dqt ensemble). `.ds-preset` is the control's
   permanent class (both hosts): on the .btn host its hover is the quiet bc/0.08 the
   .ds-icon-btn family already uses — the (0,3,0) rule beats .btn-ghost:hover (0,2,0).
   The .ds-dropdown-item host keeps its primary/0.12 accent hover: inside a popover
   hover is the cursor-highlight convention shared by every item, and the applied item
   still carries its ring + neutral fill through it (rule below). Rank on the button:
   OFF rest 0 < OFF hover 0.08 < ON rest 0.10 + ring < ON hover 0.16. */
.ds-preset.btn:hover {
    background-color: oklch(var(--bc) / 0.08);
}
/* Hover must never REPAINT the state away. Both hosts' :hover rules are (0,2,0) and
   used to win over .ds-preset-on's (0,1,0) fill — ON+hover rendered byte-identical to
   OFF+hover (ensemble R1: md5-equal element crops), so «is it on?» became unanswerable
   exactly while the pointer interrogated the control. These (0,3,0) rules keep the
   applied fill in the NEUTRAL bc family on both hosts: bc/0.16 — one step above the
   0.10 rest and above the button's own 0.08 OFF hover (was 0.26 while the OFF hover
   was 0.20, seo-uhf3); on the .ds-dropdown-item host it replaces the primary/0.12
   accent tint that would otherwise repaint the neutral state fill outright. The inset
   ring persists through hover either way. */
.ds-preset-on.btn:hover,
.ds-preset-on.ds-dropdown-item:hover {
    background-color: oklch(var(--bc) / 0.16);
}

/* ─── Surface backgrounds (replaces raw bg-base-N) ─── */
.ds-bg-base  { background-color: oklch(var(--b1)); }
/* seo-kl3g: renamed .ds-bg-dim → .ds-bg-elevated. After seo-ojuq token unification,
   --b2 in business theme is BRIGHTER than --b1 (popover-elevated navy at 30.8% L
   vs card 26.4% L) — so this surface visually elevates above .ds-mc-card body,
   не dims. Light theme: --b2 = 93% L grey (slightly elevated from 100% --b1).
   Semantic naming aligned with behavior in both themes. */
.ds-bg-elevated   { background-color: oklch(var(--b2)); }
.ds-bg-deep  { background-color: oklch(var(--b3)); }

/* seo-d190: the keyword table's source chip («Вордстат N»). It used to be painted with
   .ds-bg-elevated, i.e. plain --b2 — which lifts 7.0 L-points over white but only 4.4
   over the dark card, so in business the chip lost its silhouette entirely: 1.16:1 fill,
   while the 1px row divider crossing the same column reads 1.45:1. A chip quieter than
   the hairline around it is not a chip; the label just floated in the cell.
   Own class rather than a lift of .ds-bg-elevated (Codex review): that utility also
   paints the clustering panel's stat tiles, where the neutral one sits deliberately at
   the same weight as its bg-success/10 and bg-secondary/10 neighbours — lifting it there
   would have made «Без кластера» the loudest tile of the row for no reason.
   Dark measured on the rendered fill over .ds-mc-card: 1.47:1, the divider's own weight,
   with chroma nudged 0.038 → 0.042 so it separates by tint as well as by lightness (the
   way the neighbouring bg-info/15 chip survives). Light inherits --b2 unchanged — there
   the chip (1.23) and the divider (1.24) are already at parity. */
.ds-source-chip { background-color: oklch(var(--b2)); }
[data-theme="business"] .ds-source-chip { background-color: oklch(0.37 0.042 261); }

/* The pipeline's «you are here» marker — seo-9wku. Modernize active-tab idiom: NO
   surface, NO ring — the owner's L4 verdict on the previous filled+ringed tile
   (seo-0deo) was «standard AI design, not Modernize»; and the ensemble read that
   composition (a lit panel with a hollow circle, beside siblings with the same circles)
   as a segmented toggle with its 2nd option selected — the loudest element of the row
   was the one whose action does NOT live there (the run button sits above).
   The class goes on the LABEL span (template adds `font-semibold` beside it): the
   marker is the label's weight and a 2px accent bar right under the word —
   content-width, like Modernize's non-fill nav-tab, NOT the tile's width. The R1
   ensemble killed the tile-anchored variant: a 335px bar under a 111px label
   underlined the flex-1 hit area and read as a filled progress-track segment («the leg
   to here is done» — contradicting the ○ glyph and the «ещё не запущена» line above),
   and `bottom:0` of an `items-start` tile made the bar's Y status-dependent (a current
   step with a badge row is two lines tall, so the bar dropped under the badge, 22px
   below the label it marks). Anchored to the label it is the same shape on every rung
   of is_current.
   The label keeps the ROW'S ink, deliberately not primary (R1, four lenses): primary
   text + a line under it is the web's strongest «link» costume, and it landed on the
   one tile the user has least reason to click, while the three siblings that really
   navigate read as inert labels beside it. One accent channel — the bar — is the
   active-tab idiom; two (blue text + bar) is a hyperlink.
   The bar is a text-decoration, not a border: it paints INSIDE the label's 20px line
   box, so the box itself is the siblings' and the badge row below (on a running /
   error / paused current tile) starts exactly where it starts on a sibling. A border
   needs room — and paying it back with a negative margin (the first cut) put the badge
   row's top 3px over the bar, the badge tint compositing on the marker (Codex R1 MUST,
   measured: row top 505.5, bar 506.5–508.5). `skip-ink:none` keeps the bar continuous
   under descenders («Кластеризация», «Релевантность») — this is a marker, not a
   hyperlink underline. Offset 3px clears the descenders and stays inside the box
   (`truncate` = overflow:hidden on the same span — a bar pushed below the box would be
   clipped). Chrome/Firefox/Safari all paint `text-decoration-thickness` / `-offset`.
   Bar ink = the project's «pressed/selected» primary ink, not a marker-own literal:
   light 0.52 = the btn-primary / checkbox-primary:checked fill (component-overrides,
   seo-60gh; 5.6:1 on white), dark 0.60 = the edge those same controls draw in business
   (3.85:1 on --b1, the 1.4.11 non-text floor the 0.52 fill alone misses at 2.75:1). Not
   the text family's dark lift (0.73): R1 hierarchy measured that bar at 6.35:1 against
   2.75:1 for the run CTA's fill 40px above it — the eye landed on the pipeline rule
   before the button. Any bar that clears 3:1 on the dark card is lighter than that
   fill; 0.60 is the lowest member of the family that does, and it is the CTA's own
   edge ink, so the marker and the button read as one primary.
   The current tile takes the row's generic hover like its siblings (the old own-hover
   existed only to survive the generic `hover:bg-base-200/50` — a daisyUI utility from
   daisyui.min.css, no !important — repainting the marker FILL, which no longer
   exists; seo-pqb6 corrected the old «Play-CDN !important» wording). */
.ds-step-current {
    text-decoration: underline;
    text-decoration-color: oklch(0.52 0.12 264);
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
    text-decoration-skip-ink: none;
}
[data-theme="business"] .ds-step-current { text-decoration-color: oklch(0.60 0.12 264); }

/* ─── Divider borders ─── */
/* Replaces border-base-200 / border-base-300 (DaisyUI semantic, theme-aware) */
.ds-border-base        { border-color: oklch(var(--b2)); }
.ds-border-base-strong { border-color: oklch(var(--b3)); }
.ds-border-base-strong-half { border-color: oklch(var(--b3) / 0.5); }
/* Replaces border-gray-100 dark:border-white/5 (raw Tailwind + manual dark override) */
.ds-border-muted { border-color: oklch(var(--b2)); }
/* seo-nzfy / seo-5oip: in dark the section divider (--b2 = rgb(37,48,67) on the
   card's rgb(28,37,54)) came out WEAKER than a table's own row rules
   (rgb(51,63,85)) — the strongest lines on the page marked its least important
   boundary, and a table's bottom edge outweighed the structural divider beneath
   it. Light already sits at parity (0.93 vs rgb(229,231,235)). Only the BORDER
   token is lifted; `--b2` itself stays put because it is also a surface (D24
   chip fills depend on it). */
[data-theme="business"] .ds-border-muted { border-color: oklch(0.37 0.038 261); }
/* seo-v0ot R4: the same class of bug one token over. `--b3` in business resolves to
   rgb(26,34,48) — DARKER than the card it draws on (rgb(28,37,54)), 1.04:1. Anywhere
   this class paints a line in dark it painted nothing: on /help/task-statuses the
   4px accent strip of «Пропущена» and «Черновик» (the two neutral border_class rows
   of task_status_meta.py) was simply absent, while the eight chromatic siblings run
   4.05–7.47:1. Light is untouched: there --b3 is rgb(209,209,209) on white = 1.53:1,
   a visible hairline already. Chosen value rgb(83,91,105) = 2.24:1 composited on the
   card — deliberately between the lifted `.ds-border-muted` section divider above
   (1.47:1) and the quietest chromatic accent (border-error, 4.05:1): a neutral status
   marker must read, but must not outrank a semantic one. Only the BORDER token moves;
   `--b3` itself stays put because it is also a surface.
   WHY `:where()` (same idiom and same reason as seo-s0aw in component-overrides.css —
   see the specificity note there): a hover utility generated for this token sits at the
   SAME (0,2,0) as a bare `[data-theme="business"] .ds-border-base-strong`
   (`.hover\:border-primary\/40:hover` — one class + one pseudo-class), and the bare form
   won on order. Measured on the task-picker cards of /projects/{id}: in dark, rest AND
   hover both painted rgb(83,91,105) — the hover accent was gone, while light (no
   override) moved to rgb(102,138,212)/0.40 correctly. `:where()` contributes zero, so
   this drops to (0,1,0): rest keeps the lift, hover gets primary/40 back. A plain
   `[data-theme=…]` prefix here is a regression, and the pin asserts the `:where()` form.
   Pin: tests/test_design_system_border_base_strong_dark.py */
:where([data-theme="business"]) .ds-border-base-strong { border-color: oklch(0.47 0.025 261); }

/* ─── Shell paint (header / sidebar / footer-divider in admin + user shells) ─── */
/* Narrow utilities, deliberately split: shell layouts use one or the other (header
   needs both, footer divider only border). Do NOT shorthand into `.ds-shell-surface`. */
.ds-shell-bg      { background: var(--surface-body); }
.ds-shell-divider { border-color: var(--surface-popover-border); }

/* ─── Ghost element hover (replaces hover:bg-gray-50/100 dark:hover:bg-white/5) ─── */
/* hover darkens the bg, so a muted label (text-base-content/40–70 on pagination /
   per-page / bulk-op controls) would drop below AA on the darker hover surface (seo-vpyg:
   pagination 4.34, per-page). hover also STRENGTHENS the label to full base-content — an
   affordance + AA fix in one, mirroring .ta-menu-item:hover. The bulk-op trigger's inline
   :style (selected→bc) still wins when an action is chosen; this covers the at-rest→hover. */
.ds-hover-muted:hover { background-color: oklch(var(--b2) / 0.6); color: oklch(var(--bc)); }

/* ─── seo-l8nl + seo-b3mg: shared chromatic-badge AA contrast (status_badge + module_badge) ───
   The `bg-{c}/10 text-{c}` tinted-pill pattern fails WCAG AA: success/warning in
   corporate only; error/info/accent in BOTH themes; neutrals (text-base-content/40,/50
   on bg /15) in both. COLOR-PRESERVE (status colour is a load-bearing signal): keep each
   --{c} token's hue+chroma, adjust only lightness per theme for ≥4.5:1 on the /10 tint
   over the surface. Values derived from the live tokens (canvas oklch→sRGB + alpha-
   composite, binary-searched). Supersedes the b3mg corporate-only neutralize AND fixes
   module_badge's dark error/info/accent gap. business success/warning already pass → untouched. */
/* seo-s469: corporate success/warning/error/info darkened (L only, hue+chroma kept) to clear
   AA on the GREYER real surfaces too (data_table hover oklch(--b2/0.4); legacy user_detail
   bg-base-200/30) — the seo-l8nl values cleared AA on base-100 only. accent corporate + all
   business values already passed on the grey tint, untouched. */
[data-theme="corporate"] .ds-badge-tint.text-success { color: oklch(0.476 0.12 160) !important; }
[data-theme="corporate"] .ds-badge-tint.text-warning { color: oklch(0.508 0.12 70) !important; }
[data-theme="corporate"] .ds-badge-tint.text-error   { color: oklch(0.529 0.223 27.3) !important; }
[data-theme="corporate"] .ds-badge-tint.text-info    { color: oklch(0.476 0.12 231.6) !important; }
[data-theme="corporate"] .ds-badge-tint.text-accent  { color: oklch(0.503 0.022799 163.57888) !important; }
/* seo-3zo: primary member of the badge-tint family — raw text-primary (--p 0.64)
   on bg-primary/10 = 3.07:1 light / 3.93:1 dark < AA. No current consumer
   (the last one went neutral in seo-t0n9) — kept so the family stays complete
   and a future primary badge lands AA by default. */
[data-theme="corporate"] .ds-badge-tint.text-primary { color: oklch(0.49 0.12 264) !important; }
/* seo-rb0n R1 (dark-theme-legibility): business warning had no counterpart to the
   corporate lift above, so the chip label painted the raw --wa. Measured on the real
   fills: dark «Неполно» 6.19:1 vs its row-neighbour «Готово» 4.90:1 — the warning chip
   26% louder — while LIGHT ranks them the other way («Готово» 5.58 > «Неполно» 5.17).
   One pipeline row, two chips, and their loudness order flipped between themes. Pinned to
   the family L (error 0.749 / primary 0.73 / accent 0.702 / info 0.695) at warning's own
   hue+chroma, mirroring the corporate line: 6.19 → 5.43:1, still comfortably over AA and
   11% (not 26%) above the success chip. Touches every dark warning badge — «Неполно»,
   «Ожидает действий», «Пауза (лимит)», «Частично выполнена» — on purpose: the point is
   that the whole warning family stops out-shouting its siblings in dark. NOT «На
   проверке»: seo-l0kg moved awaiting_review off the warning slot onto its own review
   tint, so this rule never reaches it. */
[data-theme="business"]  .ds-badge-tint.text-warning { color: oklch(0.74 0.12 70) !important; }
[data-theme="business"]  .ds-badge-tint.text-error   { color: oklch(0.749 0.223 27.3) !important; }
[data-theme="business"]  .ds-badge-tint.text-info    { color: oklch(0.695 0.12 240.033697) !important; }
[data-theme="business"]  .ds-badge-tint.text-accent  { color: oklch(0.702 0.12 35.791525) !important; }
[data-theme="business"]  .ds-badge-tint.text-primary { color: oklch(0.73 0.12 264) !important; }
.ds-badge-tint.text-base-content\/40,
.ds-badge-tint.text-base-content\/50 { color: oklch(var(--bc) / 0.70) !important; }
/* seo-tgw: neutral /40 (archived/cancelled/disabled) + /50 (pending) badges sit on their
   OWN bg-base-content/15 fill, which is LIGHTER than the bare row surface — so the shared
   seo-4cec row-chrome lift's dark value (0.60, tuned for base-100 cell text) drops the badge
   to 4.05:1 at rest / 3.93:1 on hover < AA. The lift now :not(.ds-badge-tint)-excludes badges
   (see L758-) so they fall here; dark needs more alpha than the 0.70 base for the lighter
   badge fill: 0.75 → 5.17:1 on the greyest badge surface (data_table row-hover). Light passes
   at 0.70 (5.13:1) → stays on the base rule above (don't darken a passing muted badge). */
[data-theme="business"] .ds-badge-tint.text-base-content\/40,
[data-theme="business"] .ds-badge-tint.text-base-content\/50 { color: oklch(var(--bc) / 0.75) !important; }

/* ─── seo-r3fp: business badge FILLS get explicit colors (hue-preserving) ───
   The alpha composite `oklch(var(--{c})/0.1)` over the navy surface destroys a
   warm paint's hue family — computed + bead-measured (paint→/10 composite):
   warning 81.5°→261.7°, success 155.7°→240.6°, error 27.5°→308.9°. Two
   semantically OPPOSITE chips («Недоступен» actionable vs «Не проверялся»
   default) collapsed into one neutral grey род, distinguishable only by text
   paint. Explicit fills restore each family's hue AT the same loudness band —
   amp vs row 1.21/1.23/1.14:1 (was 1.21/1.18/1.13) — and keep text AA against
   the actual painted text colors: warning 6.19:1 (raw --wa), success 4.91:1
   (raw --su), error 4.65:1 (the 0.749 lift above). info was left on the composite
   here (its hue survives the shift) and joined the explicit-fill set later in
   seo-nut5 — for hover-independence of the label's contrast, not for hue.
   Corporate fills stay alpha-based — light composites keep their hue. */
[data-theme="business"] .ds-badge-tint.bg-warning\/10 { background-color: oklch(0.32 0.045 85); }
[data-theme="business"] .ds-badge-tint.bg-success\/10 { background-color: oklch(0.32 0.04 157); }
[data-theme="business"] .ds-badge-tint.bg-error\/10   { background-color: oklch(0.31 0.045 27); }
/* seo-nut5: info was the one hue family still on the alpha composite in business —
   `bg-info/15` («Вручную» source chip, keyword table) and `bg-info/10` (module badge
   paused_user, «Итерация N», freq-type / stage chips) — so its fill, and with it the
   label's contrast, depended on whatever lay UNDER the chip: 4.71:1 on the bare row,
   4.48:1 once the .ds-tr-hover tint slid in (< AA at 12px/500; light 5.60/5.22 was
   fine). An explicit hue-preserving fill, like the three above, makes the ratio
   hover-independent: text 0.695 on L 0.31 = 4.88:1, fill vs card 1.17:1 — inside the
   1.15–1.23 band of its siblings. Both alpha steps land on the same fill: the /15
   variant was only ever a hair louder than /10 on light. */
[data-theme="business"] .ds-badge-tint.bg-info\/10,
[data-theme="business"] .ds-badge-tint.bg-info\/15 { background-color: oklch(0.31 0.045 240); }
/* seo-u7aa: the NEUTRAL fill needs the same treatment — bg-base-content/15
   is an alpha of near-white bc over navy, composing to 1.51:1 vs the card,
   brighter than every hue fill above (1.15-1.23:1): «no state» was the
   loudest plate in a dark card. Explicit low-chroma navy-family value lands
   at ~1.18:1, dead centre of the hue-fill band; the seo-tgw bc/0.75 text
   rule only gains contrast on the darker fill.
   seo-7ds1: the u7aa value had the right L but chroma 0.01 — rgb(46,49,54), the ONE
   achromatic plate in a card whose every other fill (ghost button, banner, hue
   badges, the card itself) carries C 0.028–0.045: grey-on-navy (seo-ojuq class).
   Chroma alone moves to 0.033 → rgb(40,49,66); L stays, so the 1.18:1 loudness
   holds and the seo-tgw bc/0.75 label keeps its 6.5:1. The /10 (module_badge
   pending, role_badge user, ai_providers «Неактивен», step_detail) and
   bg-base-200/60 (module_badge skipped, data_sources «Не проверялся») siblings
   join the same token: /10 composited to 1.31:1 — the loudest plate in a pipeline
   row whose state is «nothing happened yet» — and b2/60 to 1.09:1, under the band.
   LIGHT is the same inversion one theme over: bc/0.10 = 1.22:1 and bc/0.15 =
   1.36:1 on white against hue fills at 1.105–1.139:1 («Не начат» louder than
   «Готово» by ~55% of fill contrast, seo-181d ensemble). oklch(0.96 0.006 262) =
   1.12:1, mid-band; bc/0.70 label on it 6.0:1. Pin: tests/test_design_system_neutral_badge_fill.py. */
[data-theme="corporate"] .ds-badge-tint.bg-base-content\/10,
[data-theme="corporate"] .ds-badge-tint.bg-base-content\/15,
[data-theme="corporate"] .ds-badge-tint.bg-base-200\/60 { background-color: oklch(0.96 0.006 262); }
[data-theme="business"] .ds-badge-tint.bg-base-content\/10,
[data-theme="business"] .ds-badge-tint.bg-base-content\/15,
[data-theme="business"] .ds-badge-tint.bg-base-200\/60 { background-color: oklch(0.313 0.033 262); }

/* ─── seo-l0kg: «На проверке» (awaiting_review) own hue-stable token ───
   The DaisyUI accent slot is NOT hue-stable across themes (corporate #67cba0
   green 163.6° vs business #EA6947 salmon 35.8°): the ONE status pinned to it
   flipped color family in dark — landing 8.5° from «Ошибка» (27.3°) — while in
   light its green sat 3.6° from success (160°). Signed L4 2026-07-20: own teal
   ~195° token — the free zone between success (157°) and info (240°), ≥35°
   from every occupied slot in BOTH themes. Covers all three ex-accent
   consumers: status_badge fill+text (task_status_meta badge_class), the /help
   left stripe (border_class), module_badge. Text-on-fill: 5.9:1 light /
   5.25:1 dark; stripe vs surface: 4.6:1 / 4.9:1. The .ds-badge-tint.text-accent
   overrides above are KEPT for family completeness (same rationale as
   text-primary) — production badges no longer carry accent. */
[data-theme="corporate"] .ds-tint-review { background-color: oklch(0.955 0.02 195); color: oklch(0.46 0.09 195); }
[data-theme="business"]  .ds-tint-review { background-color: oklch(0.32 0.04 195); color: oklch(0.72 0.10 195); }
[data-theme="corporate"] .ds-border-review { border-color: oklch(0.55 0.10 195); }
[data-theme="business"]  .ds-border-review { border-color: oklch(0.65 0.09 195); }

/* ─── seo-4ba0: breadcrumb-bar text contrast (both themes) ───
   The bar base is lifted to oklch(var(--bc)/0.70) in the markup (admin/dashboard
   layouts) — fixes resource-page crumb links that inherit it. Pattern-A admin
   crumbs carry explicit /50 (label) + /30 (separator) and the chevron svg is
   opacity-30; all rendered ~1.9–4.1:1 (fail). Lift them to /0.70 (≥4.5 AA both
   themes) without touching each page's breadcrumb block.
   seo-0wpd: the chevron override is opacity 1, NOT 0.70 — the svg inherits the
   bar's color, which is ALREADY bc/0.70, so any svg opacity MULTIPLIES it: the
   bead-time markup (color /0.70 × raw opacity-30 = bc/0.21) measured 1.53:1
   light, and seo-4ba0's first cut (opacity 0.70 → effective bc/0.49) still
   kept a second layer nobody designed. One layer carries the whole value:
   separator glyph = crumb-label ink, the same inherit-the-label law as the
   seo-iiix/seo-fg5z carets. Post-fix stroke peak measured 5.87:1 light /
   6.25:1 dark. */
.ds-breadcrumb-bar .text-base-content\/50,
.ds-breadcrumb-bar .text-base-content\/30 {
    color: oklch(var(--bc) / 0.70) !important;
}
.ds-breadcrumb-bar svg.opacity-30 {
    opacity: 1 !important;
}

/* ─── WCAG AA per-theme lifts for primary-tinted text zones ───
   Default `color: oklch(var(--p))` against primary/0.08–0.12 tint over the
   body bg gives ~3.3:1 in both themes — below SC 1.4.3 AA (4.5:1). Mirror
   the .ta-menu-item.active pattern (defined in component-overrides.css):
   darken primary on light (`oklch(0.49 ...)`, seo-oeub) and lighten on dark
   (`oklch(0.73 ...)`) so every chip/pill/counter on /admin/tasks passes AA. */
[data-theme="corporate"] .ds-search-chip,
[data-theme="corporate"] .ds-search-chip-exclude:hover,
[data-theme="corporate"] .ds-dropdown-item--active,
[data-theme="corporate"] .ds-filter-hint,
[data-theme="corporate"] .ds-tab-active .ds-tab-counter,
[data-theme="corporate"] .ds-role-badge--admin,
[data-theme="corporate"] .ds-segmented-on {
    color: oklch(0.49 0.12 264);
}
/* Segmented toggle ACTIVE segment (AND/OR search-mode in data_table) — same primary/0.1
   tint zone as the chips above, so it joins the per-theme AA lift. Was raw
   `bg-primary/10 text-primary` = 3.1 light / 3.87 dark (seo-wpmz, the seo-vpyg Alpine-
   conditional-fill follow-up the at-rest detector can't drive). */
.ds-segmented-on { background-color: oklch(var(--p) / 0.1); }
[data-theme="business"] .ds-search-chip,
[data-theme="business"] .ds-search-chip-exclude:hover,
[data-theme="business"] .ds-dropdown-item--active,
[data-theme="business"] .ds-filter-hint,
[data-theme="business"] .ds-role-badge--admin,
[data-theme="business"] .ds-segmented-on {
    color: oklch(0.73 0.12 264);
}
/* Tab counter sits on a doubly-tinted bg (ds-tab-active surface-popover +
   primary/0.10 tailwind overlay) = ~rgb(52,62,80) in business theme.
   L=0.73 gives only 4.33:1 there — bump to 0.75 for AA margin. */
[data-theme="business"] .ds-tab-active .ds-tab-counter {
    color: oklch(0.75 0.12 264);
}

/* ─── seo-dy7 → seo-jwgs → seo-np5b: data_table row-action danger/warning/primary rest colors, per theme ───
   Both themes carry explicit calmed values; hover is the LOUD state and must gain on at
   least one axis without dropping under the floor on the other: chroma up to the full
   token, contrast never under the 3:1 non-text floor. Light: --wa (L 0.555) is darker than
   its 0.62 rest — louder on both axes; --er (0.637) and --p (0.64) are LIGHTER than their
   0.54 / 0.52 rests, so their contrast drops (~3.3:1 / 3.41:1) and the loudness is
   chroma-only — accepted since seo-dy7, floor kept. Dark (rests as re-cut in seo-2l4a:
   warning 0.66, danger 0.71, primary 0.72): --wa (0.775) is lighter than the warning
   rest but since seo-2l4a its hover is an explicit 0.72/0.08/80 so the pair keeps its
   order under the pointer; --er (0.637 < 0.71) and --p (0.64 < 0.72) were darker than
   their rests on a dark surface — contrast fell (4.05:1, 3.72:1 on the hover pill) with
   no chroma gain worth the loss — so those two dark hovers are explicit brighter values
   (seo-np5b, below)
   (loudness on INTENT is fine — REST must not outrank row identity/status).
   History: seo-dy7 desaturated dark but pinned BOTH roles to L=0.62 — the two
   roles merged for a deuteranope (OKLab separation 0.0756 vs light 0.1668,
   Vienot sim distance 0.0186); seo-s8lv then raised the LIGHT alphas to clear
   the 3:1 non-text floor — an axis a readability oracle rewards — leaving the
   light delete icon at C=0.221: 1.84x the page's only primary CTA and ~7x the
   row identity text (seo-jwgs live measurements). Fix, both axes at once:
     LIGHT: cap chroma with explicit values (the mirror of what dark already
       did) and split L wide enough for a deuteranope: at danger 0.56 /
       warning 0.60 the Vienot-sim pair distance was 0.071 — only 1.4x the
       BROKEN dark state this bead exists to fix (0.049); 0.54 / 0.62 lands
       0.135, equal to dark's fixed pair. danger 0.54/0.09/25 = 5.29:1,
       warning 0.62/0.065/80 = 3.66:1 (>= 3:1 floor AND >= the disabled
       glyph bc/0.5 = 3.32:1 — live must not drop under dead). Chroma rank
       stays: identity 0.031 < warning 0.065 < danger 0.09 < CTA 0.12 <
       status badge 0.141 — destructive-at-rest outranks nothing load-bearing.
     DARK: danger raised 0.62 -> 0.67 (4.93:1 on b1) to restore a lightness
       delta; warning CANNOT move down instead: below L~0.60 a LIVE warning
       glyph drops under the DISABLED glyph (bc/0.50 = 4.13:1 on b1) — the
       seo-zyfg live-below-dead inversion on another axis. Warning keeps
       0.62/0.06/80 (4.21:1); delta-L 0.05 plus the hue axis is what a
       deuteranope gets — verified by sim at ship time, see the bead.
   All :hover rules are explicit per theme — no source-order race with the
   overrides. NEVER write the wildcard form with star-slash inside a CSS
   comment: it terminates the comment early and the parser then EATS the first
   rule below while error-recovering; this silently killed
   .ds-row-action-danger in 9a01b26, caught only by a live computed-style
   probe. */
.ds-row-action-danger { color: oklch(0.54 0.09 25); }
.ds-row-action-danger:hover { color: oklch(var(--er)); }
[data-theme="business"] .ds-row-action-danger { color: oklch(0.71 0.09 25); }
/* seo-np5b: dark hover is NOT the token here either — business --er is L 0.637, darker
   than the rest (0.67 then, 0.71 since seo-2l4a), and at 4.05:1 on the card it fell UNDER the rest's 4.93 (and
   under AA) on hover; 0.76/0.12/25 = 6.82:1 is brighter and more chromatic than rest,
   the direction a hover must go. --wa (L 0.775) is lighter than its 0.62 rest, so the
   warning hover keeps the token. */
[data-theme="business"] .ds-row-action-danger:hover { color: oklch(0.76 0.12 25); }
.ds-row-action-warning { color: oklch(0.62 0.065 80); }
.ds-row-action-warning:hover { color: oklch(var(--wa)); }
/* seo-2l4a: the dark warning rest was 0.62 — the SAME lightness as light, i.e. the one
   role in the family with no dark lift: 4.21:1 on the card, under the trash beside it
   (4.93), under the «—» placeholder (4.79) and under the disabled «Применить» label
   (4.41) — the only live glyph in the row read quieter than a «no data» mark. The first
   cut (0.72 = 6.17:1) fixed that and INVERTED the pair: the reversible archive out-shouted
   the destructive trash on peak contrast, OKLab distance and glyph ink energy alike, and
   the dark hover was already inverted (--wa 7.47 over the danger hover 6.82) — the two
   themes disagreed about which of two adjacent buttons is the dangerous one. So the
   whole dark ladder is cut once, pixel-measured on the /projects card, with the deutan
   ΔL 0.05 between warning and danger kept:
     rest   primary 0.72/0.09/264 = 6.15 ≥ danger 0.71/0.09/25 = 5.73 ≥ warning 0.66/0.06/80
            = 4.94 ≥ the «—» placeholder 4.79 > disabled 4.13
     hover  primary 0.78/0.12/264 = 7.55 ≥ danger 0.76/0.12/25 = 6.82 ≥ warning 0.72/0.08/80
   — every hover louder than its rest, the destructive action on top in both states,
   chroma 0.06 < 0.09 on the warning as before. LIGHT is deliberately untouched at 3.66:1
   (≥ the 3:1 non-text floor): its L 0.62 was set 0.08 above the light danger rest (0.54)
   by seo-jwgs for the same deutan reason, and lifting it to AA (L ≤ 0.57) would close
   that gap to 0.03 — and light already ranks the trash first on all three measures. */
[data-theme="business"] .ds-row-action-warning { color: oklch(0.66 0.06 80); }
[data-theme="business"] .ds-row-action-warning:hover { color: oklch(0.72 0.08 80); }
/* seo-np5b: the PRIMARY role (restore / enable / probe) was the one member of this
   family still painted with the raw token — `text-primary` = oklch(0.64 0.12 264)
   in BOTH themes, so it got no dark lift and measured 3.41:1 on the light row surface
   (below the 4.5:1 the two calmed roles clear) and 4.51:1 on dark — the safe action
   quieter than the destructive trash beside it (5.29 / 4.93). Same shape as the two
   roles above: explicit per-theme rest, hover snaps to the full token. Chroma 0.09 =
   danger's, so the rank «identity 0.031 < warning 0.065 < danger = primary 0.09 <
   CTA 0.12» holds and a rest-state restore never outranks the page's one primary
   CTA; the hue axis (264 vs 25) keeps the pair apart for a deuteranope.
   light 0.52/0.09/264 = 5.54:1, dark 0.70/0.09/264 = 5.72:1 (pixel-measured on the
   card surface, /projects?tab=deleted; seo-2l4a moved the dark rest to 0.72 = 6.15:1 so
   the whole dark ladder reads primary ≥ danger ≥ warning — see that block below). Hover follows the family rule above: light snaps
   to the full --p (0.64/0.12 — chroma-only loudness, 3.41:1 on white, above the 3:1
   non-text floor, the same trade the light danger hover makes); dark CANNOT use --p —
   its L 0.64 is darker than the 0.72 rest (0.70 at the time) on a dark surface, so the glyph dimmed on
   hover (3.72:1 on the hover pill, the dark lens of the seo-np5b ensemble) — dark hover
   is an explicit 0.78/0.12/264 (7.55:1 on the card, ~6.3:1 on the pill), brighter and
   more chromatic than its rest. */
.ds-row-action-primary { color: oklch(0.52 0.09 264); }
.ds-row-action-primary:hover { color: oklch(var(--p)); }
[data-theme="business"] .ds-row-action-primary { color: oklch(0.72 0.09 264); }
[data-theme="business"] .ds-row-action-primary:hover { color: oklch(0.78 0.12 264); }
/* seo-0p0f: the header's ACTIVE marks — the sort caret of the sorted column, the funnel
   of a filtered one and the open search toggle — were the last chain-wide users of the
   raw `text-primary` (0.64/0.12/264 in BOTH themes): in dark the mark that says «this
   column is sorted» measured 4.51:1 on the card while the neutral ↕ chevrons beside it
   measured 5.14:1 — the active indicator dimmer than the inactive ones, and the accent
   hue 264 on a surface of hue 262.7 separates by lightness only. Its own rung of the blue
   tier, pixel-measured (seo-0p0f ensemble, dark + hierarchy lenses): the primary
   row-action pair (0.52 / 0.70) was tried first and collided — light L 0.52 IS the CTA's
   lightness, dark L 0.70 sits on the active tab's (0.75) — and on the search toggle's own
   `bg-primary/10` plate it fell UNDER the button's rest ink (light 4.98 vs 5.77, dark 4.99
   vs 5.85): switching the search ON dimmed the control. Light 0.45/0.10/264 = 7.53:1 on
   the card, 6.77:1 on the plate (≥ rest 5.77; L 0.45 below the tab's 0.49 and the CTA's
   0.52); dark 0.80/0.10/264 = 8.16:1 on the card, 7.12:1 on the plate (≥ rest 5.85; above
   the tab's 6.87 and the chevrons' 5.14 rendered, below the labels' 12.12). Chroma 0.10
   sits between the row actions' 0.09 and the CTA's 0.12. The icon-btn pair outranks
   `.ds-icon-btn`'s own rest/hover by specificity; hover goes louder, never towards the
   surface (light darker + more chroma, dark lighter + more chroma). */
.ds-header-active, .ds-icon-btn.ds-header-active { color: oklch(0.45 0.10 264); }
.ds-icon-btn.ds-header-active:hover { color: oklch(0.40 0.13 264); }
[data-theme="business"] .ds-header-active,
[data-theme="business"] .ds-icon-btn.ds-header-active { color: oklch(0.80 0.10 264); }
[data-theme="business"] .ds-icon-btn.ds-header-active:hover { color: oklch(0.86 0.12 264); }

/* seo-uyks: a row action the user may SEE but not use (data_table row_actions
   `disabled: true`). Painted here rather than left to DaisyUI's .btn:disabled,
   which resolves against --b1 and left the destructive row action on
   /projects/{id} with a chip in light and effectively none in dark — the
   at-rest affordance of a destructive control vanished theme-dependently.
   Same alpha pair as .ds-cta-disabled above, so a blocked row action and a
   blocked CTA are one state, not two strengths. Theme-agnostic by
   construction: --bc inverts with the theme, so 0.06 is a lift on a dark
   surface and a shade on a light one. Hover is pinned to the same values —
   a disabled control must not flash an interactive response. */
.ds-row-action-disabled,
.ds-row-action-disabled:disabled,
.ds-row-action-disabled[disabled],
.ds-row-action-disabled[disabled]:hover,
.ds-row-action-disabled:disabled:hover {
    background-color: oklch(var(--bc) / 0.06);
    color: oklch(var(--bc) / 0.50);
    border-color: transparent;
    cursor: not-allowed;
    /* Same reason as .ds-cta-disabled above: without this the `title` carrying
       the block reason never shows, because DaisyUI's .btn:disabled kills
       pointer events. The native `disabled` attribute still blocks activation. */
    pointer-events: auto;
}
/* seo-vzde: disabled ICONS carry the alpha as GROUP OPACITY, not color alpha.
   Semi-transparent stroke ink is not idempotent under overdraw: where a
   multi-subpath icon crosses itself (the trash lid), bc-alpha-0.50 strokes
   stack toward opaque — junction pixels measured 9.30:1 light / 7.56:1 dark
   while the disabled LABEL beside them composites once at 3.24:1, so the dead
   glyph read livelier than the dead buttons AND than live danger rest (5.29:1).
   Full-ink color + 0.50 group opacity rasterizes the icon first and fades it
   as one layer — every full-coverage pixel, joints included, lands on the
   dictionary value (antialiased edges keep their normal fractional coverage).
   Text needs no such treatment (font glyphs never self-overlap). Keep the 0.50
   in sync with the bc-alpha ink of the two disabled dictionaries above. */
.ds-cta-disabled svg,
.ds-row-action-disabled svg {
    color: oklch(var(--bc));
    opacity: 0.50;
}

/* ─── Sticky actions column (seo-upso) ───
   The row actions sat LAST inside the table's own horizontal scroll container,
   so on any viewport where the grid did not fit they scrolled out of reach —
   1280 and 375 measured on /admin/data-sources and /projects/{id}. Nothing hinted
   that a sideways scroll existed, so «Отключить» / «Удалить» were not merely far
   away, they were undiscoverable. Widening breakpoints or shifting a column to a
   later one only moves the boundary for the pages measured today; pinning the
   column removes the class of defect for every data_table, present and future.

   The row tints must be COMPOSITED, not replaced. `.ds-tr-hover:hover` and
   `.ds-tr-selected` are alpha fills that read correctly only over the card;
   a sticky cell inheriting them alone would let the scrolling row show through.
   So the cell carries the opaque card as its background-COLOR and the row tint
   as a background-IMAGE on top — the same two layers the browser composites for
   a non-sticky cell, in the same order, so a pinned action cell is pixel-equal
   to the cell it replaced.

   `border-collapse: collapse` (tailwind preflight) is in force: cell BORDERS are
   painted by the table and do not travel with a sticky cell. Nothing here relies
   on one — the separation is the shadow below, which is painted on the cell. */
.ds-actions-sticky {
    position: sticky;
    right: 0;
    background-color: var(--surface-card);
}
.ds-tr-hover:hover > .ds-actions-sticky {
    background-image: linear-gradient(oklch(var(--b2) / 0.4), oklch(var(--b2) / 0.4));
}
.ds-tr-selected > .ds-actions-sticky {
    background-image: linear-gradient(oklch(var(--p) / 0.05), oklch(var(--p) / 0.05));
}
/* Shadow only while the table actually scrolls (data-overflowing is set by the
   probe in data_table.html). Inset-style spread so it reads as an edge under
   which content passes, not as a border of the column. */
[data-overflowing="1"] .ds-actions-sticky {
    box-shadow: -8px 0 8px -8px oklch(var(--bc) / 0.35);
}

/* ─── Progress bar at DATA rank (seo-uyks) ───
   A progress bar inside a table ROW reports a value; it is not the thing the
   page wants you to act on. Both DaisyUI defaults get that rank wrong here:
   `progress-primary` paints the accent, which made this bar the most chromatic
   block on /projects/{id} in both themes while the page's own CTA sat quieter,
   and bare `.progress` falls back to --bc at FULL opacity, i.e. a solid black
   bar — chroma 0, but now the heaviest mark in the row. Both are the same
   mistake: a redundant re-encoding of the «done/total» number beside it
   outranking everything around it.
   Neutral --bc alphas instead: the fill reads as a filled fraction, the track
   as its remainder, and neither competes with a status badge or a button.
   Theme-agnostic by construction — --bc inverts with the theme. Reserve the
   accent variants for progress the user is WAITING on (the running-step bars in
   partials/task_detail/_active_running.html), where salience is the point. */
.ds-progress-data {
    background-color: oklch(var(--bc) / 0.10);
}
.ds-progress-data::-webkit-progress-value {
    background-color: oklch(var(--bc) / 0.42);
}
.ds-progress-data::-moz-progress-bar {
    background-color: oklch(var(--bc) / 0.42);
}
/* Per-theme, because one --bc alpha produces OPPOSITE perceptual roles in the two
   themes and the dark one was wrong (seo-uyks, ensemble dark-theme-legibility).
   --bc inverts with the theme, so the SAME alpha subtracts light on white (ink —
   correct for a data mark) and ADDS light on the navy card (glow — the grammar of
   «lit up / active»). seo-uyks measured 0.40 dark rendering rgb(118,125,137) =
   3.70:1 — the loudest non-text fill in the dark card, 2.5x the next-loudest
   plaque, in a row whose state is precisely that nothing is progressing — and
   dropped it to 0.16 with the track at 0.05, signing off «2.01:1». That number
   was wrong the way seo-djcn's were: it never reached the screen, because the
   FILL composites over the LOWERED track, not over the card — live pixels
   rendered rgb(67,76,90) = 1.77:1, with a fill/track boundary of 1.55:1: a
   partially-filled bar had an unreadable «done/remaining» edge (seo-lbaf).
   seo-lbaf: fill alphas ladder-measured on live pixels OVER the track, both
   themes, softest step clearing the 3:1 non-text floor — dark 0.36 →
   rgb(104,113,125) = 3.11:1 (boundary 2.73:1), light 0.40 measured 2.98:1 (just
   under the floor) → 0.42 = 3.11:1 (boundary 2.53:1). Tracks untouched (1.14:1
   dark / 1.23:1 light — the remainder stays a whisper; the total is carried by
   the «N/M» text beside the bar). Deliberate loudness trade-off vs the seo-uyks
   quieting: 3.11:1 sits below the 3.70:1 it killed but above the floor a
   data-bearing mark must clear; whether the bar should exist at all at N/N is
   the owner's open question seo-4t1t.
   NB: no mechanical axis can catch this class — L1 contrast samples text nodes,
   surface-chroma passes because --bc chroma is 0.01, and the advisory saliency
   axis keys on chroma ratio, so an achromatic over-bright slab is GREEN in all
   three. It took an eye.
   seo-cd06 (1): the 0.05 track composited to rgb(38,47,63) = 1.14:1 on the card —
   38% of the table border's visibility beside it, so a «1/4» bar read as a grey
   stub with no extent; light sits at 1.22:1. 0.08 → rgb(44,52,68) = 1.23:1, theme
   parity; the 0.36 fill over it stays 3.26:1 to the card (floor 3:1) with a
   fill/track boundary of 2.64:1 (light 2.53:1). Ladder computed on the live
   tokens (card oklch(0.2643 0.0345 262.71), --bc oklch(0.92 0.01 258)).
   The FILL (ensemble dark-theme lens on the same commit): bc/0.36 composited to
   rgb(108,116,127) = C 0.020 — the lowest-chroma plate in a card whose every
   other fill carries 0.030–0.049 (the seo-7ds1 grey-on-navy class, one line above
   the badge it fixed; chroma in this family RISES with L, so at L 0.556 the family
   value is ≥ 0.04). Explicit oklch(0.556 0.04 262) = rgb(103,116,139): same L, so
   3.24:1 to the card and 2.63:1 to the track hold. Light stays on the alpha —
   the corporate card is achromatic end to end, a neutral fill is in-family there.
   The track's own composite (C 0.030) already lands in family. */
[data-theme="business"] .ds-progress-data {
    background-color: oklch(var(--bc) / 0.08);
}
[data-theme="business"] .ds-progress-data::-webkit-progress-value {
    background-color: oklch(0.556 0.04 262);
}
[data-theme="business"] .ds-progress-data::-moz-progress-bar {
    background-color: oklch(0.556 0.04 262);
}

/* ─── seo-1qr: data_table cell semantic text (status_code + error) AA, both themes ───
   Plain `text-error`/`text-success` (oklch --er/--su at default L) on the bare row
   surface fails SC 1.4.3 AA — error measured 3.79:1 light / 4.05:1 dark. These are
   NOT badges (no bg-{c}/10 fill, so the .ds-badge-tint overrides don't apply) and
   NOT row-action icons. Per-theme luminance lift (hue+chroma kept — the colour is a
   load-bearing error/success signal). Verified by scripts/visual_machine_layer.py
   both themes. First consumer: /admin/logs status_code + error cells (seo-1qr). */
.ds-cell-error   { color: oklch(0.52 0.223 27.3); }
.ds-cell-success { color: oklch(0.48 0.12 160); }
[data-theme="business"] .ds-cell-error   { color: oklch(0.74 0.223 27.3); }
[data-theme="business"] .ds-cell-success { color: oklch(0.74 0.12 160); }
/* seo-s33t: warning sibling — raw text-warning (corporate --wa 0.555) is 4.33:1 on the
   former .ds-step-current tint (the seo-0deo fill, gone since seo-9wku; the 12px hint
   now sits on the bare card, where the raw token is no better), < AA; 0.508 (the .ds-badge-tint value)
   = 5.28:1. Consumers: the /tasks/{id} pipeline «Неполно» hint, its twin in the action
   zone's backlog_line, and the frequency/relevance step-panel notes.
   seo-rb0n R1 (dark-theme-legibility): business was the ONE member of this family left on
   the raw token, and the raw --wa sits 0.035 ABOVE the family's dark L band — measured
   7.47:1 on the card against 5.19:1 for .ds-cell-error and 7.01:1 for .ds-cell-success,
   i.e. the WARNING line painted brighter than the ERROR ink, a severity inversion the
   light twin does not have (all three pinned into a 5.97–6.09 band). Pinned to the family
   L (0.74) at warning's own hue/chroma — the exact mirror of the corporate value above.
   After: 6.54:1 on the card, 6.14:1 on the tile's hover fill, 5.43:1 on the dark badge
   fill; all ≥ AA, and the family now reads error < warning < success in both themes. */
.ds-cell-warning { color: oklch(0.508 0.12 70); }
[data-theme="business"] .ds-cell-warning { color: oklch(0.74 0.12 70); }
/* seo-rbba: info sibling — the /tasks/{id} pipeline's paused_user «⏸» was the ONE
   state glyph left on raw `text-info` (--in 0.72 corporate = 2.41:1 light), because
   the family had no info member. Same per-theme lift at info's own hue/chroma:
   0.49 light (the .ds-text-primary L — the family's blue-ish members sit a hair
   below the 0.508 warning because blue carries less luminance per L), 0.74 dark
   (the family band). Measured on the card (ui-kit, both themes): 5.63:1 light /
   6.52:1 dark — error 5.63/5.01, warning 5.57/6.32, success 5.74/6.77 beside it.
   Consumers: pipeline_steps.html paused_user glyph; the badge beside it keeps its
   own bg-info/10 tint (.ds-badge-tint). */
.ds-cell-info    { color: oklch(0.49 0.12 231.6); }
[data-theme="business"] .ds-cell-info    { color: oklch(0.74 0.12 240); }

/* ─── seo-v9v: AA-safe primary text for primary-coloured labels on light/neutral
   surfaces (NOT on a primary-tinted pill — those use the ds-search-chip lift).
   Raw `text-primary` (--p L=0.64) measures ~3.4:1 on white < AA; this applies the
   per-theme primary lift (same values as ds-search-chip / ds-role-badge--admin).
   First consumer: the «Из Ollama» model-loader button on /admin/ai-providers form. */
.ds-text-primary { color: oklch(0.49 0.12 264); }
[data-theme="business"] .ds-text-primary { color: oklch(0.73 0.12 264); }

/* ─── seo-3zo: AA-safe required-field asterisk on form surfaces ───────────────
   Raw daisyUI `text-error` (--er 0.64) on a card surface measures 3.79:1 light /
   4.05:1 dark < WCAG AA 4.5 (scripts/visual_machine_layer.py /projects/new +
   /tasks/new). The red required-marker `*` is a load-bearing signal, not
   decorative — same per-theme AA-red lift as .ds-cell-error. First consumers:
   _project_form_fields.html + task_new.html. */
.ds-required { color: oklch(0.52 0.223 27.3); }
[data-theme="business"] .ds-required { color: oklch(0.74 0.223 27.3); }

/* ─── seo-3zo: AA-safe inline form-validation message ─────────────────────────
   Raw daisyUI `text-error` on a card = 3.79:1 light / 4.05:1 dark < AA. A distinct
   role from .ds-cell-error (table cells) and .ds-required (field marker) — an inline
   validation sentence. Same per-theme AA-red lift. First consumer: task_new.html
   «Выберите хотя бы один модуль» (shown when no analysis module is selected). */
.ds-form-error { color: oklch(0.52 0.223 27.3); }
[data-theme="business"] .ds-form-error { color: oklch(0.74 0.223 27.3); }

/* ─── seo-rcp4: WCAG AA lift — tab labels + inactive counter, both themes ───
   tab_switcher.html emits active label as `text-primary` and inactive label as
   `text-base-content/50`. Both fail SC 1.4.3 AA (4.5:1):
     LIGHT (corporate): active 3.45:1, inactive 3.02:1, inactive counter 2.86:1.
     DARK  (business):  active 3.85:1, inactive 4.23:1, inactive counter 3.81:1.
   Fix: per-theme override on the label, kept `!important`. Those two utilities
   are daisyUI's, served from daisyui.min.css BEFORE this sheet with no
   !important of their own (seo-pqb6: base.html's tailwind.config has no
   `important: true`; measured 2026-08-30) — so the themed selector below
   already outranks the bare (0,1,0) class by specificity, and the `!important`
   here only guards against a
   Play-CDN JIT-ed palette utility, which lands in an inline sheet AFTER this
   one. Counter uses .ds-tab-counter DS class — no !important needed. */
/* Light — darken primary to L=0.49 (seo-oeub; same canon as ds-search-chip: ~6.0:1 on white) */
[data-theme="corporate"] .ds-tab-track > :is(a, button).ds-tab-active {
    color: oklch(0.49 0.12 264) !important;
}
/* Light inactive label: /0.68 over white → 5.89:1 (prev /0.50 = 3.02:1) */
[data-theme="corporate"] .ds-tab-track > :is(a, button):not(.ds-tab-active) {
    color: oklch(var(--bc) / 0.68) !important;
}
/* Light inactive counter: /0.68 → 5.89:1 (prev /0.50 = 2.86:1) */
[data-theme="corporate"] .ds-tab-track > :is(a, button):not(.ds-tab-active) .ds-tab-counter {
    color: oklch(var(--bc) / 0.68);
}
/* Dark — lighten active primary to L=0.75 (same value as ds-tab-active .ds-tab-counter
   above; precomputed probe: ~5.95:1 on business b2 surface) */
[data-theme="business"] .ds-tab-track > :is(a, button).ds-tab-active {
    color: oklch(0.75 0.12 264) !important;
}
/* Dark inactive label: /0.66 over business b1 → ~6.1:1 (prev /0.50 = 4.23:1).
   hover:text-base-content (Tailwind = bc/1.0) preserved — higher alpha wins. */
[data-theme="business"] .ds-tab-track > :is(a, button):not(.ds-tab-active) {
    color: oklch(var(--bc) / 0.66) !important;
}
/* Dark inactive counter: /0.68 → ~5.95:1 (prev /0.50 = 3.81:1) */
[data-theme="business"] .ds-tab-track > :is(a, button):not(.ds-tab-active) .ds-tab-counter {
    color: oklch(var(--bc) / 0.68);
}

/* ─── seo-4cec: WCAG AA lift — data_table muted chrome, BOTH themes ───
   The data_table chrome (per-page label, pagination_info, column funnel icons,
   «Нет значений» col-picker rows, table subtitle) uses muted Tailwind alpha
   utilities that fail their WCAG threshold over the card surface:
     text-base-content/20 (funnel icons):           dark 1.75:1 / light 1.52:1 — needs ≥3:1 (non-text icon)
     text-base-content/30 (pagination_info, counts): dark 2.36:1 / light 1.94:1 — needs ≥4.5:1 (text)
     text-base-content/50 (Показывать/на странице):  dark 4.13:1 / light 3.35:1 — needs ≥4.5:1 (text)
   Light fails WORSE than dark on the same chrome — fixed in BOTH themes
   (sweep-whole-class: the bead named dark because that's where it was first
   noticed; the measurement proves light is the more severe fail).
   SCOPED to `.ds-tr-loading` — the structural root of EVERY data_table /
   data_table_section instance (wraps toolbar header chrome + body footer). This
   does NOT touch the app-wide muted-text utilities outside the table (a DS-wide
   muted-text pass is a possible future remediation, NOT this bead's scope).
   These `text-base-content/N` utilities are daisyUI's, served from
   daisyui.min.css BEFORE this sheet without !important (seo-pqb6, measured
   2026-08-30 — the Play CDN only JIT-s Tailwind PALETTE colours, into an inline
   sheet AFTER this one); the themed `.ds-tr-loading` descendant selector already
   outranks the bare class by specificity, and the `!important` kept here guards
   the same rule against such a later palette utility. */
[data-theme="corporate"] .ds-tr-loading .text-base-content\/20 {
    color: oklch(var(--bc) / 0.60) !important;  /* light funnel icons → 4.53:1 (≥3:1) */
}
[data-theme="corporate"] .ds-tr-loading .text-base-content\/30 {
    color: oklch(var(--bc) / 0.68) !important;  /* light pagination_info → 5.89:1 */
}
[data-theme="corporate"] .ds-tr-loading :not(.ds-badge-tint).text-base-content\/50 {
    color: oklch(var(--bc) / 0.68) !important;  /* light per-page label → 5.89:1 */
}
/* seo-djcn: 0.40 was arithmetic, never re-measured — the rendered pixels of a
   1.5-2px stroke came out at 2.72:1, 26% below the 3.16:1 the old comment
   promised, because anti-aliasing never reaches the nominal alpha on a thin
   stroke over a near-black surface. 0.52 pixel-measured on /admin/tasks (worst
   of 18 glyphs, sort chevron + filter funnel). Light needs no lift: the same
   token measures 3.46:1 there — the compositing asymmetry IS the defect. */
[data-theme="business"] .ds-tr-loading .text-base-content\/20 {
    color: oklch(var(--bc) / 0.52) !important;  /* dark sort/filter glyphs → measured below */
}

/* ─── seo-6gdq: the header GLYPHS need more than the /20 lift above ───────────
   Both alphas above were signed off on COMPUTED ink, which a 1.5-2px stroke never
   reaches: re-measured on rendered pixels (/admin/tasks, 9 funnels + 9 chevrons,
   ink-pixel p90 over the card), light gave funnel 2.58:1 / chevron 2.88:1 and dark
   2.90:1 / 3.08:1. All four sit under the 3:1 floor for a non-text affordance except
   the dark chevron, and ALL of them under the dead disabled vocabulary that seo-vzde
   pinned at 3.24:1 light / 3.85:1 dark — the live control read quieter than the
   unusable one. 0.75 light measures funnel 3.40:1 / chevron 4.03:1; 0.70 dark measures
   3.92:1 / 4.47:1.
   SCOPED TO `thead svg` on purpose (Codex review): the `/20` utility is shared with
   the DISABLED pagination arrows, the `|` separator in composite headers and the 64px
   empty-state icon. Lifting the whole utility would have painted the disabled arrow
   (0.75) brighter than the ENABLED one next to it (/60) — the exact live-vs-dead
   inversion this bead exists to remove. */
[data-theme="corporate"] .ds-tr-loading thead svg.text-base-content\/20 {
    color: oklch(var(--bc) / 0.75) !important;  /* light sort/filter glyphs → 3.40-4.03:1 */
}
[data-theme="business"] .ds-tr-loading thead svg.text-base-content\/20 {
    color: oklch(var(--bc) / 0.70) !important;  /* dark sort/filter glyphs → 3.92-4.47:1 */
}
[data-theme="business"] .ds-tr-loading .text-base-content\/30 {
    /* 0.55 measured only 4.47:1 in rendered pixels on the card surface (seo-ktiz
       — the old inline «4.65:1» was math, never re-measured). 0.57 pixel-measured
       4.90:1 (plateau 139,146,157 on 28,37,54), still quieter than /50's 5.23. */
    color: oklch(var(--bc) / 0.57) !important;  /* dark pagination_info → 4.90:1 measured */
}
[data-theme="business"] .ds-tr-loading :not(.ds-badge-tint).text-base-content\/50 {
    color: oklch(var(--bc) / 0.60) !important;  /* dark per-page label → 5.23:1 */
}
/* /40: row secondary text (project/user/date/status sub-line, keyword footer) — the
   L1 forcing function (seo-v4m1.8) caught this as an uncovered member of the 4cec
   «/40-/50» class (was dark 3.18:1 / light 3.18:1, < 4.5 text). Scoped to .ds-tr-loading.
   seo-tgw: :not(.ds-badge-tint) excludes neutral status_badges — they sit on their own
   bg-base-content/15 fill (lighter than the row), so this row-tuned value under-lifts them
   on dark; they fall to the dedicated [data-theme=business] .ds-badge-tint rule (0.75) above. */
[data-theme="corporate"] .ds-tr-loading :not(.ds-badge-tint).text-base-content\/40 {
    color: oklch(var(--bc) / 0.68) !important;
}
[data-theme="business"] .ds-tr-loading :not(.ds-badge-tint).text-base-content\/40 {
    color: oklch(var(--bc) / 0.60) !important;
}
/* /25: empty-cell «—» placeholders (no-value markers in freq/rel/cluster columns).
   Kept deliberately quiet — an empty cell must not visually shout — but it must
   still read as a MARKER rather than a scratch: seo-vadj measured the ORIGINAL
   values in rendered pixels at 2.46:1 dark / 2.15:1 light, both under the non-text
   3:1 floor (the older «~3:1» note was arithmetic, like seo-djcn's), and lifted
   them to the pair below. Scoped to .ds-tr-loading.

   seo-ryh6: the dark side is 0.56, not the 0.50 seo-vadj left. That pair was tuned
   against the non-text floor, on the reading that the dash is decorative because it
   carries aria-hidden with an sr-only «Нет данных» beside it. For a SIGHTED reader it
   is not decorative: on a task whose frequency/relevance/cluster columns are empty,
   the dash IS the whole rendering of «no data» — 5 of 8 data columns on /tasks/228
   are nothing but dashes. Measured on the live card surface: 0.50 gave 4.13:1 in dark
   against 4.84:1 in light, i.e. the same token was AA in one theme and short in the
   other. 0.56 lands at 4.81:1 — parity with light, above the 4.5 text floor, and still
   by far the quietest ink in the row (row text runs ~12:1). Found by the seo-hote
   ensemble's dark-theme lens; the L1 machine cannot see this class of defect at all,
   because it exempts aria-hidden nodes (seo-pwru).

   seo-z7fd: anchored to `.ds-no-data`, which `no_data_dash` emits itself, instead of a
   Tailwind utility scoped to `.ds-tr-loading`. The old selector lifted the dash inside a
   data_table and nowhere else — the same macro renders in the /projects/{id} and
   /admin/users/{id} meta chips, where the raw /25 survived at 2.15:1 light / 2.46:1 dark,
   below even the non-text floor. Sweep before the move: `text-base-content/25` appeared in
   templates ONLY in no_data_dash callsites, so nothing else lost the lift. */
.ds-no-data {
    color: oklch(var(--bc) / 0.62);
}
[data-theme="business"] .ds-no-data {
    color: oklch(var(--bc) / 0.56);
}

/* ─── seo-ckkn: the 64px empty-state icon owns its ink ───
   components/empty_state.html painted its icon with the raw `text-base-content/20`
   utility and relied on the seo-4cec `.ds-tr-loading` lift above to make it legible
   — which it is only inside a table root. The unframed variant hosted by a
   resource_card body section (/projects/{id} «Задачи») measured raw /20: ink
   (67,75,90) on (28,37,54) = 1.75:1 dark, (209,209,212) on white ≈ 1.52:1 light,
   both under the 3:1 non-text floor. Same seo-z7fd move as `.ds-no-data`: the
   component emits its own class, the values were the ones the table lift already
   gave the icon (0.60 light / 0.52 dark — the in-table render did not change at
   that step; seo-rpnj below then re-tuned both themes — light 0.50, dark 0.45 —
   for EVERY host, table included: one class, one value), so the shared `/20`
   utility stays untouched. Its other members (table_primitives /
   data_table): the DISABLED pagination arrows, the thead sort chevrons and filter
   funnels (already lifted by the seo-6gdq `thead svg` rule above) and the `|`
   separator of composite headers — lifting the utility itself would paint the dead
   arrows brighter than the live /60 ones, the inversion seo-6gdq avoided.
   Pixel-measured after the move on /projects/{id} empty «Задачи» both themes —
   values in the seo-ckkn devlog entry.
   seo-rpnj: light went 0.60 → 0.50 (owner call, option «В»). 0.60 on white gave
   the 64px glyph (2.67px stroke, full-coverage pixels reach the nominal alpha)
   a 4.53:1 peak — heavier in ink mass than its own h2 «Задачи» (120%) and
   louder than the page's dead controls (3.24:1, the seo-vzde vocabulary). A
   decorative glyph must clear the 3:1 non-text floor and not sit materially
   above the dead vocabulary. 0.50 measures 3.35:1 peak: plate-relative that is
   parity within noise with the dead label (3.28 peak / 3.24 p90); against the
   shared white card — the ground the eye actually integrates, the plate being
   only 1.12:1 off it — the icon is the quietest mark (label 3.68, kebab 3.57).
   0.45 would drop below the floor. Ink mass vs the h2 went 120% → 107%
   (density 103% → 93%); a 64px glyph at ≥3:1 cannot get fully under a 20px
   heading by mass.
   Applies to in-table empties too (same class). Dark moved by the same rule,
   0.52 → 0.45: at 0.52 the glyph peaked 4.31:1 — above the dead vocabulary
   (label 3.85, kebab 3.74) and above the banner's info icon (3.83), the one
   icon on the page that carries meaning; by mass it was already under the h2
   (49%), which is why the first pass left it alone. 0.45 measures 3.60:1 peak —
   under the dead label (3.85) and kebab (3.74), 0.6 over the floor. */
.ds-empty-icon {
    color: oklch(var(--bc) / 0.50);  /* light: measured value in the seo-rpnj devlog entry */
}
[data-theme="business"] .ds-empty-icon {
    color: oklch(var(--bc) / 0.45);  /* dark: measured value in the seo-rpnj devlog entry */
}

/* ─── seo-j7d7: WCAG AA lift — ds-icon-btn dark ───
   Measured live: ds-icon-btn (bc/0.5) on ds-mc-card (b1 = oklch(0.2643 0.0345 262.71))
   gives 4.13:1 — already passes SC 1.4.11 non-text ≥3:1.
   No override needed; bead resolved by token measurement. */

/* ─── Placeholder ink — ONE owner, every input/textarea (seo-i9ui, folds seo-jrp1) ───
   Tailwind preflight paints every placeholder #9ca3af: 2.54:1 on white (the funnel's
   «Поиск...», the data_table search box, every form field) — below the 4.5:1 AA floor
   placeholder TEXT needs; on the dark popover the same grey happens to clear it, so
   the defect was light-theme-only and invisible to a dark-first eye. seo-jrp1 had
   patched ONE textarea with a scoped bc/0.70 rule; this is the class-wide fix on the
   project's single AA-safe muted token — corporate bc/0.68 ≈ 5.9:1 on white, business
   bc/0.60 ≈ 5.2:1 on --b1 / ≈4.8:1 on the popover --b2. Selector = preflight's own
   pair (`input::placeholder, textarea::placeholder`) under a `body` prefix — see below —
   so it also reaches the data_table search field, which is a bare <input> inside an
   `.input` wrapper. Callsites must not pin `placeholder:text-*` utilities (the Play
   CDN did not reliably JIT them — that was seo-jrp1's trigger — and a second owner
   would only confuse); pinned by tests/test_design_system_placeholder_ink.py.
   `body` prefix on purpose: the Play CDN injects its generated sheet (preflight
   included) at RUNTIME, after this file, so an equal-specificity (0,0,2) rule loses
   on order — (0,0,3) wins regardless of sheet order, the same reason seo-jrp1's
   scoped selector worked. */
body input::placeholder,
body textarea::placeholder {
    color: var(--text-muted-aa);
    opacity: 1;
}

/* seo-s0aw folded the seo-jrp1 `#kw-filters .input-bordered/.select-bordered`
   patch (0.40 light / 0.50 dark) into the class-wide atom rule in
   component-overrides.css, which now fixes this defect for every form control at
   once. The id-scoped selector no longer agreed with that floor — its light /0.40
   measured 2.51:1 live (the seo-jrp1 «above 3:1» was nominal, not composited), so
   it OUTRANKED and undercut the new global 0.47. The filter row now draws the same
   0.47/0.40 as every other form control on a card (3.06:1 / 3.16:1 measured on
   /tasks/{id}); dark comes down from 4.13:1 to the floor the rest of the app uses. */

/* Warning-amber exclude chip — light only (dark --wa is already adapted).
   Same rationale as super_mode_badge Unit 3 fix: DaisyUI corporate --wa
   (0.555 0.12 70) only ~3.2:1 on the amber/0.10 tint; dropping to L=0.50
   lands above AA. */
[data-theme="corporate"] .ds-search-chip-exclude {
    color: oklch(0.50 0.12 48);
}
