/* Halo Ops theme — the Dashboard token layer.
   Design source: design-mockups/deepseek-flash/e.html; exact values captured in the
   Appendix of docs/plans/DASHBOARD_HALO_OPS_MIGRATION.md. Loaded BEFORE
   BlazorBlueprint's blazorblueprint.css per BlazorBlueprint's documented
   custom-theme order; as an unlayered `:root` + `.dark` layer it also outranks the
   package's `@layer base`/`@layer theme` token defaults, so the <link> order is not
   load-bearing. `css/themes.css` is intentionally NOT loaded: it is the opt-in
   base/primary preset system (`BbThemeSwitcher`, unused) and the dark-mode toggle
   needs no themes.css. Tailwind v4 utilities come from Halo's own build —
   `Styles/tailwind.input.css`, compiled by `scripts/build-tailwind.sh` — which now
   carries the `@theme inline` mapping the previous theme kept here.

   Dark strategy: Blueprint toggles `.dark` on <html> (ThemeService + `bb-theme`
   localStorage key); do NOT add html[data-theme] selectors here. ThemeService still
   writes an inert `data-base-color="stone"` and an inline `--radius` onto <html> —
   keep --radius in sync with ThemeOptions.DefaultRadius (0.75) in Program.cs. The
   compound `.dark[data-base-color]` match below is retained so the dark tokens also
   match the attribute ThemeService writes at runtime — it is NOT a guard against a
   returned preset stylesheet (a later-loaded `.dark[data-base-color=…]` block would
   tie on specificity and win on order).

   Helper classes are e.html's "tiny helpers" (type scale + badge/tab/axis/chart
   primitives). Names verified collision-free against BlazorBlueprint.Components
   4.1.0 prebuilt CSS and app.css. */

:root {
    color-scheme: light;
    --radius: 0.75rem; /* e rounded-xl control scale; theme.js overrides inline */

    /* Type stacks (e's tailwind.config; the package body rule reads --font-sans) */
    --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

    --background: #eceef2; /* e --bg */
    --foreground: #101828; /* e --text */
    --card: #ffffff; /* e --surface */
    --card-foreground: #101828;
    --popover: #ffffff; /* e --surface (menus) */
    --popover-foreground: #101828;
    --primary: #7c3aed; /* e --accent */
    --primary-foreground: #ffffff; /* e --accent-ink */
    --secondary: #f5f6f8; /* e --surface-2 */
    --secondary-foreground: #101828;
    --muted: #f5f6f8; /* e --surface-2 (e --surface-3 is unused in e.html) */
    --muted-foreground: #667085; /* e --muted */
    --accent: #f5f6f8; /* Bb hover surface; e --surface-2 */
    --accent-foreground: #101828;
    --destructive: #e11d48; /* e --breach solid */
    --destructive-foreground: #ffffff; /* derived: filled rose chips take white ink in light */
    --border: #d8dde5;
    --input: #d8dde5; /* e inputs use --border */
    --ring: #7c3aed; /* e focus ring = --accent */
    --chart-1: #e11d48; /* plan appendix: breach / potential / ok / info + accent */
    --chart-2: #d97706;
    --chart-3: #10b981;
    --chart-4: #0ea5e9;
    --chart-5: #7c3aed;
    --sidebar: #ffffff; /* e --surface */
    --sidebar-foreground: #101828;
    --sidebar-primary: #7c3aed;
    --sidebar-primary-foreground: #ffffff;
    --sidebar-accent: #f5f6f8; /* e nav hover = --surface-2 */
    --sidebar-accent-foreground: #101828;
    --sidebar-border: #d8dde5;
    --sidebar-ring: #7c3aed;

    /* ── Sidebar layout + nav (BbSidebar reads these; e.html geometry) ──────────
       Expanded rail = e's lg width (272px). The collapsed rail (4.5rem) is the
       standard token from the migration plan. Menu buttons take e's 13px/500 nav
       type and rounded-xl; the active item is e's .navlink-on — foreground text on
       accent-soft with an accent icon (see the [data-active=true] svg rule). Widths
       and motion stay here so collapsed styles need no overrides. */
    --sidebar-width: 17rem; /* e.html lg:w-[272px] */
    --sidebar-width-mobile: 18rem; /* Bb mobile sheet */
    --sidebar-width-icon: 4.5rem; /* collapsed icon rail (plan) */
    --sidebar-content-gap: 0.75rem; /* + 2×group py = e's 1.25rem group rhythm */

    /* e chrome geometry: header (brand + tenant + search) and footer are p-3, the nav
       is p-3. Group padding is trimmed from Bb's .5rem to .25rem so the e gutters keep
       the existing item inset (content .75 + group .25 + button .5 = unchanged 1.5rem). */
    --sidebar-header-py: 0.75rem;
    --sidebar-header-px: 0.75rem;
    --sidebar-header-content-py: 0.25rem;
    --sidebar-header-content-px: 0.25rem;
    --sidebar-content-py: 0.75rem;
    --sidebar-content-px: 0.75rem;
    --sidebar-group-py: 0.25rem;
    --sidebar-group-px: 0.25rem;
    --sidebar-footer-py: 0.75rem;
    --sidebar-footer-px: 0.75rem;
    --sidebar-menu-button-font-size: 0.8125rem; /* e nav 13px */
    --sidebar-menu-button-font-weight: 500; /* e font-medium */
    --sidebar-menu-button-active-font-weight: 500; /* e .navlink-on keeps medium weight */
    --sidebar-menu-button-active-bg: var(--accent-soft);
    --sidebar-menu-button-active-color: var(--foreground); /* e .navlink-on text; the icon carries the accent */
    --sidebar-menu-button-radius: 0.75rem; /* e rounded-xl */
    --sidebar-badge-bg: var(--breach-soft); /* future breach badge count */
    --sidebar-badge-color: var(--breach-text);
    --sidebar-badge-font-size: 10.5px;
    --sidebar-badge-font-weight: 700;
    --sidebar-badge-height: 1.125rem;
    --sidebar-badge-min-width: 1.125rem;
    --sidebar-badge-radius: 999px;

    /* Shell chrome offset consumed by viewport-height layouts (.map-page, .subject-page):
       below md the mobile strip is 3.3125rem (2 × .5rem padding + 2.25rem trigger +
       1px border) plus its .75rem bottom margin plus e's 2 × .75rem wrapper gutter =
       5.5625rem; from md the strip is gone and only the wrapper gutter remains
       (2 × .75rem, 2 × 1rem at lg). One token so viewport-height pages can never
       drift from the shell again (Step 2's /map page consumes it via app.css .map-page). */
    --halo-shell-chrome: 5.5625rem;

    /* Semantic extras (e status text/soft/border/solid sets + soft accent) */
    --accent-soft: rgba(124, 58, 237, .12);

    /* Pinned-subject avatar palette (issue #101): the stable hashed slot from
       PinnedSubjectPresentation.AvatarClass picks one pair. Light = soft solid bg +
       strong fg; keep the count aligned with PinnedSubjectPresentation.PaletteSize. */
    --avatar-0-bg: #ede9fe; /* violet */
    --avatar-0-fg: #6d28d9;
    --avatar-1-bg: #e0f2fe; /* sky */
    --avatar-1-fg: #0369a1;
    --avatar-2-bg: #d1fae5; /* emerald */
    --avatar-2-fg: #047857;
    --avatar-3-bg: #fef3c7; /* amber */
    --avatar-3-fg: #b45309;
    --avatar-4-bg: #ffe4e6; /* rose */
    --avatar-4-fg: #be123c;
    --avatar-5-bg: #e0e7ff; /* indigo */
    --avatar-5-fg: #4338ca;

    --breach-text: #be123c;
    --breach-soft: #fff1f2;
    --breach-border: #fecdd3;
    --breach: #e11d48; /* solid */

    --potential-text: #b45309;
    --potential-soft: #fffbeb;
    --potential-border: #fde68a;
    --potential: #d97706; /* solid */

    --ok-text: #047857;
    --ok-soft: #ecfdf5;
    --ok-border: #a7f3d0;
    --ok: #10b981; /* solid */

    --info-text: #0369a1;
    --info-soft: #f0f9ff;
    --info-border: #bae6fd;
    --info: #0ea5e9; /* solid */

    /* Bb alert contract (blazorblueprint.css :root/.dark): --alert-* is the
       solid/accent (icon + border), --alert-*-foreground the text tone, --alert-*-bg
       the soft tint. Mapping: success←ok, info←info, warning←potential, danger←breach. */
    --alert-success: var(--ok);
    --alert-success-foreground: var(--ok-text);
    --alert-success-bg: var(--ok-soft);

    --alert-info: var(--info);
    --alert-info-foreground: var(--info-text);
    --alert-info-bg: var(--info-soft);

    --alert-warning: var(--potential);
    --alert-warning-foreground: var(--potential-text);
    --alert-warning-bg: var(--potential-soft);

    --alert-danger: var(--breach);
    --alert-danger-foreground: var(--breach-text);
    --alert-danger-bg: var(--breach-soft);

    /* Canvas + elevation (e). --halo-canvas bundles e's two fixed radial glows and
       the 34px ops grid so surfaces that must blend with the page canvas (body, the
       mobile shell strip) share one definition; the var() refs resolve per theme. */
    --glow-a: rgba(124, 58, 237, .10);
    --glow-b: rgba(13, 116, 144, .09);
    --grid: rgba(16, 24, 40, .05);
    --halo-canvas: radial-gradient(900px 420px at 88% -8%, var(--glow-a), transparent 65%),
    radial-gradient(760px 420px at -6% 104%, var(--glow-b), transparent 62%),
    repeating-linear-gradient(0deg, var(--grid) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(90deg, var(--grid) 0 1px, transparent 1px 34px);
    --shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 12px 32px -14px rgba(16, 24, 40, .22);
    --shadow-sm: 0 1px 2px rgba(16, 24, 40, .07);
    --inset: inset 0 1px 0 rgba(255, 255, 255, .8);
}

/* From md the mobile strip is hidden; only the wrapper gutter offsets content. */
@media (min-width: 48rem) {
    :root {
        --halo-shell-chrome: 1.5rem;
    }
}

@media (min-width: 64rem) {
    :root {
        --halo-shell-chrome: 2rem;
    }
}

/* Compound selector retained so the dark tokens also match the `data-base-color`
   attribute ThemeService writes onto <html> at runtime. Not a guard against a
   reintroduced preset stylesheet: specificity ties at (0,2,0), and a later-loaded
   `.dark[data-base-color=…]` block would win on order. */
.dark,
.dark[data-base-color] {
    color-scheme: dark;

    --background: #06090f; /* e --bg */
    --foreground: #e9eef6; /* e --text */
    --card: #0d131d; /* e --surface */
    --card-foreground: #e9eef6;
    --popover: #0d131d;
    --popover-foreground: #e9eef6;
    --primary: #a78bfa; /* e --accent */
    --primary-foreground: #1b1038; /* e --accent-ink */
    --secondary: #131b28; /* e --surface-2 */
    --secondary-foreground: #e9eef6;
    --muted: #131b28; /* e --surface-2 */
    --muted-foreground: #8896a9; /* e --muted */
    --accent: #131b28; /* Bb hover surface; e --surface-2 */
    --accent-foreground: #e9eef6;
    --destructive: #fb7185; /* e --breach solid */
    --destructive-foreground: #1b1038; /* derived: dark ink on the lighter rose solid */
    --border: #1f2a3a;
    --input: #1f2a3a;
    --ring: #a78bfa; /* e focus ring = --accent */
    --chart-1: #fb7185; /* plan appendix: breach / potential / ok / info + accent */
    --chart-2: #fbbf24;
    --chart-3: #34d399;
    --chart-4: #38bdf8;
    --chart-5: #a78bfa;
    --sidebar: #0d131d; /* e --surface */
    --sidebar-foreground: #e9eef6;
    --sidebar-primary: #a78bfa;
    --sidebar-primary-foreground: #1b1038;
    --sidebar-accent: #131b28; /* e nav hover = --surface-2 */
    --sidebar-accent-foreground: #e9eef6;
    --sidebar-border: #1f2a3a;
    --sidebar-ring: #a78bfa;

    --accent-soft: rgba(167, 139, 250, .16);

    /* Pinned-subject avatar palette (dark): translucent bg + light fg, same hue order. */
    --avatar-0-bg: rgba(139, 92, 246, .18); /* violet */
    --avatar-0-fg: #c4b5fd;
    --avatar-1-bg: rgba(56, 189, 248, .18); /* sky */
    --avatar-1-fg: #7dd3fc;
    --avatar-2-bg: rgba(52, 211, 153, .18); /* emerald */
    --avatar-2-fg: #6ee7b7;
    --avatar-3-bg: rgba(251, 191, 36, .18); /* amber */
    --avatar-3-fg: #fcd34d;
    --avatar-4-bg: rgba(251, 113, 133, .18); /* rose */
    --avatar-4-fg: #fda4af;
    --avatar-5-bg: rgba(129, 140, 248, .18); /* indigo */
    --avatar-5-fg: #a5b4fc;

    --breach-text: #fda4af;
    --breach-soft: rgba(244, 63, 94, .12);
    --breach-border: rgba(244, 63, 94, .32);
    --breach: #fb7185;

    --potential-text: #fcd34d;
    --potential-soft: rgba(245, 158, 11, .12);
    --potential-border: rgba(245, 158, 11, .32);
    --potential: #fbbf24;

    --ok-text: #6ee7b7;
    --ok-soft: rgba(16, 185, 129, .12);
    --ok-border: rgba(16, 185, 129, .32);
    --ok: #34d399;

    --info-text: #7dd3fc;
    --info-soft: rgba(14, 165, 233, .12);
    --info-border: rgba(14, 165, 233, .32);
    --info: #38bdf8;

    /* Bb alert set — same var mapping as :root; values resolve from the dark status
       sets above (kept explicit so the package's own `.dark` literals cannot win). */
    --alert-success: var(--ok);
    --alert-success-foreground: var(--ok-text);
    --alert-success-bg: var(--ok-soft);

    --alert-info: var(--info);
    --alert-info-foreground: var(--info-text);
    --alert-info-bg: var(--info-soft);

    --alert-warning: var(--potential);
    --alert-warning-foreground: var(--potential-text);
    --alert-warning-bg: var(--potential-soft);

    --alert-danger: var(--breach);
    --alert-danger-foreground: var(--breach-text);
    --alert-danger-bg: var(--breach-soft);

    --glow-a: rgba(167, 139, 250, .08);
    --glow-b: rgba(56, 189, 248, .06);
    --grid: rgba(233, 238, 246, .05);
    --shadow: 0 1px 2px rgba(0, 0, 0, .55), 0 18px 44px -20px rgba(0, 0, 0, .75);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
    --inset: inset 0 1px 0 rgba(255, 255, 255, .04);
}

/* Element default in a cascade layer so the package's utility classes win: the
   previous unlayered `*` rule outranked every layered `border-*` utility (an
   element's `border-rose-300` was repainted var(--border)). `:root *` (0,1,0)
   outranks the package's own base preflight `* { border: 0 solid }` (0,0,0),
   whose shorthand would otherwise reset every default border to currentColor;
   `@layer utilities` still beats this layer regardless of specificity. */
@layer base {
    :root * {
        border-color: var(--border);
    }
}

body {
    background-color: var(--background);
    color: var(--foreground);
    /* Layered glows + fine ops grid; the vars swap per theme (.dark block). The rule
       is unlayered, so it outranks the package's `@layer base` body defaults. */
    background-image: var(--halo-canvas);
    background-attachment: fixed;
}

:focus-visible {
    outline: 2px solid var(--ring); /* e focus = --accent, which maps to Bb --ring */
    outline-offset: 2px;
}

/* Form controls: the global outline above renders as a heavy pill on rounded
   inputs/comboboxes (it follows border-radius and stacks on BlazorBlueprint's
   own ring). Replace it with a thin, muted ring. This rule is unlayered, so it
   outranks the package's `@layer` utilities; links and buttons keep the global
   visible outline for keyboard focus. */
:where(input, select, textarea,
    [role="combobox"],
    [aria-haspopup="listbox"],
    [data-slot="input"],
    [data-slot="combobox-trigger"]):not(:where(.halo-search-field-input,
    [data-slot="input-group"] input,
    [data-slot="input-group"] textarea,
    .halo-map-search-input,
    input[role="combobox"][aria-autocomplete="list"])):focus-visible {
    outline: none;
    outline-offset: 0;
    border-color: color-mix(in srgb, var(--ring) 60%, var(--border));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--ring) 35%, transparent);
}

/* Seamless autocomplete inputs (#151, #216): the thin ring above paints a 1px box
   on the control's border-box. On a borderless, full-height input inside a pill /
   composite wrapper only its LEFT and RIGHT edges are perceptible (the top and
   bottom edges coincide with the wrapper's own border), which reads as two
   stray vertical lines. The wrapper carries the focus affordance instead
   (:focus-within below), so the inner control must paint nothing at all.
   #216: BlazorBlueprint's searchable combobox renders exactly that shape — BbCommandInput
   emits `input[role="combobox"][aria-autocomplete="list"]` (carrying no data-slot) as the
   search row of the portaled popover panel. That portal is why `:focus-within` on the
   container cannot reach the input, so the open trigger carries the ring instead (see the
   `button[role="combobox"][aria-expanded="true"]` rule below). The map's JS-built search
   box is a second such control, so its input takes the same exclusion and the box owns the
   focus ring. */
.halo-search-field-input:focus-visible,
[data-slot="input-group"] :is(input, textarea):focus-visible,
.halo-map-search-input:focus-visible,
input[role="combobox"][aria-autocomplete="list"]:focus-visible {
    outline: none;
    box-shadow: none;
    border-color: transparent;
}

/* BlazorBlueprint v4's column-visibility picker renders the "Pinned" badge with
   `bb:ms-auto bb:text-xs bb:text-muted-foreground` (v3 used `ml-auto` and unprefixed
   utilities, so the old selector silently stopped matching). `ms-auto` contributes no
   gap once the column name fills the row and the text looks squashed;
   `padding-inline-start` (not margin) preserves its end alignment while guaranteeing a
   visible gap. Unlayered on purpose: it outranks the library's `@layer bb-utilities`
   regardless of load order. See ADR 0033. Remove this if BlazorBlueprint adds
   spacing upstream. */
span.bb\:ms-auto.bb\:text-xs.bb\:text-muted-foreground {
    padding-inline-start: 0.5rem;
}

/* Grid header filters (#281, widened #293). The bar's controls were first sized to
   the narrowest option ("All"), so longer enum values were truncated to "…" in the
   trigger and, because a select's overlay opens at the trigger's width, in the dropdown
   too. A single generous floor gives every kind — enum, boolean, date range, text —
   room to read; it is unlayered so it outranks the per-control `min-w-[…]` utilities in
   Halo's Tailwind output regardless of load order. See ADR 0033. #293 raises the floor
   from 13rem to 16rem (the second half of the truncation fix; wrapping below is the
   other half). */
.halo-grid-header-filters [data-test^="grid-header-filter-"] {
    min-width: 16rem;
}

/* Grid header filter dropdowns (#293): the other half of the truncation fix. BbSelect
   portals its option list to the body, so the bar's own selector cannot reach it — and
   the overlay still opens at the trigger's width, so long enum values would truncate
   to "…" without this. Option labels wrap onto multiple lines instead; the trigger
   itself stays single-line. Global (not bar-scoped) deliberately: BbSelect 4.1.0
   exposes no popover class hook (verified — its API surface carries no Popover*
   parameter, unlike BbCombobox.PopoverWidth, and the assembly holds no select-content
   class to target), so no narrower selector can reach the portaled list. Menu-style
   popovers (tenant switcher, column visibility) use menu/menuitem roles and are
   untouched by this listbox/option selector. Unlayered on purpose, like the floor
   above: it outranks the library's `@layer` option styles regardless of load order. */
[role="listbox"] [role="option"] {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* Pins the native column-selector to the toolbar's trailing edge (#281: header filters
   left, picker right). A semantic class rather than a bare `ml-auto` utility because
   Halo's Tailwind output only contains the utilities something already asked for at
   the last `scripts/build-tailwind.sh` run; halo.css always ships. A no-op if the
   toolbar is not a flex container. */
.halo-grid-toolbar-end {
    margin-inline-start: auto;
}

/* ── e.html "tiny helpers" — Halo Ops design-language primitives ─────────────
   Type scale, badges, tabs and chart text. Global on purpose: verified no
   collisions with Bb's prebuilt utility CSS or app.css. */

/* Card surface: e's `.panel` + `rounded-2xl` (1rem). */
.panel {
    border-radius: 1rem;
    box-shadow: var(--shadow), var(--inset);
}

.tnum {
    font-variant-numeric: tabular-nums;
}

.micro {
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
}

.rise {
    animation: rise .5s cubic-bezier(.16, 1, .3, 1) both;
}

@keyframes rise {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

.live-dot {
    animation: beat 1.9s ease-out infinite;
}

@keyframes beat {
    0%, 100% {
        box-shadow: 0 0 0 0 rgba(16, 185, 129, .5);
    }
    55% {
        box-shadow: 0 0 0 5px rgba(16, 185, 129, 0);
    }
}

.badge {
    display: inline-flex;
    align-items: center;
    gap: .375rem;
    white-space: nowrap;
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: .15rem .55rem;
    background: var(--secondary);
    color: var(--muted-foreground);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.45;
}

.badge-breach {
    border-color: var(--breach-border);
    background: var(--breach-soft);
    color: var(--breach-text);
}

.badge-potential {
    border-color: var(--potential-border);
    background: var(--potential-soft);
    color: var(--potential-text);
}

.badge-ok {
    border-color: var(--ok-border);
    background: var(--ok-soft);
    color: var(--ok-text);
}

.badge-info {
    border-color: var(--info-border);
    background: var(--info-soft);
    color: var(--info-text);
}

/* Non-production environment badge (sidebar brand row): translucent green with a
   slow sheen slide so the dev/staging environment never reads as production. The
   sheen is decorative and is disabled for reduced-motion users. */
.halo-env-badge {
    background: color-mix(in srgb, var(--ok-soft) 70%, transparent);
    border-color: color-mix(in srgb, var(--ok-border) 70%, transparent);
    color: var(--ok-text);
    position: relative;
    overflow: hidden;
}

.halo-env-badge::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(110deg, transparent 30%, color-mix(in srgb, var(--ok-text) 18%, transparent) 50%, transparent 70%);
    transform: translateX(-120%);
    animation: halo-env-swipe 3.2s ease-in-out infinite;
}

@keyframes halo-env-swipe {
    0%, 60% {
        transform: translateX(-120%);
    }

    100% {
        transform: translateX(120%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .halo-env-badge::after {
        animation: none;
    }
}

/* Collapsed icon rail: no room for the badge. */
[data-collapsible="icon"] .halo-env-badge {
    display: none;
}

/* Semantic status-badge tones (#129). Unlayered so they outrank the BlazorBlueprint
   variant utilities (@layer); purple stays reserved for links and primary buttons. */
.halo-badge-success {
    border-color: var(--ok-border);
    background: var(--ok-soft);
    color: var(--ok-text);
}

.halo-badge-warning {
    border-color: var(--potential-border);
    background: var(--potential-soft);
    color: var(--potential-text);
}

.halo-badge-danger {
    border-color: var(--breach-border);
    background: var(--breach-soft);
    color: var(--breach-text);
}

.halo-badge-neutral {
    border-color: var(--border);
    background: var(--secondary);
    color: var(--muted-foreground);
}

/* Bracelet battery tone (#221) — text-only tone reusing the semantic colour roles above. */
.halo-battery-ok {
    color: var(--ok-text);
}

.halo-battery-warning {
    color: var(--potential-text);
}

.halo-battery-critical {
    color: var(--breach-text);
}

.dot {
    display: inline-block;
    height: .375rem;
    width: .375rem;
    flex: none;
    border-radius: 999px;
    background: currentColor;
}

.tabbtn {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    white-space: nowrap;
    border: 1px solid transparent;
    border-radius: .625rem;
    padding: .4rem .7rem;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--muted-foreground);
    transition: background-color .15s, color .15s;
}

.tabbtn:hover {
    color: var(--foreground);
    background: var(--accent);
}

.tabbtn[aria-selected="true"],
.tabbtn-on {
    color: var(--primary);
    background: var(--accent-soft);
    border-color: var(--primary);
    box-shadow: var(--shadow-sm);
}

.gridline {
    stroke: var(--border);
    stroke-width: 1;
    stroke-dasharray: 3 5;
}

.axisline {
    stroke: var(--border);
    stroke-width: 1;
}

.axistext {
    fill: var(--muted-foreground);
    font-size: 11.5px;
    font-family: var(--font-mono);
}

.axistext-strong {
    fill: var(--foreground);
    font-size: 12px;
    font-weight: 600;
}

/* ── Shell chrome (Step 1: inset sidebar + mobile strip) ───────────────────── */

/* Provider wrapper (HeightClass="min-h-screen halo-shell"). BbSidebarProvider has no
   Class parameter, so HeightClass carries this hook. Two package defaults are reset:
   `bg-sidebar` paints an opaque full-viewport surface over the body canvas, and the
   Inset variant only styles the main inset — the sidebar itself stays flat/full-bleed.
   halo.css owns e.html's chrome: transparent wrapper + p-3/p-4 gutters, the sidebar
   as a sticky rounded card, and the main inset de-carded so the canvas (grid + glows)
   shows in the gutters and behind the page panels. */
.halo-shell {
    background-color: transparent;
    gap: 0.75rem;
    padding: 0.75rem;
}

@media (min-width: 64rem) {
    .halo-shell {
        gap: 1rem;
        padding: 1rem;
    }
}

/* Sidebar card — e.html `.panel rounded-2xl border bg-[var(--surface)]`, capped to the
   viewport and sticky so the window keeps e's scroll model. The package's content
   region is `flex-1 overflow-auto`, so the nav scrolls inside the card while header
   and footer stay pinned. `overflow: hidden` clips that scrollbar to the radius; the
   collapsed rail re-opens the clip so tooltips can escape (e.html's `#sidebar.rail`). */
@media (min-width: 48rem) {
    .halo-shell > aside[data-variant='inset'] {
        position: sticky;
        top: 0.75rem;
        height: calc(100vh - 1.5rem);
        height: calc(100dvh - 1.5rem);
        max-height: calc(100vh - 1.5rem);
        max-height: calc(100dvh - 1.5rem);
        overflow: hidden;
        border: 1px solid var(--border);
        border-radius: 1rem;
        background-color: var(--card);
        box-shadow: var(--shadow), var(--inset);
    }

    .halo-shell > aside[data-variant='inset'][data-collapsible='icon'] {
        overflow: visible;
    }

    /* The package sets the content region to overflow-hidden in icon mode, so a nav
       taller than the card (short desktop viewports) would clip icons with no way to
       reach them. e.html's rail fallback scrolls the nav instead; tooltips render
       outside the sidebar (JS-positioned, body-level), so the scroll container does
       not clip them. */
    .halo-shell > aside[data-variant='inset'][data-collapsible='icon'] [data-sidebar='content'] {
        overflow-y: auto;
    }
}

@media (min-width: 64rem) {
    .halo-shell > aside[data-variant='inset'] {
        top: 1rem;
        height: calc(100vh - 2rem);
        height: calc(100dvh - 2rem);
        max-height: calc(100vh - 2rem);
        max-height: calc(100dvh - 2rem);
    }
}

/* Short desktop viewports: the card is height-capped, so if the pinned header and
   footer alone exceed the cap they would clip. e.html's short-viewport fallback
   (e.html:182–185) scrolls the aside instead; the rail additionally has its own
   content-scroll rule above. */
@media (min-width: 64rem) and (max-height: 690px) {
    .halo-shell > aside[data-variant='inset'] {
        overflow-y: auto;
    }
}

/* Main inset — undo the package's Inset card (m-2 + rounded-xl + bg-background +
   shadow). e's content column is transparent: the page panels own their surfaces and
   the canvas stays visible between/behind them. */
.halo-shell > main {
    margin: 0;
    height: auto;
    min-height: 0;
    border-radius: 0;
    background-color: transparent;
    box-shadow: none;
}

/* e's hairlines: header bottom (directly above the Monitoring group — tenant and
   search live in the header) and footer top (above the pinned user block). Both stay
   pinned; a scrolling BbSidebarSeparator would not. */
[data-sidebar='header'] {
    border-bottom: 1px solid var(--border);
}

[data-sidebar='footer'] {
    border-top: 1px solid var(--border);
}

/* Brand header: in the collapsed rail the logo and collapse chevron stack. */
[data-collapsible="icon"] [data-sidebar="header-content"] {
    flex-direction: column;
    gap: .375rem;
}

/* Collapse chevron trigger — e's rounded muted button; flips in the rail. */
.halo-rail-trigger {
    height: 2rem;
    width: 2rem;
    border-radius: .5rem;
    color: var(--muted-foreground);
}

.halo-rail-trigger:hover {
    color: var(--foreground);
    background: var(--secondary);
}

.halo-rail-chev {
    transition: transform .2s ease;
}

[data-collapsible="icon"] .halo-rail-chev {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .halo-rail-chev {
        transition: none;
    }

    .rise,
    .live-dot {
        animation: none;
    }
}

/* Group labels — e's .micro type (Bb only exposes size/weight vars). */
[data-sidebar="group-label"] {
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--muted-foreground);
}

/* Active nav — e.html's .navlink-on: foreground text on accent-soft, accent icon
   (the package's active color applies to the whole button; the icon is re-accented). */
[data-sidebar="menu-button"] {
    color: var(--muted-foreground);
}

[data-sidebar="menu-button"]:hover {
    color: var(--foreground);
    background: var(--secondary);
}

[data-sidebar="menu-button"][data-active="true"] {
    color: var(--foreground);
    background: var(--accent-soft);
}

[data-sidebar="menu-button"][data-active="true"] svg {
    color: var(--primary);
}

/* Pinned subjects (b.html): micro label + initial-avatar rows with a status line.
   The whole group is hidden in the icon rail — the rows are labelled links and the
   4.5rem rail has no room for them (no unlabelled focusables in the rail). */
[data-collapsible="icon"] .halo-pinned-group {
    display: none;
}

/* Sidebar/search pinned rows (issue #184): the row is the positioned flex parent so the
   trailing unpin button can sit absolutely at the row's vertical centre. The anchor and
   the button are siblings — never nest the button inside the anchor. */
.halo-pinned-row {
    position: relative;
    display: flex;
    align-items: center;
}

.halo-pinned-row > .halo-pinned-item {
    flex: 1;
    min-width: 0;
}

.halo-pinned-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: .625rem;
    padding: .375rem 2rem .375rem .5rem;
    border-radius: .75rem;
    color: var(--foreground);
}

.halo-pinned-item:hover {
    background: var(--secondary);
}

/* Grid identity shortcut (issue #250): the pinned identity cell is a one-click shortcut to the
   row's view (else edit) target, invoking the same handler as that row's actions menu. The
   button's box chrome is neutralised so the shortcut keeps the identity cell's typography and
   metrics; the rule is unlayered, so it outranks BlazorBlueprint's @layer utilities. */
.halo-cell-shortcut {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    min-width: 0;
    height: auto;
    padding: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    /* Where the button wraps a multi-part identity (SubjectsGrid's avatar + name), take the
       enclosing cell's gap rather than the library button's own affordance spacing. */
    gap: inherit;
    text-align: left;
    text-decoration: none;
    white-space: normal;
}

.halo-cell-shortcut:hover {
    color: var(--primary);
    text-decoration: underline;
    text-underline-offset: .25rem;
}

/* Pinned-subject avatar palette: the stable hashed slot (PinnedSubjectPresentation.AvatarClass)
   paints BbAvatarFallback. The two-class scope outranks the fallback's bg-muted/text-muted-foreground
   utility pair (and, being unlayered, wins over BlazorBlueprint's @layer utilities regardless). */
.halo-pinned-item .halo-avatar-0 {
    background: var(--avatar-0-bg);
    color: var(--avatar-0-fg);
}

.halo-pinned-item .halo-avatar-1 {
    background: var(--avatar-1-bg);
    color: var(--avatar-1-fg);
}

.halo-pinned-item .halo-avatar-2 {
    background: var(--avatar-2-bg);
    color: var(--avatar-2-fg);
}

.halo-pinned-item .halo-avatar-3 {
    background: var(--avatar-3-bg);
    color: var(--avatar-3-fg);
}

.halo-pinned-item .halo-avatar-4 {
    background: var(--avatar-4-bg);
    color: var(--avatar-4-fg);
}

.halo-pinned-item .halo-avatar-5 {
    background: var(--avatar-5-bg);
    color: var(--avatar-5-fg);
}

.halo-pinned-name {
    display: block;
    font-size: 12.5px;
    font-weight: 600;
}

.halo-pinned-status {
    display: flex;
    align-items: center;
    gap: .375rem;
    font-size: 10.5px;
    color: var(--muted-foreground);
}

.halo-pinned-empty {
    display: block;
    padding: .375rem .5rem;
    color: var(--muted-foreground);
    font-size: 11px;
}

.halo-pinned-unpin {
    position: absolute;
    top: 50%;
    right: .375rem;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 1.5rem;
    width: 1.5rem;
    padding: 0;
    border: none;
    border-radius: .5rem;
    background: transparent;
    color: var(--muted-foreground);
    opacity: 0;
    z-index: 1;
    cursor: pointer;
    transition: opacity .12s ease, color .12s ease, background-color .12s ease;
}

.halo-pinned-item:hover .halo-pinned-unpin,
.halo-pinned-item:focus-within .halo-pinned-unpin,
.halo-pinned-row:hover .halo-pinned-unpin,
.halo-pinned-row:focus-within .halo-pinned-unpin,
.halo-pinned-unpin:focus-visible {
    opacity: 1;
}

.halo-pinned-unpin:hover {
    background: color-mix(in oklab, var(--foreground) 10%, transparent);
    color: var(--foreground);
}

/* Empty count slot stays in the DOM but paints nothing until a count lands.
   (:empty requires the component's markup to stay whitespace-free — don't pad the
   BbSidebarMenuBadge call site.) */
[data-sidebar="menu-badge"]:empty {
    display: none;
}

/* Breaches nav-group count badges (#183). Blueprint 4.1.0's prebuilt
   [data-sidebar=menu-badge] rule only sets size/padding/right, so a badge rendered as a
   child of the menu item sits in static flow BELOW the row (we wrap the menu button in a
   data-test span, so the badge is not the button's flex sibling). Pin absolute centring
   against the menu-item row, which therefore must be the positioned ancestor.
   Unlayered Halo rules outrank the package's @layer utilities. Re-check on any Blueprint
   upgrade: the contract is menu-item = position:relative, badge = absolutely centred. */
[data-sidebar="menu-item"] {
    position: relative;
}

[data-sidebar="menu-badge"] {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    z-index: 1;
}

/* Amber (pending) / grey (recent) tints; Active keeps the package default. Same palettes
   as .halo-badge-warning and .halo-badge-neutral. */
.halo-sidebar-badge-pending {
    --sidebar-badge-bg: var(--potential-soft);
    --sidebar-badge-color: var(--potential-text);
}

.halo-sidebar-badge-recent {
    --sidebar-badge-bg: var(--secondary);
    --sidebar-badge-color: var(--muted-foreground);
}

/* Breach badge sheen (#275, superseding the #245 pulse). The class mounts only while a
   non-zero count renders — a zero count leaves the badge empty and :empty-hidden, so the
   animation can never run on a blank slot. The sweep is the #132 env-badge slide scaled to
   badge size: a ::after gradient band crosses the badge once every 3.2s, riding the badge's
   own foreground (currentColor) so the theme tokens and the pending amber tint carry through
   without a colour per state. The badge is already absolutely centred with translateY(-50%),
   and the sweep animates the pseudo-element only, so that static transform is never touched. */
.halo-badge-sheen {
    overflow: hidden;
}

.halo-badge-sheen::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(110deg, transparent 25%, color-mix(in oklab, currentColor 30%, transparent) 50%, transparent 75%);
    transform: translateX(-120%);
    animation: halo-badge-sheen 3.2s ease-in-out infinite;
}

@keyframes halo-badge-sheen {
    0%, 60% {
        transform: translateX(-120%);
    }

    100% {
        transform: translateX(120%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .halo-badge-sheen::after {
        animation: none;
    }
}

/* Tenant switcher: chip (single) / select (multiple) + collapsed initials form. */
.halo-tenant-switcher {
    width: 100%;
}

/* Pure-SystemAdmin fallback (SystemAdmin claim, no tenant assignments): the only
   /tenants affordance when the tenant dropdown cannot render. Mirrors the subject
   search trigger's shell surface (small, subtle, same control height). */
.halo-tenant-manage {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    height: 2.25rem;
    padding: 0 .75rem;
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
    color: var(--muted-foreground);
    font-size: 12.5px;
    font-weight: 500;
    text-align: left;
}

.halo-tenant-manage:hover {
    color: var(--foreground);
    border-color: var(--primary);
}

/* Admin sub-actions in the tenant menu (Manage tenants / Users / Email): secondary to
   tenant switching, so render one step smaller and muted (issue #127). Unlayered rules
   win over the package's @layer typography. */
.halo-tenant-menu-admin {
    font-size: 12px;
    color: var(--muted-foreground);
}

.halo-tenant-menu-admin:hover,
.halo-tenant-menu-admin:focus-visible {
    color: var(--foreground);
}

/* Tenant dropdown grouping (#279). The Bb content tracks the trigger width via the native
   MatchTriggerWidth anchor-width technique, so no width rule is needed here for the expanded
   and single-tenant menus. The selected tenant's own rows are grouped directly beneath it; the
   group is nested one step in so the hierarchy reads without a second heading, and the package
   separator (bg-muted) is recoloured to the mock's border token. */
.halo-tenant-group {
    padding-left: .625rem;
}

.halo-tenant-menu [role="separator"] {
    background: var(--border, var(--muted));
}

/* Rail flyout (#279): the collapsed 2rem initials trigger cannot define a usable menu width,
   so the rail menu keeps the mock's fixed 13rem instead of matching the trigger. */
.halo-tenant-menu-rail {
    min-width: 13rem;
}

/* Rail: keep the path visible as a centred icon (the label is clipped by the shared
   rail-sr rule and the anchor keeps its aria-label). */
[data-collapsible="icon"] .halo-tenant-manage {
    width: 2.25rem;
    margin-inline: auto;
    padding: 0;
    justify-content: center;
}

.halo-tenant-chip {
    display: flex;
    align-items: center;
    gap: .625rem;
    min-width: 0;
    padding: .5rem;
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
}

.halo-tenant-initials {
    display: grid;
    place-items: center;
    height: 2rem;
    width: 2rem;
    flex: none;
    border-radius: .5rem;
    background: var(--foreground);
    color: var(--card);
    font-size: 11px;
    font-weight: 700;
}

.halo-tenant-name {
    min-width: 0;
    font-size: 12.5px;
    font-weight: 600;
}

/* Expanded multi-tenant control: e.html's tenant button (avatar + name/subline +
   chevron) replacing the old BbSelect so the subline can live under the name. */
.halo-tenant-full-trigger {
    height: auto;
    width: 100%;
    justify-content: flex-start;
    gap: .625rem;
    padding: .625rem;
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
    color: var(--foreground);
}

.halo-tenant-full-trigger:hover {
    border-color: var(--primary);
    background: var(--secondary);
}

.halo-tenant-meta {
    min-width: 0;
}

/* e.html's tenant subline (10.5px muted tabular counts) — PLACEHOLDER text until a
   tenant-stats API exists (see TenantSummaryPlaceholder in HeaderSidebar.razor). */
.halo-tenant-subline {
    font-size: 10.5px;
    color: var(--muted-foreground);
    font-variant-numeric: tabular-nums;
}

.halo-tenant-chevron {
    color: var(--muted-foreground);
}

/* Tenant menu rows (e.html's tenantMenu): a status dot per assignment — accent for
   the current row, border-grey otherwise — and a trailing accent check on the
   current row (BbDropdownMenuItem lays its children out as a flex row). */
.halo-tenant-dot {
    height: .375rem;
    width: .375rem;
    flex: none;
    border-radius: 999px;
    margin-right: .5rem; /* the package's menu item ships no gap utility */
    background: var(--border);
}

.halo-tenant-dot-current {
    background: var(--primary);
}

.halo-tenant-check {
    color: var(--primary);
    margin-left: .5rem;
}

.halo-tenant-switcher-rail {
    display: none;
}

/* Rail tenant button (collapsed, multi-tenant): a square initials trigger opening
   the tenant flyout, matching e.html's rail tenant button. */
.halo-tenant-rail-trigger {
    height: 2rem;
    width: 2rem;
    padding: 0;
    justify-content: center;
    border-radius: .5rem;
}

.halo-tenant-rail-trigger:hover {
    background: var(--secondary);
}

[data-collapsible="icon"] .halo-tenant-switcher-full {
    display: none;
}

/* Rail: center the initials trigger in the 4.5rem card (the header's icon layout does
   not stretch every wrapper, so center both levels) and drop the layout chevron. */
[data-collapsible="icon"] .halo-tenant-switcher {
    display: flex;
    justify-content: center;
}

[data-collapsible="icon"] .halo-tenant-switcher-rail {
    display: flex;
    justify-content: center;
    width: 100%;
}

[data-collapsible="icon"] .halo-tenant-rail-trigger {
    margin-inline: auto;
}

[data-collapsible="icon"] .halo-tenant-chevron {
    display: none;
}

[data-collapsible="icon"] .halo-tenant-chip {
    justify-content: center;
    padding: .375rem;
    /* The meta block is clipped to a 0-width flex item in the rail, but the button's
       flex gap still pushed the initials 5px left of centre — drop the gap so the
       avatar is the only in-flow child and sits dead centre. */
    gap: 0;
}

/* Subject search trigger hides in the 4.5rem rail (expand to search); it looks
   like the old search field but opens the centred SubjectSearchDialog modal. */
[data-collapsible="icon"] .halo-sidebar-search {
    display: none;
}

.halo-sidebar-search-trigger {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    height: 2.25rem;
    padding: 0 .75rem;
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
    color: var(--muted-foreground);
    font-size: 12.5px;
    text-align: left;
}

.halo-sidebar-search-trigger:hover {
    color: var(--foreground);
    border-color: var(--primary);
}

.halo-sidebar-footer {
    display: flex;
    flex-direction: column;
    gap: .625rem;
    width: 100%;
}

/* e.html's footer card: `rounded-xl border bg-[var(--surface-2)] p-2.5` holding the
   identity row and the explicit Profile / Logout pair (grid-cols-2, rounded-lg). */
.halo-user-card {
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
    padding: .625rem;
}

.halo-user-row {
    display: flex;
    align-items: center;
    gap: .625rem;
    min-width: 0;
}

/* One actions row: icon-only theme toggle, then Profile / Logout (the rail stacks
   all three icon-first). */
.halo-user-actions {
    display: grid;
    grid-template-columns: auto 1fr 1fr;
    gap: .375rem;
    margin-top: .625rem;
}

.halo-user-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .375rem;
    height: auto;
    border: 1px solid var(--border);
    border-radius: .5rem;
    background: var(--card);
    padding: .25rem .375rem;
    font-size: 11px;
    font-weight: 500;
    color: var(--foreground);
    transition: border-color .15s, background-color .15s;
}

.halo-user-action-theme {
    padding-inline: .5rem;
}

.halo-user-action:hover {
    border-color: var(--primary);
}

.halo-user-avatar {
    display: grid;
    place-items: center;
    height: 2rem;
    width: 2rem;
    flex: none;
    border-radius: 999px;
    background: var(--foreground);
    color: var(--card);
    font-size: 11px;
    font-weight: 700;
}

.halo-user-text {
    font-size: 12.5px;
    line-height: 1.25;
}

.halo-user-text .halo-user-meta {
    font-size: 11px;
}

[data-collapsible="icon"] .halo-sidebar-footer {
    align-items: center;
}

[data-collapsible="icon"] .halo-user-text {
    display: none;
}

[data-collapsible="icon"] .halo-user-row {
    justify-content: center;
}

[data-collapsible="icon"] .halo-user-card {
    padding: .375rem;
}

/* Rail: the card's two actions stack as icon-only buttons (the labels don't fit in
   4.5rem) — the Profile anchor keeps its profile-menu-trigger data-test. */
[data-collapsible="icon"] .halo-user-actions {
    grid-template-columns: 1fr;
}

/* e.html's `.rail-sr` clip: rail labels (Profile/Logout, tenant name/subline) are
   removed visually but stay in the accessibility tree — never `display: none`, which
   would leave the icon-only anchors unnamed for screen readers. */
[data-collapsible="icon"] .halo-user-action span,
[data-collapsible="icon"] .halo-tenant-manage span,
[data-collapsible="icon"] .halo-tenant-name,
[data-collapsible="icon"] .halo-tenant-subline {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Mobile nav strip — shown while the sidebar is a BbSheet (BlazorBlueprint's own
   mobile split: below md/768px it renders the Sheet). `md:hidden` in the markup
   matches that breakpoint, but this unlayered display:flex would beat the package's
   layered utility, so the 48rem hide is repeated here for the custom class. */
.halo-shell-mobile-strip {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .5rem .75rem;
    border-bottom: 1px solid var(--border);
    /* Rounded to match the sidebar card (1rem) — the strip reads as its peer card. */
    border-radius: 1rem;
    /* App-bar surface: opaque --surface (NOT the canvas) so the brand/search row
       reads as chrome. Content height stays 2.25rem, keeping the strip's 3.3125rem
       box total unchanged (the below-md .75rem bottom margin is counted separately
       in --halo-shell-chrome). */
    background-color: var(--card);
}

@media (min-width: 48rem) {
    .halo-shell-mobile-strip {
        display: none;
    }
}

/* Below md the page header card would touch the sticky strip: the .75rem bottom
   margin restores e's one-rhythm gap (counted in --halo-shell-chrome above).
   Margin-bottom never affects the strip's sticky top:0, and the max-width query
   keeps desktop untouched. */
@media (max-width: 47.99rem) {
    .halo-shell-mobile-strip {
        margin-bottom: .75rem;
    }
}

.halo-mobile-nav-trigger {
    height: 2.25rem;
    width: 2.25rem;
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
    color: var(--muted-foreground);
}

.halo-mobile-nav-trigger:hover {
    color: var(--foreground);
}

/* App-bar brand (same mark as the sidebar header) and the search shortcut. */
.halo-mobile-brand {
    display: flex;
    align-items: center;
    gap: .5rem;
    min-width: 0;
    height: 2.25rem;
    color: var(--foreground);
}

.halo-mobile-brand-mark {
    height: 1.75rem;
    width: 1.75rem;
    flex: none;
}

.halo-mobile-brand-name {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -.01em;
}

.halo-mobile-search {
    display: grid;
    place-items: center;
    margin-left: auto;
    height: 2.25rem;
    width: 2.25rem;
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
    color: var(--muted-foreground);
}

.halo-mobile-search:hover {
    color: var(--foreground);
}

/* ── Page header (PageHeader.razor) — e.html's header card ─────────────────── */
.halo-page-header {
    border: 1px solid var(--border);
    background: var(--card);
}

.halo-page-header-body {
    padding: .875rem 1rem;
}

.halo-page-header-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: .625rem .75rem;
}

.halo-page-header-heading {
    min-width: 0;
    flex: 1 1 auto;
}

.halo-page-header-identity {
    display: flex;
    align-items: center;
    gap: .625rem;
    min-width: 0;
}

.halo-page-header-avatar {
    flex: 0 0 auto;
}

.halo-page-header-titles {
    min-width: 0;
    flex: 1 1 auto;
}

.halo-page-header-subtext {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .375rem .75rem;
    margin-top: .375rem;
    font-size: 12.5px;
    color: var(--muted-foreground);
}

.halo-subject-meta {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    white-space: nowrap;
}

/* Semantic badge tones (#130). Neutral/Destructive variants come from BbBadge. */
.halo-badge-success {
    border-color: var(--ok-border);
    background: var(--ok-soft);
    color: var(--ok-text);
}

.halo-badge-danger {
    border-color: var(--breach-border);
    background: var(--breach-soft);
    color: var(--breach-text);
}

.halo-page-header-breadcrumb {
    display: flex;
    align-items: center;
    gap: .375rem;
    font-size: 11.5px;
    color: var(--muted-foreground);
}

.halo-page-header-breadcrumb a:hover {
    color: var(--foreground);
}

.halo-page-header-title {
    margin-top: .25rem;
    font-size: 21px;
    font-weight: 600;
    letter-spacing: -.025em;
    line-height: 1.2;
}

/* FocusOnNavigate focuses the page h1 after a route change (FocusOnNavigate
   Selector="h1"); a heading is not a keyboard-reachable control, so its
   programmatic focus ring is suppressed — standard Blazor template behaviour. */
.halo-page-header-title:focus,
.halo-page-header-title:focus-visible {
    outline: none;
}

/* Login title: same FocusOnNavigate programmatic-focus suppression as
   .halo-page-header-title above (a heading is never keyboard-focusable, so
   only that programmatic ring is ever shown). Scoped to the login h1 only;
   the global :focus-visible rule (keyboard a11y on controls) is untouched. */
.halo-login-title:focus,
.halo-login-title:focus-visible {
    outline: none;
}

@media (min-width: 40rem) {
    .halo-page-header-title {
        font-size: 25px;
    }
}

.halo-page-header-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    margin-left: auto;
}

.halo-page-header-tabs {
    display: flex;
    align-items: center;
    gap: .375rem;
    overflow-x: auto;
    margin-top: .875rem;
    padding-top: .75rem;
    border-top: 1px solid var(--border);
}

/* ── Ops overview (Home, Step 2) — e.html's KPI / chart / table card system ─── */

/* Page content column: e's space-y-3 / lg:space-y-4 rhythm. */
.halo-overview {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-top: 1em;
}

/* KPI grid caps at 4-across (xl): the 7 slim cards wrap to 4+3 on wide screens
   so titles never truncate. Readability beats fitting a single line. */
.halo-kpi-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.75rem;
}

/* Charts row: hidden below md — the BbSidebar drawer regime; stacked from md, 3/2 split at xl. */
.halo-charts-grid {
    display: none;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.75rem;
}

@media (min-width: 48rem) {
    .halo-charts-grid { display: grid; }

    .halo-kpi-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (min-width: 64rem) {
    .halo-overview,
    .halo-kpi-grid,
    .halo-charts-grid {
        gap: 1rem;
    }

    .halo-card {
        padding: 1rem;
    }
}

@media (min-width: 80rem) {
    .halo-kpi-grid {
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: 1rem;
    }

    .halo-charts-grid {
        grid-template-columns: repeat(5, minmax(0, 1fr));
    }

    .halo-chart-wide {
        grid-column: span 3 / span 3;
    }

    .halo-chart-narrow {
        grid-column: span 2 / span 2;
    }
}

/* The 7 slim KPI cards wrap 4+3 at xl and stay there: no 7-across tier, so
   titles ("Recent-ended breaches", "Monitored locations", …) always fit. */

/* Card — e .panel + border + surface + p-3.5/p-4 (the `.panel` helper adds the radius). */
.halo-card {
    border: 1px solid var(--border);
    background: var(--card);
    padding: 0.875rem;
}

.halo-card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.5rem;
}

.halo-card-title {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -0.025em;
}

.halo-card-sub {
    margin-top: 0.125rem;
    font-size: 11.5px;
    color: var(--muted-foreground);
}

.halo-card-note {
    margin-top: 0.5rem;
    font-size: 10.5px;
    color: var(--muted-foreground);
}

/* KPI internals (e's icon chip, 30px tabular value, muted subline, foot row). */
.halo-kpi-icon {
    display: grid;
    place-items: center;
    height: 1.75rem;
    width: 1.75rem;
    flex: none;
    border-radius: 0.5rem;
    background: var(--secondary);
    color: var(--muted-foreground);
}

.halo-kpi-value-row {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.75rem;
    margin-top: 0.625rem;
}

.halo-kpi-value {
    font-size: 30px;
    font-weight: 600;
    line-height: 1;
    letter-spacing: -0.025em;
}

.halo-kpi-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.375rem;
    margin-top: 0.75rem;
}

.halo-kpi-foot-note {
    font-size: 10.5px;
    color: var(--muted-foreground);
}

/* Compact KPI strips — horizontal layout (icon left of title, count right).
   Titles NEVER truncate: they wrap to a second line rather than ellipsis.
   Scoped to .halo-kpi-compact so breach-list cards reusing .halo-card are untouched. */
.halo-kpi-compact {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 0.375rem;
    min-width: 0;
    padding: 0.75rem 0.875rem;
}

/* Whole-card link: the KPI card itself is the anchor (no inner "Show all").
   Natively keyboard-focusable; keep card colours and add a visible focus ring. */
.halo-kpi-card-link {
    color: inherit;
    text-decoration: none;
    cursor: pointer;
}

.halo-kpi-card-link:hover {
    border-color: var(--primary);
}

.halo-kpi-card-link:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.halo-kpi-compact-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem 0.625rem;
    min-width: 0;
}

.halo-kpi-compact-title {
    flex: 1 1 auto;
    min-width: 0;
    white-space: normal;
    overflow-wrap: break-word;
}

.halo-kpi-value-sm {
    font-size: 20px;
    flex: none;
}

.halo-kpi-icon-sm {
    height: 1.5rem;
    width: 1.5rem;
    border-radius: 0.375rem;
}

/* Live dot (e's beat dot) — `.live-dot` supplies the animation, this the colour. */
.halo-live-dot {
    display: inline-block;
    height: .375rem;
    width: .375rem;
    flex: none;
    border-radius: 999px;
    background: var(--ok);
}

/* Honest empty state — e.html's dashed placeholder shell (rounded-2xl, 1rem). */
.halo-empty {
    display: grid;
    place-items: center;
    min-height: 8rem;
    margin-top: 0.75rem;
    padding: 1.5rem 1rem;
    border: 1px dashed var(--border);
    border-radius: 1rem;
    background: var(--secondary);
    text-align: center;
}

.halo-empty-lg {
    min-height: 12rem;
}

.halo-empty-icon {
    display: grid;
    place-items: center;
    height: 2.5rem;
    width: 2.5rem;
    margin: 0 auto;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--card);
    color: var(--muted-foreground);
}

.halo-empty-title {
    margin-top: 0.625rem;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--foreground);
}

.halo-empty-text {
    margin-top: 0.25rem;
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--muted-foreground);
}

/* ── Recent breach episodes table (HomeRecentBreaches, #120) ───────────────
   The widget names `.halo-table-wrap` / `.halo-recent-table` but no layer ever
   owned them, so the feed rendered as a browser-default table. Structure follows
   e.html's schedule table — a muted header band, hairline row rules and a lit
   newest row — against Halo's columns (subject · condition · type · state · last
   transition). No outer border: the enclosing `.halo-card` already draws one, so
   the header band + row hairlines carry the structure instead of a double frame. */
.halo-table-wrap {
    margin-top: 0.75rem;
    border-radius: 0.75rem;
    overflow-x: auto;
}

.halo-recent-table {
    width: 100%;
    min-width: 34rem;
    border-collapse: collapse;
    text-align: left;
    font-size: 0.8125rem;
}

.halo-recent-table thead th {
    white-space: nowrap;
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--border);
    background: var(--secondary);
    color: var(--muted-foreground);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-align: left;
    text-transform: uppercase;
}

.halo-recent-table tbody td {
    padding: 0.625rem 0.75rem;
    vertical-align: middle;
    border-bottom: 1px solid var(--border);
}

.halo-recent-table tbody tr {
    transition: background-color .15s ease;
}

/* Newest transition (the server sorts newest-first) — e.html's accent-washed
   first row. Declared before `:hover` so hovering still repaints `--secondary`,
   matching every other row and staying legible in both themes. */
.halo-recent-table tbody tr.halo-recent-newest {
    background: var(--accent-soft);
}

.halo-recent-table tbody tr:hover {
    background: var(--secondary);
}

.halo-recent-table tbody tr:last-child td {
    border-bottom: none;
}

/* Subject: the row's anchor, semibold foreground; accent + underline on hover. */
.halo-recent-subject {
    font-weight: 600;
    color: var(--foreground);
    text-decoration: none;
}

.halo-recent-subject:hover {
    color: var(--primary);
    text-decoration: underline;
}

/* The global :focus-visible ring provides the visible keyboard indicator; only
   the radius needs pinning here (avoids a duplicate, heavier outline). */
.halo-recent-subject:focus-visible {
    border-radius: 0.25rem;
}

/* Condition is the row's primary datum after the subject; type and the
   transition stamp recede to the muted, smaller register (the stamp tabular). */
.halo-recent-condition {
    font-weight: 500;
}

.halo-recent-table tbody td.halo-recent-type,
.halo-recent-table tbody td.halo-recent-when {
    color: var(--muted-foreground);
    font-size: 11.5px;
}

/* The newest row is tinted with `--accent-soft`; that lift drops the muted
   register just under WCAG AA, so those two cells retain the normal body ink. */
.halo-recent-table tbody tr.halo-recent-newest .halo-recent-type,
.halo-recent-table tbody tr.halo-recent-newest .halo-recent-when {
    color: var(--secondary-foreground);
}

.halo-recent-when {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* At lg the card gains padding; widen the table gutters to match that padding
   rhythm. This is cosmetic alignment of the row text inset, not an edge match. */
@media (min-width: 64rem) {
    .halo-recent-table thead th,
    .halo-recent-table tbody td {
        padding-left: 1rem;
        padding-right: 1rem;
    }
}

/* ── Global ops footer (AppFooter.razor) — e.html's footer panel ──────────── */
/* Slot wrapper: the auto top margin floats the footer to the viewport bottom on
   short pages. The footer itself is margin: 0 so its bottom lands exactly on
   the sidebar card bottom line; on tall pages the shell wrapper's own padding
   is the single 1rem gap below the footer — e.html's one-rhythm spacing rather
   than footer margin + shell padding. */
.halo-footer-slot {
    margin-top: auto;
}

.halo-app-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .375rem 1rem;
    margin: 0;
    padding: .625rem .875rem;
    border: 1px solid var(--border);
    background: var(--card);
    font-size: 11px;
    color: var(--muted-foreground);
}

.halo-app-footer-brand {
    font-weight: 600;
    color: var(--foreground);
}

.halo-app-footer-end {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .625rem;
    margin-left: auto;
}

.halo-app-footer-tier {
    display: flex;
    align-items: center;
}

/* Issue #76 tenant-zone disclaimer: quiet, next to the copyright link. */
.halo-app-footer-zone {
  color: var(--muted-foreground);
}

/* Non-production environment label (mirrors the sidebar badge rule). */
.halo-app-footer-env {
    color: var(--ok-text);
    font-weight: 600;
}

/* Separator between the tenant-zone disclaimer and the copyright link. */
.halo-app-footer-dot {
    line-height: 1;
}

/* Dev-only footer tier icon — e.html's chart tones. Tier colours key off data-tier because the
   loaded blazorblueprint.css ships no `text-chart-*` utilities; `--chart-*` exist in the package's
   unloaded themes.css only (our --chart-* tokens live in halo.css). */
.halo-app-footer-tier[data-tier="mobile"] {
    color: var(--chart-1);
}

.halo-app-footer-tier[data-tier="tablet"] {
    color: var(--chart-2);
}

.halo-app-footer-tier[data-tier="desktop"] {
    color: var(--chart-3);
}

.halo-app-footer-link {
    color: var(--muted-foreground);
}

.halo-app-footer-link:hover {
    color: var(--foreground);
    text-decoration: underline;
}

/* The VersionInfo chip reuses the global .device-meta strip (12.8px); inside the
   footer it steps down to the footer's 11px so it matches the TrackTraceSolutions
   link. Scoped here on purpose — Login renders the same chip at the default size. */
.halo-app-footer .device-meta {
    font-size: 11px;
}

/* Back-to-top anchor target (#top in HeaderSidebar): keep the sticky mobile app
   bar from covering the scrolled-to position on phones. */
.halo-top-anchor {
    scroll-margin-top: 4rem;
}

/* ── Subject search modal (SubjectSearchDialog.razor) ─────────────────────── */
/* e-width modal (~34rem): overrides the package's max-w-lg (32rem) utility. */
.halo-search-dialog {
    max-width: 34rem;
}

.halo-search-field {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: 0 .75rem;
    border: 1px solid var(--border);
    border-radius: .75rem;
    background: var(--secondary);
}

/* Composite wrappers own the focus affordance for their seamless inner control
   (#151): the same thin, muted ring the standalone form controls use, so the
   pill highlights as one shape instead of the input painting its own edges. */
.halo-search-field:focus-within,
[data-slot="input-group"]:focus-within,
.halo-map-search:focus-within {
    border-color: color-mix(in srgb, var(--ring) 60%, var(--border));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--ring) 35%, transparent);
}

/* #216: the searchable combobox's search input lives in a portal outside the trigger's
   subtree, so a container `:focus-within` cannot reach it. Paint the same ring on the
   trigger while its panel is open instead. Scoped to `button` because BbCommandInput's
   own input hardcodes `aria-expanded="true"`, which a bare [role="combobox"] would match. */
button[role="combobox"][aria-expanded="true"] {
    border-color: color-mix(in srgb, var(--ring) 60%, var(--border));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--ring) 35%, transparent);
}

.halo-search-field-icon {
    flex: none;
    color: var(--muted-foreground);
}

.halo-search-field-input {
    width: 100%;
    height: 2.75rem;
    border: 0;
    background: transparent;
    color: var(--foreground);
    font-size: 13.5px;
    outline: none;
}

.halo-search-field-input::placeholder {
    color: var(--muted-foreground);
}

.halo-search-results {
    max-height: 20rem;
    overflow-y: auto;
    margin-top: .5rem;
}

.halo-search-option {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
    padding: .5rem .625rem;
    border: 0;
    border-radius: .625rem;
    background: transparent;
    color: var(--foreground);
    font-size: 12.5px;
    text-align: left;
}

.halo-search-option-text {
    display: flex;
    flex-direction: column;
    gap: .125rem;
    min-width: 0;
}

.halo-search-option-name {
    font-size: 12.5px;
    font-weight: 500;
}

.halo-search-option-sub {
    font-size: 11px;
    color: var(--muted-foreground);
}

.halo-search-option:hover,
.halo-search-option.is-active {
    background: var(--accent-soft);
}

.halo-search-empty {
    padding: .75rem .625rem;
    font-size: 12.5px;
    color: var(--muted-foreground);
}

/* Recent subjects section at the dialog bottom (issue #162): rows reuse the
   global pinned-row format (halo-pinned-item + hover trash-2) for fidelity. */
.halo-search-recent {
    border-top: 1px solid var(--border);
    margin-top: .5rem;
    padding: .5rem .625rem .25rem;
}

.halo-search-recent-title {
    margin: 0 0 .25rem;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted-foreground);
}

/* ── Inline-style consolidation (sweep batch) ─────────────────────────────
   Names verified collision-free against BlazorBlueprint.Components 4.1.0
   prebuilt CSS and app.css. Only utilities VERIFIED present in Halo's own
   Tailwind v4 build are used in markup (h-8/w-8/rounded-full/object-cover/grid/gap-*);
   everything below covers values that build does NOT ship (no `contain-*`, no
   h-20/w-20, no arbitrary grid-cols/padding utilities). */

/* `contain: inline-size` breaks fixed-width tables' min-content propagation to
   the shell flex column (internal grid scroll instead of page-level horizontal
   overflow). No prebuilt `contain-*` utility exists, so one shared class. */
.halo-contain-inline {
    contain: inline-size;
}

/* Grid loading overlay (issue #158): every BbDataGrid renders this through its LoadingTemplate so
   a refresh, sort or reorder over a grid that already has rows is never silent. */
.halo-grid-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 120px;
    padding: 1.5rem;
    opacity: 0.75;
}

/* Grid refresh overlay (issue #294): the HaloGridRefreshOverlay wrapper around every async
   grid. The LoadingTemplate swaps the rows out instead of rendering over them (see the
   probe finding on that component), so refreshes over existing rows dim the grid in place:
   the wrapper is the positioning ancestor, the dim backdrop covers the stale rows with a
   centred HaloGridLoading spinner, and the dimmed content blocks interaction while the
   request is in flight. Backdrop tint rides the card surface token so it tracks the theme. */
.halo-grid-refresh-overlay {
    position: relative;
}

/* Dimmed grid content (issue #294): pointer-events:none blocks row interaction while the
   refresh overlay shows; the absolutely positioned backdrop above swallows the rest. */
.halo-grid-refresh-overlay .halo-grid-dimmed {
    pointer-events: none;
}

/* Dim backdrop (issue #294): covers the stale rows, centres the HaloGridLoading spinner,
   and sits above sticky grid headers via its own stacking level. */
.halo-grid-dim {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--card) 60%, transparent);
}

/* 80px avatar (Profile / user details / login): h-20/w-20 are absent from the
   bundle, so a shared class keeps the three sites identical. The 32px grid
   avatars use the verified h-8 w-8 rounded-full object-cover utilities directly. */
.halo-avatar-lg {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    object-fit: cover;
}

/* Label/value grid (ConsoleUserDetails): the arbitrary
   grid-template-columns:160px 1fr form is absent from the bundle. */
.halo-details-grid {
    margin-top: 0.75rem;
    display: grid;
    grid-template-columns: 160px 1fr;
    row-gap: 0.5rem;
    column-gap: 0.75rem;
    align-items: center;
}

/* Enrolment live tables (BraceletEnrolment SampleGrid + DiscoveryGrid): the
   0.35rem/0.5rem cell padding has no bundle utility. Scoped to these tables. */
.halo-enrol-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
}

.halo-enrol-table th {
    text-align: left;
    padding: 0.35rem 0.5rem;
    border-bottom: 1px solid var(--border);
}

.halo-enrol-table td {
    padding: 0.35rem 0.5rem;
}

/* ── Toast surface (#98) ─────────────────────────────────────────────────
   The package toast card uses a translucent popover surface; in dark mode it
   lets the page footer show through. Keep the dark popover colour but make
   the toast fully opaque. */
.dark .halo-toast-viewport [role='alert'] {
    background-color: var(--popover);
    backdrop-filter: none;
}

/* ── Theme transition reveal (#113) ───────────────────────────────────────
   HaloThemeToggle drives the native View Transitions API so the theme change
   radiates from the click point. The reveal is a CSS animation on the incoming
   (new) root snapshot: declaring it in CSS keeps the clip active on the very
   first painted frame, avoiding the one-frame flash a Web-Animations animation
   attached after `transition.ready` can leave. The outgoing (old) snapshot sits
   beneath and is never animated, so the effect reads the same in both
   directions. The origin is set as `--halo-vt-x`/`--halo-vt-y` (viewport
   percentages) on <html> by halo-theme.js. */
::view-transition-old(root),
::view-transition-new(root) {
    animation: none;
    mix-blend-mode: normal;
}

::view-transition-old(root) {
    z-index: 0;
}

::view-transition-new(root) {
    z-index: 1;
}

@keyframes halo-theme-reveal {
    from {
        clip-path: circle(0% at var(--halo-vt-x, 50%) var(--halo-vt-y, 50%));
        opacity: 0.7;
    }

    to {
        clip-path: circle(150% at var(--halo-vt-x, 50%) var(--halo-vt-y, 50%));
        opacity: 1;
    }
}

::view-transition-new(root) {
    animation: halo-theme-reveal 0.5s ease-in-out both;
}

/* While a theme view transition is in flight, freeze CSS transitions so the
   captured "new" snapshot matches the settled DOM — otherwise transitioned
   colors are frozen at their pre-flip values and snap at teardown. The class
   is added/removed by halo-theme.js around startViewTransition. */
html.halo-theme-swapping,
html.halo-theme-swapping *,
html.halo-theme-swapping *::before,
html.halo-theme-swapping *::after {
    transition: none !important;
}

/* Live ops pipeline pulse (#126). The dot colour encodes the age of the newest
   ingestion packet relative to the snapshot's GeneratedAt. */
.halo-live-dot {
    display: inline-block;
    flex: none;
    width: 0.625rem;
    height: 0.625rem;
    border-radius: 9999px;
    background: var(--muted-foreground, #94a3b8);
}

.halo-live-dot.is-live {
    background: #16a34a;
    animation: halo-live-pulse 1.6s ease-out infinite;
}

.halo-live-dot.is-warn {
    background: #d97706;
    animation: halo-live-pulse 2.4s ease-out infinite;
}

.halo-live-dot.is-stale {
    background: #dc2626;
}

.halo-live-dot.is-none {
    background: var(--muted-foreground, #94a3b8);
}

@keyframes halo-live-pulse {
    0% {
        transform: scale(1);
        opacity: 1;
    }

    50% {
        transform: scale(1.35);
        opacity: 0.65;
    }

    100% {
        transform: scale(1);
        opacity: 1;
    }
}

.halo-pipeline-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.halo-pipeline-state {
    font-weight: 600;
}

.halo-pipeline-age {
    margin: 0.25rem 0 0.75rem;
    color: var(--muted-foreground, #64748b);
    font-size: 0.8125rem;
}

/* Branded error surfaces (#149): 403 / 404 / 500 rendered through
   Components/Shared/ErrorPage.razor. Centred, single-column card with a
   circular status chip so the three screens read as one family. */
.halo-error-page {
    display: flex;
    justify-content: center;
    padding: 2rem 1rem;
}

.halo-error-page-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    max-width: 32rem;
    padding: 2rem 1.5rem;
    text-align: center;
}

.halo-error-page-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 4rem;
    height: 4rem;
    border-radius: 9999px;
    background: var(--secondary, #f1f5f9);
    color: var(--primary, #7c3aed);
}

.halo-error-page-title {
    margin: 0;
}

.halo-error-page-status {
    margin: 0;
    font-variant-numeric: tabular-nums;
}

.halo-error-page-message {
    margin: 0;
}

.halo-error-page-actions {
    margin-top: 0.5rem;
}

.halo-error-page-detail {
    width: 100%;
    margin-top: 0.25rem;
    overflow-wrap: anywhere;
}

/* Mandatory legal disclaimer (per-tenant rework): /disclaimer opens a non-dismissable
   BbDialog over the shell. The provider renders its modal scrim as
   `dialog[data-state=open]::backdrop` (BlazorBlueprint 4.1.0 ships blur(2px) there and
   exposes no blur API, so this override owns the backdrop lock-in). 4px keeps the shell
   behind legible but unreadable in both themes; the dim is a translucent black that
   composes over either canvas. Unlayered on purpose, like the other package overrides
   above: it outranks the library's layered backdrop rule regardless of load order. */
dialog[data-state="open"]::backdrop {
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    background: rgba(0, 0, 0, 0.5);
}

/* The /disclaimer page body behind the modal: inert (the `inert` + aria-hidden
   attributes in Disclaimer.razor remove it from tab order and the accessibility tree)
   and visually locked — blurred and dimmed so the dialog is the only readable surface.
   pointer-events is belt-and-braces alongside inert; user-select stops the blurred text
   from being selectable. */
.halo-disclaimer-inert {
    filter: blur(3px);
    opacity: 0.55;
    pointer-events: none;
    user-select: none;
}

/* Mandatory legal disclaimer modal — small-screen rendering: the dialog body
   (DisclaimerDialogContent) constrains itself to the viewport height and scrolls
   the legal text internally, while the acceptance footer (checkbox + Accept)
   stays pinned and reachable. Without this the long text pushes the actions
   off-screen on phones (375x667) with no scroll container to recover them.
   Unlayered on purpose, like the backdrop override above. */
.halo-disclaimer-card {
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    width: 100%;
    min-height: 0;
    max-width: 100%;
    max-height: 90vh;
    max-height: 90dvh;
    overflow-x: clip;
}

.halo-disclaimer-layout {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    gap: 1rem;
    min-height: 0;
}

.halo-disclaimer-body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    gap: 1rem;
    min-height: 0;
    min-width: 0;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

.halo-disclaimer-footer {
    display: flex;
    flex-direction: column;
    flex: none;
    gap: 1rem;
    min-width: 0;
    border-top: 1px solid var(--border);
    padding-top: 1rem;
}

/* ── Dashboard typography standardisation (issue #325) ─────────────────
   HelperText register: `halo-form-help` is one step below the 14px field label
   (12.5px — the same step as halo-pinned-name/halo-empty-title), muted, with a
   0.3em leading indent so the guidance reads as belonging to its control. Two
   application paths share one declaration block:
   - hand-rolled guidance carries `halo-form-help` directly;
   - every BbFormField* HelperText renders as `div[data-slot="field-description"]`
     (the wrappers expose no DescriptionClass hook), so the slot selector paints
     them identically.
   Unlayered on purpose: outranks the library's `@layer` bb-utilities pair
   (bb:text-sm bb:text-muted-foreground) regardless of load order. */
.halo-form-help,
div[data-slot="field-description"] {
    margin-left: 0.3em;
    color: var(--muted-foreground);
    font-size: 12.5px;
    line-height: 1.45;
}

/* ── Permission matrix typography (issue #325) ─────────────────────────
   The role/permission matrices on /profile (inline table) and /users/{id} (the
   shared RolePermissionPicker) rendered their Entity/column headers with bare
   font-semibold/font-medium and no size — 16px browser default, oversize next
   to the 14px card titles. Both tables carry `halo-perm-matrix`, so one rule
   pair pins the header row and the first column to the 12.5px register (the
   same step as halo-form-help/halo-empty-title); weights stay in the markup.
   Unlayered on purpose: outranks the library's `@layer` utilities regardless
   of load order. */
.halo-perm-matrix thead th[scope="col"] {
    font-size: 12.5px;
}

.halo-perm-matrix tbody th[scope="row"] {
    font-size: 12.5px;
}

/* ── Data-grid density (issue #325) ───────────────────────────────────────
   Slightly tighter body/header type across dashboard data grids (conservative
   first step): body cells step from the library's p-4/text-sm to 0.625rem /
   0.75rem gutters at 13px — the same 0.8125rem body size as Home's
   halo-recent-table — and headers from h-12 to 2.5rem at 12.5px. Scoped to
   `table[role="grid"]`, which only BbDataGrid renders, so the homepage tables
   (halo-recent-table: plain thead th, no roles) and the permission matrices
   (scope=col/row, no roles) are untouched by construction — no page carries an
   opt-out class because none is needed. Unlayered on purpose: outranks the
   library's `@layer` bb-utilities regardless of load order. */
table[role="grid"] > thead th[role="columnheader"] {
    height: 2.5rem;
    font-size: 12.5px;
}

table[role="grid"] > tbody td[role="gridcell"] {
    padding: 0.625rem 0.75rem;
    font-size: 0.8125rem;
}
