/* ============================================================================
   DESIGN TOKEN SYSTEM (layout-50 Pack 1)
   ----------------------------------------------------------------------------
   The single source of truth for color, spacing, radius, typography, elevation,
   and motion. Tokens are layered so a theme can override the SEMANTIC roles
   without touching component CSS:

     1. :root                    — the DEFAULT (light) theme + scales. Every
                                   value here matches the pre-token look exactly,
                                   so the default render is unchanged (back-compat).
     2. html[data-theme="dark"]  — dark theme: re-points the semantic color roles.
     3. html[data-contrast="high"] — high-contrast OVERRIDE (layout-50 T1B; this
                                   is the a11y slot previously reserved as a
                                   high-contrast *theme* — shipped as an override
                                   so it layers on top of light/dark/operator).
     4. html[data-density="compact"] — re-points the density (spacing) tokens.
     5. html[data-fontscale="sm|lg|xl"] — re-points the root --font-scale
                                   multiplier (per-user text size, T1B).

   The theme/density attributes are set SERVER-SIDE on <html> in App.razor from a
   cookie + the per-user UserPreference store (see ThemeResolver), so the right
   theme is in the first paint with no interactive circuit (the LanguageSwitcher
   precedent). See docs/roadmap/design-tokens.md for the full catalogue.

   Token groups:
     --color-*     semantic color ROLES (surface / text / border / brand / state)
     --space-*     spacing scale (driven by --density-* so compact tightens it)
     --radius-*    corner radii
     --font-*      typography scale (family, sizes, weights, line-height)
     --shadow-*    elevation
     --transition-* / --ease-* motion
   ============================================================================ */
:root {
    /* ── Brand (tenant accent overrides --color-primary at the layout level) ── */
    --color-primary: #2563eb;
    --color-primary-hover: #1d4ed8;
    --color-primary-dark: #1e40af;
    /* The accent as an icon on the header bar and section rail, which are a
       dark slate in EVERY theme: the accent's shade that clears 4.5:1 on the
       slate and its white hover washes (ShellAccent.ChromeInkFor; #2563eb
       read 2.1:1 there). A tenant brand writes its own on .page. The accent
       as text on the page is --color-primary-ink (below the dark block). */
    --color-chrome-accent-ink: #7ba8ff;
    /* Link ink: --color-link / --color-link-hover, read as
       var(--color-link, var(--color-primary-ink)). Deliberately NOT declared here.
       A custom property that refers to another one is resolved where it is
       declared, so `--color-link: var(--color-primary-ink)` on :root would
       freeze this default for the whole page, and the tenant accent MainLayout
       sets on .page would stop reaching links. Unset, the fallback resolves on
       the link itself and follows the accent's text shade. The dark theme
       declares a literal instead (below). */

    /* ── Text roles ── */
    --color-text: #1e293b;
    --color-text-dark: #0f172a;
    /* Secondary text. Much of it sits straight on the canvas, not on a card:
       page-header subtitles, breadcrumbs, wizard steps, the portal hero. The
       old #64748b read 4.24:1 there (4.45:1 on the portal canvas). This is
       the lightest slate that clears 4.5:1 on every light surface, Bootstrap's
       alert washes included: canvas 5.39, card 6.05, lowest .alert-danger
       4.53. Keep in step with MutedTextContrastTests. */
    --color-text-muted: #546478;
    /* Foreground for anything FILLED with --color-primary (primary buttons,
       active tabs, accent chips). Every theme below re-points it to a value
       validated at >= 4.5:1 against that theme's own --color-primary; when a
       tenant accent overrides --color-primary at the layout level, the layout
       emits a COMPUTED --color-on-primary next to it (PortalTheme.OnColorFor),
       so a brand colour can never be left with an illegible label.
       --color-text-on-primary is the older name, kept as an alias. */
    --color-on-primary: #ffffff;
    --color-text-on-primary: var(--color-on-primary);

    /* ── Surface roles ── */
    --color-bg: #f0f2f5;          /* app canvas */
    --color-bg-card: #ffffff;     /* raised surface (cards, menus, modals) */
    --color-bg-subtle: #f8fafc;   /* zebra rows, hover wash, inset panels */
    --color-table-header: #f8fafc;
    /* Behind an image in rendered content (KB Markdown plan, Pack A). Screenshots
       and diagrams are drawn for a WHITE page: a transparent PNG with black lines
       vanishes on a dark surface. Light in every theme, dimmed on the dark ones
       so it does not glare; black line art stays >= 17:1 on all three. */
    --color-media-backdrop: #ffffff;

    /* ── Border roles ── */
    --color-border: #e2e8f0;
    --color-border-input: #d1d5db;
    --color-border-strong: #cbd5e1;

    /* ── State roles (status / priority / severity / feedback) ── */
    --color-success: #16a34a;
    --color-success-bg: #dcfce7;
    --color-warning: #d97706;
    --color-warning-bg: #fef3c7;
    --color-danger: #dc2626;
    --color-danger-bg: #fee2e2;
    /* A stronger wash for marking a part INSIDE a -bg wash (the KB version
       diff's changed words). Always paired with --color-text, which reads
       >= 4.5:1 on it in every theme; measured against its own -bg row it is
       >= 1.48:1 in every theme, so the mark is visible as well as legible.
       light: text 8.40 / 7.71, vs row 1.59 / 1.55. */
    --color-success-bg-strong: #4ade80;
    --color-danger-bg-strong: #fca5a5;
    --color-info: #2563eb;
    --color-info-bg: #dbeafe;

    /* ── Spacing scale (semantic steps map onto the density tokens below) ── */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    /* Density-driven steps — compact theme tightens these. Components that want
       to react to density use --density-gap / --density-pad-y / --density-cell. */
    --density-gap: var(--space-3);
    --density-pad-y: 0.5rem;
    --density-cell: 0.75rem 1rem;   /* table cell padding */

    /* ── Radius scale ── */
    --radius-sm: 0.375rem;
    --radius-md: 0.5rem;
    --radius-lg: 0.75rem;
    --radius-pill: 999px;

    /* ── Typography scale ── */
    --font-family-base: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    /* Per-user text-size multiplier (T1B). Applied ONCE, on the root font-size
       below — every rem-based size (the whole --font-size-* scale) follows.
       Re-pointed by html[data-fontscale="…"]; default 1 keeps today's look. */
    --font-scale: 1;
    --font-size-xs: 0.75rem;
    --font-size-sm: 0.875rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.5rem;
    --font-size-2xl: 1.75rem;
    --font-weight-normal: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --font-weight-bold: 700;
    --line-height-base: 1.5;

    /* ── Elevation ── */
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04);

    /* ── Motion ── */
    --transition-fast: 0.15s ease;
    --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
}

/* ── Dark theme — re-points the semantic color roles only. Brand (--color-primary)
   stays tenant-driven; we lighten link/hover affordances at the component level.
   Spacing/radius/type are theme-agnostic, so they're inherited from :root.

   The first selector is the DARK-PRESET PORTAL ISLAND: a self-service portal
   whose tenant chose the Dark preset, read by someone on a light theme (the
   default, or the high-contrast theme value). PortalLayout.razor.css paints
   the portal's own chrome dark for that tenant whatever the reader's theme,
   but ~300 rules on the portal pages (pills, muted text, info and warning
   ink, card and wash fills) and the shared topbar menus are written in these
   app tokens. Under a light reader they stayed LIGHT on the dark portal:
   text 1.16:1, muted 3.58:1, danger 3.53:1 on the portal surface (1.06 /
   3.26 / 3.21 on its subtle wash). Re-declared on the shell, every one of
   them takes the values a dark-theme reader already gets on the same page,
   so a dark-preset tenant looks the same whatever the reader's theme
   (owner's call, 2026-10-14). Dark readers have these values from <html>
   already; operator readers keep operator's own, which are dark too.
   Bootstrap's own components follow data-bs-theme="dark", which
   PortalLayout stamps on the same shell; the few html-keyed dark bridges for
   non-Bootstrap pieces list the island beside them (the rich-text editor,
   the outline buttons). The html test sits in :where() so the high-contrast
   arm further down can override the island at equal specificity. ── */
:where(html:not([data-theme="dark"]):not([data-theme="operator"])) .portal-shell[data-portal-preset="dark"],
html[data-theme="dark"] {
    --color-text: #e2e8f0;
    --color-text-dark: #f1f5f9;
    --color-text-muted: #94a3b8;
    /* Dark keeps the tenant/default accent, so white stays the right label
       colour (#2563eb + white = 5.15:1). A tenant accent that flips the answer
       is handled by the layout-emitted computed value. */
    --color-on-primary: #ffffff;
    /* Links get their own ink here. The accent is a mid blue chosen for a
       white page: on this theme it measures 2.83:1 on the card and 1.76:1 on
       the warning wash. Buttons still FILL with the accent, so --color-primary
       stays. Blue-300 clears 4.5:1 on every surface a link meets under this
       theme: canvas, card, subtle wash, table header, the four alert washes
       and the portal's dark palette. The lowest is the
       warning wash at 5.03:1; the hover goes lighter, lowest 6.38:1. A tenant
       accent no longer colours links on this theme, for the same reason.
       High contrast moves neither the link nor these surfaces, so it keeps
       these values: AA everywhere, and 9.90 / 8.11:1 on the canvas and card,
       where nearly every link sits. The dark-preset portal island (first
       selector above) carries them too. Keep in step with
       LinkColourContrastTests. */
    --color-link: #93c5fd;
    --color-link-hover: #bfdbfe;

    --color-bg: #0f172a;
    --color-bg-card: #1e293b;
    --color-bg-subtle: #273449;
    --color-table-header: #273449;
    --color-media-backdrop: #e2e8f0;

    --color-border: #334155;
    --color-border-input: #475569;
    --color-border-strong: #64748b;

    --color-success: #22c55e;
    --color-success-bg: #14532d;
    --color-warning: #f59e0b;
    --color-warning-bg: #78350f;
    --color-danger: #f87171;
    --color-danger-bg: #7f1d1d;
    /* dark: text 4.99 / 5.25, vs row 1.48 / 1.55 (see :root). */
    --color-success-bg-strong: #17703a;
    --color-danger-bg-strong: #b91c1c;
    --color-info: #60a5fa;
    --color-info-bg: #1e3a8a;

    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.55), 0 2px 4px -1px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -2px rgba(0, 0, 0, 0.45);

    /* Keep Bootstrap's own component theming in step (form controls, dropdowns). */
    color-scheme: dark;
}

/* ── The accent as TEXT: --color-primary-ink ──────────────────────────────────
   Every rule that paints the accent as letters or an icon glyph (sort headers,
   active items, badges, pills, links) reads --color-primary-ink; fills,
   borders and focus rings keep --color-primary. The raw accent failed as text
   in every theme: dark never re-declares it, so #2563eb read 2.4:1 on the
   subtle wash, and the light blue and operator magenta fell to 3.7 and 3.9:1
   on their own 16% pill washes. A tenant brand fared worse (amber 2.1:1 on
   the light card, navy 1.2:1 on the dark one).
   The ink is the accent where it clears 4.5:1 on the theme's canvas, card,
   subtle wash and table header, and under 16% washes of the accent and of the
   ink over each; otherwise the same hue with its oklab lightness walked until
   it does (ShellAccent.InkFor, the portal's --portal-accent-ink rule; owner's
   call 2026-10-15, built-in accents included). Fallbacks below are each
   theme's own accent so treated: light #2563eb, dark #2563eb, operator
   #ff2d9b. Keep in step with ShellAccentInkTests.
   Each rule matches :root AND .page. MainLayout writes a tenant brand's shades
   on .page, tagged per theme, and a var() resolves where it is declared: on
   :root alone it would never see them. Each theme reads only its own tag, so
   the READER's theme picks, System included. High contrast moves no surface
   and keeps these. The dark-preset portal island (see the dark block) takes
   the dark one. */
:root,
.page {
    --color-primary-ink: var(--color-primary-ink-light, #1652da);
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"])) .portal-shell[data-portal-preset="dark"],
html[data-theme="dark"],
html[data-theme="dark"] .page {
    --color-primary-ink: var(--color-primary-ink-dark, #92b7ff);
}
html[data-theme="operator"],
html[data-theme="operator"] .page {
    --color-primary-ink: var(--color-primary-ink-operator, #ff5aa6);
}
/* Hover: the ink a quarter of the way to the text ink, so darker on light and
   lighter on dark, and only ever MORE legible (light #1643aa, operator
   #ff87bb). Declared beside every ink above so it mixes that element's ink. */
:root,
.page,
.portal-shell[data-portal-preset="dark"] {
    --color-primary-ink-hover: color-mix(in oklab, var(--color-primary-ink) 75%, var(--color-text-dark));
}

/* ── Compact density — tightens the density-driven spacing tokens. Comfortable
   (default) is the :root values, so omitting the attribute keeps today's look. ── */
html[data-density="compact"] {
    --density-gap: var(--space-2);
    --density-pad-y: 0.3rem;
    --density-cell: 0.4rem 0.6rem;
}

/* ── Font scale (T1B) — per-user text size. Re-points ONLY the root multiplier;
   the html rule below applies it to the root font-size, so every rem-based
   size in the app scales together. Medium (default) emits no attribute. ── */
html[data-fontscale="sm"] { --font-scale: 0.9; }
html[data-fontscale="lg"] { --font-scale: 1.125; }
html[data-fontscale="xl"] { --font-scale: 1.25; }

html, body {
    font-family: var(--font-family-base);
    font-size: var(--font-size-base);
    line-height: var(--line-height-base);
    background-color: var(--color-bg);
    color: var(--color-text);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
/* The multiplier is applied ONCE, on the ROOT font-size only. On <html>, rem
   still means the browser's initial size (16px), so this is 16px × scale; the
   body rule above stays 1rem and simply follows the scaled root — no double
   scaling for anything that uses the rem-based --font-size-* tokens. */
html {
    font-size: calc(var(--font-size-base) * var(--font-scale));
}

/* Rendered Markdown has no link rule of its own on screen, so this is the
   colour of every link in a KB article too. Links are accent text: without a
   theme's own --color-link they take --color-primary-ink (owner's call,
   2026-10-15), so a tenant brand reaches them only in a shade that reads. */
a, .btn-link {
    color: var(--color-link, var(--color-primary-ink));
}

a:hover, .btn-link:hover {
    color: var(--color-link-hover, var(--color-primary-ink-hover));
}

.btn-primary {
    /* Bootstrap 5.3 resolves .btn's colour from --bs-btn-*-color, and its
       `.btn:hover` rule (0,2,0) outranks a plain `.btn-primary { color }`
       (0,1,0) — so re-point the variables, or the label snaps back to white
       on hover/active/disabled and loses the contrast fix. */
    --bs-btn-color: var(--color-on-primary, #fff);
    --bs-btn-hover-color: var(--color-on-primary, #fff);
    --bs-btn-active-color: var(--color-on-primary, #fff);
    --bs-btn-disabled-color: var(--color-on-primary, #fff);
    color: var(--color-on-primary, #fff);
    background-color: var(--color-primary);
    border-color: var(--color-primary-hover);
    border-radius: var(--radius-md);
    font-weight: 500;
    transition: all var(--transition-fast);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
    border-color: var(--color-primary-dark);
    transform: translateY(-1px);
    box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.25);
}

.btn-secondary:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 6px -1px rgba(108, 117, 125, 0.2);
}

.btn-outline-primary:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2);
}

.btn-outline-danger:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 6px -1px rgba(220, 53, 69, 0.2);
}

.btn-danger:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 6px -1px rgba(220, 53, 69, 0.25);
}

/* ── Outline buttons, tabs and pagination ─────────────────────────────────────
   Bootstrap paints these from its own literals, which no theme reaches: its
   link blue #0d6efd (.btn-outline-primary, .nav-link, .page-link) and one hue
   per outline variant. Each theme failed somewhere. #0d6efd is 4.01:1 on the
   light canvas, 3.25:1 on the dark card and 4.02:1 on the operator card; the
   grey, green and red outlines are 4.04-4.18:1 on the light canvas and
   3.12-3.23:1 on the dark card; outline-warning and outline-info are 1.45 and
   1.75:1 on the light canvas; outline-dark is 1.05:1 on the dark card.
   Only the label and border at rest move. A hover or active state FILLS the
   button and sets its own label (white on #dc3545 is 4.53:1, black on #ffc107
   12.88:1), so it reads on any surface and keeps Bootstrap's values, except
   primary's, which follows the accent as .btn-primary does.
   Keep in step with BootstrapComponentContrastTests. */

/* Primary, tabs and pagination read the link ink, as a plain link does
   (--color-link, falling back to the accent's text shade, --color-primary-ink;
   the hover fills keep the raw accent). Declared on the component, not
   on :root, so var() resolves under .page and a tenant accent still reaches
   them. An alert re-points --color-link for its own wash (see the alert
   bridge), so these follow it there too. */
.btn-outline-primary {
    --bs-btn-color: var(--color-link, var(--color-primary-ink));
    --bs-btn-border-color: var(--color-link, var(--color-primary-ink));
    --bs-btn-hover-color: var(--color-on-primary, #fff);
    --bs-btn-hover-bg: var(--color-primary);
    --bs-btn-hover-border-color: var(--color-primary);
    --bs-btn-active-color: var(--color-on-primary, #fff);
    --bs-btn-active-bg: var(--color-primary);
    --bs-btn-active-border-color: var(--color-primary);
    --bs-btn-disabled-color: var(--color-link, var(--color-primary-ink));
    --bs-btn-disabled-border-color: var(--color-link, var(--color-primary-ink));
}
.nav {
    --bs-nav-link-color: var(--color-link, var(--color-primary-ink));
    --bs-nav-link-hover-color: var(--color-link-hover, var(--color-primary-ink-hover));
}
.nav-pills {
    --bs-nav-pills-link-active-color: var(--color-on-primary, #fff);
    --bs-nav-pills-link-active-bg: var(--color-primary);
}
.pagination {
    --bs-pagination-color: var(--color-link, var(--color-primary-ink));
    --bs-pagination-hover-color: var(--color-link-hover, var(--color-primary-ink-hover));
    --bs-pagination-focus-color: var(--color-link-hover, var(--color-primary-ink-hover));
    --bs-pagination-active-color: var(--color-on-primary, #fff);
    --bs-pagination-active-bg: var(--color-primary);
    --bs-pagination-active-border-color: var(--color-primary);
}

/* The neutral outline: body ink on a muted border, every theme. Bootstrap's
   grey #6c757d failed both the light canvas and the dark card; --color-text
   clears 10:1 on every theme's surfaces. */
.btn-outline-secondary {
    --bs-btn-color: var(--color-text);
    --bs-btn-border-color: var(--color-text-muted);
    --bs-btn-disabled-color: var(--color-text);
    --bs-btn-disabled-border-color: var(--color-text-muted);
}

/* Light: Bootstrap's state hues are fill colours, too light to be text on a
   pale surface. The theme's own state tokens are fills as well
   (--color-danger #dc2626 is 4.31:1 on the canvas, --color-success 2.94:1),
   so each variant takes a darker shade of Bootstrap's hue: the 700 shade
   where it clears the canvas, else the 800. Lowest, all on the canvas:
   red 5.77, green 6.36, amber 6.32, cyan 4.78:1. :where() keeps the
   theme test out of the specificity, so the alert rule below still wins.
   outline-dark already reads 13.75:1 here. */
:where(html:not([data-theme="dark"]):not([data-theme="operator"])) .btn-outline-danger {
    --bs-btn-color: #b91c1c;
    --bs-btn-border-color: #b91c1c;
    --bs-btn-disabled-color: #b91c1c;
    --bs-btn-disabled-border-color: #b91c1c;
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"])) .btn-outline-success {
    --bs-btn-color: #166534;
    --bs-btn-border-color: #166534;
    --bs-btn-disabled-color: #166534;
    --bs-btn-disabled-border-color: #166534;
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"])) .btn-outline-warning {
    --bs-btn-color: #92400e;
    --bs-btn-border-color: #92400e;
    --bs-btn-disabled-color: #92400e;
    --bs-btn-disabled-border-color: #92400e;
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"])) .btn-outline-info {
    --bs-btn-color: #0e7490;
    --bs-btn-border-color: #0e7490;
    --bs-btn-disabled-color: #0e7490;
    --bs-btn-disabled-border-color: #0e7490;
}

/* Dark and operator: the theme's own state hues are light enough to be text
   on its surfaces (lowest: dark --color-danger 4.54:1 on the subtle wash).
   outline-dark's near-black label takes the body ink. outline-warning and
   outline-info keep Bootstrap's hues, which already clear 6.4:1 here. */
:where(html:is([data-theme="dark"], [data-theme="operator"])) .btn-outline-danger {
    --bs-btn-color: var(--color-danger);
    --bs-btn-border-color: var(--color-danger);
    --bs-btn-disabled-color: var(--color-danger);
    --bs-btn-disabled-border-color: var(--color-danger);
}
:where(html:is([data-theme="dark"], [data-theme="operator"])) .btn-outline-success {
    --bs-btn-color: var(--color-success);
    --bs-btn-border-color: var(--color-success);
    --bs-btn-disabled-color: var(--color-success);
    --bs-btn-disabled-border-color: var(--color-success);
}
:where(html:is([data-theme="dark"], [data-theme="operator"])) .btn-outline-dark {
    --bs-btn-color: var(--color-text);
    --bs-btn-border-color: var(--color-text);
    --bs-btn-disabled-color: var(--color-text);
    --bs-btn-disabled-border-color: var(--color-text);
}

/* The dark-preset portal island (see the dark theme block) is a dark surface
   under a LIGHT reader, so the light arm's shades above would be dark text on
   it (#b91c1c is 2.6:1 on the portal surface). It gets what a dark reader
   gets: the dark arm's ink, and Bootstrap's own hues for warning and info.
   Same (0,1,0) as both arms, and later. */
:where(html:not([data-theme="dark"]):not([data-theme="operator"]) .portal-shell[data-portal-preset="dark"]) .btn-outline-danger {
    --bs-btn-color: var(--color-danger);
    --bs-btn-border-color: var(--color-danger);
    --bs-btn-disabled-color: var(--color-danger);
    --bs-btn-disabled-border-color: var(--color-danger);
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"]) .portal-shell[data-portal-preset="dark"]) .btn-outline-success {
    --bs-btn-color: var(--color-success);
    --bs-btn-border-color: var(--color-success);
    --bs-btn-disabled-color: var(--color-success);
    --bs-btn-disabled-border-color: var(--color-success);
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"]) .portal-shell[data-portal-preset="dark"]) .btn-outline-warning {
    --bs-btn-color: #ffc107;
    --bs-btn-border-color: #ffc107;
    --bs-btn-disabled-color: #ffc107;
    --bs-btn-disabled-border-color: #ffc107;
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"]) .portal-shell[data-portal-preset="dark"]) .btn-outline-info {
    --bs-btn-color: #0dcaf0;
    --bs-btn-border-color: #0dcaf0;
    --bs-btn-disabled-color: #0dcaf0;
    --bs-btn-disabled-border-color: #0dcaf0;
}
:where(html:not([data-theme="dark"]):not([data-theme="operator"]) .portal-shell[data-portal-preset="dark"]) .btn-outline-dark {
    --bs-btn-color: var(--color-text);
    --bs-btn-border-color: var(--color-text);
    --bs-btn-disabled-color: var(--color-text);
    --bs-btn-disabled-border-color: var(--color-text);
}

/* Inside an alert an outline button sits on a tinted wash, where a state hue
   fails on its own colour (dark --color-danger on the danger wash is 3.62:1,
   --color-success on the success wash 4.00:1). The alert's own ink clears
   its wash in every theme, so the label takes it: in a custom property,
   currentColor is resolved where the button uses it, which for `color` is
   the colour the button inherits from the alert. Primary is left out; it
   reads the alert's --color-link. */
.alert .btn:is(.btn-outline-secondary, .btn-outline-success, .btn-outline-danger,
               .btn-outline-warning, .btn-outline-info, .btn-outline-dark, .btn-outline-light) {
    --bs-btn-color: currentColor;
    --bs-btn-border-color: currentColor;
    --bs-btn-disabled-color: currentColor;
    --bs-btn-disabled-border-color: currentColor;
}

.btn {
    border-radius: var(--radius-md);
    font-weight: 500;
    padding: 0.5rem 1rem;
    transition: all var(--transition-fast);
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
    box-shadow: 0 0 0 0.15rem rgba(37, 99, 235, 0.25);
}

.form-control, .form-select {
    border-radius: var(--radius-md);
    border-color: var(--color-border-input);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.form-control:hover, .form-select:hover {
    border-color: #b0b8c4;
}

.form-control:focus, .form-select:focus {
    border-color: var(--color-primary);
}

.card {
    border: none;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    transition: box-shadow var(--transition-fast), transform var(--transition-fast);
}

.card:hover {
    box-shadow: var(--shadow-md);
}

.table {
    border-radius: var(--radius-md);
    overflow: hidden;
}

.table thead th {
    background-color: var(--color-table-header);
    border-bottom: 2px solid var(--color-border);
    font-weight: 600;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
}

.table tbody tr {
    transition: background-color 0.1s ease;
}

.table tbody tr:hover {
    background-color: var(--color-table-header);
}

/* Density exemplar — table cell padding follows the --density-cell token, so
   switching to compact tightens every .table on the page with no per-table CSS.
   Default value equals Bootstrap's 0.75rem 1rem (look unchanged). */
.table > :not(caption) > * > * {
    padding: var(--density-cell);
}

/* ── Admin table row actions ──
   Admin tables end in an ACTIONS column. Its buttons used to be loose inline
   siblings, so widths were content-driven and a row with one more button than
   its neighbour wrapped onto a second, ragged line. Put the buttons in a single
   .btn-group and mark the cell with this class: the block never wraps and every
   row's actions occupy the same footprint. Conditional actions are rendered
   DISABLED rather than omitted, which keeps the group's width — and each
   button's column — identical on every row. Give the <th> an explicit
   style="width:…" so the column is reserved before the table lays out. */
.admin-row-actions {
    white-space: nowrap;
}

.admin-row-actions .btn-group {
    display: inline-flex;
    flex-wrap: nowrap;
}

.content {
    padding-top: 1.5rem;
}

h1 {
    font-weight: 700;
    color: var(--color-text-dark);
    font-size: 1.75rem;
}

h1:focus {
    outline: none;
}

h2 {
    font-weight: 600;
    color: var(--color-text);
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #22c55e;
}

.invalid {
    outline: 1px solid #ef4444;
}

.validation-message {
    color: var(--color-danger, #ef4444);
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

/* ============================================================================
   #blazor-error-ui — the framework's circuit-failure banner.
   ----------------------------------------------------------------------------
   MUST be global. Every layout (MainLayout / PortalLayout / VendorLayout)
   renders this div, but the rules used to live only in MainLayout.razor.css:
   scoped CSS compiles to `#blazor-error-ui[b-t081nq90dh]`, an attribute only
   MainLayout's own markup carries — so on /portal and /vendor the banner never
   got `display: none` and rendered as an ordinary "Something went wrong." line
   in the page footer of a perfectly healthy session.

   Hidden until blazor.web.js sets display:block on it; then a fixed,
   high-contrast recovery bar pinned to the bottom of the viewport. Its own
   colours are hard-coded rather than tokenized on purpose: when this shows,
   the circuit is down, and the banner must stay legible under every theme
   (color-scheme: light only keeps the UA from re-tinting it in dark mode).
   ============================================================================ */
#blazor-error-ui {
    color-scheme: light only;
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    box-sizing: border-box;
    z-index: 1100;
    padding: 0.75rem 1.25rem;
    background: #fff4c2;
    color: #1f2430;              /* 13.4:1 on #fff4c2 */
    border-top: 2px solid #8a6d00;
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.25);
    border-radius: 0;
    font-size: 0.9375rem;
    line-height: 1.4;
}

#blazor-error-ui .reload {
    color: #7a3d00;              /* 6.3:1 on #fff4c2 */
    font-weight: 600;
    text-decoration: underline;
    margin-inline-start: 0.35rem;
}

#blazor-error-ui .reload:focus-visible,
#blazor-error-ui .dismiss:focus-visible {
    outline: 3px solid #1f2430;
    outline-offset: 2px;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    inset-inline-end: 0.75rem;
    top: 0.5rem;
    color: #1f2430;
    font-size: 1rem;
    line-height: 1;
    padding: 0.25rem;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
    border-radius: 0.5rem;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* Badge styles */
.badge {
    font-weight: 500;
    border-radius: var(--radius-sm);
    padding: 0.35em 0.65em;
    transition: all var(--transition-fast);
}

/* Page header pattern */
.page-header {
    margin-bottom: 1.5rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--color-border);
}

.page-header h1 {
    margin-bottom: 0.25rem;
}

.page-header p {
    color: var(--color-text-muted);
    margin-bottom: 0;
}

@media (prefers-reduced-motion: reduce) {
    .btn:hover,
    .card:hover,
    .dashboard-card:hover {
        transform: none !important;
    }
}

@media (prefers-reduced-motion: no-preference) {
    html {
        scroll-behavior: smooth;
    }
}

/* Rich-text body — sanitized HTML coming out of Quill on tickets and
   ticket comments. white-space: pre-wrap means legacy plain-text rows
   keep their newlines without needing migration. img tags from the
   attachment endpoint get a sensible max-width so a 4K screenshot
   doesn't blow out the ticket card. */
.ticket-rich-body {
    white-space: pre-wrap;
}
.ticket-rich-body p,
.ticket-rich-body ul,
.ticket-rich-body ol,
.ticket-rich-body blockquote,
.ticket-rich-body pre {
    margin: 0 0 0.6em 0;
}
.ticket-rich-body img {
    max-width: 100%;
    height: auto;
    border-radius: 6px;
    margin: 6px 0;
}

/* Quill toolbar tweaks — slimmer borders, less padding, so it fits
   inside our card layouts without dominating. */
.rich-text-editor-host {
    background: white;
    border: 1px solid var(--color-border);
    border-top: 0;
    border-radius: 0 0 4px 4px;
    padding: 6px 10px;
}
.ql-toolbar.ql-snow {
    border-color: var(--color-border);
    border-radius: 4px 4px 0 0;
    background: #f8fafc;
}

/* ============================================================================
   DARK-THEME BRIDGE for Bootstrap surfaces (layout-50 Pack 1)
   ----------------------------------------------------------------------------
   Bootstrap components hard-code light backgrounds/borders, so the semantic
   token re-point in html[data-theme="dark"] doesn't reach them on its own. This
   block re-skins the surfaces the SHELL relies on (card, table, form controls,
   dropdown, modal, list-group) using the SAME tokens, proving the cascade
   end-to-end. NOT a full page sweep — that's a later breadth pack; deeper
   bespoke page CSS may still show light spots under dark mode until then.
   ============================================================================ */
html[data-theme="dark"] .card {
    background-color: var(--color-bg-card);
    color: var(--color-text);
}
html[data-theme="dark"] .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--color-text);
    /* Bootstrap's .table-striped bakes its OWN --bs-table-striped-color
       (defaults to a near-black light-theme value) instead of deriving it
       from --bs-table-color, so striped (odd) rows were unreadable — nearly
       invisible dark text on the dark canvas — while non-striped rows were
       fine. Found on the live Users page. */
    --bs-table-striped-color: var(--color-text);
    --bs-table-striped-bg: var(--color-bg-subtle);
    color: var(--color-text);
}
html[data-theme="dark"] .table thead th {
    background-color: var(--color-table-header);
    border-bottom-color: var(--color-border);
}
html[data-theme="dark"] .table tbody tr:hover {
    background-color: var(--color-bg-subtle);
}
/* .table-light declares its own Bootstrap literals (#f8f9fa, black ink), so on
   this theme a .table-light row was a light stripe in a dark table: the
   inactive rows on the access pages, the excluded subscriptions on M365
   licences, the purchase-order fill-down row. Its links only read 4.90:1
   because they were the accent; the theme's --color-link is 1.71:1 on it.
   The stripe is what sets those rows apart (a cell paints its own colour, so
   their .text-muted never reaches the text), so it stays a stripe, on the
   theme's subtle wash as the .bg-light bridge below does: text 10.18:1, link
   6.96:1. Everything else is `inherit`, the table's own values from the
   bridge above, as on an ordinary row; a thead.table-light keeps the header
   fill from the rule above. Operator had the same light stripe, its magenta
   link 3.27:1 on it; on its subtle wash the text reads 13.96:1 and the link
   4.72:1 (hover 5.60:1). The class sits on a <tr> or a <thead>, never on the
   <table>, so the operator .table bridge further down does not compete. */
html:is([data-theme="dark"], [data-theme="operator"]) .table-light {
    --bs-table-color: inherit;
    --bs-table-bg: var(--color-bg-subtle);
    --bs-table-border-color: inherit;
    --bs-table-striped-color: inherit;
    --bs-table-striped-bg: inherit;
    --bs-table-active-color: inherit;
    --bs-table-active-bg: inherit;
    --bs-table-hover-color: inherit;
    --bs-table-hover-bg: inherit;
}
html[data-theme="dark"] .form-control,
html[data-theme="dark"] .form-select {
    background-color: var(--color-bg-card);
    border-color: var(--color-border-input);
    color: var(--color-text);
}
html[data-theme="dark"] .form-control::placeholder {
    color: var(--color-text-muted);
}
/* Input-group prefixes and suffixes (a phone flag, an icon, a unit) kept
   Bootstrap's light #f8f9fa while their text colour followed the theme, so the
   content sat at 1.17:1 here and 1.10:1 on operator - invisible. The recessed
   zebra-row surface with muted content gives 4.89:1 here, 6.39:1 on operator. */
html[data-theme="dark"] .input-group-text {
    background-color: var(--color-bg-subtle);
    border-color: var(--color-border-input);
    color: var(--color-text-muted);
}
html[data-theme="dark"] .dropdown-menu,
html[data-theme="dark"] .list-group-item,
html[data-theme="dark"] .modal-content,
html[data-theme="dark"] .offcanvas {
    background-color: var(--color-bg-card);
    border-color: var(--color-border);
    color: var(--color-text);
}
html[data-theme="dark"] .text-muted {
    color: var(--color-text-muted) !important;
}
/* The editor is not a Bootstrap component, so data-bs-theme does not reach it:
   the dark-preset portal island (see the dark theme block) needs the same
   bridge, or a portal ticket reply is the portal's near-white ink on white. */
html[data-theme="dark"] .rich-text-editor-host,
html[data-theme="dark"] .ql-toolbar.ql-snow,
html:not([data-theme="dark"]):not([data-theme="operator"]) .portal-shell[data-portal-preset="dark"] :is(.rich-text-editor-host, .ql-toolbar.ql-snow) {
    background: var(--color-bg-card);
    border-color: var(--color-border);
}

/* ============================================================================
   SHARED FOUNDATION COMPONENTS (layout-50 Pack 1) — token-based, theme-aware
   PageHeader / EmptyState / Skeleton / Toast. Kept here (global, not scoped)
   so they read the cascading theme tokens directly.
   ============================================================================ */

/* ── PageHeader ── */
.tyit-page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-5);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-border);
}
.tyit-page-header__titles { min-width: 0; }
.tyit-page-header__title {
    margin: 0 0 var(--space-1) 0;
    font-size: var(--font-size-2xl);
    font-weight: var(--font-weight-bold);
    color: var(--color-text-dark);
}
.tyit-page-header__icon {
    margin-inline-end: var(--space-2);
    color: var(--color-text-muted);
}
.tyit-page-header__subtitle {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}
.tyit-page-header__actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
    flex-wrap: wrap;
}
/* Narrow screens: the title block and the action buttons cannot share a row
   without clipping — stack them, keep the actions on their own wrappable row.
   Without this the header was the page-wide horizontal-overflow source at
   phone widths (390px): titles have min-width:0 but actions are flex-shrink:0. */
@media (max-width: 640.98px) {
    .tyit-page-header {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-3);
    }
    .tyit-page-header__actions {
        justify-content: flex-start;
    }
    .tyit-page-header__actions > * {
        max-width: 100%;
    }
}

/* ── EmptyState ── */
.tyit-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: var(--space-6) var(--space-4);
    color: var(--color-text-muted);
}
.tyit-empty__icon {
    font-size: 2.5rem;
    color: var(--color-border-strong);
    margin-bottom: var(--space-3);
    line-height: 1;
}
.tyit-empty__message {
    font-size: var(--font-size-base);
    color: var(--color-text);
    margin-bottom: var(--space-4);
    max-width: 32rem;
}
.tyit-empty__actions { display: flex; gap: var(--space-2); }

/* ── Skeleton ── */
.tyit-skeleton {
    display: block;
    background: linear-gradient(
        90deg,
        var(--color-bg-subtle) 25%,
        var(--color-border) 37%,
        var(--color-bg-subtle) 63%);
    background-size: 400% 100%;
    border-radius: var(--radius-sm);
    animation: tyit-skeleton-shimmer 1.4s ease infinite;
}
@keyframes tyit-skeleton-shimmer {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}
@media (prefers-reduced-motion: reduce) {
    .tyit-skeleton { animation: none; }
}

/* ── Shared motion vocabulary (Claude Design handoff — Operator Console) ──
   A small set of reusable entrance/feedback animations, ported from the
   original design mock's own keyframes (deckFadeUp/deckSpin/deckPop/
   liveBlink) and renamed to the app's own `.app-*` utility convention
   (matches .app-table/.app-num). Theme-agnostic — motion is a general
   polish layer, not something exclusive to the operator accent. */
@keyframes app-fade-up {
    from { opacity: 0; transform: translateY(11px); }
    to   { opacity: 1; transform: none; }
}
/* 🔴 fill-mode is `backwards`, NOT `both`, and that is load-bearing.
   MainLayout wraps every ops page in `<article class="content px-4 app-fade-up">`,
   so this rule's retained state is inherited by the whole application. With
   `both`, the forwards fill keeps the final keyframe applied forever — and
   although that keyframe says `transform: none`, an element under an active
   animation computes it as the IDENTITY MATRIX, not as `none`. A non-none
   transform makes the element a containing block for `position: fixed`
   descendants, so every fixed element in the app resolved its offsets against
   that article instead of the viewport. Measured on the workflow builder: its
   settings drawer (`top:0; right:0; bottom:0`) rendered 274px tall at y=94 —
   the article's box — with 944px of content scrolling inside it, instead of
   filling the 1000px viewport.
   `backwards` still fills BEFORE the animation (guarding the first-frame
   flash, which is what the fill was for) but releases afterwards, so the
   transform computes back to `none`. The final keyframe is identical to the
   element's natural state (opacity 1, no transform), so dropping the forwards
   fill changes nothing visually.
   Reduced-motion users were never affected — the block below disables the
   animation outright, so the transform never existed for them. */
.app-fade-up { animation: app-fade-up 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards; }

@keyframes app-spin { to { transform: rotate(360deg); } }
.app-spin {
    display: inline-block;
    width: 15px; height: 15px;
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, 0.4);
    border-top-color: #fff;
    animation: app-spin 0.6s linear infinite;
}

@keyframes app-pop {
    0%   { transform: scale(0.3); }
    60%  { transform: scale(1.18); }
    100% { transform: scale(1); }
}
.app-pop { display: inline-flex; align-items: center; gap: 7px; animation: app-pop 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; }

@keyframes app-live-blink {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.2; }
}
.app-live-dot {
    display: inline-block;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--color-success);
    box-shadow: 0 0 9px var(--color-success);
    animation: app-live-blink 2s infinite;
}

@media (prefers-reduced-motion: reduce) {
    .app-fade-up, .app-spin, .app-pop, .app-live-dot { animation: none !important; }
}

/* ── Toast host + toasts ── */
.tyit-toast-host {
    position: fixed;
    top: var(--space-4);
    right: var(--space-4);
    z-index: 1080;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    max-width: min(360px, calc(100vw - 2 * var(--space-4)));
    pointer-events: none;
}
/* Plain `html[dir="rtl"]` — app.css is a GLOBAL stylesheet, so the Blazor
   scoped-CSS `:global()` wrapper is not valid here and the parser dropped the
   whole rule (RTL toasts stayed pinned to the right). */
html[dir="rtl"] .tyit-toast-host { right: auto; left: var(--space-4); }
.tyit-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-3);
    background: var(--color-bg-card);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-left: 4px solid var(--color-info);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    font-size: var(--font-size-sm);
    animation: app-toast-in 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes app-toast-in {
    from { opacity: 0; transform: translateY(14px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .tyit-toast { animation: none; }
}
.tyit-toast--success { border-left-color: var(--color-success); }
.tyit-toast--warning { border-left-color: var(--color-warning); }
.tyit-toast--danger  { border-left-color: var(--color-danger); }
.tyit-toast--info    { border-left-color: var(--color-info); }
.tyit-toast__icon { flex-shrink: 0; font-size: 1.1rem; line-height: 1.3; }
.tyit-toast--success .tyit-toast__icon { color: var(--color-success); }
.tyit-toast--warning .tyit-toast__icon { color: var(--color-warning); }
.tyit-toast--danger  .tyit-toast__icon { color: var(--color-danger); }
.tyit-toast--info    .tyit-toast__icon { color: var(--color-info); }
.tyit-toast__body { flex: 1; min-width: 0; word-wrap: break-word; }
.tyit-toast__close {
    flex-shrink: 0;
    background: none;
    border: none;
    color: var(--color-text-muted);
    cursor: pointer;
    padding: 0 var(--space-1);
    font-size: 1rem;
    line-height: 1.3;
}
.tyit-toast__close:hover { color: var(--color-text); }

/* ============================================================================
   Shared presentation utilities — used directly by pages AND by the shared
   components (Pill / Avatar / MetricCard in Components/Shared). Theme-agnostic
   (token-driven), so they adapt to light / dark / operator.
   ============================================================================ */
.app-num {
    font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    font-feature-settings: "tnum";
}
.app-table thead th {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
    border-bottom: 1px solid var(--color-border);
    padding: 0.5rem 0.65rem;
    white-space: nowrap;
}
.app-table tbody td {
    padding: 0.55rem 0.65rem;
    vertical-align: middle;
    border-color: var(--color-border);
}

/* ============================================================================
   html[data-theme="operator"] — "Operator Console" theme.
   ----------------------------------------------------------------------------
   Deep-navy dark surface + signature magenta accent, from the Operator Console
   design mock. Mirrors the dark theme above: re-points the semantic colour
   ROLES + elevation only — spacing / radius / type are inherited from :root, so
   the whole shell (rail stripe + icons, primary buttons, links, active tabs)
   follows --color-primary automatically.

   To keep per-tenant brand colour instead of the magenta, delete the three
   --color-primary* lines below; the theme then inherits the tenant accent like
   the dark theme does.
   ============================================================================ */
html[data-theme="operator"] {
    /* Brand / accent — signature magenta (mock --accent #ff00aa, nudged up for
       legibility on the deep navy). Delete to keep tenant branding. */
    --color-primary: #ff2d9b;
    --color-primary-hover: #ff57b0;
    --color-primary-dark: #d11a7e;

    --color-text: #e9eef9;
    --color-text-dark: #f4f7fd;
    --color-text-muted: #97a3ba;
    /* The signature magenta is a LIGHT fill: white on it is only 3.44:1, which
       failed WCAG AA for the normal-size labels on primary buttons and active
       tabs. The accent itself is the theme's identity, so the FOREGROUND moves
       instead — the theme's own deep navy reads 5.50:1 on #ff2d9b (and 6.8:1
       on the #ff57b0 hover fill). */
    --color-on-primary: #0b1020;
    /* The magenta as an icon on the header bar and rail (see :root). */
    --color-chrome-accent-ink: #ff7cb3;

    --color-bg: #070b14;
    --color-bg-card: #0e1626;
    --color-bg-subtle: #152037;
    --color-table-header: #111a2e;
    --color-media-backdrop: #e9eef9;
    /* Aliases some chrome reads directly (NavMenu section bar). */
    --color-card: #0e1626;
    --color-subtle: #152037;

    /* Translucent hairlines (mock --border / --border-2) so the ambient glow
       reads through panel edges — the glassy Operator look, not solid boxes. */
    --color-border: rgba(255, 255, 255, 0.08);
    --color-border-input: rgba(255, 255, 255, 0.16);
    --color-border-strong: rgba(255, 255, 255, 0.22);

    --color-success: #2bd4a8;
    --color-success-bg: #0d3b30;
    --color-warning: #f6b73c;
    --color-warning-bg: #4a3610;
    --color-danger: #ff5a74;
    --color-danger-bg: #4d1626;
    /* operator: text 5.66 / 7.68, vs row 1.90 / 1.61 (see :root). */
    --color-success-bg-strong: #12695a;
    --color-danger-bg-strong: #8c1d3a;
    --color-info: #3fc6f5;
    --color-info-bg: #0c3346;

    /* Elevation — deeper than the dark theme to match the mock's float. */
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.55), 0 1px 2px rgba(0, 0, 0, 0.45);
    --shadow-md: 0 8px 20px -8px rgba(0, 0, 0, 0.70), 0 2px 6px -2px rgba(0, 0, 0, 0.50);
    --shadow-lg: 0 22px 48px -26px rgba(0, 0, 0, 0.80), 0 8px 20px -12px rgba(0, 0, 0, 0.60);

    color-scheme: dark;
}

/* Bootstrap surface bridge — mirrors the dark bridge above; Bootstrap hard-codes
   light surfaces, so the token re-point doesn't reach them on its own. */
html[data-theme="operator"] .card {
    background-color: var(--color-bg-card);
    border-color: var(--color-border);
    color: var(--color-text);
}
html[data-theme="operator"] .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--color-text);
    /* Same fix as the dark theme: Bootstrap's .table-striped bakes its own
       --bs-table-striped-color (a near-black default) instead of deriving it
       from --bs-table-color, so striped rows were unreadable. */
    --bs-table-striped-color: var(--color-text);
    --bs-table-striped-bg: var(--color-bg-subtle);
    color: var(--color-text);
}
html[data-theme="operator"] .table thead th {
    background-color: var(--color-table-header);
    border-bottom-color: var(--color-border);
}
html[data-theme="operator"] .table tbody tr:hover {
    background-color: var(--color-bg-subtle);
}
html[data-theme="operator"] .form-control,
html[data-theme="operator"] .form-select {
    background-color: var(--color-bg-card);
    border-color: var(--color-border-input);
    color: var(--color-text);
}
html[data-theme="operator"] .form-control::placeholder {
    color: var(--color-text-muted);
}
html[data-theme="operator"] .input-group-text {
    background-color: var(--color-bg-subtle);
    border-color: var(--color-border-input);
    color: var(--color-text-muted);
}
html[data-theme="operator"] .dropdown-menu,
html[data-theme="operator"] .list-group-item,
html[data-theme="operator"] .modal-content,
html[data-theme="operator"] .offcanvas {
    background-color: var(--color-bg-card);
    border-color: var(--color-border);
    color: var(--color-text);
}
html[data-theme="operator"] .text-muted {
    color: var(--color-text-muted) !important;
}
html[data-theme="operator"] .rich-text-editor-host,
html[data-theme="operator"] .ql-toolbar.ql-snow {
    background: var(--color-bg-card);
    border-color: var(--color-border);
}

/* Bootstrap utility bridge — these classes hard-code LIGHT colours that the
   token re-point can't reach, so they read as "light spots" under the theme
   (filter-count badges, card headers, soft "-subtle" fills, "-emphasis" text,
   alerts, the close "×"). Re-skin them so the whole app — not just the shell —
   goes properly dark. Shared by BOTH dark themes: user feedback ("dark theme
   isn't fully dark") confirmed the dark theme had the exact same gap, so the
   bridge is scoped to :is(dark, operator) — same specificity as before. */
html:is([data-theme="dark"], [data-theme="operator"]) .bg-light { background-color: var(--color-bg-subtle) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .text-dark { color: var(--color-text) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .card-header {
    background-color: var(--color-table-header);
    border-bottom-color: var(--color-border);
    color: var(--color-text);
}
html:is([data-theme="dark"], [data-theme="operator"]) .btn-close { filter: invert(1) grayscale(1) brightness(1.7); }

/* The accent wash stays within the 16% --color-primary-ink is measured on
   (it was 22%), and the emphasis text on it is that ink. */
html:is([data-theme="dark"], [data-theme="operator"]) .bg-primary-subtle { background-color: color-mix(in oklab, var(--color-primary) 16%, transparent) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .text-primary-emphasis { color: var(--color-primary-ink) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .bg-success-subtle { background-color: var(--color-success-bg) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .text-success-emphasis { color: var(--color-success) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .bg-warning-subtle { background-color: var(--color-warning-bg) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .text-warning-emphasis { color: var(--color-warning) !important; }

/* FILLED warning badge. The `-subtle` bridges above move the BACKGROUND onto
   the token palette; `.text-dark` is bridged separately (below) to the theme's
   body text. A `badge bg-warning text-dark` — the lifecycle badge on all three
   workflow surfaces — got both treatments and ended up as the theme's
   near-WHITE text on Bootstrap's un-bridged #ffc107: 1.32:1 on dark, 1.40:1 on
   operator. Moving the FILL onto the token instead of letting the LABEL follow
   the body text gives 8.48:1 and 11.04:1. Scoped to `.badge.` so non-badge
   warning surfaces keep their own treatment, and off the light theme, which
   already reads 9.46:1. */
html:is([data-theme="dark"], [data-theme="operator"]) .badge.bg-warning {
    background-color: var(--color-warning) !important;
    color: var(--color-bg) !important;
}
html:is([data-theme="dark"], [data-theme="operator"]) .bg-danger-subtle { background-color: var(--color-danger-bg) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .text-danger-emphasis { color: var(--color-danger) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .bg-info-subtle { background-color: var(--color-info-bg) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .text-info-emphasis { color: var(--color-info) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .bg-secondary-subtle { background-color: var(--color-bg-subtle) !important; }
html:is([data-theme="dark"], [data-theme="operator"]) .text-secondary-emphasis { color: var(--color-text-muted) !important; }

html:is([data-theme="dark"], [data-theme="operator"]) .alert-info { background-color: var(--color-info-bg); border-color: color-mix(in oklab, var(--color-info) 40%, transparent); color: var(--color-text); }
html:is([data-theme="dark"], [data-theme="operator"]) .alert-warning { background-color: var(--color-warning-bg); border-color: color-mix(in oklab, var(--color-warning) 40%, transparent); color: var(--color-text); }
html:is([data-theme="dark"], [data-theme="operator"]) .alert-danger { background-color: var(--color-danger-bg); border-color: color-mix(in oklab, var(--color-danger) 40%, transparent); color: var(--color-text); }
html:is([data-theme="dark"], [data-theme="operator"]) .alert-success { background-color: var(--color-success-bg); border-color: color-mix(in oklab, var(--color-success) 40%, transparent); color: var(--color-text); }
/* .alert-primary was the one coloured variant left out, so it stayed
   Bootstrap's #cfe2ff here, and the dark theme's link read 1.37:1 on it.
   It is a tint of the accent, mixed onto the card rather than over
   transparent (the .bg-primary-subtle bridge) so the surface is opaque and the
   ratios below are fixed: text 9.71 / 11.87:1, link 6.64 / 6.45:1 (dark /
   operator). */
html:is([data-theme="dark"], [data-theme="operator"]) .alert-primary { background-color: color-mix(in oklab, var(--color-primary) 22%, var(--color-bg-card)); border-color: color-mix(in oklab, var(--color-primary) 40%, transparent); color: var(--color-text); }
/* Neutral alert flavours (the parent-ticket and merged-away banners) — the
   same light-spot class as the semantic alerts above. .alert-dark stayed
   Bootstrap's #ced4da (the dark link 1.21:1); it joins them. */
html:is([data-theme="dark"], [data-theme="operator"]) .alert-light,
html:is([data-theme="dark"], [data-theme="operator"]) .alert-secondary,
html:is([data-theme="dark"], [data-theme="operator"]) .alert-dark { background-color: var(--color-bg-subtle); border-color: var(--color-border); color: var(--color-text); }

/* Link ink inside an alert, per theme. Plain links, .btn-link and
   .btn-outline-primary all read --color-link, so re-pointing it on .alert
   moves all three. Declared on .alert, which sits under .page, so a var()
   here still sees a tenant accent.
   - Light: Bootstrap's own washes (#cfe2ff, #d1e7dd, #f8d7da, ...) took the
     accent to 3.46-4.43:1. The accent's hover shade (--color-primary-ink a
     quarter of the way to the text ink, #1643aa) reads 5.81:1 on the darkest
     (.alert-dark #ced4da), and for any tenant brand at least 5.6:1 on every
     wash; the hover goes to the text ink (11.95:1 or more). It used to be
     --color-primary-dark, which a tenant accent sets to itself: a raw brand
     on these washes. A dark-preset portal under the light theme is the
     exception: its shell is data-bs-theme="dark", so its alerts take
     Bootstrap's DARK washes, and the island rule below keeps its own link.
   - Dark: --color-link already clears every bridged wash (lowest 5.03:1, the
     warning wash), so it needs nothing here.
   - Operator: the magenta accent read 3.34-4.18:1 on the four state washes
     (hover 3.97:1 on the warning wash). A lighter tint of it, inside alerts
     only: 5.38:1 on the warning wash, the lowest; hover 6.25:1. */
html:not([data-theme="dark"]):not([data-theme="operator"]) .alert {
    --color-link: var(--color-primary-ink-hover);
    --color-link-hover: var(--color-text-dark);
}
/* ...except inside the dark-preset portal island (see the dark theme block):
   PortalLayout stamps data-bs-theme="dark" on that shell, so its alerts take
   Bootstrap's DARK washes, where the deep accent above is dark-on-dark. They
   keep the island's link ink, which reads on every one of those washes, as a
   dark reader's do. (0,5,1) outranks the rule above. */
html:not([data-theme="dark"]):not([data-theme="operator"]) .portal-shell[data-portal-preset="dark"] .alert {
    --color-link: inherit;
    --color-link-hover: inherit;
}
html[data-theme="operator"] .alert {
    --color-link: #ff8cc6;
    --color-link-hover: #ffa3d1;
}
/* .alert-link. Bootstrap colours it from --bs-alert-link-color, which every
   .alert-<variant> points at its LIGHT --bs-*-text-emphasis ink (#58151c on
   danger). The bridges above move the surface but not that ink, so the links
   read 1.06-1.99:1: the product names in the M365 "license deficit" banner
   were invisible. Every variant is bridged now, so re-point the variable once
   on .alert. The ink is --color-text-dark, not the variant's hue token: the
   hue tokens fail on their own washes (--color-danger on --color-danger-bg is
   3.62:1 on dark). Lowest: the dark warning wash, 8.28:1. The light theme
   keeps Bootstrap's palette, where the emphasis ink already reads 5.47:1 or
   more. */
html:is([data-theme="dark"], [data-theme="operator"]) .alert { --bs-alert-link-color: var(--color-text-dark); }
/* The global `a:hover` (0,1,1) outranks Bootstrap's .alert-link (0,1,0) and
   swapped the link to the hover ink on hover. Keep the alert's own ink, as
   Bootstrap does. */
.alert-link:hover { color: var(--bs-alert-link-color); }

/* Helper/secondary-text bridge — the admin shell never sets data-bs-theme, so
   Bootstrap's :root LIGHT literals leak through every class that resolves from
   these variables: .form-text (1.1:1 on an operator card, 1.04:1 on dark —
   the "unreadable helper text" bug), .text-body-secondary, .dropdown-item
   (dark-on-dark in bridged menus), the .form-floating placeholder rule and
   SVG labels. Re-point the VARIABLES per theme rather than fighting the
   classes — .text-body-secondary carries !important, and this is the exact
   mechanism of the --bs-table-striped-color fix (#306). */
html:is([data-theme="dark"], [data-theme="operator"]) {
    --bs-body-color: var(--color-text);
    --bs-secondary-color: var(--color-text-muted);
    --bs-tertiary-color: var(--color-text-muted);
}
/* .text-secondary resolves from --bs-secondary-rgb, which also feeds
   .bg-secondary fills — re-pointing the triplet would wash those out, so
   bridge the text class directly instead (the .text-muted pattern above). */
html:is([data-theme="dark"], [data-theme="operator"]) .text-secondary {
    color: var(--color-text-muted) !important;
}
/* Disabled/readonly inputs: the surface bridge above (0,2,1) shadows
   Bootstrap's .form-control:disabled background (0,2,0), leaving disabled
   fields visually identical to editable ones — restore the affordance at
   higher specificity, on tokens so high-contrast strengthens it too. */
html:is([data-theme="dark"], [data-theme="operator"]) .form-control:disabled,
html:is([data-theme="dark"], [data-theme="operator"]) .form-control[readonly],
html:is([data-theme="dark"], [data-theme="operator"]) .form-select:disabled {
    background-color: var(--color-bg-subtle);
    color: var(--color-text-muted);
}
/* The same shadowing killed hover/focus feedback on list-group options
   (Typeahead dropdowns, pickers) — Bootstrap's :hover (0,2,0) loses to the
   surface bridge (0,2,1). Restore it. */
html:is([data-theme="dark"], [data-theme="operator"]) .list-group-item-action:hover,
html:is([data-theme="dark"], [data-theme="operator"]) .list-group-item-action:focus {
    background-color: var(--color-bg-subtle);
    color: var(--color-text);
}
/* Tabs and pagination paint their cells from --bs-body-bg and
   --bs-secondary-bg, which no theme re-points (see the helper-text bridge
   above), so the active tab and every page cell were white blocks on both
   dark themes. Their link blue happened to pass on that white (4.50:1); the
   theme's link ink would read 1.80:1 on it. Move the cells onto the theme's
   surfaces instead: the active tab is the card it opens onto, a page cell is
   a card that hovers to the subtle wash, as a table row does, and a disabled
   cell is the subtle wash. The link ink comes from the .nav / .pagination
   rules near the top of this file. */
html:is([data-theme="dark"], [data-theme="operator"]) .nav-tabs {
    --bs-nav-tabs-border-color: var(--color-border);
    --bs-nav-tabs-link-hover-border-color: var(--color-bg-subtle) var(--color-bg-subtle) var(--color-border);
    --bs-nav-tabs-link-active-color: var(--color-text-dark);
    --bs-nav-tabs-link-active-bg: var(--color-bg-card);
    --bs-nav-tabs-link-active-border-color: var(--color-border) var(--color-border) var(--color-bg-card);
}
html:is([data-theme="dark"], [data-theme="operator"]) .pagination {
    --bs-pagination-bg: var(--color-bg-card);
    --bs-pagination-border-color: var(--color-border);
    --bs-pagination-hover-bg: var(--color-bg-subtle);
    --bs-pagination-hover-border-color: var(--color-border);
    --bs-pagination-focus-bg: var(--color-bg-subtle);
    --bs-pagination-disabled-bg: var(--color-bg-subtle);
    --bs-pagination-disabled-border-color: var(--color-border);
}

/* ── Signature atmosphere — what gives the Operator Console its FEEL (the part a
   plain token re-point can't): a deep layered backdrop (navy wash + masked
   blueprint grid + two drifting glow orbs), magenta glow on the accent, and the
   deepened rail. All theme-scoped + global — no per-page edits. ── */

/* The canvas colour + base wash live on <html> so the fixed atmospheric layers
   (body's pseudo-elements, z-index:-1) sit BEHIND content while still painting.
   The content area <main> is normally an opaque --color-bg; making it (and body)
   transparent under this theme lets the backdrop show through the gaps between
   panels, exactly as the mock does. */
html[data-theme="operator"] {
    background-color: #070b14;
    background-image: radial-gradient(125% 90% at 12% -12%, #0d1730, transparent 58%);
    background-attachment: fixed;
}
html[data-theme="operator"] body,
html[data-theme="operator"] main {
    background-color: transparent;
}

/* Two independently-drifting, blurred glow blobs — real elements (see
   MainLayout.razor's .app-aurora wrapper), not a single combined
   pseudo-element, so each can drift on its own path/timing like the design
   mock's own multi-blob backdrop (and PortalLayout's .portal-aurora-blob-*).
   Invisible in light/dark — only operator gives them a background. */
.app-aurora {
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    overflow: hidden;
}
html[data-theme="operator"] .app-aurora-blob {
    position: absolute;
    border-radius: 50%;
    filter: blur(48px);
    will-change: transform;
}
html[data-theme="operator"] .app-aurora-blob-a {
    width: 46vw; height: 46vw;
    left: -10vw; top: -16vh;
    background: radial-gradient(closest-side, rgba(255, 45, 155, 0.28), transparent 72%);
    animation: operator-drift 19s ease-in-out infinite;
}
html[data-theme="operator"] .app-aurora-blob-b {
    width: 40vw; height: 40vw;
    right: -12vw; bottom: -18vh;
    background: radial-gradient(closest-side, rgba(63, 198, 245, 0.2), transparent 72%);
    animation: operator-drift-b 24s ease-in-out infinite;
}
@keyframes operator-drift-b {
    0%, 100% { transform: translate(0, 0); }
    50% { transform: translate(-3%, -2.5%); }
}
@media (prefers-reduced-motion: reduce) {
    html[data-theme="operator"] .app-aurora-blob { animation: none; }
}
/* Faint blueprint grid, faded out toward the bottom with a radial mask. */
html[data-theme="operator"] body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image:
        linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
    background-size: 48px 48px;
    -webkit-mask-image: radial-gradient(120% 100% at 50% 0%, #000 55%, transparent 100%);
    mask-image: radial-gradient(120% 100% at 50% 0%, #000 55%, transparent 100%);
}
@keyframes operator-drift {
    0%, 100% { transform: translate(0, 0); }
    50% { transform: translate(3%, 2.5%); }
}
/* Magenta glow on the accent — primary buttons + the rail active-stripe. */
html[data-theme="operator"] .btn-primary {
    box-shadow: 0 0 0 1px rgba(255, 45, 155, 0.45), 0 14px 40px -14px rgba(255, 45, 155, 0.6);
}
html[data-theme="operator"] .btn-primary:hover {
    box-shadow: 0 0 0 1px rgba(255, 87, 176, 0.55), 0 16px 44px -12px rgba(255, 45, 155, 0.7);
}
html[data-theme="operator"] .section-tab.active::before {
    box-shadow: 0 0 13px var(--color-primary);
}

/* Deepen the TopSectionNav rail to the mock's near-black navy. */
html[data-theme="operator"] .section-nav {
    background: linear-gradient(180deg, #0a1322 0%, #0b1120 100%);
}

/* JetBrains-style tabular numerals for KPI / metric figures (system mono + tnum;
   self-host JetBrains Mono + add .mono to numbers for the exact match). */
html[data-theme="operator"] .mono,
html[data-theme="operator"] .metric-value {
    font-family: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
    font-feature-settings: "tnum" 1;
}

/* ============================================================================
   html[data-contrast="high"] — HIGH-CONTRAST OVERRIDE (layout-50 T1B).
   ----------------------------------------------------------------------------
   The a11y slot the token header originally reserved as a high-contrast THEME,
   shipped as an override instead: it layers on top of whichever theme is
   active, so users don't have to trade dark mode / Operator for readability.
   Strengthens the text + border roles and makes focus unmissable; placed AFTER
   the theme blocks and keyed per theme family so each variant stays on the
   right side of its own surfaces.
   ============================================================================ */

/* Over the default LIGHT theme (no data-theme attribute). */
html[data-contrast="high"]:not([data-theme="dark"]):not([data-theme="operator"]) {
    --color-text: #0f172a;
    --color-text-dark: #020617;
    --color-text-muted: #334155;

    --color-border: #94a3b8;
    --color-border-input: #64748b;
    --color-border-strong: #475569;

    /* Helper text follows the strengthened muted role (the dark/operator
       bridge already routes these through --color-text-muted; light needs
       its own re-point because that bridge doesn't apply here). */
    --bs-body-color: var(--color-text);
    --bs-secondary-color: var(--color-text-muted);
    --bs-tertiary-color: var(--color-text-muted);
}

/* Over the DARK and OPERATOR themes — and over the dark-preset portal island
   (see the dark theme block) for a light reader who asked for high contrast,
   by the override or by the high-contrast theme value: the light arms hand
   that dark island dark ink. Both island selectors keep the html test in
   :where(), so they tie at (0,2,0) and this later one wins. */
:where(html[data-contrast="high"]:not([data-theme="dark"]):not([data-theme="operator"]), html[data-theme="high-contrast"]) .portal-shell[data-portal-preset="dark"],
html:is([data-theme="dark"], [data-theme="operator"])[data-contrast="high"] {
    --color-text: #f8fafc;
    --color-text-dark: #ffffff;
    --color-text-muted: #cbd5e1;

    --color-border: #64748b;
    --color-border-input: #94a3b8;
    --color-border-strong: #cbd5e1;
}

/* Unmissable keyboard focus, all themes. */
html[data-contrast="high"] :focus-visible {
    outline: 3px solid var(--color-primary);
    outline-offset: 2px;
}

/* ── P4 commit D — finish UiTheme.HighContrast ────────────────────────────────
   `data-theme="high-contrast"` has been a REACHABLE value since layout-50:
   ThemeSettings.ParseTheme accepts it and IsValidThemeToken passes it, so
   /api/preferences/theme?theme=high-contrast persisted happily — but no
   stylesheet anywhere matched the attribute. A user who got there (a stored
   value, a hand-typed URL, a bookmarked link) was stranded on the light palette
   with a dead attribute and no menu entry to escape through.

   The fix does NOT reverse the T1B decision that high contrast belongs on its
   own axis (UiContrast.High, above), which is right: it layers over any theme,
   so nobody trades dark mode for readability, and that is why this value stays
   out of AppearancePage.OfferedThemes. It simply makes the value RESOLVE —
   light palette plus exactly the light-arm high-contrast overrides — so the
   reachable state renders something legible. The values are copied from the
   light arm at the top of this block on purpose; they are one short list and
   sharing them via a third selector would put high-contrast into the
   `:not(...)` chain that keeps that arm off dark and operator. ── */
html[data-theme="high-contrast"] {
    --color-text: #0f172a;
    --color-text-dark: #020617;
    --color-text-muted: #334155;

    --color-border: #94a3b8;
    --color-border-input: #64748b;
    --color-border-strong: #475569;

    --bs-body-color: var(--color-text);
    --bs-secondary-color: var(--color-text-muted);
    --bs-tertiary-color: var(--color-text-muted);

    color-scheme: light;
}

html[data-theme="high-contrast"] :focus-visible {
    outline: 3px solid var(--color-primary);
    outline-offset: 2px;
}

/* ── P4 commit D — skip-link ──────────────────────────────────────────────────
   First tab stop on every page, off-screen until focused, jumping past the
   topbar and the whole left rail to <main id="main-content">. The portal rail
   is ~20 links; without this a keyboard or switch user traversed all of them on
   every single navigation. Not `display:none` — a hidden element is not
   focusable, so the link would not exist for the users it is for.

   It is hidden by CLIPPING, not by `left: -9999px`. The off-screen-left idiom
   is direction-hostile: on an RTL document (Arabic and Hebrew both set
   dir="rtl" in App.razor) the containing block's overflow runs the other way,
   so a -9999px offset creates ~9999px of horizontal scroll on every page rather
   than hiding anything. `inset-inline-start: 0` + `clip-path: inset(50%)` is
   the direction-safe equivalent: the element keeps its place in the flow, is
   focusable, and paints nothing until focus resets the clip.
   (Adversarial review P13.) ── */
.skip-link {
    position: absolute;
    inset-inline-start: 0;
    top: 0;
    z-index: 2000;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    padding: 10px 18px;
    border-radius: 0 0 var(--radius-md, 8px) 0;
    background: var(--color-primary, #2563eb);
    color: var(--color-text-on-primary, #ffffff);
    font-weight: 600;
    text-decoration: none;
}

.skip-link:focus,
.skip-link:focus-visible {
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;
    outline: 3px solid var(--color-text-dark, #020617);
    outline-offset: 2px;
}

/* ============================================================================
   SHARED FOUNDATION COMPONENTS (layout-50 T1B) — token-based, theme-aware
   Breadcrumb / StandardListToolbar / StandardFormLayout. Same contract as the
   Pack 1 block above (PageHeader/EmptyState/Skeleton/Toast): global so they
   read the cascading theme tokens directly.
   ============================================================================ */

/* ── Breadcrumb ── */
.tyit-breadcrumb { margin-bottom: var(--space-2); }
.tyit-breadcrumb__list {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--font-size-sm);
}
.tyit-breadcrumb__item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--color-text-muted);
}
.tyit-breadcrumb__item a {
    color: var(--color-text-muted);
    text-decoration: none;
}
.tyit-breadcrumb__item a:hover {
    color: var(--color-link, var(--color-primary-ink));
    text-decoration: underline;
}
.tyit-breadcrumb__sep {
    color: var(--color-border-strong);
    font-size: var(--font-size-xs);
    line-height: 1;
}
.tyit-breadcrumb__current { color: var(--color-text); font-weight: var(--font-weight-medium); }

/* ============================================================================
   RESPONSIVE LIST-PAGE TOOLBAR (.app-toolbar)
   ----------------------------------------------------------------------------
   The shared replacement for the hand-rolled `<div class="d-flex gap-2 mb-3">`
   list-page header. That pattern does not wrap, and Bootstrap's .form-control /
   .form-select are `width: 100%` flex items with no min-width floor, so on a
   phone the row simply overflowed: on /assets the search box shrank to 26px,
   the type <select> to 50px, and the ms-auto Export/Import pair landed at
   x=467/559 in a 390px viewport — i.e. off-screen, with the DOCUMENT widened
   to 643px.

   The rules here fix all three failure modes at once:
     - the row WRAPS instead of overflowing (never widens the document);
     - every control declares a flex-basis AND a min-width, so nothing can be
       squeezed below a usable size;
     - `__actions` uses margin-inline-start:auto (the ms-auto behaviour) on
       desktop and drops to its own full-width line on phones.

   Slots (all optional): __search, __select, __group, __actions.
   Used by Assets, Tickets and the list pages that shared the same markup.
   ============================================================================ */
.app-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}

/* Search: grows into the free space on desktop (roughly the old 300px cap),
   full width on phones. min-width is the anti-collapse floor. */
/* Basis deliberately small (12rem) with grow:1 — the search absorbs whatever
   is left on the line, so a full Assets toolbar still fits ONE row at 1280px
   (the layout it shipped with) and only wraps when it genuinely cannot. The
   min-width is the anti-collapse floor; the max stops it ballooning on wide
   screens. */
.app-toolbar__search {
    flex: 1 1 12rem;
    min-width: 11rem;
    max-width: 22rem;
    position: relative;
}
.app-toolbar__search > .form-control,
.app-toolbar__search > .form-select,
.app-toolbar__search > input {
    width: 100%;
}

/* Selects keep a width you can actually read the option text in. */
.app-toolbar__select {
    flex: 0 1 13rem;
    min-width: 9.5rem;
}

/* Inline cluster (toggles, switches, chips) that should stay together and
   wrap as a unit rather than tearing apart mid-group. */
.app-toolbar__group {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    min-width: 0;
}

/* Secondary/primary action cluster — the ms-auto slot. */
.app-toolbar__actions {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-inline-start: auto;
}

/* Optional explicit spacer for callers that want the split somewhere else. */
.app-toolbar__spacer { flex: 1 1 auto; }

@media (max-width: 640.98px) {
    .app-toolbar__search {
        flex-basis: 100%;
        max-width: none;
    }
    .app-toolbar__select {
        flex: 1 1 100%;
    }
    /* Actions stop being right-aligned and take their own row, so they are
       always reachable instead of being pushed past the viewport edge. */
    .app-toolbar__actions {
        margin-inline-start: 0;
        width: 100%;
    }
    .app-toolbar__actions > .btn,
    .app-toolbar__actions > .btn-group {
        flex: 1 1 auto;
    }
}

/* ── Locally scrollable strip ──
   For a control that must NOT wrap (a segmented .btn-group of status filters
   reads as one control) but is wider than a phone. It scrolls INSIDE its own
   bordered lane; the document never widens. The lane is visually indicated —
   inset surface + hairline + a thin scrollbar — so it reads as scrollable
   rather than as a clipped row. */
.app-scroll-x {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    max-width: 100%;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}
.app-scroll-x > * { flex: 0 0 auto; }
.app-scroll-x::-webkit-scrollbar { height: 6px; }
.app-scroll-x::-webkit-scrollbar-thumb {
    background: var(--color-border-strong);
    border-radius: var(--radius-pill);
}

@media (max-width: 640.98px) {
    .app-scroll-x {
        flex: 1 1 100%;
        padding: 0.3rem;
        border: 1px solid var(--color-border);
        border-radius: var(--radius-md);
        background: var(--color-bg-subtle);
    }
}

/* ── Stacked list-page table (.app-table-stack) ──
   Companion to .app-toolbar for the phone end of a list page. Desktop keeps
   the normal table; under 641px each row becomes a readable stacked block and
   each cell labels itself from its data-label attribute, so the page never
   needs a horizontally scrolling table to stay usable one-handed.

   Usage: <table class="table app-table-stack"> with data-label="…" on every
   informational <td>. Cells that need no label (checkbox column, action
   buttons) omit the attribute; give the action cell class
   .app-table-stack__actions so its buttons line up as a row of their own. */
@media (max-width: 640.98px) {
    .app-table-stack thead {
        /* Visually hidden, not display:none — column headers stay available
           to screen readers, which still announce cells against them. */
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    /* Interactive header controls (sort buttons, select-all) would otherwise
       stay in the tab order as INVISIBLE focus stops. Sorting and select-all
       are deliberately desktop affordances; per-row actions remain. */
    .app-table-stack thead input,
    .app-table-stack thead button,
    .app-table-stack thead a {
        display: none;
    }

    .app-table-stack tr {
        display: block;
        border-bottom: 1px solid var(--color-border);
        padding: var(--space-2) var(--space-3);
    }

    .app-table-stack tbody tr:last-child {
        border-bottom: 0;
    }

    .app-table-stack td {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: var(--space-2);
        border: 0;
        padding: 0.15rem 0;
        background: transparent;
        /* Bootstrap's .table-striped paints per-cell; stripes make no sense
           on stacked blocks. */
        box-shadow: none;
        text-align: start;
    }

    .app-table-stack td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        max-width: 45%;
        color: var(--color-text-muted);
        font-weight: var(--font-weight-medium);
        font-size: 0.85rem;
    }

    .app-table-stack td.text-end {
        text-align: start;
    }

    .app-table-stack__actions {
        justify-content: flex-start;
        padding-top: var(--space-2);
        flex-wrap: wrap;
    }

    /* Touch-target floor for the per-row buttons. */
    .app-table-stack__actions .btn {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }
}

/* ── StandardListToolbar ── */
.tyit-toolbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}
/* Same anti-collapse floor as .app-toolbar: the explicit min-width is what
   stops the box being squeezed to an icon before the row wraps (the old
   `input { min-width: 240px }` sized the CHILD, which a shrinking flex parent
   simply overflowed). */
.tyit-toolbar__search { position: relative; flex: 1 1 15rem; min-width: 11rem; max-width: 22rem; }
.tyit-toolbar__search input { width: 100%; }
@media (max-width: 640.98px) {
    .tyit-toolbar__search { flex-basis: 100%; max-width: none; }
    .tyit-toolbar__spacer { display: none; }
    .tyit-toolbar__actions { width: 100%; }
}
.tyit-toolbar__filters {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
}
.tyit-toolbar__spacer { flex: 1; }
.tyit-toolbar__actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}
/* Column-chooser popover — the TicketsPage grouped tool-menu treatment
   (token-surfaced panel + fixed transparent click-away backdrop; the trigger
   sits ABOVE the backdrop so switching menus stays one click). */
.tyit-toolbar__menu { position: relative; display: inline-flex; z-index: 1062; }
.tyit-toolbar__menu-list {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    min-width: 220px;
    padding: 6px;
    background: var(--color-bg-card);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    z-index: 1061;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.tyit-toolbar__menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 7px 10px;
    border: 0;
    background: transparent;
    border-radius: var(--radius-md);
    color: var(--color-text);
    font-size: var(--font-size-sm);
    text-align: left;
    cursor: pointer;
}
.tyit-toolbar__menu-item:hover { background: var(--color-bg-subtle); color: var(--color-text); }
.tyit-toolbar__menu-item .bi { font-size: .95rem; width: 18px; text-align: center; }
.tyit-toolbar__menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1060;
    background: transparent;
}

/* ── StandardFormLayout ── */
.tyit-form { display: flex; flex-direction: column; gap: var(--space-5); max-width: 860px; }
.tyit-form__validation { margin: 0; }
.tyit-form-section {
    background: var(--color-bg-card);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
}
.tyit-form-section__title {
    margin: 0 0 var(--space-1) 0;
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-dark);
}
.tyit-form-section__description {
    margin: 0;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}
.tyit-form-section__body {
    display: flex;
    flex-direction: column;
    gap: var(--density-gap);
    margin-top: var(--space-4);
}

/* ============================================================================
   INVENTORY COCKPIT (Assets module)
   ----------------------------------------------------------------------------
   Shared building blocks for the asset list, detail, creation composer and CSV
   import. Tokens only — light/dark/high-contrast and the density/font-scale
   themes all apply without a second definition.

   Everything here is written narrow-first: the acceptance target is ~390px with
   no page-level horizontal overflow and no action that cannot be reached.
   ============================================================================ */

/* ── Guided step indicator (creation composer, CSV import) ────────────────── */
.wizard-steps {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    list-style: none;
    padding: 0;
    margin: 0 0 var(--space-3) 0;
}

.wizard-steps__item { display: flex; align-items: center; }

.wizard-steps__item + .wizard-steps__item::before {
    content: "";
    display: inline-block;
    width: 1.25rem;
    height: 1px;
    background: var(--color-border-strong);
    margin-inline-end: var(--space-2);
}

.wizard-steps__btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    border: 0;
    background: transparent;
    padding: 0.25rem 0.4rem;
    border-radius: var(--radius-md);
    color: var(--color-text-muted);
    cursor: pointer;
    min-height: 44px; /* touch target */
}

.wizard-steps__btn:disabled { cursor: default; }
.wizard-steps__btn:not(:disabled):hover { color: var(--color-primary-ink); }

.wizard-steps__num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    border: 1px solid var(--color-border-strong);
    background: var(--color-bg-subtle);
    font-weight: var(--font-weight-medium);
    flex: 0 0 auto;
}

.wizard-steps__item.is-current .wizard-steps__btn {
    color: var(--color-text);
    font-weight: var(--font-weight-medium);
}

.wizard-steps__item.is-current .wizard-steps__num {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: var(--color-on-primary, #fff);
}

.wizard-steps__item.is-done .wizard-steps__num {
    border-color: var(--color-primary);
    color: var(--color-primary-ink);
}

/* A step whose section holds a validation problem flags itself, so the operator
   is never told "something is wrong" without being told WHERE. */
.wizard-steps__item.is-invalid .wizard-steps__num {
    border-color: var(--color-danger);
    color: var(--color-danger);
    background: var(--color-bg-subtle);
}

/* ── Sticky action area ───────────────────────────────────────────────────── */
/* Primary actions stay reachable while the operator scrolls a long form or a
   long detail page. On phones the primary control goes full width. */
.sticky-actions {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3) 0;
    margin-top: var(--space-3);
    background: var(--color-bg-body, var(--color-bg-card));
    border-top: 1px solid var(--color-border);
}

.sticky-actions__group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.sticky-actions .btn { min-height: 44px; display: inline-flex; align-items: center; }

@media (max-width: 640.98px) {
    .sticky-actions { flex-direction: column-reverse; align-items: stretch; }
    .sticky-actions__group--primary .btn { width: 100%; justify-content: center; }
    .sticky-actions__group--secondary { justify-content: space-between; }
}

/* ── Mobile inventory cards ───────────────────────────────────────────────── */
/* Below the table breakpoint the grid becomes a card list: a 10-column table in
   a 390px viewport either overflows the document or truncates every cell to
   uselessness. */
.inventory-table { display: none; }
.inventory-cards { display: flex; flex-direction: column; gap: var(--space-2); }

@media (min-width: 992px) {
    .inventory-table { display: block; }
    .inventory-cards { display: none; }
}

.inventory-card {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-bg-card);
    padding: var(--space-3);
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
}

.inventory-card__body { flex: 1 1 auto; min-width: 0; }

.inventory-card__title {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    margin-bottom: 0.15rem;
}

.inventory-card__tag { font-weight: var(--font-weight-semibold); }

.inventory-card__name {
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
}

.inventory-card__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-top: 0.35rem;
}

.inventory-card__meta span { display: inline-flex; align-items: center; gap: 0.3rem; }

.inventory-card__actions { flex: 0 0 auto; }
.inventory-card__actions .btn { min-height: 44px; min-width: 44px; }

/* ── Click-away backdrop for the workflow popovers ────────────────────────── */
/* Shared by WorkflowPicker, WorkflowAddStepMenu and WorkflowEmailEditor, which
   each render `<div class="wfp-backdrop">` as their dismiss surface. It used to
   be declared only inside WorkflowPicker's inline style block, and a <style>
   inside a .razor is in the DOM only while THAT component renders — so for the
   other two the class matched nothing and the backdrop laid out as a 0x0 static
   div. Measured on the builder: the add-step menu's backdrop computed
   {position: static, width: 0px, height: 0px} and clicking away left the menu
   open, with no way to dismiss it but picking a step type.
   It lives here, not in a component, because three components consume it.
   z-index 1080 sits under .wasm-menu (1090) and under the drawers' own content
   while still covering the page. */
.wfp-backdrop { position: fixed; inset: 0; z-index: 1080; }

/* ── Column chooser / saved views popovers ────────────────────────────────── */
.cockpit-panel {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-bg-card);
    padding: var(--space-3);
    margin-bottom: var(--space-3);
}

.cockpit-panel__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: var(--space-1) var(--space-3);
}

/* ── Detail groups ────────────────────────────────────────────────────────── */
/* The tab strip is a scrollable rail rather than a wrapping pile of links: six
   groups will not fit a phone row, and wrapping them pushes the content below
   the fold on every visit. */
.detail-tabs {
    display: flex;
    gap: var(--space-1);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    border-bottom: 1px solid var(--color-border);
    margin-bottom: 0;
}

.detail-tabs button {
    flex: 0 0 auto;
    white-space: nowrap;
    min-height: 44px;
}

/* Any wide child (tables, diagrams, code) scrolls inside its own box so the
   PAGE never gains a horizontal scrollbar. */
.detail-pane { padding: var(--space-4); border: 1px solid var(--color-border); border-top: 0; }
.detail-pane .table-responsive { overflow-x: auto; }

@media (max-width: 640.98px) {
    .detail-pane { padding: var(--space-3); }
}

/* ── Lifecycle command bar ────────────────────────────────────────────────── */
.lifecycle-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.lifecycle-bar .btn { min-height: 44px; }

.lifecycle-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2);
}

.lifecycle-form__field { flex: 1 1 14rem; min-width: 11rem; }

/* ── Import mapping grid ──────────────────────────────────────────────────── */
.import-map {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-3);
}

.import-ambiguity {
    border: 1px solid var(--color-warning, var(--color-border-strong));
    border-radius: var(--radius-md);
    padding: var(--space-3);
    margin-bottom: var(--space-2);
}
