/**
 * Ecombooster — Shared UI Components
 *
 * Reusable component styles for all modern admin views (hubs and standalone).
 * Prefix: ecb-*
 *
 * Components:
 *   Layout:   ecb-layout, ecb-sidebar, ecb-main, ecb-grid-2, ecb-grid-3
 *   KPI:      ecb-kpis, ecb-kpi-card, ecb-kpi-value, ecb-kpi-label, ecb-kpi-trend
 *   Sections: ecb-section, ecb-section-title, ecb-subsection-title
 *   Tables:   ecb-table, ecb-sortable (sortable headers)
 *   Badges:   ecb-badge (.draft/.approved/.published/.rejected/.info/.warning)
 *   Scores:   ecb-score (.excellent/.good/.average/.poor)
 *   Tabs:     ecb-tabs, ecb-tab, ecb-tab-panel
 *   Buttons:  ecb-btn, ecb-btn-primary, ecb-btn-secondary, ecb-btn-danger, ecb-btn-sm
 *   Modals:   ecb-modal-overlay, ecb-modal, ecb-modal-header/body/footer
 *   Loading:  ecb-loading, ecb-spinner
 *   Charts:   ecb-chart-container
 *   Toast:    ecb-toast
 *   Search:   ecb-search-dropdown, ecb-search-item
 *   Forms:    ecb-form-group, ecb-explanation
 */

/* ==========================================================================
   `hidden` actually hides (EC-3824)
   --------------------------------------------------------------------------
   `[hidden] { display: none }` lives in the *user-agent* stylesheet, which any
   author rule setting `display` outranks. `.ecb-btn { display: inline-flex }`
   is such a rule, so `button.hidden = true` set the attribute and changed
   nothing — the AI Hub Usage page shipped a "Show" button that was supposed to
   disappear under JavaScript and never did.

   Three components had each patched this for themselves before anyone stated
   the rule: `.ecb-modal-backdrop`, `.ecb-plookup-trigger-badge` and
   `.sh-bulk-bar`, the last with a comment describing exactly this mechanism.
   Twelve places in the tree toggle `.hidden`, against 643 classes that declare
   a non-none `display`; enumerating the broken pairs is not worth doing when
   the general rule fits on one line.

   `!important` is deliberate and is the only thing that works: an author rule
   of equal or higher specificity would otherwise win on source order, and
   `[hidden]` is a single attribute selector that almost anything outranks.
   Nothing should ever want a visible element carrying `hidden`.

   `common/tests/test_hidden_attribute_wins.py` keeps this rule here and keeps
   the per-component copies from coming back.
   ========================================================================== */
[hidden] {
    display: none !important;
}

/* ==========================================================================
   Design tokens — modern UI palette
   --------------------------------------------------------------------------
   These mirror the tokens shipped by the Tailwind build (frontend/tailwind.config.js)
   so non-`.twp`-scoped views (hubs, classic admin) get the same brand colors
   that Tailwind utilities produce inside `.twp`. The Tailwind config references
   these via `var(--ecb-*, <fallback>)` — defining them here makes the variable
   path active globally, not just inside the Tailwind bundle.
   ========================================================================== */
:root {
    --ecb-primary: #417690;
    --ecb-primary-dark: #2d5468;
    --ecb-text: #1f2937;
    --ecb-text-soft: #4b5563;
    --ecb-bg: #ffffff;
    --ecb-bg-soft: #f9fafb;
    --ecb-border: #e5e7eb;
    --ecb-success: #059669;
    --ecb-warning: #d97706;
    --ecb-danger: #dc2626;

    /* Theme tokens — light defaults. Switched in [data-theme="dark"] block below.
       The FOUC-prevention script in modern/base.html + common/hub_base.html sets
       data-theme on <html> from localStorage before CSS loads, so dark renders
       on first paint without flashing the light palette. */
    --theme-bg:             #f8fafc;  /* page background (slate-50) */
    --theme-bg-elevated:    #ffffff;  /* card / panel background */
    --theme-bg-subtle:      #f1f5f9;  /* hover background, secondary fill (slate-100) */
    --theme-fg:             #0f172a;  /* primary text (slate-900) */
    --theme-fg-muted:       #475569;  /* secondary text (slate-600) */
    --theme-fg-subtle:      #94a3b8;  /* tertiary text, placeholders (slate-400) */
    --theme-border:         #e2e8f0;  /* default border (slate-200) */
    --theme-border-strong:  #cbd5e1;  /* focused border (slate-300) */
    /* The border of a control that is being edited. One notch past
       `-strong`, because in a dense grid the resting border is already
       drawn and the editing one has to be told apart from it. */
    --theme-border-active:  #94a3b8;  /* slate-400 */
    --theme-shadow-1:       0 1px 0 rgba(16, 24, 40, 0.03), 0 1px 2px rgba(16, 24, 40, 0.05);
    --theme-shadow-2:       0 4px 12px rgba(15, 23, 42, 0.08);
    --theme-primary:        var(--ecb-primary, #417690);
    --theme-primary-soft:   rgba(65, 118, 144, 0.10);  /* primary tinted background */
    --theme-success:        #15803d;  /* green-700 */
    --theme-warning:        #b45309;  /* amber-700 */
    --theme-danger:         #b91c1c;  /* red-700 */

    /* Tinted backgrounds for the status tones. These were written as literal
       mints and ambers (#ECFDF5, #FDE68A) at every call site, which is why a
       green delta chip stayed a pale mint in dark mode with pale green text on
       it — invisible. A tone that has a token has a dark counterpart; a tone
       that is typed inline does not. EC-3556. */
    --theme-success-soft:   rgba(21, 128, 61, 0.10);
    --theme-warning-soft:   rgba(180, 83, 9, 0.10);
    --theme-danger-soft:    rgba(185, 28, 28, 0.08);
    /* Text on a primary-filled surface. Not --theme-bg-elevated: that flips to
       a dark slate in dark mode, while a primary-filled button stays primary in
       both, so its label has to stay light in both. EC-3564. */
    /* The wash behind a modal. Deliberately dark in both themes — a scrim's
       job is to push the page back, and a light one over a light page does not
       do that. Named rather than typed so it is a decision, not a magic
       number the colour guard has to make an exception for. EC-3566. */
    --theme-scrim:          rgba(15, 23, 42, 0.55);

    --theme-on-primary:     #ffffff;
    --theme-primary-hover:  var(--ecb-primary-dark, #2d5468);

    --theme-success-line:   #a7f3d0;
    --theme-warning-line:   #fde68a;
    --theme-danger-line:    #fecaca;

    /* Info tone. The one semantic tone that had no token, so `published` and
       `info` badges and every `.ecb-explanation` were typed as literal pale
       blues (#cce5ff, #e3f2fd, #f0f7ff) and had no dark counterpart at all.
       EC-3590. */
    --theme-info:           #1565c0;
    --theme-info-soft:      rgba(21, 101, 192, 0.10);
    --theme-info-line:      #bfdbfe;

    /* Stacking bands (EC-3674). Every layer used to pick its own number, and
       the numbers disagreed: `.twp .help-tooltip::after` sat at 1000 and
       `.ecb-pn-trigger[data-tip]::after` at 1200 — on opposite sides of
       `.ecb-modal-overlay` (1100). So one tooltip painted over that modal and
       the other vanished behind it, which is the "ends up behind something"
       this ticket was raised for.

       The values below are the ones those components already carried, so
       nothing moves except the two tooltips, which are deliberately unified.
       A tooltip sits ABOVE what it annotates, dialogs included: one that
       renders behind the dialog it belongs to is no use. The toast has the
       last word because it reports things the user must not miss. */
    --z-dropdown:      200;    /* a menu or autocomplete list owned by a field */
    --z-sticky:       1000;    /* sticky table headers, toolbars, bulk-action and save bars */
    --z-shell:        1500;    /* the shell's topbar and left rail: above the page, below a dialog */
    --z-shell-drawer: 1600;    /* the mobile rail and its backdrop */
    --z-modal-scrim:  9000;    /* the dim behind a dialog */
    --z-modal:       10000;    /* the dialog itself */
    --z-modal-stacked: 10500;  /* a dialog opened from a dialog (a confirm, a picker) */
    --z-banner:      10950;    /* act-as banner: above a dialog, below a tooltip */
    --z-tooltip:     11000;    /* tooltips, and popovers attached to <body> (select2) */
    --z-toast:       99999;    /* the global toast */
    /* EC-4766: these are the only scale. A literal z-index above 10 anywhere else
       fails common/tests/test_z_index_scale.py; Tailwind reaches them as
       z-dropdown, z-sticky, z-modal and the rest (frontend/tailwind.config.js).
       Outside the shell, components.css may not be loaded, so a stylesheet
       writes var(--z-modal, 10000): the fallback is the same number. */

    /* Decorative chip tones (EC-3672). Unlike success/warning/danger/info these
       carry no semantics — they exist so one kind of chip can be told from
       another at a glance. They lived in ten separate template <style> blocks
       until now, where the copies had already drifted: chip-orange had two
       different light values, and chip-violet had a dark variant on one page
       and not on another.

       These three keep their exact previous values, so they look identical.
       (chip-sky / chip-blue are the separate case: they fold into the existing
       --theme-info tone, which shifts their shade very slightly.)

       Note they are solid pale hex rather than the low-alpha tint of the ink
       colour that the four canonical chips use. Unifying that is a visual
       decision and deliberately not part of consolidating them. */
    --theme-violet:         #6D28D9;
    --theme-violet-soft:    #F5F3FF;
    --theme-orange:         #C2410C;
    --theme-orange-soft:    #FFF7ED;
    --theme-indigo:         #4338CA;
    --theme-indigo-soft:    #EEF2FF;

    /* EC-3593: the border step, completing these three the way the semantic
       tones already are (--theme-danger-line and friends). The demand matrix
       needs all three of soft / line / ink to draw a tier square, and it was
       carrying its own copies of the 200 steps plus a whole [data-theme=dark]
       block to invert them. */
    --theme-violet-line:    #DDD6FE;   /* violet-200 */
    --theme-orange-line:    #FED7AA;   /* orange-200 */
    --theme-indigo-line:    #C7D2FE;   /* indigo-200 */

    /* The second step of a tint, for the rare place that needs two depths of
       the same tone next to each other (the Tailwind palette bridge below is
       the only current user, where `bg-rose-50` and `bg-rose-100` must stay
       distinguishable). */
    --theme-success-soft-strong: rgba(21, 128, 61, 0.18);
    --theme-warning-soft-strong: rgba(180, 83, 9, 0.18);
    --theme-danger-soft-strong:  rgba(185, 28, 28, 0.16);
    --theme-info-soft-strong:    rgba(21, 101, 192, 0.18);

    /* A filled danger control. `--theme-danger` is the ink colour for danger
       text; a button filled with it needs a darker step for hover, the same
       way `--theme-primary-hover` serves `--theme-primary`. */
    --theme-danger-hover:   #991b1b;

    /* Modal elevation. Deeper than --theme-shadow-2 on purpose: a dialog sits
       above the page, not on it. */
    --theme-shadow-3:       0 20px 60px rgba(15, 23, 42, 0.25);

    /* A tooltip inverts the page rather than following it — dark chip on a
       light page, light chip on a dark one. Typed as #1e293b/#fff before, so
       in dark mode the tip was slate-800 on slate-900 and barely separated
       from what it floated over. */
    --theme-tooltip-bg:     #1e293b;
    --theme-tooltip-fg:     #ffffff;

    /* The act-as banner keeps its colour in both themes — its job is to stop
       you editing a customer's data believing you are yourself, and a warning
       that blends into the chrome has stopped doing that job. Tokens rather
       than literals so the theme swap is a value, not a second rule. */
    --theme-alert-fill:     #b45309;
    --theme-alert-on-fill:  #ffffff;

    /* Ink on a surface that keeps the *brand* colour in both themes — a
       marketing hero, not chrome. Distinct from `--theme-on-primary`, which
       flips to dark because `--theme-primary` lightens in dark mode. EC-3596. */
    --theme-on-brand:       #ffffff;

    /* ------------------------------------------------------------------
       Type scale — EC-3556

       Named `--ecb-type-*` rather than `--ecb-text-*` because `--ecb-text`
       and `--ecb-text-soft` a few lines above are *colours*. A size and a
       colour sharing a prefix is the confusion a token set exists to remove.

       Named for the job, not the size. The Model Report alone carried 12px,
       12.5px, 13px, 13.5px and 15px with nothing to say which meant what, so
       the next page picked its own five. Seven steps is the whole vocabulary;
       a size that is not on this list needs a reason.
       ------------------------------------------------------------------ */
    --ecb-type-eyebrow:  11px;    /* uppercase label above a title */
    --ecb-type-micro:    12px;    /* badge, axis tick, dense annotation */
    --ecb-type-meta:     12.5px;  /* the muted line under a thing */
    --ecb-type-body:     13.5px;  /* running prose */
    --ecb-type-lead:     15px;    /* lede under a title; a row's headline */
    --ecb-type-title:    18px;    /* section title */
    --ecb-type-display:  24px;    /* page title; the one number a page leads on */

    --ecb-leading-tight: 1.25;
    --ecb-leading-body:  1.6;

    /* Spacing rhythm. Vertical space between two things on a page comes from
       this list, so two sections of the same page cannot drift apart. */
    --ecb-space-1: 4px;
    --ecb-space-2: 8px;
    --ecb-space-3: 12px;
    --ecb-space-4: 16px;
    --ecb-space-5: 24px;
    --ecb-space-6: 32px;
    --ecb-space-7: 48px;

    --ecb-radius-sm:   4px;
    --ecb-radius:      8px;
    --ecb-radius-pill: 999px;

    /* Prose stops being readable well before the page runs out of width. */
    --ecb-measure: 68ch;

    --ecb-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
                     "Liberation Mono", monospace;
}

[data-theme="dark"] {
    --theme-bg:             #0f172a;  /* slate-900 */
    --theme-bg-elevated:    #1e293b;  /* slate-800 */
    --theme-bg-subtle:      #334155;  /* slate-700 */
    --theme-fg:             #f1f5f9;  /* slate-100 */
    --theme-fg-muted:       #cbd5e1;  /* slate-300 */
    --theme-fg-subtle:      #94a3b8;  /* slate-400 */
    --theme-border:         #334155;  /* slate-700 */
    --theme-border-strong:  #475569;  /* slate-600 */
    --theme-border-active:  #64748b;  /* slate-500 */
    --theme-shadow-1:       0 1px 0 rgba(0, 0, 0, 0.30), 0 2px 4px rgba(0, 0, 0, 0.40);
    --theme-shadow-2:       0 4px 12px rgba(0, 0, 0, 0.45);
    --theme-primary:        #6fa9c2;  /* lighter ecb-primary for dark bg contrast */
    --theme-primary-soft:   rgba(111, 169, 194, 0.15);
    --theme-success:        #4ade80;  /* green-400 */
    --theme-warning:        #fbbf24;  /* amber-400 */
    --theme-danger:         #f87171;  /* red-400 */

    /* The tints invert: a dark surface needs the tone laid over it, not a pale
       mint under pale green text. Borders lose their light-mode pastel and
       become the tone at low alpha, which is the only thing that reads. */
    --theme-success-soft:   rgba(74, 222, 128, 0.14);
    --theme-warning-soft:   rgba(251, 191, 36, 0.14);
    --theme-danger-soft:    rgba(248, 113, 113, 0.14);
    --theme-scrim:          rgba(2, 6, 23, 0.70);
    --theme-on-primary:     #0f172a;   /* the dark primary is light — ink on it must be dark */
    --theme-primary-hover:  #8fc0d4;

    --theme-success-line:   rgba(74, 222, 128, 0.35);
    --theme-warning-line:   rgba(251, 191, 36, 0.35);
    --theme-danger-line:    rgba(248, 113, 113, 0.35);

    --theme-info:           #60a5fa;  /* blue-400 */
    --theme-info-soft:      rgba(96, 165, 250, 0.14);
    --theme-info-line:      rgba(96, 165, 250, 0.35);

    /* EC-3672: the decorative chip tones invert like every other tint here —
       the tone laid over the dark surface, with a light ink on top. */
    --theme-violet:         #c4b5fd;
    --theme-violet-soft:    rgba(139, 92, 246, 0.18);
    --theme-orange:         #fdba74;
    --theme-orange-soft:    rgba(249, 115, 22, 0.18);
    --theme-indigo:         #c7d2fe;
    --theme-indigo-soft:    rgba(99, 102, 241, 0.18);
    --theme-violet-line:    rgba(139, 92, 246, 0.32);
    --theme-orange-line:    rgba(249, 115, 22, 0.32);
    --theme-indigo-line:    rgba(99, 102, 241, 0.32);

    --theme-success-soft-strong: rgba(74, 222, 128, 0.24);
    --theme-warning-soft-strong: rgba(251, 191, 36, 0.24);
    --theme-danger-soft-strong:  rgba(248, 113, 113, 0.24);
    --theme-info-soft-strong:    rgba(96, 165, 250, 0.24);

    --theme-danger-hover:   #fca5a5;  /* the dark danger is light — hover goes lighter still */
    --theme-shadow-3:       0 20px 60px rgba(0, 0, 0, 0.60);

    --theme-tooltip-bg:     #f1f5f9;
    --theme-tooltip-fg:     #0f172a;

    --theme-alert-fill:     #92400e;
    --theme-alert-on-fill:  #ffffff;
    --theme-on-brand:       #ffffff;
}

/* Body / shell paints the page background in the current theme. Modern shell
   templates and hub_base.html set body bg directly; those can stay as
   var(--theme-bg) once migrated. This rule is the default for any body
   that doesn't override. */
html[data-theme] body { background: var(--theme-bg); color: var(--theme-fg); }

/* Brand logo swap — show the blue mark on light, the white mark on dark. */
.theme-logo-light { display: inline-block; }
.theme-logo-dark { display: none; }
[data-theme="dark"] .theme-logo-light { display: none; }
[data-theme="dark"] .theme-logo-dark { display: inline-block; }

/* Alpine.js compatibility — needs the [x-cloak] rule to hide elements until
   Alpine initialises, and a button reset inside .twp so Tailwind utility
   classes work on <button> without inheriting browser default bg/borders.
   Lived in modern/base.html only; moved here so hub_base.html (which extends
   admin/base_site.html, not modern/base.html) also gets them. */
[x-cloak] { display: none !important; }
.twp button { background: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; }
.twp button:disabled { cursor: not-allowed; }
.twp a { color: inherit; text-decoration: none; }
.twp *, .twp *::before, .twp *::after { box-sizing: border-box; }

/* ==========================================================================
   Fix Django admin select/input overrides
   ========================================================================== */

.ecb-main select,
.ecb-main input[type="text"],
.ecb-main input[type="number"],
.ecb-main input[type="month"],
.ecb-main textarea {
    box-sizing: border-box;
    height: auto;
    min-height: 38px;
    line-height: 1.4;
    font-size: var(--ecb-type-body);
}

.ecb-main select {
    padding: 8px 12px;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    /* The arrow's stroke colour lives inside the data URI, where a custom
       property cannot reach — which is why the dark rule further down repeats
       the whole shorthand with a lighter stroke rather than overriding one
       value. The surface behind it is a token. */
    background: var(--theme-bg-elevated) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23666' fill='none' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 12px center;
    padding-right: 32px;
}

/* ==========================================================================
   Layout: sidebar + main
   ========================================================================== */

.ecb-layout {
    display: flex;
    gap: 20px;
    min-height: 600px;
    margin: 0;
}

/* Sidebar navigation — uses theme tokens so it switches with light/dark mode. */
.ecb-sidebar {
    width: 220px;
    min-width: 220px;
    background: var(--theme-bg-elevated);
    border: 1px solid var(--theme-border);
    border-radius: 8px;
    padding: 12px 0 8px 0;
    box-shadow: var(--theme-shadow-1);
    align-self: flex-start;
}

.ecb-nav,
.ecb-sidebar .ecb-nav,
.ecb-sidebar ul.ecb-nav {
    list-style: none !important;
    list-style-type: none !important;
    margin: 0 !important;
    padding: 0 !important;
}

.ecb-nav-item,
.ecb-sidebar .ecb-nav-item,
.ecb-sidebar ul.ecb-nav li.ecb-nav-item {
    display: block !important;
    list-style: none !important;
    list-style-type: none !important;
    list-style-image: none !important;
    margin: 0 !important;
    padding: 0 !important;
}

.ecb-nav-item::marker,
.ecb-sidebar li::marker { content: '' !important; display: none !important; }
.ecb-nav-item::before,
.ecb-sidebar li::before { content: none !important; }

.ecb-nav-item a {
    display: flex;
    align-items: center;
    gap: var(--ecb-space-2);
    padding: var(--ecb-space-2) var(--ecb-space-4);
    color: var(--theme-fg-muted);
    text-decoration: none;
    font-size: var(--ecb-type-body);
    border-left: 3px solid transparent;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.ecb-nav-item a:hover {
    background: var(--theme-bg-subtle);
    color: var(--theme-fg);
}

.ecb-nav-item.active a {
    background: var(--theme-bg-subtle);
    color: var(--theme-primary);
    font-weight: 600;
    border-left-color: var(--theme-primary);
}

.ecb-nav-icon {
    font-size: 16px;  /* off-scale: an emoji glyph sized to its 20px rail, not type */
    width: 20px;
    text-align: center;
    flex-shrink: 0;
    color: var(--theme-fg-subtle);
}

.ecb-nav-item.active .ecb-nav-icon { color: var(--theme-primary); }

.ecb-nav-label {
    flex: 1;
}

.ecb-badge-su {
    font-size: 9px;  /* off-scale: rides inside a nav label and must stay under it */
    font-weight: 700;
    background: var(--theme-bg-subtle);
    color: var(--theme-fg-subtle);
    padding: 1px 5px;
    border-radius: 3px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* EC-3527: an arrow on a nav entry or card that leaves this hub. Muted on
   purpose — it is a signpost, not an alert. */
.ecb-nav-external,
.hub-action-external {
    color: var(--theme-fg-subtle);
    font-size: var(--ecb-type-eyebrow);
    line-height: 1;
}

.ecb-nav-group-label {
    margin: var(--ecb-space-3) var(--ecb-space-4) var(--ecb-space-1) var(--ecb-space-4);
    color: var(--theme-fg-subtle);
    font-size: var(--ecb-type-eyebrow);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.6px;
}

.ecb-nav-group-label:first-child {
    margin-top: 4px;
}

.ecb-nav-group-divider {
    margin: 10px 16px;
    border-top: 1px solid var(--theme-border);
}

/* Dark theme overrides for Tailwind utilities used inside the modern shell header.
   The header (in templates/modern/base.html and templates/common/hub_base.html)
   uses utility classes like bg-white/90, border-slate-200, text-slate-600, hover:bg-slate-100.
   In dark mode we remap those utilities to theme tokens. Scoped to .twp > header so
   we don't accidentally touch utilities used in modern view content. */
[data-theme="dark"] .twp > header {
    background: color-mix(in srgb, var(--theme-bg) 92%, transparent) !important;
    border-color: var(--theme-border) !important;
}
[data-theme="dark"] .twp > header .text-slate-600 { color: var(--theme-fg-muted) !important; }
[data-theme="dark"] .twp > header .text-slate-500 { color: var(--theme-fg-subtle) !important; }
[data-theme="dark"] .twp > header .text-slate-900 { color: var(--theme-fg) !important; }
[data-theme="dark"] .twp > header .text-slate-400 { color: var(--theme-fg-subtle) !important; }
[data-theme="dark"] .twp > header .border-slate-200 { border-color: var(--theme-border) !important; }
[data-theme="dark"] .twp > header .hover\:bg-slate-100:hover { background: var(--theme-bg-subtle) !important; }
[data-theme="dark"] .twp > header .bg-slate-100 { background: var(--theme-bg-subtle) !important; }
[data-theme="dark"] .twp > header .hover\:text-ecb-primary:hover { color: var(--theme-primary) !important; }

/* Broad Tailwind-utility dark overrides for any .twp-scoped element (modern
   catalog list/detail, modern components, hub shell header dropdowns).
   Scoped to `.twp` so legacy admin chrome is unaffected. */
[data-theme="dark"] .twp .bg-white { background: var(--theme-bg-elevated) !important; }
[data-theme="dark"] .twp .bg-slate-50 { background: var(--theme-bg) !important; }
[data-theme="dark"] .twp .bg-slate-100 { background: var(--theme-bg-subtle) !important; }
[data-theme="dark"] .twp .bg-slate-200 { background: var(--theme-bg-subtle) !important; }

[data-theme="dark"] .twp .text-slate-300 { color: var(--theme-fg-subtle) !important; }
[data-theme="dark"] .twp .text-slate-400 { color: var(--theme-fg-subtle) !important; }
[data-theme="dark"] .twp .text-slate-500 { color: var(--theme-fg-muted) !important; }
[data-theme="dark"] .twp .text-slate-600 { color: var(--theme-fg-muted) !important; }
[data-theme="dark"] .twp .text-slate-700 { color: var(--theme-fg) !important; }
[data-theme="dark"] .twp .text-slate-800 { color: var(--theme-fg) !important; }
[data-theme="dark"] .twp .text-slate-900 { color: var(--theme-fg) !important; }

[data-theme="dark"] .twp .border-slate-100 { border-color: var(--theme-border) !important; }
[data-theme="dark"] .twp .border-slate-200 { border-color: var(--theme-border) !important; }
[data-theme="dark"] .twp .border-slate-300 { border-color: var(--theme-border-strong) !important; }
[data-theme="dark"] .twp .border-slate-400 { border-color: var(--theme-border-strong) !important; }
[data-theme="dark"] .twp .divide-slate-100 > * { border-color: var(--theme-border) !important; }
[data-theme="dark"] .twp .divide-slate-200 > * { border-color: var(--theme-border) !important; }

[data-theme="dark"] .twp .hover\:bg-slate-50:hover { background: var(--theme-bg-subtle) !important; }
[data-theme="dark"] .twp .hover\:bg-slate-100:hover { background: var(--theme-bg-subtle) !important; }
[data-theme="dark"] .twp .hover\:text-slate-700:hover { color: var(--theme-fg) !important; }
[data-theme="dark"] .twp .hover\:text-slate-900:hover { color: var(--theme-fg) !important; }

[data-theme="dark"] .twp .placeholder\:text-slate-400::placeholder,
[data-theme="dark"] .twp input::placeholder,
[data-theme="dark"] .twp textarea::placeholder { color: var(--theme-fg-subtle) !important; }

/* Tinted "soft" semantic backgrounds (badges, banners) — darken slightly so
   light tints (rose-50, amber-50, sky-50, emerald-50) read on a dark page. */
/* Tailwind names a shade; we own a tone. The bridge maps one to the other so
   a page written in utilities lands on the same four tones as a page written
   in `ecb-*`, instead of on a ninth and tenth blue nobody chose. */
[data-theme="dark"] .twp .bg-rose-50 { background: var(--theme-danger-soft) !important; }
[data-theme="dark"] .twp .bg-rose-100 { background: var(--theme-danger-soft-strong) !important; }
[data-theme="dark"] .twp .bg-amber-50 { background: var(--theme-warning-soft) !important; }
[data-theme="dark"] .twp .bg-amber-100 { background: var(--theme-warning-soft-strong) !important; }
[data-theme="dark"] .twp .bg-emerald-50 { background: var(--theme-success-soft) !important; }
[data-theme="dark"] .twp .bg-emerald-100 { background: var(--theme-success-soft-strong) !important; }
[data-theme="dark"] .twp .bg-sky-50 { background: var(--theme-info-soft) !important; }
[data-theme="dark"] .twp .bg-blue-50 { background: var(--theme-info-soft) !important; }
[data-theme="dark"] .twp .bg-blue-100 { background: var(--theme-info-soft-strong) !important; }

/* Main content area */
.ecb-main {
    flex: 1;
    padding: 0;
    background: transparent;
    border-radius: 0;
    min-width: 0;
}

/* ==========================================================================
   KPI Cards
   ========================================================================== */

.ecb-kpis {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--ecb-space-4);
    margin-bottom: var(--ecb-space-6);
}

.ecb-kpi-card {
    background: var(--theme-bg-subtle);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius);
    padding: var(--ecb-space-4) var(--ecb-space-5);
    text-align: center;
}

.ecb-kpi-value {
    font-size: var(--ecb-type-display);
    font-weight: 700;
    line-height: var(--ecb-leading-tight);
    color: var(--theme-fg);
}

.ecb-kpi-value.good { color: var(--theme-success); }
.ecb-kpi-value.warning { color: var(--theme-warning); }
.ecb-kpi-value.bad { color: var(--theme-danger); }

.ecb-kpi-label {
    font-size: var(--ecb-type-micro);
    color: var(--theme-fg-muted);
    margin-top: var(--ecb-space-1);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.ecb-kpi-trend {
    font-size: var(--ecb-type-micro);
    margin-top: var(--ecb-space-1);
}

.ecb-kpi-trend.up { color: var(--theme-success); }

/* A single KPI card used as a callout rather than one of a row. EC-3557. */
.ecb-kpi-card.is-inline {
    display: inline-block;
    text-align: left;
}
.ecb-kpi-trend.down { color: var(--theme-danger); }

/* ==========================================================================
   Section containers
   ========================================================================== */

.ecb-section {
    margin-bottom: var(--ecb-space-6);
}

.ecb-section-title {
    font-size: var(--ecb-type-title);
    font-weight: 600;
    color: var(--theme-fg);
    margin: 0 0 var(--ecb-space-3) 0;
    padding-bottom: var(--ecb-space-2);
    border-bottom: 1px solid var(--theme-border);
}

/* Groups fields inside a section ("Identity", "Scope", "Mode & preset"). Used
   26 times across the Settings Hub rule editors but never defined, so it
   rendered as unstyled body text pressed against the field above it. The top
   margin is what separates one group from the previous group's last field —
   without it the heading reads as a label for that field. */
.ecb-subsection-title {
    font-size: var(--ecb-type-body);
    font-weight: 600;
    color: var(--theme-fg-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin: var(--ecb-space-5) 0 var(--ecb-space-2) 0;
    padding-bottom: var(--ecb-space-1);
    border-bottom: 1px solid var(--theme-border);
}

/* First group in a form already sits below a heading — no extra gap wanted. */
.ecb-subsection-title:first-child {
    margin-top: 0;
}

/* ==========================================================================
   Tables
   ========================================================================== */

.ecb-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--ecb-type-body);
}

.ecb-table th {
    text-align: left;
    padding: var(--ecb-space-2) var(--ecb-space-3);
    background: var(--theme-bg-subtle);
    border-bottom: 2px solid var(--theme-border);
    font-weight: 600;
    color: var(--theme-fg-muted);
    font-size: var(--ecb-type-eyebrow);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.ecb-table td {
    padding: var(--ecb-space-2) var(--ecb-space-3);
    border-bottom: 1px solid var(--theme-border);
    vertical-align: middle;
}

.ecb-table tr:hover td {
    background: var(--theme-bg-subtle);
}

/* Several tables of the same shape stacked down one page — one per carrier, one
   per supplier — are read as a single table, so their columns have to line up.
   With the default `auto` layout each one sizes itself to its own longest cell
   and the page becomes a staircase. `fixed` makes the widths below the answer
   instead; the unsized first column takes whatever is left. */
.ecb-table.is-stable-cols { table-layout: fixed; }
.ecb-table.is-stable-cols th.ecb-col-value { width: 160px; }
.ecb-table.is-stable-cols th.ecb-col-note { width: 220px; }

/* Narrow enough and those two fixed columns are most of the width, leaving the
   name nothing to wrap in. One table fills the screen there anyway, so there is
   no second table to line up with. Placed after the rule it overrides — earlier
   in the file it would silently lose on source order (CLAUDE.md § "Responsive
   CSS"). */
@media (max-width: 900px) {
    .ecb-table.is-stable-cols { table-layout: auto; }
    .ecb-table.is-stable-cols th.ecb-col-value,
    .ecb-table.is-stable-cols th.ecb-col-note { width: auto; }
}

/* Sortable table headers */
.ecb-table th.ecb-sortable {
    cursor: pointer;
    user-select: none;
}

.ecb-table th.ecb-sortable:hover {
    color: var(--theme-primary);
}

.ecb-table th .ecb-sort-arrow {
    font-size: 10px;  /* off-scale: annotates an 11px header, so it sits under it */
    margin-left: 3px;
    color: var(--theme-fg-subtle);
}

.ecb-table th.ecb-sort-active .ecb-sort-arrow {
    color: var(--theme-primary);
}

/* ==========================================================================
   Status badges
   ========================================================================== */

.ecb-badge {
    display: inline-block;
    padding: 2px var(--ecb-space-2);
    border-radius: var(--ecb-radius-pill);
    font-size: var(--ecb-type-micro);
    font-weight: 600;
    letter-spacing: 0.3px;
}

/* Six tones, four tokens: `draft` and `warning` are the same amber, and
   `published` and `info` the same blue. They were six literal pastels before
   EC-3590 and needed a whole second block to exist in dark mode at all. */
.ecb-badge.draft { background: var(--theme-warning-soft); color: var(--theme-warning); }
.ecb-badge.approved { background: var(--theme-success-soft); color: var(--theme-success); }
.ecb-badge.published { background: var(--theme-info-soft); color: var(--theme-info); }
.ecb-badge.rejected { background: var(--theme-danger-soft); color: var(--theme-danger); }
.ecb-badge.info { background: var(--theme-info-soft); color: var(--theme-info); }
.ecb-badge.warning { background: var(--theme-warning-soft); color: var(--theme-warning); }
/* EC-3564: two CRM pages wrote `ecb-badge-muted` as a standalone name, which
   would have been the only badge not participating in the component. */
.ecb-badge.muted { background: var(--theme-bg-subtle); color: var(--theme-fg-muted); }

/* ==========================================================================
   Score indicators
   ========================================================================== */

.ecb-score {
    display: inline-block;
    font-weight: 700;
    font-size: var(--ecb-type-body);
}

/* `excellent` and `good` were #0a7e0a and #2e7d32 — two greens six percent
   apart, which is a distinction nobody could see and no token should invent.
   Both are the success tone now. EC-3590. */
.ecb-score.excellent { color: var(--theme-success); }
.ecb-score.good { color: var(--theme-success); }
.ecb-score.average { color: var(--theme-warning); }
.ecb-score.poor { color: var(--theme-danger); }

/* ==========================================================================
   Tabs
   ========================================================================== */

.ecb-tabs {
    display: flex;
    gap: 0;
    border-bottom: 2px solid var(--theme-border);
    margin-bottom: var(--ecb-space-5);
}

.ecb-tab {
    padding: var(--ecb-space-2) var(--ecb-space-5);
    color: var(--theme-fg-muted);
    text-decoration: none;
    font-size: var(--ecb-type-body);
    font-weight: 500;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    transition: color 0.15s, border-color 0.15s;
    cursor: pointer;
}

.ecb-tab:hover {
    color: var(--theme-fg);
}

.ecb-tab.active {
    color: var(--theme-primary);
    border-bottom-color: var(--theme-primary);
    font-weight: 600;
}

/* ==========================================================================
   Charts
   ========================================================================== */

.ecb-chart-container {
    position: relative;
    height: 250px;
    max-width: 100%;
}

/* ==========================================================================
   Grid sections
   ========================================================================== */

.ecb-grid-2 {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--ecb-space-5);
}

.ecb-grid-3 {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: var(--ecb-space-5);
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.ecb-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--ecb-space-1);
    padding: var(--ecb-space-2) var(--ecb-space-4);
    border: none;
    border-radius: var(--ecb-radius-sm);
    font-size: var(--ecb-type-body);
    font-weight: 500;
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s;
}

.ecb-btn-primary {
    background: var(--theme-primary);
    color: var(--theme-on-primary) !important;
}

.ecb-btn-primary:hover {
    background: var(--theme-primary-hover);
    color: var(--theme-on-primary) !important;
}

.ecb-btn,
.ecb-btn-secondary {
    background: var(--theme-bg-subtle);
    color: var(--theme-fg);
}

.ecb-btn:hover,
.ecb-btn-secondary:hover {
    background: var(--theme-border);
}

/* Restore explicit primary / danger backgrounds.
   `.ecb-btn` and `.ecb-btn-secondary` share a `background: subtle` rule
   defined after `.ecb-btn-primary` above. Same specificity (one class
   each) means the later rule wins, which silently turns primary buttons
   into subtle/grey ones. The compound `.ecb-btn.ecb-btn-primary`
   selector below has higher specificity (two classes) and pins both the
   background and colour back to the intended primary/danger values. */
.ecb-btn.ecb-btn-primary {
    background: var(--theme-primary);
    color: var(--theme-on-primary) !important;
}
.ecb-btn.ecb-btn-primary:hover {
    background: var(--theme-primary-hover);
}
.ecb-btn.ecb-btn-danger {
    background: var(--theme-danger);
    color: var(--theme-on-primary) !important;
}
.ecb-btn.ecb-btn-danger:hover {
    background: var(--theme-danger-hover);
}

.ecb-btn-danger {
    background: var(--theme-danger);
    color: var(--theme-on-primary) !important;
}

.ecb-btn-danger:hover {
    background: var(--theme-danger-hover);
    color: var(--theme-on-primary) !important;
}

.ecb-btn-sm {
    padding: var(--ecb-space-1) var(--ecb-space-2);
    font-size: var(--ecb-type-micro);
}

.ecb-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ==========================================================================
   Modals
   ========================================================================== */

.ecb-modal-overlay {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: var(--theme-scrim);
    /* EC-4766: on the modal band like .ecb-quickmodal. It used to sit at
       1100, below the shell and every other dialog (EC-3674 left the call
       open); the stacking bands decided it. */
    z-index: var(--z-modal, 10000);
    display: flex;
    align-items: center;
    justify-content: center;
}

.ecb-modal {
    background: var(--theme-bg-elevated);
    border-radius: var(--ecb-radius);
    width: 600px;
    max-width: 90vw;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    box-shadow: var(--theme-shadow-3);
}

.ecb-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--ecb-space-4) var(--ecb-space-5);
    border-bottom: 1px solid var(--theme-border);
}

.ecb-modal-header h3 {
    font-size: var(--ecb-type-title);
    color: var(--theme-fg);
    margin: 0;
}

.ecb-modal-close {
    background: none;
    border: none;
    font-size: var(--ecb-type-display);
    color: var(--theme-fg-subtle);
    cursor: pointer;
    padding: 0 var(--ecb-space-1);
    line-height: 1;
}

.ecb-modal-close:hover { color: var(--theme-fg); }

.ecb-modal-body {
    padding: var(--ecb-space-5);
    overflow-y: auto;
    flex: 1;
    color: var(--theme-fg);
}

.ecb-modal-footer {
    padding: var(--ecb-space-3) var(--ecb-space-5);
    border-top: 1px solid var(--theme-border);
    display: flex;
    gap: var(--ecb-space-2);
    justify-content: flex-end;
}

/* ==========================================================================
   Loading states
   ========================================================================== */

.ecb-loading {
    text-align: center;
    padding: var(--ecb-space-6) var(--ecb-space-5);
    color: var(--theme-fg-muted);
}

.ecb-spinner {
    display: inline-block;
    width: 24px;
    height: 24px;
    border: 3px solid var(--theme-border);
    border-top-color: var(--theme-primary);
    border-radius: 50%;
    animation: ecb-spin 0.8s linear infinite;
}

@keyframes ecb-spin {
    to { transform: rotate(360deg); }
}

/* ==========================================================================
   Toast notifications
   ========================================================================== */

.ecb-toast {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: var(--z-toast);
    min-width: 280px;
    max-width: 420px;
    padding: var(--ecb-space-3) var(--ecb-space-4);
    border-radius: var(--ecb-radius);
    font-size: var(--ecb-type-body);
    color: var(--theme-on-primary);
    box-shadow: var(--theme-shadow-2);
    opacity: 0;
    transform: translateY(-10px);
    transition: opacity 0.3s, transform 0.3s;
    pointer-events: none;
}

.ecb-toast.visible {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}

/* A toast is a filled tone, so its ink is the on-fill colour rather than
   white — the dark palette's success is a light green, and white on it is
   the same unreadable pairing the dark block used to work around by making
   every toast neutral and throwing the tone away. */
.ecb-toast.success { background: var(--theme-success); }
.ecb-toast.error { background: var(--theme-danger); }
.ecb-toast.info { background: var(--theme-primary); }
.ecb-toast.warning { background: var(--theme-warning); }

/* ==========================================================================
   Search dropdown (autocomplete)
   ========================================================================== */

.ecb-search-dropdown {
    display: none;
    position: absolute;
    z-index: var(--z-dropdown);
    left: 0; right: 0;
    background: var(--theme-bg-elevated);
    border: 1px solid var(--theme-border-strong);
    border-radius: 0 0 var(--ecb-radius-sm) var(--ecb-radius-sm);
    max-height: 200px;
    overflow-y: auto;
    box-shadow: var(--theme-shadow-2);
}

.ecb-search-dropdown.open {
    display: block;
}

.ecb-search-item {
    padding: var(--ecb-space-2) var(--ecb-space-3);
    cursor: pointer;
    border-bottom: 1px solid var(--theme-border);
    font-size: var(--ecb-type-body);
}

.ecb-search-item:hover,
.ecb-search-item.highlighted {
    background: var(--theme-primary-soft);
}

.ecb-search-item:last-child {
    border-bottom: none;
}

/* ==========================================================================
   Form groups & explanations
   ========================================================================== */

.ecb-form-group {
    margin-bottom: var(--ecb-space-3);
}

.ecb-form-group label {
    display: block;
    font-size: var(--ecb-type-micro);
    color: var(--theme-fg-muted);
    margin-bottom: var(--ecb-space-1);
    font-weight: 600;
}

.ecb-explanation {
    background: var(--theme-info-soft);
    border: 1px solid var(--theme-info-line);
    border-radius: var(--ecb-radius);
    padding: var(--ecb-space-3) var(--ecb-space-4);
    font-size: var(--ecb-type-body);
    color: var(--theme-fg);
    margin-bottom: var(--ecb-space-4);
}

/* EC-4813: an explanation the reader has to act on. */
.ecb-explanation.ecb-explanation-warning {
    background: var(--theme-warning-soft);
    border-color: var(--theme-warning-line);
}

/* ==========================================================================
   Responsive
   ========================================================================== */

@media (max-width: 900px) {
    .ecb-layout {
        flex-direction: column;
        margin: -10px -20px;
    }
    .ecb-sidebar {
        width: 100%;
        min-width: 100%;
        border-radius: 4px 4px 0 0;
        padding: 4px 0;
    }
    .ecb-nav {
        display: flex;
        overflow-x: auto;
    }
    .ecb-nav-item a {
        padding: 8px 16px;
        white-space: nowrap;
    }
    .ecb-nav-item.active a {
        border-left: none;
        border-bottom: 3px solid var(--theme-primary);
        padding-left: 16px;
    }
    .ecb-main {
        border-radius: 0 0 4px 4px;
        padding: 16px;
    }
    .ecb-grid-2,
    .ecb-grid-3 {
        grid-template-columns: 1fr;
    }
    .ecb-kpis {
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    }
}

/* ==========================================================================
   Dark mode
   ========================================================================== */

/* Dark theme overrides for shared hub components.
   Driven by [data-theme="dark"] which is set on <html> by the FOUC-prevention
   script in modern/base.html + hub_base.html (which honours either an explicit
   localStorage choice or the system prefers-color-scheme). Replaces the old
   @media (prefers-color-scheme: dark) block which used unrelated #2d2d2d-era
   greys — these tokens are slate-* aligned. */
[data-theme="dark"] .ecb-main { background: var(--theme-bg); }

/* KPI cards, section titles and table chrome used to need eighteen lines here
   to undo eighteen literals. They are tokens now, so light and dark are the
   same rule. EC-3590.

   `.ecb-section` keeps its dark background: that one is not undoing a
   literal — the light rule sets no background at all, so deleting it would
   remove a surface rather than restore one. Whether a section should be a
   card in one theme and bare in the other is a design question, not a token
   question, and it is left where it was. */
[data-theme="dark"] .ecb-section { background: var(--theme-bg-elevated); border-color: var(--theme-border); }

[data-theme="dark"] .ecb-table td { color: var(--theme-fg); }
/* Defeat any zebra-striping (Django admin's nth-child rules etc.) by giving each
   row an explicit theme background. Hover wins via specificity below. */
[data-theme="dark"] .ecb-table tbody tr:nth-child(odd) td,
[data-theme="dark"] .ecb-table tbody tr:nth-child(even) td { background: var(--theme-bg-elevated); }
[data-theme="dark"] .ecb-table tbody tr:hover td,
[data-theme="dark"] .ecb-table tbody tr:nth-child(odd):hover td,
[data-theme="dark"] .ecb-table tbody tr:nth-child(even):hover td { background: var(--theme-bg-subtle); }

/* Universal zebra-striping defeat for ANY table inside .ecb-main in dark mode.
   Django admin's base CSS sets odd/even row backgrounds (white / #f9f9f9) on
   tables it considers part of the admin shell — those leak into our hub
   tables (.sa-table, .sh-table, .gp-table, .fc-matrix, .ai-preview-table etc.)
   and render light text on light bg. This rule wipes them so the hub's own
   colours show through. Hub-specific overrides win via more-specific selectors. */
[data-theme="dark"] .ecb-main table tbody tr,
[data-theme="dark"] .ecb-main table tbody tr td,
[data-theme="dark"] .ecb-main table tbody tr:nth-child(odd),
[data-theme="dark"] .ecb-main table tbody tr:nth-child(even),
[data-theme="dark"] .ecb-main table tbody tr:nth-child(odd) td,
[data-theme="dark"] .ecb-main table tbody tr:nth-child(even) td { background: transparent; color: var(--theme-fg); }
[data-theme="dark"] .ecb-main table tbody tr:hover td,
[data-theme="dark"] .ecb-main table tbody tr:nth-child(odd):hover td,
[data-theme="dark"] .ecb-main table tbody tr:nth-child(even):hover td { background: var(--theme-bg-subtle); }
/* Headers get a subtle theme-aware bg so they read as distinct from body rows
   without being jarring (matches the .sph-mini-table treatment). */
[data-theme="dark"] .ecb-main table thead th,
[data-theme="dark"] .ecb-main table thead tr,
[data-theme="dark"] .ecb-main table thead tr th { background: var(--theme-bg-subtle); color: var(--theme-fg-muted); border-bottom-color: var(--theme-border); }

/* Inline `<code>` / `<pre>` inside hub tables — keep text bright (not the
   muted fg-muted some admin CSS slips in), with a subtle bg tint so the
   monospace runs read as code. */
[data-theme="dark"] .ecb-main code,
[data-theme="dark"] .ecb-main pre,
[data-theme="dark"] .ecb-main td code,
[data-theme="dark"] .ecb-main td pre {
    color: var(--theme-fg);
    background: var(--theme-bg-subtle);
    padding: 1px 4px;
    border-radius: 3px;
}
[data-theme="dark"] .ecb-main pre,
[data-theme="dark"] .ecb-main td pre { padding: 8px 12px; }

/* Modal chrome, tabs, the secondary button, the search dropdown, the
   explanation panel and form labels: all tokens now, all identical in both
   themes, all deleted from here. EC-3590.

   The one line left standing is the search item's text colour, because the
   light rule sets none — an item inherits, and what it inherits from a hub
   page in dark mode is not reliably the panel's foreground. */
[data-theme="dark"] .ecb-search-item { color: var(--theme-fg); }

[data-theme="dark"] .ecb-main input[type="text"],
[data-theme="dark"] .ecb-main input[type="number"],
[data-theme="dark"] .ecb-main input[type="month"],
[data-theme="dark"] .ecb-main input[type="date"],
[data-theme="dark"] .ecb-main input[type="datetime-local"],
[data-theme="dark"] .ecb-main input[type="email"],
[data-theme="dark"] .ecb-main input[type="search"],
[data-theme="dark"] .ecb-main input[type="tel"],
[data-theme="dark"] .ecb-main input[type="time"],
[data-theme="dark"] .ecb-main input[type="url"],
[data-theme="dark"] .ecb-main input[type="password"],
[data-theme="dark"] .ecb-main textarea {
    background: var(--theme-bg-subtle);  /* one notch lighter than .ecb-section cards (mirrors light-mode: white input on slate-50 page) */
    color: var(--theme-fg);
    border-color: var(--theme-border-strong);
}
/* Date pickers — invert the native calendar icon so it's visible on dark bg. */
[data-theme="dark"] .ecb-main input[type="date"]::-webkit-calendar-picker-indicator,
[data-theme="dark"] .ecb-main input[type="datetime-local"]::-webkit-calendar-picker-indicator,
[data-theme="dark"] .ecb-main input[type="time"]::-webkit-calendar-picker-indicator { filter: invert(0.85); }

/* Placeholder + date-input native sub-parts. Browsers render the "yyyy-mm-dd"
   pieces inside <input type="date"> via shadow-DOM substyles that don't inherit
   the parent color. Set them explicitly so empty/filled date inputs look the
   same as the text inputs next to them. */
[data-theme="dark"] .ecb-main input::placeholder,
[data-theme="dark"] .ecb-main textarea::placeholder { color: var(--theme-fg-subtle); }
[data-theme="dark"] .ecb-main input[type="date"]::-webkit-datetime-edit,
[data-theme="dark"] .ecb-main input[type="date"]::-webkit-datetime-edit-text,
[data-theme="dark"] .ecb-main input[type="date"]::-webkit-datetime-edit-year-field,
[data-theme="dark"] .ecb-main input[type="date"]::-webkit-datetime-edit-month-field,
[data-theme="dark"] .ecb-main input[type="date"]::-webkit-datetime-edit-day-field,
[data-theme="dark"] .ecb-main input[type="datetime-local"]::-webkit-datetime-edit,
[data-theme="dark"] .ecb-main input[type="datetime-local"]::-webkit-datetime-edit-text,
[data-theme="dark"] .ecb-main input[type="time"]::-webkit-datetime-edit,
[data-theme="dark"] .ecb-main input[type="time"]::-webkit-datetime-edit-text { color: var(--theme-fg-subtle); }
/* When the field has a value, parts read in regular fg colour. */
[data-theme="dark"] .ecb-main input[type="date"]:valid::-webkit-datetime-edit-year-field,
[data-theme="dark"] .ecb-main input[type="date"]:valid::-webkit-datetime-edit-month-field,
[data-theme="dark"] .ecb-main input[type="date"]:valid::-webkit-datetime-edit-day-field { color: var(--theme-fg); }
/* Select gets the shorthand kept intact so background-repeat: no-repeat and
   background-position: right 12px center stick (otherwise the SVG arrow tiles
   across the whole control). */
[data-theme="dark"] .ecb-main select {
    background: var(--theme-bg-subtle) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23cbd5e1' fill='none' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 12px center;
    color: var(--theme-fg);
    border-color: var(--theme-border-strong);
}

/* The dark toast rule painted every toast neutral, which made a success and
   an error look the same — it existed because the light rule filled the toast
   with a literal green or red that white text needed. Both are tokens now, so
   the tone survives into dark mode instead of being flattened away. EC-3590. */

/* Select2 widgets — ship their own CSS bundle that's hardcoded white/light.
   Remap the surfaces to theme tokens in dark mode. Scoped broadly so admin,
   modern and hub uses all switch consistently. */
[data-theme="dark"] .select2-container--default .select2-selection--single,
[data-theme="dark"] .select2-container--default .select2-selection--multiple {
    background: var(--theme-bg-subtle);  /* lighter than .ecb-section card so the input stands out */
    border-color: var(--theme-border-strong);
    color: var(--theme-fg);
}
[data-theme="dark"] .select2-container--default .select2-selection--single .select2-selection__rendered,
[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__rendered { color: var(--theme-fg); }
[data-theme="dark"] .select2-container--default .select2-selection--single .select2-selection__placeholder,
[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__placeholder { color: var(--theme-fg-subtle); }
[data-theme="dark"] .select2-container--default .select2-selection__arrow b { border-top-color: var(--theme-fg-subtle); }
[data-theme="dark"] .select2-container--default .select2-selection__clear { color: var(--theme-fg-subtle); }
[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__choice {
    background: var(--theme-bg-subtle);
    border-color: var(--theme-border);
    color: var(--theme-fg);
}
[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { color: var(--theme-fg-subtle); }

/* Dropdown panel + search input + result rows. */
[data-theme="dark"] .select2-dropdown {
    background: var(--theme-bg-elevated);
    border-color: var(--theme-border);
    color: var(--theme-fg);
}
[data-theme="dark"] .select2-container--default .select2-search--dropdown .select2-search__field {
    background: var(--theme-bg);
    border-color: var(--theme-border);
    color: var(--theme-fg);
}
[data-theme="dark"] .select2-results__option { color: var(--theme-fg); }
[data-theme="dark"] .select2-container--default .select2-results__option--highlighted[aria-selected],
[data-theme="dark"] .select2-container--default .select2-results__option--highlighted {
    background: var(--theme-primary);
    color: var(--theme-on-primary);
}
[data-theme="dark"] .select2-container--default .select2-results__option[aria-selected="true"] {
    background: var(--theme-bg-subtle);
    color: var(--theme-fg);
}
[data-theme="dark"] .select2-container--default .select2-results__group { color: var(--theme-fg-muted); }
[data-theme="dark"] .select2-container--focus .select2-selection,
[data-theme="dark"] .select2-container--default.select2-container--focus .select2-selection--multiple { border-color: var(--theme-primary); }

/* Rich-text content inside every Summernote editor in modern views (EC-4373). The shell's reset leaves
   h2 at body size and lists without bullets, so a description with a heading and
   a spec list read as one run of plain paragraphs — in the editor, where the
   reviewer decides whether the structure is right. One rule for every editor;
   do not restyle per page. */
.twp .note-editable h2 { font-size: var(--ecb-type-title); font-weight: 600; line-height: 1.3; margin: .9em 0 .4em; }
.twp .note-editable h3 { font-size: var(--ecb-type-lead); font-weight: 600; line-height: 1.3; margin: .8em 0 .35em; }
.twp .note-editable h4 { font-size: var(--ecb-type-body); font-weight: 600; margin: .7em 0 .3em; }
.twp .note-editable > :first-child { margin-top: 0; }
.twp .note-editable p { margin: 0 0 .6em; }
.twp .note-editable ul { list-style: disc; padding-left: 1.4em; margin: 0 0 .6em; }
.twp .note-editable ol { list-style: decimal; padding-left: 1.4em; margin: 0 0 .6em; }
.twp .note-editable li { display: list-item; margin: .15em 0; }
.twp .note-editable blockquote { border-left: 3px solid var(--theme-border-strong); padding-left: .8em; margin: 0 0 .6em; color: var(--theme-fg-muted); }

/* Summernote rich-text editor — content area needs explicit light text in
   dark mode. The toolbar stays as-is (it's iconographic with its own pale bg
   that reads on dark). */
[data-theme="dark"] .note-editor .note-editing-area .note-editable,
[data-theme="dark"] .note-editable {
    background: var(--theme-bg-subtle);
    color: var(--theme-fg);
}
[data-theme="dark"] .note-editor .note-editing-area .note-codable {
    background: var(--theme-bg-subtle);
    color: var(--theme-fg);
    border-color: var(--theme-border-strong);
}
[data-theme="dark"] .note-editor.note-frame { border-color: var(--theme-border); }
[data-theme="dark"] .note-editor .note-statusbar { background: var(--theme-bg-subtle); border-top-color: var(--theme-border); }
[data-theme="dark"] .note-editor .note-placeholder { color: var(--theme-fg-subtle); }

/* ── Act-as banner (EC-3143) ───────────────────────────────────────────────
   Shown for as long as a superuser is acting as another user. Deliberately
   loud and never dismissible: its whole job is to stop you editing a
   customer's data believing you are yourself. Amber rather than red — this is
   a state to stay aware of, not an error. */
.act-as-banner {
    position: sticky;
    top: 0;
    z-index: var(--z-banner);          /* above the topbar, below the modals (10900+) */
    background: var(--theme-alert-fill);
    color: var(--theme-alert-on-fill);
    box-shadow: var(--theme-shadow-1);
}
.act-as-banner__inner {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: var(--ecb-space-2) var(--ecb-space-4);
    font-size: var(--ecb-type-body);
    line-height: 1.3;
    flex-wrap: wrap;
}
.act-as-banner__icon { height: 18px; width: 18px; flex: 0 0 auto; }
.act-as-banner__text { flex: 1 1 auto; }
.act-as-banner__text strong { font-weight: 600; }
.act-as-banner__muted { opacity: .8; margin-left: var(--ecb-space-2); font-size: var(--ecb-type-micro); }
.act-as-banner__form { flex: 0 0 auto; margin: 0; }
/* Tinted from the banner's own ink rather than from a literal white, so the
   button follows the fill if the fill ever changes. */
.act-as-banner__button {
    background: color-mix(in srgb, var(--theme-alert-on-fill) 15%, transparent);
    border: 1px solid color-mix(in srgb, var(--theme-alert-on-fill) 45%, transparent);
    color: var(--theme-alert-on-fill);
    border-radius: var(--ecb-radius-sm);
    padding: var(--ecb-space-1) var(--ecb-space-2);
    font-size: var(--ecb-type-micro);
    font-weight: 500;
    cursor: pointer;
}
.act-as-banner__button:hover { background: color-mix(in srgb, var(--theme-alert-on-fill) 28%, transparent); }

/* ── Back to Gen 2 (EC-4579) ───────────────────────────────────────────────
   Shown on Django pages opened from the Gen 2 interface, so leaving Gen 2 is
   never one-way. Quiet on purpose: it is a way home, not a warning. */
.gen2-return-bar {
    background: var(--theme-bg-subtle);
    border-bottom: 1px solid var(--theme-border);
}
.gen2-return-bar__inner {
    display: flex;
    align-items: center;
    gap: var(--ecb-space-2);
    padding: var(--ecb-space-1) var(--ecb-space-4);
    font-size: var(--ecb-type-micro);
}
.gen2-return-bar__link { color: var(--theme-primary); font-weight: 500; text-decoration: none; }
.gen2-return-bar__link:hover { text-decoration: underline; }
.gen2-return-bar__muted { color: var(--theme-fg-muted); }

/* ── Copy-to-catalog modal (EC-3840) ─────────────────────────────────────────
   Two pieces that would otherwise be inline styles, which the craft-layer
   ratchet rightly refuses. Neither has a Tailwind class in the committed
   bundle: `resize-none` is simply absent, and rebuilding the bundle to add it
   is EC-3579's job, not a side effect of a modal fix.

   The name field is a textarea so a long product name wraps instead of being
   clipped; it is sized to its content by `autoGrow()`, so the drag handle would
   fight the script and is taken away. The fold toggle lines up with the variant
   children above it, which carry the same 24px indent.  */
.twp .ecb-copy-name { resize: none; }
.twp .ecb-copy-fold { margin-left: 24px; width: calc(100% - 24px); }

/* ── Tooltips inside modern views ────────────────────────────────────────────
   `data-tip` + `icon-tooltip` / `help-tooltip` is the house tooltip convention
   (CLAUDE.md bans the `title` attribute), but the CSS for it lived only in
   static/catalog/css/product_custom_admin.css. Modern views do not load that
   file, so every data-tip on a modern page — including the help icon in the
   shared page header — silently did nothing (EC-3236).

   Scoped to .twp, which the modern shell puts on <main>. The product detail
   opts its body out of .twp precisely because it brings its own complete CSS,
   so these rules cannot collide with the originals. Those need JS to add
   .tooltip-visible after a delay; these are hover/focus only, so they work
   without shipping that JS to every modern page. */
.twp .icon-tooltip,
.twp .help-tooltip { position: relative; }

.twp .icon-tooltip::after,
.twp .help-tooltip::after {
    content: attr(data-tip);
    position: absolute;
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    padding: var(--ecb-space-1) var(--ecb-space-2);
    font-size: var(--ecb-type-micro);
    font-weight: 400;
    line-height: 1.4;
    color: var(--theme-tooltip-fg);
    background: var(--theme-tooltip-bg);
    border-radius: var(--ecb-radius);
    white-space: nowrap;
    z-index: var(--z-tooltip);
    box-shadow: var(--theme-shadow-2);
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity .12s ease-in-out;
}

.twp .icon-tooltip::before,
.twp .help-tooltip::before {
    content: '';
    position: absolute;
    bottom: calc(100% + 2px);
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-top-color: var(--theme-tooltip-bg);
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity .12s ease-in-out;
}

/* Keyboard users get the same affordance as the mouse. */
.twp .icon-tooltip:hover::after,
.twp .icon-tooltip:hover::before,
.twp .icon-tooltip:focus-visible::after,
.twp .icon-tooltip:focus-visible::before,
.twp .help-tooltip:hover::after,
.twp .help-tooltip:hover::before,
.twp .help-tooltip:focus-visible::after,
.twp .help-tooltip:focus-visible::before {
    opacity: 1;
    visibility: visible;
}

/* EC-3674: when tooltip_escape.js has taken over for this element — because an
   ancestor clips — the pseudo-element must get out of the way, or the tip
   renders twice and the clipped copy is the one nearest the cursor.

   That suppression used to live here, naming these two classes. It is now in
   static/common/css/tooltip_escape.css, loaded next to the script itself:
   scoping it to .icon-tooltip / .help-tooltip covered 134 of the tree's 277
   data-tip elements and left the other 143 — including every bare data-tip on
   a modern page — drawing both tips at once (EC-3839). Do not re-add a copy
   here; a second definition is how the two stopped agreeing in the first
   place. */

/* The escaped tip itself. Same ink, background, radius and shadow as the
   ::after above, so which mechanism drew it is not something a user can see.
   Position and offsets are left to EcbOverlay. */
.ecb-escaped-tip {
    position: fixed;
    padding: var(--ecb-space-1) var(--ecb-space-2);
    font-size: var(--ecb-type-micro);
    font-weight: 400;
    line-height: 1.4;
    color: var(--theme-tooltip-fg);
    background: var(--theme-tooltip-bg);
    border-radius: var(--ecb-radius);
    white-space: nowrap;
    z-index: var(--z-tooltip);
    box-shadow: var(--theme-shadow-2);
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity .12s ease-in-out;
}
.ecb-escaped-tip.is-wrapped {
    white-space: normal;
    max-width: 280px;
}

/* A long tip would otherwise run off the viewport on one line. */
.twp .icon-tooltip[data-tip-wrap]::after,
.twp .help-tooltip[data-tip-wrap]::after {
    white-space: normal;
    width: max-content;
    max-width: 240px;
}

/* ─────────────────────────────────────────────────────────────────────────
   In-place relation editors — EC-3251 / EC-3255 (pencil triggers)

   These rules used to be injected by static/common/js/relation_modal.js at
   load. That script is deferred, so on a long changelist the server-rendered
   pencils were painted unsized — an SVG with no width/height falls back to
   300x150 px — for the whole parse+script window (EC-3362). Sizing has to be in
   the head with the rest of the CSS; not scoped to .twp because the triggers
   also render in classic admin changelist cells.
   ───────────────────────────────────────────────────────────────────────── */
.ecb-pn-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    vertical-align: middle;
    width: 1.15em;
    height: 1.15em;
    color: var(--theme-fg-muted);
    text-decoration: none;
    position: relative;
    cursor: pointer;
}

/* A trigger is an <a> in the classic changelist cells and a <button> in the
   modern components. Tailwind's preflight resets buttons, but it is scoped to
   `.twp` — so a <button> trigger rendered on a classic page kept the admin's
   own button chrome and the pencil showed up as an empty grey box (EC-3959).
   The reset belongs here, with the rest of what a trigger looks like, rather
   than as inline styles on every template that renders one. */
button.ecb-pn-trigger {
    -webkit-appearance: none;
    appearance: none;
    border: 0;
    background: none;
    padding: 0;
    margin: 0;
    line-height: 0;
}

.ecb-pn-trigger:hover,
.ecb-pn-trigger:focus {
    color: var(--theme-primary);
    text-decoration: none;
}

.ecb-pn-trigger svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* Classic admin has no global .icon-tooltip rule, so the tooltip is defined
   here too — scoped to the trigger, and driven by data-tip per the project
   rule that the title attribute is never used for tooltips. */
.ecb-pn-trigger[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    bottom: 130%;
    left: 50%;
    transform: translateX(-50%);
    background: var(--theme-tooltip-bg);
    color: var(--theme-tooltip-fg);
    font-size: var(--ecb-type-eyebrow);
    line-height: 1.3;
    white-space: nowrap;
    padding: var(--ecb-space-1) 7px;
    border-radius: var(--ecb-radius-sm);
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s;
    z-index: var(--z-tooltip);
}

/* `:focus-visible`, not `:focus` — the same as the house `.icon-tooltip`
   convention above. A pencil keeps focus after it is clicked, so with `:focus`
   the tooltip stayed on screen over the modal the click had just opened, until
   something else was clicked to take the focus away (EC-3959). Keyboard users
   still get it: `:focus-visible` is exactly the case where the focus arrived
   without a pointer. */
.ecb-pn-trigger[data-tip]:hover::after,
.ecb-pn-trigger[data-tip]:focus-visible::after {
    opacity: 1;
}

/* The related-products cell's total, and the control that reveals the rest
   (EC-3959). Both surfaces use them — the classic changelist has no Tailwind
   of its own and the modern cell inherits nothing here — so they are styled
   once, next to the pencil they sit beside. The chevron turns rather than
   being swapped, and the tooltip swaps between "show N more" and "show fewer"
   from the script. */
.ecb-rel-total,
.ecb-rel-toggle {
    color: var(--theme-fg-muted);
}

.ecb-rel-toggle {
    display: inline-flex;
    align-items: center;
    gap: 1px;
    -webkit-appearance: none;
    appearance: none;
    border: 0;
    background: none;
    padding: 0;
    margin: 0;
    line-height: 1;
    cursor: pointer;
    position: relative;
}

.ecb-rel-toggle:hover,
.ecb-rel-toggle:focus-visible {
    color: var(--theme-primary);
}

.ecb-rel-toggle svg {
    transition: transform .12s ease-in-out;
}

.ecb-rel-toggle[aria-expanded="true"] svg {
    transform: rotate(180deg);
}

/* No trigger tooltip while one of these modals is open (EC-3959).
   Clicking a pencil leaves the pointer exactly where it was, and `:hover` is
   only re-evaluated when the pointer moves — so the tooltip stayed painted
   over the dialog the click had just opened, and only went away on the next
   click. The class is put on <body> by openShell() in relation_modal.js;
   `!important` because the tooltip deliberately outranks the dialog
   (`--z-tooltip` > `--z-modal`) and the rule above is equally specific. */
body.ecb-rm-modal-open .ecb-pn-trigger[data-tip]::after {
    opacity: 0 !important;
}

/* Optically centre the pencil on the first value line in the change-form
   summary, where line-height is taller than the icon. */
.ecb-pn-summary .ecb-pn-trigger {
    margin-top: .18em;
}

/* ─────────────────────────────────────────────────────────────────────────
   Editable relation chip (EC-3371)

   Modern list chips that double as triggers for the in-place relation editors.
   The chip states the value; the pencil says the value can be changed. Chip
   tones (.chip, .chip-slate, …) are defined per modern page, but this lives
   here because the modern shell loads components.css in the head — the pencil
   is then sized before the deferred modal script runs, same reason as
   .ecb-pn-trigger above (EC-3362).
   ───────────────────────────────────────────────────────────────────────── */
.chip-editable {
    cursor: pointer;
}

.chip-editable .chip-pencil {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: .95em;
    height: .95em;
    opacity: .55;
    transition: opacity .12s;
}

/* The shared PENCIL_SVG carries width/height attributes as its no-CSS
   fallback; inside a chip the wrapper decides the size instead. */
.chip-editable .chip-pencil svg {
    width: 100%;
    height: 100%;
    display: block;
}

.chip-editable:hover .chip-pencil,
.chip-editable:focus-visible .chip-pencil {
    opacity: 1;
}


/* ==========================================================================
   Craft layer — EC-3556

   The vocabulary a report is written in: a title block, a prose measure, a
   panel, a bar, a delta, a plot frame, a run timeline, a caveat.

   These were hand-rolled as `mr-*` for the Model Report and would have been
   hand-rolled again, differently, for the next page that needed a bar. Names
   say what a thing is, not where it first appeared, so `ecb-bar` is the bar on
   any page rather than the bar on the model report.

   Everything here is a real class rather than a utility because Tailwind's
   `.twp` wrapper closes above `{% block hub_content %}` in hub_base.html — a
   utility class written in a hub sub-page is a silent no-op.

   Source order in this block is base rules, then the responsive overrides,
   then the dark overrides. An override placed above the rule it overrides
   loses silently and looks like it works; `common/tests/test_craft_layer.py`
   asserts the order rather than trusting it.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Title blocks and prose
   -------------------------------------------------------------------------- */

/* The way back to the page this one hangs off. Every hub sub-page has one and
   every one of them typed its own margin. */
.ecb-backlink {
    display: inline-block;
    margin: 0 0 var(--ecb-space-4);
    font-size: var(--ecb-type-meta);
}

/* Eyebrow, title, one line of lede. `.is-section` is the same three parts at
   the size a section deserves; the page gets one, sections get the rest. */
.ecb-pagehead {
    margin: 0 0 var(--ecb-space-6);
    max-width: var(--ecb-measure);
}

.ecb-pagehead-title {
    margin: 0 0 var(--ecb-space-2);
    font-size: var(--ecb-type-display);
    line-height: var(--ecb-leading-tight);
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--theme-fg);
    text-wrap: balance;
}

.ecb-pagehead.is-section {
    margin: var(--ecb-space-7) 0 var(--ecb-space-4);
}

.ecb-pagehead.is-section .ecb-pagehead-title {
    font-size: var(--ecb-type-title);
}

/* Mono, tracked out, small. It marks where a new part of the page starts, so
   it is the one place the primary colour appears in running content. */
.ecb-eyebrow {
    font-family: var(--ecb-font-mono);
    font-size: var(--ecb-type-eyebrow);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--theme-primary);
    margin: 0 0 var(--ecb-space-2);
}

.ecb-lede {
    margin: 0;
    font-size: var(--ecb-type-lead);
    line-height: var(--ecb-leading-body);
    color: var(--theme-fg-muted);
    max-width: 58ch;
}

.ecb-prose {
    margin-top: 0;
    margin-bottom: var(--ecb-space-3);
    font-size: var(--ecb-type-body);
    line-height: var(--ecb-leading-body);
    color: var(--theme-fg-muted);
    max-width: var(--ecb-measure);
}

.ecb-prose:last-child { margin-bottom: 0; }
.ecb-prose strong { color: var(--theme-fg); font-weight: 600; }

/* Explanatory prose across the full width of its panel.
 *
 * A panel is as wide as the page; prose is only readable to about 68ch. Left
 * alone that leaves a third of the panel empty, which reads as an unfinished
 * box rather than as a deliberate measure.
 *
 * `columns: 20rem 2` is the whole rule, and the width comes first on purpose:
 * it means "two columns, but never narrower than 20rem", so the browser drops
 * to one column when there is no room for two. That responds to the *space the
 * panel actually has* rather than to the viewport — which matters here because
 * the hub sidebar makes the viewport a poor proxy for the content width, and a
 * breakpoint guessed against it is wrong at exactly one screen size.
 *
 * Use it for three or more paragraphs of explanation. One paragraph in a
 * two-column frame leaves the second column empty, which is the problem this
 * solves, backwards.
 */
.ecb-prose-columns {
    columns: 20rem 2;
    column-gap: var(--ecb-space-6);
}

/* The measure is the column now, so the paragraph must not keep its own. */
.ecb-prose-columns .ecb-prose { max-width: none; }

/* A paragraph split across the column break puts half a sentence at the bottom
   of one column and half at the top of the next, with a gap between them. */
.ecb-prose-columns .ecb-prose { break-inside: avoid; }

.ecb-prose-columns .ecb-prose:last-child { margin-bottom: 0; }

/* The muted line under a thing: what it was scored on, when, out of how many. */
.ecb-meta {
    font-size: var(--ecb-type-meta);
    line-height: var(--ecb-leading-body);
    color: var(--theme-fg-muted);
}

/* Longhand on purpose: a shorthand would reset `margin-left` and, at
   (0,1,1), outrank any modifier that wants to set it — which is how the
   `ecb-indent` under a checkbox lost its indent. */
p.ecb-meta { margin-top: var(--ecb-space-2); margin-bottom: 0; }

/* The same line used as a page's standfirst rather than as an annotation
   under something, so it needs room below it. Written as a modifier on the
   component and not as a margin utility: `p.ecb-meta` is (0,1,1) — the
   longhand above is deliberate, see EC-3565 — and a bare `.foo-mb-5` at
   (0,1,0) loses to it silently. EC-3591 found two of those. */
p.ecb-meta.is-standfirst { margin-bottom: var(--ecb-space-5); }
.ecb-meta strong { color: var(--theme-fg); font-weight: 600; }

/* An honest empty state. A feature with no measurement says so here rather
   than showing a chart of zeroes. */
.ecb-empty {
    font-size: var(--ecb-type-body);
    line-height: var(--ecb-leading-body);
    color: var(--theme-fg-muted);
    max-width: var(--ecb-measure);
    margin-top: 0;
    margin-bottom: 0;
}

/* --------------------------------------------------------------------------
   Panels
   -------------------------------------------------------------------------- */

.ecb-panel {
    background: var(--theme-bg-elevated);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius);
    padding: var(--ecb-space-5);
    margin: 0 0 var(--ecb-space-4);
    box-shadow: var(--theme-shadow-1);
}

/* The one panel on a page that carries its answer. */
.ecb-panel.is-accent { border-left: 3px solid var(--theme-primary); }

/* Supporting material — how a number was arrived at, what it excludes. It
   belongs on the page, and it is not the thing being read first. */
.ecb-panel.is-quiet {
    background: var(--theme-bg-subtle);
    box-shadow: none;
}

/* A heading inside a panel. Smaller than a section title because the panel's
   own border already says a new thing has started. */
.ecb-panel-title {
    margin: 0 0 var(--ecb-space-3);
    font-size: var(--ecb-type-lead);
    line-height: var(--ecb-leading-tight);
    font-weight: 600;
    color: var(--theme-fg);
}

/* A value large enough to be the answer to the page's question. Mono because
   what it usually holds is an identifier — a model name, a SKU, a version. */
.ecb-headline-mono {
    font-family: var(--ecb-font-mono);
    font-size: var(--ecb-type-display);
    line-height: var(--ecb-leading-tight);
    font-weight: 600;
    color: var(--theme-primary);
    overflow-wrap: anywhere;
}

/* Key/value facts under a headline. A definition list rather than prose,
   because each one is a separate claim the reader may want to check. */
.ecb-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--ecb-space-4);
    margin: var(--ecb-space-5) 0 0;
}

.ecb-fact-key {
    font-family: var(--ecb-font-mono);
    font-size: var(--ecb-type-eyebrow);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--theme-fg-muted);
}

.ecb-fact-value {
    margin: var(--ecb-space-1) 0 0;
    font-size: var(--ecb-type-body);
    line-height: var(--ecb-leading-body);
    color: var(--theme-fg);
}

/* One per line rather than comma-joined: a template loop that has to skip rows
   cannot tell which of the ones it kept is last, and a trailing comma after a
   single item is exactly the kind of detail that makes a page look unfinished. */
.ecb-fact-item { display: block; }

/* --------------------------------------------------------------------------
   Choices — a small set of mutually exclusive views of the same page
   -------------------------------------------------------------------------- */

.ecb-choices {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ecb-space-2);
    margin: 0 0 var(--ecb-space-5);
}

.ecb-choice {
    display: block;
    flex: 1 1 14rem;
    min-width: 0;
    padding: var(--ecb-space-3) var(--ecb-space-4);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius);
    background: var(--theme-bg-elevated);
    color: var(--theme-fg);
    text-decoration: none;
    transition: border-color 0.12s, background 0.12s;
}

/*
 * Django admin's base.css underlines every anchor with
 *   a:not([role="button"], #header a, #nav-sidebar a, ...)
 * whose `:not()` takes the specificity of its strongest argument — an id — so
 * the rule lands at (1,0,2) and no class selector can reach it. A whole card
 * of underlined text is the result, which is why this one declaration carries
 * `!important`: the alternative is not a tidier fix, it is no fix. Using
 * `role="button"` to slip into the exception list would be lying about a
 * navigation link to win a cascade argument.
 *
 * The colour needs no such help — admin sets it at `a:link` (0,1,1), which
 * (0,2,0) outranks.
 */
.ecb-choice:link,
.ecb-choice:visited { color: var(--theme-fg); text-decoration: none !important; }

.ecb-choice:hover { border-color: var(--theme-border-strong); }

.ecb-choice:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 2px;
}

.ecb-choice.is-active {
    border-color: var(--theme-primary);
    background: var(--theme-primary-soft);
    box-shadow: inset 0 0 0 1px var(--theme-primary);
}

.ecb-choice-label {
    display: block;
    font-size: var(--ecb-type-body);
    font-weight: 600;
    color: var(--theme-fg);
}

.ecb-choice-hint {
    display: block;
    margin-top: var(--ecb-space-1);
    font-size: var(--ecb-type-meta);
    line-height: 1.4;
    color: var(--theme-fg-muted);
}

/* --------------------------------------------------------------------------
   Bars — one measured value per row, against a shared track
   -------------------------------------------------------------------------- */

.ecb-bars {
    display: grid;
    gap: var(--ecb-space-2);
    margin: var(--ecb-space-3) 0 0;
}

.ecb-bar {
    display: grid;
    grid-template-columns: minmax(9rem, 16rem) 1fr auto;
    gap: var(--ecb-space-3);
    align-items: center;
    font-size: var(--ecb-type-body);
}

.ecb-bar-label {
    min-width: 0;
    overflow-wrap: anywhere;
    color: var(--theme-fg);
}

/*
 * Block elements, deliberately. A <span> ignores height and percentage width,
 * so every bar renders at the same size and the page reads as missing data
 * rather than as a bug. That happened once in the prototype and cost a round of
 * confusion; `ai_content/tests/test_model_report.py` guards it.
 */
.ecb-bar-track {
    display: block;
    height: 18px;
    background: var(--theme-bg-subtle);
    border-radius: var(--ecb-radius-sm);
    overflow: hidden;
}

.ecb-bar-fill {
    display: block;
    height: 100%;
    background: var(--theme-fg-subtle);
}

/*
 * EC-3825. A feature the customer has not used yet is listed anyway, so they
 * can discover it exists. Muted rather than hidden: it is an invitation, and it
 * must not compete with the rows that carry real numbers. The label keeps
 * enough contrast to be read — dimming the row is not the same as retiring it.
 *
 * Placed after the three base `.ecb-bar-*` rules on purpose. CLAUDE.md
 * § "Responsive CSS — the override must come after the rule" is about media
 * queries, but the reason generalises: an override written above what it
 * modifies is a coin toss on specificity, and here it also put a descendant
 * selector above the definition a guard test reads for `display: block`.
 */
.ecb-bar.is-unused .ecb-bar-label,
.ecb-bar.is-unused .ecb-bar-aside {
    color: var(--theme-fg-muted);
}

.ecb-bar.is-unused .ecb-bar-track {
    opacity: 0.45;
}

.ecb-bar-fill.is-best { background: var(--theme-primary); }

.ecb-bar-value {
    font-variant-numeric: tabular-nums;
    min-width: 4.5rem;
    text-align: right;
    color: var(--theme-fg);
}

/* A second, quieter figure after the bar's own — a cost beside a count.
   Its own line so the two never read as one number. EC-3758. */
.ecb-bar-aside {
    display: block;
    font-size: var(--ecb-type-eyebrow);
    color: var(--theme-fg-muted);
}

/* The same bar at the size a supporting series deserves. */
.ecb-bars.is-compact { gap: var(--ecb-space-1); }
.ecb-bars.is-compact .ecb-bar {
    grid-template-columns: minmax(8rem, 13rem) 1fr auto;
    gap: var(--ecb-space-2);
    font-size: var(--ecb-type-meta);
}
.ecb-bars.is-compact .ecb-bar-track { height: 12px; }
.ecb-bars.is-compact .ecb-bar-value { min-width: 3.5rem; }

/* --------------------------------------------------------------------------
   Delta — a signed change, next to the value it changed from
   -------------------------------------------------------------------------- */

.ecb-delta {
    display: inline-block;
    font-variant-numeric: tabular-nums;
    font-size: var(--ecb-type-micro);
    font-weight: 600;
    padding: 2px 7px;
    border-radius: var(--ecb-radius-pill);
}

.ecb-delta.up   { background: var(--theme-success-soft); color: var(--theme-success); }
.ecb-delta.down { background: var(--theme-danger-soft);  color: var(--theme-danger); }
.ecb-delta.flat { background: var(--theme-bg-subtle);    color: var(--theme-fg-muted); }

/* --------------------------------------------------------------------------
   Plot — a frame for an inline SVG chart, and the parts drawn inside it
   -------------------------------------------------------------------------- */

.ecb-plot {
    margin: var(--ecb-space-4) 0 0;
    padding: var(--ecb-space-4) var(--ecb-space-3) var(--ecb-space-2);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius);
    background: var(--theme-bg-elevated);
    overflow-x: auto;
}

/* The frame scrolls rather than the chart shrinking. A scatter plot squeezed
   into 360px keeps its shape and loses its labels, which is worse than a chart
   the reader has to push sideways. */
.ecb-plot svg { display: block; min-width: 26rem; width: 100%; height: auto; }

.ecb-plot-grid {
    stroke: var(--theme-border);
    stroke-width: 1;
    stroke-dasharray: 3 4;
}

.ecb-plot-axis { stroke: var(--theme-border-strong); stroke-width: 1; }

.ecb-plot-tick {
    font-family: var(--ecb-font-mono);
    font-size: 10px;  /* off-scale: a ruler marking, not a label on the data */
    fill: var(--theme-fg-muted);
}

.ecb-plot-dot { fill: var(--theme-primary); }
.ecb-plot-dot.is-dominated { fill: var(--theme-fg-subtle); }

.ecb-plot-name { font-size: var(--ecb-type-eyebrow); font-weight: 600; fill: var(--theme-fg); }
.ecb-plot-name.is-dominated { font-weight: 400; fill: var(--theme-fg-subtle); }

/* What the picture means, under the picture. A plot without one is a shape. */
.ecb-plot-caption {
    margin: var(--ecb-space-2) 0 0;
    font-size: var(--ecb-type-meta);
    line-height: var(--ecb-leading-body);
    color: var(--theme-fg-muted);
    max-width: var(--ecb-measure);
}

/* --------------------------------------------------------------------------
   Timeline — the same measurement repeated over time
   -------------------------------------------------------------------------- */

.ecb-timeline { margin: var(--ecb-space-3) 0 0; }

.ecb-timeline-run {
    display: grid;
    grid-template-columns: 7.5rem 1fr;
    gap: var(--ecb-space-3);
    padding: var(--ecb-space-3) 0;
    border-top: 1px solid var(--theme-border);
}

.ecb-timeline-run:first-child { border-top: 0; padding-top: 0; }

.ecb-timeline-when {
    font-size: var(--ecb-type-meta);
    line-height: 1.45;
    color: var(--theme-fg-muted);
}

.ecb-timeline-body { min-width: 0; }

/* --------------------------------------------------------------------------
   Caveat — what the number above it does not cover
   -------------------------------------------------------------------------- */

.ecb-caveat {
    margin: var(--ecb-space-3) 0 0;
    padding: var(--ecb-space-3) var(--ecb-space-4);
    border: 1px solid var(--theme-warning-line);
    border-radius: var(--ecb-radius-sm);
    background: var(--theme-warning-soft);
    color: var(--theme-warning);
    font-size: var(--ecb-type-body);
    line-height: var(--ecb-leading-body);
    max-width: var(--ecb-measure);
}

.ecb-caveat a { color: inherit; text-decoration: underline; }

.ecb-caveat.is-info {
    border-color: var(--theme-primary);
    background: var(--theme-primary-soft);
    color: var(--theme-primary);
}

.ecb-caveat.is-muted {
    border-color: var(--theme-border);
    background: var(--theme-bg-subtle);
    color: var(--theme-fg-muted);
}

/* --------------------------------------------------------------------------
   Disclosure — a list of rows that each open to their own evidence
   -------------------------------------------------------------------------- */

/* One bordered ledger with hairline-separated rows, rather than a stack of
   separate cards: the rows are a set to be compared, and gaps between cards
   say they are not. */
.ecb-disclosure-list {
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius);
    background: var(--theme-bg-elevated);
    overflow: hidden;
}

.ecb-disclosure { border-top: 1px solid var(--theme-border); }
.ecb-disclosure:first-child { border-top: 0; }

.ecb-disclosure > summary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--ecb-space-3);
    padding: var(--ecb-space-4) var(--ecb-space-5);
    cursor: pointer;
    list-style: none;
    transition: background 0.12s;
}

.ecb-disclosure > summary::-webkit-details-marker { display: none; }
.ecb-disclosure > summary:hover { background: var(--theme-bg-subtle); }

.ecb-disclosure > summary:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: -2px;
}

/* The affordance is a caret rather than a word, and it states which way the
   row will move when it is clicked. */
.ecb-disclosure > summary::after {
    content: "\25BE";
    margin-left: auto;
    color: var(--theme-fg-muted);
}

.ecb-disclosure[open] > summary::after { content: "\25B4"; }
.ecb-disclosure[open] > summary { padding-bottom: var(--ecb-space-2); }

.ecb-disclosure-title {
    flex: 1 1 14rem;
    min-width: 0;
    font-size: var(--ecb-type-lead);
    font-weight: 600;
    color: var(--theme-fg);
}

.ecb-disclosure-body {
    padding: 0 var(--ecb-space-5) var(--ecb-space-5);
}

.ecb-disclosure-body > .ecb-eyebrow { margin-top: var(--ecb-space-5); }

/* --------------------------------------------------------------------------
   Craft layer — responsive

   Below the base rules, which is the whole point: these have the same
   specificity as the rules above and would lose silently if placed first.
   The widths are the ones the shared shell already wraps at.
   -------------------------------------------------------------------------- */

@media (max-width: 900px) {
    .ecb-pagehead-title { font-size: var(--ecb-type-title); }
    .ecb-headline-mono { font-size: var(--ecb-type-title); }
    .ecb-pagehead.is-section { margin-top: var(--ecb-space-6); }

    .ecb-panel { padding: var(--ecb-space-4); }
    .ecb-disclosure > summary { padding: var(--ecb-space-3) var(--ecb-space-4); }
    .ecb-disclosure-body { padding: 0 var(--ecb-space-4) var(--ecb-space-4); }

    /* The label column stops earning its width before the bar does. */
    .ecb-bar,
    .ecb-bars.is-compact .ecb-bar {
        grid-template-columns: minmax(7rem, 11rem) 1fr auto;
    }
}

@media (max-width: 768px) {
    /* A 7.5rem date column against a full-width bar chart leaves the bars too
       narrow to compare, which is the only reason the timeline exists. */
    .ecb-timeline-run { grid-template-columns: 1fr; gap: var(--ecb-space-2); }

    /* Below this the three-column bar cannot hold a model name, a track and a
       value. Label above, track and value beside each other. */
    .ecb-bar,
    .ecb-bars.is-compact .ecb-bar {
        grid-template-columns: 1fr auto;
        row-gap: var(--ecb-space-1);
    }
    .ecb-bar-label,
    .ecb-bars.is-compact .ecb-bar-label {
        grid-column: 1 / -1;
    }

    .ecb-choice { flex-basis: 100%; }
    .ecb-facts { gap: var(--ecb-space-3); }
}

/* Motion here is decoration on a hover, so it goes first when it is unwanted. */
@media (prefers-reduced-motion: reduce) {
    .ecb-choice,
    .ecb-disclosure > summary { transition: none; }
}

/* --------------------------------------------------------------------------
   Craft layer — dark

   Every colour above is already a token, so this block exists only for the two
   surfaces that are not: the plot's own background sits on the elevated
   surface, and a bar track needs more separation from it than slate-700 gives.
   -------------------------------------------------------------------------- */

[data-theme="dark"] .ecb-bar-track,
[data-theme="dark"] .ecb-bars.is-compact .ecb-bar-track {
    background: var(--theme-bg);
}

[data-theme="dark"] .ecb-panel.is-quiet { background: var(--theme-bg); }

/* EC-3564 gave the badge a dark mode by remapping its six literal pastels
   here. EC-3590 put the tones on tokens at the source, so this block said the
   same thing twice — with one drift: `published` was mapped to the success
   tone because there was no info token to map it to. There is now, and a
   published badge is blue in both themes.
*/


/* ==========================================================================
   Names that were written before they existed — EC-3564

   Every class below was already in templates, matching no rule anywhere, and
   therefore rendering as unstyled markup: no error, no warning, nothing in a
   console. Twelve CRM pages laid out their section headings this way; the AI
   Hub dashboard rendered the trial banner — a customer-facing offer — as bare
   text, because `dashboard.html` extends `modern/_partials/hub_template.html`
   rather than the AI hub base that held the styles.

   The names are kept as they were written wherever the shape is genuinely
   reusable, so no template churns for the sake of a rename. Where a craft-layer
   name already said the same thing the templates moved to it instead.

   Base rules, then the responsive overrides, then dark — the media query has
   the same specificity as the rules above it and would lose silently if placed
   first.
   ========================================================================== */

/* The selected one in a row of filter buttons. Undefined until EC-3564, so the
   CRM contact filters — All / Suppliers / Unlinked — gave no indication of
   which one was active. It hid inside a `{% if %}` in the class attribute,
   which is why the first sweep for unstyled names did not see it. */
.ecb-btn.ecb-btn-active {
    background: var(--theme-primary);
    border-color: var(--theme-primary);
    color: var(--theme-on-primary);
}

.ecb-btn.ecb-btn-active:link,
.ecb-btn.ecb-btn-active:visited { color: var(--theme-on-primary); }

/* A section's title with its actions on the same line. The pattern twelve CRM
   pages had already agreed on without a rule to back it. */
.ecb-section-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ecb-space-3);
    margin: 0 0 var(--ecb-space-4);
    padding-bottom: var(--ecb-space-3);
    border-bottom: 1px solid var(--theme-border);
}

.ecb-section-header h2 {
    margin: 0;
    font-size: var(--ecb-type-title);
    line-height: var(--ecb-leading-tight);
    font-weight: 600;
    color: var(--theme-fg);
}

.ecb-header-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ecb-space-2);
    margin-left: auto;
}

/* A list with nothing in it yet. Distinct from `ecb-empty`, which is one muted
   line saying a particular value is missing: this is the whole collection, and
   it is the place to say what to do about it. */
.ecb-empty-state {
    padding: var(--ecb-space-6) var(--ecb-space-4);
    text-align: center;
    color: var(--theme-fg-muted);
    font-size: var(--ecb-type-body);
    line-height: var(--ecb-leading-body);
}

.ecb-empty-state p { margin: 0 0 var(--ecb-space-2); }
.ecb-empty-state p:last-of-type { margin-bottom: var(--ecb-space-4); }
.ecb-empty-state .ecb-btn { margin-top: var(--ecb-space-2); }

/* An inline label in front of a value — "To:", "From:". Not a form label; that
   is `.ecb-form-group label`. */
.ecb-label {
    font-size: var(--ecb-type-meta);
    font-weight: 600;
    color: var(--theme-fg-muted);
    margin-right: var(--ecb-space-1);
}

/* A superuser can be signed in without a company. Both templates that show this
   carried the whole appearance as an inline style, which meant a hardcoded
   amber that stayed light-on-light in dark mode. */
.ecb-no-company-banner {
    margin: 0 0 var(--ecb-space-4);
    padding: var(--ecb-space-3) var(--ecb-space-4);
    border: 1px solid var(--theme-warning-line);
    border-radius: var(--ecb-radius);
    background: var(--theme-warning-soft);
    color: var(--theme-warning);
    font-size: var(--ecb-type-body);
    line-height: var(--ecb-leading-body);
}

/* Spinner sized to sit inside a button next to its label, rather than the
   block `.ecb-spinner` that centres itself in an empty panel. */
.ecb-spinner-inline {
    display: inline-block;
    width: 0.9em;
    height: 0.9em;
    vertical-align: -0.1em;
    margin-right: var(--ecb-space-1);
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: ecb-spin 0.7s linear infinite;
}


/* Which features a setting reaches. Carried its whole appearance inline,
   including a literal #E2E8F0 border that did not survive dark mode. */
.ecb-setting-reach {
    margin: var(--ecb-space-3) 0 0;
    padding: var(--ecb-space-3) var(--ecb-space-4);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius-sm);
    background: var(--theme-bg-subtle);
    font-size: var(--ecb-type-meta);
    line-height: var(--ecb-leading-body);
    color: var(--theme-fg-muted);
}

.ecb-setting-reach strong { display: block; color: var(--theme-fg); }

/* --------------------------------------------------------------------------
   Trial banner — EC-2553, moved here by EC-3564

   It lived in `ai_content/hub/base.html`. `ai_content/hub/dashboard.html`
   extends `modern/_partials/hub_template.html` instead, so the one page most
   likely to show a new customer their trial state rendered it unstyled. Moving
   it out of a page-local <style> is what makes it reach every template that
   renders the banner, which is the actual fix.

   The tones were literal mints and ambers; they are the tokens now, so the
   banner has a dark mode it did not have.
   -------------------------------------------------------------------------- */

.ecb-trial-banner {
    display: flex;
    align-items: center;
    gap: var(--ecb-space-3);
    padding: var(--ecb-space-3) var(--ecb-space-4);
    margin: 0 0 var(--ecb-space-4);
    border-radius: var(--ecb-radius);
    font-size: var(--ecb-type-lead);
    line-height: 1.4;
    box-shadow: var(--theme-shadow-1);
}

.ecb-trial-banner.is-welcome {
    background: var(--theme-primary-soft);
    border: 1px solid var(--theme-primary);
    color: var(--theme-primary);
}

.ecb-trial-banner.is-active {
    background: var(--theme-success-soft);
    border: 1px solid var(--theme-success-line);
    color: var(--theme-success);
}

.ecb-trial-banner.is-expired {
    background: var(--theme-danger-soft);
    border: 1px solid var(--theme-danger-line);
    color: var(--theme-danger);
}

.ecb-trial-banner.is-cancelled {
    background: var(--theme-bg-subtle);
    border: 1px solid var(--theme-border);
    color: var(--theme-fg-muted);
}

.ecb-trial-banner-msg { flex: 1; min-width: 0; }

.ecb-trial-banner-cta {
    flex-shrink: 0;
    padding: var(--ecb-space-2) var(--ecb-space-4);
    border: none;
    border-radius: var(--ecb-radius-sm);
    background: var(--theme-primary);
    color: var(--theme-on-primary);
    font-size: var(--ecb-type-body);
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s;
}

/* Admin's a:not([role="button"], #header a, ...) reaches (1,0,2) — see the
   craft-layer notes. A call to action is the one link that must not look like
   body text, so this is the same deliberate exception. */
.ecb-trial-banner-cta:link,
.ecb-trial-banner-cta:visited {
    color: var(--theme-on-primary);
    text-decoration: none !important;
}
.ecb-trial-banner-cta:hover { background: var(--theme-primary-hover); }

/* --------------------------------------------------------------------------
   Responsive — after the base rules, which is the whole point
   -------------------------------------------------------------------------- */

@media (max-width: 900px) {
    /* Right-aligned actions are right while the row fits on one line and wrong
       the moment it wraps: the heading ends far left on one line and the
       buttons far right on the next, with a meaningless gap between them. The
       same failure `.breadcrumbs-actions` had in the shared shell. */
    .ecb-header-actions { margin-left: 0; }

    .ecb-trial-banner { flex-wrap: wrap; }
    .ecb-trial-banner-cta { flex-basis: 100%; text-align: center; }
}

/* --------------------------------------------------------------------------
   Reduced motion
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    .ecb-spinner-inline { animation-duration: 2s; }
    .ecb-trial-banner-cta { transition: none; }
}


/* ==========================================================================
   Shapes the AI Hub pages were typing inline — EC-3565

   Three pages carried 273 inline `style=` attributes between them, 252 of which
   were constants a class could express. An inline style cannot be overridden by
   a stylesheet without `!important`, so every one of them was a place those
   pages stopped being improvable centrally — and several carried literal
   colours, which is a colour that is wrong on exactly one theme.

   What is deliberately NOT converted: `style="display:none"` as an initial
   state. Those 16 are toggled by 64 `style.display` assignments in the pages'
   own scripts. They are state, not appearance; nobody wants to override a
   visibility toggle from a stylesheet, and rewriting the script to match would
   be a behavioural change with real breakage risk for no styling benefit. Same
   reasoning that left the JS hooks alone in EC-3564.
   ========================================================================== */

/* A table's full-width status row: loading, empty, or failed. Written inline
   six times on the dashboard alone, in two colours, because there was no name
   for "the row that says why there are no rows". */
.ecb-table-status {
    text-align: center;
    padding: var(--ecb-space-4);
    color: var(--theme-fg-muted);
    font-size: var(--ecb-type-body);
}

.ecb-table-status.is-error { color: var(--theme-danger); }
.ecb-meta.is-error { color: var(--theme-danger); }

/* Numeric cells line up on the decimal point and read right-to-left. */
.ecb-table th.num,
.ecb-table td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* Dense cells in a wide table — a cost, a token count. */
.ecb-table td.compact {
    font-size: var(--ecb-type-micro);
    white-space: nowrap;
}

/* A row that navigates. The cursor is the only thing that says so, which is
   why it must not be typed per row and forgotten on one. */
.ecb-table tr.is-clickable { cursor: pointer; }

/* An action that is a link in everything but element: no fill, no border, and
   underlined so it is not mistaken for disabled text. */
.ecb-btn-link {
    background: transparent;
    border: none;
    padding: var(--ecb-space-1) var(--ecb-space-2);
    color: var(--theme-fg-muted);
    font-size: var(--ecb-type-body);
    text-decoration: underline;
    cursor: pointer;
}

.ecb-btn-link:hover { color: var(--theme-fg); }

.ecb-btn-link:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 2px;
}

/* `.ecb-search-dropdown` is absolutely positioned, so whatever holds it has to
   establish the containing block. Typed inline wherever the dropdown was used,
   which means the one place it is forgotten drops the menu at the page origin. */
.ecb-search-anchor { position: relative; }

/* A button that fills its column — the primary action of a narrow panel. */
.ecb-btn.is-block {
    display: block;
    width: 100%;
}

/* The centred row under a list that loads more of it. */
.ecb-more-row {
    margin-top: var(--ecb-space-5);
    text-align: center;
}

/* `ecb-meta` reads well inline, in the middle of a label or a sentence. Under a
   form field it has to be its own line — which is what the page-local
   `ecb-help-text` rules were doing before EC-3564 folded them in here. A
   modifier rather than a second name, like `ecb-bars.is-compact`. */
.ecb-meta.is-block {
    display: block;
    margin-top: var(--ecb-space-1);
}

/* Columns sitting close together — a set of small fields rather than a set of
   panels, which is what the default 24px gap is for. */
.ecb-grid-3.is-tight,
.ecb-grid-2.is-tight { gap: var(--ecb-space-3); }

/* Four narrow numeric fields across. */
.ecb-grid-4 {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--ecb-space-2);
}

/* --------------------------------------------------------------------------
   Dark — after the base rules
   -------------------------------------------------------------------------- */

[data-theme="dark"] .ecb-btn-link { color: var(--theme-fg-muted); }
[data-theme="dark"] .ecb-btn-link:hover { color: var(--theme-fg); }

/* --------------------------------------------------------------------------
   Form controls

   `.ecb-input` was already written in four templates and defined only as a
   descendant (`.ecb-form-group .ecb-input`, `.sh-pagination-size .ecb-input`),
   so a bare use got nothing and every page typed the border and the radius
   inline — with a literal #E2E8F0 that stayed light in dark mode. The scoped
   rules still win where they exist; this is the floor beneath them.
   -------------------------------------------------------------------------- */

.ecb-input,
.ecb-select,
.ecb-textarea {
    width: 100%;
    padding: var(--ecb-space-2) var(--ecb-space-3);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius-sm);
    background: var(--theme-bg-elevated);
    color: var(--theme-fg);
    font-size: var(--ecb-type-body);
    font-family: inherit;
    line-height: 1.4;
}

.ecb-input:focus,
.ecb-select:focus,
.ecb-textarea:focus {
    outline: 2px solid var(--theme-primary);
    outline-offset: -1px;
    border-color: var(--theme-primary);
}

.ecb-input.is-auto,
.ecb-select.is-auto { width: auto; }

/* A search box does not get more useful past about 400px. */
.ecb-input.is-narrow { max-width: 400px; }

/* A field holding a two- or three-digit limit. */
.ecb-input.is-tiny { width: 80px; }
.ecb-input.is-small { max-width: 120px; }

/* A heading over a group of fields inside a panel. */
.ecb-group-title {
    margin: var(--ecb-space-4) 0 var(--ecb-space-2);
    font-size: var(--ecb-type-body);
    font-weight: 600;
    color: var(--theme-primary);
}

/* A form that should not run the whole width of the page. */
.ecb-form-narrow { max-width: 640px; }
.ecb-form-medium { max-width: 720px; }

/* One setting and its explanation, separated from the next by a hairline. The
   divider was typed inline five times with a literal #E2E8F0, which meant a
   pale line on a dark surface. */
.ecb-setting-row {
    padding: var(--ecb-space-4) 0;
    border-bottom: 1px solid var(--theme-border);
}

.ecb-setting-row:last-child { border-bottom: 0; }

/* The same row with its hairline above — a footer rather than a divider. */
.ecb-setting-row.is-footer {
    border-bottom: 0;
    border-top: 1px solid var(--theme-border);
}

/* Text belonging to the checkbox above it, aligned past the box. */
.ecb-indent { margin-left: 26px; }

/* A field spanning every column of the grid it sits in. */
.ecb-span-all { grid-column: 1 / -1; }

/* Muted without being disabled — a value inherited from somewhere else. */
.ecb-dim { opacity: 0.75; }

/* A field holding a one- or two-digit number, centred because it is read as a
   quantity rather than as text. */
.ecb-input.is-micro {
    width: 60px;
    text-align: center;
}

/* --------------------------------------------------------------------------
   Split layout — controls beside their result
   -------------------------------------------------------------------------- */

.ecb-split {
    display: flex;
    gap: var(--ecb-space-5);
    align-items: flex-start;
}

/* The controls. A fixed band rather than a fraction: a filter panel that grows
   with the window is mostly empty on a wide screen. */
.ecb-split-aside {
    flex: 0 0 auto;
    min-width: 300px;
    max-width: 340px;
}

/* `min-width: 0` is load-bearing — without it a flex child refuses to shrink
   below its content and a wide table pushes the panel off screen. */
.ecb-split-main {
    flex: 1;
    min-width: 0;
}

/* --------------------------------------------------------------------------
   Small pieces
   -------------------------------------------------------------------------- */

/* A row of buttons that sit together. `ecb-header-actions` is the same shape
   pushed to the right; this one stays where it is put. */
.ecb-btn-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ecb-space-2);
}

.ecb-btn-row .ecb-push { margin-left: auto; }

/* A slim progress bar. The same track and fill as `ecb-bars`, at the size a
   background job's progress deserves rather than a measurement's. */
.ecb-bar-track.is-slim {
    height: 8px;
    border-radius: var(--ecb-radius-pill);
}

.ecb-bar-track.is-slim .ecb-bar-fill {
    background: var(--theme-primary);
    border-radius: var(--ecb-radius-pill);
    transition: width 0.3s;
}

/* Preformatted payload — a prompt, a response, a diff. */
.ecb-pre {
    margin: 0;
    padding: var(--ecb-space-3);
    background: var(--theme-bg-subtle);
    border-radius: var(--ecb-radius-sm);
    font-size: var(--ecb-type-micro);
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-x: auto;
    color: var(--theme-fg);
}

/* Content that is not the customer's to see by default — a system prompt. */
.ecb-pre.is-danger {
    background: var(--theme-danger-soft);
    border: 1px solid var(--theme-danger-line);
}

/* A disclosure whose whole job is to keep something long out of the way. */
.ecb-details {
    margin-top: var(--ecb-space-3);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius-sm);
}

.ecb-details > summary {
    padding: var(--ecb-space-3) var(--ecb-space-4);
    cursor: pointer;
    font-size: var(--ecb-type-body);
    color: var(--theme-fg-muted);
}

.ecb-details > summary:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: -2px;
}

.ecb-details-body { padding: 0 var(--ecb-space-4) var(--ecb-space-4); }

/* A checkbox column in a table — as narrow as the box in it. */
.ecb-table th.tick,
.ecb-table td.tick { width: 30px; }

/* A header cell carrying the column the reader is being pointed at. */
.ecb-table th.is-highlight { background: var(--theme-warning-soft); }

/* A header cell for a read-only column beside an editable one. */
.ecb-table th.rowhead {
    text-align: left;
    vertical-align: bottom;
}

.ecb-table th.is-quiet {
    background: var(--theme-bg-subtle);
    color: var(--theme-fg-muted);
}

/* A list that scrolls inside its own box rather than growing the page. */
.ecb-scroll-list {
    max-height: 300px;
    overflow-y: auto;
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius-sm);
    margin-bottom: var(--ecb-space-2);
}

.ecb-pre.is-scroll { max-height: 300px; overflow-y: auto; }
.ecb-pre.is-scroll-tall { max-height: 400px; overflow-y: auto; }

/* A Select2 widget takes a moment to replace its <select>. Without a shim the
   panel is a hole of exactly the widget's height until it does. */
.ecb-select-placeholder {
    display: flex;
    align-items: center;
    height: 32px;
    padding: 0 var(--ecb-space-3);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius-sm);
    background: var(--theme-bg-subtle);
    color: var(--theme-fg-subtle);
    font-size: var(--ecb-type-meta);
}

/* A checkbox with its label on one line. */
.ecb-check {
    display: flex;
    align-items: center;
    gap: var(--ecb-space-1);
    font-size: var(--ecb-type-micro);
    color: var(--theme-fg-muted);
    cursor: pointer;
}

/* Panels that share a row and wrap when they cannot. */
.ecb-columns {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ecb-space-4);
    margin-top: var(--ecb-space-4);
}

.ecb-columns > * { flex: 1; min-width: 250px; }

/* A column in a wrapping row that keeps a usable width before it wraps. */
.ecb-col { flex: 1 1 300px; min-width: 240px; }

/* Emphasis inside otherwise muted text, without the weight of <strong>. */
.ecb-strong { font-weight: 500; color: var(--theme-fg); }

/* The "Saved!" that appears beside a submit button and fades from mind. */
.ecb-saved-note {
    margin-left: var(--ecb-space-3);
    color: var(--theme-success);
    font-size: var(--ecb-type-body);
}

/* A heading inside a disclosure body, above a block of payload. */
.ecb-pre-title {
    margin: 0 0 var(--ecb-space-2);
    font-size: var(--ecb-type-body);
    font-weight: 600;
    color: var(--theme-primary);
}

.ecb-pre-title.is-danger { color: var(--theme-danger); }

/* The one big glyph in an empty state. */
.ecb-empty-icon { font-size: 40px;  /* off-scale: a decorative glyph, sized as an illustration */ margin: 0 0 var(--ecb-space-2); }

/* Spacing helpers for the handful of places a shape has no other name. Kept
   to the rhythm so they cannot drift, and deliberately few. */
.ecb-mt-1 { margin-top: var(--ecb-space-1); }
.ecb-mt-2 { margin-top: var(--ecb-space-2); }
.ecb-mt-3 { margin-top: var(--ecb-space-3); }
.ecb-mt-4 { margin-top: var(--ecb-space-4); }
.ecb-mt-5 { margin-top: var(--ecb-space-5); }
.ecb-ml-2 { margin-left: var(--ecb-space-2); }
.ecb-mb-2 { margin-bottom: var(--ecb-space-2); }
.ecb-mb-3 { margin-bottom: var(--ecb-space-3); }

/* --------------------------------------------------------------------------
   Responsive — after the base rules
   -------------------------------------------------------------------------- */

@media (max-width: 900px) {
    /* Below this the aside and the result each get too little to be usable
       side by side, so they stack and the aside stops being a fixed band. */
    .ecb-split { flex-direction: column; }
    .ecb-split-aside { max-width: none; min-width: 0; width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
    .ecb-bar-track.is-slim .ecb-bar-fill { transition: none; }
}


/* ==========================================================================
   Promoted from hub-local stylesheets — EC-3566

   Every rule below was defined inside one hub's own <style> block while wearing
   the shared `ecb-` prefix. The name said "available everywhere"; the rule was
   available in one hub. That is worse than a private prefix would have been,
   because the name itself is the misleading part — and it is how three classes
   came to be written in a second app where they resolved to nothing (EC-3564).

   Promoting was safe to do wholesale here, which was worth checking first: no
   name was defined differently in two templates, and after EC-3564 no name was
   used outside the app that defined it. So no page gains styling it did not
   have, and none loses any. What changes is that the next page can use these.

   Literal colours became tokens on the way in, so these shapes have a dark mode
   they did not have.
   ========================================================================== */

.ecb-sortable.ecb-sort-busy::after {
    content: '';
    position: absolute;
    top: 60px;
    left: 50%;
    transform: translateX(-50%);
    width: 28px;
    height: 28px;
    border: 3px solid var(--theme-border);
    border-top-color: var(--theme-primary);
    border-radius: 50%;
    animation: ecb-spin 0.8s linear infinite;
    z-index: 5; }  /* EC-3674: in-page affordance inside its own stacking context, not a layer of the page. */
.ecb-sortable.ecb-sort-busy {
    opacity: 0.6;
    pointer-events: none;
    cursor: wait; }
.ecb-sortable-th.ecb-sort-desc::after { content: '▼'; color: var(--theme-primary); }
.ecb-sortable-th.ecb-sort-asc::after { content: '▲'; color: var(--theme-primary); }
.ecb-sortable-th::after {
    content: '↕';
    position: absolute;
    right: 4px;
    color: var(--theme-fg-subtle);
    font-size: var(--ecb-type-eyebrow); }
.ecb-sortable-th:hover { background: var(--theme-bg-subtle); }
/* EC-2648 — sortable table headers. Clickable, with up/down indicator. */
.ecb-sortable-th {
    cursor: pointer;
    user-select: none;
    position: relative;
    padding-right: 18px !important; }
.ecb-progress-fill {
    height: 100%;
    width: 0;
    background: linear-gradient(90deg, var(--theme-primary), var(--theme-primary-hover));
    animation: ecb-progress 12s ease-out infinite;
    border-radius: 3px; }
/* EC-2648 — indeterminate progress bar in the loading shell. The bar
   fills slowly over ~12s (typical engine run on a 1000-product catalog),
   then loops. It's purely visual feedback — server-side progress events
   don't exist for the sync engine call. */
.ecb-progress-track {
    width: 280px;
    height: 6px;
    margin: 0 auto;
    background: var(--theme-bg-subtle);
    border-radius: 3px;
    overflow: hidden; }
.ecb-spinner-large {
    width: 32px; height: 32px;
    border: 3px solid var(--theme-border);
    border-top-color: var(--theme-primary);
    border-radius: 50%;
    animation: ecb-spin 0.8s linear infinite;
    display: inline-block;
    margin-bottom: 12px; }
/* EC-2648 — large spinner used by the async-render shell. The shell
   renders sidebar+tabs synchronously so the user sees this instantly
   while the partial (?_partial=1) computes server-side. */
.ecb-report-loading {
    text-align: center;
    padding: 40px 20px;
    color: var(--theme-fg-muted); }
.ecb-tab-settings:hover { color: var(--theme-primary); }
.ecb-tab-settings { color: var(--theme-fg-subtle); font-weight: 500; }
.ecb-tab-spacer { flex: 1; }
/* Promoted from the procurement hub. That block also redefined `.ecb-tab`
   itself with different padding and colours — rules that never applied, because
   components.css is linked a second time after the page's <style> and the
   shared rule won. They were deleted rather than promoted: making them apply
   would have restyled the tab bar for the first time in its life, which is not
   this ticket's job. The double link is EC-3573. */
.ecb-tab-nav {
    display: flex;
    gap: 4px;
    border-bottom: 2px solid var(--theme-border);
    margin: 4px 0 18px 0;
    flex-wrap: wrap;
    align-items: stretch; }
.ecb-muted { color: var(--theme-fg-muted); }
.ecb-numeric { text-align: right; font-variant-numeric: tabular-nums; }
/* `.ecb-table th` sets text-align:left at (0,1,1) and `.ecb-numeric` is (0,1,0),
   so a numeric column's heading stayed left while its values went right. Every
   `<th class="ecb-numeric">` in the repo was affected, not only the one that
   was noticed -- the procurement hub tables carry five of them. EC-3758. */
.ecb-table th.ecb-numeric { text-align: right; }
.ecb-filter-row .ecb-filter-spacer { flex: 1; }
.ecb-modal-actions { display: flex; gap: var(--ecb-space-2); justify-content: flex-end; }
.ecb-modal-title { margin: 0; font-size: var(--ecb-type-title); color: var(--theme-fg); }
.ecb-modal-backdrop {
    position: fixed; inset: 0; z-index: var(--z-modal-scrim);
    background: var(--theme-scrim);
    display: flex; align-items: center; justify-content: center;
    padding: 24px; }
.ecb-settings-errors {
    color: var(--theme-danger); font-size: var(--ecb-type-micro); margin-top: 2px; }
.ecb-settings-row-input input[type="text"]:focus,
.ecb-settings-row-input input[type="email"]:focus,
.ecb-settings-row-input input[type="number"]:focus,
.ecb-settings-row-input select:focus,
.ecb-settings-row-input textarea:focus {
    border-color: var(--theme-primary);
    outline: none;
    box-shadow: 0 0 0 3px var(--theme-primary-soft); }
.ecb-settings-row-input input[type="text"],
.ecb-settings-row-input input[type="email"],
.ecb-settings-row-input input[type="number"],
.ecb-settings-row-input select,
.ecb-settings-row-input textarea {
    border: 1px solid var(--theme-border-strong);
    border-radius: 6px;
    padding: 6px 10px;
    background: var(--theme-bg-elevated);
    color: var(--theme-fg); }
.ecb-settings-row-input input,
.ecb-settings-row-input select,
.ecb-settings-row-input textarea { font: inherit; }
.ecb-settings-row-label small { display: block; color: var(--theme-fg-muted); font-weight: 400; }
.ecb-settings-row-label { font-weight: 500; padding-top: 4px; color: var(--theme-fg); }
.ecb-settings-group-body {
    padding: 14px 18px;
    display: grid;
    grid-template-columns: 260px 1fr;
    gap: 12px 16px;
    align-items: start; }
.ecb-settings-group-header {
    padding: 10px 16px;
    border-bottom: 1px solid var(--theme-border);
    background: var(--theme-bg);
    font-weight: 600;
    color: var(--theme-primary);
    border-radius: 8px 8px 0 0; }
.ecb-settings-group {
    border: 1px solid var(--theme-border);
    border-radius: 8px;
    margin-bottom: 18px;
    background: var(--theme-bg-elevated);
    box-shadow: var(--theme-shadow-1); }
.ecb-settings-form { max-width: 880px; }

/* Names the animation `.ecb-progress-fill` uses. It stayed behind in the
   procurement hub when the rule moved, which would have left the bar static
   on every other page — an indeterminate progress bar that does not move is
   indistinguishable from a broken one. */
@keyframes ecb-progress {
    0%   { width: 0; }
    50%  { width: 70%; }
    90%  { width: 95%; }
    100% { width: 100%; }
}


/* ==========================================================================
   The modern-view vocabulary — EC-3592

   These names are not `ecb-*`, and renaming them is not what this section is
   for. `.chip`, `.card`, `.field`, `.row` and `.page-title` were already
   written across the modern templates — `.chip` alone appears in 38 of them —
   and every one of those 38 carried its own copy of the rule. Copy number 39
   is what a new page starts from today.

   So the rules move here under the names the markup already uses. A page that
   still declares its own copy keeps winning: `components.css` loads before any
   page `<style>` (EC-3573) and the local rule has the same specificity, so
   promoting one changes nothing until the local copy is deleted. What it does
   change is the pages that were using a name **nobody had defined for them** —
   `procurement/url_detail.html` and its three siblings extend a shell that
   defines `.chip-slate` only, so their amber, rose and emerald chips rendered
   with no tone at all.

   Scoped to `.twp` exactly as every local copy is, so a generic name like
   `.row` or `.card` cannot leak into classic admin.

   The copies are gone (EC-3654): 875 declarations that computed to exactly what
   these rules say, and 363 more that differed and adopted them. What survives
   locally is geometry a page decided for itself — a 24px title, a 26px Select2
   — and each of those carries a comment naming the shared value it overrides.
   `common/tests/test_css_vocabulary.py` fails on a new copy; it compares
   computed values rather than text, because a literal and the token it was
   promoted to are the same declaration written twice.

   The tones are the four semantic tokens (EC-3590), which is why there is no
   dark block here: the local copies each needed four extra rules to exist in
   dark mode, and a token needs none.
   ========================================================================== */

.twp .chip {
    display: inline-flex;
    align-items: center;
    gap: var(--ecb-space-1);
    padding: 1px 7px;
    border-radius: var(--ecb-radius-sm);
    font-size: var(--ecb-type-eyebrow);
    font-weight: 500;
    white-space: nowrap;
}

.twp .chip-rose    { background: var(--theme-danger-soft);  color: var(--theme-danger); }
.twp .chip-amber   { background: var(--theme-warning-soft); color: var(--theme-warning); }
.twp .chip-emerald { background: var(--theme-success-soft); color: var(--theme-success); }
.twp .chip-slate   { background: var(--theme-bg-subtle);    color: var(--theme-fg-muted); }

/* EC-3672: the remaining five, moved here from ten per-template <style> blocks.
   chip-sky and chip-blue were the same colour under two names and both are the
   info tone, so they share one rule — that continues EC-3590, which introduced
   --theme-info precisely because such badges "were typed as literal pale blues
   (#cce5ff, #e3f2fd, #f0f7ff) and had no dark counterpart at all". */
.twp .chip-sky,
.twp .chip-blue    { background: var(--theme-info-soft);    color: var(--theme-info); }
.twp .chip-violet  { background: var(--theme-violet-soft);  color: var(--theme-violet); }

/* The "generate with AI" affordance. It sits beside a field rather than at the
   top of a page, so it is a soft chip and not a filled primary button.

   It was declared in four places and rendered three ways (EC-3692): a filled
   violet button in ai_content/css/ai_content_admin.css, this soft chip in two
   catalog templates, and Tailwind utilities on the product lens overview. The
   filled one won on the product list purely by source order -- that stylesheet
   loads after tailwind.css from `modern_extra_js_head`, and both are (0,1,0).

   Unscoped on purpose: the AI hub is the first-generation vanilla-JS stack and
   is not inside `.twp`, so a `.twp`-scoped rule would not reach it. */
.ai-gen-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--ecb-space-1);
    padding: 3px 8px;
    border-radius: var(--ecb-radius-sm);
    border: 1px solid var(--theme-violet-line);
    background: var(--theme-violet-soft);
    color: var(--theme-violet);
    font-size: var(--ecb-type-eyebrow);
    font-weight: 500;
    line-height: 1.4;
    white-space: nowrap;
    cursor: pointer;
    transition: background .15s;
}

.ai-gen-btn:hover { background: color-mix(in srgb, var(--theme-violet) 12%, transparent); }
.ai-gen-btn:active { background: color-mix(in srgb, var(--theme-violet) 18%, transparent); }
.ai-gen-btn[disabled] { opacity: .5; cursor: not-allowed; }
.twp .chip-orange  { background: var(--theme-orange-soft);  color: var(--theme-orange); }
.twp .chip-indigo  { background: var(--theme-indigo-soft);  color: var(--theme-indigo); }

/* A table row that responds to the pointer. */
.twp .row { transition: background-color .12s ease; }
.twp .row:hover { background: var(--theme-bg-subtle); }

/* A panel in a modern view. Softer corner than `.ecb-panel` and no padding of
   its own — the modern pages set theirs with Tailwind utilities. */
.twp .card {
    background: var(--theme-bg-elevated);
    border: 1px solid var(--theme-border);
    border-radius: 12px;
}

.twp .page-title {
    font-size: 28px;  /* off-scale: one step above --ecb-type-display, for a detail page that leads on a name rather than a number */
    font-weight: 600;
    color: var(--theme-fg);
    line-height: var(--ecb-leading-tight);
}

/* A labelled field in a detail page's inline-edit form. */
.twp .field { display: flex; flex-direction: column; gap: var(--ecb-space-1); }

.twp .field-label {
    font-size: var(--ecb-type-eyebrow);
    font-weight: 500;
    color: var(--theme-fg-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* The padding and radius here are the control's own geometry rather than page
   rhythm, so they keep the values they were designed at — 6/10 sits between two
   steps of the spacing scale, and rounding it to 4/8 shrinks every field on the
   detail pages for no reason anyone asked for. */
.twp .field-input,
.twp .field-textarea {
    width: 100%;
    padding: 6px 10px;
    font-size: var(--ecb-type-body);
    line-height: 1.4;
    background: var(--theme-bg-elevated);
    border: 1px solid var(--theme-border);
    border-radius: 6px;
    color: var(--theme-fg);
    transition: border-color .12s ease, background-color .12s ease;
}

/* EC-3654: the textarea came along. Every one of the four detail pages that
   declared `.field-input` declared it in the same breath — `.twp .field-input,
   .twp .field-textarea { … }`, byte-identical in all four — so promoting one
   without the other would have meant deleting half a rule and leaving the
   textarea unstyled. It is the same control with a second line. */
.twp .field-textarea { min-height: 80px; resize: vertical; font-family: inherit; }

.twp .field-input:focus,
.twp .field-textarea:focus { outline: none; border-color: var(--theme-border-active); }

/* Save state, shown on the field itself rather than in a banner: the tint says
   which field it is about, which a banner cannot. */
.twp .field-input.saving, .twp .field-textarea.saving { background: var(--theme-warning-soft); }
.twp .field-input.saved,  .twp .field-textarea.saved  { background: var(--theme-success-soft); }
.twp .field-input.error,  .twp .field-textarea.error  { background: var(--theme-danger-soft); border-color: var(--theme-danger); }

/* The bar that slides up when rows are selected. It floats over the page, so
   it takes the floating shadow rather than the card one — a little tighter
   than the two-layer shadow each page had typed for itself. */
.twp .bulk-action-bar { box-shadow: var(--theme-shadow-2); }

/* Fixed layout so the `width` on each column header is honoured; without it the
   browser shrinks a number input to about 20px. Reverts to auto once the row
   reflows, where the widths no longer describe anything. */
.twp table.stable-cols { table-layout: fixed; }

/* Pricing-lens column widths (EC-3696).
   Fourteen columns, and `InlineStyleRatchetTests` exists to keep that many
   `style="width:"` attributes out of the template — so they live here and are
   applied through a <colgroup>.

   The table's min-width is load-bearing, not decoration. `stable-cols` sets
   table-layout:fixed, so with `w-full` alone the declared column widths get
   scaled down to fit the container and the one column without a width (Name)
   collapses to zero: headers overlap and every row grows to the height of a
   wrapped title. Letting the table exceed its container is what makes the
   surrounding .overflow-x-auto do its job.

   Compact has five columns and fits, so it declares no minimum. */
.twp table.lens-pricing { min-width: 1600px; }
.twp table.lens-pricing.lens-pricing--no-contribution { min-width: 1390px; }
.twp table.lens-pricing.lens-pricing--compact { min-width: 0; }

.twp .lens-pricing col.pc-open      { width: 40px; }
.twp .lens-pricing col.pc-sku       { width: 140px; }
.twp .lens-pricing col.pc-name      { width: 240px; }
.twp .lens-pricing col.pc-cost      { width: 110px; }
.twp .lens-pricing col.pc-override  { width: 120px; }
.twp .lens-pricing col.pc-money     { width: 110px; }
.twp .lens-pricing col.pc-margin    { width: 100px; }
.twp .lens-pricing col.pc-percent   { width: 90px; }
.twp .lens-pricing col.pc-done      { width: 70px; }

/* Compact groups several fields into one cell, so its columns are wider. */
.twp .lens-pricing col.pc-group-costs  { width: 180px; }
.twp .lens-pricing col.pc-group-prices { width: 170px; }

@media (max-width: 900px) {
    .twp table.stable-cols { table-layout: auto; }
}

/* ── Sticky list header (EC-3871) ──────────────────────────────────────────
   Scrolling a long list scrolled its column headers away, so past the first
   screenful the columns were unlabelled.

   It is not one line of CSS. A sticky element sticks against its nearest
   scrollport, and these tables sit in an `overflow-x-auto` wrapper — next to
   `overflow-x: auto`, an `overflow-y: visible` computes to `auto`, so that
   wrapper IS the scrollport rather than the page. A `position: sticky` on the
   `th` therefore sticks to a box with no vertical scroll of its own and
   scrolls away with the page: it looks right in review and does nothing.

   So the wrapper owns the vertical scroll too. `sticky_list_head.js` sizes it
   to the space left below it and adds `is-sticky-head` only when the content
   is actually taller than that — a three-row list keeps the plain page scroll
   and grows no stray scrollbar.

   The background belongs on the `th`, not the `thead`: a thead's background
   paints behind the row and scrolls out from under a sticky cell, leaving the
   labels sitting on top of the data. Same for the bottom rule, which is an
   inset shadow because a border on a sticky cell is painted at its original
   position in some engines. */
.twp .js-sticky-head.is-sticky-head {
    overflow-y: auto;
}

.twp .js-sticky-head.is-sticky-head thead th {
    position: sticky;
    top: 0;
    z-index: 2;                       /* over the rows; far below the shell's
                                         topbar at 120 and any modal */
    background: var(--theme-bg-subtle);
    box-shadow: inset 0 -1px 0 var(--theme-border);
}

/* A header cell that carries its own checkbox or control must not let a row
   show through it while scrolling. */
.twp .js-sticky-head.is-sticky-head thead tr { background: var(--theme-bg-subtle); }

/* Click-to-edit cells: the reading state is a bordered box that only shows its
   border on hover, so the grid does not look like a form until you aim at it. */
/* Same again: 6px of horizontal padding is the click target in a dense grid
   cell, and 4px loses it. */
.twp .cell-edit-text,
.twp .cell-edit-text-multi {
    padding: 2px 6px;
    border-radius: var(--ecb-radius-sm);
    border: 1px solid transparent;
    cursor: text;
    min-height: 22px;
    line-height: 1.4;
}

.twp .cell-edit-text {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.twp .cell-edit-text-multi {
    display: block;
    white-space: pre-wrap;
    word-break: break-word;
}

.twp .cell-edit-text:hover,
.twp .cell-edit-text-multi:hover {
    border-color: var(--theme-border);
    background: var(--theme-bg-subtle);
}

.twp .cell-edit-input,
.twp .cell-edit-textarea {
    width: 100%;
    min-width: 0;
    box-sizing: border-box;
    padding: 2px 6px;
    font-size: var(--ecb-type-body);
    line-height: 1.4;
    background: var(--theme-bg-elevated);
    border: 1px solid var(--theme-border-active);
    border-radius: var(--ecb-radius-sm);
    color: var(--theme-fg);
    font-family: inherit;
    outline: none;
}

.twp .cell-edit-textarea { min-height: 60px; resize: vertical; }

/* A run of small stats — a number with a label under it, several in a row.
   Written as five inline styles per stat before EC-3592, which is why the
   procurement order list carried the same four declarations eleven times. */
.twp .statline {
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--ecb-space-6);
    row-gap: var(--ecb-space-1);
    align-items: end;
}

.twp .stat-key {
    font-size: 10px;  /* off-scale: sits under a 13px value in a table cell and has to read as its caption */
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--theme-fg-muted);
}

.twp .stat-value {
    font-size: var(--ecb-type-body);
    font-weight: 600;
    line-height: 1.1;
    color: var(--theme-fg);
}

/* Select2 renders its own bundle at its own sizes; these bring one single-select
   into line with the `.field-input` next to it. */
.twp-select2 .select2-container--default .select2-selection--single,
.select2-container--default .select2-selection--single {
    height: 36px;
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius);
}

.select2-container--default .select2-selection--single .select2-selection__rendered {
    line-height: 36px;
    padding-left: var(--ecb-space-3);
    color: var(--theme-fg);
    font-size: var(--ecb-type-body);
}

.select2-container--default .select2-selection--single .select2-selection__arrow { height: 34px; }
.select2-dropdown { border: 1px solid var(--theme-border-strong); }


/* ==========================================================================
   The quick modal — EC-3602

   `openModal()` / `closeModal()` in `static/site/js/common.js` fill a `#modal`
   and a `#modal-overlay` and toggle them with `style.display`. Five templates
   carry that pair, and all five wrote every one of its properties inline —
   including `background: #fff`, which is why the label-print dialog was a
   white card on a dark page.

   `display` stays inline: it is the state the script owns, not appearance.
   Everything else is here.

   Two sizes. The default is the small one the stock-in pages open a label
   dialog into; `.is-wide` is the 600px one `admin/base.html` declares.
   ========================================================================== */

.ecb-quickmodal-overlay {
    position: fixed;
    inset: 0;
    background: var(--theme-scrim);
    z-index: var(--z-modal);
}

.ecb-quickmodal {
    position: fixed;
    top: 15%;
    left: 50%;
    transform: translateX(-50%);
    background: var(--theme-bg-elevated);
    color: var(--theme-fg);
    border-radius: var(--ecb-radius);
    box-shadow: var(--theme-shadow-3);
    padding: var(--ecb-space-3);
    min-width: 220px;
    max-width: 280px;
    z-index: calc(var(--z-modal) + 1);
}

.ecb-quickmodal.is-wide {
    top: 10%;
    padding: var(--ecb-space-5);
    min-width: 600px;
    max-width: 750px;
    max-height: 80vh;
    overflow: hidden;
}

/* The doubled class is deliberate. `modern/_shell_assets.html` carries
   `.twp h1, .twp h2, .twp h3, ... { font: inherit; margin: 0 }` — (0,1,1), and
   it loads *after* this file, so a plain `.ecb-quickmodal > h3` ties on
   specificity and loses on source order. The page-local rule this replaces beat
   the reset with an id selector, which a shared component cannot use. Repeating
   the class takes it to (0,2,1) without inventing a wrapper. */
.ecb-quickmodal.ecb-quickmodal > h3 {
    margin: 0 0 var(--ecb-space-3);
    font-size: var(--ecb-type-lead);
    font-weight: 600;
    color: var(--theme-fg);
}


/* ==========================================================================
   Filter row and sortable table — EC-3596

   Both names are `ecb-*` and both were defined in `procurement/hub/base.html`
   only, which is the defect EC-3564 exists to catch: `crm/hub/threads.html`
   writes `ecb-filter-row` and got no rule at all, and `.ecb-sortable.ecb-sort-busy::after`
   in this file positions an absolute spinner against a `position: relative`
   that lived in the procurement hub's own `<style>` — so on any other page the
   spinner anchored to whatever ancestor happened to be positioned.
   ========================================================================== */

.ecb-filter-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ecb-space-3);
    align-items: center;
    padding: var(--ecb-space-2) var(--ecb-space-3);
    background: var(--theme-bg);
    border: 1px solid var(--theme-border);
    border-radius: var(--ecb-radius);
    margin-bottom: var(--ecb-space-4);
}

.ecb-filter-row label { font-size: var(--ecb-type-micro); color: var(--theme-fg-muted); }

.ecb-filter-row select,
.ecb-filter-row input[type="text"],
.ecb-filter-row input[type="email"],
.ecb-filter-row input[type="number"] {
    border: 1px solid var(--theme-border-strong);
    border-radius: var(--ecb-radius);
}

.ecb-filter-row select:focus,
.ecb-filter-row input[type="text"]:focus,
.ecb-filter-row input[type="email"]:focus,
.ecb-filter-row input[type="number"]:focus {
    border-color: var(--theme-primary);
    outline: none;
    box-shadow: 0 0 0 3px var(--theme-primary-soft);
}

/* Submitting: the row dims and stops taking clicks, and the button label is
   replaced by a spinner so the same click cannot be made twice. */
.ecb-filter-row.is-loading { opacity: 0.6; pointer-events: none; }

.ecb-filter-row .ecb-spinner {
    display: none;
    width: 12px;
    height: 12px;
    border-width: 2px;
    vertical-align: -2px;
    margin-right: var(--ecb-space-1);
}

.ecb-filter-row.is-loading .ecb-spinner { display: inline-block; }

/* The positioned ancestor `.ecb-sortable.ecb-sort-busy::after` above needs. */
.ecb-sortable { position: relative; }


/* --------------------------------------------------------------------------
   A heading with a muted line under it — EC-3596

   Written as `<h2 style="margin-bottom:4px">` followed by
   `<p class="ecb-muted" style="margin-top:0">` on nine Procurement Hub pages.
   The two inline styles exist only to say *these two belong together*, which
   is the thing a class is for.

   Deliberately does not touch type size or measure, unlike `.ecb-pagehead` and
   `.ecb-lede`: these headings keep whatever the page gives them, so adopting
   the pair is a spacing decision and not a redesign.
   -------------------------------------------------------------------------- */

.ecb-intro-title { margin: 0 0 var(--ecb-space-1); }
.ecb-intro-lede  { margin: 0 0 var(--ecb-space-3); }

/* The same slot filled by a paragraph rather than a heading — a status
   line above a report that is still running, for instance. */
.ecb-lead-line {
    margin: 0 0 var(--ecb-space-1);
    font-weight: 600;
    color: var(--theme-fg);
}

/* A small annotation set beside the thing it annotates rather than under it. */
.ecb-note-inline {
    margin-left: var(--ecb-space-2);
    font-size: var(--ecb-type-micro);
    font-weight: normal;
}

/* A block of preformatted text a person reads — an email body, a generated
   pitch — rather than code. Wraps, and takes the page's font. */
.ecb-pre-prose {
    white-space: pre-wrap;
    background: var(--theme-bg);
    border: 1px solid var(--theme-border);
    padding: var(--ecb-space-3);
    border-radius: var(--ecb-radius);
    font-family: inherit;
    font-size: var(--ecb-type-body);
    color: var(--theme-fg);
}

.ecb-inline-form { display: inline; }
.ecb-rule { margin: var(--ecb-space-5) 0; }
.ecb-mt-6 { margin-top: var(--ecb-space-6); }
.ecb-mb-4 { margin-bottom: var(--ecb-space-4); }

/* An annotation on its own line under the field it explains. */
.ecb-note-block {
    margin-top: var(--ecb-space-2);
    font-size: var(--ecb-type-micro);
}

/* A label that is not the field's name — a checkbox caption, an option. */
.ecb-label-plain { font-weight: normal; }

/* A form that must not become a flex item of its own: its children take part
   in the parent's layout instead. Used where a filter row's controls are
   wrapped in a <form> for submission but belong to the row visually. */
.ecb-layout-passthrough { display: contents; }

/* ==========================================================================
   Admin-only control (EC-4023)
   --------------------------------------------------------------------------
   A setting on a customer's own page that only an Ecombooster admin may
   change — switching a product feed on, a supplier's feed interval. The
   customer never sees the control; a superuser does, including while acting
   as the customer (`common.act_as.is_ecombooster_admin`). The frame is what
   makes that visible: without it, a superuser acting as a customer cannot
   tell a control the customer has from one they do not.

   Three shells render these: the Settings Hub, the modern views and the
   classic change form. The classic form marks the widget, not the row —
   `formfield_for_dbfield` is the only hook that reaches both the change form
   and list_editable — so the row is found with `:has()`.
   ========================================================================== */
.ecb-admin-control,
.form-row:has(.ecb-admin-control-input) {
    border: 1px dashed var(--theme-warning);
    background: var(--theme-warning-soft);
    border-radius: var(--ecb-radius);
    padding: var(--ecb-space-3);
}
.ecb-admin-control-input {
    outline: 2px solid var(--theme-warning);
    outline-offset: 1px;
}
.ecb-admin-control-note {
    display: block;
    margin-top: var(--ecb-space-2);
    font-size: var(--ecb-type-micro);
    color: var(--theme-warning);
}
.ecb-admin-control-note strong {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-right: var(--ecb-space-1);
}

/* EC-3352: iOS Safari zooms the page into any form field under 16px when it is
   focused. The admin used to stop that with `user-scalable=no` on the viewport,
   which also took pinch-zoom away from everyone on a warehouse tablet. Zoom is
   allowed again; this is the fix for the focus zoom instead. `-webkit-touch-callout`
   is supported by iOS Safari alone, so Android and desktop keep their sizes. Last
   in the file, so it is not undone by the base rules above it. */
@supports (-webkit-touch-callout: none) {
    input:not([type="checkbox"]):not([type="radio"]),
    select,
    textarea {
        font-size: 16px; /* off-scale: iOS focus-zoom threshold, not type */
    }
}

/* EC-4905: reminder above the markup rule lists while a recalculation was deferred. */
.ecb-pending-recalc {
    margin: 0 0 16px;
    padding: 12px 16px;
    border: 1px solid var(--ecb-warning);
    background: color-mix(in srgb, var(--ecb-warning) 10%, var(--ecb-bg));
    color: var(--ecb-text);
    border-radius: 6px;
    display: flex;
    gap: 16px;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
}
.ecb-pending-recalc__list {
    margin: 6px 0 0;
    padding-left: 18px;
    list-style: disc;
}
.ecb-pending-recalc__btn {
    padding: 8px 14px;
    border-radius: 6px;
    border: 1px solid var(--ecb-warning);
    background: var(--ecb-warning);
    color: var(--ecb-bg);
    font-weight: 600;
    cursor: pointer;
}
