/*
 * Portal component theme — light/dark via .portal-page-themed[data-bs-theme].
 * Opt-in on pages with .portal-page-themed (QA is first adopter).
 * Liquid glass, two material tiers: translucent card surfaces (NO blur) and
 * blurred overlay shells (modals/menus/⌘K). Neutral grays with a purple
 * whisper + NHRL brand purple (primary/badges) + teal accents. No magenta
 * CTAs. Degrade gates live at the END of this file — keep them last.
 */

/* ── Dark tokens (global via html[data-bs-theme]; :has kept for per-page sync) ── */
html[data-bs-theme="dark"] body,
body:has(.portal-page-themed[data-bs-theme="dark"]),
/* A NESTED scope: an element that paints in a theme the page is not in. The
   Appearance menu's swatches are the first consumer — each shows the real
   surface and keyline of the theme it selects, rather than an approximation
   somebody keeps in step by hand. (0,2,0) beats the page rules above on the
   element that wears it, which is the point. The gallery's theme-split section
   extends this to the degrade gates; the swatches need only the tokens. */
.portal-theme-scope[data-bs-theme="dark"] {
    /* Liquid-glass card material: translucent ink, NO per-element blur.
       Legibility math: worst-case backdrop is canvas #100f18 under the
       violet aurora peak (rgba(94,62,197,.15)) ≈ rgb(26,20,47); this
       surface at 0.72 alpha composites to ≈ rgb(35,32,50) — text below
       reads ≈ 12.9:1, muted ≈ 6.4:1. Sticky/pinned elements must use the
       -opaque twin (or --bs-body-bg), never this.

       Retuned 2026-08-13 (v3, "sharper dark"). It used to be
       rgba(28,27,36,0.78) over a #131218 canvas, which composited only
       ~7/255 lighter than the canvas it sat on — so a panel was invisible
       except for its hairline, and the hairline was WHITE. The whole app
       read as grey lines drawn on grey. The pane is lighter and thinner
       now (more of the aurora comes through, which is what makes glass
       look like glass), and the canvas went near-black underneath it, so
       the surface separates on its own VALUE and the edge can go dark. */
    --portal-c-surface: rgba(38, 36, 51, 0.72);
    /* Recomputed as the real composite of the glass over the canvas — the old
       #201e2a predated the canvas going near-black, so flipping glass off
       moved every card by a couple of values. */
    --portal-c-surface-opaque: #1f1d2a;

    /* The SEE-THROUGH twin — the system bar's tint, one rung thinner than
       the card surface above. The launcher cards and the home page's floor
       group re-point --portal-c-surface at this so the canvas comes through
       them the way it comes through the bar at the bottom of the page.

       It is a token in the ladder rather than a literal in those two rules
       for exactly one reason: a THEME must be able to answer it. Matrix
       redefines the whole --portal-c-* set and knows nothing about the
       chrome tint, so a literal here left its cards wearing dark theme's
       purple-grey glass in a green portal — the wrong material, not merely
       the wrong shade.

       Built from --portal-chrome-opacity-*, which the three glass gates at
       the end of this file already re-point: the see-through surface goes
       near-opaque with everything else, with nothing local to keep in step. */
    --portal-c-surface-seethrough: rgba(31, 29, 44, var(--portal-chrome-opacity-dark, 0.2));

    /* ── The surface ladder ──────────────────────────────────────────
       canvas → sunken → surface → raised → overlay.

       The middle two existed; the ends did not, so "this thing is above that
       thing" was expressed by a shadow — and in dark theme a black shadow over
       rgb(13,12,20) can move a pixel about 8/255, with the rest-to-hover step
       around 1 or 2. Elevation was, physically, invisible. Every lift a user
       actually perceived was the 2px translate.

       -sunken is what a panel head or a table head sits in; -raised is what a
       hovered or selected or floating child rises to. Both have opaque twins
       because all five degrade scopes re-point the translucent ones, and the
       twins stay HEX: the gates assert /^rgb\(/ and Chromium serialises
       color-mix as color(srgb …). */
    --portal-c-surface-sunken: rgba(0, 0, 0, 0.32);
    --portal-c-surface-raised: rgba(52, 49, 68, 0.76);
    --portal-c-surface-raised-opaque: #2b2839;
    /* sunken composited over surface-opaque — the twin the degrade gates
       re-point (found MISSING by the surface-ladder probe, 2026-08-24). */
    --portal-c-surface-sunken-opaque: #15141d;
    /* The hover flood. An inset spread rather than a background swap, so the
       surface underneath is never replaced — replacing it is how a hovered
       card stops being a card. */
    --portal-c-surface-lift: rgba(255, 255, 255, 0.04);
    /* ── Edge light ──────────────────────────────────────────────────
       The glass is EDGES, not blur (cards never get backdrop-filter).
       Four primitives compose into three tiered bundles; components read
       the bundles, never the primitives, so the whole material re-tunes
       from here. Light source is top, consistently, everywhere.
         specular        top 1px catch
         specular-strong same, for dialog/hero tiers
         edge-shade      bottom 1px — grounds the surface against the canvas
         edge-bloom      soft interior pool under the top edge; the negative
                         spread pulls it off the side walls, so it reads as
                         light gathering in the top corners rather than a band.
       None of these are gate-managed: on a degraded (opaque) surface the
       edges still draw, which is exactly the pre-glass Apple-soft look. */
    --portal-c-specular: rgba(255, 255, 255, 0.14);
    --portal-c-specular-strong: rgba(255, 255, 255, 0.22);
    --portal-c-edge-shade: rgba(0, 0, 0, 0.5);
    --portal-c-edge-bloom: rgba(255, 255, 255, 0.06);
    --portal-c-edge-card: inset 0 1px 0 var(--portal-c-specular),
                          inset 0 -1px 0 var(--portal-c-edge-shade);

    /* ── Interaction: the rim light ───────────────────────────────────
       A hover in dark theme could only ever be a translate, because a black
       shadow over a near-black canvas moves a pixel by about 8/255 and the
       rest-to-hover step by 1 or 2. Elevation was, physically, invisible.

       So the SPECULAR steps instead: the top catch brightens and the bottom
       shade deepens, which reads as the edge catching more light because the
       surface came up. That is visible on a venue panel; a shadow delta of
       2/255 is not.

       Same geometry as --portal-c-edge-card on purpose — inset 0 1px 0 and
       inset 0 -1px 0 — because tools-grid.spec.js resolves that bundle through
       a probe and asserts the card composes it. Only the colours step. */
    --portal-c-specular-raised: rgba(255, 255, 255, 0.22);
    --portal-c-specular-pressed: rgba(255, 255, 255, 0.08);
    --portal-c-edge-shade-raised: rgba(0, 0, 0, 0.6);
    --portal-c-edge-card-raised: inset 0 1px 0 var(--portal-c-specular-raised),
                                 inset 0 -1px 0 var(--portal-c-edge-shade-raised);
    --portal-c-edge-card-pressed: inset 0 1px 0 var(--portal-c-specular-pressed),
                                  inset 0 -1px 0 var(--portal-c-edge-shade-raised);
    /* Control tier: a chip is not a card, so its bottom shade is half strength
       — a full one reads as a card that shrank. */
    --portal-c-edge-chip: inset 0 1px 0 var(--portal-c-specular),
                          inset 0 -1px 0 color-mix(in srgb, var(--portal-c-edge-shade) 50%, transparent);
    /* Pointer-tracked specular, hero surfaces only. Never a stat grid: a light
       following the pointer across a polled surface is noise. */
    --portal-c-specular-pointer: rgba(255, 255, 255, 0.09);
    --portal-c-edge-dialog: inset 0 1px 0 var(--portal-c-specular-strong),
                            inset 0 -1px 0 var(--portal-c-edge-shade),
                            inset 0 14px 28px -20px var(--portal-c-edge-bloom);
    --portal-c-edge-hero: inset 0 1px 0 var(--portal-c-specular-strong),
                          inset 0 -1px 0 var(--portal-c-edge-shade),
                          inset 0 22px 44px -26px var(--portal-c-edge-bloom);
    /* Interior sheen — a background-IMAGE (never a backdrop-filter), so it
       costs nothing and survives every degrade gate. */
    --portal-c-surface-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 22%);
    /* Specular baked into solid button fills. One primitive so the whole
       control family re-tunes together. */
    --portal-c-btn-specular: rgba(255, 255, 255, 0.18);
    /* The ladder's name for this is -sunken; the old name stays as an alias
       so the ~20 consumers do not all move in this commit. */
    --portal-c-surface-header: var(--portal-c-surface-sunken);
    /* ── The keyline is BLACK in dark theme ──────────────────────────
       Both of these were white tints (0.10 / 0.05) until 2026-08-13, and
       that is the single change that made the app read grey: a white
       hairline over the card surface computes to ~rgb(49,48,54) — a light
       grey rectangle drawn around every panel, on a near-black page. The
       physical model was backwards. A pane of glass lying on a black
       surface does not have a bright outline; it has a dark seam where it
       meets the surface, and a bright catch on its top edge where the
       light hits. So the border goes to black and --portal-c-specular
       (above) does the lighting — border first, specular 1px inside it,
       which is the machined double edge.

       Consequence worth knowing before retuning: on the canvas itself the
       keyline composites to about the canvas value, so a card edge reads
       as a clean CUT rather than a drawn line. Inside a surface (fields, rows,
       table heads, chips) it reads as a real dark rule. Both are wanted.
       This is also why --portal-c-surface had to get lighter in the same
       pass: a dark keyline needs a surface bright enough to cut into. */
    --portal-c-border: rgba(0, 0, 0, 0.72);
    --portal-c-border-subtle: rgba(0, 0, 0, 0.45);
    /* ── …and the CONTROL edge is still a light rim ──────────────────
       The keyline above is for SURFACES: a pane bounded by a dark seam,
       which works because a surface is always lighter than what it lies
       on. A control is not a surface — it is an object that has to read
       against whatever happens to be behind it, and that includes the bare
       canvas, where a dark rim on a dark fill is a void rather than a box.
       That is not hypothetical: it shipped for an afternoon, and the way
       it showed up was the filter field on /tools bleeding into the page
       while the button beside it (already on the outline token) looked
       right.

       So the two edges are deliberately opposite, and which one a
       component takes is decided by what it IS, not where it currently
       sits: cards, groups, panels, table heads, dividers and rows take
       --portal-c-border; fields, pickers, pagination chips and standalone
       pills take this. --portal-c-btn-outline-border is the same idea at
       button strength and predates the split.

       In LIGHT theme both resolve to the same ink — the distinction only
       exists because dark inverted. Keep the light twin pointed at
       --portal-c-border so light theme cannot drift out of this pass. */
    --portal-c-edge-control: rgba(255, 255, 255, 0.14);
    /* The quiet twin, for a control that is present but not offering itself
       (a disabled pagination chip). Still a rim — a disabled control still
       has to have a shape — just a dimmer one. */
    --portal-c-edge-control-subtle: rgba(255, 255, 255, 0.08);
    --portal-c-text: #e8e7ee;
    --portal-c-text-muted: #a5a2b3;
    /* Apple-soft elevation: larger blur, lower opacity; hairlines still do
       the separating work, the shadow grounds.
       One altitude scale, one top key light. The ambient layer tightens and
       the key layer grows as things rise: rest 1/8 → raised 2/12 → overlay
       4/24 → hero 8/32. Hover and press compose WITH an edge bundle — never
       replace the whole box-shadow, or the specular dies on interaction.

       Alphas went up with the v3 canvas: a 0.22 shadow on a near-black
       page has almost no room left to darken, so the old scale had
       quietly stopped grounding anything. The keyline does the contact
       edge now and these do the altitude. */
    --portal-c-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.38);
    --portal-c-shadow-raised: 0 2px 4px rgba(0, 0, 0, 0.42), 0 12px 32px rgba(0, 0, 0, 0.48);
    --portal-c-shadow-pressed: 0 1px 1px rgba(0, 0, 0, 0.4), 0 3px 8px rgba(0, 0, 0, 0.3);
    --portal-c-brand: rgb(41, 28, 94);
    /* NHRL off-white, the brand's fourth colour (site.css keeps it only as
       deprecated utility classes). Theme-invariant, like the purple: a brand
       constant, not a surface that flips with the theme. */
    --portal-c-brand-offwhite: #f4f4f4;
    /* Teal is the ONLY interactive accent. Every tint derives from the ink
       by color-mix, so the family can never drift out of hue again (the
       light theme had two different teals before this). */
    --portal-c-accent: #00dad8;
    --portal-c-accent-muted: color-mix(in srgb, var(--portal-c-accent) 12%, transparent);
    --portal-c-accent-border: color-mix(in srgb, var(--portal-c-accent) 30%, transparent);
    /* Focus: a crisp 1px accent rim inside a soft wash — machined, not the
       Bootstrap glow. One token so every field/control focuses identically. */
    --portal-c-focus-ring: 0 0 0 1px var(--portal-c-accent-border), 0 0 0 0.25rem var(--portal-c-accent-muted);
    /* Text/icons sitting ON an accent fill. Dark theme fills are bright teal,
       so ink is near-black; the light theme's accent is dark, so ink is white. */
    --portal-c-on-accent: #00201f;
    --portal-c-tab-track: rgba(0, 0, 0, 0.4);
    /* The track is a WELL: a recessed inset makes the raised active pill read
       as an iOS segmented control rather than two flat rectangles. */
    --portal-c-tab-track-inset: inset 0 1px 3px rgba(0, 0, 0, 0.45);
    --portal-c-tab-hover: rgba(255, 255, 255, 0.06);
    /* Tracks the surface: the pill has to stay a clear step above the glass
       it sits on, and the glass moved up in v3. */
    --portal-c-tab-active-bg: #3f3c4e;
    --portal-c-tab-active-border: rgba(0, 0, 0, 0.5);
    --portal-c-tab-active-shadow: 0 1px 3px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.14);
    --portal-c-tab-active-color: #f8f9fa;
    --portal-c-tab-inactive-color: rgba(255, 255, 255, 0.55);
    /* btn-primary stays desaturated NHRL violet — with neutral surfaces it is
       the last brand voice on controls. */
    --portal-c-btn-primary-bg: #453c66;
    --portal-c-btn-primary-bg-hover: #574a80;
    --portal-c-btn-primary-border: rgba(255, 255, 255, 0.1);
    --portal-c-btn-primary-border-hover: rgba(0, 218, 216, 0.35);
    --portal-c-btn-primary-shadow: inset 0 1px 0 var(--portal-c-btn-specular), 0 1px 2px rgba(0, 0, 0, 0.25), 0 2px 6px rgba(0, 0, 0, 0.15);
    --portal-c-btn-primary-shadow-hover: inset 0 1px 0 var(--portal-c-btn-specular), 0 2px 4px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.15);
    --portal-c-btn-danger-bg: #c92a2a;
    --portal-c-btn-danger-bg-hover: #e03131;
    /* Depth (v3 §2.7): top-lit ±8%% luminance over the flat fill. The grads
       are var-based, so this same text serves both themes; gate 4 carries
       the resolved literal twins for engines without color-mix. Press goes
       DARKER (+scale, +pressed shadow) — depth inverts, like a real key. */
    --portal-c-btn-primary-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-primary-bg) 86%, #fff) 0%, var(--portal-c-btn-primary-bg) 50%, color-mix(in srgb, var(--portal-c-btn-primary-bg) 88%, #000) 100%);
    --portal-c-btn-primary-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-primary-bg-hover) 86%, #fff) 0%, var(--portal-c-btn-primary-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-primary-bg-hover) 88%, #000) 100%);
    --portal-c-btn-primary-bg-active: color-mix(in srgb, var(--portal-c-btn-primary-bg) 90%, #000);
    --portal-c-btn-danger-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-danger-bg) 90%, #fff) 0%, var(--portal-c-btn-danger-bg) 50%, color-mix(in srgb, var(--portal-c-btn-danger-bg) 92%, #000) 100%);
    --portal-c-btn-danger-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-danger-bg-hover) 90%, #fff) 0%, var(--portal-c-btn-danger-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-danger-bg-hover) 92%, #000) 100%);
    --portal-c-btn-success-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-success-bg) 90%, #fff) 0%, var(--portal-c-btn-success-bg) 50%, color-mix(in srgb, var(--portal-c-btn-success-bg) 92%, #000) 100%);
    --portal-c-btn-success-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-success-bg-hover) 90%, #fff) 0%, var(--portal-c-btn-success-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-success-bg-hover) 92%, #000) 100%);
    --portal-c-btn-warning-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-warning-bg) 90%, #fff) 0%, var(--portal-c-btn-warning-bg) 50%, color-mix(in srgb, var(--portal-c-btn-warning-bg) 92%, #000) 100%);
    --portal-c-btn-warning-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-warning-bg-hover) 90%, #fff) 0%, var(--portal-c-btn-warning-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-warning-bg-hover) 92%, #000) 100%);
    --portal-c-btn-danger-border: rgba(255, 255, 255, 0.08);
    --portal-c-btn-danger-border-hover: rgba(255, 255, 255, 0.14);
    --portal-c-btn-danger-shadow: inset 0 1px 0 var(--portal-c-btn-specular), 0 1px 2px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(201, 42, 42, 0.2);
    --portal-c-btn-danger-shadow-hover: inset 0 1px 0 var(--portal-c-btn-specular), 0 2px 4px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(201, 42, 42, 0.25);
    --portal-c-btn-danger-disabled-bg: rgba(201, 42, 42, 0.35);
    --portal-c-btn-outline-border: var(--portal-c-edge-control);
    /* A whisper of fill so an outline button reads as a glass chip rather
       than a wireframe — one step quieter than a solid, never inset. */
    --portal-c-btn-outline-bg: rgba(255, 255, 255, 0.04);
    --portal-c-btn-outline-color: rgba(255, 255, 255, 0.85);
    --portal-c-btn-outline-hover-bg: rgba(255, 255, 255, 0.06);
    --portal-c-btn-outline-hover-color: #fff;
    --portal-c-btn-outline-hover-border: rgba(255, 255, 255, 0.28);
    /* Badges get ONE inset and no drop shadow — a card-sized shadow on a
       2.5rem chip reads fuzzy (learned the hard way on .portal-icon-badge). */
    --portal-c-badge-inset: inset 0 1px 0 rgba(255, 255, 255, 0.07);
    --portal-c-badge-brand-bg: rgb(41, 28, 94);
    --portal-c-badge-brand-border: rgba(255, 255, 255, 0.1);
    --portal-c-badge-live-bg: var(--portal-c-accent-muted);
    --portal-c-badge-live-color: var(--portal-c-accent);
    --portal-c-badge-muted-bg: rgba(255, 255, 255, 0.07);
    --portal-c-badge-muted-color: rgba(255, 255, 255, 0.7);
    --portal-c-badge-running-bg: color-mix(in srgb, var(--portal-c-accent) 14%, transparent);
    --portal-c-badge-running-color: var(--portal-c-accent);
    --portal-c-badge-success-bg: rgba(47, 191, 113, 0.14);
    --portal-c-badge-success-color: #45d381;
    --portal-c-badge-warning-bg: rgba(245, 179, 66, 0.14);
    --portal-c-badge-warning-color: #f5b342;
    --portal-c-badge-danger-bg: rgba(224, 73, 73, 0.16);
    --portal-c-badge-danger-color: #ff7d7d;
    /* Weight class (3 / 12 / 30 / freestyle). NEUTRAL HERE, ON PURPOSE, and
       that is the whole design: weight colour is BrettZone's broadcast
       identity, so the app kit owns the HOOK and the sport theme owns the
       COLOUR. Under dark, light and matrix a weight chip is an ordinary
       muted chip and a weight stripe is an ordinary keyline — which is the
       correct outcome for a theme that is not BrettZone, not a shortfall.
       portal-sport-theme.css re-points these four pairs at --bz-w3/-w12/
       -w30/-w-fs; nothing else in the tree should. */
    --portal-c-w3-bg: var(--portal-c-badge-muted-bg);
    --portal-c-w3-color: var(--portal-c-badge-muted-color);
    --portal-c-w12-bg: var(--portal-c-badge-muted-bg);
    --portal-c-w12-color: var(--portal-c-badge-muted-color);
    --portal-c-w30-bg: var(--portal-c-badge-muted-bg);
    --portal-c-w30-color: var(--portal-c-badge-muted-color);
    --portal-c-wfs-bg: var(--portal-c-badge-muted-bg);
    --portal-c-wfs-color: var(--portal-c-badge-muted-color);
    --portal-c-w-stripe: var(--portal-c-border-subtle);
    /* Corner colours (v3 §2.8) — live-bracket's recipe, promoted. The inks
       are readable mixes toward the text ink; soft/border are the wash and
       keyline weights. mission-control's literal pair was the bug that
       motivated this. */
    --portal-c-corner-pink-ink: color-mix(in srgb, var(--portal-c-corner-pink) 62%, var(--portal-c-text));
    --portal-c-corner-blue-ink: color-mix(in srgb, var(--portal-c-corner-blue) 42%, var(--portal-c-text));
    --portal-c-corner-pink-soft: color-mix(in srgb, var(--portal-c-corner-pink) 14%, transparent);
    --portal-c-corner-blue-soft: color-mix(in srgb, var(--portal-c-corner-blue) 14%, transparent);
    --portal-c-corner-pink-border: color-mix(in srgb, var(--portal-c-corner-pink) 45%, transparent);
    --portal-c-corner-blue-border: color-mix(in srgb, var(--portal-c-corner-blue) 45%, transparent);
    /* Impersonation is a MODE, not a message: the same amber in both themes,
       tokenised so the banner and its controls retune together. */
    --portal-c-impersonation-bg: #b45309;
    --portal-c-impersonation-ink: #fff;
    /* ── Validation family (v3 §2.7) — the states that did not exist ──
       Border + ring + glyph per outcome, derived from the badge inks so the
       page's one red stays one red. The glyphs are Bootstrap's own paths
       with the fill re-issued inside the URI (the accordion-chevron trap:
       a fill cannot be themed from outside a data URI). */
    --portal-c-input-border-invalid: color-mix(in srgb, var(--portal-c-badge-danger-color) 55%, transparent);
    --portal-c-input-ring-invalid: 0 0 0 1px color-mix(in srgb, var(--portal-c-badge-danger-color) 45%, transparent), 0 0 0 0.25rem color-mix(in srgb, var(--portal-c-badge-danger-color) 14%, transparent);
    --portal-c-input-glyph-invalid: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff7d7d'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff7d7d' stroke='none'/%3e%3c/svg%3e");
    --portal-c-input-border-valid: color-mix(in srgb, var(--portal-c-badge-success-color) 55%, transparent);
    --portal-c-input-ring-valid: 0 0 0 1px color-mix(in srgb, var(--portal-c-badge-success-color) 45%, transparent), 0 0 0 0.25rem color-mix(in srgb, var(--portal-c-badge-success-color) 14%, transparent);
    --portal-c-input-glyph-valid: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8' height='8'%3e%3cpath fill='%2345d381' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
    --portal-c-btn-success-bg: #2f9e44;
    --portal-c-btn-success-bg-hover: #37b24c;
    --portal-c-btn-success-border: rgba(255, 255, 255, 0.08);
    --portal-c-btn-success-border-hover: rgba(255, 255, 255, 0.14);
    --portal-c-btn-warning-bg: #d9822b;
    --portal-c-btn-warning-bg-hover: #ec8f30;
    --portal-c-btn-warning-border: rgba(255, 255, 255, 0.08);
    --portal-c-btn-warning-border-hover: rgba(255, 255, 255, 0.14);
    --portal-c-progress-bg: rgba(255, 255, 255, 0.06);
    --portal-c-progress-bar-bg: var(--portal-c-accent);
    --portal-c-progress-bar-stripe: rgba(0, 0, 0, 0.15);
    /* Skeletons were the last raw rgba in the kit — tokenised so the loading
       state tracks the surface it stands in for. */
    --portal-c-skeleton-bg: rgba(255, 255, 255, 0.06);
    --portal-c-skeleton-sheen: rgba(255, 255, 255, 0.05);
    /* Info = brand violet (aurora family) — teal stays "live", green "good".
       Kills raw Bootstrap cyan text-bg-info, the loudest off-palette color. */
    --portal-c-badge-info-bg: rgba(143, 126, 231, 0.14);
    --portal-c-badge-info-color: #a99cf0;
    --portal-c-status-idle: var(--portal-c-text-muted);
    /* Interactive row states — background only, never transforms. */
    --portal-c-row-hover: rgba(255, 255, 255, 0.04);
    /* Strong enough to carry selection on its own — the 2px accent stripe
       was retired with the card stripes (it drew at every cell edge and
       fought the glass), so the tint is the whole affordance now. */
    --portal-c-row-selected: color-mix(in srgb, var(--portal-c-accent) 12%, transparent);
    /* Overlay elevation (dropdowns, modals, toasts) — cards keep --portal-c-shadow.
       -menu is the chrome-nested menu tier (sidebar/topbar dropdowns), -hero
       the ⌘K/login tier. Both replace literals that used to bypass this scale. */
    --portal-c-elev-overlay: 0 4px 16px rgba(0, 0, 0, 0.45), 0 24px 64px rgba(0, 0, 0, 0.6);
    --portal-c-elev-menu: 0 12px 40px rgba(0, 0, 0, 0.6);
    --portal-c-elev-hero: 0 8px 24px rgba(0, 0, 0, 0.45), 0 32px 88px rgba(0, 0, 0, 0.62);
    /* Form fields — recessed against the surface (same tone the alertify
       prompt inputs already ship). Deeper with v3: the surface above them
       moved up ~9/255, so holding the old 0.25 would have shrunk the step
       between "surface" and "well" that makes a field look cut in. */
    --portal-c-input-bg: rgba(0, 0, 0, 0.38);
    /* A field is a well cut INTO the surface — the inset shade is the top
       lip catching the same top light everything else does. */
    --portal-c-input-inset: inset 0 1px 2px rgba(0, 0, 0, 0.35);
    /* Dark fields stay recessed on focus: a bright fill would flare in a
       dark venue. The light theme lifts instead (see its twin). */
    --portal-c-input-bg-focus: var(--portal-c-input-bg);
    /* Was 0.35, which composited to about 2.9:1 against the field — under the
       4.5:1 the contract promises, on the one piece of text that tells you
       what a field wants. */
    --portal-c-input-placeholder: rgba(232, 231, 238, 0.62);
    /* Hover wash on chrome controls (sidebar toggle, bell) — chrome theme
       tracks the global theme, so this may flip with it. */
    --portal-c-chrome-hover: rgba(255, 255, 255, 0.08);
    /* Overlay glass shells (modals, alertify, ⌘K, in-page menus): translucent,
       paired with --portal-blur-overlay. The -opaque twin is the nested-glass
       (chrome/modal-descendant menus) and degrade fallback. */
    --portal-c-overlay: rgba(42, 40, 56, 0.72);
    --portal-c-overlay-opaque: #222030;
    /* Full-viewport scrims (AI drawer backdrop). Deliberately NOT re-pointed
       by the degrade gates — a scrim that degrades to opaque would blank the
       whole page behind a drawer. */
    --portal-c-scrim: rgba(0, 0, 0, 0.5);
    --portal-c-kbd: rgba(255, 255, 255, 0.35);
}

/* ── Light tokens (global via html[data-bs-theme]; :has kept for per-page sync) ── */
html[data-bs-theme="light"] body,
body:has(.portal-page-themed[data-bs-theme="light"]),
/* A NESTED scope: an element that paints in a theme the page is not in. The
   Appearance menu's swatches are the first consumer — each shows the real
   surface and keyline of the theme it selects, rather than an approximation
   somebody keeps in step by hand. (0,2,0) beats the page rules above on the
   element that wears it, which is the point. The gallery's theme-split section
   extends this to the degrade gates; the swatches need only the tokens. */
.portal-theme-scope[data-bs-theme="light"] {
    /* White glass at 0.68 over the (now faintly tinted) #f4f4f6 canvas. It
       dropped from 0.78 on purpose: at 0.78 over a flat white page the glass
       had nothing to be glass OF, and the white specular edge was invisible
       against its own surface. Body text still computes ≈16:1. */
    --portal-c-surface: rgba(255, 255, 255, 0.68);
    /* Not pure white: the composite of 68% white over #f4f4f6 is #fcfcfc, and
       a card that goes brighter when glass turns off is a card that moved. */
    --portal-c-surface-opaque: #fcfcfc;

    /* The see-through twin — see the dark block. On paper the bar is 0.85
       rather than 0.2: white glass over a white canvas needs the body, and
       the worked contrast figures above are what bound how thin it can go.

       WHITE, not the canvas colour. --portal-c-chrome-surface-light says
       rgba(244,244,246,…), but the light chrome rule in portal-theme.css does
       not use it — the bar paints white — and the canvas colour at 0.85 over
       the canvas composites back to the canvas, which is a card defined by
       nothing but its hairline. This is the value the bar actually wears. */
    --portal-c-surface-seethrough: rgba(255, 255, 255, var(--portal-chrome-opacity-light, 0.85));

    /* The ladder's light twin — see the dark block for what each rung is for. */
    --portal-c-surface-sunken: rgba(28, 25, 54, 0.04);
    --portal-c-surface-raised: rgba(255, 255, 255, 0.86);
    --portal-c-surface-raised-opaque: #fefefe;
    --portal-c-surface-sunken-opaque: #f3f3f4;
    /* On paper the lift is MORE white, not less shade. */
    --portal-c-surface-lift: rgba(255, 255, 255, 0.5);
    /* Light mode inverts the roles of the edge pair: the white top catch is
       the quiet one and the BOTTOM SHADE is what separates glass from canvas
       — the same way a real pane sits on a white table. */
    --portal-c-specular: rgba(255, 255, 255, 0.85);
    --portal-c-specular-strong: rgba(255, 255, 255, 0.95);
    --portal-c-edge-shade: rgba(28, 25, 54, 0.07);
    --portal-c-edge-bloom: rgba(255, 255, 255, 0.55);
    --portal-c-edge-card: inset 0 1px 0 var(--portal-c-specular),
                          inset 0 -1px 0 var(--portal-c-edge-shade);

    /* The light twin — see the dark block. On paper the catch is already
       near-white, so the step is mostly in the shade: the surface reads as
       lifting off the page rather than lighting up. */
    --portal-c-specular-raised: rgba(255, 255, 255, 0.95);
    --portal-c-specular-pressed: rgba(255, 255, 255, 0.6);
    --portal-c-edge-shade-raised: rgba(28, 25, 54, 0.11);
    --portal-c-edge-card-raised: inset 0 1px 0 var(--portal-c-specular-raised),
                                 inset 0 -1px 0 var(--portal-c-edge-shade-raised);
    --portal-c-edge-card-pressed: inset 0 1px 0 var(--portal-c-specular-pressed),
                                  inset 0 -1px 0 var(--portal-c-edge-shade-raised);
    --portal-c-edge-chip: inset 0 1px 0 var(--portal-c-specular),
                          inset 0 -1px 0 color-mix(in srgb, var(--portal-c-edge-shade) 50%, transparent);
    --portal-c-specular-pointer: rgba(94, 62, 197, 0.06);
    --portal-c-edge-dialog: inset 0 1px 0 var(--portal-c-specular-strong),
                            inset 0 -1px 0 var(--portal-c-edge-shade),
                            inset 0 14px 28px -20px var(--portal-c-edge-bloom);
    --portal-c-edge-hero: inset 0 1px 0 var(--portal-c-specular-strong),
                          inset 0 -1px 0 var(--portal-c-edge-shade),
                          inset 0 22px 44px -26px var(--portal-c-edge-bloom);
    --portal-c-surface-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.65) 0%, rgba(255, 255, 255, 0) 26%);
    --portal-c-btn-specular: rgba(255, 255, 255, 0.16);
    --portal-c-surface-header: var(--portal-c-surface-sunken);
    /* Hairlines do more work on the lower-alpha surface, so they derive from
       one ink at two strengths instead of drifting apart as literals. */
    --portal-c-border: color-mix(in srgb, rgb(28, 25, 54) 14%, transparent);
    --portal-c-border-subtle: color-mix(in srgb, rgb(28, 25, 54) 8%, transparent);
    /* Light theme never inverted, so surface edge and control edge are the
       same ink here. Pointed at the token rather than repeated, so this can
       never drift into a second light hairline — see the dark twin for why
       the split exists at all. */
    --portal-c-edge-control: var(--portal-c-border);
    --portal-c-edge-control-subtle: var(--portal-c-border-subtle);
    --portal-c-text: #1c1b24;
    --portal-c-text-muted: #676473;
    /* Apple-soft: the old 1px ring is retired — the hairline border owns
       that job on glass. */
    --portal-c-shadow: 0 1px 2px rgba(24, 20, 45, 0.06), 0 8px 24px rgba(24, 20, 45, 0.08);
    --portal-c-shadow-raised: 0 2px 4px rgba(24, 20, 45, 0.07), 0 12px 32px rgba(24, 20, 45, 0.11);
    --portal-c-shadow-pressed: 0 1px 1px rgba(24, 20, 45, 0.06), 0 3px 8px rgba(24, 20, 45, 0.06);
    --portal-c-brand: rgb(41, 28, 94);
    --portal-c-brand-offwhite: #f4f4f4; /* brand constant — see dark block */
    /* One teal, derived. This used to be two: ink #0a7c7a with every tint
       mixed from rgb(0,168,166) — a different hue, so washes never quite
       matched the text they surrounded. 14% keeps the old visible weight. */
    --portal-c-accent: #0a7c7a;
    --portal-c-accent-muted: color-mix(in srgb, var(--portal-c-accent) 14%, transparent);
    --portal-c-accent-border: color-mix(in srgb, var(--portal-c-accent) 40%, transparent);
    --portal-c-focus-ring: 0 0 0 1px var(--portal-c-accent-border), 0 0 0 0.25rem var(--portal-c-accent-muted);
    --portal-c-on-accent: #ffffff;
    --portal-c-tab-track: #ececef;
    --portal-c-tab-track-inset: inset 0 1px 2px rgba(28, 25, 54, 0.06);
    --portal-c-tab-hover: rgba(28, 25, 54, 0.05);
    --portal-c-tab-active-bg: #fff;
    --portal-c-tab-active-border: rgba(28, 25, 54, 0.10);
    --portal-c-tab-active-shadow: 0 1px 3px rgba(24, 20, 45, 0.08), 0 2px 8px rgba(24, 20, 45, 0.05);
    --portal-c-tab-active-color: rgb(41, 28, 94);
    --portal-c-tab-inactive-color: #676473;
    --portal-c-btn-primary-bg: rgb(41, 28, 94);
    --portal-c-btn-primary-bg-hover: #3a2a80;
    --portal-c-btn-primary-border: rgb(41, 28, 94);
    --portal-c-btn-primary-border-hover: #3a2a80;
    --portal-c-btn-primary-shadow: inset 0 1px 0 var(--portal-c-btn-specular), 0 1px 2px rgba(15, 23, 42, 0.12), 0 2px 6px rgba(15, 23, 42, 0.08);
    --portal-c-btn-primary-shadow-hover: inset 0 1px 0 var(--portal-c-btn-specular), 0 2px 4px rgba(15, 23, 42, 0.14), 0 4px 12px rgba(15, 23, 42, 0.1);
    --portal-c-btn-danger-bg: #c92a2a;
    --portal-c-btn-danger-bg-hover: #e03131;
    /* Depth (v3 §2.7): top-lit ±8%% luminance over the flat fill. The grads
       are var-based, so this same text serves both themes; gate 4 carries
       the resolved literal twins for engines without color-mix. Press goes
       DARKER (+scale, +pressed shadow) — depth inverts, like a real key. */
    --portal-c-btn-primary-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-primary-bg) 86%, #fff) 0%, var(--portal-c-btn-primary-bg) 50%, color-mix(in srgb, var(--portal-c-btn-primary-bg) 88%, #000) 100%);
    --portal-c-btn-primary-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-primary-bg-hover) 86%, #fff) 0%, var(--portal-c-btn-primary-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-primary-bg-hover) 88%, #000) 100%);
    --portal-c-btn-primary-bg-active: color-mix(in srgb, var(--portal-c-btn-primary-bg) 90%, #000);
    --portal-c-btn-danger-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-danger-bg) 90%, #fff) 0%, var(--portal-c-btn-danger-bg) 50%, color-mix(in srgb, var(--portal-c-btn-danger-bg) 92%, #000) 100%);
    --portal-c-btn-danger-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-danger-bg-hover) 90%, #fff) 0%, var(--portal-c-btn-danger-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-danger-bg-hover) 92%, #000) 100%);
    --portal-c-btn-success-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-success-bg) 90%, #fff) 0%, var(--portal-c-btn-success-bg) 50%, color-mix(in srgb, var(--portal-c-btn-success-bg) 92%, #000) 100%);
    --portal-c-btn-success-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-success-bg-hover) 90%, #fff) 0%, var(--portal-c-btn-success-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-success-bg-hover) 92%, #000) 100%);
    --portal-c-btn-warning-grad: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-warning-bg) 90%, #fff) 0%, var(--portal-c-btn-warning-bg) 50%, color-mix(in srgb, var(--portal-c-btn-warning-bg) 92%, #000) 100%);
    --portal-c-btn-warning-grad-hover: linear-gradient(180deg, color-mix(in srgb, var(--portal-c-btn-warning-bg-hover) 90%, #fff) 0%, var(--portal-c-btn-warning-bg-hover) 50%, color-mix(in srgb, var(--portal-c-btn-warning-bg-hover) 92%, #000) 100%);
    --portal-c-btn-danger-border: #c92a2a;
    --portal-c-btn-danger-border-hover: #e03131;
    --portal-c-btn-danger-shadow: inset 0 1px 0 var(--portal-c-btn-specular), 0 1px 2px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(201, 42, 42, 0.15);
    --portal-c-btn-danger-shadow-hover: inset 0 1px 0 var(--portal-c-btn-specular), 0 2px 4px rgba(15, 23, 42, 0.1), 0 4px 12px rgba(201, 42, 42, 0.18);
    --portal-c-btn-danger-disabled-bg: rgba(201, 42, 42, 0.35);
    --portal-c-btn-outline-border: color-mix(in srgb, rgb(28, 25, 54) 18%, transparent);
    --portal-c-btn-outline-bg: color-mix(in srgb, rgb(28, 25, 54) 3%, transparent);
    --portal-c-btn-outline-color: color-mix(in srgb, rgb(28, 25, 54) 68%, #fff);
    --portal-c-btn-outline-hover-bg: color-mix(in srgb, rgb(28, 25, 54) 5%, transparent);
    --portal-c-btn-outline-hover-color: color-mix(in srgb, rgb(28, 25, 54) 88%, #fff);
    --portal-c-btn-outline-hover-border: color-mix(in srgb, rgb(28, 25, 54) 24%, transparent);
    --portal-c-badge-inset: inset 0 1px 0 rgba(255, 255, 255, 0.6);
    --portal-c-badge-brand-bg: rgb(41, 28, 94);
    --portal-c-badge-brand-border: rgba(41, 28, 94, 0.12);
    --portal-c-badge-live-bg: color-mix(in srgb, var(--portal-c-accent) 10%, transparent);
    --portal-c-badge-live-color: var(--portal-c-accent);
    --portal-c-badge-muted-bg: #f1f3f5;
    --portal-c-badge-muted-color: #64748b;
    --portal-c-badge-running-bg: color-mix(in srgb, var(--portal-c-accent) 10%, transparent);
    --portal-c-badge-running-color: var(--portal-c-accent);
    --portal-c-badge-success-bg: rgba(43, 138, 62, 0.12);
    --portal-c-badge-success-color: #2b8a3e;
    --portal-c-badge-warning-bg: rgba(217, 130, 43, 0.14);
    --portal-c-badge-warning-color: #9c5700;
    --portal-c-badge-danger-bg: rgba(201, 42, 42, 0.1);
    --portal-c-badge-danger-color: #c92a2a;
    /* Weight class (3 / 12 / 30 / freestyle). NEUTRAL HERE, ON PURPOSE, and
       that is the whole design: weight colour is BrettZone's broadcast
       identity, so the app kit owns the HOOK and the sport theme owns the
       COLOUR. Under dark, light and matrix a weight chip is an ordinary
       muted chip and a weight stripe is an ordinary keyline — which is the
       correct outcome for a theme that is not BrettZone, not a shortfall.
       portal-sport-theme.css re-points these four pairs at --bz-w3/-w12/
       -w30/-w-fs; nothing else in the tree should. */
    --portal-c-w3-bg: var(--portal-c-badge-muted-bg);
    --portal-c-w3-color: var(--portal-c-badge-muted-color);
    --portal-c-w12-bg: var(--portal-c-badge-muted-bg);
    --portal-c-w12-color: var(--portal-c-badge-muted-color);
    --portal-c-w30-bg: var(--portal-c-badge-muted-bg);
    --portal-c-w30-color: var(--portal-c-badge-muted-color);
    --portal-c-wfs-bg: var(--portal-c-badge-muted-bg);
    --portal-c-wfs-color: var(--portal-c-badge-muted-color);
    --portal-c-w-stripe: var(--portal-c-border-subtle);
    /* Corner colours (v3 §2.8) — live-bracket's recipe, promoted. The inks
       are readable mixes toward the text ink; soft/border are the wash and
       keyline weights. mission-control's literal pair was the bug that
       motivated this. */
    --portal-c-corner-pink-ink: color-mix(in srgb, var(--portal-c-corner-pink) 62%, var(--portal-c-text));
    --portal-c-corner-blue-ink: color-mix(in srgb, var(--portal-c-corner-blue) 42%, var(--portal-c-text));
    --portal-c-corner-pink-soft: color-mix(in srgb, var(--portal-c-corner-pink) 14%, transparent);
    --portal-c-corner-blue-soft: color-mix(in srgb, var(--portal-c-corner-blue) 14%, transparent);
    --portal-c-corner-pink-border: color-mix(in srgb, var(--portal-c-corner-pink) 45%, transparent);
    --portal-c-corner-blue-border: color-mix(in srgb, var(--portal-c-corner-blue) 45%, transparent);
    /* Impersonation is a MODE, not a message: the same amber in both themes,
       tokenised so the banner and its controls retune together. */
    --portal-c-impersonation-bg: #b45309;
    --portal-c-impersonation-ink: #fff;
    /* ── Validation family (v3 §2.7) — the states that did not exist ──
       Border + ring + glyph per outcome, derived from the badge inks so the
       page's one red stays one red. The glyphs are Bootstrap's own paths
       with the fill re-issued inside the URI (the accordion-chevron trap:
       a fill cannot be themed from outside a data URI). */
    --portal-c-input-border-invalid: color-mix(in srgb, var(--portal-c-badge-danger-color) 55%, transparent);
    --portal-c-input-ring-invalid: 0 0 0 1px color-mix(in srgb, var(--portal-c-badge-danger-color) 45%, transparent), 0 0 0 0.25rem color-mix(in srgb, var(--portal-c-badge-danger-color) 14%, transparent);
    --portal-c-input-glyph-invalid: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23c92a2a'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23c92a2a' stroke='none'/%3e%3c/svg%3e");
    --portal-c-input-border-valid: color-mix(in srgb, var(--portal-c-badge-success-color) 55%, transparent);
    --portal-c-input-ring-valid: 0 0 0 1px color-mix(in srgb, var(--portal-c-badge-success-color) 45%, transparent), 0 0 0 0.25rem color-mix(in srgb, var(--portal-c-badge-success-color) 14%, transparent);
    --portal-c-input-glyph-valid: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8' height='8'%3e%3cpath fill='%232b8a3e' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
    --portal-c-btn-success-bg: #2f9e44;
    --portal-c-btn-success-bg-hover: #37b24c;
    --portal-c-btn-success-border: #2f9e44;
    --portal-c-btn-success-border-hover: #37b24c;
    --portal-c-btn-warning-bg: #d9822b;
    --portal-c-btn-warning-bg-hover: #ec8f30;
    --portal-c-btn-warning-border: #d9822b;
    --portal-c-btn-warning-border-hover: #ec8f30;
    --portal-c-progress-bg: #e9ecef;
    --portal-c-progress-bar-bg: var(--portal-c-accent);
    --portal-c-progress-bar-stripe: rgba(255, 255, 255, 0.2);
    --portal-c-skeleton-bg: rgba(41, 28, 94, 0.06);
    --portal-c-skeleton-sheen: rgba(255, 255, 255, 0.5);
    --portal-c-badge-info-bg: rgba(94, 62, 197, 0.10);
    --portal-c-badge-info-color: #5e3ec5;
    --portal-c-status-idle: var(--portal-c-text-muted);
    --portal-c-row-hover: rgba(28, 25, 54, 0.04);
    /* See the dark twin — tint-only selection, slightly stronger on white. */
    --portal-c-row-selected: color-mix(in srgb, var(--portal-c-accent) 14%, transparent);
    --portal-c-elev-overlay: 0 4px 16px rgba(24, 20, 45, 0.07), 0 24px 64px rgba(24, 20, 45, 0.16);
    --portal-c-elev-menu: 0 12px 40px rgba(41, 28, 94, 0.12);
    --portal-c-elev-hero: 0 8px 24px rgba(24, 20, 45, 0.08), 0 32px 88px rgba(24, 20, 45, 0.18);
    /* Recessed, not flat white — a field on light glass now reads as cut into
       the surface the same way the dark one does. On focus it fills opaque:
       maximum contrast exactly while you are typing in it. */
    --portal-c-input-bg: rgba(28, 25, 54, 0.045);
    --portal-c-input-inset: inset 0 1px 2px rgba(28, 25, 54, 0.05);
    --portal-c-input-bg-focus: var(--portal-c-surface-opaque);
    --portal-c-input-placeholder: rgba(28, 27, 36, 0.62);
    --portal-c-chrome-hover: rgba(28, 25, 54, 0.06);
    --portal-c-overlay: rgba(252, 252, 253, 0.78);
    --portal-c-overlay-opaque: #fafafb;
    --portal-c-scrim: rgba(28, 25, 54, 0.35);
    --portal-c-kbd: rgba(28, 27, 36, 0.4);
}

/* ── Cards ────────────────────────────────────────────────────────── */
/* Glass material = translucent surface + interior sheen + the card edge
   bundle (top catch, bottom shade) + hairline + soft shadow. NO
   backdrop-filter on cards, ever: blur creates a containing block (breaks
   Popper/fixed descendants) and costs GPU per element. The sheen is a
   background-image, so it composites for free and survives every gate. */
/* Bootstrap declares --bs-card-border-radius ON .card itself, defaulting it
   to --bs-border-radius (6px) — so a :root override never wins, and until now
   every card in the portal rendered at the CONTROL radius while the contract
   promised 10px. The tier system was a fiction for the most common surface in
   the product; only .portal-stat-card looked right, because portal-kit.css
   sets its radius by hand. Setting the component variable (rather than
   border-radius directly) keeps Bootstrap's derived inner radius working, so
   card headers and footers stay concentric for free. */
.portal-page-themed .card {
    --bs-card-border-radius: var(--bs-border-radius-lg);
    /* Bootstrap derives the inner radius from the BASE radius, not the card
       radius — so a 10px shell wore 5px header caps. Re-derive from the same
       tier the shell uses. */
    --bs-card-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));
    background-color: var(--portal-c-surface);
    background-image: var(--portal-c-surface-sheen);
    border-color: var(--portal-c-border);
    box-shadow: var(--portal-c-edge-card), var(--portal-c-shadow);
}

.portal-page-themed .card-header {
    background-color: var(--portal-c-surface-header);
    border-bottom-color: var(--portal-c-border);
    color: var(--portal-c-text);
}

/* portal-accent-card marks the page's primary working surface (see the
   style contract) — it no longer paints a visual stripe. A flat opaque
   border read as a Bootstrap-era accent against translucent glass; the
   card's own translucency + specular highlight now carry the hierarchy. */
.portal-page-themed .card.portal-accent-card {
    box-shadow: var(--portal-c-edge-dialog), var(--portal-c-shadow-raised);
}

/* Future-proofing against portal-motion's .portal-hoverable rule (0,3,0):
   none of the 25 accent-card templates combine the two today, but the first
   that does must not flatten to the card bundle mid-hover. */
.portal-page-themed .card.portal-accent-card.portal-hoverable:hover {
    box-shadow: var(--portal-c-edge-dialog), var(--portal-c-elev-menu);
}

.portal-page-themed .card.portal-accent-card.portal-hoverable:active {
    box-shadow: var(--portal-c-edge-dialog), var(--portal-c-shadow-pressed);
}


/* Card titles step DOWN from the page title (1.35rem) — panel headers at
 * Bootstrap's default 1.25rem read 96% of the page title and flatten the
 * hierarchy. The class controls appearance; use whatever heading level the
 * document outline needs. */
.portal-page-themed .card-header .card-title {
    font-size: var(--portal-title-card);
    font-weight: var(--portal-weight-label);
    line-height: var(--portal-leading-tight, 1.2);
}

.portal-page-themed .card-header .card-title > i {
    font-size: 0.8125em;
    opacity: 0.7;
}

/* ── Rules ────────────────────────────────────────────────────────────
 * Bootstrap's <hr> is `border-top: 1px solid currentColor; opacity: .25`,
 * which on a dark page is a grey line made out of the TEXT colour — the one
 * divider in the app that could not be fixed by re-pointing
 * --bs-border-color, because it never reads that token. Same keyline as
 * everything else instead, at full opacity (the token carries its own
 * alpha; stacking .25 on top of it would erase the line entirely).
 *
 * Dark-scoped deliberately. Light theme's <hr> is a 25%-ink line on paper,
 * which is already right and is not what this pass was asked to change —
 * the unscoped version of this rule moved 894 pixels of the light gallery. */
.portal-page-themed[data-bs-theme="dark"] hr {
    border-top: 1px solid var(--portal-c-border);
    opacity: 1;
}

/* ── Tables ───────────────────────────────────────────────────────────
 * The house table voice: hairline borders, no zebra, quiet hover, and a
 * mono micro-label header that rhymes with .portal-stat-label. Numeric
 * columns opt into .portal-col-num (right-aligned, tabular). */
.portal-page-themed .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--portal-c-text);
    --bs-table-border-color: var(--portal-c-border-subtle);
    --bs-table-hover-bg: var(--portal-c-row-hover);
    --bs-table-hover-color: var(--portal-c-text);
    --bs-table-striped-bg: transparent;
    --bs-table-striped-color: var(--portal-c-text);
    font-size: var(--portal-text-sm, 0.8125rem);
}

.portal-page-themed .table > :not(caption) > * > * {
    padding: 0.45rem 0.75rem;
}

.portal-page-themed .table thead th {
    font-family: var(--portal-font-mono);
    font-size: var(--portal-text-2xs);
    font-weight: var(--portal-weight-medium);
    letter-spacing: var(--portal-label-tracking, 0.12em);
    text-transform: uppercase;
    color: var(--portal-c-text-muted);
    border-bottom: 1px solid var(--portal-c-border);
    white-space: nowrap;
}

.portal-page-themed .portal-col-num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* Selection is the tint alone. The old inset accent stripe landed on
   EVERY cell's left edge (> * is required — tr backgrounds/shadows don't
   render reliably), drawing bars at each column boundary; and accent
   stripes are retired anyway (they fight the glass surfaces). */
.portal-page-themed .table tr.portal-row-selected > * {
    background-color: var(--portal-c-row-selected);
}

/* ── Segmented control (.portal-segmented) ────────────────────────────
   CONTROL-tier (36px, lands on a filter row); .portal-tabs stays the
   navigation tier. They share the material tokens and the same active
   voice — exclusive choice = raised pill — and the thumb mechanism is
   initTabs' own (data-thumb + the shared --portal-tab-thumb-* vars), so
   the slide cannot drift between the two. components/segmented.twig +
   portalKit.segmented(). */
.portal-segmented {
    position: relative;
    display: inline-flex;
    align-items: stretch;
    padding: 0.1875rem;
    gap: 0.1875rem;
    min-height: var(--portal-control-h);
    background: var(--portal-c-tab-track);
    border: 1px solid var(--portal-c-border);
    border-radius: var(--bs-border-radius-lg);
    box-shadow: var(--portal-c-tab-track-inset);
}

.portal-segmented--fill { display: flex; }
.portal-segmented--fill > * { flex: 1 1 0; }

.portal-segmented-btn,
.portal-segmented > .btn,
.portal-segmented > .btn-check + .btn,
.portal-segmented > .nav-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--portal-space-1);
    min-height: calc(var(--portal-control-h) - 0.375rem - 2px);
    padding: 0.25rem 0.8rem;
    font-size: var(--portal-text-sm);
    font-weight: var(--portal-weight-label);
    line-height: 1.2;
    white-space: nowrap;
    border: 1px solid transparent;
    /* Concentric with the 10px track across its 3px gap, floored at 4px. */
    border-radius: max(var(--bs-border-radius-sm), calc(var(--bs-border-radius-lg) - 0.1875rem));
    background: transparent;
    color: var(--portal-c-tab-inactive-color);
    box-shadow: none;
    cursor: pointer;
    transition: color var(--portal-dur-1) var(--portal-ease-out),
                background-color var(--portal-dur-1) var(--portal-ease-out),
                box-shadow var(--portal-dur-1) var(--portal-ease-out);
}

.portal-segmented-btn:hover:not(:disabled):not(.is-active) {
    color: var(--portal-c-tab-active-color);
    background: var(--portal-c-tab-hover);
}

.portal-segmented-btn.is-active,
.portal-segmented-btn[aria-pressed="true"],
.portal-segmented-btn[aria-selected="true"],
.portal-segmented > .btn.active,
.portal-segmented > .btn-check:checked + .btn,
.portal-segmented > .nav-link.active {
    color: var(--portal-c-tab-active-color);
    background: var(--portal-c-tab-active-bg);
    border-color: var(--portal-c-tab-active-border);
    box-shadow: var(--portal-c-tab-active-shadow);
}

/* Thrown position mirrored from a stateful parent (infra's cage tiles): the
   poll retags the TILE's data-state; the matching button reads as active. */
.portal-tile[data-state="on"] .portal-segmented-btn[data-state="on"],
.portal-tile[data-state="off"] .portal-segmented-btn[data-state="off"] {
    color: var(--portal-c-tab-active-color);
    background: var(--portal-c-tab-active-bg);
    border-color: var(--portal-c-tab-active-border);
    box-shadow: var(--portal-c-tab-active-shadow);
}

.portal-segmented-btn:focus-visible { outline: 2px solid var(--portal-c-accent); outline-offset: -2px; }

.portal-segmented-btn:disabled { opacity: 0.5; cursor: default; }

/* Sliding thumb — initTabs' own vars, so the mechanism is one mechanism. */
.portal-page-themed .portal-segmented[data-thumb].has-thumb::before {
    content: "";
    position: absolute;
    top: var(--portal-tab-thumb-top, 0.1875rem);
    left: 0;
    width: var(--portal-tab-thumb-w, 0);
    height: var(--portal-tab-thumb-h, 0);
    translate: var(--portal-tab-thumb-x, 0) 0;
    border-radius: max(var(--bs-border-radius-sm), calc(var(--bs-border-radius-lg) - 0.1875rem));
    background: var(--portal-c-tab-active-bg);
    border: 1px solid var(--portal-c-tab-active-border);
    box-shadow: var(--portal-c-tab-active-shadow);
    pointer-events: none;
    transition: var(--portal-tab-thumb-anim, translate var(--portal-dur-2) var(--portal-ease-spring, var(--portal-ease-out)),
                width var(--portal-dur-2) var(--portal-ease-out));
}

.portal-page-themed .portal-segmented[data-thumb].has-thumb > .is-active,
.portal-page-themed .portal-segmented[data-thumb].has-thumb > .btn-check:checked + .btn {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    position: relative;
    z-index: 1;
}

@media (pointer: coarse) {
    .portal-segmented { min-height: 2.75rem; }
    .portal-segmented-btn { min-height: calc(2.75rem - 0.375rem - 2px); }
}

@media (prefers-reduced-motion: reduce) {
    .portal-segmented-btn,
    .portal-page-themed .portal-segmented[data-thumb].has-thumb::before { transition: none; }
}

/* ── Accent rail (the 3px "this row/card is called out" edge) ─────────
   A utility that COMPOSES: on a card or ios list the rail joins the edge
   bundle and shadow instead of replacing them. data-rail picks the ink
   from the corner/badge families; pages animating a rail away set
   --portal-rail-color to transparent at the keyframe's end. */
.portal-rail-start  { --portal-rail: inset 3px 0 0 var(--portal-rail-color, var(--portal-c-accent)); box-shadow: var(--portal-rail); }
.portal-rail-end    { --portal-rail: inset -3px 0 0 var(--portal-rail-color, var(--portal-c-accent)); box-shadow: var(--portal-rail); }
.portal-rail-top    { --portal-rail: inset 0 3px 0 var(--portal-rail-color, var(--portal-c-accent)); box-shadow: var(--portal-rail); }
.portal-rail-bottom { --portal-rail: inset 0 -3px 0 var(--portal-rail-color, var(--portal-c-accent)); box-shadow: var(--portal-rail); }

[data-rail="pink"]    { --portal-rail-color: var(--portal-c-corner-pink); }
[data-rail="blue"]    { --portal-rail-color: var(--portal-c-corner-blue); }
[data-rail="success"] { --portal-rail-color: var(--portal-c-badge-success-color); }
[data-rail="danger"]  { --portal-rail-color: var(--portal-c-badge-danger-color); }
[data-rail="warning"] { --portal-rail-color: var(--portal-c-badge-warning-color); }

.portal-page-themed .card:is(.portal-rail-start, .portal-rail-end, .portal-rail-top, .portal-rail-bottom),
.portal-ios-list:is(.portal-rail-start, .portal-rail-end, .portal-rail-top, .portal-rail-bottom) {
    box-shadow: var(--portal-rail), var(--portal-c-edge-card), var(--portal-c-shadow);
}

.portal-page-themed .card.portal-hoverable:is(.portal-rail-start, .portal-rail-end, .portal-rail-top, .portal-rail-bottom):hover {
    box-shadow: var(--portal-rail), var(--portal-c-edge-card), var(--portal-c-shadow-raised);
}

/* ── Tabs rail (the row a tab strip lives on) ─────────────────────────
   One layout for the "tabs left, actions right" row every tabbed page
   hand-rolled: infra, sysmgmt, judging, notifications, RI, buddy … */
.portal-tabs-rail {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--portal-space-2);
    flex-wrap: wrap;
    margin-bottom: var(--portal-space-4);
}

.portal-tabs-rail--nowrap { flex-wrap: nowrap; align-items: center; }

.portal-tabs-rail-actions {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
    margin-left: auto;
    flex-shrink: 0;
}

.portal-tab-panel { flex: 1 1 auto; min-height: 0; }

@media (max-width: 767.98px) {
    .portal-tabs-rail--nowrap { flex-wrap: wrap; }
    .portal-tabs-rail--nowrap > .portal-tabs { flex-basis: 100%; }
    .portal-tabs-rail-actions { justify-content: flex-end; }
}

/* ── Segmented tabs (.portal-tabs) ────────────────────────────────── */
.portal-page-themed .portal-tabs.nav-pills {
    display: inline-flex;
    gap: 0.2rem;
    padding: 0.25rem;
    margin: 0;
    background: var(--portal-c-tab-track);
    border: 1px solid var(--portal-c-border);
    border-radius: var(--bs-border-radius-lg);
    /* Recessed track: the raised active pill needs a well to sit in, or the
       pair reads as two flat rectangles instead of a segmented control. */
    box-shadow: var(--portal-c-tab-track-inset);
    /* A tab strip never wraps — wrapped pills read as chips. It scrolls,
       snaps, and keeps its overscroll to itself; portal-kit.js stamps
       data-overflow so the fades below only paint when there is actually
       more strip to see. */
    flex-wrap: nowrap;
    /* …and never wider than its slot: inside a flex row min-width:auto lets
       the strip size to its pills and push the PAGE wide — the cap is what
       hands the overflow to the strip's own scroll instead. */
    max-width: 100%;
    min-width: 0;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
}

.portal-page-themed .portal-tabs::-webkit-scrollbar { display: none; }

.portal-page-themed .portal-tabs[data-overflow] {
    /* The fade is a mask, not a gradient overlay — an overlay would tint the
       pills; a mask lets the canvas show through the fading pill itself. */
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1.5rem, #000 calc(100% - 1.5rem), transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, #000 1.5rem, #000 calc(100% - 1.5rem), transparent 100%);
}

.portal-page-themed .portal-tabs[data-overflow="start"] {
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1.5rem);
    mask-image: linear-gradient(90deg, transparent 0, #000 1.5rem);
}

.portal-page-themed .portal-tabs[data-overflow="end"] {
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent 100%);
    mask-image: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent 100%);
}

.portal-page-themed .portal-tabs .nav-item {
    margin: 0;
}

.portal-page-themed .portal-tabs .nav-link {
    scroll-snap-align: start;
    scroll-margin-inline: 0.25rem;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: var(--portal-text-sm);
    font-weight: var(--portal-weight-label);
    color: var(--portal-c-tab-inactive-color);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--bs-border-radius);
    padding: 0.4rem 0.9rem;
    transition: color var(--portal-dur-1) var(--portal-ease-out),
                background var(--portal-dur-1) var(--portal-ease-out),
                box-shadow var(--portal-dur-1) var(--portal-ease-out),
                border-color var(--portal-dur-1) var(--portal-ease-out);
}

.portal-page-themed .portal-tabs .nav-link:hover:not(.active) {
    color: var(--portal-c-tab-active-color);
    background: var(--portal-c-tab-hover);
}

.portal-page-themed .portal-tabs .nav-link.active,
.portal-page-themed .portal-tabs .nav-link.active:hover,
.portal-page-themed .portal-tabs .nav-link.active:focus {
    color: var(--portal-c-tab-active-color);
    background: var(--portal-c-tab-active-bg);
    border-color: var(--portal-c-tab-active-border);
    box-shadow: var(--portal-c-tab-active-shadow);
}

.portal-page-themed .portal-tabs .nav-link:focus-visible {
    outline: 2px solid var(--portal-c-accent);
    outline-offset: 2px;
}

/* ── The sliding thumb (opt-in: .portal-tabs[data-thumb]) ─────────────
 *
 * Without it, switching tabs is one pill going dark and another lighting up —
 * two events that happen to be simultaneous. With it, ONE pill moves, which is
 * what a segmented control is: a single selection travelling between slots.
 *
 * The thumb is a ::before on the track, translated and sized by
 * portalKit.initTabs() from the active link's own box, so it can never drift
 * from where the pill actually is. It rides --portal-ease-spring: this is a
 * transform on a non-blurred element, which is precisely the case the spring
 * is sanctioned for.
 *
 * REST IS PIXEL-IDENTICAL to the per-link pill. The active link keeps painting
 * its own background until the JS takes over (.has-thumb is added by the same
 * call that measures the first position), so a page without the script, or
 * before it runs, looks exactly as it does today — and every existing baseline
 * holds.
 *
 * The one Life moment that is identical on touch: no hover involved. */
.portal-page-themed .portal-tabs[data-thumb] {
    position: relative;
}

.portal-page-themed .portal-tabs[data-thumb].has-thumb::before {
    content: "";
    position: absolute;
    top: var(--portal-tab-thumb-top, 0.25rem);
    left: 0;
    width: var(--portal-tab-thumb-w, 0);
    height: var(--portal-tab-thumb-h, 0);
    translate: var(--portal-tab-thumb-x, 0) 0;
    border-radius: var(--bs-border-radius);
    background: var(--portal-c-tab-active-bg);
    border: 1px solid var(--portal-c-tab-active-border);
    box-shadow: var(--portal-c-tab-active-shadow);
    /* `translate` rather than a transform, per the contract: it composites
       without fighting anything else that wants the transform property. */
    /* --portal-tab-thumb-anim is set to `none` by initTabs for the first
       placement and on resize, so the thumb appears where it belongs instead
       of sliding in from the left edge on load. */
    transition: var(--portal-tab-thumb-anim, translate var(--portal-dur-2) var(--portal-ease-spring),
                width var(--portal-dur-2) var(--portal-ease-spring));
    pointer-events: none;
}

/* Once the thumb is real, the active link stops painting its own pill —
   otherwise there are two, and the stationary one is the wrong one. */
.portal-page-themed .portal-tabs[data-thumb].has-thumb .nav-link.active,
.portal-page-themed .portal-tabs[data-thumb].has-thumb .nav-link.active:hover,
.portal-page-themed .portal-tabs[data-thumb].has-thumb .nav-link.active:focus {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    /* The label has to sit ON the thumb, not behind it. */
    position: relative;
    z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
    .portal-page-themed .portal-tabs[data-thumb].has-thumb::before {
        transition: none;
    }
}

/* ── Buttons ──────────────────────────────────────────────────────── */
/* NOTE: setting font-size and padding here out-specifies the --bs-btn-* custom
 * properties that .btn-sm/.btn-lg set, so ALL portal buttons are one size —
 * --portal-control-h. That is the house look, but it is why the fields need
 * the matching rule further down: Bootstrap kept its two field sizes and the
 * small one ended up 4.9px shorter than the button beside it. min-height only
 * pins the already-computed height (35.88px) to the token's round 36px so the
 * two can be asserted equal; it never changes where the label sits. */
.portal-page-themed .btn {
    font-weight: var(--portal-weight-label);
    font-size: var(--portal-text-sm);
    border-radius: var(--bs-border-radius);
    padding: 0.45rem 1rem;
    min-height: var(--portal-control-h);
    /* Transform on the spring so the RELEASE reads as a release; everything
       else on ease-out, because colour springing is a flicker. */
    transition: transform var(--portal-dur-2) var(--portal-ease-spring),
                box-shadow var(--portal-dur-1) var(--portal-ease-out),
                background-color var(--portal-dur-1) var(--portal-ease-out),
                border-color var(--portal-dur-1) var(--portal-ease-out),
                color var(--portal-dur-1) var(--portal-ease-out);
}

/* Buttons do NOT lift. A card lifting says "this whole surface is a thing you
   can take"; a button is already obviously pressable, and a 1px hover rise on
   a toolbar of six of them is six things twitching as the pointer crosses. The
   family's own -hover background and border step is the hover affordance.
   .portal-hoverable (portal-motion.css) remains the one sanctioned lift. */
.portal-page-themed .btn:not(:disabled):active {
    /* The press IS the feedback, so it happens on the press. 3% is small
       enough to survive a 36px control without the label reflowing, and safe
       because a button is never a blurred surface — scaling one of those
       re-samples the backdrop every frame. */
    transform: scale(0.97);
    transition-duration: var(--portal-dur-0, 80ms);
}

/* One focus ring for every button, keyboard only. (0,3,0) beats Bootstrap's
   .btn:focus-visible { outline: 0 } at (0,2,0); the .btn-check form is (0,4,0)
   against its (0,3,0). :focus deliberately does NOT ring — a clicked button
   holding a ring until the next click reads as a stuck state, which is why
   the twelve variant groups moved to :focus-visible in this commit. */
.portal-page-themed .btn:focus-visible,
.portal-page-themed .btn-check:focus-visible + .btn {
    outline: 2px solid var(--portal-c-accent);
    outline-offset: 2px;
}

.portal-page-themed .btn-primary,
.portal-page-themed .btn-primary:hover,
.portal-page-themed .btn-primary:focus-visible,
.portal-page-themed .btn-primary:active,
.portal-page-themed .btn-primary.active,
.portal-page-themed .btn-primary:disabled {
    --bs-btn-bg: var(--portal-c-btn-primary-bg);
    --bs-btn-border-color: var(--portal-c-btn-primary-border);
    --bs-btn-hover-bg: var(--portal-c-btn-primary-bg-hover);
    --bs-btn-hover-border-color: var(--portal-c-btn-primary-border-hover);
    --bs-btn-active-bg: var(--portal-c-btn-primary-bg-hover);
    --bs-btn-active-border-color: var(--portal-c-btn-primary-border-hover);
    --bs-btn-disabled-bg: var(--portal-c-btn-primary-bg);
    --bs-btn-disabled-border-color: var(--portal-c-btn-primary-border);
    background-color: var(--portal-c-btn-primary-bg);
    background-image: var(--portal-c-btn-primary-grad);
    border-color: var(--portal-c-btn-primary-border);
    box-shadow: var(--portal-c-btn-primary-shadow);
    color: #fff;
}

.portal-page-themed .btn-primary:hover:not(:disabled),
.portal-page-themed .btn-primary:focus-visible:not(:disabled),
.portal-page-themed .btn-primary:active:not(:disabled) {
    background-color: var(--portal-c-btn-primary-bg-hover);
    background-image: var(--portal-c-btn-primary-grad-hover);
    border-color: var(--portal-c-btn-primary-border-hover);
    box-shadow: var(--portal-c-btn-primary-shadow-hover);
    color: #fff;
}

.portal-page-themed .btn-danger,
.portal-page-themed .btn-danger:hover,
.portal-page-themed .btn-danger:focus-visible,
.portal-page-themed .btn-danger:active,
.portal-page-themed .btn-danger.active {
    --bs-btn-bg: var(--portal-c-btn-danger-bg);
    --bs-btn-border-color: var(--portal-c-btn-danger-border);
    --bs-btn-hover-bg: var(--portal-c-btn-danger-bg-hover);
    --bs-btn-hover-border-color: var(--portal-c-btn-danger-border-hover);
    --bs-btn-active-bg: var(--portal-c-btn-danger-bg-hover);
    --bs-btn-active-border-color: var(--portal-c-btn-danger-border-hover);
    background-color: var(--portal-c-btn-danger-bg);
    background-image: var(--portal-c-btn-danger-grad);
    border-color: var(--portal-c-btn-danger-border);
    box-shadow: var(--portal-c-btn-danger-shadow);
    color: #fff;
}

.portal-page-themed .btn-danger:hover:not(:disabled),
.portal-page-themed .btn-danger:focus-visible:not(:disabled),
.portal-page-themed .btn-danger:active:not(:disabled) {
    background-color: var(--portal-c-btn-danger-bg-hover);
    background-image: var(--portal-c-btn-danger-grad-hover);
    border-color: var(--portal-c-btn-danger-border-hover);
    box-shadow: var(--portal-c-btn-danger-shadow-hover);
    color: #fff;
}

.portal-page-themed .btn-danger:disabled {
    --bs-btn-disabled-bg: var(--portal-c-btn-danger-disabled-bg);
    --bs-btn-disabled-border-color: transparent;
    background-color: var(--portal-c-btn-danger-disabled-bg);
    background-image: none;
    border-color: transparent;
    box-shadow: none;
    opacity: 0.55;
    color: #fff;
}

.portal-page-themed .btn-outline-secondary,
.portal-page-themed .btn-outline-secondary:hover,
.portal-page-themed .btn-outline-secondary:focus-visible,
.portal-page-themed .btn-outline-secondary:active {
    /* A whisper of fill, not transparent: on glass an outline button with no
       material at all reads as a wireframe next to the solids. One step
       quieter than a solid: the CONTROL-tier edge (chip bundle — half-depth
       bottom shade), not the card bundle and not a drop shadow. §2.7's call;
       this line replaced an interim "no inset, no shadow". */
    --bs-btn-bg: var(--portal-c-btn-outline-bg);
    --bs-btn-hover-bg: var(--portal-c-btn-outline-hover-bg);
    --bs-btn-active-bg: var(--portal-c-btn-outline-hover-bg);
    background-color: var(--portal-c-btn-outline-bg);
    background-image: none;
    border-color: var(--portal-c-btn-outline-border);
    color: var(--portal-c-btn-outline-color);
    box-shadow: var(--portal-c-edge-chip);
}

.portal-page-themed .btn-outline-secondary:hover:not(:disabled),
.portal-page-themed .btn-outline-secondary:focus-visible:not(:disabled),
.portal-page-themed .btn-outline-secondary:active:not(:disabled) {
    background-color: var(--portal-c-btn-outline-hover-bg);
    border-color: var(--portal-c-btn-outline-hover-border);
    color: var(--portal-c-btn-outline-hover-color);
}

/* Solid .btn-secondary re-reads as the NEUTRAL TONAL button (quiet action) —
   raw Bootstrap gray #6c757d competing with primary was a stock-template tell.
   New markup should still prefer .btn-outline-secondary (see style contract). */
.portal-page-themed .btn-secondary,
.portal-page-themed .btn-secondary:hover,
.portal-page-themed .btn-secondary:focus-visible,
.portal-page-themed .btn-secondary:active,
.portal-page-themed .btn-secondary.active {
    --bs-btn-bg: var(--portal-c-badge-muted-bg);
    --bs-btn-border-color: var(--portal-c-edge-control);
    --bs-btn-color: var(--portal-c-text);
    --bs-btn-hover-bg: var(--portal-c-btn-outline-hover-bg);
    --bs-btn-hover-border-color: var(--portal-c-btn-outline-hover-border);
    --bs-btn-hover-color: var(--portal-c-btn-outline-hover-color);
    --bs-btn-active-bg: var(--portal-c-btn-outline-hover-bg);
    --bs-btn-active-border-color: var(--portal-c-btn-outline-hover-border);
    --bs-btn-active-color: var(--portal-c-btn-outline-hover-color);
    background-color: var(--portal-c-badge-muted-bg);
    background-image: none;
    border-color: var(--portal-c-edge-control);
    color: var(--portal-c-text);
    box-shadow: var(--portal-c-edge-chip);
}

.portal-page-themed .btn-secondary:hover:not(:disabled),
.portal-page-themed .btn-secondary:focus-visible:not(:disabled),
.portal-page-themed .btn-secondary:active:not(:disabled) {
    background-color: var(--portal-c-btn-outline-hover-bg);
    border-color: var(--portal-c-btn-outline-hover-border);
    color: var(--portal-c-btn-outline-hover-color);
}

/* .btn-info maps onto the primary tokens — raw Bootstrap cyan is off-palette.
   Banned in new markup (style contract); this catches the legacy uses. */
.portal-page-themed .btn-info,
.portal-page-themed .btn-info:hover,
.portal-page-themed .btn-info:focus-visible,
.portal-page-themed .btn-info:active,
.portal-page-themed .btn-info.active,
.portal-page-themed .btn-info:disabled {
    --bs-btn-bg: var(--portal-c-btn-primary-bg);
    --bs-btn-border-color: var(--portal-c-btn-primary-border);
    --bs-btn-color: #fff;
    --bs-btn-hover-bg: var(--portal-c-btn-primary-bg-hover);
    --bs-btn-hover-border-color: var(--portal-c-btn-primary-border-hover);
    --bs-btn-active-bg: var(--portal-c-btn-primary-bg-hover);
    --bs-btn-active-border-color: var(--portal-c-btn-primary-border-hover);
    --bs-btn-disabled-bg: var(--portal-c-btn-primary-bg);
    --bs-btn-disabled-border-color: var(--portal-c-btn-primary-border);
    background-color: var(--portal-c-btn-primary-bg);
    background-image: var(--portal-c-btn-primary-grad);
    border-color: var(--portal-c-btn-primary-border);
    box-shadow: var(--portal-c-btn-primary-shadow);
    color: #fff;
}

.portal-page-themed .btn-info:hover:not(:disabled),
.portal-page-themed .btn-info:focus-visible:not(:disabled),
.portal-page-themed .btn-info:active:not(:disabled) {
    background-color: var(--portal-c-btn-primary-bg-hover);
    background-image: var(--portal-c-btn-primary-grad-hover);
    border-color: var(--portal-c-btn-primary-border-hover);
    box-shadow: var(--portal-c-btn-primary-shadow-hover);
    color: #fff;
}

/* Press goes DARKER, flat: the grad disappears and the fill drops 10% — depth
   inverting under the finger, like a real key. Scale and the pressed shadow
   come from the base .btn rules. (0,3,0) after the hover trios, so source
   order decides in press's favour while pressed. */
.portal-page-themed .btn-primary:active:not(:disabled),
.portal-page-themed .btn-info:active:not(:disabled) {
    background-image: none;
    background-color: var(--portal-c-btn-primary-bg-active);
}

/* Disabled never carries depth — a grad on an inert control is a promise. */
.portal-page-themed .btn-primary:disabled,
.portal-page-themed .btn-info:disabled {
    background-image: none;
    box-shadow: none;
    opacity: 0.55;
}

.portal-page-themed .btn-outline-info,
.portal-page-themed .btn-outline-info:hover,
.portal-page-themed .btn-outline-info:focus-visible,
.portal-page-themed .btn-outline-info:active {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--portal-c-btn-outline-color);
    --bs-btn-border-color: var(--portal-c-btn-outline-border);
    --bs-btn-hover-bg: var(--portal-c-btn-outline-hover-bg);
    --bs-btn-hover-color: var(--portal-c-btn-outline-hover-color);
    --bs-btn-hover-border-color: var(--portal-c-btn-outline-hover-border);
    --bs-btn-active-bg: var(--portal-c-btn-outline-hover-bg);
    --bs-btn-active-color: var(--portal-c-btn-outline-hover-color);
    --bs-btn-active-border-color: var(--portal-c-btn-outline-hover-border);
    background-color: transparent;
    background-image: none;
    border-color: var(--portal-c-btn-outline-border);
    color: var(--portal-c-btn-outline-color);
    box-shadow: none;
}

.portal-page-themed .btn-outline-info:hover:not(:disabled),
.portal-page-themed .btn-outline-info:focus-visible:not(:disabled),
.portal-page-themed .btn-outline-info:active:not(:disabled) {
    background-color: var(--portal-c-btn-outline-hover-bg);
    border-color: var(--portal-c-btn-outline-hover-border);
    color: var(--portal-c-btn-outline-hover-color);
}

/* Selected is a TOGGLE state, so it recesses — a raised or flat selected
   would be indistinguishable from hover. (0,4,0) via .btn-check outranks
   the (0,3,0) state rules above. */
.portal-page-themed .btn-outline-info.active,
.portal-page-themed .btn-check:checked + .btn-outline-info,
.portal-page-themed .btn-check:active + .btn-outline-info {
    background-color: var(--portal-c-accent-muted);
    border-color: var(--portal-c-edge-control);
    color: var(--portal-c-accent);
    box-shadow: var(--portal-c-tab-track-inset);
}

/* ── Outline buttons carry their colour in the INK, not the rim ──────
   Every outline variant below used to draw its semantic colour as the
   BORDER: teal at 30/40% on .btn-outline-primary, and raw Bootstrap
   #dc3545 / #198754 / #ffc107 on the danger/success/warning trio, which
   had no portal rule at all. That is the same "outline drawn on top" look
   the v3 keyline pass removed from cards — see the note on the hover lift
   in portal-motion.css, which retired the identical teal rim for the
   identical reason. On a dark surface whose resting edge is a black
   keyline and a specular catch, a coloured rim reads as a stray box
   floating over the material rather than an edge of it.

   So the rim is now the neutral control keyline that every outline button
   shares (--portal-c-btn-outline-border, the button-strength twin of
   --portal-c-edge-control), and the semantics move to the LABEL plus a
   tinted hover wash. .btn-outline-primary and .btn-outline-secondary
   therefore differ only in ink, which is what "accent-flavored secondary"
   always meant. The danger trio picks up the badge ink/tint tokens so a
   destructive trigger and a danger badge finally agree on which red the
   portal uses — they were #dc3545 and #c92a2a side by side. */
.portal-page-themed .btn-outline-primary,
.portal-page-themed .btn-outline-primary:hover,
.portal-page-themed .btn-outline-primary:focus-visible,
.portal-page-themed .btn-outline-primary:active {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--portal-c-accent);
    --bs-btn-border-color: var(--portal-c-btn-outline-border);
    --bs-btn-hover-bg: var(--portal-c-accent-muted);
    --bs-btn-hover-color: var(--portal-c-accent);
    --bs-btn-hover-border-color: var(--portal-c-btn-outline-hover-border);
    --bs-btn-active-bg: var(--portal-c-accent-muted);
    --bs-btn-active-color: var(--portal-c-accent);
    --bs-btn-active-border-color: var(--portal-c-btn-outline-hover-border);
    background-color: transparent;
    background-image: none;
    border-color: var(--portal-c-btn-outline-border);
    color: var(--portal-c-accent);
    box-shadow: none;
}

.portal-page-themed .btn-outline-primary:hover:not(:disabled),
.portal-page-themed .btn-outline-primary:focus-visible:not(:disabled),
.portal-page-themed .btn-outline-primary:active:not(:disabled) {
    background-color: var(--portal-c-accent-muted);
    border-color: var(--portal-c-btn-outline-hover-border);
    color: var(--portal-c-accent);
}

/* Selected is a TOGGLE state, so it recesses — a raised or flat selected
   would be indistinguishable from hover. (0,4,0) via .btn-check outranks
   the (0,3,0) state rules above. */
.portal-page-themed .btn-outline-primary.active,
.portal-page-themed .btn-check:checked + .btn-outline-primary,
.portal-page-themed .btn-check:active + .btn-outline-primary {
    background-color: var(--portal-c-accent-muted);
    border-color: var(--portal-c-edge-control);
    color: var(--portal-c-accent);
    box-shadow: var(--portal-c-tab-track-inset);
}

/* .btn-outline-danger is the row-level destructive trigger (style
   contract) and the third most-used button in the portal, so it had the
   loudest raw-Bootstrap leak: a #dc3545 rim that also filled SOLID on
   hover, in a red that exists nowhere else in the palette. */
.portal-page-themed .btn-outline-danger,
.portal-page-themed .btn-outline-danger:hover,
.portal-page-themed .btn-outline-danger:focus-visible,
.portal-page-themed .btn-outline-danger:active {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--portal-c-badge-danger-color);
    --bs-btn-border-color: var(--portal-c-btn-outline-border);
    --bs-btn-hover-bg: var(--portal-c-badge-danger-bg);
    --bs-btn-hover-color: var(--portal-c-badge-danger-color);
    --bs-btn-hover-border-color: var(--portal-c-btn-outline-hover-border);
    --bs-btn-active-bg: var(--portal-c-badge-danger-bg);
    --bs-btn-active-color: var(--portal-c-badge-danger-color);
    --bs-btn-active-border-color: var(--portal-c-btn-outline-hover-border);
    background-color: transparent;
    background-image: none;
    border-color: var(--portal-c-btn-outline-border);
    color: var(--portal-c-badge-danger-color);
    box-shadow: none;
}

.portal-page-themed .btn-outline-danger:hover:not(:disabled),
.portal-page-themed .btn-outline-danger:focus-visible:not(:disabled),
.portal-page-themed .btn-outline-danger:active:not(:disabled) {
    background-color: var(--portal-c-badge-danger-bg);
    border-color: var(--portal-c-btn-outline-hover-border);
    color: var(--portal-c-badge-danger-color);
}

/* Selected is a TOGGLE state, so it recesses — a raised or flat selected
   would be indistinguishable from hover. (0,4,0) via .btn-check outranks
   the (0,3,0) state rules above. */
.portal-page-themed .btn-outline-danger.active,
.portal-page-themed .btn-check:checked + .btn-outline-danger,
.portal-page-themed .btn-check:active + .btn-outline-danger {
    background-color: var(--portal-c-badge-danger-bg);
    border-color: var(--portal-c-edge-control);
    color: var(--portal-c-badge-danger-color);
    box-shadow: var(--portal-c-tab-track-inset);
}

.portal-page-themed .btn-outline-success,
.portal-page-themed .btn-outline-success:hover,
.portal-page-themed .btn-outline-success:focus-visible,
.portal-page-themed .btn-outline-success:active {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--portal-c-badge-success-color);
    --bs-btn-border-color: var(--portal-c-btn-outline-border);
    --bs-btn-hover-bg: var(--portal-c-badge-success-bg);
    --bs-btn-hover-color: var(--portal-c-badge-success-color);
    --bs-btn-hover-border-color: var(--portal-c-btn-outline-hover-border);
    --bs-btn-active-bg: var(--portal-c-badge-success-bg);
    --bs-btn-active-color: var(--portal-c-badge-success-color);
    --bs-btn-active-border-color: var(--portal-c-btn-outline-hover-border);
    background-color: transparent;
    background-image: none;
    border-color: var(--portal-c-btn-outline-border);
    color: var(--portal-c-badge-success-color);
    box-shadow: none;
}

.portal-page-themed .btn-outline-success:hover:not(:disabled),
.portal-page-themed .btn-outline-success:focus-visible:not(:disabled),
.portal-page-themed .btn-outline-success:active:not(:disabled) {
    background-color: var(--portal-c-badge-success-bg);
    border-color: var(--portal-c-btn-outline-hover-border);
    color: var(--portal-c-badge-success-color);
}

/* Selected is a TOGGLE state, so it recesses — a raised or flat selected
   would be indistinguishable from hover. (0,4,0) via .btn-check outranks
   the (0,3,0) state rules above. */
.portal-page-themed .btn-outline-success.active,
.portal-page-themed .btn-check:checked + .btn-outline-success,
.portal-page-themed .btn-check:active + .btn-outline-success {
    background-color: var(--portal-c-badge-success-bg);
    border-color: var(--portal-c-edge-control);
    color: var(--portal-c-badge-success-color);
    box-shadow: var(--portal-c-tab-track-inset);
}

.portal-page-themed .btn-outline-warning,
.portal-page-themed .btn-outline-warning:hover,
.portal-page-themed .btn-outline-warning:focus-visible,
.portal-page-themed .btn-outline-warning:active {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--portal-c-badge-warning-color);
    --bs-btn-border-color: var(--portal-c-btn-outline-border);
    --bs-btn-hover-bg: var(--portal-c-badge-warning-bg);
    --bs-btn-hover-color: var(--portal-c-badge-warning-color);
    --bs-btn-hover-border-color: var(--portal-c-btn-outline-hover-border);
    --bs-btn-active-bg: var(--portal-c-badge-warning-bg);
    --bs-btn-active-color: var(--portal-c-badge-warning-color);
    --bs-btn-active-border-color: var(--portal-c-btn-outline-hover-border);
    background-color: transparent;
    background-image: none;
    border-color: var(--portal-c-btn-outline-border);
    color: var(--portal-c-badge-warning-color);
    box-shadow: none;
}

.portal-page-themed .btn-outline-warning:hover:not(:disabled),
.portal-page-themed .btn-outline-warning:focus-visible:not(:disabled),
.portal-page-themed .btn-outline-warning:active:not(:disabled) {
    background-color: var(--portal-c-badge-warning-bg);
    border-color: var(--portal-c-btn-outline-hover-border);
    color: var(--portal-c-badge-warning-color);
}

/* Selected is a TOGGLE state, so it recesses — a raised or flat selected
   would be indistinguishable from hover. (0,4,0) via .btn-check outranks
   the (0,3,0) state rules above. */
.portal-page-themed .btn-outline-warning.active,
.portal-page-themed .btn-check:checked + .btn-outline-warning,
.portal-page-themed .btn-check:active + .btn-outline-warning {
    background-color: var(--portal-c-badge-warning-bg);
    border-color: var(--portal-c-edge-control);
    color: var(--portal-c-badge-warning-color);
    box-shadow: var(--portal-c-tab-track-inset);
}

/* Semantic solid buttons (success = confirm/add, warning = cautionary
   actions like Force Check In) — same token pattern as .btn-danger so raw
   Bootstrap green/yellow never leaks into themed pages. */
.portal-page-themed .btn-success,
.portal-page-themed .btn-success:hover,
.portal-page-themed .btn-success:focus-visible,
.portal-page-themed .btn-success:active,
.portal-page-themed .btn-success.active {
    --bs-btn-bg: var(--portal-c-btn-success-bg);
    --bs-btn-border-color: var(--portal-c-btn-success-border);
    --bs-btn-hover-bg: var(--portal-c-btn-success-bg-hover);
    --bs-btn-hover-border-color: var(--portal-c-btn-success-border-hover);
    --bs-btn-active-bg: var(--portal-c-btn-success-bg-hover);
    --bs-btn-active-border-color: var(--portal-c-btn-success-border-hover);
    background-color: var(--portal-c-btn-success-bg);
    background-image: var(--portal-c-btn-success-grad);
    border-color: var(--portal-c-btn-success-border);
    box-shadow: var(--portal-c-btn-primary-shadow);
    color: #fff;
}

.portal-page-themed .btn-success:hover:not(:disabled),
.portal-page-themed .btn-success:focus-visible:not(:disabled),
.portal-page-themed .btn-success:active:not(:disabled) {
    background-color: var(--portal-c-btn-success-bg-hover);
    background-image: var(--portal-c-btn-success-grad-hover);
    border-color: var(--portal-c-btn-success-border-hover);
    box-shadow: var(--portal-c-btn-primary-shadow-hover);
    color: #fff;
}

.portal-page-themed .btn-success:disabled {
    background-color: var(--portal-c-btn-success-bg);
    background-image: none;
    border-color: transparent;
    box-shadow: none;
    opacity: 0.55;
    color: #fff;
}

.portal-page-themed .btn-warning,
.portal-page-themed .btn-warning:hover,
.portal-page-themed .btn-warning:focus-visible,
.portal-page-themed .btn-warning:active,
.portal-page-themed .btn-warning.active {
    --bs-btn-bg: var(--portal-c-btn-warning-bg);
    --bs-btn-border-color: var(--portal-c-btn-warning-border);
    --bs-btn-hover-bg: var(--portal-c-btn-warning-bg-hover);
    --bs-btn-hover-border-color: var(--portal-c-btn-warning-border-hover);
    --bs-btn-active-bg: var(--portal-c-btn-warning-bg-hover);
    --bs-btn-active-border-color: var(--portal-c-btn-warning-border-hover);
    background-color: var(--portal-c-btn-warning-bg);
    background-image: var(--portal-c-btn-warning-grad);
    border-color: var(--portal-c-btn-warning-border);
    box-shadow: var(--portal-c-btn-primary-shadow);
    color: #fff;
}

.portal-page-themed .btn-warning:hover:not(:disabled),
.portal-page-themed .btn-warning:focus-visible:not(:disabled),
.portal-page-themed .btn-warning:active:not(:disabled) {
    background-color: var(--portal-c-btn-warning-bg-hover);
    background-image: var(--portal-c-btn-warning-grad-hover);
    border-color: var(--portal-c-btn-warning-border-hover);
    box-shadow: var(--portal-c-btn-primary-shadow-hover);
    color: #fff;
}

.portal-page-themed .btn-warning:disabled {
    background-color: var(--portal-c-btn-warning-bg);
    background-image: none;
    border-color: transparent;
    box-shadow: none;
    opacity: 0.55;
    color: #fff;
}

/* Pressed = the control COMPRESSES: back to the baseline and down an
   elevation step, specular kept so the edge never blinks out mid-press.
   MUST stay last in this section — it ties every family's
   `:active:not(:disabled)` rule at (0,4,0) and wins on order alone. Moving
   it above them silently restores the hover shadow on press. Outline
   buttons keep their flat wash (they never carry a resting shadow). */
.portal-page-themed .btn:not(:disabled):not([class*="btn-outline"]):active {
    box-shadow: inset 0 1px 0 var(--portal-c-btn-specular), var(--portal-c-shadow-pressed);
}

/* ── Badges ───────────────────────────────────────────────────────── */
.portal-page-themed .badge.cage-number,
.portal-page-themed .badge.text-bg-primary.cage-number {
    background-color: var(--portal-c-badge-brand-bg) !important;
    border: 1px solid var(--portal-c-badge-brand-border);
    color: #fff !important;
    font-weight: var(--portal-weight-strong);
    letter-spacing: var(--portal-label-tracking);
}

/* Keyline, not an accent rim — see the note on the outline buttons above.
   The teal tint and the teal ink already say "live"; the rim only drew a
   lit box around the chip. Every other badge in the family is on this
   border, so the accent ones were also the odd shape in their own row. */
.portal-page-themed .cage-live-badge,
.portal-page-themed .badge.cage-live-badge {
    background-color: var(--portal-c-badge-live-bg) !important;
    color: var(--portal-c-badge-live-color) !important;
    border: 1px solid var(--portal-c-border);
}

.portal-page-themed .qa-status-badge,
.portal-page-themed .portal-status-badge {
    border: 1px solid var(--portal-c-border);
    /* One inset, no drop shadow: a chip this small reads fuzzy under a
       card-sized shadow, but a 1px top catch makes it pressed glass rather
       than a printed sticker. */
    box-shadow: var(--portal-c-badge-inset);
    font-weight: var(--portal-weight-label);
    font-size: var(--portal-text-xs);
    letter-spacing: var(--portal-tracking-caps);
    text-transform: uppercase;
}

.portal-page-themed .qa-status-badge.text-bg-secondary,
.portal-page-themed .portal-status-badge.text-bg-secondary {
    background-color: var(--portal-c-badge-muted-bg) !important;
    color: var(--portal-c-badge-muted-color) !important;
}

/* No border-color of their own: both fall back to the family keyline set
   on .portal-status-badge above. They were the only two states drawing a
   teal rim, which is what made them read as outlined chips in a row of
   filled ones. */
.portal-page-themed .qa-status-badge.text-bg-primary,
.portal-page-themed .qa-status-badge.qa-status-running,
.portal-page-themed .portal-status-badge.portal-status-running {
    background-color: var(--portal-c-badge-running-bg) !important;
    color: var(--portal-c-badge-running-color) !important;
}

.portal-page-themed .portal-status-badge.portal-status-live {
    background-color: var(--portal-c-badge-live-bg) !important;
    color: var(--portal-c-badge-live-color) !important;
}

.portal-page-themed .portal-status-badge.portal-status-brand {
    background-color: var(--portal-c-badge-brand-bg) !important;
    color: #fff !important;
    border-color: var(--portal-c-badge-brand-border);
}

/* Semantic status badge variants — soft tinted backgrounds instead of raw
   Bootstrap solid text-bg-* colors, matching the muted/running badge look. */
.portal-page-themed .portal-status-badge.text-bg-success,
.portal-page-themed .qa-status-badge.text-bg-success {
    background-color: var(--portal-c-badge-success-bg) !important;
    color: var(--portal-c-badge-success-color) !important;
}

.portal-page-themed .portal-status-badge.text-bg-warning,
.portal-page-themed .qa-status-badge.text-bg-warning {
    background-color: var(--portal-c-badge-warning-bg) !important;
    color: var(--portal-c-badge-warning-color) !important;
}

.portal-page-themed .portal-status-badge.text-bg-danger,
.portal-page-themed .qa-status-badge.text-bg-danger {
    background-color: var(--portal-c-badge-danger-bg) !important;
    color: var(--portal-c-badge-danger-color) !important;
}

.portal-page-themed .portal-status-badge.text-bg-info,
.portal-page-themed .qa-status-badge.text-bg-info {
    background-color: var(--portal-c-badge-info-bg) !important;
    color: var(--portal-c-badge-info-color) !important;
}

/* Raw Bootstrap badges on themed pages fall back onto the palette too —
   cyan info and gray secondary were the loudest stock leaks. Solid brand /
   success / warning / danger raw badges keep their meaning until each page
   migrates to .portal-status-badge (style contract). */
.portal-page-themed .badge.text-bg-info:not(.portal-status-badge):not(.qa-status-badge) {
    background-color: var(--portal-c-badge-info-bg) !important;
    color: var(--portal-c-badge-info-color) !important;
    border: 1px solid var(--portal-c-border);
}

.portal-page-themed .badge.text-bg-secondary:not(.portal-status-badge):not(.qa-status-badge):not(.cage-number) {
    background-color: var(--portal-c-badge-muted-bg) !important;
    color: var(--portal-c-badge-muted-color) !important;
    border: 1px solid var(--portal-c-border);
}

.portal-page-themed .qa-management .badge.text-bg-secondary {
    background-color: var(--portal-c-badge-muted-bg) !important;
    color: var(--portal-c-badge-muted-color) !important;
    border: 1px solid var(--portal-c-border);
    font-weight: var(--portal-weight-label);
    letter-spacing: var(--portal-tracking-caps);
}

.portal-page-themed .portal-icon-badge,
.portal-page-themed .qa-mgmt-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--bs-border-radius);
    font-size: var(--portal-text-base);
    /* Tinted fill + border + a single top catch. Still no drop shadow — a
       card-sized shadow on a 2.5rem chip reads fuzzy. */
    border: 1px solid var(--portal-c-border);
    box-shadow: var(--portal-c-badge-inset);
}

.portal-page-themed .portal-icon-badge.text-bg-primary,
.portal-page-themed .qa-mgmt-icon.text-bg-primary {
    background-color: var(--portal-c-badge-brand-bg) !important;
    color: #fff !important;
    border: 1px solid var(--portal-c-badge-brand-border);
}

/* ── Icon tile (v3 §2.8) — ONE class for every squared/round icon well.
   .portal-stat-icon / .portal-icon-badge / .portal-ios-row-icon /
   .portal-empty-state-icon keep working as today (their own rules stand);
   new work takes .portal-icon-tile + a size modifier, and the [data-tint]
   vocabulary is shared with the row icon (portal-ios.css). #13's page
   sub-commits migrate the old names onto this. */
.portal-icon-tile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: var(--portal-icon-tile-size, var(--portal-icon-tile-md));
    height: var(--portal-icon-tile-size, var(--portal-icon-tile-md));
    border-radius: var(--bs-border-radius);
    font-size: 1rem;
    background: var(--portal-c-accent-muted);
    color: var(--portal-c-accent);
    border: 1px solid var(--portal-c-accent-border);
    box-shadow: var(--portal-c-badge-inset);
}

.portal-icon-tile--sm { --portal-icon-tile-size: var(--portal-icon-tile-sm); font-size: 0.8125rem; }
.portal-icon-tile--md { --portal-icon-tile-size: var(--portal-icon-tile-md); }
.portal-icon-tile--lg { --portal-icon-tile-size: var(--portal-icon-tile-lg); border-radius: 50%; }
.portal-icon-tile--xl { --portal-icon-tile-size: var(--portal-icon-tile-xl); border-radius: 50%; font-size: var(--portal-text-2xl); }

.portal-icon-tile[data-tint="accent"]  { background: var(--portal-c-accent-muted); color: var(--portal-c-accent); }
.portal-icon-tile[data-tint="brand"]   { background: var(--portal-c-badge-brand-bg); color: #fff; border-color: var(--portal-c-badge-brand-border); }
.portal-icon-tile[data-tint="success"] { background: var(--portal-c-badge-success-bg); color: var(--portal-c-badge-success-color); border-color: transparent; }
.portal-icon-tile[data-tint="warning"] { background: var(--portal-c-badge-warning-bg); color: var(--portal-c-badge-warning-color); border-color: transparent; }
.portal-icon-tile[data-tint="danger"]  { background: var(--portal-c-badge-danger-bg); color: var(--portal-c-badge-danger-color); border-color: transparent; }
.portal-icon-tile[data-tint="muted"]   { background: var(--portal-c-badge-muted-bg); color: var(--portal-c-badge-muted-color); border-color: transparent; }

/* ── Card header (components/card-header.twig) ────────────────────────
   The same slot vocabulary as .portal-group-head: tile, title, count,
   hint, actions at the far end. Retires the page forks one sub-commit at
   a time (dashboard, RI, sparky-predict). */
.portal-card-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--portal-space-2);
}

.portal-card-header-title { margin: 0; min-width: 0; }

.portal-card-header-hint { font-size: var(--portal-text-xs); }

.portal-card-header-actions {
    display: flex;
    align-items: center;
    gap: var(--portal-space-2);
    margin-left: auto;
    flex-shrink: 0;
}

/* Launcher badges tinted by category (v3 #10b) — the same six-tint
   vocabulary as .portal-ios-row-icon (portal-ios.css), spoken by the badge
   until #13's .portal-icon-tile supersedes this rule. The MAP — which
   category wears which tint — is the host's (_tool-grid.twig); these rules
   are only the vocabulary. Solid fills, same base border and inset as every
   other badge. */
.portal-page-themed .portal-icon-badge[data-tint="accent"]  { background-color: var(--portal-c-accent); color: var(--portal-c-on-accent); }
.portal-page-themed .portal-icon-badge[data-tint="brand"]   { background-color: var(--portal-c-brand); color: #fff; }
.portal-page-themed .portal-icon-badge[data-tint="success"] { background-color: var(--bs-success); color: #fff; }
.portal-page-themed .portal-icon-badge[data-tint="warning"] { background-color: var(--bs-warning); color: #1c1b24; }
.portal-page-themed .portal-icon-badge[data-tint="danger"]  { background-color: var(--bs-danger); color: #fff; }

.portal-page-themed .portal-icon-badge[data-tint="muted"] {
    /* Declaration-order fallback for the color-mix, same one the row icon's
       parse-floor twin argues: full-strength muted ink where the mix cannot
       parse. */
    background-color: var(--portal-c-text-muted);
    background-color: color-mix(in srgb, var(--portal-c-text-muted) 70%, transparent);
    color: #fff;
}

/* ── Status indicators ────────────────────────────────────────────── */
.portal-page-themed .qa-status-dot-sm {
    background: var(--portal-c-accent);
}

.portal-page-themed .qa-status-badge.qa-status-running .qa-status-dot {
    background: var(--portal-c-accent);
    box-shadow: 0 0 0 3px var(--portal-c-accent-muted);
}

/* ── Progress bar ────────────────────────────────────────────────── */
.portal-page-themed .progress {
    background-color: var(--portal-c-progress-bg);
    border-radius: var(--bs-border-radius);
    overflow: hidden;
}

.portal-page-themed .progress-bar {
    background-color: var(--portal-c-progress-bar-bg);
    background-image: linear-gradient(
        45deg,
        var(--portal-c-progress-bar-stripe) 25%,
        transparent 25%,
        transparent 50%,
        var(--portal-c-progress-bar-stripe) 50%,
        var(--portal-c-progress-bar-stripe) 75%,
        transparent 75%,
        transparent
    );
    transition: width 0.4s var(--portal-ease-out, ease);
}

/* ── Pagination ───────────────────────────────────────────────────────
 * Bootstrap pagination rides its own --bs-pagination-* vars; stock blue
 * (--bs-primary active page + link-blue text) was the last raw-Bootstrap
 * leak on themed pages. Quiet voice to match table footers: hairline
 * chips, row-hover wash, the primary-button family on the active page,
 * teal focus ring. Tokens flip with the theme, so one rule covers both. */
.portal-page-themed .pagination {
    --bs-pagination-color: var(--portal-c-text-muted);
    --bs-pagination-bg: transparent;
    --bs-pagination-border-color: var(--portal-c-edge-control);
    --bs-pagination-hover-color: var(--portal-c-text);
    --bs-pagination-hover-bg: var(--portal-c-row-hover);
    --bs-pagination-hover-border-color: var(--portal-c-edge-control);
    --bs-pagination-focus-color: var(--portal-c-text);
    --bs-pagination-focus-bg: var(--portal-c-row-hover);
    --bs-pagination-focus-box-shadow: var(--portal-c-focus-ring);
    /* Active page = the primary-button family: NHRL violet (dark) / brand
       purple (light), white ink in both. */
    --bs-pagination-active-color: #fff;
    --bs-pagination-active-bg: var(--portal-c-btn-primary-bg);
    --bs-pagination-active-border-color: var(--portal-c-btn-primary-border);
    --bs-pagination-disabled-color: var(--portal-c-status-idle);
    --bs-pagination-disabled-bg: transparent;
    --bs-pagination-disabled-border-color: var(--portal-c-edge-control-subtle);
}


/* ── Stock Bootstrap BLUE, in dark theme ─────────────────────────────
 * The last raw #0d6efd left on themed pages, and it only became obvious
 * once the greys around it were sorted: on the v3 dark material the
 * accordion's expanded header (navy fill, blue ink, a chevron drawn as a
 * near-black SVG that is invisible on a dark surface) and the list group's
 * .active row (electric blue, full bleed) were the loudest things in the
 * gallery. Same class of leak the portal already killed for text-bg-info.
 *
 * DARK-SCOPED, like the <hr> rule above and for the same reason: light
 * theme was deliberately frozen for this pass. Light still renders these
 * three in Bootstrap blue — off-palette there too, just not broken — and
 * finishing the job is logged as UI-IDEAS AX rather than smuggled in here.
 *
 * The chevrons have to be re-issued as whole data URIs: Bootstrap bakes the
 * fill colour into the SVG, so there is no variable to re-point. Both are
 * the same path as the vendor file with only the fill changed. */
.portal-page-themed[data-bs-theme="dark"] .accordion {
    --bs-accordion-bg: transparent;
    --bs-accordion-border-color: var(--portal-c-border);
    --bs-accordion-color: var(--portal-c-text);
    --bs-accordion-btn-color: var(--portal-c-text);
    /* Expanded = the accent wash, the same affordance a selected row uses. */
    --bs-accordion-active-bg: var(--portal-c-accent-muted);
    --bs-accordion-active-color: var(--portal-c-accent);
    --bs-accordion-btn-focus-box-shadow: var(--portal-c-focus-ring);
}

/* The chevrons must be declared on ::after, not on .accordion: Bootstrap's own
   dark block sets both icon variables at `[data-bs-theme=dark]
   .accordion-button::after`, i.e. directly on the element that consumes them,
   so an inherited value from an ancestor — however specific that ancestor's
   selector is — never reaches it. Declaring here is the only way to win, and
   the same trap as --bs-card-border-radius being set ON the component. */
.portal-page-themed[data-bs-theme="dark"] .accordion-button::after {
    --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23a5a2b3'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
    --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2300dad8'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
}

.portal-page-themed[data-bs-theme="dark"] .list-group {
    --bs-list-group-bg: transparent;
    --bs-list-group-color: var(--portal-c-text);
    --bs-list-group-border-color: var(--portal-c-border);
    --bs-list-group-action-hover-bg: var(--portal-c-row-hover);
    --bs-list-group-action-hover-color: var(--portal-c-text);
    /* Selection is the accent TINT, not a solid fill — same rule the tables
       follow (--portal-c-row-selected), so a selected list row and a
       selected table row finally read as the same state. */
    --bs-list-group-active-bg: var(--portal-c-row-selected);
    --bs-list-group-active-border-color: var(--portal-c-accent-border);
    --bs-list-group-active-color: var(--portal-c-text);
}

/* The range thumb is a hard-coded #0d6efd in the vendor file — no variable
   exists, so the three vendor-prefixed pseudo-elements each need saying. */
.portal-page-themed[data-bs-theme="dark"] .form-range::-webkit-slider-thumb {
    background-color: var(--portal-c-accent);
}

.portal-page-themed[data-bs-theme="dark"] .form-range::-moz-range-thumb {
    background-color: var(--portal-c-accent);
}

.portal-page-themed[data-bs-theme="dark"] .form-range::-ms-thumb {
    background-color: var(--portal-c-accent);
}


/* ── Overlay glass: in-page menus ─────────────────────────────────────
 * True glass only where no ancestor is itself backdrop-filtered: a child
 * backdrop-filter inside a filtered ancestor samples the ancestor's
 * backdrop root, not the page — it blurs nothing in Chromium and
 * mis-clips in Safari. So chrome-nested menus (topbar bell, sidebar user,
 * footer social) and menus inside modal/alertify glass shells fall back
 * to the opaque elevated surface. Guards sit AFTER the glass rules and
 * either out-rank them (IDs) or tie-and-win by order. */
/* :not(.portal-impersonation-switch-menu): that menu hard-codes
   data-bs-theme="dark" (navigation.twig) so its item text stays light —
   glassing it with the LIGHT theme's near-white overlay would be unreadable.
   It keeps stock Bootstrap dark rendering. */
body:has(.portal-page-themed) .dropdown-menu:not(.portal-impersonation-switch-menu) {
    --bs-dropdown-bg: transparent;
    /* Item states ride Bootstrap's own vars — without these the active item
       stays stock primary blue (the gallery's statically-open menu exhibit
       exposed it). Active = the house accent treatment (toc links,
       enhanced-select selected). */
    --bs-dropdown-link-color: var(--portal-c-text);
    --bs-dropdown-link-hover-color: var(--portal-c-text);
    --bs-dropdown-link-hover-bg: var(--portal-c-row-hover);
    --bs-dropdown-link-active-color: var(--portal-c-accent);
    --bs-dropdown-link-active-bg: var(--portal-c-accent-muted);
    --bs-dropdown-divider-bg: var(--portal-c-border);
    background-color: var(--portal-c-overlay);
    border: 1px solid var(--portal-c-border);
    box-shadow: var(--portal-c-edge-card), var(--portal-c-elev-overlay);
    backdrop-filter: var(--portal-blur-overlay);
    -webkit-backdrop-filter: var(--portal-blur-overlay);
    /* Bootstrap declares --bs-dropdown-border-radius ON .dropdown-menu itself
       and defaults it to --bs-border-radius (6px, the CONTROL tier) — the
       identical trap --bs-card-border-radius sprang above, and it was still
       live here until 2026-08-26. Every menu in the portal rendered at the
       control radius while the contract puts menus on the 10px card tier and
       this very rule dresses them in --portal-c-edge-card. The comment below
       even asserted "Bootstrap gives .dropdown-menu the lg radius"; it does
       not, and measuring said 6px.

       Only the notification bell looked right, and only because
       notifications-bell.css set border-radius by hand — the workaround the
       card fix warns against, since a literal cannot feed the concentric
       math below. Set the component variable instead. */
    --bs-dropdown-border-radius: var(--bs-border-radius-lg);

    /* Concentric radius, declared once for the children below.
       CHILD RADIUS = max(sm, parent radius − inset gap).
       The menu now really does wear the lg radius (above) with 0.5rem of
       padding, so an item nested inside it is tighter than its parent rather
       than equal to it — matching corners at two different sizes is what
       reads as "drawn by hand" rather than "stacked rectangles". Until the
       line above existed this derived from a parent radius the parent did not
       have. The max() floor stops deep nesting computing a negative radius. */
    --portal-radius-parent: var(--bs-border-radius-lg);
    --portal-radius-gap: 0.5rem;
}

body:has(.portal-page-themed) .dropdown-menu:not(.portal-impersonation-switch-menu) .dropdown-item {
    border-radius: max(var(--bs-border-radius-sm), calc(var(--portal-radius-parent) - var(--portal-radius-gap)));
}

/* THE TAB BAR IS THE SAME KIND OF ANCESTOR and joined this rule on
   2026-08-31, when it grew a drop-up of its own (the Account slot). It reads
   the same chrome tokens as #site-footer — same tint, same blur — so a menu
   inside it hits the identical trap the comment above describes: its own
   backdrop-filter samples the bar rather than the page, and the panel renders
   as a see-through sheet with the tool grid legible straight through the
   Logout row. One selector, because it is one problem. */
#site-footer .dropdown-menu,
#portal-tab-bar .dropdown-menu {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background-color: var(--portal-c-overlay-opaque);
}

body:has(.portal-page-themed[data-bs-theme]) :is(.modal-content, .alertify) .dropdown-menu {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background-color: var(--portal-c-overlay-opaque);
}

/* ── Forms ────────────────────────────────────────────────────────────
 * Recessed fields, teal focus ring, accent checks — stock blue checkboxes
 * and focus rings were the settings-page template tell. */
/* A field is cut INTO the surface: recessed fill + a top lip catching the
   same light as everything else. */
.portal-page-themed .form-control,
.portal-page-themed .form-select {
    background-color: var(--portal-c-input-bg);
    /* Control edge, NOT the keyline — a field has to hold its shape on the
       bare canvas as well as inside a card, and the canvas is the case a
       dark rim cannot serve. See --portal-c-edge-control. */
    border-color: var(--portal-c-edge-control);
    box-shadow: var(--portal-c-input-inset);
    color: var(--portal-c-text);
}

/* Small fields meet the button height (see --portal-control-h). Bootstrap sizes
 * .form-control-sm to 31px and .btn-sm to the same — but the portal's own .btn
 * rule above overrides btn-sm back to 36px, so every filter bar in the app had
 * a search 4.9px shorter than the button next to it. Raising the field is the
 * cheaper half of that pair to move: buttons are the portal's established
 * density, and .btn-sm is already the only button size there is.
 *
 * min-height, not height — an <input>/<select> centres its value in the box, so
 * this reads as taller padding and never clips. padding-y comes with it so a
 * field that DOES grow past the floor (a multi-row .form-select) keeps the same
 * breathing room. textarea is excluded: min-height there is the author's rows
 * count, and a 36px floor on a 2-row box would be a floor on nothing. */
.portal-page-themed .form-control-sm:not(textarea),
.portal-page-themed .form-select-sm {
    min-height: var(--portal-control-h);
    padding-top: 0.4rem;
    padding-bottom: 0.4rem;
}

.portal-page-themed .form-control::placeholder {
    color: var(--portal-c-input-placeholder);
}

/* Focus keeps the well (the inset stays) and adds the accent ring. In light
   theme --portal-c-input-bg-focus fills opaque for typing contrast; in dark
   it resolves back to the recessed fill, because a bright field flares in a
   dark venue. */
.portal-page-themed .form-control:focus,
.portal-page-themed .form-select:focus {
    background-color: var(--portal-c-input-bg-focus);
    border-color: var(--portal-c-accent-border);
    box-shadow: var(--portal-c-input-inset), var(--portal-c-focus-ring);
    color: var(--portal-c-text);
}

/* Validation, after :focus in source so .is-invalid:focus beats plain :focus
   at equal specificity. The select keeps its chevron: the glyph rides
   --bs-form-select-bg-icon, never background-image. */
.portal-page-themed .form-control.is-invalid,
.portal-page-themed .form-select.is-invalid,
.portal-page-themed .was-validated .form-control:invalid,
.portal-page-themed .was-validated .form-select:invalid {
    border-color: var(--portal-c-input-border-invalid);
    background-image: var(--portal-c-input-glyph-invalid);
    --bs-form-select-bg-icon: var(--portal-c-input-glyph-invalid);
}

.portal-page-themed .form-select.is-invalid,
.portal-page-themed .was-validated .form-select:invalid {
    background-image: none;
}

.portal-page-themed .form-control.is-invalid:focus,
.portal-page-themed .form-select.is-invalid:focus {
    border-color: var(--portal-c-input-border-invalid);
    box-shadow: var(--portal-c-input-inset), var(--portal-c-input-ring-invalid);
}

.portal-page-themed .form-control.is-valid,
.portal-page-themed .form-select.is-valid,
.portal-page-themed .was-validated .form-control:valid:not(:placeholder-shown),
.portal-page-themed .was-validated .form-select:valid {
    border-color: var(--portal-c-input-border-valid);
    background-image: var(--portal-c-input-glyph-valid);
    --bs-form-select-bg-icon: var(--portal-c-input-glyph-valid);
}

.portal-page-themed .form-select.is-valid,
.portal-page-themed .was-validated .form-select:valid {
    background-image: none;
}

.portal-page-themed .form-control.is-valid:focus,
.portal-page-themed .form-select.is-valid:focus {
    border-color: var(--portal-c-input-border-valid);
    box-shadow: var(--portal-c-input-inset), var(--portal-c-input-ring-valid);
}

.portal-page-themed .form-check-input.is-invalid {
    border-color: var(--portal-c-input-border-invalid);
}

.portal-page-themed .form-check-input.is-invalid:checked {
    background-color: var(--portal-c-badge-danger-color);
    border-color: var(--portal-c-badge-danger-color);
}

.portal-page-themed .form-label {
    font-size: var(--portal-text-sm);
    font-weight: var(--portal-weight-label);
    color: var(--portal-c-text);
    margin-bottom: var(--portal-space-1);
}

.portal-page-themed .form-text {
    font-size: var(--portal-text-xs);
    color: var(--portal-c-text-muted);
}

/* AUTOFILL — the one field state the portal never styled, and the only place
 * Chrome's own blue still reaches a user.
 *
 * Reported on /login, which is where it is most likely to be seen: anyone with
 * a saved password gets both fields painted in Chrome's autofill wash the
 * moment the page loads. Nothing in this stylesheet had an opinion about it and
 * Bootstrap only adjusts the padding, so the field kept the browser's colour —
 * a pale blue box sitting inside a dark glass card. It is not a focus ring and
 * not a border: the portal's own focus ring is teal and correct, which is why
 * reading the CSS never explains this one. You have to have a password saved.
 *
 * `background-color` cannot fix it — Chrome refuses it on an autofilled field.
 * A large inset shadow is the standard lever, and it is why the :focus rule
 * below has to restate the ring: box-shadow is one property, so filling the
 * field would otherwise cancel the focus ring entirely and take a real
 * affordance away to fix a cosmetic one.
 *
 * -webkit-text-fill-color, not color, for the same reason: `color` is also
 * overridden on an autofilled field. */
.portal-page-themed .form-control:-webkit-autofill,
.portal-page-themed .form-control:-webkit-autofill:hover {
    -webkit-text-fill-color: var(--portal-c-text);
    -webkit-box-shadow: var(--portal-c-input-inset), 0 0 0 100px var(--portal-c-input-bg) inset;
    box-shadow: var(--portal-c-input-inset), 0 0 0 100px var(--portal-c-input-bg) inset;
    caret-color: var(--portal-c-text);
    border-color: var(--portal-c-edge-control);
}

.portal-page-themed .form-control:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--portal-c-text);
    -webkit-box-shadow: var(--portal-c-focus-ring), 0 0 0 100px var(--portal-c-input-bg-focus) inset;
    box-shadow: var(--portal-c-focus-ring), 0 0 0 100px var(--portal-c-input-bg-focus) inset;
    border-color: var(--portal-c-accent-border);
}

.portal-page-themed .form-check-input {
    background-color: var(--portal-c-input-bg);
    border-color: var(--portal-c-btn-outline-border);
}

.portal-page-themed .form-check-input:focus {
    border-color: var(--portal-c-accent-border);
    box-shadow: var(--portal-c-focus-ring);
}

.portal-page-themed .form-check-input:checked {
    background-color: var(--portal-c-accent);
    border-color: var(--portal-c-accent);
}

/* Dark theme's accent fill is bright teal — the check/dot glyph flips to
 * near-black ink (--portal-c-on-accent). Light theme's accent is dark, so
 * Bootstrap's stock white glyph already reads. */
/* (Switch handles ride --bs-form-switch-bg, a different variable — the white
   handle on teal reads fine and is untouched by this override.) */
html[data-bs-theme="dark"] .portal-page-themed .form-check-input:checked[type="checkbox"] {
    --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%2300201f' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
}

html[data-bs-theme="dark"] .portal-page-themed .form-check-input:checked[type="radio"] {
    --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%2300201f'/%3e%3c/svg%3e");
}

/* ── Dropzone (file chooser) ──────────────────────────────────────────
 * A click/drag target for file inputs, styled to the same glass language
 * as form controls — not the bare OS "Choose file" button. Markup: a
 * <label class="portal-dropzone"> wraps a `hidden` <input type="file">. */
.portal-page-themed .portal-dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    padding: 1.4rem 1rem;
    border: 1px dashed var(--portal-c-border);
    border-radius: var(--bs-border-radius-lg); /* was the phantom --portal-radius-md — a token nothing defines */
    color: var(--portal-c-text-muted);
    font-size: var(--portal-text-sm);
    text-align: center;
    cursor: pointer;
    transition: border-color var(--portal-dur-1, 120ms) var(--portal-ease-out, ease), color var(--portal-dur-1, 120ms) var(--portal-ease-out, ease), background-color var(--portal-dur-1, 120ms) var(--portal-ease-out, ease);
}

.portal-page-themed .portal-dropzone:hover,
.portal-page-themed .portal-dropzone.is-dragover {
    border-color: var(--portal-c-accent);
    color: var(--portal-c-text);
    background-color: var(--portal-c-accent-muted);
}

.portal-page-themed .portal-dropzone:focus-within {
    box-shadow: var(--portal-c-focus-ring);
}

.portal-page-themed .portal-dropzone i {
    font-size: var(--portal-text-xl);
}

/* ── Modals ───────────────────────────────────────────────────────────
 * One dialog language: overlay glass (translucent shell + blur) + hairline +
 * specular + overlay shadow + the 3px accent stripe the alertify re-skin
 * also carries. Keyed off body:has() because modals typically sit outside
 * <main>; the tokens flip globally with the theme so runtime toggles stay
 * in sync. ⌘K styles itself (global-search.css).
 * NO overflow:hidden here — .modal-content is the containing block for
 * enhanced-select/dropdown menus opened inside modals (account/schedule,
 * system-management), and a clip cuts them off at the modal edge. Bootstrap's
 * own inner-radius vars round the header/footer to the shell. */
/* ── Scrims ───────────────────────────────────────────────────────────
 * Bootstrap's modal and offcanvas backdrops join the dimmer tier.
 *
 * Alertify's dimmer has carried --portal-blur-dim since the glass restyle, so
 * a confirm() softened the page behind it while a Bootstrap modal opened over
 * a razor-sharp one — two dialogs a few seconds apart, made of different
 * material. The offcanvas scrim had the same split on phones, sitting right
 * beside the AI drawer's blurred backdrop.
 *
 * This adds no new class of cost: dimmers are hard-capped at 2px precisely
 * because they cover the whole viewport, and the portal already pays that on
 * every confirm() in the app. --portal-blur-dim is gate-managed, so all three
 * degrade paths take these back to a plain scrim on their own.
 *
 * will-change is the same cold-start fix alertify documents at its dimmer:
 * without it the compositing surface is allocated as the fade begins, and the
 * first frames show the page still sharp behind a scrim that is already
 * darkening. */
/* THE DIM CANNOT LIVE IN `opacity`, and that is not a style preference.
 *
 * Stock Bootstrap dims the scrim with opacity (`--bs-backdrop-opacity: .5` on
 * a solid black element). An element with opacity < 1 is composited as its own
 * group, and a group's backdrop is EMPTY — so `backdrop-filter` on that same
 * element has nothing behind it to filter. The declaration computes perfectly
 * (getComputedStyle reports `blur(2px)`), the gates all pass, and the page
 * behind a modal stays razor sharp. It did, for the whole life of the glass
 * pass, on every modal and offcanvas in the portal.
 *
 * So the dim moves into the COLOUR and the element stays fully opaque. Both
 * halves of the glass then work as designed: this scrim defocuses the page,
 * and .modal-content's own blur(20px) sits on top of an already-soft field.
 *
 * The fade animates background-color + backdrop-filter rather than opacity
 * (see portal-motion.css) — if opacity animated, the blur would be inert for
 * the whole transition and snap on at the end.
 *
 * Verify by REMOVING the filter and diffing pixels behind the scrim, never by
 * reading the computed value back. Computed style is exactly what lied here. */
body:has(.portal-page-themed) .modal-backdrop,
body:has(.portal-page-themed) .offcanvas-backdrop {
    --bs-backdrop-bg: transparent;
    --bs-backdrop-opacity: 1;
    background-color: transparent;
    opacity: 1;
    /* blur(0) rather than none: same filter function at both ends, so the
       transition interpolates instead of stepping. */
    backdrop-filter: blur(0);
    -webkit-backdrop-filter: blur(0);
    will-change: backdrop-filter;
}

body:has(.portal-page-themed) .modal-backdrop.show,
body:has(.portal-page-themed) .offcanvas-backdrop.show {
    background-color: var(--portal-c-scrim, rgba(0, 0, 0, 0.5));
    backdrop-filter: var(--portal-blur-dim);
    -webkit-backdrop-filter: var(--portal-blur-dim);
    opacity: 1;
}

/* THE MODAL'S DIM LIVES ON `.modal`, NOT ON ITS BACKDROP — and that is a
 * SEQUENCING fix, not a styling one.
 *
 * Bootstrap's Modal.show() is `this._backdrop.show(() => this._showElement())`,
 * and Backdrop.show() waits out its own transition before calling back. So the
 * dialog is `display: none` for the whole time the scrim is animating. Measured
 * on the gallery's three overlays, all identical: the screen dimmed and blurred
 * for 223ms with NOTHING else on it, and only then did the dialog get a layout
 * box and begin its own 320ms rise — 573ms end to end, with a hard seam in the
 * middle where the dialog materialised. That seam is what reads as a modal
 * "jumping" into place rather than arriving.
 *
 * The wait is measured off the BACKDROP element's own transition-duration
 * (getTransitionDurationFromElement), so the only way to shorten it is for the
 * backdrop not to transition. Splitting the scrim's two jobs does that without
 * losing either one:
 *
 *   · the backdrop keeps the BLUR and stops transitioning — Bootstrap measures
 *     0ms and reveals the dialog immediately. A 2px defocus arriving in one
 *     frame is not a thing the eye can catch; a 50% dim would be.
 *   · `.modal` carries the DIM, and `.modal` is the element whose opacity fade
 *     already carries the dialog (portal-motion.css). One transition now drives
 *     both, so the scrim and the dialog are the same gesture by construction
 *     rather than by two durations agreeing.
 *
 * Measured after: dialog present at 24ms, settled at 374ms. Same end state to
 * the pixel — the dim is the same token at the same opacity, one element up.
 *
 * OFFCANVAS IS DELIBERATELY NOT CHANGED. Its show() runs the panel and the
 * backdrop in parallel already, so it never had the seam, and it has no
 * `.modal` equivalent to hang a dim on.
 *
 * The degrade gates keep working untouched: they re-point --portal-blur-dim,
 * which is still the backdrop's only job. */
body:has(.portal-page-themed) .modal.fade {
    background-color: var(--portal-c-scrim, rgba(0, 0, 0, 0.5));
}

body:has(.portal-page-themed) .modal-backdrop.show {
    background-color: transparent;
}

body:has(.portal-page-themed) .modal .modal-content:not(.portal-search-content) {
    /* Same inner-radius trap as .card, dialog tier: 13px caps inside the
       14px shell (.modal-content clips nothing by design, so mis-registered
       corners are visible). */
    --bs-modal-inner-border-radius: calc(var(--bs-border-radius-xl) - var(--bs-border-width));
    background-color: var(--portal-c-overlay);
    border: 1px solid var(--portal-c-border);
    border-radius: var(--bs-border-radius-xl);
    isolation: isolate;
    backdrop-filter: var(--portal-blur-overlay);
    -webkit-backdrop-filter: var(--portal-blur-overlay);
    /* Lets the browser allocate the blur's compositing surface while the
       dialog is still laid out at opacity:0 (Bootstrap forces a reflow
       before adding .show), instead of cold-starting it the instant the
       fade begins — that cold start is what reads as "shows the background
       text, then blurs it". */
    will-change: backdrop-filter;
    box-shadow: var(--portal-c-edge-dialog), var(--portal-c-elev-overlay);
    color: var(--portal-c-text);
}

body:has(.portal-page-themed) .modal .modal-content:not(.portal-search-content) .modal-header {
    background-color: var(--portal-c-surface-header);
    border-bottom: 1px solid var(--portal-c-border);
    padding: 1rem 1.25rem;
}

body:has(.portal-page-themed) .modal .modal-content:not(.portal-search-content) .modal-title {
    font-size: var(--portal-title-card);
    font-weight: var(--portal-weight-label);
}

body:has(.portal-page-themed) .modal .modal-content:not(.portal-search-content) .modal-footer {
    border-top: 1px solid var(--portal-c-border-subtle);
}

html[data-bs-theme="dark"] body:has(.portal-page-themed) .modal .btn-close {
    filter: invert(1) grayscale(100%);
}

/* ── Touch behaviour ──────────────────────────────────────────────────
 * touch-action: manipulation removes the double-tap-zoom wait on controls.
 * Scoped to interactive elements ONLY — a touch that STARTS on a button is
 * never someone's pinch anchor, but the same declaration on html/body/main
 * or an image would take page zoom away, and zoom is an accessibility
 * requirement on app pages (base.twig's viewport comment). */
.portal-page-themed .btn,
.portal-page-themed .nav-link,
.portal-page-themed .dropdown-item,
.portal-page-themed .form-check-input,
.portal-page-themed .page-link,
.portal-sidebar-link,
.portal-sidebar-group-toggle,
.portal-tab-link,
.portal-search-btn,
.portal-footer-theme-btn,
.portal-appearance-toggle {
    touch-action: manipulation;
}

/* Chrome is app furniture, not copy: a long press should not start a text
   selection or raise the iOS link sheet. Content inside <main> stays fully
   selectable, and so does the system bar's centre column — ops copy the
   floor tray's numbers out of it. */
.portal-sidebar-nav {
    -webkit-user-select: none;
    user-select: none;
}

.portal-sidebar-link {
    -webkit-touch-callout: none;
}

/* ── Responsive: touch + phones ───────────────────────────────────────
 * Touch targets: bump to the 44px floor the style contract mandates, without
 * disturbing desktop density. (This was 2.5rem/40px and quietly under-spec
 * against the contract it implements.)
 *
 * Fields are here for the same reason buttons are, and were missing: a field is
 * a touch target too, and one 6-8px shorter than the button it sits beside is
 * both under the floor and visibly out of line — which is the phone half of the
 * filter-bar mismatch --portal-control-h fixes on the desktop side. The old
 * comment here claimed btn-sm rows were "~31px"; they have not been since the
 * .btn override above, and that stale number is what hid the gap. */
@media (pointer: coarse) {
    .portal-page-themed .btn,
    .portal-page-themed .nav-link,
    .portal-page-themed .page-link,
    .portal-page-themed .portal-chip.is-interactive {
        min-height: 2.75rem;
    }

    .portal-page-themed .btn-sm {
        padding: 0.5rem 0.9rem;
        min-height: 2.75rem;
    }

    .portal-page-themed .form-control:not(textarea),
    .portal-page-themed .form-select {
        min-height: 2.75rem;
    }

    .portal-page-themed .form-check-input {
        width: 1.15em;
        height: 1.15em;
    }

    /* Menu rows and chrome triggers were the touch floor's blind spot: the
     * rule above covers buttons and fields, but every MENU in the app — the
     * user dropdown, the bell, Follow — is .dropdown-item, and those sat at
     * roughly 34px. On the hardware this floor exists for (pit iPads, phones
     * in a venue) a 34px row next to a 44px button is the one people miss.
     *
     * display: flex is what makes min-height mean something. A block-level
     * .dropdown-item grows its BOX to 2.75rem and leaves the text at the top;
     * flex + align-items centres it, which is the only visible change on a
     * fine pointer — there is none, because this whole block is coarse-only.
     *
     * The chrome triggers are square icon buttons, so they take both
     * dimensions. */
    body:has(.portal-page-themed) .dropdown-item {
        display: flex;
        align-items: center;
        min-height: 2.75rem;
    }

    .portal-notif-btn,
    .footer-social-toggle,
    .portal-footer-theme-btn,
    .portal-appearance-toggle,
    .portal-sidebar-group-toggle {
        min-width: 2.75rem;
        min-height: 2.75rem;
    }
}

/* Sub-16px inputs make iOS Safari auto-zoom the page on focus. */
@media (max-width: 575.98px) {
    .portal-page-themed .form-control,
    .portal-page-themed .form-select,
    .portal-page-themed .form-control-sm,
    .portal-page-themed .form-select-sm {
        font-size: var(--portal-text-base);
    }

    /* That 14px → 16px bump is 3px of extra line box, which pushed a small
     * field back past the button beside it (38.8px against 36) — the same gap
     * --portal-control-h just closed, reopened by the anti-zoom rule. Take the
     * difference out of the padding so the height stays on the token. Only a
     * narrow desktop window lands here alone; a real phone is also a coarse
     * pointer, where both this and the button go to the 44px floor above. */
    .portal-page-themed .form-control-sm:not(textarea),
    .portal-page-themed .form-select-sm {
        padding-top: 0.3125rem;
        padding-bottom: 0.3125rem;
    }
}

/* ── Impersonation banner ─────────────────────────────────────────── */
/* Sticky top bar shown when an admin is acting as another user.
   Uses amber/warning tones to be visually distinct from normal chrome. */
.portal-impersonation-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: var(--portal-z-top); /* above the topbar (z-index ~1030) */
    background-color: var(--portal-c-impersonation-bg);
    color: var(--portal-c-impersonation-ink);
    font-size: var(--portal-text-sm);
    font-weight: var(--portal-weight-medium);
    border-bottom: 2px solid rgba(0, 0, 0, 0.18);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
    /* It displaces the topbar, so it inherits the topbar's job of painting
       under the status bar in a standalone PWA. */
    padding-top: var(--portal-safe-top);
    padding-left: var(--portal-safe-left);
    padding-right: var(--portal-safe-right);
}

.portal-impersonation-inner {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
    padding: .45rem 1.25rem;
}

.portal-impersonation-actor {
    opacity: .85;
    font-size: var(--portal-text-sm);
}

.portal-impersonation-readonly-badge {
    display: inline-flex;
    align-items: center;
    font-size: var(--portal-text-xs);
    font-weight: var(--portal-weight-label);
    background-color: rgba(0, 0, 0, 0.25);
    border: 1px solid rgba(255, 255, 255, 0.35);
    color: #fff;
    border-radius: var(--bs-border-radius-sm);
    padding: .1rem .45rem;
    white-space: nowrap;
}

/* Groups the Switch and Stop controls at the right end of the banner */
.portal-impersonation-actions {
    display: flex;
    align-items: center;
    gap: .4rem;
}

.portal-impersonation-stop-form {
    margin: 0;
}

/* Quick-switch button — same visual style as the stop button */
.portal-impersonation-switch-btn {
    background-color: rgba(0, 0, 0, 0.22);
    border: 1px solid rgba(255, 255, 255, 0.35);
    color: #fff;
    font-size: var(--portal-text-sm);
    padding: .2rem .65rem;
    transition: background-color var(--portal-dur-1, 120ms);
    white-space: nowrap;
}

.portal-impersonation-switch-btn:hover,
.portal-impersonation-switch-btn:focus {
    background-color: rgba(0, 0, 0, 0.35);
    color: #fff;
    border-color: rgba(255, 255, 255, 0.5);
}

.portal-impersonation-switch-menu {
    min-width: 240px;
    max-width: 320px;
}

.portal-impersonation-switch-results {
    max-height: 220px;
    overflow-y: auto;
}

.portal-impersonation-switch-item {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    width: 100%;
    text-align: left;
    padding: .3rem .75rem;
}

.portal-impersonation-switch-empty {
    font-size: var(--portal-text-sm);
    color: var(--bs-secondary-color);
    padding: .3rem .75rem;
}

.portal-impersonation-stop-btn {
    background-color: rgba(0, 0, 0, 0.22);
    border: 1px solid rgba(255, 255, 255, 0.35);
    color: #fff;
    font-size: var(--portal-text-sm);
    padding: .2rem .65rem;
    transition: background-color var(--portal-dur-1, 120ms);
}

.portal-impersonation-stop-btn:hover {
    background-color: rgba(0, 0, 0, 0.35);
    color: #fff;
    border-color: rgba(255, 255, 255, 0.5);
}

/* Push the chrome down when the banner is visible.
   The height is MEASURED (portal-layout.js writes --portal-impersonation-height
   from a ResizeObserver) because 36px is only true on one line: the banner's
   inner row wraps at phone widths, and in a standalone PWA it also carries the
   status-bar inset. The 36px literal survives as the no-JS fallback.
   Re-pointing --portal-chrome-offset-top here grows the .portal-content-offset
   spacer for the banner, so content starts below it. The banner already
   carries the safe-top inside its measured height, so the formula swaps
   safe-top for the banner rather than adding both. Custom properties
   inherit by proximity, so this body-level re-point beats navigation.css's
   :root definition regardless of sheet order. */
body:has(.portal-impersonation-banner) {
    --portal-chrome-offset-top: calc(
        var(--portal-impersonation-height, 36px) + var(--portal-content-top-gap)
    );
}

/* The sidebar needed its own copy of that offset here, as `top` plus a
   `height`. It does not any more (2026-08-28): the returning slab takes
   --portal-chrome-offset-top for its top and leaves `bottom` to do the rest,
   so the re-point above moves it. The old pair was also subtly wrong — top,
   bottom and height all set over-constrains an absolutely positioned box and
   the browser resolves it by ignoring `bottom`, which is where the slab's
   8px foot inset lived. */

/* ── Global accent utility ──────────────────────────────────────────── */
.text-accent { color: var(--portal-c-accent) !important; }

/* ── Reduced motion ───────────────────────────────────────────────────
 *
 * BEFORE the degrade gates, deliberately: those must stay last in this file,
 * because their re-point selectors tie the token blocks' specificity and win
 * on source order.
 *
 * Everything v3 added to the interaction composition dies here — the press
 * scale on buttons, the lift and press on cards. A press-scale is a small
 * motion, which is exactly the kind people who set this are asking not to
 * have: vestibular triggers are not proportional to how big the movement
 * looks. `transition: none` alone would not be enough on its own, because the
 * transform would still apply, just instantly — so the transform is reset
 * too. */
/* ── Forms & overlays: the Bootstrap pieces the kit forgot (v3 §2.8) ──
   input-group addons, tooltip/popover, kbd/code/pre, range, offcanvas.
   No blur on tooltip/popover/offcanvas — the budget is chrome + the
   topmost overlay, and none of these is it. */
.portal-page-themed .input-group-text {
    background: var(--portal-c-surface-header);
    border-color: var(--portal-c-edge-control);
    color: var(--portal-c-text-muted);
    font-size: var(--portal-text-sm);
    min-height: var(--portal-control-h);
}

body:has(.portal-page-themed) .tooltip {
    --bs-tooltip-bg: var(--portal-c-overlay-opaque);
    --bs-tooltip-color: var(--portal-c-text);
    --bs-tooltip-font-size: var(--portal-text-sm);
    --bs-tooltip-border-radius: var(--bs-border-radius);
    --bs-tooltip-opacity: 1;
}

body:has(.portal-page-themed) .tooltip-inner {
    border: 1px solid var(--portal-c-border);
    box-shadow: var(--portal-c-edge-card), var(--portal-c-elev-menu);
    padding: 0.4rem 0.65rem;
    font-weight: var(--portal-weight-medium);
}

body:has(.portal-page-themed) .popover {
    --bs-popover-bg: var(--portal-c-overlay-opaque);
    --bs-popover-border-color: var(--portal-c-border);
    --bs-popover-header-bg: var(--portal-c-surface-header);
    --bs-popover-body-color: var(--portal-c-text);
    --bs-popover-header-color: var(--portal-c-text);
    --bs-popover-border-radius: var(--bs-border-radius-lg);
    box-shadow: var(--portal-c-edge-card), var(--portal-c-elev-menu);
}

.portal-page-themed kbd {
    background: var(--portal-c-badge-muted-bg);
    border: 1px solid var(--portal-c-edge-control);
    border-bottom-width: 2px;
    border-radius: var(--bs-border-radius-sm);
    font-family: var(--portal-font-mono);
    font-size: var(--portal-text-xs);
    color: var(--portal-c-kbd);
    padding: 0.1rem 0.35rem;
}

.portal-page-themed :not(pre) > code {
    font-family: var(--portal-font-mono);
    font-size: 0.9em;
    color: var(--portal-c-accent);
    background: var(--portal-c-accent-muted);
    padding: 0.1rem 0.3rem;
    border-radius: var(--bs-border-radius-sm);
}

.portal-page-themed pre {
    background: var(--portal-c-input-bg);
    border: 1px solid var(--portal-c-border);
    border-radius: var(--bs-border-radius);
    color: var(--portal-c-text);
}

/* Range — both themes (closes the dark-only thumb; retires judging's fork
   in its sub-commit). Vendor groups written separately on purpose: one
   unknown selector voids a shared rule. */
.portal-page-themed .form-range::-webkit-slider-runnable-track {
    height: 0.375rem;
    background: var(--portal-c-tab-track);
    box-shadow: var(--portal-c-tab-track-inset);
    border-radius: var(--bs-border-radius-pill);
}

.portal-page-themed .form-range::-moz-range-track {
    height: 0.375rem;
    background: var(--portal-c-tab-track);
    box-shadow: var(--portal-c-tab-track-inset);
    border-radius: var(--bs-border-radius-pill);
}

.portal-page-themed .form-range::-webkit-slider-thumb {
    width: 1.125rem;
    height: 1.125rem;
    background: var(--portal-c-accent);
    border: 2px solid var(--portal-c-surface-opaque);
    box-shadow: var(--portal-c-shadow-pressed);
    margin-top: -0.375rem;
}

.portal-page-themed .form-range::-moz-range-thumb {
    width: 1.125rem;
    height: 1.125rem;
    background: var(--portal-c-accent);
    border: 2px solid var(--portal-c-surface-opaque);
    box-shadow: var(--portal-c-shadow-pressed);
}

.portal-page-themed .form-range:focus::-webkit-slider-thumb { box-shadow: var(--portal-c-focus-ring); }
.portal-page-themed .form-range:focus::-moz-range-thumb { box-shadow: var(--portal-c-focus-ring); }

.portal-page-themed .form-range:active::-webkit-slider-thumb {
    background: var(--portal-c-accent);
    background: color-mix(in srgb, var(--portal-c-accent) 85%, #000);
}

.portal-page-themed .form-range:active::-moz-range-thumb {
    background: var(--portal-c-accent);
    background: color-mix(in srgb, var(--portal-c-accent) 85%, #000);
}

@media (pointer: coarse) {
    .portal-page-themed .form-range::-webkit-slider-thumb { width: 1.5rem; height: 1.5rem; margin-top: -0.5625rem; }
    .portal-page-themed .form-range::-moz-range-thumb { width: 1.5rem; height: 1.5rem; }
}

/* Offcanvas: the shell family — opaque, no fifth blur. The
   :not(.portal-sidebar) exclusion went with the drawer (2026-08-28): the
   returning sidebar is a plain fixed slab and was never going to match this. */
body:has(.portal-page-themed) .offcanvas {
    --bs-offcanvas-bg: var(--portal-c-overlay-opaque);
    --bs-offcanvas-color: var(--portal-c-text);
    --bs-offcanvas-border-color: var(--portal-c-border);
    box-shadow: var(--portal-c-edge-dialog), var(--portal-c-elev-overlay);
}

body:has(.portal-page-themed) .offcanvas .offcanvas-header {
    background: var(--portal-c-surface-header);
}

@media (prefers-reduced-motion: reduce) {
    .portal-page-themed .btn,
    .portal-stat-card,
    .portal-accent-card,
    .portal-page-themed .card.portal-hoverable {
        transition: none;
    }

    .portal-page-themed .btn:not(:disabled):active,
    a.portal-stat-card:hover,
    a.portal-stat-card:active,
    .portal-stat-card.portal-hoverable:hover,
    .portal-stat-card.portal-hoverable:active,
    .portal-page-themed .card.portal-hoverable:hover,
    .portal-page-themed .card.portal-hoverable:active {
        transform: none;
    }
}

/* ═════════════════════════════════════════════════════════════════════
   GLASS DEGRADE — where translucency turns off. Three gates, one mechanism:
   (a fourth gate, the parse floor, follows them — it is about COLOUR, not glass)
   re-point the glass tokens to their opaque twins. Everything consuming
   --portal-c-surface / --portal-c-overlay / --portal-blur-* degrades
   automatically; component rules never carry their own fallbacks — sole
   exception: the #portal-lens literals (login.css, global-search.css) must
   carry local mirrors because literals bypass the token gates. Scope-level
   re-points also exist (mission-control's .mc-tv kiosk). --portal-c-scrim is
   deliberately NOT re-pointed (an opaque scrim would blank the page).
   All three gates produce the SAME opaque state — one degraded look to QA.
   Gate 4 below is a different subject entirely; see its own header.

   Gate 1 — engine can't blur. Older WebKit only implements the -webkit-
            spelling, so the probe tests BOTH.
   Gate 2 — user/OS asked for reduced transparency (a11y). Chromium 118+
            honors it; where unsupported, Gate 3 is the ops path.
   Gate 3 — ops kill switch: html[data-portal-glass="off"], stamped
            pre-paint by the base.twig theme snippet from
            localStorage('nhrl-portal-glass'); ?glass=off persists it on a
            device once, ?glass=on clears. No cookie, no server state.

   This block must stay LAST in this file: the re-point selectors tie the
   token blocks' specificity and win by source order. (html[data-bs-theme]
   body ties html[data-bs-theme="dark"] body; the :has() shapes tie too.)
   ═════════════════════════════════════════════════════════════════════ */

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    :root {
        --portal-blur-chrome: none;
        --portal-blur-overlay: none;
        --portal-blur-dim: none;
        /* Un-blurred 0.2-alpha chrome over scrolling content is unreadable —
           snap to the near-opaque fallback (portal-theme.css tokens). */
        --portal-chrome-opacity-dark: var(--portal-chrome-opacity-dark-fallback);
        --portal-chrome-opacity-light: 0.98;
    }

    html[data-bs-theme] body,
    body:has(.portal-page-themed[data-bs-theme]),
    .portal-theme-scope[data-bs-theme] {
        --portal-c-surface: var(--portal-c-surface-opaque);
        --portal-c-surface-raised: var(--portal-c-surface-raised-opaque);
        --portal-c-surface-sunken: var(--portal-c-surface-sunken-opaque);
        --portal-c-overlay: var(--portal-c-overlay-opaque);
    }
}

@media (prefers-reduced-transparency: reduce) {
    :root {
        --portal-blur-chrome: none;
        --portal-blur-overlay: none;
        --portal-blur-dim: none;
        --portal-chrome-opacity-dark: var(--portal-chrome-opacity-dark-fallback);
        --portal-chrome-opacity-light: 0.98;
    }

    html[data-bs-theme] body,
    body:has(.portal-page-themed[data-bs-theme]),
    .portal-theme-scope[data-bs-theme] {
        --portal-c-surface: var(--portal-c-surface-opaque);
        --portal-c-surface-raised: var(--portal-c-surface-raised-opaque);
        --portal-c-surface-sunken: var(--portal-c-surface-sunken-opaque);
        --portal-c-overlay: var(--portal-c-overlay-opaque);
    }
}

html[data-portal-glass="off"] {
    --portal-blur-chrome: none;
    --portal-blur-overlay: none;
    --portal-blur-dim: none;
    --portal-chrome-opacity-dark: var(--portal-chrome-opacity-dark-fallback);
    --portal-chrome-opacity-light: 0.98;
}

html[data-portal-glass="off"] body,
html[data-portal-glass="off"] body:has(.portal-page-themed[data-bs-theme]),
html[data-portal-glass="off"] .portal-theme-scope[data-bs-theme] {
    --portal-c-surface: var(--portal-c-surface-opaque);
    --portal-c-surface-raised: var(--portal-c-surface-raised-opaque);
        --portal-c-surface-sunken: var(--portal-c-surface-sunken-opaque);
    --portal-c-overlay: var(--portal-c-overlay-opaque);
}

/* ═════════════════════════════════════════════════════════════════════
   GATE 4 — THE PARSE FLOOR. Not a glass gate: a colour gate.

   color-mix() inside a custom property is the one construct in this design
   system that fails DANGEROUSLY on an engine that does not support it, and it
   fails in a way no fallback catches.

   A custom property's value is not parse-validated at declaration time. On
   Safari 15 or a 2022 Firefox, `--portal-c-border: color-mix(in srgb, …)` is
   accepted, stored, and marked guaranteed-invalid. The token IS defined — so
   `var(--portal-c-border, rgba(0,0,0,.5))` never reaches its fallback, and
   every consumer computes to the unset value instead. The keyline does not go
   grey; it goes AWAY. Sixteen tokens do this, and between them they carry every
   border, every selected row and every accent tint in the app.

   So this gate re-declares those sixteen with literal equivalents.
   color-mix(in srgb, C p%, transparent) is exactly C at alpha p — verified
   against Chromium's own serialisation, which reports
   color-mix(in srgb, rgb(28,25,54) 14%, transparent) as
   color(srgb 0.109804 0.0980392 0.211765 / 0.14).

   An old engine gets a plainer portal. It never gets a borderless one.

   TWO blocks on purpose. The @supports half is what actually ships; the
   html[data-portal-engine="legacy"] half is how it is TESTED, because the gate
   is unreachable in any browser Playwright can drive. A gate that cannot be
   exercised is a gate nobody knows is broken — the stamp is stamped pre-paint
   by base.twig and by stampHtml() under test. ParseFloorTwinsTest holds the two
   halves to the same sixteen tokens and to the values they mirror, so they
   cannot drift apart or fall behind a retune.

   Like the three gates above, this must stay LAST: the selectors tie the token
   blocks' specificity and win on source order.
   ═════════════════════════════════════════════════════════════════════ */

@supports not (color: color-mix(in srgb, red 50%, blue)) {
    html[data-bs-theme="dark"] body,
    body:has(.portal-page-themed[data-bs-theme="dark"]) {
        --portal-c-accent-muted: rgba(0, 218, 216, 0.12);
        --portal-c-edge-chip: inset 0 1px 0 rgba(255, 255, 255, 0.14),
                              inset 0 -1px 0 rgba(0, 0, 0, 0.25);
        --portal-c-accent-border: rgba(0, 218, 216, 0.3);
        --portal-c-badge-running-bg: rgba(0, 218, 216, 0.14);
        --portal-c-row-selected: rgba(0, 218, 216, 0.12);
        --portal-c-btn-primary-grad: linear-gradient(180deg, #5f577b 0%, #453c66 50%, #3d355a 100%);
        --portal-c-btn-primary-grad-hover: linear-gradient(180deg, #6f6392 0%, #574a80 50%, #4d4171 100%);
        --portal-c-btn-primary-bg-active: #3e365c;
        --portal-c-btn-danger-grad: linear-gradient(180deg, #ce3f3f 0%, #c92a2a 50%, #b92727 100%);
        --portal-c-btn-danger-grad-hover: linear-gradient(180deg, #e34646 0%, #e03131 50%, #ce2d2d 100%);
        --portal-c-btn-success-grad: linear-gradient(180deg, #44a857 0%, #2f9e44 50%, #2b913f 100%);
        --portal-c-btn-success-grad-hover: linear-gradient(180deg, #4bba5e 0%, #37b24c 50%, #33a446 100%);
        --portal-c-btn-warning-grad: linear-gradient(180deg, #dd8e40 0%, #d9822b 50%, #c87828 100%);
        --portal-c-btn-warning-grad-hover: linear-gradient(180deg, #ee9a45 0%, #ec8f30 50%, #d9842c 100%);
        --portal-c-input-border-invalid: rgba(255, 125, 125, 0.55);
        --portal-c-input-ring-invalid: 0 0 0 1px rgba(255, 125, 125, 0.45), 0 0 0 0.25rem rgba(255, 125, 125, 0.14);
        --portal-c-input-border-valid: rgba(69, 211, 129, 0.55);
        --portal-c-input-ring-valid: 0 0 0 1px rgba(69, 211, 129, 0.45), 0 0 0 0.25rem rgba(69, 211, 129, 0.14);
        --portal-c-corner-pink-ink: #d96dd8;
        --portal-c-corner-blue-ink: #87e2e5;
        --portal-c-corner-pink-soft: rgba(207, 35, 202, 0.14);
        --portal-c-corner-blue-soft: rgba(0, 218, 216, 0.14);
        --portal-c-corner-pink-border: rgba(207, 35, 202, 0.45);
        --portal-c-corner-blue-border: rgba(0, 218, 216, 0.45);
    }

    html[data-bs-theme="light"] body,
    body:has(.portal-page-themed[data-bs-theme="light"]) {
        --portal-c-border: rgba(28, 25, 54, 0.14);
        --portal-c-edge-chip: inset 0 1px 0 rgba(255, 255, 255, 0.85),
                              inset 0 -1px 0 rgba(28, 25, 54, 0.035);
        --portal-c-border-subtle: rgba(28, 25, 54, 0.08);
        --portal-c-accent-muted: rgba(10, 124, 122, 0.14);
        --portal-c-accent-border: rgba(10, 124, 122, 0.4);
        --portal-c-badge-live-bg: rgba(10, 124, 122, 0.1);
        --portal-c-badge-running-bg: rgba(10, 124, 122, 0.1);
        --portal-c-row-selected: rgba(10, 124, 122, 0.14);
        --portal-c-btn-primary-grad: linear-gradient(180deg, #473c75 0%, #291c5e 50%, #241953 100%);
        --portal-c-btn-primary-grad-hover: linear-gradient(180deg, #564892 0%, #3a2a80 50%, #332571 100%);
        --portal-c-btn-primary-bg-active: #251955;
        --portal-c-btn-danger-grad: linear-gradient(180deg, #ce3f3f 0%, #c92a2a 50%, #b92727 100%);
        --portal-c-btn-danger-grad-hover: linear-gradient(180deg, #e34646 0%, #e03131 50%, #ce2d2d 100%);
        --portal-c-btn-success-grad: linear-gradient(180deg, #44a857 0%, #2f9e44 50%, #2b913f 100%);
        --portal-c-btn-success-grad-hover: linear-gradient(180deg, #4bba5e 0%, #37b24c 50%, #33a446 100%);
        --portal-c-btn-warning-grad: linear-gradient(180deg, #dd8e40 0%, #d9822b 50%, #c87828 100%);
        --portal-c-btn-warning-grad-hover: linear-gradient(180deg, #ee9a45 0%, #ec8f30 50%, #d9842c 100%);
        --portal-c-input-border-invalid: rgba(201, 42, 42, 0.55);
        --portal-c-input-ring-invalid: 0 0 0 1px rgba(201, 42, 42, 0.45), 0 0 0 0.25rem rgba(201, 42, 42, 0.14);
        --portal-c-input-border-valid: rgba(43, 138, 62, 0.55);
        --portal-c-input-ring-valid: 0 0 0 1px rgba(43, 138, 62, 0.45), 0 0 0 0.25rem rgba(43, 138, 62, 0.14);
        --portal-c-corner-pink-ink: #8b208b;
        --portal-c-corner-blue-ink: #106b70;
        --portal-c-corner-pink-soft: rgba(207, 35, 202, 0.14);
        --portal-c-corner-blue-soft: rgba(0, 218, 216, 0.14);
        --portal-c-corner-pink-border: rgba(207, 35, 202, 0.45);
        --portal-c-corner-blue-border: rgba(0, 218, 216, 0.45);
        --portal-c-btn-outline-border: rgba(28, 25, 54, 0.18);
        --portal-c-btn-outline-bg: rgba(28, 25, 54, 0.03);
        --portal-c-btn-outline-color: #656376;
        --portal-c-btn-outline-hover-bg: rgba(28, 25, 54, 0.05);
        --portal-c-btn-outline-hover-color: #37354e;
        --portal-c-btn-outline-hover-border: rgba(28, 25, 54, 0.24);
    }

    /* Matrix declares its own four with !important, so the twins must too. */
    html[data-portal-theme="matrix"] body {
        --portal-c-accent-muted: rgba(57, 255, 106, 0.14) !important;
        --portal-c-accent-border: rgba(57, 255, 106, 0.32) !important;
        --portal-c-badge-running-bg: rgba(57, 255, 106, 0.16) !important;
        --portal-c-row-selected: rgba(57, 255, 106, 0.14) !important;
    }
}

/* The same floor, reachable on a modern engine so it can be tested and
   photographed. base.twig stamps data-portal-engine="legacy" when the engine
   fails the same probe; the gallery and parse-floor.spec.js stamp it directly. */
html[data-portal-engine="legacy"][data-bs-theme="dark"] body,
html[data-portal-engine="legacy"] body:has(.portal-page-themed[data-bs-theme="dark"]) {
    --portal-c-accent-muted: rgba(0, 218, 216, 0.12);
    --portal-c-edge-chip: inset 0 1px 0 rgba(255, 255, 255, 0.14),
                          inset 0 -1px 0 rgba(0, 0, 0, 0.25);
    --portal-c-accent-border: rgba(0, 218, 216, 0.3);
    --portal-c-badge-running-bg: rgba(0, 218, 216, 0.14);
    --portal-c-row-selected: rgba(0, 218, 216, 0.12);
    --portal-c-btn-primary-grad: linear-gradient(180deg, #5f577b 0%, #453c66 50%, #3d355a 100%);
    --portal-c-btn-primary-grad-hover: linear-gradient(180deg, #6f6392 0%, #574a80 50%, #4d4171 100%);
    --portal-c-btn-primary-bg-active: #3e365c;
    --portal-c-btn-danger-grad: linear-gradient(180deg, #ce3f3f 0%, #c92a2a 50%, #b92727 100%);
    --portal-c-btn-danger-grad-hover: linear-gradient(180deg, #e34646 0%, #e03131 50%, #ce2d2d 100%);
    --portal-c-btn-success-grad: linear-gradient(180deg, #44a857 0%, #2f9e44 50%, #2b913f 100%);
    --portal-c-btn-success-grad-hover: linear-gradient(180deg, #4bba5e 0%, #37b24c 50%, #33a446 100%);
    --portal-c-btn-warning-grad: linear-gradient(180deg, #dd8e40 0%, #d9822b 50%, #c87828 100%);
    --portal-c-btn-warning-grad-hover: linear-gradient(180deg, #ee9a45 0%, #ec8f30 50%, #d9842c 100%);
    --portal-c-input-border-invalid: rgba(255, 125, 125, 0.55);
    --portal-c-input-ring-invalid: 0 0 0 1px rgba(255, 125, 125, 0.45), 0 0 0 0.25rem rgba(255, 125, 125, 0.14);
    --portal-c-input-border-valid: rgba(69, 211, 129, 0.55);
    --portal-c-input-ring-valid: 0 0 0 1px rgba(69, 211, 129, 0.45), 0 0 0 0.25rem rgba(69, 211, 129, 0.14);
    --portal-c-corner-pink-ink: #d96dd8;
    --portal-c-corner-blue-ink: #87e2e5;
    --portal-c-corner-pink-soft: rgba(207, 35, 202, 0.14);
    --portal-c-corner-blue-soft: rgba(0, 218, 216, 0.14);
    --portal-c-corner-pink-border: rgba(207, 35, 202, 0.45);
    --portal-c-corner-blue-border: rgba(0, 218, 216, 0.45);
}

html[data-portal-engine="legacy"][data-bs-theme="light"] body,
html[data-portal-engine="legacy"] body:has(.portal-page-themed[data-bs-theme="light"]) {
    --portal-c-border: rgba(28, 25, 54, 0.14);
    --portal-c-edge-chip: inset 0 1px 0 rgba(255, 255, 255, 0.85),
                          inset 0 -1px 0 rgba(28, 25, 54, 0.035);
    --portal-c-border-subtle: rgba(28, 25, 54, 0.08);
    --portal-c-accent-muted: rgba(10, 124, 122, 0.14);
    --portal-c-accent-border: rgba(10, 124, 122, 0.4);
    --portal-c-badge-live-bg: rgba(10, 124, 122, 0.1);
    --portal-c-badge-running-bg: rgba(10, 124, 122, 0.1);
    --portal-c-row-selected: rgba(10, 124, 122, 0.14);
    --portal-c-btn-primary-grad: linear-gradient(180deg, #473c75 0%, #291c5e 50%, #241953 100%);
    --portal-c-btn-primary-grad-hover: linear-gradient(180deg, #564892 0%, #3a2a80 50%, #332571 100%);
    --portal-c-btn-primary-bg-active: #251955;
    --portal-c-btn-danger-grad: linear-gradient(180deg, #ce3f3f 0%, #c92a2a 50%, #b92727 100%);
    --portal-c-btn-danger-grad-hover: linear-gradient(180deg, #e34646 0%, #e03131 50%, #ce2d2d 100%);
    --portal-c-btn-success-grad: linear-gradient(180deg, #44a857 0%, #2f9e44 50%, #2b913f 100%);
    --portal-c-btn-success-grad-hover: linear-gradient(180deg, #4bba5e 0%, #37b24c 50%, #33a446 100%);
    --portal-c-btn-warning-grad: linear-gradient(180deg, #dd8e40 0%, #d9822b 50%, #c87828 100%);
    --portal-c-btn-warning-grad-hover: linear-gradient(180deg, #ee9a45 0%, #ec8f30 50%, #d9842c 100%);
    --portal-c-input-border-invalid: rgba(201, 42, 42, 0.55);
    --portal-c-input-ring-invalid: 0 0 0 1px rgba(201, 42, 42, 0.45), 0 0 0 0.25rem rgba(201, 42, 42, 0.14);
    --portal-c-input-border-valid: rgba(43, 138, 62, 0.55);
    --portal-c-input-ring-valid: 0 0 0 1px rgba(43, 138, 62, 0.45), 0 0 0 0.25rem rgba(43, 138, 62, 0.14);
    --portal-c-corner-pink-ink: #8b208b;
    --portal-c-corner-blue-ink: #106b70;
    --portal-c-corner-pink-soft: rgba(207, 35, 202, 0.14);
    --portal-c-corner-blue-soft: rgba(0, 218, 216, 0.14);
    --portal-c-corner-pink-border: rgba(207, 35, 202, 0.45);
    --portal-c-corner-blue-border: rgba(0, 218, 216, 0.45);
    --portal-c-btn-outline-border: rgba(28, 25, 54, 0.18);
    --portal-c-btn-outline-bg: rgba(28, 25, 54, 0.03);
    --portal-c-btn-outline-color: #656376;
    --portal-c-btn-outline-hover-bg: rgba(28, 25, 54, 0.05);
    --portal-c-btn-outline-hover-color: #37354e;
    --portal-c-btn-outline-hover-border: rgba(28, 25, 54, 0.24);
}

html[data-portal-engine="legacy"][data-portal-theme="matrix"] body {
    --portal-c-accent-muted: rgba(57, 255, 106, 0.14) !important;
    --portal-c-accent-border: rgba(57, 255, 106, 0.32) !important;
    --portal-c-badge-running-bg: rgba(57, 255, 106, 0.16) !important;
    --portal-c-row-selected: rgba(57, 255, 106, 0.14) !important;
}
