/* =============================================================================
 * Frihat Legal Platform — Design System / 08 FEEDBACK
 * -----------------------------------------------------------------------------
 * Everything that tells the user what just happened: alerts, toasts, badges and
 * status chips, empty states, skeletons, spinners and loading overlays, progress
 * bars, the SweetAlert2 skin, ABP's own message and busy surfaces, and the
 * dedicated inline error class the project rule requires.
 *
 * SLICE BOUNDARIES
 *   Buttons             belong to 03-buttons.css   (we only POSITION them here)
 *   Forms and validation
 *   message placement   belong to 04-forms.css
 *   Modal, dropdown,
 *   card, dialog shells belong to 07-surfaces.css  (this file matches them)
 *
 * CONTRACT
 *   Every colour comes from a `fx-*` token declared in 00-tokens.css. There is
 *   no raw hex, no rgb(), no `@import`, no `@layer`.
 *
 *   `!important` APPEARS EXACTLY FIVE TIMES — FOUR IN SECTION 8 AND ONE IN
 *   SECTION 2 — AND THE RULE IS NARROWER THAN "AVOID IT". LeptonXLite's
 *   `js-bundle.rtl.css` paints the SweetAlert2 popup with `background: #ffffff
 *   !important`, `color: #686b6e !important` and a flat `border-radius
 *   !important` on both the popup and its buttons, with no dark counterpart —
 *   which is why the client saw a white confirmation dialog in dark mode. The
 *   same bundle also ships `#toast-container > div { opacity: 0.95 !important }`,
 *   which held the application's most frequent notification permanently
 *   translucent over whatever it happened to land on; that one is answered in
 *   section 2. An important author declaration can only be answered by another
 *   one, so the flag in both places is not a shortcut, it is the only available
 *   mechanism. A new `!important` in this file is admissible only if it CANCELS
 *   a specific vendor `!important`, names that rule in a comment at the call
 *   site, and reads a variable or a neutral identity value rather than a
 *   hardcoded colour. Nothing else qualifies.
 *   Logical properties throughout, so RTL is correct by construction. Physical
 *   values appear only inside transforms and symmetric gradients, each commented.
 *   There is not one hardcoded duration in this file. Every animation reads a
 *   `fx-dur-*` token or is derived from one by `calc()`, which is what lets
 *   the motion damper in 01-base reach all of it.
 *   The single exception is `fx-toast-duration`, and it is an exception on
 *   purpose: it measures how long a message stays READABLE, not how long a
 *   thing takes to move, so it is the one time token 00-tokens leaves undamped
 *   under `prefers-reduced-motion`. Two places consume it — the toast progress
 *   bar in section 2, and the slowed spinner in section 11, which needs a
 *   period that SURVIVES the damper for the same human perception reason.
 *   Reduced motion must never buy less motion at the cost of less reading time.
 *   `transition-property: all` appears nowhere. Negative `letter-spacing` is
 *   never typed — only read from a `fx-tracking-*` token, which the Arabic
 *   guard in 00-tokens zeroes.
 *
 * ══ THE 2026-08-02 LINEAR FEEDBACK RETUNE — READ THIS BEFORE EDITING ═══════
 *
 * THERE IS NO TINTED STATUS FILL LEFT IN THIS FILE, AND NO COLOURED RAIL.
 *   The previous pass expressed severity as a pastel background plus a 4px
 *   accent bar on the inline start edge. The reference does neither. An alert
 *   is a NEUTRAL PANEL — the same surface, hairline and corner a card has —
 *   and the status is carried by TWO things only: the colour of a 16px icon and
 *   the colour of the text. A toast is the same: a plain floating card with a
 *   coloured glyph, no rail, no wash. The `fx-alert-accent` /
 *   `fx-alert-glyph` and `fx-toast-accent` locals all still work; they now
 *   drive the icon rather than the surface.
 *
 * DEPTH IS THREE WEAK SIGNALS STACKED, NEVER ONE STRONG ONE.
 *   A floating surface here is a background LEVEL step, plus a 1px TRANSLUCENT
 *   hairline, plus one weak shadow. Nothing in the light theme exceeds 10%
 *   alpha. If something in this file ever reads as elevated because of a single
 *   strong shadow, that is the defect.
 *
 * TWO LINE FAMILIES, KEPT APART.
 *   `fx-color-border*` are TRANSLUCENT and describe the EDGE of a floating or
 *   elevated surface — a toast, an alert panel, a confirmation dialog.
 *   `fx-color-divider*` are OPAQUE and describe an in-flow SEPARATOR inside a
 *   surface whose colour is already known. Never swap one for the other.
 *
 * ACCENT DISCIPLINE (00-tokens 3.9, REWRITTEN 2026-08-02, third pass). The rule
 *   is now IDENTITY AND CURRENT POSITION ARE BRAND, INTERACTION IS NEUTRAL,
 *   over TEN numbered sites rather than six. Re-checked site by site against the
 *   new contract and this file's brand content is unchanged — which is the
 *   correct outcome, because feedback is what happens AFTER an interaction and
 *   almost none of it answers "who is this product" or "where am I". FOUR
 *   sanctioned sites appear here and nothing else may carry brand:
 *     · THE FILLED BADGE — SITE 7, and it is this file's one brand FILL. It is
 *       deliberately the EXCEPTION: three of the four badge variants are
 *       OUTLINE ONLY with a transparent background. A badge REPORTING A STATE
 *       is the sanctioned case; a chip that merely shows where the pointer
 *       landed is a grey wash, which is why `.fx-chip-primary` stays neutral.
 *     · the SweetAlert2 CONFIRM button — SITE 6, the primary action, which the
 *       library renders outside the `.btn` family so its fill has to be mapped
 *       here rather than in the buttons slice.
 *     · ALERT LINK TEXT — SITE 8, through `fx-color-link`. Underlined link
 *       text is brand everywhere in the platform and an alert is no exception.
 *     · THE FOCUS RING — SITE 5, inherited through `fx-color-focus-ring` on
 *       the chip remove control and the swal2 fields and buttons. It is brand
 *       in BOTH themes now, where it used to be brand in dark only: a ring
 *       reports WHERE THE KEYBOARD IS, which is position, not response.
 *   Everything else is neutral and the FORBIDDEN list names each of them by
 *   type — an alert or toast SURFACE, a progress bar, a spinner: the toast auto
 *   dismiss bar, the spinner ring, the swal2 timer bar and the swal2 question
 *   icon all read a TEXT TIER instead. Nothing here lost a legitimate brand
 *   site during the over strict pass, and nothing here is owed one back.
 *
 *   THE TWO NEW BRAND TOKENS ARE NOT INTERCHANGEABLE, should a future edit
 *   reach for one: `fx-color-indicator` is a BAR, a rule or a glyph, measured
 *   against SC 1.4.11 at 3:1; `fx-color-brand-text` is anything READ AS TEXT,
 *   measured against SC 1.4.3 at 4.5:1, which is why it resolves to olive on
 *   the dark page — brand green cannot be text there, the frozen ramp tops out
 *   below the threshold. Neither is consumed in this file.
 *
 * FOCUS IS AN OUTLINE. `outline: 2px solid var(fx-color-focus-ring)` at
 *   `outline-offset: 2px` on `:focus-visible`, and nothing else. No bloom, no
 *   halo, no `fx-shadow-focus-glow` — that token is DEPRECATED and appears
 *   nowhere below. `fx-shadow-well` is a different matter and is FROZEN: its
 *   strongest band is the WCAG 2.2 SC 1.4.11 guarantee that makes a borderless
 *   text control identifiable at 3:1, and it stays on every swal2 field.
 *
 * HOVER IS GATED behind `@media (any-hover: hover)`.
 *
 * MOTION. Nothing is authored in the 200 to 400ms band. This file used to carry
 *   a paragraph refusing to use `fx-dur-slow` because the token was 250ms and
 *   therefore sat inside that band. THAT CONTRADICTION IS SETTLED: the token is
 *   180ms now and its MEANING is "a self contained entrance the user is not
 *   waiting on". It is consequently the SweetAlert2 arrival timing here, so a
 *   confirmation and the `.modal` in 07 land at the same speed as one object.
 *   A message that appears in place — an alert, a toast, an inline error — is
 *   NOT that case: it interrupts what the user is doing and stays on
 *   `fx-dur-base`, and a scrim fades one rung under the dialog it sits behind.
 *   Every entrance sits inside `@media (prefers-reduced-motion: no-preference)`.
 *   Looping indicators — the skeleton sweep, the spinner, the indeterminate
 *   progress sliver — are derived from `fx-dur-slower` and are periods rather
 *   than transitions, so the band rule does not apply to them.
 *
 * GLASSMORPHISM IS RETIRED. There is no `backdrop-filter` left in this file:
 *   the loading overlay and the SweetAlert2 scrim are flat colours. The
 *   `fx-glass-*` tokens survive in 00-tokens but nothing here consumes them.
 *
 * NEVER COLOUR ALONE
 *   Alerts, chips and status pills all carry a glyph or a distinct SHAPE in
 *   addition to their hue, so meaning survives for a colour blind reader and in
 *   a monochrome print. With the tinted fills gone the icon and the marker
 *   shape are doing MORE of that work, not less, so neither is optional. See
 *   sections 1 and 3.
 *
 * TYPE
 *   Sized off the compact token scale (base 15px). Badges and chips carry an
 *   explicit `max(0.75rem, ...)` floor so they can never render below 12px no
 *   matter how the scale is retuned, and they are the one thing in this file at
 *   weight 600 — everything else is 500 or 400. Hierarchy is a COLOUR TIER.
 *
 * TOUCH TARGETS (WCAG 2.2 SC 2.5.8)
 *   Smallest interactive target in this slice is 24x24 CSS px (the chip remove
 *   control); every other control is 28px or more. All rise to 44px under
 *   `@media (pointer: coarse)`.
 *
 * FAB CLEARANCE
 *   Toasts and the FAB both want the block end inline end corner. ASSUMPTION:
 *   the FAB owns that corner and the TOAST STACK steps aside, lifting itself by
 *   `fx-fab-clearance` (default 4.5rem). A page with no FAB can flatten the
 *   stack back down with `fx-fab-clearance: 0`.
 * ========================================================================== */


/* ── 1) Alerts ──────────────────────────────────────────────────────────────
 * A NEUTRAL PANEL WITH A COLOURED ICON. There is no tinted fill and no accent
 * rail: the surface is one level step with a translucent hairline and an 8px
 * corner — structurally the same object as a card — and the ONLY things that
 * carry severity are the icon's colour and the heading's colour.
 *
 * WHY THE RAIL WENT. A 4px coloured bar down one edge is a rail, and a rail is
 * a brand-shaped decoration on a component whose job is to be read. The
 * reference has none. Removing it also removed the last place in this file
 * where a status hue was painted as a large area rather than as a small mark.
 *
 * ACCESSIBILITY: the icon is REQUIRED, not decorative — with the fill gone it
 * is now HALF the signal rather than a redundancy. Markup should supply
 * `<i class="alert-icon ..."></i>`. When it does not, the `::before` fallback
 * draws a variant specific GLYPH inside a variant specific SHAPE (circle versus
 * square), which keeps the meaning legible in greyscale. Pair every alert with
 * `role="alert"` for assertive messages or `role="status"` for passive ones.
 * ------------------------------------------------------------------------- */
.alert {
    /* NEUTRAL. Level 2 rather than level 1 so the panel reads on a level 0 page
     * and on a level 1 card alike. */
    --bs-alert-bg: var(--fx-color-level-2, var(--fx-color-bg-subtle));
    --bs-alert-color: var(--fx-color-text-muted);
    --bs-alert-border: var(--fx-border-width);
    --bs-alert-border-color: var(--fx-color-border-subtle);
    --bs-alert-border-radius: var(--fx-radius-md);
    --bs-alert-link-color: var(--fx-color-link);
    --bs-alert-padding-y: var(--fx-space-3);
    --bs-alert-padding-x: var(--fx-space-4);

    /* Local pair every variant re-points. These now drive the ICON and the
     * HEADING, never the surface. */
    --fx-alert-accent: var(--fx-color-text-subtle);
    --fx-alert-glyph: "i";

    display: flex;
    align-items: flex-start;
    gap: var(--fx-space-3);
    max-inline-size: 100%;
    min-inline-size: 0;
    font-size: var(--fx-text-sm);
    line-height: var(--fx-leading-normal, 1.7);
    letter-spacing: var(--fx-tracking-ui);
    overflow-wrap: anywhere;
}

/* Icon slot, whether markup supplied or generated. 16px — the default icon size
 * everywhere in the system. */
.alert > .alert-icon,
.alert::before {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--fx-icon-md);
    block-size: var(--fx-icon-md);
    margin-block-start: 1px;
    font-size: var(--fx-text-2xs);
    font-weight: var(--fx-weight-bold, 700);
    font-style: normal;
    line-height: 1;
    /* THE STATUS, half of it. */
    color: var(--fx-alert-accent);
}
/* Generated fallback marker: only when the markup did not provide an icon.
 * The `border` here DRAWS THE MARKER — it is the ring of a glyph badge, not a
 * stroke around a box — which is why it survives. Tokenised so it thickens with
 * everything else under `prefers-contrast: more`. */
.alert:not(:has(> .alert-icon))::before {
    content: var(--fx-alert-glyph);
    border: var(--fx-border-width-strong) solid currentColor;
    /* Circle is the neutral shape; the warning and danger variants swap it for
     * a square below, so the SHAPE differs and not only the hue. */
    border-radius: var(--fx-radius-circle);
}
.alert > .alert-content {
    flex: 1 1 auto;
    min-inline-size: 0;
}
/* THE STATUS, the other half. The heading takes the accent colour so severity
 * survives for a reader who cannot resolve a 16px glyph. */
.alert-heading {
    font-size: var(--fx-text-base, var(--fx-text-md));
    font-weight: var(--fx-weight-semibold, 600);
    letter-spacing: var(--fx-tracking-title);
    color: var(--fx-alert-accent);
}
.alert-link {
    color: var(--bs-alert-link-color);
    font-weight: var(--fx-weight-medium, 500);
}

/* THE VARIANTS RE-POINT TWO LOCALS AND NOTHING ELSE. Every `bs-alert-bg`,
 * `bs-alert-color` and `bs-alert-border-color` override that used to live
 * in these blocks is gone: a tinted alert surface is exactly what this pass
 * removed, and `.alert-primary` was additionally painting a BRAND wash. The
 * rewritten accent contract does not restore it — an ALERT OR TOAST SURFACE is
 * named on the FORBIDDEN list in its own right and routes to the status
 * families instead. */
.alert-success {
    --fx-alert-accent: var(--fx-color-success-fg);
    --fx-alert-glyph: "\2713";  /* check mark */
}
.alert-warning {
    --fx-alert-accent: var(--fx-color-warning-fg);
    --fx-alert-glyph: "!";
}
.alert-danger {
    --fx-alert-accent: var(--fx-color-danger-fg);
    --fx-alert-glyph: "\00D7";  /* multiplication sign */
}
.alert-info {
    --fx-alert-accent: var(--fx-color-info-fg);
    --fx-alert-glyph: "i";
}
/* No brand tint. A "primary" alert is simply the neutral panel with a primary
 * text heading, which is what an emphasised notice looks like in the reference. */
.alert-primary {
    --fx-alert-accent: var(--fx-color-text);
    --fx-alert-glyph: "\2022";  /* bullet */
}
/* Shape differentiation: the two "stop and read" variants get a square marker,
 * so severity is distinguishable without perceiving colour. The literal
 * fallback is deliberate — this radius is carrying a NON COLOUR accessibility
 * signal, so it must not silently resolve to `unset` if the token ever fails to
 * parse. */
.alert-warning:not(:has(> .alert-icon))::before,
.alert-danger:not(:has(> .alert-icon))::before {
    border-radius: var(--fx-radius-xs, 4px);
}

/* Arrives rather than appears, and only where motion is welcome.
 *
 * WHY THIS ONE IS A BARE FADE WHEN THE CONTRACT NORMALLY FORBIDS ONE. 00-tokens
 * section 7 bans a bare fade for a POPUP — something that arrives over the page
 * from a trigger — and routes those to opacity plus scale anchored to the
 * control. An alert is the other kind of thing entirely: it is IN FLOW, it has
 * no trigger to grow out of, and scaling a full width panel from 96 percent
 * makes the paragraph inside it visibly resize. 01-base's own vocabulary assigns
 * fx-fade-in to exactly this case. The rise entrance was costed and rejected for
 * a specific reason rather than a general one: `.lpx-content` already rises the
 * whole main region on every Razor navigation, so a static alert on a settings
 * page would travel 16px in two nested stages on first paint, and the second
 * 8px would be motion nobody asked for on an element that never moved.
 *
 * `backwards`, not `both` — THE FILL MODE RULE in 01-base section 14, and on
 * THIS component it fixes a live bug rather than a theoretical one. Bootstrap
 * dismisses an `.alert.fade` by taking `.show` off and letting the base rule
 * fall the element to opacity 0. A forward filled entrance outranks that base
 * rule, so a dismissed alert stayed fully visible until the vendor unmounted the
 * node — and an alert built as `fade` without `show` was forced visible from the
 * start. Handing the element back to the cascade is what makes the vendor's own
 * hide work, and is what gives the exit below a value to leave from. */
@media (prefers-reduced-motion: no-preference) {
    .alert {
        animation: fx-fade-in var(--fx-dur-base) var(--fx-ease-standard) backwards;
    }
}
/* THE DISMISSAL, TOKENISED. Added 2026-08-03, and it was the last hardcoded
 * duration governing anything this file paints. Bootstrap ships `.fade` as
 * `transition: opacity .15s linear` at (0,1,0) and its Alert waits for that
 * transition before removing the node, so 150ms of LINEAR fade was the real
 * dismissal of every alert on the platform: off the duration ladder, on no
 * curve the system uses, and — because it is a literal rather than a token —
 * invisible to the reduced motion damper in 01-base, which is the part that
 * makes it a defect rather than a preference. A two class selector outranks the
 * vendor without an importance flag. 60ms on `fx-ease-in-quad` is the exit pair
 * every other surface in this file and in 07 leaves on, so a dismissed alert, a
 * dismissed toast and a dismissed dialog now agree with each other. */
.alert.fade {
    transition: opacity var(--fx-dur-exit) var(--fx-ease-in-quad);
}

/* Dismiss control. Bootstrap pins it with `right: 0`, which is the physical
 * right in the LTR build we serve; both inline sides are set explicitly here so
 * the result is correct under either direction regardless of source order.
 * SC 2.5.8: 28x28 floor, 44x44 on coarse pointers (see section 10). */
.alert-dismissible {
    padding-inline-end: calc(var(--fx-space-4) + var(--fx-control-h-sm));
}
.alert-dismissible .btn-close {
    inset-block-start: 0;
    inset-inline-end: 0;
    inset-inline-start: auto;
    padding: var(--fx-space-3);
    min-inline-size: var(--fx-control-h-sm);
    min-block-size: var(--fx-control-h-sm);
}


/* ── 2) Toasts ──────────────────────────────────────────────────────────────
 * THE ANATOMY, and every number in it is load bearing:
 *
 *   WIDTH     364px, and 100% of the strip below the small breakpoint
 *   PADDING   16px block, 16px inline start, 24px inline END. The asymmetry is
 *             deliberate: the dismiss control lives at the inline end and needs
 *             room that the message text must not run into.
 *   SURFACE   `fx-color-level-3` — the floating tier every overlay in 07 uses
 *   EDGE      1px `fx-color-border`, TRANSLUCENT
 *   CORNER    `fx-radius-sm` (6px). A toast is a small object.
 *   SHADOW    `fx-shadow-float-md`
 *   STATUS    carried by the 16px ICON'S COLOUR ONLY. No coloured left rail, no
 *             tinted background. The rail that used to run down the inline
 *             start edge is gone.
 *
 * LAYOUT NOTE. The toast is a two column GRID — an icon column and a content
 * column — rather than a flex row, because `.toast-header` and `.toast-body`
 * are SIBLINGS in the Bootstrap markup and a flex row would put them side by
 * side. The grid lets the generated icon span both rows in column one while
 * the header and body stack in column two, with no wrapper element and no
 * change to any markup. Grid columns follow the inline direction, so RTL is
 * correct with no mirrored rule.
 *
 * ACCESSIBILITY: a toast container should carry `aria-live="polite"` and
 * `aria-atomic="true"`, with each toast `role="status"`. Use `role="alert"` with
 * `aria-live="assertive"` only for errors the user must act on now. A toast that
 * auto dismisses must never be the only place an error is reported.
 * ------------------------------------------------------------------------- */
.toast {
    --bs-toast-zindex: var(--fx-z-toast);
    /* LEVEL 3 explicitly, not `surface-raised`. The two agree in light, but in
     * dark `surface-raised` is level 2 — the ENGAGED plane, where hover states
     * and field wells live — and a toast floating over the page must not share
     * a plane with a hovered row underneath it. */
    --bs-toast-bg: var(--fx-color-level-3, var(--fx-color-surface-raised));
    --bs-toast-color: var(--fx-color-text-muted);
    --bs-toast-border-width: var(--fx-border-width);
    --bs-toast-border-color: var(--fx-color-border);
    --bs-toast-border-radius: var(--fx-radius-sm);
    --bs-toast-box-shadow: var(--fx-shadow-float-md, var(--fx-shadow-lg));
    /* No tinted header band, matching the card and the modal. */
    --bs-toast-header-bg: transparent;
    --bs-toast-header-color: var(--fx-color-text);
    --bs-toast-header-border-color: transparent;
    --bs-toast-padding-y: var(--fx-space-4);
    --bs-toast-padding-x: var(--fx-space-4);
    --bs-toast-spacing: var(--fx-space-3);
    --bs-toast-font-size: var(--fx-text-sm);
    /* 364px, viewport clamped. */
    --bs-toast-max-width: min(22.75rem, calc(100vw - var(--fx-gutter) * 2));
    /* Status colour and fallback glyph. Re-pointed by the variants below; these
     * drive the ICON only. */
    --fx-toast-accent: var(--fx-color-text-subtle);
    --fx-toast-glyph: "i";

    position: relative;
    overflow: hidden;
    inline-size: 100%;
    max-inline-size: var(--bs-toast-max-width);
    /* The icon column and the content column — see the LAYOUT NOTE above. */
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    column-gap: var(--fx-space-3);
    /* THE ASYMMETRY: extra room at the inline end for the dismiss control. */
    padding-inline-start: var(--fx-space-4);
    padding-inline-end: var(--fx-space-5);
    letter-spacing: var(--fx-tracking-ui);
    pointer-events: auto;
}
/* The status glyph. Generated only when the markup did not supply its own
 * `.toast-icon`, exactly as the alert does. Spans both grid rows so it aligns
 * with the header when there is one and with the body when there is not. */
.toast > .toast-icon,
.toast:not(:has(> .toast-icon))::before {
    grid-column: 1;
    grid-row: 1 / -1;
    align-self: start;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--fx-icon-md);
    block-size: var(--fx-icon-md);
    margin-block-start: 1px;
    font-size: var(--fx-text-2xs);
    font-weight: var(--fx-weight-bold, 700);
    font-style: normal;
    line-height: 1;
    /* THE ONLY THING THAT CARRIES STATUS. */
    color: var(--fx-toast-accent);
}
.toast:not(:has(> .toast-icon))::before {
    content: var(--fx-toast-glyph);
    border: var(--fx-border-width-strong) solid currentColor;
    border-radius: var(--fx-radius-circle);
}
.toast > .toast-header,
.toast > .toast-body {
    grid-column: 2;
    /* The toast owns the inline padding now, so the children give theirs up or
     * the message would be inset twice. */
    padding-inline: 0;
}

/* ENTRANCE: the toast slides in ALONG THE INLINE AXIS from the edge it is
 * anchored to, so it reads as arriving from off screen rather than
 * materialising in place. Toasts and the offcanvas are the only two components
 * in the system with a genuine edge to come from, and are therefore the only
 * two allowed a slide.
 * `fx-slide-in-inline` is the shared keyframe in 01-base.css; the distance and
 * its sign are passed in through `fx-slide-from` so one keyframe serves both
 * scripts. The stack lives at the inline END, so the toast comes from further
 * toward that end — a POSITIVE x in LTR, a negative one in RTL, which is the
 * single mirrored declaration below. */
.toast {
    --fx-slide-from: var(--fx-space-4);
}
[dir="rtl"] .toast {
    --fx-slide-from: calc(var(--fx-space-4) * -1);
}
@media (prefers-reduced-motion: no-preference) {
    /* `backwards`, not `both` — THE FILL MODE RULE in 01-base section 14. The
     * to-frame is `translateX(0)`, an IDENTITY transform, and a forward fill
     * kept it applied for the toast's whole life. An identity transform is still
     * a transform: the toast became a containing block for anything inside it
     * positioned `fixed` and carried a compositing layer it had no use for, all
     * to hold a value the element's own resting style already has. */
    .toast.show {
        animation: fx-slide-in-inline var(--fx-dur-base) var(--fx-ease-decel) backwards;
    }
    /* An exit is faster than its entrance. */
    .toast.hide,
    .toast.hiding {
        animation: fx-toast-out var(--fx-dur-exit) var(--fx-ease-in-quad) both;
    }
}
/* A stack of toasts arrives in sequence, not as a block: put `.fx-stagger` on
 * the container and each toast follows the one before it by 40ms. */
.toast-header {
    font-size: var(--fx-text-xs);
    font-weight: var(--fx-weight-semibold, 600);
    gap: var(--fx-space-2);
    background-color: transparent;
    border-block-end: 0;
}
.toast-header .btn-close {
    margin: 0;
    margin-inline-start: auto;
    min-inline-size: var(--fx-control-h-sm);
    min-block-size: var(--fx-control-h-sm);
}
.toast-body {
    overflow-wrap: anywhere;
}
/* Variants re-point the icon colour and its fallback glyph. Nothing else. */
.toast-success {
    --fx-toast-accent: var(--fx-color-success-fg);
    --fx-toast-glyph: "\2713";
}
.toast-warning {
    --fx-toast-accent: var(--fx-color-warning-fg);
    --fx-toast-glyph: "!";
}
.toast-danger,
.toast-error {
    --fx-toast-accent: var(--fx-color-danger-fg);
    --fx-toast-glyph: "\00D7";
}
.toast-info {
    --fx-toast-accent: var(--fx-color-info-fg);
    --fx-toast-glyph: "i";
}
/* Square marker on the two "stop and read" variants, so severity survives in
 * greyscale here exactly as it does on the alert. */
.toast-warning:not(:has(> .toast-icon))::before,
.toast-danger:not(:has(> .toast-icon))::before,
.toast-error:not(:has(> .toast-icon))::before {
    border-radius: var(--fx-radius-xs, 4px);
}

/* Bootstrap's own container: stacking behaviour only, so a page that positions
 * it with utilities keeps control of placement. */
.toast-container {
    --bs-toast-zindex: var(--fx-z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--fx-space-2);
    max-inline-size: 100%;
    pointer-events: none;
}

/* Our own stack, and the common Bootstrap `position-fixed` pairing. Anchored at
 * the block end so the newest toast appears nearest the thumb, offset above the
 * FAB, and inset from any device safe area. The safe-area insets are load
 * bearing on a notched phone and must not be dropped. */
.fx-toast-stack,
.toast-container.position-fixed {
    position: fixed;
    z-index: var(--fx-z-toast);
    inset-block-start: auto;
    inset-block-end: calc(
        var(--fx-space-4)
        + env(safe-area-inset-bottom, 0px)
        + var(--fx-fab-clearance, 4.5rem)
    );
    inset-inline-end: calc(var(--fx-space-4) + env(safe-area-inset-right, 0px));
    inset-inline-start: auto;
    display: flex;
    /* Newest toast, appended last, sits closest to the block end edge. */
    flex-direction: column-reverse;
    gap: var(--fx-space-2);
    inline-size: min(22.75rem, calc(100vw - var(--fx-gutter) * 2));
    max-block-size: calc(100svh - (var(--fx-space-7) * 2));
    overflow: hidden;
    pointer-events: none;
}

/* Auto dismiss progress. The bar is anchored at the inline start edge and
 * shrinks its own inline size, so it drains toward the start in BOTH scripts
 * without any RTL specific rule.
 *
 * NEUTRAL BY DEFAULT. This used to fall back to `fx-color-primary`, which put
 * brand colour on a progress indicator — see the accent rule in 00-tokens 3.9,
 * which sends a progress or loading bar to a text tier. A status variant still
 * tints it, because a status hue carries meaning and is not brand. */
.fx-toast-progress {
    position: absolute;
    inset-block-end: 0;
    inset-inline-start: 0;
    block-size: var(--fx-border-width-strong);
    inline-size: 100%;
    background-color: var(--fx-toast-accent, var(--fx-color-text-subtle));
    /* THE DRAIN IS A TRANSFORM NOW, NOT AN INLINE SIZE. Changed 2026-08-03.
     *
     * It used to animate `inline-size` from 100% to 0. That is a LAYOUT
     * property: for five continuous seconds, on every animation frame, the
     * browser re-laid out the bar, its containing toast and everything the toast
     * contains — and it did so on the main thread, during the exact window when
     * the page is usually still settling from whatever action produced the
     * toast. A scaleX runs on the compositor and touches neither layout nor
     * paint. Same pixels, same five seconds, none of the work.
     *
     * THE ORIGIN IS WHAT KEEPS IT DIRECTION CORRECT. The old version was RTL
     * safe for free, because shrinking an inline size drains toward the inline
     * START in both scripts with no rule written. `transform-origin` takes
     * physical keywords only, so that one property is mirrored below and it is
     * the single physical declaration this change introduces. Draining toward
     * the start is deliberate in both scripts: the bar empties in the direction
     * the eye has already finished reading.
     *
     * `will-change` IS EARNED HERE AND IS NOT SPECULATIVE. A transform genuinely
     * runs on this element, uninterrupted, for the entire time it is mounted —
     * which is the one condition under which promoting a layer pays for itself.
     * The element is a two pixel sliver that unmounts with its toast, so the
     * layer is small and short lived. Nothing else in this file gets one. */
    transform-origin: left center;
    will-change: transform;
    /* The ONE duration in this file that is not an `fx-dur-*` token, and the
     * only one that must not be: this is how long the message is READABLE, not
     * how long an animation takes. It is therefore left alone by the reduced
     * motion block in section 11 — a user who asked for less motion still needs
     * the same seconds to read the sentence. `forwards` is REQUIRED on this one
     * and is the exception the fill mode rule names: the to-frame is an EMPTY
     * bar, which is not the element's resting style, and it has to hold until
     * the toast unmounts or the bar would refill at the moment it expired. */
    animation: fx-toast-drain var(--fx-toast-duration, 5s) linear forwards;
}
/* The one mirrored declaration the transform costs — see the note above. */
[dir="rtl"] .fx-toast-progress {
    transform-origin: right center;
}
/* Pause while the user is engaged with the message. The JS only has to keep the
 * element mounted; the pause is a pure styling hook here.
 *
 * THE TWO HALVES ARE GATED DIFFERENTLY ON PURPOSE. Keyboard engagement is real
 * on every device, so `:focus-within` is ungated. The POINTER half is not: on a
 * touch screen a tap leaves a latched hover state the user cannot leave, which
 * would freeze the drain bar and keep an expired toast on screen indefinitely.
 * So it sits behind `any-hover`, like every other hover rule in this file. */
.toast:focus-within .fx-toast-progress {
    animation-play-state: paused;
}
@media (any-hover: hover) {
    .toast:hover .fx-toast-progress,
    .fx-toast-stack:hover .fx-toast-progress {
        animation-play-state: paused;
    }
}

@keyframes fx-toast-drain {
    /* scaleX rather than inline-size — see the note on `.fx-toast-progress`.
     * The X axis is physical; the direction the bar drains toward is decided by
     * `transform-origin`, which the RTL rule beside that one mirrors. */
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}
/* The entrance keyframe that used to live here was replaced by the shared
 * `fx-slide-in-inline`. Only the exit is local, because nothing shared
 * expresses "leave the way you came" without a second custom property. */
@keyframes fx-toast-out {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: scale(0.98); }
}

/* Below the small breakpoint a floating card covers most of the screen, so the
 * stack becomes a full width strip pinned to the block end. */
@media (max-width: 575.98px) {
    .fx-toast-stack,
    .toast-container.position-fixed {
        inset-inline: var(--fx-space-2);
        inline-size: auto;
        inset-block-end: calc(
            var(--fx-space-2)
            + env(safe-area-inset-bottom, 0px)
            + var(--fx-fab-clearance, 4.5rem)
        );
    }
    .toast {
        max-inline-size: 100%;
    }
}

/* toastr, the notification library ABP's `abp.notify` uses under LeptonX. Its
 * markup is `#toast-container > div.toast-*`, unrelated to the Bootstrap toast
 * above, so it is retokenised here to keep the two families identical.
 *
 * ONE DIFFERENCE, AND IT IS THE LIBRARY'S FAULT: toastr paints its own status
 * icon as a base64 PNG through a rule flagged `!important`, which this file's
 * contract forbids answering in kind. So the status here is carried by the
 * TITLE'S COLOUR rather than by a glyph we control — still colour plus text,
 * still not a rail and not a fill, and the inline start padding is kept so the
 * library's own icon has its gutter. */
#toast-container > div {
    inline-size: min(22.75rem, calc(100vw - var(--fx-gutter) * 2));
    max-inline-size: 100%;
    padding-block: var(--fx-space-4);
    padding-inline-end: var(--fx-space-5);
    background-color: var(--fx-color-level-3, var(--fx-color-surface-raised));
    /* A translucent hairline, and NO status rail. */
    border: var(--fx-border-width) solid var(--fx-color-border);
    border-radius: var(--fx-radius-sm);
    box-shadow: var(--fx-shadow-float-md, var(--fx-shadow-lg));
    color: var(--fx-color-text-muted);
    font-family: var(--fx-font-body);
    font-size: var(--fx-text-sm);
    letter-spacing: var(--fx-tracking-ui);
    opacity: 1;
}
/* THE ARRIVAL AND THE DISMISSAL OF THE TOAST THE APPLICATION ACTUALLY SHOWS.
 * Added 2026-08-03 after MEASURING the live notification rather than reasoning
 * about it, and what the measurement found is written out below because it
 * changes what this block is for.
 *
 * WHAT ACTUALLY RENDERS. `abp.notify.success` is called from the Courts module
 * on every save and every delete. It does NOT produce the Bootstrap `.toast`
 * the section above styles, and it does not produce a bare toastr card either.
 * It produces `div#toast-container > div.abp-toast.abp-toast-success`, and the
 * card is painted and ANIMATED by `abp-toast.css`, a stylesheet ABP loads long
 * before this one. So the most frequently seen feedback surface in the product
 * was the one surface running entirely on vendor motion.
 *
 * WHAT THE VENDOR MOTION IS, AND WHY ALL THREE FAULTS MATTER.
 *   · `@keyframes toastIn` travels `translateX(100%)` — the card's OWN FULL
 *     WIDTH, measured at 364px on the running app. The sanctioned toast slide
 *     on this platform is `fx-space-4`, 16px. 00-tokens section 7 states the
 *     principle directly: the residual translate exists to give the motion a
 *     DIRECTION, not a JOURNEY. 364px is a journey.
 *   · Both `toastIn` and the `.toast-removing` exit run 0.3s, which is inside
 *     the 200 to 400ms band 00-tokens bans outright, and they run the SAME 0.3s
 *     in both directions, so the dismissal takes exactly as long as the arrival.
 *   · The durations are HARDCODED, and that is the fault that turns a style
 *     disagreement into an accessibility defect. `.abp-toast` is (0,1,0). The
 *     universal reduced motion damper in 01-base section 15 is (0,0,0) and
 *     therefore LOSES to it — which is the precise limitation that section now
 *     documents. MEASURED ON THIS MACHINE, which reports
 *     `prefers-reduced-motion: reduce`: the toast still flew 364px across the
 *     viewport in 300ms. A user who asked the operating system for stillness was
 *     getting the largest single movement in the application.
 *
 * WHAT THIS BLOCK DOES ABOUT IT. `#toast-container > div` is (1,0,1) and beats
 * `.abp-toast` at (0,1,0) comfortably, and `.toast-removing` is answered one
 * step longer for the same reason — no importance flag is needed anywhere, which
 * is what keeps this inside the file's contract.
 *   · MOTION ALLOWED: the same shared `fx-slide-in-inline` the Bootstrap toast
 *     uses, the same `fx-dur-base`, the same decelerating curve, the same 16px
 *     distance read from `fx-slide-from` and mirrored for RTL — and an exit on
 *     the `fx-dur-exit` pair, so leaving is finally faster than arriving. The
 *     two toast families now agree on motion as well as on surface, hairline,
 *     corner, shadow and type. Two notification systems arriving at different
 *     speeds on one screen is worse than either speed.
 *   · MOTION REDUCED: both vendor keyframes are cancelled outright in section 11
 *     rather than damped, because a 364px translate at 0.01ms is a teleport and
 *     the from-frame would still paint for a tick.
 *
 * `backwards` IS DOING REAL WORK HERE AND IS NOT A CONSISTENCY CHOICE. The
 * library drives its own dismissal by adding `.toast-removing` and then removing
 * the node on a timer, and it writes opacity through the style attribute while
 * it does so. A CSS animation outranks the style attribute, so a forward filled
 * ENTRANCE would pin the card at opacity 1 and swallow the removal whole. Under
 * `backwards` our entrance owns its 160ms and then hands the element straight
 * back, which is the division of labour this file wants: we own the arrival, and
 * the exit below is a deliberate, separately declared replacement rather than an
 * accident of fill mode. */
#toast-container > div {
    --fx-slide-from: var(--fx-space-4);
}
[dir="rtl"] #toast-container > div {
    --fx-slide-from: calc(var(--fx-space-4) * -1);
}
@media (prefers-reduced-motion: no-preference) {
    #toast-container > div {
        animation: fx-slide-in-inline var(--fx-dur-base) var(--fx-ease-decel) backwards;
    }
    /* THE EXIT, REPLACING THE VENDOR'S 300ms MIRROR OF ITS OWN ENTRANCE.
     * (1,1,1), one step past `.toast-removing` at (0,1,0). `fx-toast-out` is the
     * same keyframe the Bootstrap toast leaves on — opacity to 0 with a 2 percent
     * shrink, no travel — so a dismissed notification and a dismissed toast are
     * one object. `both` is REQUIRED and is the fill mode rule's stated
     * exception: the to-frame is opacity 0, which is not a resting style, and it
     * has to hold for the frames between the animation ending and ABP removing
     * the node, or the card would flash back to full opacity as it died. */
    #toast-container > div.toast-removing {
        animation: fx-toast-out var(--fx-dur-exit) var(--fx-ease-in-quad) both;
    }
}

#toast-container > .toast-success { --fx-toast-accent: var(--fx-color-success-fg); }
#toast-container > .toast-warning { --fx-toast-accent: var(--fx-color-warning-fg); }
#toast-container > .toast-error   { --fx-toast-accent: var(--fx-color-danger-fg); }
#toast-container > .toast-info    { --fx-toast-accent: var(--fx-color-info-fg); }
#toast-container .toast-title {
    font-weight: var(--fx-weight-semibold, 600);
    color: var(--fx-toast-accent, var(--fx-color-text));
}
#toast-container .toast-message {
    color: var(--fx-color-text-muted);
    overflow-wrap: anywhere;
}
#toast-container .toast-close-button {
    min-inline-size: var(--fx-control-h-sm);
    min-block-size: var(--fx-control-h-sm);
    color: var(--fx-color-text-subtle);
    text-shadow: none;
    opacity: 1;
}
@media (max-width: 575.98px) {
    #toast-container > div {
        inline-size: auto;
    }
}


/* ── 2.1) ABP's OWN TOAST — `.abp-toast`, the card the product actually shows
 * ---------------------------------------------------------------------------
 * Added 2026-08-03. The block above already owned this element's SURFACE — the
 * level 3 fill, the translucent hairline, the 6px corner, the float shadow, the
 * type and, since the motion pass, the arrival and the dismissal. What it did
 * NOT own was the card's GEOMETRY, and geometry is most of what a reader sees.
 * `abp-toast.css` (served from
 * `libs/abp/aspnetcore-mvc-ui-theme-shared/toast/abp-toast.css`) writes
 *
 *     .abp-toast { display: grid; grid-template-columns: 35px 1fr; gap: 5px;
 *                  margin: 5px 0; padding: 10px; width: 350px;
 *                  color: #fff; border-radius: 8px; font-size: 16px; }
 *     .abp-toast-icon .icon    { font-size: 30px; }
 *     .abp-toast-title         { font-size: 1rem; }
 *     .abp-toast-message       { max-width: 240px; }
 *     .abp-toast-close-button  { top: 0; right: 0; width: 25px; height: 100%; }
 *     .abp-toast-close-button:focus { outline: none; }
 *
 * and everything in that list is a raw literal belonging to no design system.
 * `#toast-container > div` is (1,0,1) and had already taken the width, the
 * colour, the corner, the shadow and the size; what remained visible on the
 * running app was measured before this change and was: a 10px inline START
 * padding where the system asks for 16, a 5px block MARGIN the stack should be
 * spacing instead, a 35px icon COLUMN holding a 30px glyph where the system's
 * icon is 16, a 5px column GAP where the system's is 12, a 240px cap on the
 * message inside a 364px card, a dismiss control pinned to the PHYSICAL right,
 * and a suppressed focus ring on it.
 *
 * THE TARGET IS `.toast` IN SECTION 2 AND NOTHING ELSE. Same 364px, same
 * 16 / 24 / 16 / 16 asymmetric padding with the extra room at the inline end for
 * the dismiss, same `auto minmax(0, 1fr)` two column grid, same 12px column gap,
 * same 16px icon — and the SAME RULE ABOUT WHAT CARRIES STATUS: the icon's
 * colour, alone. No tinted background, no coloured rail. The vendor's
 * `.abp-toast-success` and friends paint a SOLID status fill with a 2px status
 * border; both were already cancelled by the surface rule above, and this block
 * finishes the job by routing the status to the one mark that is allowed to
 * carry it.
 *
 * WHY THE SELECTORS ARE `#toast-container …` AND NOT BARE `.abp-toast …`.
 * Everything here is scoped through the container id so it clears the vendor
 * sheet on specificity alone, exactly as the surface rules above do, and so the
 * toastr rules that share this container are left strictly alone. That second
 * half matters: the note above records that toastr paints its status icon as a
 * base64 PNG behind an importance flag and therefore has to carry its status in
 * the TITLE'S colour and keep a wide inline start gutter for the image. Neither
 * compromise applies here — ABP's icon is a font glyph in an element we can
 * address — so `.abp-toast` gets the honest treatment and its title stays
 * NEUTRAL, matching `.toast`. Two different answers because the two libraries
 * hand us two different amounts of control, not because the design differs.
 * ------------------------------------------------------------------------- */

/* The stack spaces its own children, as the Bootstrap `.toast-container` does,
 * so the card no longer carries a block margin of its own. ABP's container is
 * already `display: flex; flex-direction: column`, so this is the same
 * mechanism, not a new one. */
#toast-container {
    gap: var(--fx-space-2);
}

#toast-container > .abp-toast {
    /* The neutral default; the four variants below re-point it. Declared here
     * rather than inherited so a notification raised with no severity class
     * still resolves to a real colour instead of the initial value. */
    --fx-toast-accent: var(--fx-color-text-subtle);

    /* GEOMETRY. Each of these cancels one literal from the list above. */
    margin-block: 0;
    /* THE ASYMMETRY, completed. The block padding and the inline END were
     * already set by the surface rule; this is the inline START, which was the
     * one edge still reading the vendor's 10px and the only reason the measured
     * padding did not match section 2's stated anatomy. */
    padding-inline-start: var(--fx-space-4);
    /* `auto` rather than a fixed column: the icon sizes itself from the 16px
     * glyph below, so the column can never be wider than the mark it holds. */
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--fx-space-3);
    row-gap: 0;
    align-items: start;

    /* ── THE ONE `!important` OUTSIDE SECTION 8. ────────────────────────────
     * IT CANCELS EXACTLY ONE VENDOR FLAG AND IS NAMED HERE AS THE FILE'S
     * CONTRACT REQUIRES. LeptonXLite's `js-bundle.rtl.css` ships
     *
     *     #toast-container > div { opacity: 0.95 !important; }
     *
     * with no counterpart in either theme. That is (1,0,1) AND flagged, so the
     * plain `opacity: 1` in the surface rule above — same specificity, no flag —
     * lost to it, and every notification this application has ever shown has
     * been five percent transparent. MEASURED on the running app before this
     * change: computed opacity 0.95.
     *
     * WHY IT IS NOT A STYLE PREFERENCE. A toast is the one surface that is
     * GUARANTEED to land on content nobody chose — it appears wherever the user
     * happened to be, over a grid, a form or a dialog. Letting five percent of
     * an arbitrary page through the card puts an unpredictable pattern behind
     * text the user is being asked to read, and it does it to the acknowledgment
     * of every save and every delete in the product. Contrast that cannot be
     * predicted cannot be verified, which takes the surface outside SC 1.4.3
     * rather than merely making it look soft.
     *
     * AN IMPORTANT AUTHOR DECLARATION CAN ONLY BE ANSWERED BY ANOTHER ONE.
     * Specificity is no help — the vendor rule is already as specific as the
     * rule it beat. This is the same mechanism, and the same justification, as
     * the four flags in section 8, and it reads the identity value 1 rather than
     * any colour.
     *
     * WHAT IT COSTS, STATED PLAINLY SO NOBODY REDISCOVERS IT AS A BUG. In the
     * cascade an important author declaration outranks ANIMATION declarations,
     * so `opacity` on this element can no longer be animated — the fade channel
     * of `fx-slide-in-inline` and of `fx-toast-out` does not apply here. THIS
     * TAKES NOTHING AWAY: the vendor's own flag already outranked both of those
     * animations in exactly the same way, so the fade was never reaching this
     * element and the entrance and exit were already running as translate and
     * scale only. The change is legibility, and legibility alone. If the fade is
     * ever wanted back on this one element it has to be expressed as a
     * TRANSITION, which is the only tier that outranks an important
     * declaration — not by weakening this rule. */
    opacity: 1 !important;
}

/* Status, and this is the ONLY thing the four variants do. The vendor's solid
 * fills and 2px status borders are cancelled by the surface rule above; here
 * the severity is handed to the icon, which is where section 2's anatomy says
 * it lives. Same specificity as the rule above, so source order decides — these
 * must stay below it. */
#toast-container > .abp-toast-success { --fx-toast-accent: var(--fx-color-success-fg); }
#toast-container > .abp-toast-warning { --fx-toast-accent: var(--fx-color-warning-fg); }
#toast-container > .abp-toast-error   { --fx-toast-accent: var(--fx-color-danger-fg); }
#toast-container > .abp-toast-info    { --fx-toast-accent: var(--fx-color-info-fg); }

/* THE MARK THAT CARRIES THE STATUS. 16px, the default icon size everywhere in
 * the system, down from the vendor's 30px — which was nearly twice the height of
 * the message beside it and read as an illustration rather than as a status
 * mark. The 1px optical nudge and the `start` alignment are copied from
 * `.toast > .toast-icon` so the two families align their glyph identically. */
#toast-container .abp-toast-icon {
    inline-size: var(--fx-icon-md);
    block-size: var(--fx-icon-md);
    margin-block-start: 1px;
    color: var(--fx-toast-accent);
}
#toast-container .abp-toast-icon .icon {
    font-size: var(--fx-icon-md);
    line-height: 1;
}

/* NEUTRAL, unlike the toastr title above it — see the header note on why the two
 * differ. The title is the message's heading, not its severity. */
#toast-container .abp-toast-title {
    color: var(--fx-color-text);
    font-size: var(--fx-text-xs);
    font-weight: var(--fx-weight-semibold, 600);
    line-height: var(--fx-leading-normal, 1.7);
}

/* THE 240px CAP GOES. It is a physical `max-width` typed against the vendor's
 * own 350px card; inside a 364px one it stopped the sentence roughly 50px short
 * of the space available and wrapped Arabic that had no reason to wrap. The
 * grid's `minmax(0, 1fr)` column is what bounds the text now, which is the
 * measurement that is actually true. */
#toast-container .abp-toast-message {
    max-inline-size: none;
    color: var(--fx-color-text-muted);
    font-size: var(--fx-text-sm);
    overflow-wrap: anywhere;
}

/* ROOM FOR THE DISMISS, RESERVED WHERE THE DISMISS ACTUALLY IS. The control is
 * absolutely positioned inside this element rather than laid out beside the text
 * the way `.toast-header .btn-close` is, so the card's own inline end padding
 * cannot clear it and the text would run underneath. The reserve is the
 * control's own width, read from the same token the control reads, so the two
 * can never drift apart — including under `pointer: coarse`, where section 10
 * grows both together. */
/* THE STACK ANCHORS ON THE INLINE END, LIKE EVERY OTHER STACK IN THE PRODUCT.
 *
 * ABP ships this container pinned PHYSICALLY — `right: 30px; bottom: 30px` — and
 * nothing here overrode it, while `.fx-toast-stack` and Bootstrap's
 * `.toast-container.position-fixed` both anchor at `inset-inline-end`. In an
 * English document the three agree by coincidence. In Arabic they do not: the
 * platform's own toasts appear in one bottom corner and ABP's — the ones raised
 * on every save and delete — appear in the OTHER. Two notification systems on
 * opposite sides of the same screen is not a preference, it is a defect the
 * physical property was always going to produce.
 *
 * The physical pair is zeroed rather than left to fight the logical one: with a
 * non-auto `right`, a non-auto `left` and a non-auto width all in play the box
 * is over-constrained, and under `direction: rtl` CSS resolves that by ignoring
 * `left` — so the vendor would have won silently. The same reasoning is written
 * out over the dismiss control below, which had the identical bug.
 *
 * The offset reads the page gutter rather than the vendor's 30px, so the stack
 * sits on the same margin as everything else and follows the gutter's own
 * responsive tiers. Safe-area insets are already handled for the other stacks
 * further up; this container inherits nothing from them, hence the restatement. */
/* ORDER IS LOAD BEARING HERE, AND THE FIRST ATTEMPT AT THIS RULE CANCELLED
 * ITSELF. It listed the logical properties first and then reset the physical
 * pair — but in an RTL document `inset-inline-end` RESOLVES TO `left`, so the
 * later `left: auto` overwrote the value the rule had just set. Both inline
 * edges ended up `auto`, the fixed container fell back to its static position,
 * and it landed physically right: measured `left 1560 / right 1910` in a 1920px
 * window, i.e. the opposite corner from `.fx-toast-stack`, which is precisely
 * the defect the rule exists to remove. It was broken symmetrically in LTR too.
 * The physical resets therefore come FIRST and the logical values last.
 *
 * THE FLAGS CANCEL AN INLINE STYLE, WHICH IS THE DOCUMENTED EXCEPTION. ABP's
 * toast script writes `style="inset: auto 30px 30px auto"` onto this container
 * at runtime, and an inline declaration outranks every author rule that is not
 * flagged. There is no selector that beats it and no hook to stop it being
 * written, so the flag is the only thing between the platform's two toast
 * stacks and opposite corners of an Arabic screen. */
#toast-container {
    right: auto;
    left: auto;
    top: auto;
    bottom: auto;
    inset-inline-start: auto !important;
    inset-inline-end: max(var(--fx-gutter), env(safe-area-inset-right, 0px)) !important;
    inset-block-start: auto !important;
    inset-block-end: max(var(--fx-gutter), env(safe-area-inset-bottom, 0px)) !important;
}

#toast-container .abp-toast-content {
    padding-inline-end: var(--fx-control-h-sm);
}

/* THE DISMISS. Pinned to the INLINE end, not the physical right.
 *
 * `inset-inline-start: auto` is load bearing and is not tidiness. The vendor
 * writes `right: 0` physically; in an RTL document `inset-inline-end` resolves
 * to `left`, so without the `auto` the element would have a non-auto `left`, a
 * non-auto `right` and a non-auto width at once. That is over-constrained, and
 * CSS resolves it by IGNORING `left` under `direction: rtl` — the vendor's
 * `right: 0` would have won and the control would have stayed on the wrong side
 * of an Arabic card, which is the side the message text starts on. */
#toast-container .abp-toast-close-button {
    inset-block-start: 0;
    inset-inline-end: 0;
    inset-inline-start: auto;
    inline-size: var(--fx-control-h-sm);
    block-size: var(--fx-control-h-sm);
    padding: 0;
    /* A row control, not a circle — the same corner every other small square
     * control in the system takes. */
    border-radius: var(--fx-radius-item);
    color: var(--fx-color-text-subtle);
}
#toast-container .abp-toast-close-button .icon,
#toast-container .abp-toast-close-button > i {
    font-size: var(--fx-icon-sm);
    line-height: 1;
}
/* THE FOCUS RING, HANDED BACK. `abp-toast.css` writes
 * `.abp-toast-close-button:focus { outline: none }` at (0,2,0) and puts nothing
 * in its place, so the only keyboard operable control on the notification was
 * invisible to a keyboard user — SC 2.4.7 outright. (1,1,1) clears it without an
 * importance flag. `:focus-visible` rather than `:focus`, so a pointer user
 * never sees it, and the standard positive offset because the card's 24px inline
 * end padding leaves the ring somewhere to be drawn. */
#toast-container .abp-toast-close-button:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: var(--fx-focus-offset);
}
@media (any-hover: hover) {
    #toast-container .abp-toast-close-button:hover {
        color: var(--fx-color-text);
    }
}


/* ── 3) Badges, chips and status tags ──────────────────────────────────────
 * THE FOUR VARIANTS, AND THE ONE THAT IS FILLED.
 *
 *   `.badge`              FILLED — solid `fx-color-primary`. This is SITE 7
 *                         of the ten sanctioned brand sites (renumbered when
 *                         3.9 was rewritten; it was site 2 under the superseded
 *                         six site contract) and it is the only brand fill in
 *                         this file. It is the EXCEPTION.
 *   `.fx-badge-outline`   outline, transparent background, emphasis hairline
 *   `.fx-badge-quiet`     outline, transparent background, subtle hairline
 *   `.fx-badge-tone`      outline, transparent background, STATUS hairline and
 *                         status text — paired with `.fx-badge-success` and
 *                         friends, or used directly by `.fx-chip` / `.fx-status`
 *
 * THREE OF THE FOUR ARE OUTLINE ONLY. That ratio is the point: a screen full of
 * filled badges is a screen full of brand, which is what the accent rule
 * exists to prevent.
 *
 * THE SIZE LADDER IS 20 / 24 / 28px, with radius 4 / 7 / 6 and inline padding
 * 4 / 6 / 8. The radius is not monotonic and that is not a typo — it is the
 * measured reference set, where the middle size is the roundest.
 *
 * THE TRANSPARENT 1.5px BORDER ON THE ROOT IS STRUCTURAL, NOT DECORATION.
 * Every badge carries it whether or not it is an outline variant, so switching
 * a badge from filled to bordered at runtime — a status changing, a filter
 * being selected — changes a colour and NEVER reflows the row it sits in.
 * Deleting it from the root would make every such swap jump by 3px.
 *
 * `font-variant-numeric: tabular-nums` is on the root because a badge is very
 * often a COUNT, and a count that changes width as it ticks is the classic
 * reason a toolbar jitters.
 *
 * `.fx-chip` is the neutral tag and `.fx-status` adds the leading marker that
 * makes state readable WITHOUT colour. The marker shapes are unchanged — they
 * are the non colour signal and are now carrying more of the load than before,
 * because the tinted chip fills are gone too.
 * ------------------------------------------------------------------------- */
.badge,
.fx-chip,
.fx-status {
    /* The size locals. Overridden as a group by the two size modifiers. */
    --fx-badge-h: var(--fx-control-h-xs);        /* 24px */
    --fx-badge-radius: var(--fx-radius-badge);   /* 7px — see the note below */
    --fx-badge-pad-x: var(--fx-space-1-5);       /* 6px — see the note below */
    --fx-badge-font: max(0.75rem, var(--fx-text-xs));

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--fx-space-1);
    max-inline-size: 100%;
    block-size: var(--fx-badge-h);
    min-block-size: var(--fx-badge-h);
    padding-block: 0;
    padding-inline: var(--fx-badge-pad-x);
    border-radius: var(--fx-badge-radius);
    /* THE NO-REFLOW BORDER, now `fx-border-width-mid`. 1.5px sits between
     * `fx-border-width` (1px) and `fx-border-width-strong` (2px); it was a
     * literal here and reported upward, and the token exists for exactly this
     * reservation. NOTE WHAT THE TOKEN DELIBERATELY DOES NOT DO: unlike the
     * hairline it is NOT halved on a 2dppx display, because its whole purpose
     * is that the box never changes size — halving it at one device pixel ratio
     * and not another would reintroduce the reflow it exists to prevent. */
    border: var(--fx-border-width-mid) solid transparent;
    font-size: var(--fx-badge-font);
    /* 600 — badges and titles are the only things in the system at semibold. */
    font-weight: var(--fx-weight-semibold, 600);
    line-height: 1;
    letter-spacing: var(--fx-tracking-ui);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: middle;
}
/* Neither the 6px inline padding nor the 7px radius sits on the 4px space
 * ladder or on the radius ladder (4 / 6 / 8 / 10 / 12 / 14). Both are measured
 * values from the reference, both used to be literals reported upward, and both
 * now have purpose named tokens: `fx-space-1-5` is THE one half step, for
 * small object interiors only, and `fx-radius-badge` sits beside
 * `fx-radius-control` and `fx-radius-item` as a shape named for what it is
 * rather than for how big it is. The 20px and 28px tiers stay on the ladder
 * proper and need no token of their own.
 *
 * THE 20px TIER IS FOR NON INTERACTIVE PILLS ONLY, AND THAT IS AN
 * ACCESSIBILITY BOUNDARY, NOT A NAMING ONE. `fx-badge-h-sm` is deliberately
 * NOT a rung on the `fx-control-h-*` ladder: `fx-control-h-xs` (24px) sits
 * exactly on the WCAG 2.2 SC 2.5.8 floor and is the absolute minimum for
 * anything a user can operate, so a 20px CONTROL fails outright. This size is
 * for a badge, chip or status pill that is REPORTING state and cannot be
 * clicked. The instant the thing wearing it gains a click handler, a dismiss
 * cross or a keyboard focus it stops being eligible, and the guard immediately
 * below enforces that in CSS rather than leaving it to a reviewer. */
.badge.fx-badge-sm,
.fx-chip.fx-badge-sm,
.fx-status.fx-badge-sm {
    --fx-badge-h: var(--fx-badge-h-sm);          /* 20px */
    --fx-badge-radius: var(--fx-radius-xs);      /* 4px */
    --fx-badge-pad-x: var(--fx-space-1);         /* 4px */
}
/* THE SC 2.5.8 GUARD. A small pill that turns out to be operable reverts to the
 * default 24px tier and takes the default radius and padding with it, so it
 * stays on the badge shape ladder rather than becoming a 20px box with a 24px
 * control sticking out of its clipped overflow. This is a floor, not a style:
 * never route around it by re-declaring `fx-badge-h` at a call site.
 *
 * WIDENED 2026-08-02, BECAUSE THE ORIGINAL LIST WAS A SAMPLE RATHER THAN A TEST.
 * It caught `a`, `button`, `[role="button"]`, `[role="link"]`, `[tabindex]` and a
 * nested `.fx-chip-remove`, and every one of those is genuinely operable — but so
 * are a dozen things it did not name, and each of them left a real 20px target
 * shipping. In particular:
 *
 *   · THE OTHER WIDGET ROLES. A pill written as `[role="menuitem"]`, `"tab"`,
 *     `"option"`, `"switch"`, `"checkbox"` or `"radio"` is operated exactly as a
 *     button is; the ARIA name it happens to carry has no bearing on how big a
 *     finger is. `[role="tab"]` is the likeliest of these to appear here, since a
 *     count pill inside a tab strip is the pattern this file's `.fx-nav-count`
 *     sibling in 06 already serves.
 *   · `[onclick]`. A handler attached in markup is the oldest way to make
 *     something clickable and leaves no role and no tabindex behind. It is worth
 *     saying that it is caught here as a SAFETY NET and not as an endorsement:
 *     an `onclick` on a non-focusable element is unreachable by keyboard, is a
 *     `script-src-attr` exposure under a strict CSP, and should be rewritten as a
 *     real control rather than merely resized.
 *   · A NESTED CONTROL THAT IS NOT `.fx-chip-remove`. The `:has()` clause tested
 *     for one specific class, so a chip containing any other `<a>` or `<button>`
 *     kept the 20px height and clipped it. The clause now tests for the SHAPE of
 *     a control instead of for one class name, which is what it should always
 *     have done; `.fx-chip-remove` stays listed explicitly so the rule keeps
 *     working if that control is ever authored on a bare `<span>`.
 *
 * The host list collapses to one `:is()` for readability. Specificity is
 * unchanged at (0,3,0) — `:is()` takes the weight of its heaviest argument, and
 * `.badge` and `[role="button"]` each weigh one class — so this still clears the
 * (0,2,0) tier rule above it by exactly the margin it did before. */
:is(.badge, .fx-chip, .fx-status).fx-badge-sm:is(
    a, button, [onclick], [tabindex],
    [role="button"], [role="link"], [role="menuitem"], [role="menuitemcheckbox"],
    [role="menuitemradio"], [role="tab"], [role="option"], [role="switch"],
    [role="checkbox"], [role="radio"], [role="treeitem"]
),
:is(.badge, .fx-chip, .fx-status).fx-badge-sm:has(:is(
    a, button, [onclick], [tabindex], .fx-chip-remove,
    [role="button"], [role="link"], [role="menuitem"], [role="menuitemcheckbox"],
    [role="menuitemradio"], [role="tab"], [role="option"], [role="switch"],
    [role="checkbox"], [role="radio"], [role="treeitem"]
)) {
    --fx-badge-h: var(--fx-control-h-xs);        /* 24px, the SC 2.5.8 floor */
    --fx-badge-radius: var(--fx-radius-badge);   /* 7px */
    --fx-badge-pad-x: var(--fx-space-1-5);       /* 6px */
}
.badge.fx-badge-lg,
.fx-chip.fx-badge-lg,
.fx-status.fx-badge-lg {
    --fx-badge-h: var(--fx-control-h-sm);        /* 28px */
    --fx-badge-radius: var(--fx-radius-sm);      /* 6px */
    --fx-badge-pad-x: var(--fx-space-2);         /* 8px */
    --fx-badge-font: max(0.75rem, var(--fx-text-base));
}

/* VARIANT 1 of 4 — FILLED, and SITE 7. The one brand FILL in this file, and it
 * survives the rewritten accent contract untouched: a filled badge REPORTS A
 * STATE, which is identity rather than interaction feedback. Bootstrap's own
 * `bs-badge-*` locals are mapped so a `.badge` written anywhere in the
 * platform inherits this without knowing the class names above exist. */
.badge {
    --bs-badge-color: var(--fx-color-text-on-primary);
    --bs-badge-padding-x: var(--fx-badge-pad-x);
    --bs-badge-padding-y: 0;
    --bs-badge-border-radius: var(--fx-badge-radius);
    --bs-badge-font-size: var(--fx-badge-font);
    --bs-badge-font-weight: var(--fx-weight-semibold, 600);
    background-color: var(--fx-color-primary);
    color: var(--fx-color-text-on-primary);
}

/* VARIANTS 2 and 3 of 4 — OUTLINE, transparent background. */
.badge.fx-badge-outline {
    background-color: transparent;
    border-color: var(--fx-color-border-emphasis);
    color: var(--fx-color-text-muted);
}
.badge.fx-badge-quiet {
    background-color: transparent;
    border-color: var(--fx-color-border-subtle);
    color: var(--fx-color-text-subtle);
}

/* VARIANT 4 of 4 — TONAL OUTLINE. The status family, and the shape every chip
 * and status tag below is built from. Transparent fill, a status coloured
 * hairline and status coloured text: severity without a wash. */
.badge.fx-badge-tone,
.fx-chip,
.fx-status {
    --fx-chip-fg: var(--fx-color-text-muted);
    --fx-chip-border: var(--fx-color-border-emphasis);
    --fx-chip-marker: var(--fx-color-text-subtle);
    /* Marker shape defaults to a filled circle; status modifiers vary it so the
     * tags differ in FORM as well as hue. */
    --fx-chip-marker-radius: var(--fx-radius-circle);
    --fx-chip-marker-rotate: 0deg;

    background-color: transparent;
    border-color: var(--fx-chip-border);
    color: var(--fx-chip-fg);
    /* A chip is a label rather than a count, so it may be a weight lighter than
     * a badge without losing the silhouette. */
    font-weight: var(--fx-weight-medium, 500);
}

/* Leading marker. Present on `.fx-status` always; opt in on a plain chip with
 * `.fx-chip-dot`. */
.fx-status::before,
.fx-chip.fx-chip-dot::before {
    content: "";
    flex: 0 0 auto;
    inline-size: 0.5em;
    block-size: 0.5em;
    background-color: var(--fx-chip-marker);
    border-radius: var(--fx-chip-marker-radius);
    /* Rotation only turns a square into a diamond; symmetric, so RTL neutral. */
    transform: rotate(var(--fx-chip-marker-rotate));
}

/* --- Legal domain statuses ------------------------------------------------
 * ADD A NEW STATUS BY COPYING ONE BLOCK. Every status must set three locals: a
 * foreground that clears 4.5:1 on the surface behind it, a hairline colour, and
 * a marker SHAPE (radius and/or rotation) that no other status already uses, so
 * the set stays distinguishable in greyscale.
 *
 *   active    filled circle    open matter, in progress
 *   pending   hollow circle    awaiting an action or a decision
 *   closed    square           concluded and archived out of the active list
 *   archived  rounded square   retained for the record only
 *   overdue   diamond          past a deadline, needs attention now
 *
 * NOTE THE MISSING FOURTH LOCAL. `fx-chip-bg` is gone from every block below:
 * these are outline tags now and the fill is transparent in all five states.
 * ------------------------------------------------------------------------- */
.fx-status-active {
    --fx-chip-fg: var(--fx-color-success-fg);
    --fx-chip-border: var(--fx-color-success-border);
    --fx-chip-marker: var(--fx-color-success-fg);
    --fx-chip-marker-radius: var(--fx-radius-circle);
}
.fx-status-pending {
    --fx-chip-fg: var(--fx-color-warning-fg);
    --fx-chip-border: var(--fx-color-warning-border);
    --fx-chip-marker: var(--fx-color-warning-fg);
    --fx-chip-marker-radius: var(--fx-radius-circle);
}
/* Hollow ring rather than a filled dot: second shape in the set. The ring is
 * drawn with an inset shadow rather than a border so it does not change the
 * marker's box size and knock the chip's baseline out of alignment. */
.fx-status-pending::before {
    background-color: transparent;
    box-shadow: inset 0 0 0 var(--fx-border-width-strong) var(--fx-chip-marker);
}
.fx-status-closed {
    --fx-chip-fg: var(--fx-color-text-muted);
    --fx-chip-border: var(--fx-color-border-emphasis);
    --fx-chip-marker: var(--fx-color-text-muted);
    --fx-chip-marker-radius: 0;
}
.fx-status-archived {
    --fx-chip-fg: var(--fx-color-text-subtle);
    --fx-chip-border: var(--fx-color-border-subtle);
    --fx-chip-marker: var(--fx-color-text-subtle);
    /* Literal fallback on purpose. This radius is the ONLY thing separating the
     * archived marker (rounded square) from the closed one (hard square) in
     * greyscale — if the token fails to resolve it computes to 0 and two
     * distinct legal statuses become the same shape in the same grey. That is a
     * colour only distinction, which is exactly what this set exists to avoid,
     * so the shape does not get to depend on the token layer parsing. */
    --fx-chip-marker-radius: var(--fx-radius-xs, 4px);
}
.fx-status-overdue {
    --fx-chip-fg: var(--fx-color-danger-fg);
    --fx-chip-border: var(--fx-color-danger-border);
    --fx-chip-marker: var(--fx-color-danger-fg);
    --fx-chip-marker-radius: 0;
    --fx-chip-marker-rotate: 45deg;
}

/* --- The two dormancy states -----------------------------------------------
 * Added 2026-08-29 on the owner's ruling for SCRUM-221. A case that goes six
 * months without a recorded action is متروكة — the COURT's classification, and
 * reviving the file costs a fee. At four months the office raises مهملة against
 * itself as an early warning, while there is still a month or two to act.
 *
 * BOTH TAKE THE DANGER TONE, ON THE OWNER'S INSTRUCTION («علمهم بالاحمر»), and
 * that overturns an earlier reading of the same ruling in which متروكة stayed
 * neutral so the red would belong only to the rows still worth acting on. The
 * office's answer is that a file already lost to a deadline is not a calmer
 * fact than one about to be, and it is their file. Recorded rather than
 * silently reversed, because the earlier reasoning is still sound design and is
 * simply not the call being made here.
 *
 * SO SEVERITY MOVES TO THE MARKER, WHICH IS WHAT THIS SET IS FOR. Both are
 * diamonds, because both say the same kind of thing — a legal clock has run —
 * and the pair reads as one family rather than two unrelated warnings. The fill
 * is the ladder: مهملة is a HOLLOW diamond, still open, still recoverable;
 * متروكة is FILLED, closed, decided. In greyscale, or for a reader who cannot
 * separate the two reds, the fill is the whole distinction and it survives
 * alone — which is precisely the guarantee this section's header asks for.
 *
 * ONE SHAPE COLLISION, DELIBERATE: متروكة renders identically to `overdue`. The
 * rule above wants shapes unique so a set stays legible together, and these two
 * never render together — `overdue` marks a TASK past its deadline, this marks
 * a FILE past the court's. They make the same claim, so making the same picture
 * is honest. The class exists separately so the markup does not have to call a
 * dormant case overdue in order to look right.
 * ------------------------------------------------------------------------- */
.fx-status-neglected {
    --fx-chip-fg: var(--fx-color-danger-fg);
    --fx-chip-border: var(--fx-color-danger-border);
    --fx-chip-marker: var(--fx-color-danger-fg);
    --fx-chip-marker-radius: 0;
    --fx-chip-marker-rotate: 45deg;
}

/* Hollow diamond. Same inset shadow the pending ring uses rather than a border,
 * for the same reason: a border would grow the marker's box and drop the chip's
 * baseline out of line with the متروكة chip beside it in the next row.
 *
 * 1px, NOT THE STRONG GRADE THE PENDING RING TAKES, AND THAT IS A MEASUREMENT
 * RATHER THAN A PREFERENCE. The marker is 0.5em, which is 6px at the 20px
 * chip's 12px type. A 2px ring leaves a 2px hole; rendered beside the filled
 * متروكة diamond at real size the two were near indistinguishable — the
 * rotation puts a corner at each of the hole's four sides and closes what
 * little of it there was. At 1px the hole is 4px and the pair separates.
 *
 * The pending ring keeps 2px because it is a CIRCLE, whose hole loses nothing
 * to corners. Same technique, different geometry, so the same number does not
 * buy the same result.
 *
 * A LITERAL 1px AND NOT `--fx-border-width`, WHICH WOULD HAVE FAILED ONLY ON
 * THE MACHINES THAT MATTER. That token is the HAIRLINE grade and 00-tokens
 * halves it to 0.5px under `min-resolution: 2dppx` — correct for a rule drawn
 * along a card edge, wrong for a ring that has to hold a 4px hole open. On a
 * retina screen the ring would have thinned to the point where the hollow
 * diamond reads as an empty box or as nothing at all, and the office would
 * have seen it while this desktop never did. The archived marker two blocks up
 * refuses the token layer for the same class of reason: when a shape is the
 * only thing separating two legal states, the shape does not get to vary with
 * the display. */
.fx-status-neglected::before {
    background-color: transparent;
    box-shadow: inset 0 0 0 1px var(--fx-chip-marker);
}

.fx-status-dormant {
    --fx-chip-fg: var(--fx-color-danger-fg);
    --fx-chip-border: var(--fx-color-danger-border);
    --fx-chip-marker: var(--fx-color-danger-fg);
    --fx-chip-marker-radius: 0;
    --fx-chip-marker-rotate: 45deg;
}

/* Neutral chip tones for non status labelling. `.fx-chip-primary` is NOT a
 * brand tint: 3.9's own worked example splits the case exactly here — a chip
 * that reports STATE which will be applied is site 7 and takes the filled
 * `.badge`; a chip that merely shows which one the pointer landed on is a grey
 * wash. This class is the second of those, so it is the emphasised neutral
 * outline and stays that way under the rewritten contract. */
.fx-chip-primary {
    --fx-chip-fg: var(--fx-color-text);
    --fx-chip-border: var(--fx-color-border-emphasis);
    --fx-chip-marker: var(--fx-color-text-muted);
}
.fx-chip-info {
    --fx-chip-fg: var(--fx-color-info-fg);
    --fx-chip-border: var(--fx-color-info-border);
    --fx-chip-marker: var(--fx-color-info-fg);
}

/* Status badge shorthands, so a `.badge.fx-badge-tone.fx-badge-success` reads
 * the same as the equivalent chip. */
.fx-badge-success { --fx-chip-fg: var(--fx-color-success-fg); --fx-chip-border: var(--fx-color-success-border); }
.fx-badge-warning { --fx-chip-fg: var(--fx-color-warning-fg); --fx-chip-border: var(--fx-color-warning-border); }
.fx-badge-danger  { --fx-chip-fg: var(--fx-color-danger-fg);  --fx-chip-border: var(--fx-color-danger-border); }
.fx-badge-info    { --fx-chip-fg: var(--fx-color-info-fg);    --fx-chip-border: var(--fx-color-info-border); }


/* ── VARIANT 4, REACHED FROM THE OTHER SIDE: BOOTSTRAP'S CONTEXTUAL UTILITIES
 *    WHERE THEY LAND ON A BADGE ─────────────────────────────────────────────
 *
 * ADDED 2026-08-03, on a live audit finding rather than on a reading. On
 * /Identity/Roles the "public role" flag is written by the Identity module as
 * `.badge.rounded-pill.bg-info`, and it was computing to RAW BOOTSTRAP CYAN.
 * Measured on the running application at 1920px, light theme:
 *
 *     fill #0DCAF0 · label #FFFFFF ...... 1.96:1
 *     the same fill under this file's own badge ink #EEF4E8 ...... 1.75:1
 *
 * SC 1.4.3 wants 4.5:1 for that label. It fails by a factor of two, and it was
 * additionally the ONLY saturated cyan in a green and neutral product. The same
 * renderer emits `.bg-success` for the "default role" flag, so the defect was
 * never one class; it is the whole contextual family arriving unbridged.
 *
 * WHY IT REACHED THE PAGE. Bootstrap writes the utility as
 * `background-color: rgba(var(bs-info-rgb), var(bs-bg-opacity)) !important`,
 * reading the RGB TRIPLET rather than the hex token. 03-leptonx bridges a great
 * deal of Bootstrap but not those triplets, and 10-utilities shadows only the
 * PRIMARY member of the family. Every other contextual utility on the platform
 * is therefore still stock Bootstrap 5.3. THE COMPLETE BRIDGE BELONGS IN
 * 03-leptonx AND IS REPORTED TO THAT FILE — this block fixes the component this
 * file owns, does not depend on that bridge landing, and stays correct if it
 * does, because it overrides the background outright rather than retinting it.
 *
 * ══ WHY IT IS NOT GREEN, WHICH IS THE FIRST THING A READER WILL ASK ════════
 *
 * Section 3.9 site 7 sanctions A FILLED BADGE, and the parenthetical is doing
 * the work: "the status pill that is GENUINELY a status". A ROLE FLAG is not
 * one, and the mechanical test says so three times over:
 *
 *   Q1. Does "this role is public" say WHO THIS PRODUCT IS, or WHERE THE USER
 *       IS? Neither. It is a property of one row of data.
 *   Q2. Does it respond to a pointer or a keyboard? No.
 *   Q3. So it is Q3 — reach for weight, shape, space and a text tier.
 *
 * And the frequency argument that retired the list marker in the same section
 * lands on it independently: every sanctioned brand site is BOUNDED BY THE
 * PAGE, and this one is bounded by the data. Painting the office's brand green
 * on a permission flag would say the product identifies with the flag.
 *
 * ══ AND WHY IT IS NOT A FILL EITHER, IN ANY HUE ════════════════════════════
 *
 * The tempting fix is to leave it filled and swap the cyan for
 * fx-color-info, which measures 5.76:1 under the same ink and would close the
 * SC 1.4.3 finding in one line. It is refused for two reasons, and the second
 * is the one that decides it:
 *
 *   · SECTION 1 ALREADY RULED THIS SHAPE OUT. Alerts in this file lost their
 *     tinted surface three passes ago and now carry severity as a coloured mark
 *     on a neutral plane. A tinted badge is that same object an order of
 *     magnitude smaller; restoring it here would reopen that decision from
 *     underneath rather than argue with it. The section header above says the
 *     same thing in its own words: three of the four variants are outline only,
 *     and the ratio IS the point.
 *   · A FILLED SET IS INDISTINGUISHABLE IN GREYSCALE. See the marker note
 *     below, where it is measured.
 *
 * So the utilities land on VARIANT 4, the tonal outline, which is the shape
 * this file already uses for exactly this job.
 *
 * ══ WHY THE HAIRLINE IS `currentColor` AND NOT `fx-color-*-border` ═════════
 *
 * Variant 4 proper takes the `-border` rung, and that rung does not survive
 * being asked to carry a boundary. Measured against the plane these badges
 * actually sit on — level 2, #F7FBF5 in light and #1B291F in dark:
 *
 *     light   info 1.47 · success 1.43 · warning 1.42 · danger 1.54
 *     dark    info 3.68 · success 3.66 · warning 3.78 · danger 2.89
 *
 * Once the fill is gone the outline is the only thing that identifies the
 * object as a badge at all, which is SC 1.4.11 at 3:1, and light fails it four
 * times out of four. `currentColor` resolves to the same `-fg` the label
 * already uses, so the stroke measures exactly what the label measures — 5.52
 * to 6.28:1 light, 5.51 to 8.31:1 dark — and it is also what the
 * `prefers-contrast: more` and `forced-colors` tiers in section 11 write for
 * `.badge`, so all three agree instead of one being an exception.
 *
 * THAT WEAK `-border` RUNG IS A REAL FINDING ABOUT `.fx-badge-tone` AND
 * `.fx-status` TOO, and it is reported rather than swept: those are authored
 * variants where a soft edge was chosen deliberately, and re-tuning five legal
 * status classes is a change to that block, not to this one.
 *
 * ══ THE MARKERS ARE THE ENTIRE NON COLOUR CHANNEL ══════════════════════════
 *
 * The arithmetic that forces them is the opposite of what a reader expects, so
 * it is recorded. The four status hues were chosen in 00-tokens 1.6 to sit at
 * SIMILAR APPARENT LIGHTNESS, precisely so that one ink could serve all four
 * fills. Similar lightness means IDENTICAL GREYSCALE. Measured as sRGB luma on
 * the tokens themselves, 0 black to 255 white:
 *
 *     light   info 86 · success 85 · warning 90 · danger 65
 *     dark    info 182 · success 182 · warning 187 · danger 144
 *
 * Info and success are ONE STEP apart in light and EXACTLY EQUAL in dark. Hue
 * is therefore not a channel this set has; it never was, and no retune of the
 * colours can give it one without breaking the shared ink. Under
 * `forced-colors` the situation is not close — every author colour is discarded
 * outright. So each SEMANTIC takes a marker SHAPE that no other status in this
 * section already spends, which is the rule the legal status block above states
 * in its own header, applied to the family that arrived without one:
 *
 *     success   filled circle    as `.fx-status-active`, and the same hue
 *     warning   hollow circle    as `.fx-status-pending`, and the same hue
 *     danger    filled diamond   as `.fx-status-overdue`, and the same hue
 *     info      hollow square    the one shape the set had not spent
 *
 * THE NEUTRALS TAKE NO MARKER, deliberately. `bg-secondary`, `bg-light` and
 * `bg-dark` assert no severity at all — they are a grey, a pale and an inked
 * label — and a marker on them would invent a meaning the class does not carry.
 * They take a text tier each and nothing else, which is the Q3 answer.
 *
 * `bg-primary` AND `text-bg-primary` ARE NOT IN ANY LIST BELOW, and their
 * absence is the decision. A badge that asks for the brand by name is site 7
 * asking for site 7; the base `.badge` rule already paints it, and 10-utilities
 * already maps the utility onto fx-color-primary with its paired ink. Adding
 * them here would take the one sanctioned brand fill off a badge.
 *
 * WHAT THIS BLOCK DOES NOT TOUCH: `.rounded-pill`. The Identity module writes
 * it on these badges by hand, 03-leptonx points `bs-border-radius-pill` at
 * fx-radius-PILL on purpose, and a capsule asked for by name in markup is the
 * sanctioned meaning of that token. It is reported as an observation, not
 * overridden here — and nothing in this block reintroduces a capsule anywhere.
 * ────────────────────────────────────────────────────────────────────────── */
.badge:is(
    .bg-info, .bg-success, .bg-warning, .bg-danger,
    .bg-secondary, .bg-light, .bg-dark,
    .text-bg-info, .text-bg-success, .text-bg-warning, .text-bg-danger,
    .text-bg-secondary, .text-bg-light, .text-bg-dark
) {
    /* TWO IMPORTANCE FLAGS, AND EACH CANCELS A VENDOR ONE. Bootstrap's utility
     * API writes every class in the list above with `!important` — `.bg-*` on
     * the background, `.text-bg-*` on the background AND on the colour — so a
     * rule without the flag loses at any specificity whatsoever. No inline style
     * and no third party sheet is involved: these two lines cancel
     * `bootstrap.rtl.css` and nothing else, and they are the only two flags this
     * block spends. The border needs none, because Bootstrap writes no border on
     * these utilities and the `.badge` root it has to beat is (0,1,0). */
    background-color: transparent !important;
    color: var(--fx-badge-ctx-fg, var(--fx-color-text-muted)) !important;
    border-color: currentColor;
}

/* One local per member, so the rule above stays a single shape decision and the
 * hue is chosen here. Every semantic reads the `-fg` rung and never the bare
 * solid: 2.7 is explicit that a bare solid is a FILL UNDER A LIGHT LABEL and
 * that anything READ or anything acting as a BOUNDARY takes `-fg`, which is
 * both jobs at once here. All four semantics carry the same split, so the
 * light-only debt named on fx-color-danger is not special to danger. */
.badge:is(.bg-info,      .text-bg-info)      { --fx-badge-ctx-fg: var(--fx-color-info-fg); }
.badge:is(.bg-success,   .text-bg-success)   { --fx-badge-ctx-fg: var(--fx-color-success-fg); }
.badge:is(.bg-warning,   .text-bg-warning)   { --fx-badge-ctx-fg: var(--fx-color-warning-fg); }
.badge:is(.bg-danger,    .text-bg-danger)    { --fx-badge-ctx-fg: var(--fx-color-danger-fg); }
.badge:is(.bg-secondary, .text-bg-secondary) { --fx-badge-ctx-fg: var(--fx-color-text-muted); }
.badge:is(.bg-light,     .text-bg-light)     { --fx-badge-ctx-fg: var(--fx-color-text-subtle); }
.badge:is(.bg-dark,      .text-bg-dark)      { --fx-badge-ctx-fg: var(--fx-color-text); }

/* The marker. Default shape is the filled circle, which is `success`; the three
 * rules after it vary the shape and nothing else. The badge root is already an
 * inline flex row with an `fx-space-1` gap, so the marker needs no margin of its
 * own and the label does not move. */
.badge:is(
    .bg-info, .bg-success, .bg-warning, .bg-danger,
    .text-bg-info, .text-bg-success, .text-bg-warning, .text-bg-danger
)::before {
    content: "";
    flex: 0 0 auto;
    inline-size: 0.5em;
    block-size: 0.5em;
    background-color: currentColor;
    border-radius: var(--fx-radius-circle);
}
/* HOLLOW, for warning and info. The ring is an inset shadow rather than a
 * border for the reason `.fx-status-pending` gives above: a border would change
 * the marker's box and knock the label off its baseline, and these two sit in
 * the same row as the filled pair. */
.badge:is(.bg-warning, .text-bg-warning, .bg-info, .text-bg-info)::before {
    background-color: transparent;
    box-shadow: inset 0 0 0 var(--fx-border-width-strong) currentColor;
}
/* SQUARE, for info — a hollow square against warning's hollow circle. Written
 * as a literal zero rather than through a token on purpose, the same way
 * `.fx-status-archived` pins its corner: this radius is the only thing that
 * separates two semantics once colour is gone, so it must not become `unset` if
 * the token layer ever fails to parse. */
.badge:is(.bg-info, .text-bg-info)::before {
    border-radius: 0;
}
/* DIAMOND, for danger — the loudest shape for the loudest severity, and the
 * only one of the four with a corner pointing at the reader. The rotation is
 * symmetric about both axes, so it is direction neutral and needs no RTL
 * treatment. */
.badge:is(.bg-danger, .text-bg-danger)::before {
    border-radius: 0;
    transform: rotate(45deg);
}


/* Removable chip. SC 2.5.8: 24x24 is the hard floor, read from
 * `fx-control-h-xs` — the rung that sits exactly ON that floor — rather than
 * typed, and lifted to 44 on coarse pointers in section 10. A chip that hosts
 * one of these can never be the 20px size; see the guard above.
 * Skin is inherited from the chip, not from a button. */
.fx-chip-remove {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--fx-control-h-xs);
    block-size: var(--fx-control-h-xs);
    margin-inline-start: var(--fx-space-1);
    margin-inline-end: calc(var(--fx-space-1) * -1);
    padding: 0;
    background: none;
    border: 0;
    border-radius: var(--fx-radius-circle);
    color: inherit;
    font-size: inherit;
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--fx-dur-fast) var(--fx-ease-standard);
}
@media (any-hover: hover) {
    .fx-chip-remove:hover {
        background-color: var(--fx-color-hover);
    }
}
/* Focus is the outline. The glow that used to be here is gone with the token
 * that produced it.
 *
 * THE OFFSET IS NEGATIVE, AND THAT IS NOT A STYLE CHOICE. This control lives
 * INSIDE a chip, and the chip root carries `overflow: hidden` to keep a long
 * label ellipsised — which clips anything a descendant paints outside its own
 * box. At the sanctioned +2px the ring was drawn into that clipped region and
 * lost its block start, block end and inline end edges. It went unnoticed while
 * the light ring was translucent ink at roughly 3.5:1; the ring is green-800
 * now, 7.24 to 8.27:1, and a three sided indicator is unmissable. Drawing it
 * INSIDE the 24px button keeps the whole ring visible and is the same treatment
 * the flush list row and the accordion header already use in 07.
 * `fx-shadow-focus` is NOT the answer here: a box-shadow is clipped by an
 * ancestor's overflow exactly as an outline is. It remains the sanctioned ring
 * only where the element clips ITS OWN overflow, which is a different case. */
.fx-chip-remove:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: calc(var(--fx-focus-offset) * -1);
}


/* ── 4) Empty state ─────────────────────────────────────────────────────────
 * Every list page renders this instead of a bare empty grid. Calm, centred, and
 * measure capped so the supporting copy stays readable on a 4K monitor.
 * ------------------------------------------------------------------------- */
.fx-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--fx-space-3);
    inline-size: 100%;
    max-inline-size: min(28rem, 100%);
    margin-inline: auto;
    padding-block: var(--fx-space-7);
    padding-inline: var(--fx-space-4);
    text-align: center;
    color: var(--fx-color-text-muted);
}
.fx-empty-state-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: calc(var(--fx-icon-lg) * 2);
    block-size: calc(var(--fx-icon-lg) * 2);
    font-size: var(--fx-icon-lg);
    /* TERTIARY glyph on a level 2 disc. A filled disc reads as a placed object;
     * an outlined one reads as a missing image. */
    color: var(--fx-color-text-subtle);
    background-color: var(--fx-color-level-2, var(--fx-color-bg-subtle));
    border: 0;
    border-radius: var(--fx-radius-circle);
}
.fx-empty-state-title {
    margin: 0;
    font-family: var(--fx-font-heading);
    font-size: var(--fx-text-base, var(--fx-text-md));
    font-weight: var(--fx-weight-semibold, 600);
    letter-spacing: var(--fx-tracking-title);
    color: var(--fx-color-text);
}
.fx-empty-state-text {
    margin: 0;
    font-size: var(--fx-text-sm);
    line-height: var(--fx-leading-relaxed, 1.9);
    color: var(--fx-color-text-muted);
    overflow-wrap: anywhere;
}
.fx-empty-state-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--fx-space-2);
    margin-block-start: var(--fx-space-2);
}

/* THE ARRIVAL, WHICH THIS COMPONENT DID NOT HAVE. Added 2026-08-03.
 *
 * An empty state is almost never on the page at first paint. It is what REPLACES
 * a grid or a skeleton the moment a query comes back with nothing, which means
 * it appears in a region the user is already watching, in place of something
 * else — the one situation where arriving instantly reads as a glitch rather
 * than as speed. Everything else on this platform that swaps into a settled
 * region has an entrance; this was the hole.
 *
 * `fx-rise-in`, and it is the RIGHT keyframe here where it was the wrong one for
 * the alert a few sections up. An alert lands inside `.lpx-content`, which is
 * already rising on page load, so a second rise stacks. An empty state lands
 * long AFTER that entrance has finished, in a region that is standing still, so
 * the 8px is read as the block settling into the space the data was going to
 * occupy. It is not a popup and takes no scale.
 *
 * `fx-dur-base` (160ms), not `fx-dur-slow`. The user is waiting on this: it is
 * the answer to a query they just ran. It is also the timing the toast and the
 * alert use, so every "here is what happened" surface in this file lands
 * together.
 *
 * THE PARTS CAN STAGGER IF A PAGE WANTS IT — add `.fx-stagger` to the root in
 * markup and the icon, title, copy and actions arrive 40ms apart on the shared
 * mechanism in 01-base. It is deliberately NOT declared here: four children
 * ramping over 120ms in front of somebody who has just been told there is
 * nothing to see is a flourish charged to a disappointment. */
@media (prefers-reduced-motion: no-preference) {
    .fx-empty-state {
        animation: fx-rise-in var(--fx-dur-base) var(--fx-ease-decel) backwards;
    }
}


/* ── 5) Skeletons ───────────────────────────────────────────────────────────
 * Perceived performance during a load. Under reduced motion the shimmer stops
 * and the block falls back to a flat tint that is still obviously a
 * placeholder. Mark the region `aria-busy="true"` and `aria-hidden="true"` on
 * the shapes so a screen reader hears the loading state instead of a wall of
 * empty boxes.
 *
 * Retuned onto the LEVEL ladder rather than onto the well: a skeleton stands in
 * for content that is going to appear on a surface, so its base is the plane
 * that surface sits on and the sweep peaks one plane above it. The old pairing
 * used `fx-color-bg-subtle` — the well, a plane BELOW the page — which in
 * dark painted the placeholder darker than the card it was standing in.
 * ------------------------------------------------------------------------- */
.fx-skeleton {
    /* A real sweep: a narrow bright band travelling across a flat base, rather
     * than the whole block pulsing. The five stops are what make it read as a
     * highlight moving over a surface instead of a colour change. */
    display: block;
    max-inline-size: 100%;
    background-color: var(--fx-color-level-2, var(--fx-color-bg-subtle));
    /* Physical 90deg: mirrored for RTL below so the sweep always runs with the
     * reading direction. */
    background-image: linear-gradient(
        90deg,
        var(--fx-color-level-2) 0%,
        var(--fx-color-level-2) 35%,
        var(--fx-color-hover) 45%,
        var(--fx-color-level-3) 50%,
        var(--fx-color-hover) 55%,
        var(--fx-color-level-2) 65%,
        var(--fx-color-level-2) 100%
    );
    background-size: 200% 100%;
    background-repeat: no-repeat;
    border-radius: var(--fx-radius-xs);
    user-select: none;
}
[dir="rtl"] .fx-skeleton {
    background-image: linear-gradient(
        270deg,
        var(--fx-color-level-2) 0%,
        var(--fx-color-level-2) 35%,
        var(--fx-color-hover) 45%,
        var(--fx-color-level-3) 50%,
        var(--fx-color-hover) 55%,
        var(--fx-color-level-2) 65%,
        var(--fx-color-level-2) 100%
    );
}
/* The sweep is a PERIOD, not a transition, so the 200 to 400ms band rule does
 * not apply to it. Derived from a token rather than typed: 1.5s at the default
 * scale, and it slows with everything else if the scale is retuned. */
@media (prefers-reduced-motion: no-preference) {
    .fx-skeleton {
        animation: fx-skeleton-sweep calc(var(--fx-dur-slower) * 3) var(--fx-ease-standard) infinite;
    }
}
.fx-skeleton-text {
    block-size: 0.7em;
    margin-block: 0.25em;
}
.fx-skeleton-text:last-child {
    inline-size: 60%;
}
.fx-skeleton-title {
    block-size: 1.1em;
    inline-size: 40%;
    margin-block-end: var(--fx-space-3);
}
.fx-skeleton-avatar {
    inline-size: calc(var(--fx-icon-lg) * 1.5);
    block-size: calc(var(--fx-icon-lg) * 1.5);
    flex: 0 0 auto;
    border-radius: var(--fx-radius-circle);
}
/* Rows are separated by their own height and padding, which is also what the
 * real table does now — see 07 section 9. No rule, no stripe. */
.fx-skeleton-row {
    display: flex;
    align-items: center;
    gap: var(--fx-space-3);
    min-block-size: var(--fx-row-h, var(--fx-control-h-lg));
    padding-block: var(--fx-space-2);
    border-block-end: 0;
    animation: none;
    background: none;
}
.fx-skeleton-row > .fx-skeleton {
    flex: 1 1 auto;
    block-size: 0.8em;
}

@keyframes fx-skeleton-sweep {
    /* Background position is physical; the gradient direction above is what the
     * RTL rule mirrors, so the perceived sweep follows the reading order. */
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}


/* ── 6) Spinners and loading overlays ──────────────────────────────────────
 * Any region that swaps in a spinner should carry `aria-busy="true"` while it
 * loads, and the spinner itself needs a visually hidden label or `aria-label`
 * so the state is announced, not merely drawn.
 *
 * A SPINNER IS NEUTRAL. It used to be `fx-color-primary`, which spent brand
 * on a loading indicator — see the accent rule in 00-tokens 3.9, which sends a
 * progress or loading indicator to a text tier unless the progress itself is
 * the brand moment. It is not: a busy ring appears on every screen in the
 * application, which is the definition of a colour that has stopped meaning
 * anything.
 * ------------------------------------------------------------------------- */
/* The spinner "border" is the RING ITSELF, not a stroke around a box. Tokenised
 * so it tracks `prefers-contrast: more` with the rest of the system. */
.spinner-border,
.spinner-grow {
    --bs-spinner-width: var(--fx-icon-lg);
    --bs-spinner-height: var(--fx-icon-lg);
    --bs-spinner-border-width: var(--fx-border-width-strong);
    color: var(--fx-color-text-subtle);
    vertical-align: middle;
}
.spinner-border-sm,
.spinner-grow-sm {
    --bs-spinner-width: var(--fx-icon-sm);
    --bs-spinner-height: var(--fx-icon-sm);
    --bs-spinner-border-width: var(--fx-border-width-strong);
}

/* Our own two tone ring: the track stays visible, so the spinner reads as a
 * loading indicator rather than a fragment. */
.fx-spinner {
    display: inline-block;
    flex: 0 0 auto;
    inline-size: var(--fx-icon-lg);
    block-size: var(--fx-icon-lg);
    /* Same rationale as `.spinner-border`: this draws the track and the head of
     * the ring, so it is the component, not an outline around one. */
    border: var(--fx-border-width-strong) solid var(--fx-color-border-emphasis);
    border-block-start-color: var(--fx-color-text-subtle);
    border-radius: var(--fx-radius-circle);
    /* Rotation is physical by definition and symmetric in effect. The period is
     * derived from the motion scale rather than typed, so the damper reaches
     * it; a loop is a period and not a transition, so the 200 to 400ms band
     * rule does not apply. */
    animation: fx-spin calc(var(--fx-dur-slower) * 2) linear infinite;
}
.fx-spinner-sm { inline-size: var(--fx-icon-sm); block-size: var(--fx-icon-sm); }
.fx-spinner-lg { inline-size: calc(var(--fx-icon-lg) * 2); block-size: calc(var(--fx-icon-lg) * 2); }

@keyframes fx-spin {
    to { transform: rotate(360deg); }
}

/* Full surface overlay. Put it inside any element that is `position: relative`
 * to lock just that region, or inside a fixed wrapper for the page. It centres
 * correctly at every size because it fills its containing block.
 *
 * THE BLUR IS GONE. `backdrop-filter` was the last glass treatment in this
 * file; the overlay is now an opaque plane, which is also the only version that
 * is legible without a supports query and a second opaque fallback. */
.fx-loading-overlay {
    position: absolute;
    inset: 0;
    z-index: var(--fx-z-fixed);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--fx-space-3);
    padding: var(--fx-space-4);
    background-color: var(--fx-color-overlay);
    border-radius: inherit;
    color: var(--fx-color-text-muted);
    font-size: var(--fx-text-sm);
    text-align: center;
}
.fx-loading-overlay-fixed {
    position: fixed;
    z-index: var(--fx-z-backdrop);
}

/* THE ANTI FLASH DELAY — the answer to "a loading state that flashes".
 * Added 2026-08-03, and the DELAY is the feature. The fade is incidental.
 *
 * A busy overlay is mounted the instant a request leaves and unmounted the
 * instant it returns. On a local network most of them return in well under a
 * tenth of a second, so the overlay's entire visible life is one or two frames:
 * the screen greys and un-greys faster than the eye can resolve what happened,
 * and the user reads it as the page glitching rather than as the page working.
 * The classic failure is that this gets WORSE as the backend gets faster.
 *
 * HOW THE DELAY DOES IT. `backwards` applies the from-frame — opacity 0 —
 * throughout `animation-delay`, so for the first 160ms the overlay is mounted,
 * blocking input, and completely invisible. A request that finishes inside that
 * window unmounts it having never painted, and the user sees nothing at all,
 * which is the correct report for an operation that was instant. A request that
 * outlives the window fades up over 100ms and is then obviously a loading state.
 * One declaration replaces the setTimeout dance this normally takes in script.
 *
 * BOTH VALUES ARE TOKENS, so the whole thing is inside the no-preference gate
 * and simply does not apply under reduced motion — where the overlay appears at
 * once, which is right: that user asked for state changes to be swaps.
 *
 * `.abp-block-area` GETS THE SAME TREATMENT AND IS THE ONE THAT MATTERS. ABP
 * raises its own busy blocker around every AJAX call the framework makes, so it
 * is the loading state this application shows most often by a wide margin. The
 * platform's own overlay and the framework's now flash identically, which is to
 * say neither of them does. */
@media (prefers-reduced-motion: no-preference) {
    .fx-loading-overlay,
    .abp-block-area {
        animation: fx-fade-in var(--fx-dur-fast) var(--fx-ease-standard)
                   var(--fx-dur-base) backwards;
    }
}

/* ABP's own busy blocker (`libs/abp/core/abp.css`) ships a white sheet and a
 * blue ring that ignore the theme. Repaint both onto tokens, and onto the same
 * neutral the spinners above use. */
.abp-block-area {
    background-color: var(--fx-color-overlay);
    opacity: 1;
    z-index: var(--fx-z-backdrop);
}
.abp-block-area.abp-block-area-busy::after {
    color: var(--fx-color-text-muted);
    font-family: var(--fx-font-body);
    font-size: var(--fx-text-sm);
}
.abp-block-area.abp-block-area-busy::before {
    border-color: var(--fx-color-border-emphasis);
    border-block-start-color: var(--fx-color-text-subtle);
}


/* ── 7) Progress ────────────────────────────────────────────────────────────
 * A thin neutral track with a thin neutral fill. Determinate bars carry a value
 * and should expose `role="progressbar"` with `aria-valuenow`. The indeterminate
 * variant deliberately has NO value and must omit `aria-valuenow` so assistive
 * tech does not announce a false percentage.
 *
 * THE BAR IS NOT BRAND. `bs-progress-bar-bg` used to be `fx-color-primary`;
 * the accent rule sends a progress bar to `fx-color-text-subtle` unless the
 * progress IS the brand moment, which a generic bar never is. The status
 * modifiers still tint, because a status hue carries meaning.
 *
 * The inset shadow that used to sit under the track is gone too: a 4px bar does
 * not need a depth signal, and the one it had was the only one in this file
 * carrying real weight.
 * ------------------------------------------------------------------------- */
.progress,
.progress-stacked {
    --bs-progress-height: var(--fx-space-1);
    --bs-progress-bg: var(--fx-color-level-2, var(--fx-color-bg-subtle));
    --bs-progress-border-radius: var(--fx-radius-pill);
    --bs-progress-bar-bg: var(--fx-color-text-subtle);
    --bs-progress-bar-color: var(--fx-color-text-on-primary);
    --bs-progress-font-size: max(0.75rem, var(--fx-text-2xs));
    --bs-progress-box-shadow: none;
    inline-size: 100%;
    overflow: hidden;
}
.progress-bar {
    transition: width var(--fx-dur-base) var(--fx-ease-standard);
}
.progress-bar-success { --bs-progress-bar-bg: var(--fx-color-success); }
.progress-bar-warning { --bs-progress-bar-bg: var(--fx-color-warning); }
.progress-bar-danger  { --bs-progress-bar-bg: var(--fx-color-danger); }
.progress-bar-info    { --bs-progress-bar-bg: var(--fx-color-info); }

/* Striped: the stripe angle is physical and symmetric, so it needs no mirror. */
.progress-bar-striped {
    background-image: linear-gradient(
        45deg,
        var(--fx-color-hover) 25%,
        transparent 25%,
        transparent 50%,
        var(--fx-color-hover) 50%,
        var(--fx-color-hover) 75%,
        transparent 75%,
        transparent
    );
    background-size: var(--fx-space-4) var(--fx-space-4);
}

/* Indeterminate: no value, a travelling sliver instead of a fill. A period, not
 * a transition — see the note on the skeleton sweep.
 *
 * THE TRAVEL IS A TRANSFORM NOW, NOT A MARGIN. Changed 2026-08-03, and it is the
 * same defect the toast drain bar had, in its worst form: this animation runs
 * `infinite`. It used to move `margin-inline-start` from -35% to 100%, and a
 * margin is a LAYOUT property — so for as long as the bar was on screen, forever,
 * every frame re-laid out the sliver inside its track on the main thread, while
 * the user waited for the very work the bar exists to report. A translate runs on
 * the compositor and costs nothing on the thread doing the loading.
 *
 * THE ARITHMETIC, because the numbers stop being self evident once they are
 * percentages of the ELEMENT rather than of the track. A translate percentage
 * resolves against the element's own width, and the element is 35% of the track:
 *   fully off the start edge  = -35% of track = -100% of self
 *   fully past the end edge   = +100% of track = 100/35 = 285.714% of self
 * If the 35% width above is ever retuned, both keyframe values change with it.
 *
 * DIRECTION IS CARRIED BY A MULTIPLIER RATHER THAN BY A SECOND KEYFRAME SET.
 * `translateX` is physical and the old logical margin was direction correct for
 * free, so the sign is flipped through `fx-progress-dir` for RTL and the single
 * keyframe serves both scripts — the same shape `fx-slide-in-inline` in 01-base
 * uses for the toast, and the reason no second copy has to be kept in sync.
 *
 * `will-change` IS EARNED, on the same test the drain bar passes: a transform
 * genuinely runs here, continuously, for the whole time the element is mounted. */
.progress.fx-progress-indeterminate .progress-bar,
.fx-progress-indeterminate > .progress-bar {
    --fx-progress-dir: 1;
    inline-size: 35%;
    will-change: transform;
    animation: fx-progress-slide calc(var(--fx-dur-slower) * 3) var(--fx-ease-standard) infinite;
    transition: none;
}
[dir="rtl"] .progress.fx-progress-indeterminate .progress-bar,
[dir="rtl"] .fx-progress-indeterminate > .progress-bar {
    --fx-progress-dir: -1;
}
@keyframes fx-progress-slide {
    /* Travels across the track. See the arithmetic and the direction note above. */
    from { transform: translateX(calc(-100% * var(--fx-progress-dir, 1))); }
    to   { transform: translateX(calc(285.714% * var(--fx-progress-dir, 1))); }
}


/* ── 7.1) THE LOADING SYSTEM — the parts sections 5 to 7 did not have ───────
 *
 * Sections 5, 6 and 7 above already ship the PRIMITIVES: a skeleton, a spinner,
 * a region overlay with the anti flash delay, and a progress bar in both
 * determinate and indeterminate form. 09-tabulator section 27 ships the grid's
 * skeleton rows. None of that is duplicated here and none of it should be — if
 * you are about to add a shape to this section, check those first.
 *
 * WHAT WAS ACTUALLY MISSING was everything that is not a shape:
 *   · a NAVIGATION progress bar, for the full page loads a server rendered
 *     Razor application does on every link click
 *   · a BUTTON busy state that does not resize the button
 *   · a STAGE line, for the multi second report and export waits
 * Those three are below, and the scheduling that governs all of them lives in
 * frihat-loading.js and in the two fx-load-* tokens in 00-tokens section 7.
 *
 * ══ WHY NONE OF THIS IS BRAND, INCLUDING THE BAR ═══════════════════════════
 *
 * The brief that commissioned this section asked whether the product's
 * signature loading bar may carry the brand green, and 00-tokens 3.9 leaves the
 * door open: its FORBIDDEN list sends a progress bar to fx-color-text-subtle
 * "unless the progress itself is the brand moment". That clause is real. It
 * does not apply here, and the reason is the same test 3.9 used to strike the
 * list marker off its own sanctioned list:
 *
 *   EVERY SANCTIONED SITE IN 3.9 IS BOUNDED. Once per page, once per screen
 *   region, once per sorted column. A navigation progress bar appears on EVERY
 *   LINK CLICK IN THE PRODUCT, without bound, for the entire working day. A
 *   colour shown that often is the definition of one that has stopped meaning
 *   anything — which is the argument section 6 above already used to take the
 *   brand off the spinner, and it applies with more force here, not less.
 *
 * So the bar is not brand. It is not fx-color-text-subtle either, and that is
 * the one genuine finding: the bar rides the TOP EDGE OF THE VIEWPORT, which on
 * every application page is the brand header band (3.9 site 11). A subtle ink
 * tier is computed against a CONTENT plane and would be very nearly invisible
 * on green. The bar therefore takes fx-band-fg — THE BAND'S OWN FOREGROUND INK,
 * the tier already proven against that exact fill at 7.38:1 in 03-leptonx
 * section 4. It is a text tier, exactly as 3.9 requires; it is simply the text
 * tier belonging to the plane it is drawn on rather than to the page.
 *
 * That also makes it theme independent for free: fx-color-band and
 * fx-color-band-fg are both PINNED across the two themes, so the bar is one
 * colour at one contrast everywhere, which is what a signature element wants.
 * The fallback covers the account shell, which has no band.
 * ────────────────────────────────────────────────────────────────────────── */

/* ── The navigation bar ──
 * One element, appended to <body> once and reused. Fixed to the top edge, above
 * the band and below any modal, so a page load reports itself over the chrome
 * without ever covering a dialog the user is reading.
 *
 * IT IS DRIVEN BY A CUSTOM PROPERTY, NOT BY A WIDTH. `--fx-loadbar-progress` is
 * a 0 to 1 scalar that script sets; the fill reads it through scaleX. That is a
 * compositor property, so the trickle costs nothing on the main thread — which
 * is the whole point, because the main thread is busy doing the work the bar
 * exists to report. It is the same reasoning section 7 records for turning the
 * indeterminate sliver from a margin into a transform.
 *
 * NO `will-change`. The indeterminate sliver earns one because it animates
 * continuously for as long as it is mounted; this bar moves a handful of times
 * per navigation and is then gone, so a permanent compositor layer for it would
 * be a cost with no matching benefit. */
/* ══ THE BAR CARRIES ITS OWN GROUND. READ THIS BEFORE CHANGING EITHER COLOUR.
 *
 * THE DEFECT THIS FIXES, measured in the browser at 2.80:1 against a 4.5:1
 * floor. The fill used to read `var(--fx-band-fg, var(--fx-color-text-subtle))`.
 * That arithmetic was right — the band's ink on the band's fill IS 7.38:1 — and
 * the rule was still broken, because `--fx-band-fg` is declared by 03-leptonx
 * ON `.lpx-topbar`, and this element is appended to <body>. A custom property
 * is INHERITED, not global: outside the topbar's subtree the name resolves to
 * nothing, the fallback took over, and the bar painted `#8b9a8f` on green.
 *
 * THE FALLBACK IS WHAT HID IT. A missing token with no fallback would have
 * painted `transparent` and been obvious in the first second of use; the
 * fallback rendered something plausible instead, which is how this survived
 * both an audit and a visual check. A fallback on a colour token is a decision
 * about what to do when the design system is absent, and "quietly pick a
 * different tier" is rarely the right answer.
 *
 * THE FIX IS NOT A LITERAL AND NOT A MOVE. Two changes:
 *   1. Both colours now come from `--fx-color-band` / `--fx-color-band-fg`,
 *      which 00-tokens declares at the THEME ROOT (2.4.2) rather than on a
 *      component. They resolve anywhere in the document, including on <body>.
 *   2. The bar PAINTS ITS OWN TRACK in the band's fill. So the 7.38:1 no longer
 *      depends on the bar happening to sit over the band — it is true by
 *      construction, on any backdrop, including the account shell that renders
 *      no band at all and the moment mid-scroll when the band is not there.
 * Both tokens are PINNED across the two themes, so the bar is one colour at one
 * measured contrast everywhere. */
.fx-loadbar {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    z-index: var(--fx-z-tooltip);
    block-size: var(--fx-load-bar-size);
    /* The track. See the note above: this is what makes the contrast a property
     * of the component rather than of where it happens to be sitting. */
    background-color: var(--fx-color-band);
    pointer-events: none;
    /* Hidden until script raises it. `visibility` rather than `display` so the
     * fill's transition has something to run on when it is revealed. */
    visibility: hidden;
    opacity: 0;
}
.fx-loadbar[data-active="true"] {
    visibility: visible;
    opacity: 1;
}
.fx-loadbar-fill {
    display: block;
    block-size: 100%;
    /* THE BAND'S INK ON THE BAND'S FILL, and both resolved from the THEME ROOT
     * rather than from the topbar's subtree — see the note on `.fx-loadbar`
     * above, which is where the 2.80:1 defect is recorded. NO FALLBACK, and
     * that is deliberate: if this token ever stops resolving the bar must fail
     * visibly rather than quietly repaint itself at a different tier. */
    background-color: var(--fx-color-band-fg);
    /* Fills from the reading start edge. `transform-origin` takes no logical
     * keyword, so the RTL case is a one line override below rather than a
     * second rule set — the same shape section 7 uses for the sliver. */
    transform-origin: 0 50%;
    transform: scaleX(var(--fx-loadbar-progress, 0));
    transition: transform var(--fx-dur-base) var(--fx-ease-decel);
}
[dir="rtl"] .fx-loadbar-fill {
    transform-origin: 100% 50%;
}
/* The completion fade. Script sets this for one frame at the end so the bar
 * reaching 100% is seen before it leaves, rather than vanishing at 90%. */
.fx-loadbar[data-done="true"] {
    opacity: 0;
    transition: opacity var(--fx-dur-fast) var(--fx-ease-in-quad);
}


/* ── The button busy state ──
 * THE BUTTON DOES NOT CHANGE SIZE, and the mechanism is the reason this is CSS
 * rather than script. The obvious implementation swaps the label for a spinner
 * and then has to measure and pin the old width from JavaScript, which is a
 * layout read on every submit and is wrong the moment a font finishes loading.
 *
 * Instead the LABEL STAYS EXACTLY WHERE IT IS and is merely made invisible with
 * `color: transparent`. It still occupies its box, so the button's intrinsic
 * width is unchanged by construction — no measurement, no pinning, no reflow,
 * and correct for any label in any language at any font size. The spinner is
 * then drawn in the middle by a pseudo element.
 *
 * NO `!important` ANYWHERE. `.btn` plus `.fx-btn-busy` weighs (0,2,0), which
 * matches the heaviest colour rule Bootstrap and 04-buttons put on a button
 * variant, and this file loads after both — so source order carries it. The
 * bare `.fx-btn-busy` alongside covers non Bootstrap buttons.
 *
 * THE RING CANNOT READ `currentColor`, AND THAT IS THE SUBTLETY. `color` on
 * this element is transparent by the rule below, and a pseudo element inherits
 * it — so a ring drawn in currentColor would be invisible, which is the obvious
 * implementation and the wrong one. It reads `--fx-btn-busy-ink` instead, which
 * resolves to the button's OWN variant ink (`--bs-btn-color`) independently of
 * the `color` property. A primary button therefore spins in its on-primary ink
 * and a ghost button in body ink, automatically, with no per variant rule and
 * NO NEW CONTRAST OBLIGATION TO MEASURE: every one of them is already the
 * button's own proven pairing, unchanged. */
.btn.fx-btn-busy,
.fx-btn-busy {
    position: relative;
    color: transparent;
    /* Resolved from the variant rather than from `color`. See the note above —
     * this is the declaration that keeps the ring visible. */
    --fx-btn-busy-ink: var(--bs-btn-color, var(--fx-color-text));
    /* Double submit is impossible while this class is on: the click never
     * reaches the element. Script also sets `disabled`; this is the half that
     * survives a page author who re-enables the control by hand. */
    pointer-events: none;
    cursor: default;
}
.btn.fx-btn-busy::after,
.fx-btn-busy::after {
    content: "";
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    inline-size: var(--fx-icon-sm);
    block-size: var(--fx-icon-sm);
    margin-block-start: calc(var(--fx-icon-sm) / -2);
    margin-inline-start: calc(var(--fx-icon-sm) / -2);
    /* The track is the same ink at low alpha, so the ring reads as a ring on
     * every button variant without naming a single one of them. */
    border: var(--fx-border-width-strong) solid
            color-mix(in srgb, var(--fx-btn-busy-ink) 30%, transparent);
    border-block-start-color: var(--fx-btn-busy-ink);
    border-radius: var(--fx-radius-circle);
    /* A period, not a transition — the 200 to 400ms ban does not reach it, and
     * it is derived from the scale rather than typed. Matches the four other
     * rings in this file exactly. */
    animation: fx-spin calc(var(--fx-dur-slower) * 2) linear infinite;
}
/* ── The blocked region's containing block ──
 * `.fx-loading-overlay` in section 6 is `position: absolute; inset: 0`, so it
 * fills its nearest POSITIONED ancestor. A region that is `position: static`
 * has none, and the overlay would escape to the page and grey the whole
 * viewport — the "local over global" rule broken by omission rather than by
 * decision.
 *
 * SCRIPT ADDS THIS CLASS ONLY TO STATIC HOSTS. An element the page author
 * already positioned is left alone, because promoting it would change what its
 * own absolutely positioned children resolve against. See mountOverlay in
 * frihat-loading.js — the computed position is read before this is applied. */
.fx-block-host {
    position: relative;
}


/* ── The stage line — for waits measured in seconds ──
 * A report or an export is not a spinner's job. This is a LABEL plus a bar,
 * where the label says what is happening in words and the bar says how far in.
 * Both determinate and paced indeterminate forms reuse section 7's `.progress`
 * unchanged; this adds only the row that carries the sentence. */
.fx-load-stage {
    display: flex;
    flex-direction: column;
    gap: var(--fx-space-2);
    padding-block: var(--fx-space-3);
}
/* The mark above a stage line. Centred on its own row so the label and the bar
 * below it keep a full measure — the mark is identity, not a bullet. */
.fx-load-stage-brand {
    display: flex;
    justify-content: center;
}

.fx-load-stage-label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--fx-space-3);
    color: var(--fx-color-text-muted);
    font-size: var(--fx-text-sm);
}
/* The percentage. Tabular figures so a bar climbing past 9 to 10 percent does
 * not shift the sentence beside it. */
.fx-load-stage-value {
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
    color: var(--fx-color-text-subtle);
}

/* THE FILL IS DRIVEN BY A CUSTOM PROPERTY, not by an inline width.
 * `frihat-loading.js` sets `--fx-stage-progress`; nothing in this codebase
 * assigns a presentational inline style, and a `style-src` policy without
 * 'unsafe-inline' would drop one — the CSP readiness CLAUDE.md 2.7 asks for.
 *
 * `width` rather than a transform, and it IS direction correct: `.progress` is
 * a flex container, so the bar is laid out from the inline start edge and
 * fills toward the end edge in both scripts with no mirror rule. That is also
 * why section 7's existing `transition: width` applies here unchanged rather
 * than needing a second declaration. The transform treatment is reserved for
 * the INDETERMINATE sliver, which animates continuously and therefore must not
 * touch layout; this bar moves only when the job reports a step. */
.fx-load-stage .progress-bar {
    width: var(--fx-stage-progress, 0%);
}


/* ── Skeleton shape containers ──
 * The SHAPES are section 5's and are not restated. These are the three frames
 * script assembles them into, and they exist so a skeleton matches the geometry
 * of the thing it stands in for — which is the entire reason a skeleton beats a
 * spinner. A placeholder that does not match causes the jump it was added to
 * prevent. */
.fx-skeleton-shape {
    padding: var(--fx-space-3);
}
.fx-skeleton-shape > * + * {
    margin-block-start: var(--fx-space-3);
}
/* A form: label over control, repeated. The pair is what makes it read as a
 * form rather than as a list of bars. */
.fx-skeleton-field > .fx-skeleton-text {
    inline-size: 30%;
    margin-block-end: var(--fx-space-2);
}
/* The control's own height and corner, so the placeholder occupies exactly the
 * box the real field will. `fx-control-h-lg` is the height 05-forms gives a
 * standard input; there is no bare `fx-control-h` in the system, so it is named
 * directly rather than through a fallback chain that would silently be wrong. */
.fx-skeleton-field > .fx-skeleton-control {
    block-size: var(--fx-control-h-lg);
    border-radius: var(--fx-radius-control);
}
/* Two column form, matching the density the real forms use. Collapses with
 * them at the same breakpoint. */
.fx-skeleton-grid-2 {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--fx-space-4);
}
@media (max-width: 47.99em) {
    .fx-skeleton-grid-2 { grid-template-columns: minmax(0, 1fr); }
}


/* ── 7.2) THE BRAND MOMENT — the signature loader ───────────────────────────
 *
 * ══ THE MODEL IS INVERTED HERE, AND IT IS A DELIBERATE REVERSAL ════════════
 *
 * Section 7.1 and 00-tokens section 7 describe a system whose first principle
 * is DO NOT PAINT unless the wait is real. That system was built, shipped, and
 * was invisible in the browser — correctly, by its own rules, and it was the
 * wrong product. The owner asked for a signature: something seen on EVERY
 * action, because the identity comes from the repetition, not from the artwork.
 *
 * SO THE BRANDED PATH DOES NOT CONSULT `--fx-load-delay`. It appears at once
 * and is HELD for `--fx-load-min-visible` before it is allowed to leave.
 *
 * AND THE FLASH PROBLEM SOLVES ITSELF, which is the part worth writing down: a
 * strobe is an indicator that appears and vanishes inside a few frames. A
 * guaranteed 500ms floor cannot strobe, by construction. The delay was only
 * ever one of two ways to buy that guarantee; the floor is the other, and it is
 * the one that buys it WHILE BEING SEEN. Nothing about the anti-flash reasoning
 * was wrong — the dial simply moved from "never paint" to "always paint, never
 * briefly". `--fx-load-delay` is kept and still governs the UNBRANDED paths.
 *
 * ══ THE MARK IS `icon-192x192.png` AND NEVER `logo.png` ════════════════════
 *
 * THIS IS A BUG FIX INHERITED FROM THE LOGIN PAGE, not a preference, and it is
 * the single most likely thing for a future edit to get wrong — `logo.png` is
 * the obvious name and it is the wrong file. logo.png is a TRANSPARENT PNG
 * whose artwork is black and gold. In dark mode the black half of it — the book
 * outline and the second line of the wordmark — disappears against the surface,
 * so half the logo is simply not there. Login.cshtml records the same swap for
 * the same reason. icon-192x192.png CARRIES ITS OWN GROUND, which is why it is
 * the one asset in this product that is correct in both themes.
 *
 * IT IS ALSO WHY THIS SECTION OWES NO CONTRAST MEASUREMENT ON THE MARK ITSELF.
 * The mark is an opaque logotype, and SC 1.4.11 exempts logotypes outright. The
 * numbers that ARE owed are for the ring and the label, which are ours, and
 * they are recorded against each surface below.
 *
 * BOTH ASSETS ARE LOCAL. No CDN, no data URI, no remote anything — the owner's
 * instruction on external references is absolute and this section is the most
 * tempting place in the codebase to break it.
 * ────────────────────────────────────────────────────────────────────────── */

/* The mark. A fixed square so a surface's height never depends on how the asset
 * is cropped, and `object-fit` so a replaced asset with a different intrinsic
 * ratio is contained rather than stretched — the same two guards the login band
 * uses, for the same reasons. */
/* THE MARK IS A CIRCLE AS OF 2026-08-14, by owner instruction, and the ASSET is
 * circular — the artwork scaled inside a green disc. It is NOT a square asset
 * with a radius applied, and the distinction is load bearing: a `border-radius`
 * circle looks identical on screen, but it cannot reach the BROWSER TAB at all,
 * and it would clip the calligraphy exactly where the tall strokes meet the
 * corners. The radius below is therefore a GUARD, not the mechanism — it keeps
 * the box honest if a square asset is ever swapped back in by mistake. */
.fx-brand-mark {
    display: block;
    inline-size: var(--fx-brand-size);
    block-size: var(--fx-brand-size);
    border-radius: var(--fx-radius-circle);
    object-fit: contain;
    flex: 0 0 auto;
    user-select: none;
}

/* ── The loader: a circular mark inside a concentric circular ring ──
 *
 * TWO CIRCLES NOW, WHICH MAKES THE GAP BETWEEN THEM A DESIGN DECISION RATHER
 * THAN LEFTOVER SPACE. While the mark was a rounded square the ring was simply
 * a box around it and the clearance did not read as anything; two concentric
 * circles a few pixels apart with no considered gap read as a rendering
 * artifact — a logo that has been accidentally double-stroked — not as a mark.
 *
 * THE GEOMETRY IS DERIVED, NOT A MULTIPLIER. The ring's diameter used to be a
 * flat `* 1.75`, which left roughly a third of the mark's own width as dead air
 * and drifted with every size variant. It is now built from the parts that
 * actually exist:
 *
 *     ring diameter = mark + 2 x (clear gap + ring stroke)
 *
 * so the CLEAR SPACE is stated once, in one place, and every size — sm, base,
 * lg — inherits the same optical relationship instead of the same ratio. The
 * ring is a separate element rather than a border on the mark because the mark
 * is an <img>, and a rotating border on a replaced element rotates the artwork
 * with it.
 *
 * THE GAP IS PROPORTIONAL TO THE MARK, at 0.18 of its diameter. Constant
 * clearance would crowd the small variant and look slack on the large one; a
 * proportional gap holds the same relationship at every size, which is what
 * makes the three variants read as one mark rather than three. */
.fx-brand-loader {
    --fx-brand-size: calc(var(--fx-icon-lg) * 2);
    --fx-brand-gap: calc(var(--fx-brand-size) * 0.18);
    --fx-brand-ring: calc(
        var(--fx-brand-size) + (var(--fx-brand-gap) + var(--fx-border-width-strong)) * 2
    );
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    inline-size: var(--fx-brand-ring);
    block-size: var(--fx-brand-ring);
}
.fx-brand-loader-sm { --fx-brand-size: var(--fx-icon-lg); }
.fx-brand-loader-lg { --fx-brand-size: calc(var(--fx-icon-lg) * 4); }

/* The ring. Concentric with the mark and sized off the same token, so one
 * declaration moves both. */
.fx-brand-loader-ring {
    position: absolute;
    inset: 0;
    border: var(--fx-border-width-strong) solid var(--fx-color-border-emphasis);
    border-block-start-color: var(--fx-color-text-subtle);
    border-radius: var(--fx-radius-circle);
    /* A period, not a transition — the 200 to 400ms ban does not reach it — and
     * derived from the motion scale rather than typed, exactly as the four
     * other rings in this file are. */
    animation: fx-spin calc(var(--fx-dur-slower) * 2) linear infinite;
}

/* The label under a loader. */
.fx-brand-loader-label {
    color: var(--fx-color-text-muted);
    font-size: var(--fx-text-sm);
    text-align: center;
}

/* ── The ring's DETERMINATE mode — one shape, two modes ──
 * Where real progress exists the ring SWEEPS rather than spinning, so a long
 * job does not need a second widget bolted underneath the mark. The owner's
 * ruling is that the mark and the ring are the whole loading state; adding a
 * bar inside it would put the window back in a different shape.
 *
 * A conic gradient masked to an annulus, driven by `--fx-ring-progress` (0..1).
 * The border is dropped in this mode because the ring IS the painted area now;
 * leaving it would draw a second, static circle outside the sweeping one.
 *
 * THE MASK USES `currentColor`, NOT A COLOUR LITERAL. A mask reads only the
 * ALPHA channel, so any opaque colour works — and `currentColor` is the one
 * that is opaque without writing a raw colour value into a component, which
 * this system forbids. `transparent` is the hole; `currentColor` is the ink.
 *
 * ROTATION IS PHYSICAL AND SYMMETRIC, so there is no RTL mirror. A progress
 * ring sweeps clockwise in both scripts — the same argument section 6 records
 * for the spinner's rotation, and the reason neither needs a direction token. */
.fx-brand-loader-ring[data-determinate="true"] {
    border: 0;
    animation: none;
    background-image: conic-gradient(
        var(--fx-color-text-subtle) calc(var(--fx-ring-progress, 0) * 360deg),
        var(--fx-color-border-emphasis) 0
    );
    -webkit-mask-image: radial-gradient(
        closest-side,
        transparent calc(100% - var(--fx-border-width-strong) * 2),
        currentColor calc(100% - var(--fx-border-width-strong) * 2)
    );
    mask-image: radial-gradient(
        closest-side,
        transparent calc(100% - var(--fx-border-width-strong) * 2),
        currentColor calc(100% - var(--fx-border-width-strong) * 2)
    );
}


/* ── THE LOADING POPUP — THE ONE COMPONENT, AND THE MAXIMUM FOOTPRINT ──
 *
 * ══ A LOADING STATE IS NEVER A PAGE ════════════════════════════════════════
 *
 * Owner ruling, verbatim: "loading must not be a page a popup window enough."
 *
 * So this is the CEILING on how much of the screen any wait may occupy: a
 * compact centred card over a scrim the page is still readable through. There
 * is no full-bleed surface in this system, no splash, no interstitial between
 * routes, and no state that replaces the page the user was looking at.
 *
 * A FULL-SCREEN BOOT SPLASH WAS BUILT HERE AND HAS BEEN REMOVED, not resized.
 * That distinction is the reason this note exists: a splash scaled down keeps
 * its splash assumptions — it still owns the viewport's stacking context, it
 * still expects the app to fade in behind it, and it still reads as a phase the
 * product passes through rather than as a panel the product shows. The boot
 * reveal now draws THIS component, the same one every later wait draws.
 *
 * THAT SAMENESS IS THE SIGNATURE. Boot, a long navigation and a report all
 * paint the identical card in the identical place. Four unrelated animations
 * would be four things a user has to learn; one card shown in four situations
 * is a thing they recognise. It is also why the component carries no per-
 * surface modifier: if a caller needs it to look different, the answer is
 * almost always that the caller should not be using it.
 *
 * IT IS NOT A SPECIAL CASE IN THE DESIGN SYSTEM. The card is on the float rung
 * (level 3) with the system's float shadow and the control radius, exactly like
 * any other floating panel in 07-surfaces. It is a card that happens to be
 * busy. Nothing here overrides the surface ladder.
 * ────────────────────────────────────────────────────────────────────────── */

/* ══ THERE IS NO CARD. SUPERSEDES THE "FLOAT RUNG" NOTE ABOVE. ═════════════
 *
 * Owner ruling, 2026-08-14, verbatim: "logo with circular loading are enough
 * without the white window background."
 *
 * So the loading state is exactly TWO THINGS — the mark, and a ring around it —
 * sitting directly on the scrim. No panel, no fill, no border, no shadow, no
 * radius, no padding box. The earlier instruction to put this on the float rung
 * as "a card that happens to be busy" is WITHDRAWN; it is a mark on a scrim.
 *
 * WHAT THE CARD WAS SILENTLY DOING, and what had to be rebuilt without it: it
 * gave the mark a KNOWN BACKDROP. Every contrast number for the ring used to be
 * measured against level 3 and was therefore true on every page in the product.
 * With the card gone the ring sits over the scrim over ARBITRARY CONTENT, so
 * the scrim is now the only thing guaranteeing legibility — which is why
 * `--fx-color-scrim-loading` exists as its own token, solved from the worst
 * backdrop rather than tuned on the dashboard. Read its note in 00-tokens
 * before changing either value.
 *
 * THE MARK ITSELF NEEDS NO GUARANTEE, and that is the one piece of luck here:
 * icon-192x192.png is OPAQUE and carries its own ground, so it is legible over
 * any backdrop by construction. Had the product's mark been the transparent
 * logo.png, removing the card would have made it illegible on half the screens
 * in the application and this ruling could not have been implemented as asked.
 * ────────────────────────────────────────────────────────────────────────── */

/* ── The scrim ──
 * DELIBERATELY THE LIGHTEST VEIL IN THE SYSTEM, and the arithmetic is the point
 * of the owner's ruling rather than a taste call.
 *
 * The system has two existing veils and BOTH ARE TOO HEAVY HERE, because both
 * were tuned for the opposite intent — obscuring what is behind:
 *   --fx-color-scrim    0.65 white / 0.85 black   the dialog veil
 *   --fx-color-overlay  0.55 white / 0.70 black   the region-block veil
 * A loading popup is not allowed to take the page over, so the user has to be
 * able to READ the page under it — that is the entire reason this is a popup
 * and not a full surface.
 *
 * IT IS A DEDICATED TOKEN AND NOT A MIX OF AN EXISTING VEIL. Two earlier
 * versions of this rule derived the scrim with `color-mix` — once from
 * `--fx-color-level-0`, which resolved to almost exactly the page colour and
 * dimmed nothing at all, and once from `--fx-color-overlay` at reduced alpha,
 * which measured 1.04:1 against a light page. Both were fine while a CARD sat
 * on top carrying the mark. Neither survives the card's removal, because the
 * scrim is now the mark's only backdrop. See `--fx-color-scrim-loading`. */
.fx-load-popup {
    position: fixed;
    inset: 0;
    z-index: var(--fx-z-modal);
    display: none;
    align-items: center;
    justify-content: center;
    padding: var(--fx-space-4);
    background-color: var(--fx-color-scrim-loading);
}
.fx-load-popup[data-active="true"] {
    display: flex;
}

/* The stack. NOT A CARD — no fill, no border, no shadow, no radius, no padding
 * box. It exists only to centre the mark over the label and to hold the two on
 * one axis. If a future edit adds a `background-color` here, that edit is
 * reintroducing the window the owner removed. */
.fx-load-popup-stack {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--fx-space-4);
    /* Bounds the LABEL's line length only; nothing is painted at this width. */
    max-inline-size: min(90vw, 20rem);
    text-align: center;
}

/* The label. Body ink rather than the muted tier: it is sitting on a scrim over
 * unknown content, not on a known panel, so it takes the strongest text colour
 * the theme has. Measured over the scrim it reads 16.34:1 in light and 12.89:1
 * in dark against the worst backdrop of each.
 *
 * IT IS ABSENT ON MOST SURFACES. Only the long-operation caller passes a label;
 * boot and navigation draw the mark alone, per the ruling. `:empty` collapses
 * the row so the gap above does not become dead space. */
.fx-load-popup-label {
    color: var(--fx-color-text);
    font-size: var(--fx-text-sm);
}
.fx-load-popup-label:empty {
    display: none;
}

@media (prefers-reduced-motion: no-preference) {
    .fx-load-popup[data-active="true"] {
        animation: fx-fade-in var(--fx-dur-enter) var(--fx-ease-standard);
    }
}


/* ── 8) SweetAlert2 skin ────────────────────────────────────────────────────
 * The vendored build at wwwroot/libs/sweetalert2/ exposes a complete
 * `swal2-*` custom property API on `:root` and writes its rules with `:where()`
 * so their specificity is effectively zero. Both facts mean this whole skin is
 * achievable with NO `!important` anywhere.
 *
 * IT IS THE SAME OBJECT AS `.modal` IN 07-surfaces.css, and every number below
 * matches that anatomy: level 3 in light and level 1 in DARK, a 1px translucent
 * hairline, an 8px corner, `fx-shadow-float-lg`, a flat colour scrim with NO
 * blur, and the arrival with the one percent upward drift. The brand accent
 * strip that used to run along the block start edge is REMOVED, exactly as it
 * was on the modal and the hand built dialog. It stays removed under the
 * rewritten contract for the same reason it is removed there: a coloured line
 * on a dialog is not one of the ten sanctioned sites.
 *
 * THE CONFIRM BUTTON KEEPS ITS BRAND FILL. That is SITE 6 of the sanctioned ten
 * (site 1 under the superseded six site contract), the primary action, and
 * SweetAlert2 renders `.swal2-styled` outside the `.btn` family so the buttons
 * slice cannot reach it. It is the only brand fill in this section, and the
 * deny, cancel, timer bar and question icon are all neutral or status coloured
 * around it.
 *
 * ══ WHERE THE VARIABLES ARE DECLARED, AND WHY IT IS THREE SELECTORS ═══════
 *
 * The library declares its variables on `:root` (0,1,0), so `html:root` (0,1,1)
 * wins on `<html>` regardless of which stylesheet the bundler emits first.
 * THAT IS NOT SUFFICIENT ON ITS OWN, and the reason is a cascade mechanic worth
 * knowing: a custom property declared ON an element always beats one INHERITED
 * from an ancestor, whatever the two selectors' specificities are. SweetAlert2
 * writes `data-swal2-theme` onto the `.swal2-container` it creates — an element
 * BETWEEN `<html>` and the popup — and its own sheet carries
 * `[data-swal2-theme=dark]` and an `[data-swal2-theme=auto]` block keyed to
 * `prefers-color-scheme`. If either ever matched, it would declare
 * swal2-background on that container and nothing declared on `<html>` would
 * reach the popup at all.
 *
 * MEASURED IN THE RUNNING APP 2026-08-03: the attribute's value is `light`, the
 * library's own default, because ABP passes no `theme` option — and the vendored
 * sheet has NO `[data-swal2-theme=light]` block, so today nothing is declared on
 * the container and the html-level values do reach the popup. That is luck
 * rather than design: the day ABP, a page script or a library upgrade passes
 * `dark` or `auto`, every token below would be shadowed on the one element that
 * matters, and `auto` would additionally key the dialog to the OPERATING SYSTEM
 * setting rather than to `data-bs-theme`, so it could disagree with the app
 * theme while the app itself was correct.
 *
 * So each block below is declared on `html:root` AND on the two swal2 elements,
 * as one rule with three selectors — one copy of the values, three places they
 * land. `html .swal2-container` is (0,1,1) and `html[data-bs-theme="dark"]
 * .swal2-container` is (0,2,1), both of which outrank the library's (0,1,0)
 * attribute blocks on the very element those blocks target.
 * ------------------------------------------------------------------------- */
html:root,
html .swal2-container,
html .swal2-popup {
    --swal2-background: var(--fx-color-level-3, var(--fx-color-surface));
    --swal2-color: var(--fx-color-text-muted);
    --swal2-backdrop: var(--fx-color-scrim);
    /* A translucent hairline, matching `.modal`'s edge. */
    --swal2-border: var(--fx-border-width) solid var(--fx-color-border-subtle);
    --swal2-border-radius: var(--fx-radius-md);
    /* Fluid: never wider than the viewport at any resolution. */
    --swal2-width: min(28rem, calc(100vw - var(--fx-gutter) * 2));
    --swal2-padding: 0 0 var(--fx-surface-pad-md);
    --swal2-title-padding: var(--fx-surface-pad-md) var(--fx-surface-pad-md) 0;
    --swal2-html-container-padding: var(--fx-space-3) var(--fx-surface-pad-md) var(--fx-space-2);
    --swal2-actions-justify-content: flex-end;
    --swal2-actions-margin: var(--fx-surface-pad-md) var(--fx-surface-pad-md) 0;
    --swal2-actions-width: auto;

    /* SITE 6 — the primary action. The one brand fill in this section.
     * Buttons take `fx-radius-control`, the single token that decides whether
     * every button in the platform is a pill or a rounded rectangle. Never
     * hardcode a pill radius at a call site, and never route a FIELD through
     * this token. */
    --swal2-confirm-button-background-color: var(--fx-color-primary);
    --swal2-confirm-button-color: var(--fx-color-text-on-primary);
    --swal2-confirm-button-border-radius: var(--fx-radius-control);
    --swal2-deny-button-background-color: var(--fx-color-danger);
    --swal2-deny-button-color: var(--fx-color-text-on-primary);
    --swal2-deny-button-border-radius: var(--fx-radius-control);
    --swal2-cancel-button-background-color: var(--fx-color-level-2);
    --swal2-cancel-button-color: var(--fx-color-text-muted);
    --swal2-cancel-button-border-radius: var(--fx-radius-control);

    /* ══ THE VENDOR FIELD SLOTS, AND WHAT THEY ARE STILL FOR ═══════════════
     *
     * These seven were the whole of this file's field treatment. They are now
     * the FALLBACK HALF of it, and the split is worth stating because it is the
     * thing a future edit will get wrong.
     *
     * `.swal2-input`, `.swal2-textarea` and `.swal2-select` are a bare
     * `<input type="…">`, a bare `<textarea>` and a bare `<select>`, so every
     * one of them is already inside 05-forms section 1's selector list and
     * already carries the fx-ctl-* contract. Their shape is therefore taken
     * from that contract, in the one hardening rule further down this section —
     * not from the values below. A swal2 field and a form field are ONE
     * component at ONE size, which is the point of the contract.
     *
     * The slots survive because the library reads them on controls the contract
     * deliberately does NOT reach: `.swal2-file`, which is an
     * `<input type="file">` and belongs to 05-forms section 10.5's separate
     * local family, and the `.swal2-range` assembly. Without these seven those
     * two would fall back to the library's own hardcoded #d9d9d9 hairline and
     * 0.1875em corner. Every value below is read from the SAME 00-tokens name
     * the fx-ctl-* default resolves to, so the two halves cannot disagree.
     *
     * FIELDS ARE FILLED WELLS, NOT OUTLINED. `fx-shadow-well` is NOT
     * decoration. Its strongest band is the WCAG 2.2 SC 1.4.11 guarantee that
     * makes a borderless control identifiable at 3:1 (4.13:1 light, 4.34:1
     * dark, measured in 00-tokens section 6). The fill alone is ~1.05:1 against
     * the dialog. That token is FROZEN — lowering its alphas, removing it, or
     * substituting a weaker inset in any of the three shadow slots below makes
     * every field here non-conformant rather than merely different looking.
     * The FOCUS RING is the outline written further down, never a box-shadow,
     * so all three library shadow slots keep the well in every state. */
    --swal2-input-background: var(--fx-color-field-bg);
    --swal2-input-border: 0 solid transparent;
    --swal2-input-border-radius: var(--fx-radius-md);
    --swal2-input-focus-border: 0 solid transparent;
    --swal2-input-box-shadow: var(--fx-shadow-well);
    --swal2-input-hover-box-shadow: var(--fx-shadow-well);
    --swal2-input-focus-box-shadow: var(--fx-shadow-well);
    /* THE ONE DURATION THE LIBRARY STILL OWNED. Left unmapped, the vendored
     * default is `border-color 0.2s, box-shadow 0.2s`, and 200ms is the floor of
     * the 200 to 400ms band this system does not author in. It also reached a
     * real control: 05-forms' own transition is (0,1,1) and beats the library on
     * an `<input>`, but only (0,0,1) on a bare `<textarea>`, which the library
     * out-specifies at (0,0,2) — so a swal2 textarea was the one field on the
     * platform still easing at the vendor's speed. Named properties, never
     * `all`, and the same pair 05-forms transitions. */
    --swal2-input-transition:
        background-color var(--fx-dur-fast) var(--fx-ease-standard),
        box-shadow var(--fx-dur-fast) var(--fx-ease-standard);
    /* The library paints this as a box-shadow on focus. Focus is an OUTLINE in
     * this system, so the slot is emptied and the real ring is declared below. */
    --swal2-outline: none;

    --swal2-footer-border-color: transparent;
    --swal2-footer-color: var(--fx-color-text-subtle);
    --swal2-validation-message-background: transparent;
    --swal2-validation-message-color: var(--fx-color-danger-fg);
    /* Neutral, for the same reason the progress bar is. */
    --swal2-timer-progress-bar-background: var(--fx-color-text-subtle);
    --swal2-close-button-color: var(--fx-color-text-subtle);
    --swal2-close-button-font-size: var(--fx-text-2xl);
    /* The swal2 toast is a floating card, exactly like `.toast`. */
    --swal2-toast-border: var(--fx-border-width) solid var(--fx-color-border);
    --swal2-toast-box-shadow: var(--fx-shadow-float-md, var(--fx-shadow-lg));
}
html[data-bs-theme="dark"]:root,
html[data-bs-theme="dark"] .swal2-container,
html[data-bs-theme="dark"] .swal2-popup {
    /* DARK KEEPS THE DIALOG LOW ON THE LADDER, matching `.modal` in 07: the
     * ladder steps are only ~1.05:1 in dark, so pushing a dialog to the top of
     * it reads as a grey slab and the rising dark shadow alpha does the
     * separating instead. */
    --swal2-background: var(--fx-color-level-1, var(--fx-color-surface));
    --swal2-color: var(--fx-color-text-muted);
    /* The field well reverses direction in dark — a control is LIT above the
     * page rather than recessed into it — and `fx-color-field-bg` already
     * carries that flip, so nothing needs restating here beyond the plane the
     * cancel button sits on. */
    --swal2-cancel-button-background-color: var(--fx-color-level-2, var(--fx-color-surface-raised));
    --swal2-cancel-button-color: var(--fx-color-text-muted);
}

/* ══ THE LEPTONX OVERRIDE, AND FOUR OF THIS FILE'S FIVE `!important` FLAGS
 * (the fifth is the toast opacity in section 2, which answers a different rule
 * in the same LeptonX bundle on exactly the same reasoning)
 *
 * THE BUG THIS FIXES, reported by the client from the running app: the confirm
 * dialog rendered WHITE while the application was in dark mode.
 *
 * IT IS NOT A TOKEN PROBLEM AND IT IS NOT THE `data-swal2-theme` MECHANIC
 * described above — both were checked in the running app first and both were
 * innocent. Measured on the live popup with the app in dark:
 *     swal2-background  resolved to #17251b   (our dark value, correct)
 *     swal2-color       resolved to #c8d6ca   (our dark value, correct)
 *     computed background  rgb(255, 255, 255)   (white)
 * The variables arrive; nothing consults them. LeptonXLite's own
 * `js-bundle.rtl.css` — which loads before every file in this design system —
 * ships this, and it is the whole of the defect:
 *
 *     .swal2-popup { border-radius: .5rem !important;
 *                    background: #ffffff !important;
 *                    color: #686b6e !important; }
 *     .swal2-popup .swal2-styled { border-radius: .5rem !important; }
 *
 * Three hardcoded values, flagged important, with no dark counterpart of any
 * kind. An important author declaration can only be answered by another
 * important author declaration, so this is one of the two places in this file
 * where the no-important rule in the header genuinely cannot be honoured — the
 * other being the toast opacity in section 2, which answers a rule of the same
 * shape in the same bundle. The header records both. NOTHING ELSE IN THIS FILE
 * MAY CARRY THE FLAG, and anything added anywhere must be a rule that cancels a
 * vendor `!important` and must name the vendor rule it answers, as these do.
 *
 * THEY READ THE VARIABLE, NEVER A COLOUR. Each declaration re-asserts the swal2
 * slot the theme blocks above already set, so the light and dark answers stay in
 * exactly one place and this block only wins the fight. Writing a token here
 * instead would put a fourth copy of the dialog surface in the file.
 *
 * SPECIFICITY: LeptonX's popup rule is `.swal2-popup` (0,1,0) and its button
 * rule is `.swal2-popup .swal2-styled` (0,2,0). The two selectors below are
 * (0,2,0) and (0,3,0), so both win on specificity and neither depends on which
 * order the bundler emits the two files in.
 *
 * THE BUTTON RADIUS IS PART OF THE SAME DEFECT. `.swal2-styled` covers the
 * confirm, deny and cancel buttons at once, so LeptonX's flat .5rem overrode all
 * three of the per-button radius slots mapped above. fx-radius-control is the
 * single token that decides whether every button in the platform is a pill or a
 * rounded rectangle, and a confirmation's buttons are not exempt from it. */
.swal2-container .swal2-popup {
    background-color: var(--swal2-background) !important;
    color: var(--swal2-color) !important;
    border-radius: var(--swal2-border-radius) !important;
}
.swal2-container .swal2-popup .swal2-styled {
    border-radius: var(--fx-radius-control) !important;
}

/* The library rule is `div:where(.swal2-container) div:where(.swal2-popup)`,
 * specificity (0,0,2). A two class selector outranks it comfortably. */
.swal2-container .swal2-popup {
    box-shadow: var(--fx-shadow-float-lg, var(--fx-shadow-2xl));
    font-family: var(--fx-font-body);
    font-size: var(--fx-text-sm);
    letter-spacing: var(--fx-tracking-ui);
    /* Fluid height, matching `.modal-content` in 07-surfaces.css. */
    max-block-size: calc(100vh - (var(--fx-space-6) * 2));
    max-block-size: calc(100svh - (var(--fx-space-6) * 2));
    overflow-y: auto;
}

/* THE ARRIVAL, matching `.modal-dialog`.
 *
 * SweetAlert2 ships its own `swal2-show` keyframe with a hardcoded 0.3s bounce
 * that neither reads like our modal nor answers the motion damper — and 300ms
 * sits squarely in the 200 to 400ms band the retune bans. Replacing
 * `animation` on a two class selector overrides it without `!important`,
 * because the library writes the rule as `:where(.swal2-show)`.
 *
 * The local keyframe combines the scale with the ONE PERCENT UPWARD DRIFT that
 * makes a dialog arrive rather than inflate, which no shared keyframe expresses
 * — two animations on the same element both writing `transform` do not compose,
 * the last one declared simply wins. Unlike the dropdown in 07, the popup is
 * NOT positioned by Popper: swal2 lays it out with flexbox on the container, so
 * a keyframe touching `transform` is safe here.
 *
 * EXIT: the same keyframe reversed on the exit curve and the shorter exit
 * duration, so the dialog leaves the way it arrived, faster than it came, with
 * no second set of frames to keep in sync. */
@media (prefers-reduced-motion: no-preference) {
    /* `fx-dur-slow` (180ms) — the dialog arrival timing, the same one
     * `.modal` and `.frihat-dialog` read in 07, so all three dialog families
     * land as one object. The token used to be 250ms and unusable; it is the
     * top of the under the cursor band now and it MEANS a self contained
     * entrance the user is not waiting on, which is exactly this. */
    /* `backwards` on the ENTRANCE, `both` on the EXIT, and the asymmetry is the
     * fill mode rule in 01-base section 14 rather than an oversight. The
     * entrance ends on `transform: none` and opacity 1, which is the popup's own
     * resting style — filling that forward pinned a transform on a panel hosting
     * swal2's absolutely positioned close control and its validation message,
     * for as long as the confirmation stayed open, in exchange for nothing. The
     * exit ends on opacity 0, which is NOT a resting style and MUST hold, or the
     * dialog would flash back to full opacity for the frames between the end of
     * the animation and the library removing the node. */
    .swal2-container .swal2-popup.swal2-show {
        animation: fx-swal-in var(--fx-dur-slow) var(--fx-ease-standard) backwards;
    }
    .swal2-container .swal2-popup.swal2-hide {
        animation: fx-swal-in var(--fx-dur-exit) var(--fx-ease-in-quad) reverse both;
    }
}
@keyframes fx-swal-in {
    from {
        opacity: 0;
        transform: scale(var(--fx-enter-scale)) translateY(1%);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

.swal2-container .swal2-title {
    font-family: var(--fx-font-heading);
    /* Matches `.modal-title`, so a confirmation and a dialog are one object. */
    font-size: var(--fx-text-lg);
    font-weight: var(--fx-weight-semibold, 600);
    line-height: var(--fx-leading-snug, 1.35);
    letter-spacing: var(--fx-tracking-title);
    color: var(--fx-color-text);
    /* Confirmation copy follows the reading direction, not a hardcoded centre,
     * so long Arabic sentences do not read as ragged on both edges. */
    text-align: start;
}
.swal2-container .swal2-html-container {
    font-size: var(--fx-text-sm);
    line-height: var(--fx-leading-normal, 1.7);
    color: var(--fx-color-text-muted);
    text-align: start;
    overflow-wrap: anywhere;
}
/* THE ACTION BAND, matching `.modal-footer` and `.frihat-dialog-actions`: a
 * 56px minimum separated by a TRANSLUCENT hairline rather than by space alone.
 * The band height is `fx-dialog-band-h`, the token 07 also reads, so all
 * three dialog families share ONE number instead of restating the derivation —
 * and all three grow to 64px together when the control ladder steps up for
 * touch, which a typed 56px would not do. */
.swal2-container .swal2-actions {
    gap: var(--fx-space-2);
    align-items: center;
    min-block-size: var(--fx-dialog-band-h);
    margin-block-start: var(--fx-surface-pad-md);
    padding-block: var(--fx-space-2);
    border-block-start: var(--fx-border-width) solid var(--fx-color-border);
}
/* Button skins are the buttons slice's concern in general, but SweetAlert2
 * renders its own `.swal2-styled` elements outside the `.btn` family, so their
 * geometry has to be set here to match. Colour comes from the variables mapped
 * above. */
.swal2-container .swal2-styled {
    min-block-size: var(--fx-control-h-md);
    padding-block: var(--fx-space-2);
    padding-inline: var(--fx-control-pad-x-md);
    font-family: inherit;
    font-size: var(--fx-text-sm);
    /* 500, the workhorse UI weight. A button label is chrome, not a title. */
    font-weight: var(--fx-weight-medium, 500);
    letter-spacing: var(--fx-tracking-ui);
}
.swal2-container .swal2-confirm {
    box-shadow: none;
}
/* A secondary action is a TONAL FILL at level 2 plus a hairline, which is how
 * every other secondary button in the platform is drawn. */
.swal2-container .swal2-cancel {
    border: var(--fx-border-width) solid var(--fx-color-border-subtle);
}
/* Focus is the OUTLINE, exactly as on `.card`, the dialog fields and every
 * other focusable thing in the document. One indicator, one thing to verify,
 * and it survives forced colours where every box-shadow is dropped. */
.swal2-container .swal2-styled:focus-visible,
.swal2-container .swal2-input:focus-visible,
.swal2-container .swal2-textarea:focus-visible,
.swal2-container .swal2-select:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: var(--fx-focus-offset);
}
/* THE FOCUS FILL IS NOT RESTATED HERE ANY MORE. It used to be
 * `background: var(fx-color-field-bg-focus)` on a `:focus` rule, which was a
 * second copy of a decision 05-forms section 2 already makes by moving the
 * fx-ctl-bg LOCAL to the focus fill. The hardening rule below paints
 * `background-color: var(fx-ctl-bg)`, so by the time it resolves the local is
 * already the focus value and the state arrives with nothing named twice —
 * hover included, which this file never had a rule for at all. */
/* Close button: `justify-self: end` in the library is already grid direction
 * aware, so only the target size and the tier need setting. */
.swal2-container .swal2-close {
    min-inline-size: var(--fx-control-h-sm);
    min-block-size: var(--fx-control-h-sm);
    color: var(--fx-color-text-subtle);
}
@media (any-hover: hover) {
    .swal2-container .swal2-close:hover {
        color: var(--fx-color-text);
    }
}
/* The footer is the quietest text in the dialog and sits after a full space
 * step, which is separation enough without drawing anything. */
.swal2-container .swal2-footer {
    margin-block-start: var(--fx-space-3);
    border-block-start: 0;
    font-size: var(--fx-text-xs);
}

/* Icons. The library hardcodes five pastel colours in rules of specificity
 * (0,1,1) at most, so a two class selector wins without `!important`.
 * The QUESTION icon is neutral: a question is not a status, and a GENERIC ICON
 * is on the FORBIDDEN list — it takes a text tier, one below the label beside
 * it. Re-checked against the rewritten contract and it stays grey; the only
 * brand in this section is the confirm button, site 6. */
.swal2-icon.swal2-success {
    border-color: var(--fx-color-success-border);
    color: var(--fx-color-success-fg);
}
.swal2-icon.swal2-success [class^="swal2-success-line"] {
    background-color: var(--fx-color-success-fg);
}
.swal2-icon.swal2-success .swal2-success-ring {
    border-color: var(--fx-color-success-border);
}
.swal2-icon.swal2-error {
    border-color: var(--fx-color-danger-border);
    color: var(--fx-color-danger-fg);
}
.swal2-icon.swal2-error [class^="swal2-x-mark-line"] {
    background-color: var(--fx-color-danger-fg);
}
.swal2-icon.swal2-warning {
    border-color: var(--fx-color-warning-border);
    color: var(--fx-color-warning-fg);
}
.swal2-icon.swal2-info {
    border-color: var(--fx-color-info-border);
    color: var(--fx-color-info-fg);
}
.swal2-icon.swal2-question {
    border-color: var(--fx-color-border-emphasis);
    color: var(--fx-color-text-subtle);
}
.swal2-container .swal2-icon {
    margin-block: var(--fx-surface-pad-md) var(--fx-space-2);
}

/* ══ THE FIELD HARDENING RULE — GEOMETRY FROM THE CONTRACT, NEVER FROM HERE ══
 *
 * WHAT WAS HERE was `color: var(fx-color-text)` and
 * `font-size: var(fx-text-sm)`. The colour was a second copy of fx-ctl-fg, and
 * the type size was worse than a copy: it pinned a swal2 field to the 13px sm
 * step while the same control on a Razor page or in a `.frihat-dialog` sits at
 * the document size. One dialog family disagreeing with the other two about how
 * big the text in a field is, which is the client's "from another planet"
 * complaint in miniature.
 *
 * WHY A RULE EXISTS AT ALL, when 07's `.frihat-dialog` needed none. A swal2
 * field is a bare `<input type="…">`, `<textarea>` or `<select>`, so it already
 * carries the fx-ctl-* contract from 05-forms section 1 — but the library
 * out-specifies parts of that contract on two of the three, and a HALF migrated
 * field is a worse result than one that lost outright:
 *
 *   `div:where(.swal2-container) .swal2-input`     (0,1,1)  height, padding
 *   `div:where(.swal2-container) .swal2-textarea`  (0,1,1)  height, padding
 *   `div:where(.swal2-container) .swal2-select`    (0,1,1)  padding, background,
 *                                                           colour, font-size
 *   `div:where(.swal2-container) input:where(…)`   (0,0,2)  border, radius,
 *                                                           background, shadow,
 *                                                           font-size
 * 05-forms reaches an `<input type="text">` at (0,1,1) and wins, but a bare
 * `<textarea>` and a bare `<select>` only at (0,0,1) and loses — so a swal2
 * select was taking our well and our radius while keeping the library's fill,
 * padding and type.
 *
 * THIS IS THE SAME SHAPE AS 05-forms SECTION Z, and it is the sanctioned way to
 * answer a specificity problem: every declaration RESTATES A PROPERTY WHILE
 * READING A LOCAL, and not one of them restates a VALUE. No size step is
 * declared, so these fields sit on the default step exactly as the
 * `.frihat-dialog` family and a Razor page form do. To resize a swal2 field,
 * add fx-ctl-* locals to this rule and change nothing else.
 *
 * `block-size: auto` IS LOAD BEARING: the library pins 2.625em on an input and
 * 6.75em on a textarea, and a fixed height would defeat both the contract's
 * min-block-size and the `field-sizing: content` growth 05-forms section 8
 * turns on for every textarea on the platform.
 *
 * THE WELL ARRIVES THROUGH fx-ctl-shadow AND MUST NOT BE WRITTEN OUT. Its
 * strongest band is the SC 1.4.11 identification cue for every borderless
 * control here; on a swal2 popup it measures 4.09:1 against the field fill and
 * 4.33:1 against the light popup surface, 4.29:1 and 4.56:1 in dark. A
 * box-shadow literal in this rule would flatten it. */
.swal2-container :is(.swal2-input, .swal2-textarea, .swal2-select) {
    min-block-size: var(--fx-ctl-h);
    block-size: auto;
    padding-block: var(--fx-ctl-pad-y);
    padding-inline: var(--fx-ctl-pad-x);
    font-family: inherit;
    font-size: var(--fx-ctl-font-size);
    line-height: var(--fx-ctl-leading);
    color: var(--fx-ctl-fg);
    caret-color: var(--fx-ctl-caret);
    background-color: var(--fx-ctl-bg);
    background-clip: padding-box;
    border: var(--fx-ctl-border);
    border-radius: var(--fx-ctl-radius);
    box-shadow: var(--fx-ctl-shadow);
}
/* THE HEIGHT THE RULE ABOVE WOULD OTHERWISE STEAL, restated immediately after
 * it for the same reason 05-forms section Z restates the identical pair: a
 * textarea's two-row floor lives at (0,0,1) in section 8 and the (0,2,0) rule
 * above would collapse it to a single line. Both halves read locals, so section
 * 8 keeps owning the numbers and this only wins the fight — including the prose
 * leading, which section 8 supplies by redeclaring fx-ctl-leading. */
.swal2-container .swal2-textarea {
    min-block-size: calc(var(--fx-ctl-h) * 2);
}
.swal2-container .swal2-timer-progress-bar {
    background-color: var(--fx-color-text-subtle);
}

/* THE SCRIM, matching `.modal-backdrop` in 07-surfaces.css: one flat colour and
 * no blur. SweetAlert2 has no two state pair to transition between —
 * `.swal2-backdrop-show` is present the moment the container is inserted — so
 * the fade arrives as a keyframe, exactly as it does for
 * `.frihat-dialog-backdrop`. It consumes the SHARED `fx-fade-in` from 01-base;
 * the local `backdrop-filter` keyframe that used to live here went with the
 * glass treatment. */
@media (prefers-reduced-motion: no-preference) {
    .swal2-container.swal2-backdrop-show {
        animation: fx-fade-in var(--fx-dur-base) var(--fx-ease-standard) backwards;
    }
}
/* THE CHOREOGRAPHY, STATED ONCE FOR ALL THREE DIALOG FAMILIES.
 * The scrim above runs `fx-dur-base` (160ms) and the popup runs `fx-dur-slow`
 * (180ms) from the same instant, so the page dims FIRST and the confirmation
 * lands into an already darkened page one rung later. That ordering is the whole
 * reason it reads as one gesture rather than as two things appearing: a scrim
 * that arrived with or after its panel would have the window land on a page that
 * was still bright underneath it, and the eye reads that as two events.
 * `.modal` in 07 is 160 then 180, and `.frihat-dialog` in 07 is 160 then 180.
 * All three are the same gesture, which is why a Bootstrap modal, a hand built
 * module dialog and a SweetAlert2 confirmation cannot be told apart in motion.
 * If any one of these three numbers is ever changed, change all six. */

/* Sub 768px: the confirmation matches the modal bottom sheet tier so the two
 * never disagree about what a dialog looks like on a phone. */
@media (max-width: 767.98px) {
    html:root {
        --swal2-width: 100vw;
        --swal2-padding: 0 0 var(--fx-space-4);
        --swal2-title-padding: var(--fx-space-4) var(--fx-space-4) 0;
        --swal2-html-container-padding: var(--fx-space-3) var(--fx-space-4) var(--fx-space-2);
        --swal2-actions-margin: var(--fx-space-4) var(--fx-space-4) 0;
    }
    .swal2-container .swal2-popup:not(.swal2-toast) {
        max-block-size: 92svh;
        border-end-start-radius: 0;
        border-end-end-radius: 0;
        padding-block-end: calc(var(--fx-space-4) + env(safe-area-inset-bottom, 0px));
    }
    /* Actions stretch to full width, matching the modal footer treatment. */
    .swal2-container .swal2-actions {
        flex-wrap: wrap;
    }
    .swal2-container .swal2-styled {
        flex: 1 1 100%;
        margin: 0;
    }
}


/* ── 9) ABP message and notify surfaces + the inline error class ───────────
 * ABP renders confirmations and messages through SweetAlert2 wrappers, which
 * section 8 already covers, and adds its own container classes on top. Those
 * are pinned to the same tokens here so nothing arrives unskinned.
 *
 * `.fx-inline-error` is the project rule made concrete: an error surfaces in
 * the UI as a readable inline block, never as a raw exception or a console
 * trace. Give it `role="alert"` so it is announced the moment it appears, and
 * never put a stack trace, a SQL fragment or any professional secret inside it.
 *
 * IT IS NO LONGER A TINTED BOX. It used to be a red wash with a red rail, which
 * is a lot of area for one sentence under a field. It is now a red GLYPH and
 * red TEXT and nothing else — the same treatment the alert got, taken one step
 * further because an inline field error sits inside a form, where a second
 * boxed surface competes with the control it belongs to.
 * ------------------------------------------------------------------------- */
.abp-message-content,
.abp-message-detail {
    color: var(--fx-color-text-muted);
    font-family: var(--fx-font-body);
    font-size: var(--fx-text-sm);
    overflow-wrap: anywhere;
}
.abp-message-detail {
    color: var(--fx-color-text-subtle);
    font-size: var(--fx-text-xs);
}
.abp-error-title {
    font-family: var(--fx-font-heading);
    font-size: var(--fx-text-lg);
    font-weight: var(--fx-weight-semibold, 600);
    letter-spacing: var(--fx-tracking-title);
    color: var(--fx-color-danger-fg);
}
/* A floating notification card, so it is treated exactly like `.toast` — level
 * 3, a translucent hairline, a 6px corner and the ambient float. */
.abp-notification {
    background-color: var(--fx-color-level-3, var(--fx-color-surface-raised));
    border: var(--fx-border-width) solid var(--fx-color-border);
    border-radius: var(--fx-radius-sm);
    box-shadow: var(--fx-shadow-float-md, var(--fx-shadow-lg));
    color: var(--fx-color-text-muted);
    font-size: var(--fx-text-sm);
}

.fx-inline-error {
    display: flex;
    align-items: flex-start;
    gap: var(--fx-space-2);
    max-inline-size: 100%;
    min-inline-size: 0;
    margin-block: var(--fx-space-2);
    background-color: transparent;
    border: 0;
    color: var(--fx-color-danger-fg);
    font-size: var(--fx-text-xs);
    line-height: var(--fx-leading-normal, 1.7);
    letter-spacing: var(--fx-tracking-ui);
    overflow-wrap: anywhere;
}
/* Shape plus glyph, so the error is not signalled by red alone. */
.fx-inline-error::before {
    content: "\00D7";
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--fx-icon-sm);
    block-size: var(--fx-icon-sm);
    margin-block-start: 1px;
    /* Draws the marker badge, same as the alert glyph — a shape, not a stroke.
     * The square corner is the non colour half of the signal, so it carries a
     * literal fallback for the reason given on the status markers above. */
    border: var(--fx-border-width-strong) solid currentColor;
    border-radius: var(--fx-radius-xs, 4px);
    font-size: var(--fx-text-2xs);
    font-weight: var(--fx-weight-bold, 700);
    line-height: 1;
}
.fx-inline-error:empty {
    display: none;
}
@media (prefers-reduced-motion: no-preference) {
    /* Errors arrive rather than blink into place, on the shared keyframe.
     * Opacity only, and that is the correct choice here for the reason 01-base
     * gives fx-fade-in: this message appears directly under a field the user is
     * looking at, and a rise or a scale would move the control's neighbourhood
     * at the moment they need to read it. `backwards` per the fill mode rule;
     * the class is toggled by jQuery show/hide in the Courts script, and a
     * forward filled opacity would have fought the hide. */
    .fx-inline-error {
        animation: fx-fade-in var(--fx-dur-base) var(--fx-ease-standard) backwards;
    }
}


/* ── 10) Coarse pointer targets (WCAG 2.2 SC 2.5.8) ────────────────────────
 * Every control in this slice grows past 40px on touch and pen. On a mouse the
 * declared minimums are 24px (chip remove) and 28px (everything else), both
 * clearing the 24x24 floor.
 * ------------------------------------------------------------------------- */
@media (pointer: coarse) {
    .alert-dismissible .btn-close,
    .toast-header .btn-close,
    #toast-container .toast-close-button,
    .swal2-container .swal2-close {
        min-inline-size: var(--fx-tap-min);
        min-block-size: var(--fx-tap-min);
    }
    /* ABP'S TOAST DISMISS, ADDED 2026-08-03 WITH SECTION 2.1. It is a sized
     * control rather than a min-sized one, so BOTH axes are restated rather than
     * floored — a `min-inline-size` alone would have lost to the explicit
     * `inline-size` beside it and the control would have stayed 28px on a touch
     * screen while claiming to be 44. */
    #toast-container .abp-toast-close-button {
        inline-size: var(--fx-tap-min);
        block-size: var(--fx-tap-min);
    }
    /* AND THE RESERVE GROWS WITH IT, for the reason section 2.1 gives: the
     * control is absolutely positioned over the message, so the text clearance
     * is not maintained by layout and has to be restated wherever the control's
     * width changes. Without this line the 44px target would sit on top of the
     * sentence it is dismissing. */
    #toast-container .abp-toast-content {
        padding-inline-end: var(--fx-tap-min);
    }
    .alert-dismissible {
        padding-inline-end: calc(var(--fx-space-4) + var(--fx-tap-min));
    }
    .fx-chip-remove {
        inline-size: calc(var(--fx-tap-min) - var(--fx-space-1));
        block-size: calc(var(--fx-tap-min) - var(--fx-space-1));
    }
    /* AND THE CHIP HAS TO GROW WITH IT. The root clips its own overflow, so a
     * 40px remove control inside a 28px pill would have its top and bottom
     * edges cut off and the ACTUAL target would drop back under the SC 2.5.8
     * floor — the growth would be cosmetic and the conformance imaginary. Only
     * chips that host the control are lifted; a plain status pill is not
     * operable and stays on the badge ladder. */
    .badge:has(.fx-chip-remove),
    .fx-chip:has(.fx-chip-remove),
    .fx-status:has(.fx-chip-remove) {
        --fx-badge-h: calc(var(--fx-tap-min) - var(--fx-space-1));
    }
    .swal2-container .swal2-styled {
        min-block-size: var(--fx-tap-min);
    }
}


/* ── 11) Reduced motion, increased contrast and forced colours ─────────────
 * Every entrance above is already gated behind
 * `@media (prefers-reduced-motion: no-preference)`, which is the correct place
 * for one: a damped animation still paints its fill-mode start frame for a
 * tick, which is a visible flash. This block handles what gating cannot — the
 * looping indicators, which must keep RUNNING but slower, and the one dwell
 * time that must not be damped at all.
 *
 * The forced-colors branch closing this section answers a different question
 * from the contrast one beside it. `prefers-contrast: more` strengthens this
 * file's own palette; forced colours REPLACE it, and since every severity in
 * this file is expressed as a fill, replacing the palette erases the whole
 * signal rather than sharpening it. That branch is about keeping the SHAPE of a
 * message when its colour is no longer ours to choose.
 * ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    .fx-skeleton,
    [dir="rtl"] .fx-skeleton {
        background-image: none;
        background-color: var(--fx-color-level-2, var(--fx-color-bg-subtle));
        animation: none;
    }
    /* ABP'S OWN TOAST KEYFRAMES, CANCELLED. Added 2026-08-03, and this is the
     * single most consequential line in this block — it is the one place in the
     * platform where a user who asked for stillness was still being shown the
     * LARGEST movement in the application.
     *
     * MEASURED, NOT INFERRED: on a machine reporting this preference, an
     * `abp.notify` toast was travelling 364px in 300ms. `abp-toast.css` writes
     * its `toastIn` and `toastOut` durations as literals inside the `animation`
     * shorthand on `.abp-toast` at (0,1,0), and the universal damper in 01-base
     * section 15 is (0,0,0), so the safety net lost by one step and the vendor
     * kept both its period and its distance. Section 15 documents that
     * limitation and says the fix belongs to the file that owns the surface.
     * This file owns it, so here it is.
     *
     * CANCELLED RATHER THAN DAMPED. Reaching for `animation-duration` would
     * leave a 364px translate running in 0.01ms, which is a teleport rather than
     * stillness, and the from-frame would still paint for a tick before it. The
     * second selector is one step longer so it clears `.toast-removing`, which
     * declares its exit at the same (0,1,0) the base rule does. The card still
     * appears and still disappears — instantly, in place, which is exactly what
     * this preference asks a state change to do. */
    #toast-container > div,
    #toast-container > div.toast-removing {
        animation: none;
    }
    /* THE DWELL IS NOT MOTION, AND MUST NOT BE SHORTENED. The universal damper
     * in 01-base sets `animation-duration: 0.01ms` on every element; if that
     * ever reached the progress bar, the toast would report itself expired
     * before the sentence could be read, and a user who asked for less motion
     * would have been given less READING TIME instead. The full animation is
     * therefore restated here at its undamped dwell, which also makes the
     * guarantee explicit rather than dependent on cascade order. Only the
     * easing is pinned; the seconds are the same seconds everyone else gets. */
    .fx-toast-progress {
        animation: fx-toast-drain var(--fx-toast-duration, 5s) linear forwards;
    }
    .progress-bar-striped.progress-bar-animated {
        animation: none;
    }
    /* BOOTSTRAP'S PLACEHOLDERS ARE THE VENDOR'S SKELETON, AND THEY WERE THE ONLY
     * LOADING PLACEHOLDER ON THE PLATFORM THIS BRANCH DID NOT STOP. Added
     * 2026-08-03. `.fx-skeleton` two rules up is cancelled properly, but a page
     * that reaches for `.placeholder-glow` or `.placeholder-wave` instead got
     * neither treatment: both are `infinite` with a LITERAL 2s inside the
     * `animation` shorthand — the (0,1,0) and (0,2,0) shape that out-specifies
     * the universal damper in 01-base section 15 exactly as ABP's toast did.
     * Same specificity as the vendor rules, and this file loads after
     * `bootstrap.rtl.css`, so source order carries both without escalation.
     * STOPPED, not slowed: a placeholder is not a spinner. It stands in for
     * content rather than reporting that work is happening, so a still
     * placeholder is still legible as "not here yet" — which is why the skeleton
     * above is stopped too, and the spinners below are not. */
    .placeholder-glow .placeholder,
    .placeholder-wave {
        animation: none;
    }
    /* The sliver stops and the bar fills its track, so "busy" is still readable
     * as a state without anything travelling. `transform: none` is restated
     * BECAUSE the keyframe now moves a transform rather than a margin: killing
     * the animation alone would leave the element wherever the transform last
     * put it, which after the change is 285 percent off the end of its own
     * track, i.e. invisible. Under the old margin version there was nothing to
     * restate and this line would have been dead — it is the direct cost of the
     * change above and had to be paid in the same edit. */
    .progress.fx-progress-indeterminate .progress-bar,
    .fx-progress-indeterminate > .progress-bar {
        inline-size: 100%;
        transform: none;
        will-change: auto;
        animation: none;
        opacity: 0.6;
    }
    /* The exit pair added to the alert in section 1 is cancelled here for the
     * same reason section 12 of 07-surfaces cancels its own: the DURATION is a
     * token and is damped to 0.01ms anyway, but `fx-ease-in-quad` on a zero
     * length transition is a curve left lying in a branch that has no use for
     * one. A reduced motion state change is a swap. */
    .alert.fade,
    .progress-bar,
    .fx-chip-remove {
        transition: none;
    }
    /* A spinner is the one thing that may still move here, because stopping it
     * makes "still working" indistinguishable from "frozen" — the user would be
     * left staring at a static ring with no way to tell a slow query from a
     * hung one. It is slowed to one turn every two and a half seconds instead.
     *
     * WHY THIS IS NOT `fx-dur-*`. Every duration token is damped to 0.01ms in
     * this very media query, so deriving from one would spin the ring at
     * thousands of revolutions per second — the exact opposite of the
     * intention. `fx-toast-duration` is the only time token 00-tokens
     * deliberately leaves UNDAMPED, precisely because it measures human
     * perception rather than animation, which is the same quantity being
     * measured here. So the period is derived from it rather than typed, and
     * this file still contains no hardcoded duration. */
    .spinner-border,
    .spinner-grow,
    .fx-spinner {
        animation-duration: calc(var(--fx-toast-duration, 5s) / 2);
    }
    /* ABP'S BUSY RING JOINS THE THREE ABOVE. Added 2026-08-03. It is the loading
     * indicator this application shows most often — the framework raises it
     * around every AJAX call it makes — and it was the one spinner on screen
     * that this branch did not reach, so a reduced motion user got three of the
     * platform's four rings slowed to a readable turn and the fourth still
     * running at whatever `abp.css` hardcoded. The specificity here is (0,2,1),
     * matching the vendor's own `.abp-block-area-busy::before` rule exactly, so
     * source order in the bundle carries it — the same mechanism the two rules
     * further up this file already use to repaint that pseudo element's colours.
     * Slowed rather than stopped, for the reason given above it: a still ring
     * cannot be told from a hung request. */
    .abp-block-area.abp-block-area-busy::before {
        animation-duration: calc(var(--fx-toast-duration, 5s) / 2);
    }
    /* AND SWEETALERT2'S LOADER, THE LAST SPINNER IN THE SLICE. Added 2026-08-03.
     * Section 8 skins every other part of this library and had left its busy ring
     * spinning on the vendor's literal 1.5s, which the (0,0,0) damper cannot
     * reach. The library writes the rule as
     * `div:where(.swal2-container) div:where(.swal2-loader)`, and `:where()`
     * contributes NOTHING to specificity, so the whole thing weighs (0,0,2) and a
     * two class selector clears it without an importance flag — the same property
     * of this library's stylesheet that lets the entire skin in section 8 avoid
     * one. Slowed to match the four rings above rather than stopped, for the
     * reason stated there: a still ring cannot be told from a hung request, and a
     * confirmation dialog waiting on a server is exactly when that matters. */
    .swal2-container .swal2-loader {
        animation-duration: calc(var(--fx-toast-duration, 5s) / 2);
    }

    /* ══ THE LOADING SYSTEM (section 7.1) UNDER REDUCED MOTION ═════════════
     *
     * READ THE SPECIFICITY ARITHMETIC BEFORE EDITING EITHER RULE. A media query
     * contributes NOTHING to specificity, and this file has already been bitten
     * by that: section 11's header records nine rules that were silently inert
     * because a base rule outweighed them. The two rules below are written to
     * match their base rules EXACTLY, digit for digit, so that equal
     * specificity plus later source order carries them. Neither is a guess.
     *
     *   THE BUTTON RING — the one that would have been inert.
     *     base    (7.1)  `.btn.fx-btn-busy::after`  = (0,2,1)
     *                    `.fx-btn-busy::after`      = (0,1,1)
     *     here           the SAME two selector list = (0,2,1) and (0,1,1)
     *   Writing this branch as the short `.fx-btn-busy::after` alone would
     *   weigh (0,1,1), LOSE to the (0,2,1) base rule on every real Bootstrap
     *   button in the product, and leave a ring spinning at full speed for a
     *   user who asked it not to — while looking correct in the file. The
     *   redundant looking `.btn` prefix is the entire point of the rule.
     *
     *   THE BAR — `.fx-loadbar-fill` (0,1,0) in both places, and
     *   `.fx-loadbar[data-done="true"]` (0,2,0) in both places.
     *
     * THE RING IS SLOWED, NOT STOPPED, and the bar IS stopped. That split is
     * this file's existing doctrine and not a new judgement: a spinner reports
     * "still working", so stopping it makes a slow request indistinguishable
     * from a hung one (see the four rings above). A progress bar reports a
     * POSITION, which is legible standing still, so it simply stops moving and
     * still says how far in the work is.
     *
     * THE PERIOD IS DERIVED FROM `fx-toast-duration`, NEVER FROM `fx-dur-*`.
     * Every fx-dur-* token is damped to 0.01ms inside this very media query, so
     * deriving from one here would spin the ring at roughly a hundred thousand
     * revolutions per second — the precise inversion of the intent. This is the
     * defect the four rings above document having already been fixed once; it
     * is not repeated here. */
    .btn.fx-btn-busy::after,
    .fx-btn-busy::after {
        animation-duration: calc(var(--fx-toast-duration, 5s) / 2);
    }
    /* Stopped, and the transition removed with it. The scaleX position script
     * last set is KEPT — unlike the indeterminate sliver two rules up, whose
     * transform had to be restated because its keyframe would have parked it
     * off the end of its own track. Here the transform IS the value, so
     * cancelling the transition leaves the bar exactly where the work is. */
    .fx-loadbar-fill {
        transition: none;
    }
    .fx-loadbar[data-done="true"] {
        transition: none;
    }

    /* ══ THE BRANDED LOADER UNDER REDUCED MOTION (section 7.2) ═════════════
     *
     * THE DEFECT THIS CLOSES. Before 2026-08-14 the anti-flash delay lived ONLY
     * inside `@media (prefers-reduced-motion: no-preference)` and had no
     * `reduce` branch at all, so a user who asked for less motion got a hard
     * on/off paint on every single click — the strobe, delivered to precisely
     * the group least able to tolerate it, by a rule written to prevent it.
     *
     * THE INVERSION FIXES IT STRUCTURALLY RATHER THAN BY ADDING A BRANCH. The
     * hold is now enforced in script by `fx-load-min-visible`, and script does
     * not consult a media query — so a reduced-motion user gets the SAME
     * indicator, appearing at the same moment, held for the same 500ms. What
     * changes here is only that it does not MOVE.
     *
     * STOPPED, NOT SLOWED, AND THIS IS A DEPARTURE FROM THIS FILE'S DOCTRINE.
     * The four rings above are slowed rather than stopped on the reasoning that
     * "a still ring cannot be told from a hung request". That reasoning does
     * not transfer: those rings are shown for an UNBOUNDED wait, whereas this
     * one is guaranteed to appear, hold and leave. There is no hung state for
     * it to be confused with. The owner's instruction was explicit — a brand
     * mark that appears, holds, and leaves, with no spin, no shimmer and no
     * pulse — and the bounded lifetime is what makes it safe to follow.
     *
     * THE RING BECOMES A COMPLETE STATIC RING rather than vanishing. An arc
     * frozen at a random angle reads as a broken spinner; a full even ring
     * reads as a deliberate frame around the mark. `border-block-start-color`
     * is reset to the track colour, which is what turns the arc into a ring.
     *
     * SPECIFICITY: base `.fx-brand-loader-ring` is (0,1,0) and this is (0,1,0),
     * later in source. `.fx-load-popup[data-active="true"]` is (0,2,0) in both
     * places. Both carry on equal specificity plus source order. */
    /* ══ A STILL RING MUST STILL READ AS "WORKING" ═════════════════════════
     *
     * The instruction is to solve this WITHOUT MOTION rather than to drop to
     * nothing, and the obvious two answers are both wrong:
     *   · a full even ring reads as a decorative FRAME around the logo, which
     *     says the app is idle, not busy
     *   · an arc frozen at whatever angle it stopped at reads as a BROKEN
     *     spinner, which is worse than either
     *
     * A DASHED RING IS THE ANSWER. It is a long-established convention for
     * "pending / in progress", it is unmistakably not a solid frame, it is
     * evenly distributed so there is no arbitrary stopping angle, and it holds
     * completely still. The dash pattern comes from the border style, so no
     * geometry or keyframe is involved at all.
     *
     * THE WHOLE RING TAKES THE SIGNAL COLOUR, NOT THE TRACK COLOUR, AND THAT
     * IS A FIX — MEASURED AT 1.33:1 ON THE RUNNING APP BEFORE IT.
     *
     * The first version of this rule levelled the ring by dropping the HEAD
     * down to `--fx-color-border-emphasis`, so all four edges matched. The
     * design intent was right and is unchanged — one even dashed circle, no
     * arbitrary stopping angle. The COLOUR was wrong: border-emphasis is a
     * hairline and divider token at 14% alpha, so the levelling painted the
     * ring's only visible element in a colour meant for dividers. Composited
     * over the loading scrim it measured 1.33:1 over a dark page and 1.50:1
     * over a light one, against a 3:1 floor for a non-text graphic.
     *
     * IT LANDED ON EXACTLY THE USERS THIS BRANCH EXISTS TO SERVE. A reduced
     * motion user has no spin to fall back on, so the ring is the entire
     * indicator; at 1.33:1 there was effectively nothing on screen.
     *
     * The ring is therefore levelled UP to `--fx-color-text-subtle` — the tier
     * the animated ring already uses for its head, and the one whose contrast
     * against this scrim was measured when the popup was built. Same four
     * backdrops, all clearing the floor:
     *     light theme over a PALE page ......... 5.97:1
     *     light theme over DARK content ........ 3.95:1
     *     dark  theme over LIGHT content ....... 5.13:1
     *     dark  theme over a DARK page ......... 6.94:1
     *
     * `border-color` RATHER THAN THE FOUR LONGHANDS. It sets every edge in one
     * declaration, and it also has to OUT-CASCADE the base rule's
     * `border-block-start-color`. Logical and physical border colours cascade
     * together, so at equal specificity (0,1,0) the later declaration wins —
     * which this is, by source order. */
    .fx-brand-loader-ring {
        animation: none;
        border-style: dashed;
        border-color: var(--fx-color-text-subtle);
    }
    /* The DETERMINATE ring is exempt: it is not reporting "something is
     * happening", it is reporting a POSITION, which is legible standing still
     * and is genuinely meaningful. It keeps its sweep and simply stops
     * animating — the same split section 11 already makes between a spinner and
     * a progress bar. Its base rule is (0,2,0), so this one is too. */
    .fx-brand-loader-ring[data-determinate="true"] {
        border-style: solid;
    }
    /* The popup arrives as a swap rather than a fade. It still ARRIVES, at the
     * same moment and for the same held duration — the hold is enforced in
     * script, which does not consult a media query, so a reduced-motion user
     * sees exactly the same indicator for exactly as long. */
    .fx-load-popup[data-active="true"] {
        animation: none;
    }
}

/* THE ESCAPE HATCH FROM THE NEAR-INVISIBLE LINE TIER.
 * The translucent border family is deliberately allowed to sit between 1.03:1
 * and 1.5:1 against its surface, because a panel edge IDENTIFIES nothing and is
 * therefore outside SC 1.4.11. A user who asks the operating system for more
 * contrast has overridden that default, and the token layer already collapses
 * both line families onto the 3:1 capable `fx-color-border-strong` for them.
 * This block adds what the token layer cannot reach: a real edge on the two
 * surfaces still drawn without one, and a real boundary on the inline error,
 * which is otherwise pure text.
 *
 * WRITTEN AS THE `border` SHORTHAND, DELIBERATELY. Setting only a width and a
 * colour paints NOTHING where a rule above used `border: 0`, because that
 * shorthand also resets `border-style` to `none`, and a width plus a colour
 * with no style is inert. Any future edit here must keep the style component. */
@media (prefers-contrast: more) {
    .fx-inline-error {
        padding: var(--fx-space-2) var(--fx-space-3);
        border: calc(var(--fx-border-width) * 2) solid currentColor;
        border-radius: var(--fx-radius-sm);
    }
    .fx-empty-state-icon {
        border: calc(var(--fx-border-width) * 2) solid var(--fx-color-border-strong);
    }
    /* The badge's transparent 1.5px root border becomes a real one, so a filled
     * badge and an outline badge are both bounded rather than only one of them. */
    .badge,
    .fx-chip,
    .fx-status {
        border-color: currentColor;
    }
    /* THE SWAL2 FIELDS ARE GONE FROM THIS BLOCK, and removing them fixed a
     * defect rather than tidying one. 05-forms section 16.3 already gives every
     * bare input, select and textarea a full strength 2px stroke under this
     * query, drawn as an OUTLINE at a negative offset precisely so nothing
     * reflows. The `border` written here landed on top of that outline: two
     * strokes 1px apart on every swal2 field, and the dialog's contents shifted
     * by 2px on each axis the moment a user asked for more contrast. */
    .swal2-container .swal2-footer {
        border-block-start: var(--fx-border-width) solid var(--fx-color-border-strong);
    }
    .fx-loading-overlay {
        background-color: var(--fx-color-surface);
    }
}

/* Windows High Contrast and any other forced-colors mode. ADDED 2026-08-02; this
 * file had no branch for it, which was the gap.
 *
 * THIS FILE SUPPRESSES NO FOCUS RING ON A FOCUSABLE ELEMENT, and that is worth
 * stating so nobody goes looking for a restoration that is not here. The one
 * `outline: none` in the file is `swal2-outline` in section 8, a VENDOR LOCAL
 * being emptied so the library stops painting its own ring as a box-shadow; the
 * real outline is declared two rules later and is a genuine outline, so it
 * survives this mode untouched. `.fx-chip-remove` in section 3 likewise keeps a
 * real outline, at a negative offset. Nothing needs handing back.
 *
 * WHAT THIS FILE LOSES IS ITS ENTIRE VOCABULARY. Feedback here is carried almost
 * wholly by FILL: an alert, a toast, a badge, a chip and a status tag are each a
 * tinted plane with no stroke, and the severity IS the tint. Forced colours
 * discard author fills, so all five flatten onto Canvas simultaneously and an
 * error alert becomes a paragraph. The severity hue genuinely cannot be
 * preserved — that is the mode's whole purpose and not a defect to work around —
 * but the BOUNDARY can be, and a bounded block that says "Error" still reads as
 * an alert where an unbounded one does not.
 *
 * `currentColor` on the small tags rather than a fixed keyword: the mode has
 * already forced their text to a palette colour by the time the border resolves,
 * so the stroke follows whatever ink the row ended up with and a tag inside a
 * button and a tag inside body copy each stay legible. This mirrors what the
 * `prefers-contrast: more` block above does with the same three selectors.
 *
 * THE PROGRESS BAR IS THE ONE PLACE THAT OPTS OUT. A determinate bar's value is
 * expressed only as the width of a filled div; discard the fill and the control
 * reports nothing at all, which is a loss of information rather than a loss of
 * decoration. It takes `forced-color-adjust: none` with Highlight against the
 * track's ButtonBorder so the ratio stays visible. Nothing else in this file
 * earns that opt-out.
 * ------------------------------------------------------------------------- */
@media (forced-colors: active) {
    /* The tinted planes. A boundary, since the tint itself cannot survive. */
    .alert,
    .toast,
    .fx-empty-state-icon,
    .swal2-container .swal2-popup {
        border: var(--fx-border-width) solid ButtonBorder;
    }

    /* ABP'S TOAST NEEDS ITS BOUNDARY RESTATED AT ITS OWN SPECIFICITY, and this
     * is the hardening-versus-accessibility trap this file has now hit in three
     * places. Section 2 paints the card's hairline through `#toast-container >
     * div`, which is (1,0,1); the selector list above is (0,1,0) at most and
     * therefore CANNOT reach it. Written as `.abp-toast` the rule would have
     * looked correct in review and painted a translucent author colour that
     * forced colours discards, leaving the most frequent surface in the product
     * as unbounded text on Canvas. (1,1,0) clears the section 2 rule outright,
     * so this does not even depend on source order. */
    #toast-container > .abp-toast {
        border: var(--fx-border-width) solid ButtonBorder;
    }

    /* THE SWAL2 FIELDS NEED THEIR STROKE HANDED BACK EXPLICITLY, and this is a
     * consequence of the hardening rule in section 8 rather than of anything in
     * this mode. 05-forms section 16.4 already gives every bare input, select
     * and textarea `border: 1px solid ButtonBorder` here — but at (0,0,1), and
     * the hardening rule re-asserts `border: var(fx-ctl-border)` at (0,2,0),
     * which resolves to `0` and therefore ERASES it. Forced colours discards
     * author fills and drops box-shadows outright, so the field's fill and its
     * well both stop existing at once and an unstroked control becomes an
     * invisible rectangle. Restated at (0,2,0) so the two rules land level and
     * source order settles it in this one's favour. */
    .swal2-container :is(.swal2-input, .swal2-textarea, .swal2-select) {
        border: var(--fx-border-width) solid ButtonBorder;
    }

    /* The small tags follow their own forced ink, as in the contrast block. */
    .badge,
    .fx-chip,
    .fx-status {
        border: var(--fx-border-width) solid currentColor;
    }

    /* THE CONTEXTUAL BADGE MARKERS, HANDED BACK — AND THIS IS THE ONE PLACE IN
     * THE FILE WHERE THIS MODE COSTS INFORMATION RATHER THAN DECORATION.
     *
     * Section 3 maps Bootstrap's contextual utilities onto the tonal outline and
     * gives each semantic a marker SHAPE, because the four status hues are
     * indistinguishable in greyscale by construction — the measurements are
     * recorded over that block and they are not close. Here, hue is not merely
     * flattened, it is DISCARDED, so the shape is the whole remaining channel.
     *
     * And both halves of that channel stop painting at once in this mode. A
     * FILLED marker is a `background-color`, which is forced to a system colour
     * and lands on the plane it sits on; a HOLLOW marker is an inset
     * `box-shadow`, and forced colours drops shadows outright. Left alone, all
     * four markers would vanish on the exact display where they are the only
     * thing left. A BORDER survives, because `border-color` is FORCED to a
     * palette colour rather than discarded, so both kinds are restated as one:
     *
     *   · a filled marker becomes a 0.5em box whose border is half its own size
     *     on every edge. Under the global `box-sizing: border-box` in 01-base
     *     the four edges meet in the middle and the box paints solid, which is
     *     the same silhouette the fill gave.
     *   · a hollow marker keeps a thin border and its transparent middle, which
     *     is what the dropped inset shadow was drawing anyway.
     *
     * `.fx-status` AND `.fx-chip-dot` HAVE THE SAME GAP AND ARE DELIBERATELY NOT
     * SWEPT IN HERE. Their shapes are assigned across five separate legal status
     * classes with two different techniques, so handing them back is a change to
     * that block and not to this one. It is reported rather than done. */
    .badge:is(.bg-success, .text-bg-success, .bg-danger, .text-bg-danger)::before {
        border: 0.25em solid currentColor;
    }
    .badge:is(.bg-info, .text-bg-info, .bg-warning, .text-bg-warning)::before {
        background-color: transparent;
        border: var(--fx-border-width-strong) solid currentColor;
    }

    /* A skeleton is a placeholder for content that has not arrived. With its
     * sweep and its fill both gone it is an empty box, so it needs an edge to
     * remain a legible "loading" affordance rather than dead space. */
    .fx-skeleton {
        border: var(--fx-border-width) solid ButtonBorder;
    }

    /* THE OPT-OUT. See the note above on why this one and nothing else. */
    .progress,
    .progress-stacked {
        border: var(--fx-border-width) solid ButtonBorder;
    }

    .progress-bar {
        background-color: Highlight;
        forced-color-adjust: none;
    }

    /* The striped overlay is a gradient on top of that fill and would read as
     * noise once the fill is a single system colour. */
    .progress-bar-striped {
        background-image: none;
    }

    /* ══ THE LOADING SYSTEM (section 7.1) UNDER FORCED COLOURS ═════════════
     *
     * Forced colours REPLACE the palette rather than strengthen it, so every
     * indicator in 7.1 that is expressed purely as a background fill would
     * otherwise be erased — a bar with no bar in it. Each is repainted onto a
     * SYSTEM colour and opted out of further substitution, exactly as
     * `.progress-bar` above is.
     *
     * `Highlight` for the navigation bar, matching the progress bar's choice a
     * few rules up so the two loading indicators in the system stay the same
     * colour when the palette stops being ours. The `fx-band-fg` in the base
     * rule is meaningless here — the band's own fill is substituted too. */
    .fx-loadbar-fill {
        background-color: Highlight;
        forced-color-adjust: none;
    }

    /* The button ring is a BORDER, not a fill, so it survives substitution in
     * shape but loses the track-versus-head distinction that makes it read as
     * a ring: `color-mix` at 30% alpha collapses to the same system colour as
     * the head. The track is therefore made transparent and only the head is
     * painted, which is still unambiguously a spinner and is the treatment
     * that degrades most cleanly.
     *
     * SPECIFICITY, for the same reason as the reduced motion branch: the base
     * rule's heaviest selector is (0,2,1), so the `.btn` prefix is restated
     * here too. Without it this rule weighs (0,1,1) and never applies to a
     * Bootstrap button. */
    .btn.fx-btn-busy::after,
    .fx-btn-busy::after {
        border-color: transparent;
        border-block-start-color: Highlight;
        forced-color-adjust: none;
    }

    /* The label stays transparent by the base rule, which under forced colours
     * would hide the button's text with no ring big enough to replace it. The
     * text is restored: a busy button that still reads as a button is better
     * than a blank one, and the ring beside it still reports the state. */
    .btn.fx-btn-busy,
    .fx-btn-busy {
        color: ButtonText;
    }

    /* Skeletons are placeholders for content that is not there. Under forced
     * colours a filled block is indistinguishable from real content, so they
     * become outlined frames instead — still obviously "not here yet", and no
     * longer impersonating text.
     *
     * THE `[dir="rtl"]` SELECTOR IS NOT OPTIONAL AND IS NOT A COPY-PASTE SLIP.
     * Section 5 declares the sweep TWICE — once on `.fx-skeleton` (0,1,0) and
     * once on `[dir="rtl"] .fx-skeleton` (0,2,0) to mirror the gradient. A
     * single-class rule here would weigh (0,1,0), lose to the (0,2,0) RTL rule,
     * and leave the gradient painted on every skeleton in the product's PRIMARY
     * reading direction — inert in Arabic, correct-looking in English, which is
     * the worst possible way for this to fail. The reduced motion branch above
     * cancels the same pair for the same reason; this matches it deliberately. */
    .fx-skeleton,
    [dir="rtl"] .fx-skeleton {
        background-image: none;
        background-color: Canvas;
        border: var(--fx-border-width) solid GrayText;
        forced-color-adjust: none;
    }

    /* THE BRANDED LOADER (section 7.2). The MARK is an image and is left
     * entirely alone: `forced-color-adjust` defaults to `auto` on replaced
     * content, which preserves it, and a logotype is exempt from SC 1.4.11
     * anyway. What needs help is the ring, whose two-tone arc collapses to one
     * substituted colour and stops reading as a spinner — so the track is made
     * transparent and only the head is painted, the same degradation the button
     * ring above takes. */
    .fx-brand-loader-ring {
        border-color: transparent;
        border-block-start-color: Highlight;
        forced-color-adjust: none;
    }
    /* THE SCRIM IS SUBSTITUTED TO AN OPAQUE SYSTEM CANVAS, and this is the one
     * place forced colours make the loading state MORE of a takeover rather
     * than less. It cannot be helped and it is the correct trade: there is no
     * card here any more, so the scrim is the mark's only backdrop, and a
     * translucent veil is precisely what this mode refuses to render
     * predictably. An opaque Canvas guarantees the ring's 3:1 against a known
     * ground; leaving it translucent would put the ring over arbitrary content
     * with no contrast guarantee at all. */
    .fx-load-popup {
        background-color: Canvas;
    }
    .fx-load-popup-label {
        color: CanvasText;
    }
}


/* ── 12) Print ──────────────────────────────────────────────────────────────
 * Transient chrome disappears. Alerts survive as bordered text blocks, because
 * a printed record of a warning is often the point of printing the page.
 * ------------------------------------------------------------------------- */
@media print {
    .toast,
    .toast-container,
    .fx-toast-stack,
    #toast-container,
    .fx-skeleton,
    .fx-skeleton-row,
    .spinner-border,
    .spinner-grow,
    .fx-spinner,
    .fx-loading-overlay,
    .abp-block-area,
    /* Section 7.1's three additions. A loading indicator on paper is noise at
     * best and a misreport at worst — the page it was describing has finished
     * loading by definition, or it would not be at the printer. The stage line
     * goes with them: a report the user has chosen to print is done. */
    .fx-loadbar,
    .fx-load-stage,
    .fx-skeleton-shape,
    /* Section 7.2's branded surfaces. A loading popup on paper is an artefact
     * of the browser, not of the record. */
    .fx-load-popup,
    .fx-brand-loader,
    .swal2-container {
        display: none;
    }
    /* The busy button prints as an ordinary button. Only the ring and the
     * transparent label are cancelled — the control itself is part of the page
     * and its label is information. Matching the (0,2,0) and (0,2,1) weights of
     * the base rules in 7.1, for the reason set out in section 11. */
    .btn.fx-btn-busy,
    .fx-btn-busy {
        color: inherit;
    }
    .btn.fx-btn-busy::after,
    .fx-btn-busy::after {
        display: none;
    }
    .alert,
    .fx-inline-error {
        background-color: transparent;
        border: 1pt solid var(--fx-color-border-strong);
        border-radius: var(--fx-radius-md);
        padding: var(--fx-space-3) var(--fx-space-4);
        box-shadow: none;
        break-inside: avoid;
    }
    .fx-chip,
    .fx-status,
    .badge {
        background-color: transparent;
        border: 1pt solid var(--fx-color-border-strong);
        color: var(--fx-color-text);
    }
    /* THE CONTEXTUAL BADGES NEED THAT INK RESTATED WITH A FLAG, and the reason
     * is mechanical rather than stylistic. The mapping in section 3 sets its
     * colour with `!important` because it is cancelling Bootstrap's own, and an
     * important declaration outranks a plain one at ANY specificity — so the
     * rule immediately above cannot reach a `.badge.bg-danger`, and a printed
     * page would keep the screen's status hue on paper that is very often
     * monochrome. The flag is repeated here for that and for nothing else; it is
     * still cancelling `bootstrap.rtl.css`, one link further along the chain.
     * The background needs no restatement: section 3 already forces it
     * transparent, which is what this block wants anyway. */
    .badge:is(
        .bg-info, .bg-success, .bg-warning, .bg-danger,
        .bg-secondary, .bg-light, .bg-dark,
        .text-bg-info, .text-bg-success, .text-bg-warning, .text-bg-danger,
        .text-bg-secondary, .text-bg-light, .text-bg-dark
    ) {
        color: var(--fx-color-text) !important;
    }
    .progress {
        border: 1pt solid var(--fx-color-border-strong);
    }
    .fx-empty-state {
        padding-block: var(--fx-space-4);
    }
}
