/* RBMG Core v0.4.1-draft — stylesheet
 * GENERATED by tools/build-dist.py. Do not edit this file: an edit here
 * is invisible to every check the core ships, and is a defect rather than
 * a fork. Change the source and rebuild.
 *
 * Load order (the three-layer rule, and nothing else to remember):
 *   core  →  brand  →  site
 *
 * Concatenated, in this order:
 *   core.layers.css
 *   core.tokens.css
 *   core.foundation.css
 *   core.typography.css
 *   core.surfaces.css
 *   core.components.css
 *   core.forms.css
 *   core.overlays.css
 */

/* ---------- core.layers.css ---------- */
/* ==========================================================================
   RBMG Core — layer order
   css/core.layers.css · v0.2.0-draft

   THIS FILE MUST LOAD FIRST. Nothing else.

   Why it exists as a separate file: CSS registers a layer's position at
   the point its NAME is first seen. The order statement was originally
   inside core.foundation.css, which works only if foundation happens to
   load before the token sheet — otherwise `tokens` gets registered ahead
   of `reset` and the whole cascade inverts, silently. Splitting the
   declaration out makes the correct order independent of file order, so
   the failure is no longer possible to cause by reordering <link> tags.

   Everything downstream depends on this one line:
     · utilities beat components by cascade POSITION, which is what
       retires `!important` across the shop
     · a brand sheet cannot outrank a component's structure, because it
       only ever writes into `tokens`

   THE SIXTH LAYER — `a11y`, added v0.4.1 (Pilot 04, E-106).

   `site.css` loads into `utilities` and loads LAST, so until now there was no
   position from which a core guarantee could outrank site composition without
   `!important`, which this project bans. That made "accessibility is a core
   property, not a per-project install" true of everything the core DRAWS and
   false of anything a site adds.

   E-106 is what that costs. The reduced-motion block was headed "one place,
   covering every component above" and was an enumerated list of eight
   selectors in one file — so a site composing a transition shipped it to a
   user who asked for no motion, and nothing anywhere said so. It is the
   TAP_RULES shape: the blind spot is the list, not the thing it forgot. (It
   also missed nine CORE rules in three other files, which is how far a list
   drifts from its own heading in three versions.)

   `a11y` is the one place a core guarantee is allowed to win, and the bar for
   putting something in it is deliberately high: it must be a WCAG obligation
   the USER has expressed, not a preference the core holds. Reduced motion is
   the only thing in it. A colour, a size or a spacing decision does NOT belong
   here — those are the site's to compose, and a layer that grew past its one
   purpose would just be `!important` with better manners.
   ========================================================================== */

@layer reset, tokens, base, components, utilities, a11y;

/* ---------- core.tokens.css ---------- */
/* ==========================================================================
   RBMG Core — tokens layer (Tier 0B semantic roles)
   core.tokens.css · v0.2.0-draft · 2026-08-07

   THE CONTRACT
   --------------------------------------------------------------------------
   Components read ONLY the roles in this file. A component never references
   a Tier 0A brand primitive; a brand sheet never defines a new role; a site
   sheet touches neither. (Precedence: CORE > BRAND > SITE — see
   rbmg-phase0-tier-map.md, "Precedence" block.)

   A brand sheet's entire job is answering this file's role list.

   Roles are marked one of three ways:

     [REQUIRED]  No core default exists. The brand sheet MUST answer it.
                 A role the Figma file doesn't answer is a blocking question
                 for the designer — never an invented value, never a snap to
                 a neighboring hex.
     [DEFAULT]   A core default ships below. A brand may override it; a brand
                 taking the default LISTS it in the role-satisfaction
                 confirmation ("--surface: core default, white page ground")
                 rather than re-declaring it.
     [DERIVED]   Computed by the core. NEVER authored in a brand sheet — an
                 authored -hover is a lint error. This is what makes the
                 stale-hover failure found in the audit inexpressible.

   LOAD ORDER
   --------------------------------------------------------------------------
   @layer reset, tokens, base, components, utilities, a11y;
   core.tokens.css loads first inside `tokens`, brand.css second (same
   layer) — the brand's :root re-declarations win by source order, and no
   file outside the tokens layer may declare a token.
   ========================================================================== */

@layer tokens {
  :root {
    /* ----------------------------------------------------------------------
       BRAND ACTION COLORS
       The brand sheet points these at Tier 0A primitives:

         --brand-primary      [REQUIRED] the lead action/identity color
         --brand-secondary    [REQUIRED] the supporting brand color
         --brand-accent       [DEFAULT]  the highlight/CTA-contrast color

       primary and secondary have no usable default, so none is shipped;
       they are declared here as documentation only.

       --brand-accent IS defaulted, as of v0.3.0 (Pilot 01, F-013). It was
       [REQUIRED], on the reasoning that no default is usable — but the
       first real brand had exactly TWO action hues. No third colour
       appeared anywhere in a 10,596px comp, and none in its Figma
       variables. The role is not decorative (it feeds the icon roles,
       .btn-accent, .bg-accent and the gated --on-accent), so leaving it
       unanswered left those UNRESOLVED, and a two-hue brand was forced to
       either alias a hue it already had or invent one.

       The wrong version this prevents is the one that actually happens on
       a deadline: somebody picks a plausible third hue no designer chose,
       and it ships as brand. Falling back to --brand-primary keeps the
       "no hex-named one-off" guarantee — the variant list is still
       generated from roles — while letting a two-hue brand answer
       honestly by answering nothing.
       ---------------------------------------------------------------------- */
    --brand-accent: var(--brand-primary);

    /* ----------------------------------------------------------------------
       CONTRAST DIRECTION — [DERIVED] · the primitive everything else uses
       v0.2.0

       `--contrast-toward` resolves to white on a dark fill and black on a
       light fill, computed from the fill's OWN lightness via relative
       color syntax. Every publisher re-declares it from its own fill (see
       core.surfaces.css), so "which way is more contrast?" is answered by
       measured luminance rather than by selector-list membership.

       Pilot 01 is why this exists. Direction used to be hardcoded —
       `.bg-surface` lived in the "light" list and always darkened. On a
       dark-ground brand that made every hover LESS legible
       (--link 16.30:1 → --link-hover 10.15:1) while still clearing 4.5:1,
       so no threshold gate could see it. A threshold cannot detect a
       guarantee being violated; direction has to be computed, not listed.

       IT DECIDES ON MEASURED LUMINANCE NOW, NOT ON A PROXY FOR IT —
       v0.4.1, Pilot 04 (E-035). See --contrast-pick below, which is the whole
       mechanism and the whole story.
       ---------------------------------------------------------------------- */
    --contrast-toward: color(from var(--surface) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));

    /* ----------------------------------------------------------------------
       --contrast-pick — [DERIVED] · THE AUTO-CONTRAST SWITCH ITSELF
       v0.4.1, Pilot 04 (E-035). Never authored, never overridden; it is not a
       colour and a brand has no opinion about it.

       This is not a value. It is the EXPRESSION every auto-contrast
       derivation in the core substitutes into a relative colour function:

           color(from <fill> srgb-linear
                 var(--contrast-pick) var(--contrast-pick) var(--contrast-pick))

       In srgb-linear, `r`, `g` and `b` are the fill's LINEAR-LIGHT channels,
       so 0.2126r + 0.7152g + 0.0722b IS the WCAG relative luminance — exactly,
       with no gamma maths and no pow(). White is (1 1 1) in that space and
       black is (0 0 0), so driving all three channels with one 0-or-1 switch
       yields one or the other. The switch is 1 while luminance is below the
       point where white and black TIE:

           Y_tie = √0.0525 − 0.05 = 0.17913, where both measure 4.58:1

       WHAT THIS REPLACES, AND WHY THE REPLACEMENT IS A DIFFERENT KIND OF
       CLAIM. Every derivation used to read
       `oklch(from <fill> clamp(0, (l / 0.623 - 1) * -infinity, 1) 0 0)`:
       OKLCH lightness compared against the OKLCH perceptual midpoint. Two
       separate errors compounded there.

       The pivot was the wrong number. L = Y^⅓ for a neutral, so the tie sits
       at L = 0.5637 and everything in [0.5637, 0.623] got the label that
       measures WORSE — bottoming out at 3.60:1, an AA failure chosen
       automatically by the mechanism whose whole job is to make an illegible
       label inexpressible. Pilot 04's AC blue sits at L = 0.607, dead centre:
       white 3.83:1 where black would have been 5.49:1, and the brand darkened
       its own hue to work around it. Plain #808080 got white at 3.95:1.

       And OKLCH LIGHTNESS IS NOT LUMINANCE. Chroma shifts L away from Y^⅓ by
       up to ~0.05, so no pivot on L can be right everywhere. Swept over the
       whole sRGB gamut (140,608 fills), the best possible pivot is ≈0.575 and
       it still leaves 2,800 fills with a sub-4.5:1 label, worst case 4.07:1 on
       a saturated violet. The tie point 0.5637 leaves 3,459. The shipped 0.623
       left 11,668, worst case 3.40:1. THE FIRST VERSION OF THIS FIX MOVED THE
       PIVOT TO 0.5637 AND THE GAMUT SWEEP CAUGHT IT AT 3.93:1 — a better
       constant, still a proxy, still wrong on 2% of the gamut.

       Measuring the thing itself removes the class: a label derived this way
       cannot measure below 4.58:1 for ANY fill in the gamut, and the sweep in
       roles.test.mjs asserts exactly that rather than reporting how bad it is.

       ONE PLACE, BY CONSTRUCTION. The old form repeated its constant 22 times
       and the only defence was a test that they all agreed. Here the constant
       exists once and every derivation reads it, so drift is not a thing that
       can happen.

       BROWSER FLOOR IS UNCHANGED, and it is MEASURED rather than assumed.
       Relative colour syntax was already a hard requirement — every derivation
       in this file used `oklch(from …)` — and `srgb-linear` in color() predates
       it in all three engines. `npm run gate:browsers` resolves this expression
       in Chrome, WebKit and Firefox against ten fills chosen to cover every way
       the decision can be made, and asserts the PAINTED label plus its measured
       ratio. All three agree, identically, on all ten.

       That gate is separate from gate:rendered on purpose. Geometry is a
       question one engine can stand in for; colour-syntax support is per-engine
       by definition, and this expression is the one thing in the core with no
       fallback — an engine that cannot parse it makes the declaration invalid
       at computed-value time, and a button label INHERITS THE PAGE INK. Run it
       whenever this expression changes.
       ---------------------------------------------------------------------- */
    --contrast-pick: clamp(0, (0.17913 - (0.2126 * r + 0.7152 * g + 0.0722 * b)) * infinity, 1);

    /* ----------------------------------------------------------------------
       HOVER TINTS — [DERIVED]
       A hover state must always move AWAY from its own surface. Mixing
       toward --contrast-toward does that at either polarity; the legacy
       "+20% white" rule only did it on light grounds.

       The original derivation was verified byte-exact against every
       authored hover pair in the 2026-08 audit — but that estate was
       entirely light-ground, so "mix with white" and "mix away from the
       surface" were indistinguishable in the evidence. Pilot 01
       separated them. Recorded because the same conflation is likely
       elsewhere: see the gradient premise in the pilot report.
       ---------------------------------------------------------------------- */
    --brand-primary-hover:   color-mix(in srgb, var(--brand-primary),   var(--contrast-toward) 20%);
    --brand-secondary-hover: color-mix(in srgb, var(--brand-secondary), var(--contrast-toward) 20%);
    --brand-accent-hover:    color-mix(in srgb, var(--brand-accent),    var(--contrast-toward) 20%);

    /* ----------------------------------------------------------------------
       SURFACES — [DEFAULT]
       The publisher side of the surface-aware system. Every surface utility
       in the core publishes --link/--link-hover/--icon-accent values legible
       against itself; these are the page-level defaults.
       ---------------------------------------------------------------------- */
    --surface:            #ffffff;
    --surface-hover:      color-mix(in srgb, var(--surface), black 6%);  /* the #f0f0f0 pattern, derived */
    --surface-raised:     #ffffff;   /* + --shadow-sm; distinct role so a brand can tint cards */
    --surface-sunken:     #eef0f5;   /* wells, alternating bands */
    --surface-inverse:    #121212;   /* dark bands; brands usually point this at a deep brand hue */

    /* THE SECOND SUNKEN STEP — [DEFAULT], v0.4.1, Pilot 04 (E-114).
       The surface vocabulary was surface / raised / sunken / inverse: exactly
       ONE step below the page ground. Pilot 04's comp paints two greys —
       #EEF0F5 in three sections and #E5E7EA in two more, the pair that brackets
       the page's lower half — and the second one had no role to land on.

       There is no site-tier workaround and that is the whole ticket: a brand
       may answer roles and may not define one, a site may not reference a
       Tier 0A primitive, and scoping --surface-sunken on a section needs a
       value that could only be a hex. The build shipped both bands on
       --surface-sunken as a stated deviation.

       DERIVED, not typed, and mixed toward the page ground's OWN contrast
       direction so it darkens on a light brand and lightens on a dark one. 4%
       is the number the comp validates rather than a taste: #EEF0F5 mixed 4%
       toward black is #E4E6EB against the comp's #E5E7EA — one bit per
       channel. Derive it, then check it lands on the value someone already
       picked by eye, which is how the hover tints and the border ramp were
       each settled.

       It publishes like any other surface (.bg-surface-sunken-strong is in the
       neutral publisher list) and it has gate rows, because a surface with no
       gate row is the TAP_RULES shape — a list that measured seven selectors
       and said nothing whatever about the eighth.

       The direction is derived from --surface INLINE rather than by reading
       var(--contrast-toward), and that is deliberate twice over. "Sunken" is
       a relationship to the PAGE GROUND, not to whatever band you happen to be
       inside, so reading a published --contrast-toward would make the role mean
       different things in different places. And a :root role whose value
       mentions --contrast-toward is one every publisher must re-declare — the
       rule roles.test.mjs enforces — which would be true of a link colour and
       is nonsense for a surface. */
    --surface-sunken-strong:
      color-mix(in srgb, var(--surface-sunken),
                color(from var(--surface) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick)) 4%);

    --on-surface:         #121212;
    --on-inverse:         #ffffff;

    /* MUTED INK — [DERIVED from the surface it is gated against], v0.4.0.
       These were two hardcoded values, and --on-inverse-muted was
       rgba(255,255,255,.8): a role meaning "muted text on the inverse
       surface" whose default ASSUMED that surface is dark. On a light inverse
       island it measures 1.26:1 composited — invisible — which is the
       --on-inverse failure exactly, in the role sitting next to it.

       BOTH REFERENCE BRANDS MASKED IT by answering the role, which is the same
       masking that hid the original. Pilot 02 hit it, said so in a comment
       ("invisible on a #DCDFE7 card"), overrode it with a ramp stop, and that
       stop measured 4.09:1 — so the workaround failed too, and nothing caught
       either one until the muted rows landed.

       Deriving from the ink and mixing 35% toward the surface makes it correct
       at any polarity by construction. THE CHAIN IS SAFE, and it is worth
       being explicit about why, because the house rule forbids chaining a
       gated [DEFAULT] to a role a brand overrides: --on-inverse-muted is gated
       against --surface-inverse, and it derives from --on-inverse, which is
       gated against that same surface. A brand that answers the parent
       correctly gets a correct child; a brand that answers it wrongly fails on
       the parent's own gate row first. Same shape as --on-btn-* following its
       seam rather than the base role.

       30%, NOT 35% — v0.4.1, Pilot 04 (E-036). 35% was measured against a
       near-black ink (#121212 → 5.83:1 on white, next to the 5.57:1 of the
       #656870 it replaced: derive it, then check it lands on the value someone
       already picked by eye). It does not survive a CHROMATIC ink. Pilot 04's
       page ink is a navy #0F2848, so the default resolves to #637388 and
       measures 4.84:1 on white and 4.25:1 on the sunken band — a FAIL, and
       Pilot 02's real brand measured 4.30:1 on the same row. Two of two real
       brands, while both references passed.

       The core has already learned that retuning a muted constant "only moves
       which brand fails", so this was measured before it moved — four brands
       (both references, Pilot 02's, Pilot 04's) × four neutral grounds, with
       each brand's own answer stripped so the DEFAULT is what is under test:

         toward --surface 35% (was)   Pilot 04 sunken 4.25  FAIL
         toward --surface-sunken 35%  Ironwood raised 4.45  FAIL — a NEW one
         toward --surface 30%         nothing fails

       The middle row is the intuitive fix and it is wrong, which is the second
       polarity earning its keep before a line was written: on a dark-ground
       brand the RAISED surface is lighter than the page ground, so fading the
       ink toward the sunken band moves it the wrong way.

       What forces the decision rather than leaving it a brand's problem is
       --surface-sunken-strong above: adding a fourth neutral ground makes the
       35% default fail on two brands instead of one. Blast radius is zero —
       all four brands answer --on-surface-muted themselves, so this governs
       only brands not yet written. */
    --on-surface-muted:   color-mix(in srgb, var(--on-surface), var(--surface) 30%);
    --on-inverse-muted:   color-mix(in srgb, var(--on-inverse), var(--surface-inverse) 30%);

    /* ----------------------------------------------------------------------
       ON-COLORS — [DEFAULT, contrast-gated] · COMPUTED, not chained
       v0.2.0 — CHANGED. These used to default to `var(--on-inverse)`.

       That was the single root cause of Pilot 01's colour failures.
       `--on-inverse` means "text on the inverse surface." A dark-ground
       brand must set it DARK, because its inverse surface is the page's
       one light island. One correct edit to one surface role then
       silently broke three unrelated roles:
         --on-primary   6.05:1 → 2.69:1
         --on-secondary 10.62:1 → 1.53:1
         --on-accent    6.05:1 → 2.69:1
       All six declared gates still reported PASS.

       THE RULE THIS ESTABLISHES: never var()-chain a gated [DEFAULT] to
       another role a brand is expected to override. A gated role must
       resolve from the thing it is gated AGAINST — so each on-colour is
       now computed from its own brand hue's lightness and is correct at
       any polarity by construction. Verified by pixel sampling: white on
       #C02021 (16.3:1), black on #F2A54A (10.3:1), black on #2BC4A8
       (8.4:1) — all chosen automatically.

       A brand may still override with a literal (e.g. an off-white that
       matches its palette); the gate then checks the literal.
       ---------------------------------------------------------------------- */
    --on-primary:   color(from var(--brand-primary) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --on-secondary: color(from var(--brand-secondary) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --on-accent:    color(from var(--brand-accent) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));

    /* ----------------------------------------------------------------------
       BUTTON FILL SEAMS — [DEFAULT] value-only, component-scoped
       v0.3.0 — NEW (Pilot 01, F-010).

       --brand-secondary is documented as the deep hue behind BANDS and
       also generates .btn-secondary. The pilot's brand gave those jobs
       different values (#2E414C for bands, #5E6B78 for the button), so
       answering the role correctly for one made the other wrong — and the
       build shipped with no secondary button rather than a wrong one.

       A brand using one value for both answers nothing here. One that
       splits them answers a line. The variant list is still generated
       from roles, so the "no hex-named one-off" guarantee is intact.

       The LABELS are [DERIVED] from the seam, not from the base role. If
       they chained to --on-primary, a brand overriding the fill would keep
       the base hue's label — which is the stale-hover failure that
       commissioned this project, and the var()-chain rule from v0.2.0,
       arriving together. Deriving from the seam means answering the fill
       is enough; the label follows and the gate measures the pair.

       Scope note: these are BRAND-tier (:root) seams, so deriving the
       labels at :root is correct. Compare --on-gradient, which is
       re-declared by its surface publisher because gradients are selected
       PER SECTION — a derived role resolves on the element that declares
       it, so anything a descendant can rebind must be re-derived where it
       is rebound.
       ---------------------------------------------------------------------- */
    --on-btn-primary:   color(from var(--btn-primary, var(--brand-primary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --on-btn-secondary: color(from var(--btn-secondary, var(--brand-secondary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --on-btn-accent:    color(from var(--btn-accent, var(--brand-accent)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));

    /* ----------------------------------------------------------------------
       THE FILLED ACTIVE TAB — v0.4.0, Pilot 03.
       `rbmg-tabs[active="fill"]`, the seam and its derived label.

       --tab-active says "active" with a COLOUR: one role drives the label
       and the 3px indicator together, deliberately, so answering it cannot
       leave a stale indicator behind. Pilot 03's comp says "active" with a
       SURFACE — a filled pill lifted off a brand band, carrying an icon and
       a label in a second hue. That is not a value --tab-active can answer,
       because it is not a colour change: it is a fill, and a fill needs a
       PAIR.

       This is the .btn lesson arriving at a component that had learned only
       half of it. A single role painting a fill AND its label is the
       .btn-outline-inverse failure written down — `currentColor` on its own
       background, 1.00:1, invisible. So the fill is a seam and the label is
       [DERIVED] FROM THE SEAM, never from --tab-active:

         --tab-active       is the colour treatment  (undeclared seam)
         --tab-active-fill  is the surface treatment (undeclared seam)

       They are independent on purpose. A brand answering one says nothing
       about the other, and a site choosing `active="fill"` gets a label that
       already tracks whatever fill it lands on.

       WHY --surface IS THE FALLBACK, not a brand hue. Publishers re-declare
       --contrast-toward, --link, --icon-accent and --border from their own
       fill; they do NOT re-declare --surface. So inside a .bg-primary band
       var(--surface) still resolves to the PAGE GROUND — which is exactly
       what a filled active tab means: the tab lifts out of the band to the
       page's own surface. It reads correctly at either polarity for free,
       and a brand that wants a different pill answers the seam.

       The seam stays UNDECLARED with its fallback at the point of use. A
       [DEFAULT] declared here and chained to a role a brand overrides is the
       --on-inverse failure, and this file is not going to commit it twice.
       ---------------------------------------------------------------------- */
    --on-tab-active-fill: color(from var(--tab-active-fill, var(--surface)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));

    /* ----------------------------------------------------------------------
       LINK & ICON — the surface-aware system
       v0.2.0 — RENAMED. `-on-light` / `-on-dark` became
       `-on-surface` / `-on-inverse`.

       The old names encoded a brand's BRIGHTNESS into the role name. On a
       dark-ground brand a correct brand sheet had to read
       `--link-on-light: white`, which reads like a bug and invites the
       next person to "fix" it straight back into a white-on-white
       failure. A role should name WHICH SURFACE it sits on — invariant —
       not how bright that surface happens to be.

       Old names still resolve as a fallback; they are deprecated and the
       linter should flag them.

       --link / --link-hover / --icon-accent are PUBLISHED CONTEXT VALUES:
       set only by surface publishers (core.surfaces.css), read by
       consumers, never authored by a brand or site sheet.
       ---------------------------------------------------------------------- */
    --link-on-surface: var(--link-on-light, var(--brand-primary));
    --link-on-inverse: var(--link-on-dark,  var(--on-inverse));
    --icon-on-surface: var(--icon-on-light, var(--brand-accent));
    --icon-on-inverse: var(--icon-on-dark,  var(--on-inverse));
    /* --muted is the PUBLISHED muted-text value, and it is the same split
       --link already uses: the brand answers --on-surface-muted (what muted
       copy looks like on the page ground), and every surface publisher
       re-declares --muted from its OWN fill.

       v0.4.0 — NEW (Pilot 02, tickets 19 and 25). --on-surface-muted was
       consumed directly by .text-muted, the placeholder, the drawer/tab
       secondary text and .breadcrumb-item.active, so on .bg-primary,
       .bg-secondary, .bg-accent and any gradient it resolved to the PAGE
       GROUND's muted grey and was inherited already-resolved. Nothing
       republished it, exactly as nothing republished --on-gradient and then
       --focus-ring. Third time for the same shape, in a third role.

       Consumers read --muted. A brand still answers --on-surface-muted and
       nothing changes for it. */
    --muted:       var(--on-surface-muted);
    /* root-level published defaults (double as the page-ground fallback): */
    --link:        var(--link-on-surface);
    --link-hover:  color-mix(in srgb, var(--link-on-surface), var(--contrast-toward) 20%);
    --icon-accent: var(--icon-on-surface);

    /* ----------------------------------------------------------------------
       LINK UNDERLINE — [DEFAULT] · the affordance, not decoration
       v0.4.0 — NEW (Pilot 02, ticket 20).

       These two exist so that "our links look like this" is a brand answer
       instead of a reason to write `text-decoration` in site.css. What a
       brand may NOT do is remove the underline by answering a role: zeroing
       the thickness kills the hover thickening too (it derives by
       multiplication), and the rendered gate then fails every link in running
       text. The knob tunes the affordance; it cannot delete it.

       0.0625em is 1px at body size and scales with the type, so a link inside
       a display heading gets a rule proportional to its own letterforms
       rather than a hairline. Offset is a role because the right value
       depends on the brand's descenders.
       ---------------------------------------------------------------------- */
    --link-underline-thickness: 0.0625em;
    --link-underline-offset:    0.15em;

    /* ----------------------------------------------------------------------
       FOCUS RING — [DEFAULT, contrast-gated at 3:1]
       v0.2.0 — NEW.

       Pilot 01: :focus-visible was styled only on .btn, form controls and
       two overlay internals. Everything else — nav toggles, tabs,
       accordion toggles, footer links — fell back to the browser default
       (#005FCC), which measured 2.72:1 on a dark-ground brand, under
       WCAG 2.4.11's 3:1. A site-authored focus ring is exactly the
       per-project accessibility install this core exists to retire, so
       the ring is now global (see core.foundation.css) and follows the
       surface's own contrast direction.
       ---------------------------------------------------------------------- */
    --focus-ring: var(--contrast-toward);

    /* ----------------------------------------------------------------------
       SCRIM — [DEFAULT] · text over imagery
       v0.2.0 — NEW.

       Pilot 01 put text over photography in four of fourteen sections and
       found NO compliant path: every core surface is opaque, so the only
       expressible option was an rgba() in site.css — a raw colour outside
       the brand sheet, i.e. a Tier-2 violation. The most common
       composition pattern in the comp had no legal expression, and the
       mechanism keeping text legible over images was un-tokenized and
       unmeasurable.

       --scrim is a translucent veil of the page ground. The .bg-scrim
       utilities publish link and icon colours like any other surface, so
       text over a photo is governed by the same system as text anywhere
       else.

       v0.4.0 — THE RAMP IS ANSWERABLE TOO (Pilot 02, ticket 6). Only the
       scrim's SOURCE COLOUR was a role; its weight was baked in at 72% and 88%
       opaque. That is 3-9x heavier than the pilot's comp wanted, so three
       sections hand-composed their own color-mix() in site.css — a raw
       composite outside the brand sheet, which is the Tier-2 violation the
       scrim was introduced to end. A knob nobody can reach is a knob that gets
       rebuilt locally.

       Stated as "how much of the image shows through", because that is the
       decision a designer is actually making when they look at a comp.

       ⚠️ WHAT THE GATE CANNOT DO WITH THIS, stated because loosening the knob
       makes it matter more: the role gate resolves `transparent` as the colour
       it is mixed with, so it measures text on a FULLY OPAQUE scrim. That is
       an upper bound, and the lighter a brand sets the ramp the looser it
       gets. Text over imagery is not gate-measurable in general — that is the
       whole reason --gradient-ref exists for gradients. The gate now PRINTS
       the alpha beside the ratio so the assumption is on screen rather than
       implied.
       ---------------------------------------------------------------------- */
    --scrim-alpha:        28%;   /* how much of the image shows through */
    --scrim-strong-alpha: 12%;
    --scrim:        color-mix(in srgb, var(--surface), transparent var(--scrim-alpha));
    --scrim-strong: color-mix(in srgb, var(--surface), transparent var(--scrim-strong-alpha));

    /* ----------------------------------------------------------------------
       GRADIENT — [DEFAULT]
       v0.3.0 — REBUILT. Pilot 01, F-009.

       v0.1.0 modelled a gradient as ONE endpoint pair rendered at six fixed
       LINEAR angles, on the audit finding that "every audited gradient
       family is deep-hue → lead-hue at an angle." Two things went wrong.

       First, the premise was true of the LEGACY ESTATE, not of RBMG
       brands. The first new brand shipped SIX separately named gradients
       (Hero, Steel, Brands Block, Our Story, Maint Plans, Footer), one of
       which is a 16-stop conic sweep at irregular stops — not two-stop,
       not linear, not one of six angles, and six different ones. The six
       angle utilities were used ZERO times on that build; both gradient
       sections were flattened and logged as deviations.

       Second — the part not to repeat — the fix is NOT a richer parametric
       model (shape + position + stops as roles). That is the same mistake
       one level more general: inventing a universal shape from a sample of
       brands, which the next brand's weirdness breaks again. Someone will
       ship a mesh gradient, an SVG fill, or two blend-mode layers.

       SO: the core stops modelling the IMAGE, and models only the thing it
       actually needs — the colour text has to survive.

         --gradient      the background-image, whatever CSS can express. A
                         brand may declare as many named ones as it has
                         (--gradient-hero, --gradient-footer, …); site
                         composition selects one per section.
         --gradient-ref  [GATED] the flat colour the core reasons about:
                         the stop where text has the LEAST contrast. Every
                         legibility decision — text colour, the link and
                         icon publishers, hover direction — derives from
                         this, and the role gate measures it.

       That split is what makes the wrong version inexpressible. The wrong
       version is six hand-authored background-image blobs in site.css:
       raw colour outside the brand sheet, the Tier-2 violation this
       project exists to stop, and unmeasurable besides. A gradient now
       cannot enter the system without declaring the surface the core is to
       reason about — a named gradient with no --ref is a gate failure, not
       a silent fallback.

       FIGMA TRAP, recorded because it costs an hour every time:
       `get_variable_defs` returns gradient variables as EMPTY STRINGS —
       the API does not serialise gradient values. A brand sheet built from
       Figma variables alone silently has no gradients at all. Read them
       off the node's fills instead.
       ---------------------------------------------------------------------- */
    --gradient-from: var(--brand-secondary);
    --gradient-to:   var(--brand-primary);
    /* the angle is its own role so .bg-gradient-<n> can re-angle a brand's
       named gradient rather than replacing it — ticket 11 */
    --gradient-angle: 135deg;
    --gradient:      linear-gradient(var(--gradient-angle, 135deg), var(--gradient-from), var(--gradient-to));
    --gradient-ref:  var(--gradient-from);

    /* [DERIVED] Text on a gradient, computed from the reference stop's own
       lightness — never chained to another role. Chaining is what produced
       Pilot 01's invisible button, and v0.1.0's gradient publisher had the
       same shape: it set --link to --on-secondary, which is only correct
       for as long as --gradient-from happens to be the secondary hue. */
    --on-gradient: color(from var(--gradient-ref) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));

    /* ----------------------------------------------------------------------
       STATUS — [DEFAULT]
       Validation/feedback roles. Defaults chosen to clear AA on both
       --surface and --surface-sunken (demo: danger 6.54 / 6.04,
       success 5.34 / 4.94).

       STATUS IS BINARY — BY DECISION, NOT OMISSION (Sept 2026).
       --warning and --info will not be added. The core consumes status in
       exactly one place (form validation), which is a pass/fail signal;
       nothing in the core paints a warning yellow or an info blue, and a
       role with zero consumers is a bug factory. Where a site design
       calls for a yellow/highlight treatment, that is a brand accent or a
       site-sheet addition, appropriate to that design — never a status
       role. (Also: gate-passing "warning yellow" is a dark ochre that no
       longer reads as yellow, and info-blue collides with link colour.)
       ---------------------------------------------------------------------- */
    --danger:  #B3261E;
    --success: #1B7A4B;
    --on-danger:  #ffffff;
    --on-success: #ffffff;

    /* ----------------------------------------------------------------------
       BORDERS — [DEFAULT]
       NOTE the third role. --border and --border-strong are DECORATIVE
       (dividers, card edges) and may be low-contrast by design.
       --border-control is FUNCTIONAL: it draws the edge of an interactive
       control, so WCAG 1.4.11 requires 3:1 against the adjacent surface.
       They must be separate roles — the audited neutral border ramps
       measure ~1.5:1 on white, so a control edge drawn with --border-
       strong would fail while looking correct. Gated by the role check.

       v0.4.0 — DERIVED, and re-declared by every publisher (Pilot 02,
       ticket 19). These were two hand-picked greys declared once at :root, so
       a band with any other fill drew a light-grey divider on it: Pilot 02's
       rbmg-accordion separator measured 1.46:1 worst / 1.54:1 median on its
       red band. Not a contrast failure exactly — --border is decorative on
       purpose — but a divider nobody can see is a divider that is not there,
       and the colour had no relationship to the band at all.

       Deriving from --contrast-toward means the edge is always the band's own
       ink, faded. The percentages were chosen so the derivation reproduces the
       hexes it replaces on a white ground: mix(black, white 90%) is #e6e6e6
       against the old #e5e7ea, and 80% is #cccccc against #ccced6. Same
       validation the hover derivation got in the audit — derive it, then check
       it lands on the value someone had already picked by eye.

       They mix toward --surface rather than toward `transparent` on purpose.
       A translucent border would look identical, and would be UNMEASURABLE:
       the role gate resolves `transparent` as white, so every gate row about a
       translucent colour would report a number the browser never paints.

       THE SEAMS ARE NEW IN v0.4.1 — Pilot 04, E-118, and the ticket was
       smaller than what verifying it found.

       As filed: the inverse publisher hard-codes its border derivation with no
       --border-on-inverse to answer, so a comp wanting a visible rule on a dark
       band (Pilot 04's FAQ draws #DCDFE7, 11.12:1 on its navy) gets the core's
       1.36:1 and has no legal way to say otherwise. `.border-strong` is not the
       escape hatch either — same shape at 80%, ~1.89:1.

       What the grep found: **a brand's answer to --border was already dead.**
       These are documented [DEFAULT] roles a brand may override, and every
       publisher re-declares them — including the one that matches `body`. A
       declaration on body beats an inherited :root value, so a brand answering
       --border at :root has it overridden on the whole document. Pilot 04's
       brand sheet answers #DCDFE7, its read-back lists it as overridden, and
       the page paints the derived #E6E6E6. Nobody noticed, because a decorative
       border being slightly the wrong grey looks like nothing at all.

       So both halves close the same way, and the naming is not invented: the
       core already splits exactly this way for the two other roles a publisher
       owns — --link-on-surface / --link-on-inverse and --icon-on-surface /
       --icon-on-inverse. Borders join that family. A brand answers the SEAM,
       every publisher reads it, and the answer actually paints.

       They stay UNDECLARED, with the fallback at each point of use, because a
       [DEFAULT] declared here and chained to --surface or --surface-inverse
       (both of which a brand overrides) is the --on-inverse failure. The bands
       and gradients keep their own derivation: no build has needed a seam
       there, and this file seeds from measured violations rather than a
       theoretical complete set.

       --border-control IS THE THIRD, and it is the one that mattered most to
       find, because it is the FUNCTIONAL role. Every publisher sets it to
       var(--muted), so a brand answering --border-control at :root had that
       answer overridden too — and unlike the decorative pair, THE GATE WAS
       READING THE DEAD VALUE. Its two rows resolved the brand's :root answer
       while the browser painted --muted: a gate measuring the intent while the
       cascade did something else, which is the failure this whole file exists
       to end, sitting inside the file.

       Note the publisher's override was PROTECTIVE, not merely wrong, and that
       is why the seam splits by ground rather than passing one answer through.
       The dark reference brand answers --border-control with a light grey,
       correct on its dark page ground and about 1.5:1 on its LIGHT inverse
       island — a 1.4.11 failure. One brand-wide answer cannot be right on both
       polarities, which is the same argument that renamed --link-on-light to
       --link-on-surface in v0.2.0.

       MIGRATION, one line per answer: a brand answering --border,
       --border-strong or --border-control renames it to the -on-surface form
       (or -on-inverse, if the answer was ever meant for the dark island).
       Nothing renders differently for the decorative pair — those answers were
       not being painted. --border-control now paints what the brand asked for
       instead of --muted, so its gate rows finally measure the rendered edge;
       both reference brands were re-measured on the seam before this shipped.
       ---------------------------------------------------------------------- */
    --border:         var(--border-on-surface,         color-mix(in srgb, var(--contrast-toward), var(--surface) 90%));
    --border-strong:  var(--border-strong-on-surface,  color-mix(in srgb, var(--contrast-toward), var(--surface) 80%));
    --border-control: var(--border-control-on-surface, var(--muted));  /* functional, 3:1-gated */

    /* ----------------------------------------------------------------------
       RADIUS — [DEFAULT]
       Seeded from the reference brand's three-stop scale. A fully square
       brand zeroes ALL of these in its brand sheet —
       that is the entire mechanism for the square look; no component knows.
       ---------------------------------------------------------------------- */
    --radius-xs:   0.25rem;    /* v0.2.0 — added; pilot needed a 4px stop (chips, small controls) */
    --radius-sm:   0.5rem;
    --radius-md:   0.75rem;    /* was --border-radius-standard */
    --radius-lg:   1rem;       /* was --border-radius-card     */
    --radius-xl:   1.25rem;    /* was --border-radius-large    */
    --radius-pill: 50rem;

    /* ----------------------------------------------------------------------
       ELEVATION — [DEFAULT]
       ---------------------------------------------------------------------- */
    --shadow-sm:       0 0 8px rgba(0, 0, 0, 0.05);
    --shadow-md:       0 0.5rem 1rem rgba(0, 0, 0, 0.15);
    --shadow-dropdown: 0 3rem 3rem rgba(0, 0, 0, 0.175);

    /* ----------------------------------------------------------------------
       MOTION — [DEFAULT]
       Already universal at 0.25s across all seven sites.

       The CURVE was the missing half. Duration was a role; easing was a
       literal — `ease-out` typed into the three rules someone happened to
       notice and omitted from the other ten, which therefore ran the CSS
       default `ease`. Motion was the one value a brand could not answer,
       so "make it feel like us" had nowhere to land.

       Three roles, not one: a thing arriving and a thing leaving want
       different curves, and a single symmetric curve makes one of them feel
       wrong. That is precisely why `ease-out` got hand-written in the first
       place — the split is the generalization of that patch. Duration stays
       single: 0.25s was universal across the estate, and inventing an exit
       duration would be inventing variance no brand has asked for. The exit
       reads quicker because of its curve, not because of a second number.
       ---------------------------------------------------------------------- */
    --transition-standard: 0.25s;
    --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);   /* symmetric — hover, colour, border */
    --ease-enter:    cubic-bezier(0, 0, 0.2, 1);     /* decelerate — arriving, settles into place */
    --ease-exit:     cubic-bezier(0.4, 0, 1, 1);     /* accelerate — leaving, gets out of the way */

    /* ----------------------------------------------------------------------
       STRUCTURE KNOBS — [DEFAULT]
       Value-shaped levers into core structural defaults, so brands adjust
       them without touching structure (per the precedence block).
       ---------------------------------------------------------------------- */
    --heading-transform: uppercase;  /* near-universal treatment; a brand may set none */
    --scroll-offset: 120px;          /* anchor scroll-margin under the fixed header   */

    /* ----------------------------------------------------------------------
       NAVBAR KNOBS — [DEFAULT]
       v0.4.0 — NEW, with the navbar shell (Pilot 02, ticket 2).

       Geometry a brand answers so the shell's STRUCTURE never has to be
       rewritten in site.css. --navbar-fit is the width the header's content
       must not be squeezed below: Pilot 02's needed 1420px and a .container
       capped it at 1320 at every width, which crushed the brand mark 46.5% on
       every desktop. Leave slack in it — a header that fits to the pixel
       breaks on a longer phone number.

       --z-navbar exists because a third-party chat widget is the one thing
       that genuinely has to be layered against a site's header, and the
       alternative is an !important in site.css. It sits under the drawer
       (1050) and the modal (1060) on purpose.

       THREE MORE SEAMS IN v0.4.1, all Pilot 04, and all UNDECLARED with their
       fallback at the point of use — see core.components.css for each:

         --navbar-shadow            E-076. The core supports a TRANSPARENT
                                    header in JS (core.behaviors.js grew
                                    data-hero-pad for exactly that case and
                                    documents it at length) and .navbar
                                    hard-coded box-shadow: var(--shadow-md), so
                                    the only way to build the thing the
                                    behaviour exists to serve was to override a
                                    core declaration. Half that ticket
                                    dissolved on reading the rule: .navbar sets
                                    NO background-color at all, so a
                                    transparent header is already expressible
                                    by simply not composing a .bg-* utility
                                    onto the shell. Only the shadow was stuck.

         --navbar-utility-display   E-077. See the shrink hook.

         --navbar-pad-x             E-082. The full-bleed inset existed only as
                                    an inline clamp() on .navbar-inner, so
                                    rbmg-dropdown[mega] — which sets itself
                                    position: static and therefore cannot
                                    inherit that padding — had no way to align
                                    its panel with the nav links above it.
                                    Pilot 04 restated the clamp verbatim in
                                    site.css and labelled it as duplication.
                                    It is declared ON .navbar rather than here,
                                    because it is per-header geometry a section
                                    may rebind, not a page-level value.
       ---------------------------------------------------------------------- */
    --navbar-fit:                 1460px;
    --navbar-inset:               5rem;
    --navbar-brand-width:         12rem;
    --navbar-brand-width-shrunk:  7rem;
    --z-navbar:                   1040;

    /* ----------------------------------------------------------------------
       FONT ROLES
       [REQUIRED] the brand sheet answers: --font-heading --font-body
                  --font-label
       [DEFAULT]  --font-overline falls back to --font-label (v0.3.0,
                  Pilot 01 F-008). A brand whose eyebrow is the label
                  family answers nothing. A brand whose eyebrow is a
                  different voice — the pilot's was a black extended
                  italic SERIF against a black SANS label, in every
                  section — answers one line instead of logging the
                  section unbuildable.

       The overline style knobs are value-shaped levers into core
       structure, the same device as --heading-transform. They live here
       rather than in a site sheet because "the eyebrow is italic" is a
       brand fact, and site.css restyling a core utility is the failure
       mode this project exists to prevent:

         --overline-weight     default --font-weight-black
         --overline-style      default normal   (a brand may set italic)
         --overline-tracking   default normal
         --overline-transform  default uppercase
       ---------------------------------------------------------------------- */

    /* ----------------------------------------------------------------------
       WEIGHTS — [DEFAULT]
       ---------------------------------------------------------------------- */
    --font-weight-black:    900;
    --font-weight-bold:     700;
    --font-weight-semibold: 600;
    --font-weight-medium:   500;
    --font-weight-regular:  400;

    /* ----------------------------------------------------------------------
       TYPE SCALE — [DEFAULT] (locked decision: the audited fluid model is
       the core scale, so it moves OUT of brand sheets entirely — ~35
       lines leave every brand.css)
       Sizes calibrated at a 1600px viewport (Figma reference); clamp()
       floor = mobile minimum; slope scales linearly 375px → 1600px.
       ---------------------------------------------------------------------- */
    --fs-display-lg: clamp(2rem, calc(1.541rem + 1.959vw), 3.5rem);    /* 32 → 56 */
    --lh-display-lg: 1.143;
    --fs-display-sm: clamp(1.75rem, calc(1.367rem + 1.633vw), 3rem);   /* 28 → 48 */
    --lh-display-sm: 1.167;
    --fs-h1: clamp(1.625rem, calc(1.319rem + 1.306vw), 2.625rem);      /* 26 → 42 */
    --lh-h1: 1.190;
    --fs-h2: clamp(1.375rem, calc(1.146rem + 0.98vw), 2.125rem);       /* 22 → 34 */
    --lh-h2: 1.235;
    --fs-h3: clamp(1.25rem, calc(1.097rem + 0.653vw), 1.75rem);        /* 20 → 28 */
    --lh-h3: 1.286;
    --fs-h4: clamp(1.125rem, calc(1.049rem + 0.327vw), 1.375rem);      /* 18 → 22 */
    --lh-h4: 1.364;
    --fs-h5: clamp(1.125rem, calc(1.087rem + 0.163vw), 1.25rem);       /* 18 → 20 */
    --lh-h5: 1.4;
    --fs-p-lg: clamp(1rem, calc(0.962rem + 0.163vw), 1.125rem);        /* 16 → 18 */
    --lh-p-lg: 1.25;
    --fs-p-md: 1rem;            --lh-p-md: 1.5;
    --fs-p-sm: 0.875rem;        --lh-p-sm: 1.571;
    --fs-p-xs: 0.75rem;         --lh-p-xs: 1.667;
    --fs-label-lg: 1rem;        --lh-label-lg: 1.125;
    --fs-label-md: 0.875rem;    --lh-label-md: 1.143;
    --fs-label-sm: 0.75rem;     --lh-label-sm: 1.167;
    --fs-label-xs: 0.625rem;    --lh-label-xs: 1.2;
    --fs-overline-lg: 0.875rem; --lh-overline-lg: 1.429;
    --fs-overline-sm: 0.75rem;  --lh-overline-sm: 1.667;

    /* ----------------------------------------------------------------------
       ICON SIZE — [DEFAULT]
       v0.4.0 — NEW (Pilot 02, ticket 8). TEN live instances, and every one
       was a `raw-token-value` linter finding that got JUSTIFIED rather than
       fixed — which is the tell that the vocabulary was missing rather than
       the build being sloppy.

       Every --fs-* above is a TEXT role. Binding a decorative glyph to
       --fs-h3 because the numbers happen to match is the --on-inverse-as-a-
       fill mistake one level over: it asserts a relationship the design never
       made, and it breaks the moment a brand retunes its heading scale — the
       glyph moves with a heading it has nothing to do with.

       So an icon gets its own scale. This is a SEPARATE ladder from
       .icon-stacked's size modifiers, which size the backdrop BOX; this sizes
       the glyph itself.

       The defaults cover what the pilot's comp actually used (16, 20, 24, 36).
       A brand whose comp draws a 36px glyph answers --icon-xl and stops
       writing a literal in site.css, which is the whole point.

       v0.4.1 — A SIXTH STOP (Pilot 04, E-110), and the note above is the thing
       it corrects. "A brand answers --icon-xl and stops" holds only while a
       comp uses one size above 24. Pilot 04 uses two: 36 on the About chips and
       the Opener pennants (measured off the comp's own raster as a red-pixel
       bounding box, not read off a declared value) and 47–48 on the Financing
       benefits. --icon-xl was already answered at 36 because two sections need
       it there, so the fifth benefit's glyph shipped 12px small.

       A brand may ANSWER a role and may not INVENT one, so --icon-2xl in a
       brand sheet would be introducing structure at the value tier and there
       was no compliant workaround at all. This is not a wrong default: it is a
       scale one stop short, which only shows on a brand using more of the range
       than the reference brands do. */
    --icon-xs:  0.875rem;   /* 14 */
    --icon-sm:  1rem;       /* 16 */
    --icon-md:  1.25rem;    /* 20 */
    --icon-lg:  1.5rem;     /* 24 */
    --icon-xl:  2.25rem;    /* 36 — the pilot's recurring glyph size */
    --icon-2xl: 3rem;       /* 48 */
  }
}

/* ==========================================================================
   ROLE-SATISFACTION CHECK (run at brand-sheet time; see tier map §handoff)
   --------------------------------------------------------------------------
   Walk the roles above. Every [REQUIRED] role resolves to a Tier 0A
   primitive. Every [DEFAULT] role is either overridden or listed as taken.
   No [DERIVED] role appears in the brand sheet at all.
   Confirm back in ≤10 lines: the brand, and every role taking a core
   default, with what that means visually.
   ========================================================================== */

/* ---------- core.foundation.css ---------- */
/* ==========================================================================
   RBMG Core — foundation (layer order, reset, base, grid, utilities)
   core.foundation.css · v0.1.0-draft · GENERATED by tools/build-foundation.py
   DO NOT EDIT BY HAND — a local edit to this file is a defect, not a fork.

   Public API contract (locked, tier map §Layout): Bootstrap's exact
   breakpoints (576/768/992/1200/1400), container max-widths
   (540/720/960/1140/1320), 12-column math, and unprefixed Bootstrap-
   compatible class names, so figma-build's Appendix A, the srcset
   pipeline, patterns.md, and the team's habits transfer unedited.
   Internal implementation is free — and differs where Bootstrap's had a
   known failure (see the .row gutter clamp).

   Deliberately not carried in this draft (flagged, not silent):
   `.container-{bp}` responsive-container variants — pending a usage check
   across the seven sites before they earn generation.
   ========================================================================== */

/* The layer order lives in css/core.layers.css, which MUST load first.
   It is NOT declared here: a layer's position is fixed when its name is
   first seen, so declaring the order in this file would make the cascade
   depend on whether foundation happens to load before the token sheet. */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
  h1, h2, h3, h4, h5, h6, p, figure, blockquote { margin: 0 0 1rem; }
  ul, ol { margin: 0 0 1rem; padding-left: 2rem; }
  img, svg, video { vertical-align: middle; max-width: 100%; }
  /* TICKET 13 — the reset half-implemented fluid images for three versions.
     `max-width: 100%` with no `height: auto` distorts any <img> carrying the
     width and height ATTRIBUTES the SOP requires for CLS: the attributes set a
     used height, max-width clamps the width below its attribute value, and
     nothing recomputes the height. Pilot 02 hit it 3+ times and worked around
     it locally each time; its brand mark was squashed 46.5% by the same shape
     one component over. `height: auto` restores the intrinsic ratio, and a
     component that genuinely wants a fixed height sets one in a later layer.
     img ONLY — an <svg> with width/height attributes and no viewBox has no
     ratio to restore, so `height: auto` would collapse it. */
  img { height: auto; }
  button, input, select, textarea { font: inherit; color: inherit; }
  /* Shell-inheritance patterns, promoted to deliberate core rules
     (were `!important` hacks on every site; @layer makes that vestigial): */
  em, u { font-style: inherit; text-decoration: inherit; }
  /* `a { text-decoration: none }` LIVED HERE THROUGH v0.3.1 and it was a core
     defect wearing a shell-inheritance hat. It is now an affordance in
     @layer base — see the note on the `a` rule below. */
  [id] { scroll-margin-top: var(--scroll-offset, 120px); } /* fixed-header anchor offset */
}
@layer base {
  body {
    font-family: var(--font-body);
    font-size: var(--fs-p-md);
    line-height: var(--lh-p-md);
    font-weight: var(--font-weight-regular);
    color: var(--on-surface);
    background-color: var(--surface);
    /* Keeps a stray wide child from producing a horizontal scrollbar.
       WARNING (Pilot 01, F-017): this also makes scrollWidth incapable of
       exceeding clientWidth, so a "no horizontal scroll" check measured
       on the document passes UNCONDITIONALLY. It hid a real defect during
       the pilot — a header ending at 1221px in a 1200px viewport with the
       phone CTA clipped and unreachable, while scrollWidth reported a
       clean 1200. Keep the clip; do NOT let it double as the test. The
       acceptance check is per-element: no element's right edge may cross
       the viewport edge. See CLAUDE.md. */
    overflow-x: clip;
  }
  h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-bold);
    text-transform: var(--heading-transform, uppercase); /* core default; brand overrides the token */
  }
  h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); }
  h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); }
  h3 { font-size: var(--fs-h3); line-height: var(--lh-h3); }
  h4 { font-size: var(--fs-h4); line-height: var(--lh-h4); }
  h5 { font-size: var(--fs-h5); line-height: var(--lh-h5); }
  /* Surface-aware consumer: bare links read the nearest published pair.

     THE UNDERLINE IS THE AFFORDANCE, and it is here because Pilot 02 proved
     colour cannot be (ticket 20). v0.1.0-v0.3.1 reset `text-decoration: none`
     globally and distinguished a link by colour alone. Measured on the pilot,
     that fails on EVERY surface at once:

       · .bg-gradient / -primary / -secondary / -accent publish --link as the
         same value as their own text colour, by construction — a link on a
         brand band is the band's on-colour, so it is the body copy's colour.
       · The PAGE GROUND does it too, and that one is not a brand mistake. On
         the pilot's #0F222E ground its lead hue measures 2.69:1, so a brand
         answering --link-on-surface with anything legible answers it with the
         body text colour. NO COLOUR EXISTS that is both legible on a dark
         ground and distinct from the copy on it.

     A DARK-GROUND BRAND THEREFORE CANNOT SATISFY SC 1.4.1 THROUGH --link AT
     ALL. The pilot papered over it with a four-selector underline list in
     site.css and the list kept growing — which was the argument, not the fix.

     Underline-by-default is the same move the focus ring made in v0.2.0, for
     the same reason: an accessibility property that every site has to
     remember to install is one this core exists to retire. It inverts the
     FAILURE POLARITY, which is the whole point. Before, forgetting produced an
     invisible link nobody notices. Now, forgetting produces an unwanted
     underline on a nav item, which a design review catches in one look. Loud
     beats silent.

     Everything the core knows is a CONTROL rather than prose opts out in its
     own component sheet (.btn, the dropdown and drawer menus, .breadcrumb).
     Everything a SITE owns opts out with .link-bare, which is greppable and
     reviewable — and the rendered gate refuses it on a link in running text,
     so the escape hatch cannot be used to reopen the hole. */
  a {
    color: var(--link);
    text-decoration-line: underline;
    text-decoration-thickness: var(--link-underline-thickness, 0.0625em);
    text-underline-offset: var(--link-underline-offset, 0.15em);
    transition: color var(--transition-standard) var(--ease-standard),
                text-decoration-thickness var(--transition-standard) var(--ease-standard);
  }
  /* Hover thickens the rule as well as moving the colour, and the thickening
     is the part that always works. --link-hover mixes 20% toward
     --contrast-toward; on a surface where --link ALREADY equals that value —
     every brand band, and a dark ground's own copy colour — the mix is
     identity and the colour hover is a NO-OP. That is ticket 1, and it is
     invisible to a threshold gate because both endpoints clear 4.5:1. A
     geometry change cannot be a no-op at any polarity. Derived from the rest
     thickness rather than a second role, so a brand retuning one gets both
     and cannot flatten them into each other. */
  a:hover, a:focus-visible {
    color: var(--link-hover);
    text-decoration-thickness: calc(var(--link-underline-thickness, 0.0625em) * 2);
  }

  /* GLOBAL FOCUS RING (v0.2.0).
     Pilot 01: :focus-visible was styled only on .btn, form controls and
     two overlay internals. Nav toggles, tabs, accordion toggles and
     footer links all fell back to the browser default (#005FCC), which
     measured 2.72:1 on a dark-ground brand — under WCAG 2.4.11's 3:1.
     A site-authored focus ring is exactly the per-project accessibility
     install this core exists to retire.
     :where() keeps specificity at 0 so components still override, and
     --focus-ring follows the surface's own contrast direction, so it is
     correct at any polarity by construction. */
  :where(:focus-visible) {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
  }
}
@layer utilities {
  /* ---- Containers ------------------------------------------------ */
  .container, .container-fluid {
    --container-pad-x: .75rem;
    width: 100%;
    padding-inline: var(--container-pad-x);
    margin-inline: auto;
  }
  @media (min-width: 576px) { .container { max-width: 540px; } }
  @media (min-width: 768px) { .container { max-width: 720px; } }
  @media (min-width: 992px) { .container { max-width: 960px; } }
  @media (min-width: 1200px) { .container { max-width: 1140px; } }
  @media (min-width: 1400px) { .container { max-width: 1320px; } }

  /* ---- Row / columns ---------------------------------------------
     Same public model as Bootstrap (negative-margin gutters), one fix:
     the row's negative margin is CLAMPED to the container's padding, so
     a gutter wider than the container pad (the g-5 failure) widens the
     content inset instead of causing horizontal scroll. This is the
     acceptance test from the tier map, passing by construction. */
  .row {
    --gutter-x: 1.5rem;
    --gutter-y: 0;
    display: flex;
    flex-wrap: wrap;
    margin-top: calc(-1 * var(--gutter-y));
    margin-inline: max(calc(-.5 * var(--gutter-x)), calc(-1 * var(--container-pad-x, .75rem)));
  }
  .row > * {
    flex-shrink: 0;
    width: 100%;
    max-width: 100%;
    padding-inline: calc(var(--gutter-x) * .5);
    margin-top: var(--gutter-y);
  }

  /* ---- 12-column system, responsive ------------------------------ */
  .col { flex: 1 0 0%; }
  .col-auto { flex: 0 0 auto; width: auto; }
  .col-1 { flex: 0 0 auto; width: 8.333333%; }
  .col-2 { flex: 0 0 auto; width: 16.666667%; }
  .col-3 { flex: 0 0 auto; width: 25%; }
  .col-4 { flex: 0 0 auto; width: 33.333333%; }
  .col-5 { flex: 0 0 auto; width: 41.666667%; }
  .col-6 { flex: 0 0 auto; width: 50%; }
  .col-7 { flex: 0 0 auto; width: 58.333333%; }
  .col-8 { flex: 0 0 auto; width: 66.666667%; }
  .col-9 { flex: 0 0 auto; width: 75%; }
  .col-10 { flex: 0 0 auto; width: 83.333333%; }
  .col-11 { flex: 0 0 auto; width: 91.666667%; }
  .col-12 { flex: 0 0 auto; width: 100%; }
  .offset-0 { margin-inline-start: 0%; }
  .offset-1 { margin-inline-start: 8.333333%; }
  .offset-2 { margin-inline-start: 16.666667%; }
  .offset-3 { margin-inline-start: 25%; }
  .offset-4 { margin-inline-start: 33.333333%; }
  .offset-5 { margin-inline-start: 41.666667%; }
  .offset-6 { margin-inline-start: 50%; }
  .offset-7 { margin-inline-start: 58.333333%; }
  .offset-8 { margin-inline-start: 66.666667%; }
  .offset-9 { margin-inline-start: 75%; }
  .offset-10 { margin-inline-start: 83.333333%; }
  .offset-11 { margin-inline-start: 91.666667%; }
  .row-cols-auto > * { flex: 0 0 auto; width: auto; }
  .row-cols-1 > * { flex: 0 0 auto; width: 100%; }
  .row-cols-2 > * { flex: 0 0 auto; width: 50%; }
  .row-cols-3 > * { flex: 0 0 auto; width: 33.333333%; }
  .row-cols-4 > * { flex: 0 0 auto; width: 25%; }
  .row-cols-5 > * { flex: 0 0 auto; width: 20%; }
  .row-cols-6 > * { flex: 0 0 auto; width: 16.666667%; }
  .g-0, .gx-0 { --gutter-x: 0; }
  .g-0, .gy-0 { --gutter-y: 0; }
  .g-1, .gx-1 { --gutter-x: .25rem; }
  .g-1, .gy-1 { --gutter-y: .25rem; }
  .g-2, .gx-2 { --gutter-x: .5rem; }
  .g-2, .gy-2 { --gutter-y: .5rem; }
  .g-3, .gx-3 { --gutter-x: 1rem; }
  .g-3, .gy-3 { --gutter-y: 1rem; }
  .g-4, .gx-4 { --gutter-x: 1.5rem; }
  .g-4, .gy-4 { --gutter-y: 1.5rem; }
  .g-5, .gx-5 { --gutter-x: 3rem; }
  .g-5, .gy-5 { --gutter-y: 3rem; }
  .g-6, .gx-6 { --gutter-x: 4rem; }
  .g-6, .gy-6 { --gutter-y: 4rem; }
  .g-7, .gx-7 { --gutter-x: 5rem; }
  .g-7, .gy-7 { --gutter-y: 5rem; }
  .order-first { order: -1; }
  .order-0 { order: 0; }
  .order-1 { order: 1; }
  .order-2 { order: 2; }
  .order-3 { order: 3; }
  .order-4 { order: 4; }
  .order-5 { order: 5; }
  .order-last { order: 6; }
  @media (min-width: 576px) {
    .col-sm { flex: 1 0 0%; }
    .col-sm-auto { flex: 0 0 auto; width: auto; }
    .col-sm-1 { flex: 0 0 auto; width: 8.333333%; }
    .col-sm-2 { flex: 0 0 auto; width: 16.666667%; }
    .col-sm-3 { flex: 0 0 auto; width: 25%; }
    .col-sm-4 { flex: 0 0 auto; width: 33.333333%; }
    .col-sm-5 { flex: 0 0 auto; width: 41.666667%; }
    .col-sm-6 { flex: 0 0 auto; width: 50%; }
    .col-sm-7 { flex: 0 0 auto; width: 58.333333%; }
    .col-sm-8 { flex: 0 0 auto; width: 66.666667%; }
    .col-sm-9 { flex: 0 0 auto; width: 75%; }
    .col-sm-10 { flex: 0 0 auto; width: 83.333333%; }
    .col-sm-11 { flex: 0 0 auto; width: 91.666667%; }
    .col-sm-12 { flex: 0 0 auto; width: 100%; }
    .offset-sm-0 { margin-inline-start: 0%; }
    .offset-sm-1 { margin-inline-start: 8.333333%; }
    .offset-sm-2 { margin-inline-start: 16.666667%; }
    .offset-sm-3 { margin-inline-start: 25%; }
    .offset-sm-4 { margin-inline-start: 33.333333%; }
    .offset-sm-5 { margin-inline-start: 41.666667%; }
    .offset-sm-6 { margin-inline-start: 50%; }
    .offset-sm-7 { margin-inline-start: 58.333333%; }
    .offset-sm-8 { margin-inline-start: 66.666667%; }
    .offset-sm-9 { margin-inline-start: 75%; }
    .offset-sm-10 { margin-inline-start: 83.333333%; }
    .offset-sm-11 { margin-inline-start: 91.666667%; }
    .row-cols-sm-auto > * { flex: 0 0 auto; width: auto; }
    .row-cols-sm-1 > * { flex: 0 0 auto; width: 100%; }
    .row-cols-sm-2 > * { flex: 0 0 auto; width: 50%; }
    .row-cols-sm-3 > * { flex: 0 0 auto; width: 33.333333%; }
    .row-cols-sm-4 > * { flex: 0 0 auto; width: 25%; }
    .row-cols-sm-5 > * { flex: 0 0 auto; width: 20%; }
    .row-cols-sm-6 > * { flex: 0 0 auto; width: 16.666667%; }
    .g-sm-0, .gx-sm-0 { --gutter-x: 0; }
    .g-sm-0, .gy-sm-0 { --gutter-y: 0; }
    .g-sm-1, .gx-sm-1 { --gutter-x: .25rem; }
    .g-sm-1, .gy-sm-1 { --gutter-y: .25rem; }
    .g-sm-2, .gx-sm-2 { --gutter-x: .5rem; }
    .g-sm-2, .gy-sm-2 { --gutter-y: .5rem; }
    .g-sm-3, .gx-sm-3 { --gutter-x: 1rem; }
    .g-sm-3, .gy-sm-3 { --gutter-y: 1rem; }
    .g-sm-4, .gx-sm-4 { --gutter-x: 1.5rem; }
    .g-sm-4, .gy-sm-4 { --gutter-y: 1.5rem; }
    .g-sm-5, .gx-sm-5 { --gutter-x: 3rem; }
    .g-sm-5, .gy-sm-5 { --gutter-y: 3rem; }
    .g-sm-6, .gx-sm-6 { --gutter-x: 4rem; }
    .g-sm-6, .gy-sm-6 { --gutter-y: 4rem; }
    .g-sm-7, .gx-sm-7 { --gutter-x: 5rem; }
    .g-sm-7, .gy-sm-7 { --gutter-y: 5rem; }
    .order-sm-first { order: -1; }
    .order-sm-0 { order: 0; }
    .order-sm-1 { order: 1; }
    .order-sm-2 { order: 2; }
    .order-sm-3 { order: 3; }
    .order-sm-4 { order: 4; }
    .order-sm-5 { order: 5; }
    .order-sm-last { order: 6; }
  }
  @media (min-width: 768px) {
    .col-md { flex: 1 0 0%; }
    .col-md-auto { flex: 0 0 auto; width: auto; }
    .col-md-1 { flex: 0 0 auto; width: 8.333333%; }
    .col-md-2 { flex: 0 0 auto; width: 16.666667%; }
    .col-md-3 { flex: 0 0 auto; width: 25%; }
    .col-md-4 { flex: 0 0 auto; width: 33.333333%; }
    .col-md-5 { flex: 0 0 auto; width: 41.666667%; }
    .col-md-6 { flex: 0 0 auto; width: 50%; }
    .col-md-7 { flex: 0 0 auto; width: 58.333333%; }
    .col-md-8 { flex: 0 0 auto; width: 66.666667%; }
    .col-md-9 { flex: 0 0 auto; width: 75%; }
    .col-md-10 { flex: 0 0 auto; width: 83.333333%; }
    .col-md-11 { flex: 0 0 auto; width: 91.666667%; }
    .col-md-12 { flex: 0 0 auto; width: 100%; }
    .offset-md-0 { margin-inline-start: 0%; }
    .offset-md-1 { margin-inline-start: 8.333333%; }
    .offset-md-2 { margin-inline-start: 16.666667%; }
    .offset-md-3 { margin-inline-start: 25%; }
    .offset-md-4 { margin-inline-start: 33.333333%; }
    .offset-md-5 { margin-inline-start: 41.666667%; }
    .offset-md-6 { margin-inline-start: 50%; }
    .offset-md-7 { margin-inline-start: 58.333333%; }
    .offset-md-8 { margin-inline-start: 66.666667%; }
    .offset-md-9 { margin-inline-start: 75%; }
    .offset-md-10 { margin-inline-start: 83.333333%; }
    .offset-md-11 { margin-inline-start: 91.666667%; }
    .row-cols-md-auto > * { flex: 0 0 auto; width: auto; }
    .row-cols-md-1 > * { flex: 0 0 auto; width: 100%; }
    .row-cols-md-2 > * { flex: 0 0 auto; width: 50%; }
    .row-cols-md-3 > * { flex: 0 0 auto; width: 33.333333%; }
    .row-cols-md-4 > * { flex: 0 0 auto; width: 25%; }
    .row-cols-md-5 > * { flex: 0 0 auto; width: 20%; }
    .row-cols-md-6 > * { flex: 0 0 auto; width: 16.666667%; }
    .g-md-0, .gx-md-0 { --gutter-x: 0; }
    .g-md-0, .gy-md-0 { --gutter-y: 0; }
    .g-md-1, .gx-md-1 { --gutter-x: .25rem; }
    .g-md-1, .gy-md-1 { --gutter-y: .25rem; }
    .g-md-2, .gx-md-2 { --gutter-x: .5rem; }
    .g-md-2, .gy-md-2 { --gutter-y: .5rem; }
    .g-md-3, .gx-md-3 { --gutter-x: 1rem; }
    .g-md-3, .gy-md-3 { --gutter-y: 1rem; }
    .g-md-4, .gx-md-4 { --gutter-x: 1.5rem; }
    .g-md-4, .gy-md-4 { --gutter-y: 1.5rem; }
    .g-md-5, .gx-md-5 { --gutter-x: 3rem; }
    .g-md-5, .gy-md-5 { --gutter-y: 3rem; }
    .g-md-6, .gx-md-6 { --gutter-x: 4rem; }
    .g-md-6, .gy-md-6 { --gutter-y: 4rem; }
    .g-md-7, .gx-md-7 { --gutter-x: 5rem; }
    .g-md-7, .gy-md-7 { --gutter-y: 5rem; }
    .order-md-first { order: -1; }
    .order-md-0 { order: 0; }
    .order-md-1 { order: 1; }
    .order-md-2 { order: 2; }
    .order-md-3 { order: 3; }
    .order-md-4 { order: 4; }
    .order-md-5 { order: 5; }
    .order-md-last { order: 6; }
  }
  @media (min-width: 992px) {
    .col-lg { flex: 1 0 0%; }
    .col-lg-auto { flex: 0 0 auto; width: auto; }
    .col-lg-1 { flex: 0 0 auto; width: 8.333333%; }
    .col-lg-2 { flex: 0 0 auto; width: 16.666667%; }
    .col-lg-3 { flex: 0 0 auto; width: 25%; }
    .col-lg-4 { flex: 0 0 auto; width: 33.333333%; }
    .col-lg-5 { flex: 0 0 auto; width: 41.666667%; }
    .col-lg-6 { flex: 0 0 auto; width: 50%; }
    .col-lg-7 { flex: 0 0 auto; width: 58.333333%; }
    .col-lg-8 { flex: 0 0 auto; width: 66.666667%; }
    .col-lg-9 { flex: 0 0 auto; width: 75%; }
    .col-lg-10 { flex: 0 0 auto; width: 83.333333%; }
    .col-lg-11 { flex: 0 0 auto; width: 91.666667%; }
    .col-lg-12 { flex: 0 0 auto; width: 100%; }
    .offset-lg-0 { margin-inline-start: 0%; }
    .offset-lg-1 { margin-inline-start: 8.333333%; }
    .offset-lg-2 { margin-inline-start: 16.666667%; }
    .offset-lg-3 { margin-inline-start: 25%; }
    .offset-lg-4 { margin-inline-start: 33.333333%; }
    .offset-lg-5 { margin-inline-start: 41.666667%; }
    .offset-lg-6 { margin-inline-start: 50%; }
    .offset-lg-7 { margin-inline-start: 58.333333%; }
    .offset-lg-8 { margin-inline-start: 66.666667%; }
    .offset-lg-9 { margin-inline-start: 75%; }
    .offset-lg-10 { margin-inline-start: 83.333333%; }
    .offset-lg-11 { margin-inline-start: 91.666667%; }
    .row-cols-lg-auto > * { flex: 0 0 auto; width: auto; }
    .row-cols-lg-1 > * { flex: 0 0 auto; width: 100%; }
    .row-cols-lg-2 > * { flex: 0 0 auto; width: 50%; }
    .row-cols-lg-3 > * { flex: 0 0 auto; width: 33.333333%; }
    .row-cols-lg-4 > * { flex: 0 0 auto; width: 25%; }
    .row-cols-lg-5 > * { flex: 0 0 auto; width: 20%; }
    .row-cols-lg-6 > * { flex: 0 0 auto; width: 16.666667%; }
    .g-lg-0, .gx-lg-0 { --gutter-x: 0; }
    .g-lg-0, .gy-lg-0 { --gutter-y: 0; }
    .g-lg-1, .gx-lg-1 { --gutter-x: .25rem; }
    .g-lg-1, .gy-lg-1 { --gutter-y: .25rem; }
    .g-lg-2, .gx-lg-2 { --gutter-x: .5rem; }
    .g-lg-2, .gy-lg-2 { --gutter-y: .5rem; }
    .g-lg-3, .gx-lg-3 { --gutter-x: 1rem; }
    .g-lg-3, .gy-lg-3 { --gutter-y: 1rem; }
    .g-lg-4, .gx-lg-4 { --gutter-x: 1.5rem; }
    .g-lg-4, .gy-lg-4 { --gutter-y: 1.5rem; }
    .g-lg-5, .gx-lg-5 { --gutter-x: 3rem; }
    .g-lg-5, .gy-lg-5 { --gutter-y: 3rem; }
    .g-lg-6, .gx-lg-6 { --gutter-x: 4rem; }
    .g-lg-6, .gy-lg-6 { --gutter-y: 4rem; }
    .g-lg-7, .gx-lg-7 { --gutter-x: 5rem; }
    .g-lg-7, .gy-lg-7 { --gutter-y: 5rem; }
    .order-lg-first { order: -1; }
    .order-lg-0 { order: 0; }
    .order-lg-1 { order: 1; }
    .order-lg-2 { order: 2; }
    .order-lg-3 { order: 3; }
    .order-lg-4 { order: 4; }
    .order-lg-5 { order: 5; }
    .order-lg-last { order: 6; }
  }
  @media (min-width: 1200px) {
    .col-xl { flex: 1 0 0%; }
    .col-xl-auto { flex: 0 0 auto; width: auto; }
    .col-xl-1 { flex: 0 0 auto; width: 8.333333%; }
    .col-xl-2 { flex: 0 0 auto; width: 16.666667%; }
    .col-xl-3 { flex: 0 0 auto; width: 25%; }
    .col-xl-4 { flex: 0 0 auto; width: 33.333333%; }
    .col-xl-5 { flex: 0 0 auto; width: 41.666667%; }
    .col-xl-6 { flex: 0 0 auto; width: 50%; }
    .col-xl-7 { flex: 0 0 auto; width: 58.333333%; }
    .col-xl-8 { flex: 0 0 auto; width: 66.666667%; }
    .col-xl-9 { flex: 0 0 auto; width: 75%; }
    .col-xl-10 { flex: 0 0 auto; width: 83.333333%; }
    .col-xl-11 { flex: 0 0 auto; width: 91.666667%; }
    .col-xl-12 { flex: 0 0 auto; width: 100%; }
    .offset-xl-0 { margin-inline-start: 0%; }
    .offset-xl-1 { margin-inline-start: 8.333333%; }
    .offset-xl-2 { margin-inline-start: 16.666667%; }
    .offset-xl-3 { margin-inline-start: 25%; }
    .offset-xl-4 { margin-inline-start: 33.333333%; }
    .offset-xl-5 { margin-inline-start: 41.666667%; }
    .offset-xl-6 { margin-inline-start: 50%; }
    .offset-xl-7 { margin-inline-start: 58.333333%; }
    .offset-xl-8 { margin-inline-start: 66.666667%; }
    .offset-xl-9 { margin-inline-start: 75%; }
    .offset-xl-10 { margin-inline-start: 83.333333%; }
    .offset-xl-11 { margin-inline-start: 91.666667%; }
    .row-cols-xl-auto > * { flex: 0 0 auto; width: auto; }
    .row-cols-xl-1 > * { flex: 0 0 auto; width: 100%; }
    .row-cols-xl-2 > * { flex: 0 0 auto; width: 50%; }
    .row-cols-xl-3 > * { flex: 0 0 auto; width: 33.333333%; }
    .row-cols-xl-4 > * { flex: 0 0 auto; width: 25%; }
    .row-cols-xl-5 > * { flex: 0 0 auto; width: 20%; }
    .row-cols-xl-6 > * { flex: 0 0 auto; width: 16.666667%; }
    .g-xl-0, .gx-xl-0 { --gutter-x: 0; }
    .g-xl-0, .gy-xl-0 { --gutter-y: 0; }
    .g-xl-1, .gx-xl-1 { --gutter-x: .25rem; }
    .g-xl-1, .gy-xl-1 { --gutter-y: .25rem; }
    .g-xl-2, .gx-xl-2 { --gutter-x: .5rem; }
    .g-xl-2, .gy-xl-2 { --gutter-y: .5rem; }
    .g-xl-3, .gx-xl-3 { --gutter-x: 1rem; }
    .g-xl-3, .gy-xl-3 { --gutter-y: 1rem; }
    .g-xl-4, .gx-xl-4 { --gutter-x: 1.5rem; }
    .g-xl-4, .gy-xl-4 { --gutter-y: 1.5rem; }
    .g-xl-5, .gx-xl-5 { --gutter-x: 3rem; }
    .g-xl-5, .gy-xl-5 { --gutter-y: 3rem; }
    .g-xl-6, .gx-xl-6 { --gutter-x: 4rem; }
    .g-xl-6, .gy-xl-6 { --gutter-y: 4rem; }
    .g-xl-7, .gx-xl-7 { --gutter-x: 5rem; }
    .g-xl-7, .gy-xl-7 { --gutter-y: 5rem; }
    .order-xl-first { order: -1; }
    .order-xl-0 { order: 0; }
    .order-xl-1 { order: 1; }
    .order-xl-2 { order: 2; }
    .order-xl-3 { order: 3; }
    .order-xl-4 { order: 4; }
    .order-xl-5 { order: 5; }
    .order-xl-last { order: 6; }
  }
  @media (min-width: 1400px) {
    .col-xxl { flex: 1 0 0%; }
    .col-xxl-auto { flex: 0 0 auto; width: auto; }
    .col-xxl-1 { flex: 0 0 auto; width: 8.333333%; }
    .col-xxl-2 { flex: 0 0 auto; width: 16.666667%; }
    .col-xxl-3 { flex: 0 0 auto; width: 25%; }
    .col-xxl-4 { flex: 0 0 auto; width: 33.333333%; }
    .col-xxl-5 { flex: 0 0 auto; width: 41.666667%; }
    .col-xxl-6 { flex: 0 0 auto; width: 50%; }
    .col-xxl-7 { flex: 0 0 auto; width: 58.333333%; }
    .col-xxl-8 { flex: 0 0 auto; width: 66.666667%; }
    .col-xxl-9 { flex: 0 0 auto; width: 75%; }
    .col-xxl-10 { flex: 0 0 auto; width: 83.333333%; }
    .col-xxl-11 { flex: 0 0 auto; width: 91.666667%; }
    .col-xxl-12 { flex: 0 0 auto; width: 100%; }
    .offset-xxl-0 { margin-inline-start: 0%; }
    .offset-xxl-1 { margin-inline-start: 8.333333%; }
    .offset-xxl-2 { margin-inline-start: 16.666667%; }
    .offset-xxl-3 { margin-inline-start: 25%; }
    .offset-xxl-4 { margin-inline-start: 33.333333%; }
    .offset-xxl-5 { margin-inline-start: 41.666667%; }
    .offset-xxl-6 { margin-inline-start: 50%; }
    .offset-xxl-7 { margin-inline-start: 58.333333%; }
    .offset-xxl-8 { margin-inline-start: 66.666667%; }
    .offset-xxl-9 { margin-inline-start: 75%; }
    .offset-xxl-10 { margin-inline-start: 83.333333%; }
    .offset-xxl-11 { margin-inline-start: 91.666667%; }
    .row-cols-xxl-auto > * { flex: 0 0 auto; width: auto; }
    .row-cols-xxl-1 > * { flex: 0 0 auto; width: 100%; }
    .row-cols-xxl-2 > * { flex: 0 0 auto; width: 50%; }
    .row-cols-xxl-3 > * { flex: 0 0 auto; width: 33.333333%; }
    .row-cols-xxl-4 > * { flex: 0 0 auto; width: 25%; }
    .row-cols-xxl-5 > * { flex: 0 0 auto; width: 20%; }
    .row-cols-xxl-6 > * { flex: 0 0 auto; width: 16.666667%; }
    .g-xxl-0, .gx-xxl-0 { --gutter-x: 0; }
    .g-xxl-0, .gy-xxl-0 { --gutter-y: 0; }
    .g-xxl-1, .gx-xxl-1 { --gutter-x: .25rem; }
    .g-xxl-1, .gy-xxl-1 { --gutter-y: .25rem; }
    .g-xxl-2, .gx-xxl-2 { --gutter-x: .5rem; }
    .g-xxl-2, .gy-xxl-2 { --gutter-y: .5rem; }
    .g-xxl-3, .gx-xxl-3 { --gutter-x: 1rem; }
    .g-xxl-3, .gy-xxl-3 { --gutter-y: 1rem; }
    .g-xxl-4, .gx-xxl-4 { --gutter-x: 1.5rem; }
    .g-xxl-4, .gy-xxl-4 { --gutter-y: 1.5rem; }
    .g-xxl-5, .gx-xxl-5 { --gutter-x: 3rem; }
    .g-xxl-5, .gy-xxl-5 { --gutter-y: 3rem; }
    .g-xxl-6, .gx-xxl-6 { --gutter-x: 4rem; }
    .g-xxl-6, .gy-xxl-6 { --gutter-y: 4rem; }
    .g-xxl-7, .gx-xxl-7 { --gutter-x: 5rem; }
    .g-xxl-7, .gy-xxl-7 { --gutter-y: 5rem; }
    .order-xxl-first { order: -1; }
    .order-xxl-0 { order: 0; }
    .order-xxl-1 { order: 1; }
    .order-xxl-2 { order: 2; }
    .order-xxl-3 { order: 3; }
    .order-xxl-4 { order: 4; }
    .order-xxl-5 { order: 5; }
    .order-xxl-last { order: 6; }
  }

  /* ---- Display / flex / text-align, responsive ------------------- */
  .d-none { display: none; }
  .d-inline { display: inline; }
  .d-inline-block { display: inline-block; }
  .d-block { display: block; }
  .d-grid { display: grid; }
  .d-flex { display: flex; }
  .d-inline-flex { display: inline-flex; }
  .flex-row { flex-direction: row; }
  .flex-column { flex-direction: column; }
  .flex-row-reverse { flex-direction: row-reverse; }
  .flex-column-reverse { flex-direction: column-reverse; }
  .flex-wrap { flex-wrap: wrap; }
  .flex-nowrap { flex-wrap: nowrap; }
  .flex-fill { flex: 1 1 auto; }
  .flex-grow-0 { flex-grow: 0; }
  .flex-grow-1 { flex-grow: 1; }
  .flex-shrink-0 { flex-shrink: 0; }
  .flex-shrink-1 { flex-shrink: 1; }
  .justify-content-start { justify-content: flex-start; }
  .justify-content-end { justify-content: flex-end; }
  .justify-content-center { justify-content: center; }
  .justify-content-between { justify-content: space-between; }
  .justify-content-around { justify-content: space-around; }
  .justify-content-evenly { justify-content: space-evenly; }
  .align-items-start { align-items: flex-start; }
  .align-items-end { align-items: flex-end; }
  .align-items-center { align-items: center; }
  .align-items-baseline { align-items: baseline; }
  .align-items-stretch { align-items: stretch; }
  .align-self-start { align-self: flex-start; }
  .align-self-end { align-self: flex-end; }
  .align-self-center { align-self: center; }
  .align-self-stretch { align-self: stretch; }
  .text-start { text-align: left; }
  .text-center { text-align: center; }
  .text-end { text-align: right; }
  .gap-0 { gap: 0; }
  .gap-1 { gap: .25rem; }
  .gap-2 { gap: .5rem; }
  .gap-3 { gap: 1rem; }
  .gap-4 { gap: 1.5rem; }
  .gap-5 { gap: 3rem; }
  .gap-6 { gap: 4rem; }
  .gap-7 { gap: 5rem; }
  @media (min-width: 576px) {
    .d-sm-none { display: none; }
    .d-sm-inline { display: inline; }
    .d-sm-inline-block { display: inline-block; }
    .d-sm-block { display: block; }
    .d-sm-grid { display: grid; }
    .d-sm-flex { display: flex; }
    .d-sm-inline-flex { display: inline-flex; }
    .flex-sm-row { flex-direction: row; }
    .flex-sm-column { flex-direction: column; }
    .flex-sm-row-reverse { flex-direction: row-reverse; }
    .flex-sm-column-reverse { flex-direction: column-reverse; }
    .flex-sm-wrap { flex-wrap: wrap; }
    .flex-sm-nowrap { flex-wrap: nowrap; }
    .flex-sm-fill { flex: 1 1 auto; }
    .flex-sm-grow-0 { flex-grow: 0; }
    .flex-sm-grow-1 { flex-grow: 1; }
    .flex-sm-shrink-0 { flex-shrink: 0; }
    .flex-sm-shrink-1 { flex-shrink: 1; }
    .justify-content-sm-start { justify-content: flex-start; }
    .justify-content-sm-end { justify-content: flex-end; }
    .justify-content-sm-center { justify-content: center; }
    .justify-content-sm-between { justify-content: space-between; }
    .justify-content-sm-around { justify-content: space-around; }
    .justify-content-sm-evenly { justify-content: space-evenly; }
    .align-items-sm-start { align-items: flex-start; }
    .align-items-sm-end { align-items: flex-end; }
    .align-items-sm-center { align-items: center; }
    .align-items-sm-baseline { align-items: baseline; }
    .align-items-sm-stretch { align-items: stretch; }
    .align-self-sm-start { align-self: flex-start; }
    .align-self-sm-end { align-self: flex-end; }
    .align-self-sm-center { align-self: center; }
    .align-self-sm-stretch { align-self: stretch; }
    .text-sm-start { text-align: left; }
    .text-sm-center { text-align: center; }
    .text-sm-end { text-align: right; }
    .gap-sm-0 { gap: 0; }
    .gap-sm-1 { gap: .25rem; }
    .gap-sm-2 { gap: .5rem; }
    .gap-sm-3 { gap: 1rem; }
    .gap-sm-4 { gap: 1.5rem; }
    .gap-sm-5 { gap: 3rem; }
    .gap-sm-6 { gap: 4rem; }
    .gap-sm-7 { gap: 5rem; }
  }
  @media (min-width: 768px) {
    .d-md-none { display: none; }
    .d-md-inline { display: inline; }
    .d-md-inline-block { display: inline-block; }
    .d-md-block { display: block; }
    .d-md-grid { display: grid; }
    .d-md-flex { display: flex; }
    .d-md-inline-flex { display: inline-flex; }
    .flex-md-row { flex-direction: row; }
    .flex-md-column { flex-direction: column; }
    .flex-md-row-reverse { flex-direction: row-reverse; }
    .flex-md-column-reverse { flex-direction: column-reverse; }
    .flex-md-wrap { flex-wrap: wrap; }
    .flex-md-nowrap { flex-wrap: nowrap; }
    .flex-md-fill { flex: 1 1 auto; }
    .flex-md-grow-0 { flex-grow: 0; }
    .flex-md-grow-1 { flex-grow: 1; }
    .flex-md-shrink-0 { flex-shrink: 0; }
    .flex-md-shrink-1 { flex-shrink: 1; }
    .justify-content-md-start { justify-content: flex-start; }
    .justify-content-md-end { justify-content: flex-end; }
    .justify-content-md-center { justify-content: center; }
    .justify-content-md-between { justify-content: space-between; }
    .justify-content-md-around { justify-content: space-around; }
    .justify-content-md-evenly { justify-content: space-evenly; }
    .align-items-md-start { align-items: flex-start; }
    .align-items-md-end { align-items: flex-end; }
    .align-items-md-center { align-items: center; }
    .align-items-md-baseline { align-items: baseline; }
    .align-items-md-stretch { align-items: stretch; }
    .align-self-md-start { align-self: flex-start; }
    .align-self-md-end { align-self: flex-end; }
    .align-self-md-center { align-self: center; }
    .align-self-md-stretch { align-self: stretch; }
    .text-md-start { text-align: left; }
    .text-md-center { text-align: center; }
    .text-md-end { text-align: right; }
    .gap-md-0 { gap: 0; }
    .gap-md-1 { gap: .25rem; }
    .gap-md-2 { gap: .5rem; }
    .gap-md-3 { gap: 1rem; }
    .gap-md-4 { gap: 1.5rem; }
    .gap-md-5 { gap: 3rem; }
    .gap-md-6 { gap: 4rem; }
    .gap-md-7 { gap: 5rem; }
  }
  @media (min-width: 992px) {
    .d-lg-none { display: none; }
    .d-lg-inline { display: inline; }
    .d-lg-inline-block { display: inline-block; }
    .d-lg-block { display: block; }
    .d-lg-grid { display: grid; }
    .d-lg-flex { display: flex; }
    .d-lg-inline-flex { display: inline-flex; }
    .flex-lg-row { flex-direction: row; }
    .flex-lg-column { flex-direction: column; }
    .flex-lg-row-reverse { flex-direction: row-reverse; }
    .flex-lg-column-reverse { flex-direction: column-reverse; }
    .flex-lg-wrap { flex-wrap: wrap; }
    .flex-lg-nowrap { flex-wrap: nowrap; }
    .flex-lg-fill { flex: 1 1 auto; }
    .flex-lg-grow-0 { flex-grow: 0; }
    .flex-lg-grow-1 { flex-grow: 1; }
    .flex-lg-shrink-0 { flex-shrink: 0; }
    .flex-lg-shrink-1 { flex-shrink: 1; }
    .justify-content-lg-start { justify-content: flex-start; }
    .justify-content-lg-end { justify-content: flex-end; }
    .justify-content-lg-center { justify-content: center; }
    .justify-content-lg-between { justify-content: space-between; }
    .justify-content-lg-around { justify-content: space-around; }
    .justify-content-lg-evenly { justify-content: space-evenly; }
    .align-items-lg-start { align-items: flex-start; }
    .align-items-lg-end { align-items: flex-end; }
    .align-items-lg-center { align-items: center; }
    .align-items-lg-baseline { align-items: baseline; }
    .align-items-lg-stretch { align-items: stretch; }
    .align-self-lg-start { align-self: flex-start; }
    .align-self-lg-end { align-self: flex-end; }
    .align-self-lg-center { align-self: center; }
    .align-self-lg-stretch { align-self: stretch; }
    .text-lg-start { text-align: left; }
    .text-lg-center { text-align: center; }
    .text-lg-end { text-align: right; }
    .gap-lg-0 { gap: 0; }
    .gap-lg-1 { gap: .25rem; }
    .gap-lg-2 { gap: .5rem; }
    .gap-lg-3 { gap: 1rem; }
    .gap-lg-4 { gap: 1.5rem; }
    .gap-lg-5 { gap: 3rem; }
    .gap-lg-6 { gap: 4rem; }
    .gap-lg-7 { gap: 5rem; }
  }
  @media (min-width: 1200px) {
    .d-xl-none { display: none; }
    .d-xl-inline { display: inline; }
    .d-xl-inline-block { display: inline-block; }
    .d-xl-block { display: block; }
    .d-xl-grid { display: grid; }
    .d-xl-flex { display: flex; }
    .d-xl-inline-flex { display: inline-flex; }
    .flex-xl-row { flex-direction: row; }
    .flex-xl-column { flex-direction: column; }
    .flex-xl-row-reverse { flex-direction: row-reverse; }
    .flex-xl-column-reverse { flex-direction: column-reverse; }
    .flex-xl-wrap { flex-wrap: wrap; }
    .flex-xl-nowrap { flex-wrap: nowrap; }
    .flex-xl-fill { flex: 1 1 auto; }
    .flex-xl-grow-0 { flex-grow: 0; }
    .flex-xl-grow-1 { flex-grow: 1; }
    .flex-xl-shrink-0 { flex-shrink: 0; }
    .flex-xl-shrink-1 { flex-shrink: 1; }
    .justify-content-xl-start { justify-content: flex-start; }
    .justify-content-xl-end { justify-content: flex-end; }
    .justify-content-xl-center { justify-content: center; }
    .justify-content-xl-between { justify-content: space-between; }
    .justify-content-xl-around { justify-content: space-around; }
    .justify-content-xl-evenly { justify-content: space-evenly; }
    .align-items-xl-start { align-items: flex-start; }
    .align-items-xl-end { align-items: flex-end; }
    .align-items-xl-center { align-items: center; }
    .align-items-xl-baseline { align-items: baseline; }
    .align-items-xl-stretch { align-items: stretch; }
    .align-self-xl-start { align-self: flex-start; }
    .align-self-xl-end { align-self: flex-end; }
    .align-self-xl-center { align-self: center; }
    .align-self-xl-stretch { align-self: stretch; }
    .text-xl-start { text-align: left; }
    .text-xl-center { text-align: center; }
    .text-xl-end { text-align: right; }
    .gap-xl-0 { gap: 0; }
    .gap-xl-1 { gap: .25rem; }
    .gap-xl-2 { gap: .5rem; }
    .gap-xl-3 { gap: 1rem; }
    .gap-xl-4 { gap: 1.5rem; }
    .gap-xl-5 { gap: 3rem; }
    .gap-xl-6 { gap: 4rem; }
    .gap-xl-7 { gap: 5rem; }
  }
  @media (min-width: 1400px) {
    .d-xxl-none { display: none; }
    .d-xxl-inline { display: inline; }
    .d-xxl-inline-block { display: inline-block; }
    .d-xxl-block { display: block; }
    .d-xxl-grid { display: grid; }
    .d-xxl-flex { display: flex; }
    .d-xxl-inline-flex { display: inline-flex; }
    .flex-xxl-row { flex-direction: row; }
    .flex-xxl-column { flex-direction: column; }
    .flex-xxl-row-reverse { flex-direction: row-reverse; }
    .flex-xxl-column-reverse { flex-direction: column-reverse; }
    .flex-xxl-wrap { flex-wrap: wrap; }
    .flex-xxl-nowrap { flex-wrap: nowrap; }
    .flex-xxl-fill { flex: 1 1 auto; }
    .flex-xxl-grow-0 { flex-grow: 0; }
    .flex-xxl-grow-1 { flex-grow: 1; }
    .flex-xxl-shrink-0 { flex-shrink: 0; }
    .flex-xxl-shrink-1 { flex-shrink: 1; }
    .justify-content-xxl-start { justify-content: flex-start; }
    .justify-content-xxl-end { justify-content: flex-end; }
    .justify-content-xxl-center { justify-content: center; }
    .justify-content-xxl-between { justify-content: space-between; }
    .justify-content-xxl-around { justify-content: space-around; }
    .justify-content-xxl-evenly { justify-content: space-evenly; }
    .align-items-xxl-start { align-items: flex-start; }
    .align-items-xxl-end { align-items: flex-end; }
    .align-items-xxl-center { align-items: center; }
    .align-items-xxl-baseline { align-items: baseline; }
    .align-items-xxl-stretch { align-items: stretch; }
    .align-self-xxl-start { align-self: flex-start; }
    .align-self-xxl-end { align-self: flex-end; }
    .align-self-xxl-center { align-self: center; }
    .align-self-xxl-stretch { align-self: stretch; }
    .text-xxl-start { text-align: left; }
    .text-xxl-center { text-align: center; }
    .text-xxl-end { text-align: right; }
    .gap-xxl-0 { gap: 0; }
    .gap-xxl-1 { gap: .25rem; }
    .gap-xxl-2 { gap: .5rem; }
    .gap-xxl-3 { gap: 1rem; }
    .gap-xxl-4 { gap: 1.5rem; }
    .gap-xxl-5 { gap: 3rem; }
    .gap-xxl-6 { gap: 4rem; }
    .gap-xxl-7 { gap: 5rem; }
  }

  /* ---- Spacing (m/p, six-step scale + auto margins), responsive -- */
  .m-0 { margin: 0; }
  .m-1 { margin: .25rem; }
  .m-2 { margin: .5rem; }
  .m-3 { margin: 1rem; }
  .m-4 { margin: 1.5rem; }
  .m-5 { margin: 3rem; }
  .m-6 { margin: 4rem; }
  .m-7 { margin: 5rem; }
  .m-auto { margin: auto; }
  .mt-0 { margin-top: 0; }
  .mt-1 { margin-top: .25rem; }
  .mt-2 { margin-top: .5rem; }
  .mt-3 { margin-top: 1rem; }
  .mt-4 { margin-top: 1.5rem; }
  .mt-5 { margin-top: 3rem; }
  .mt-6 { margin-top: 4rem; }
  .mt-7 { margin-top: 5rem; }
  .mt-auto { margin-top: auto; }
  .mb-0 { margin-bottom: 0; }
  .mb-1 { margin-bottom: .25rem; }
  .mb-2 { margin-bottom: .5rem; }
  .mb-3 { margin-bottom: 1rem; }
  .mb-4 { margin-bottom: 1.5rem; }
  .mb-5 { margin-bottom: 3rem; }
  .mb-6 { margin-bottom: 4rem; }
  .mb-7 { margin-bottom: 5rem; }
  .mb-auto { margin-bottom: auto; }
  .ms-0 { margin-inline-start: 0; }
  .ms-1 { margin-inline-start: .25rem; }
  .ms-2 { margin-inline-start: .5rem; }
  .ms-3 { margin-inline-start: 1rem; }
  .ms-4 { margin-inline-start: 1.5rem; }
  .ms-5 { margin-inline-start: 3rem; }
  .ms-6 { margin-inline-start: 4rem; }
  .ms-7 { margin-inline-start: 5rem; }
  .ms-auto { margin-inline-start: auto; }
  .me-0 { margin-inline-end: 0; }
  .me-1 { margin-inline-end: .25rem; }
  .me-2 { margin-inline-end: .5rem; }
  .me-3 { margin-inline-end: 1rem; }
  .me-4 { margin-inline-end: 1.5rem; }
  .me-5 { margin-inline-end: 3rem; }
  .me-6 { margin-inline-end: 4rem; }
  .me-7 { margin-inline-end: 5rem; }
  .me-auto { margin-inline-end: auto; }
  .mx-0 { margin-inline: 0; }
  .mx-1 { margin-inline: .25rem; }
  .mx-2 { margin-inline: .5rem; }
  .mx-3 { margin-inline: 1rem; }
  .mx-4 { margin-inline: 1.5rem; }
  .mx-5 { margin-inline: 3rem; }
  .mx-6 { margin-inline: 4rem; }
  .mx-7 { margin-inline: 5rem; }
  .mx-auto { margin-inline: auto; }
  .my-0 { margin-top: 0; margin-bottom: 0; }
  .my-1 { margin-top: .25rem; margin-bottom: .25rem; }
  .my-2 { margin-top: .5rem; margin-bottom: .5rem; }
  .my-3 { margin-top: 1rem; margin-bottom: 1rem; }
  .my-4 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
  .my-5 { margin-top: 3rem; margin-bottom: 3rem; }
  .my-6 { margin-top: 4rem; margin-bottom: 4rem; }
  .my-7 { margin-top: 5rem; margin-bottom: 5rem; }
  .my-auto { margin-top: auto; margin-bottom: auto; }
  .p-0 { padding: 0; }
  .p-1 { padding: .25rem; }
  .p-2 { padding: .5rem; }
  .p-3 { padding: 1rem; }
  .p-4 { padding: 1.5rem; }
  .p-5 { padding: 3rem; }
  .p-6 { padding: 4rem; }
  .p-7 { padding: 5rem; }
  .pt-0 { padding-top: 0; }
  .pt-1 { padding-top: .25rem; }
  .pt-2 { padding-top: .5rem; }
  .pt-3 { padding-top: 1rem; }
  .pt-4 { padding-top: 1.5rem; }
  .pt-5 { padding-top: 3rem; }
  .pt-6 { padding-top: 4rem; }
  .pt-7 { padding-top: 5rem; }
  .pb-0 { padding-bottom: 0; }
  .pb-1 { padding-bottom: .25rem; }
  .pb-2 { padding-bottom: .5rem; }
  .pb-3 { padding-bottom: 1rem; }
  .pb-4 { padding-bottom: 1.5rem; }
  .pb-5 { padding-bottom: 3rem; }
  .pb-6 { padding-bottom: 4rem; }
  .pb-7 { padding-bottom: 5rem; }
  .ps-0 { padding-inline-start: 0; }
  .ps-1 { padding-inline-start: .25rem; }
  .ps-2 { padding-inline-start: .5rem; }
  .ps-3 { padding-inline-start: 1rem; }
  .ps-4 { padding-inline-start: 1.5rem; }
  .ps-5 { padding-inline-start: 3rem; }
  .ps-6 { padding-inline-start: 4rem; }
  .ps-7 { padding-inline-start: 5rem; }
  .pe-0 { padding-inline-end: 0; }
  .pe-1 { padding-inline-end: .25rem; }
  .pe-2 { padding-inline-end: .5rem; }
  .pe-3 { padding-inline-end: 1rem; }
  .pe-4 { padding-inline-end: 1.5rem; }
  .pe-5 { padding-inline-end: 3rem; }
  .pe-6 { padding-inline-end: 4rem; }
  .pe-7 { padding-inline-end: 5rem; }
  .px-0 { padding-inline: 0; }
  .px-1 { padding-inline: .25rem; }
  .px-2 { padding-inline: .5rem; }
  .px-3 { padding-inline: 1rem; }
  .px-4 { padding-inline: 1.5rem; }
  .px-5 { padding-inline: 3rem; }
  .px-6 { padding-inline: 4rem; }
  .px-7 { padding-inline: 5rem; }
  .py-0 { padding-top: 0; padding-bottom: 0; }
  .py-1 { padding-top: .25rem; padding-bottom: .25rem; }
  .py-2 { padding-top: .5rem; padding-bottom: .5rem; }
  .py-3 { padding-top: 1rem; padding-bottom: 1rem; }
  .py-4 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
  .py-5 { padding-top: 3rem; padding-bottom: 3rem; }
  .py-6 { padding-top: 4rem; padding-bottom: 4rem; }
  .py-7 { padding-top: 5rem; padding-bottom: 5rem; }
  @media (min-width: 576px) {
    .m-sm-0 { margin: 0; }
    .m-sm-1 { margin: .25rem; }
    .m-sm-2 { margin: .5rem; }
    .m-sm-3 { margin: 1rem; }
    .m-sm-4 { margin: 1.5rem; }
    .m-sm-5 { margin: 3rem; }
    .m-sm-6 { margin: 4rem; }
    .m-sm-7 { margin: 5rem; }
    .m-sm-auto { margin: auto; }
    .mt-sm-0 { margin-top: 0; }
    .mt-sm-1 { margin-top: .25rem; }
    .mt-sm-2 { margin-top: .5rem; }
    .mt-sm-3 { margin-top: 1rem; }
    .mt-sm-4 { margin-top: 1.5rem; }
    .mt-sm-5 { margin-top: 3rem; }
    .mt-sm-6 { margin-top: 4rem; }
    .mt-sm-7 { margin-top: 5rem; }
    .mt-sm-auto { margin-top: auto; }
    .mb-sm-0 { margin-bottom: 0; }
    .mb-sm-1 { margin-bottom: .25rem; }
    .mb-sm-2 { margin-bottom: .5rem; }
    .mb-sm-3 { margin-bottom: 1rem; }
    .mb-sm-4 { margin-bottom: 1.5rem; }
    .mb-sm-5 { margin-bottom: 3rem; }
    .mb-sm-6 { margin-bottom: 4rem; }
    .mb-sm-7 { margin-bottom: 5rem; }
    .mb-sm-auto { margin-bottom: auto; }
    .ms-sm-0 { margin-inline-start: 0; }
    .ms-sm-1 { margin-inline-start: .25rem; }
    .ms-sm-2 { margin-inline-start: .5rem; }
    .ms-sm-3 { margin-inline-start: 1rem; }
    .ms-sm-4 { margin-inline-start: 1.5rem; }
    .ms-sm-5 { margin-inline-start: 3rem; }
    .ms-sm-6 { margin-inline-start: 4rem; }
    .ms-sm-7 { margin-inline-start: 5rem; }
    .ms-sm-auto { margin-inline-start: auto; }
    .me-sm-0 { margin-inline-end: 0; }
    .me-sm-1 { margin-inline-end: .25rem; }
    .me-sm-2 { margin-inline-end: .5rem; }
    .me-sm-3 { margin-inline-end: 1rem; }
    .me-sm-4 { margin-inline-end: 1.5rem; }
    .me-sm-5 { margin-inline-end: 3rem; }
    .me-sm-6 { margin-inline-end: 4rem; }
    .me-sm-7 { margin-inline-end: 5rem; }
    .me-sm-auto { margin-inline-end: auto; }
    .mx-sm-0 { margin-inline: 0; }
    .mx-sm-1 { margin-inline: .25rem; }
    .mx-sm-2 { margin-inline: .5rem; }
    .mx-sm-3 { margin-inline: 1rem; }
    .mx-sm-4 { margin-inline: 1.5rem; }
    .mx-sm-5 { margin-inline: 3rem; }
    .mx-sm-6 { margin-inline: 4rem; }
    .mx-sm-7 { margin-inline: 5rem; }
    .mx-sm-auto { margin-inline: auto; }
    .my-sm-0 { margin-top: 0; margin-bottom: 0; }
    .my-sm-1 { margin-top: .25rem; margin-bottom: .25rem; }
    .my-sm-2 { margin-top: .5rem; margin-bottom: .5rem; }
    .my-sm-3 { margin-top: 1rem; margin-bottom: 1rem; }
    .my-sm-4 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
    .my-sm-5 { margin-top: 3rem; margin-bottom: 3rem; }
    .my-sm-6 { margin-top: 4rem; margin-bottom: 4rem; }
    .my-sm-7 { margin-top: 5rem; margin-bottom: 5rem; }
    .my-sm-auto { margin-top: auto; margin-bottom: auto; }
    .p-sm-0 { padding: 0; }
    .p-sm-1 { padding: .25rem; }
    .p-sm-2 { padding: .5rem; }
    .p-sm-3 { padding: 1rem; }
    .p-sm-4 { padding: 1.5rem; }
    .p-sm-5 { padding: 3rem; }
    .p-sm-6 { padding: 4rem; }
    .p-sm-7 { padding: 5rem; }
    .pt-sm-0 { padding-top: 0; }
    .pt-sm-1 { padding-top: .25rem; }
    .pt-sm-2 { padding-top: .5rem; }
    .pt-sm-3 { padding-top: 1rem; }
    .pt-sm-4 { padding-top: 1.5rem; }
    .pt-sm-5 { padding-top: 3rem; }
    .pt-sm-6 { padding-top: 4rem; }
    .pt-sm-7 { padding-top: 5rem; }
    .pb-sm-0 { padding-bottom: 0; }
    .pb-sm-1 { padding-bottom: .25rem; }
    .pb-sm-2 { padding-bottom: .5rem; }
    .pb-sm-3 { padding-bottom: 1rem; }
    .pb-sm-4 { padding-bottom: 1.5rem; }
    .pb-sm-5 { padding-bottom: 3rem; }
    .pb-sm-6 { padding-bottom: 4rem; }
    .pb-sm-7 { padding-bottom: 5rem; }
    .ps-sm-0 { padding-inline-start: 0; }
    .ps-sm-1 { padding-inline-start: .25rem; }
    .ps-sm-2 { padding-inline-start: .5rem; }
    .ps-sm-3 { padding-inline-start: 1rem; }
    .ps-sm-4 { padding-inline-start: 1.5rem; }
    .ps-sm-5 { padding-inline-start: 3rem; }
    .ps-sm-6 { padding-inline-start: 4rem; }
    .ps-sm-7 { padding-inline-start: 5rem; }
    .pe-sm-0 { padding-inline-end: 0; }
    .pe-sm-1 { padding-inline-end: .25rem; }
    .pe-sm-2 { padding-inline-end: .5rem; }
    .pe-sm-3 { padding-inline-end: 1rem; }
    .pe-sm-4 { padding-inline-end: 1.5rem; }
    .pe-sm-5 { padding-inline-end: 3rem; }
    .pe-sm-6 { padding-inline-end: 4rem; }
    .pe-sm-7 { padding-inline-end: 5rem; }
    .px-sm-0 { padding-inline: 0; }
    .px-sm-1 { padding-inline: .25rem; }
    .px-sm-2 { padding-inline: .5rem; }
    .px-sm-3 { padding-inline: 1rem; }
    .px-sm-4 { padding-inline: 1.5rem; }
    .px-sm-5 { padding-inline: 3rem; }
    .px-sm-6 { padding-inline: 4rem; }
    .px-sm-7 { padding-inline: 5rem; }
    .py-sm-0 { padding-top: 0; padding-bottom: 0; }
    .py-sm-1 { padding-top: .25rem; padding-bottom: .25rem; }
    .py-sm-2 { padding-top: .5rem; padding-bottom: .5rem; }
    .py-sm-3 { padding-top: 1rem; padding-bottom: 1rem; }
    .py-sm-4 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
    .py-sm-5 { padding-top: 3rem; padding-bottom: 3rem; }
    .py-sm-6 { padding-top: 4rem; padding-bottom: 4rem; }
    .py-sm-7 { padding-top: 5rem; padding-bottom: 5rem; }
  }
  @media (min-width: 768px) {
    .m-md-0 { margin: 0; }
    .m-md-1 { margin: .25rem; }
    .m-md-2 { margin: .5rem; }
    .m-md-3 { margin: 1rem; }
    .m-md-4 { margin: 1.5rem; }
    .m-md-5 { margin: 3rem; }
    .m-md-6 { margin: 4rem; }
    .m-md-7 { margin: 5rem; }
    .m-md-auto { margin: auto; }
    .mt-md-0 { margin-top: 0; }
    .mt-md-1 { margin-top: .25rem; }
    .mt-md-2 { margin-top: .5rem; }
    .mt-md-3 { margin-top: 1rem; }
    .mt-md-4 { margin-top: 1.5rem; }
    .mt-md-5 { margin-top: 3rem; }
    .mt-md-6 { margin-top: 4rem; }
    .mt-md-7 { margin-top: 5rem; }
    .mt-md-auto { margin-top: auto; }
    .mb-md-0 { margin-bottom: 0; }
    .mb-md-1 { margin-bottom: .25rem; }
    .mb-md-2 { margin-bottom: .5rem; }
    .mb-md-3 { margin-bottom: 1rem; }
    .mb-md-4 { margin-bottom: 1.5rem; }
    .mb-md-5 { margin-bottom: 3rem; }
    .mb-md-6 { margin-bottom: 4rem; }
    .mb-md-7 { margin-bottom: 5rem; }
    .mb-md-auto { margin-bottom: auto; }
    .ms-md-0 { margin-inline-start: 0; }
    .ms-md-1 { margin-inline-start: .25rem; }
    .ms-md-2 { margin-inline-start: .5rem; }
    .ms-md-3 { margin-inline-start: 1rem; }
    .ms-md-4 { margin-inline-start: 1.5rem; }
    .ms-md-5 { margin-inline-start: 3rem; }
    .ms-md-6 { margin-inline-start: 4rem; }
    .ms-md-7 { margin-inline-start: 5rem; }
    .ms-md-auto { margin-inline-start: auto; }
    .me-md-0 { margin-inline-end: 0; }
    .me-md-1 { margin-inline-end: .25rem; }
    .me-md-2 { margin-inline-end: .5rem; }
    .me-md-3 { margin-inline-end: 1rem; }
    .me-md-4 { margin-inline-end: 1.5rem; }
    .me-md-5 { margin-inline-end: 3rem; }
    .me-md-6 { margin-inline-end: 4rem; }
    .me-md-7 { margin-inline-end: 5rem; }
    .me-md-auto { margin-inline-end: auto; }
    .mx-md-0 { margin-inline: 0; }
    .mx-md-1 { margin-inline: .25rem; }
    .mx-md-2 { margin-inline: .5rem; }
    .mx-md-3 { margin-inline: 1rem; }
    .mx-md-4 { margin-inline: 1.5rem; }
    .mx-md-5 { margin-inline: 3rem; }
    .mx-md-6 { margin-inline: 4rem; }
    .mx-md-7 { margin-inline: 5rem; }
    .mx-md-auto { margin-inline: auto; }
    .my-md-0 { margin-top: 0; margin-bottom: 0; }
    .my-md-1 { margin-top: .25rem; margin-bottom: .25rem; }
    .my-md-2 { margin-top: .5rem; margin-bottom: .5rem; }
    .my-md-3 { margin-top: 1rem; margin-bottom: 1rem; }
    .my-md-4 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
    .my-md-5 { margin-top: 3rem; margin-bottom: 3rem; }
    .my-md-6 { margin-top: 4rem; margin-bottom: 4rem; }
    .my-md-7 { margin-top: 5rem; margin-bottom: 5rem; }
    .my-md-auto { margin-top: auto; margin-bottom: auto; }
    .p-md-0 { padding: 0; }
    .p-md-1 { padding: .25rem; }
    .p-md-2 { padding: .5rem; }
    .p-md-3 { padding: 1rem; }
    .p-md-4 { padding: 1.5rem; }
    .p-md-5 { padding: 3rem; }
    .p-md-6 { padding: 4rem; }
    .p-md-7 { padding: 5rem; }
    .pt-md-0 { padding-top: 0; }
    .pt-md-1 { padding-top: .25rem; }
    .pt-md-2 { padding-top: .5rem; }
    .pt-md-3 { padding-top: 1rem; }
    .pt-md-4 { padding-top: 1.5rem; }
    .pt-md-5 { padding-top: 3rem; }
    .pt-md-6 { padding-top: 4rem; }
    .pt-md-7 { padding-top: 5rem; }
    .pb-md-0 { padding-bottom: 0; }
    .pb-md-1 { padding-bottom: .25rem; }
    .pb-md-2 { padding-bottom: .5rem; }
    .pb-md-3 { padding-bottom: 1rem; }
    .pb-md-4 { padding-bottom: 1.5rem; }
    .pb-md-5 { padding-bottom: 3rem; }
    .pb-md-6 { padding-bottom: 4rem; }
    .pb-md-7 { padding-bottom: 5rem; }
    .ps-md-0 { padding-inline-start: 0; }
    .ps-md-1 { padding-inline-start: .25rem; }
    .ps-md-2 { padding-inline-start: .5rem; }
    .ps-md-3 { padding-inline-start: 1rem; }
    .ps-md-4 { padding-inline-start: 1.5rem; }
    .ps-md-5 { padding-inline-start: 3rem; }
    .ps-md-6 { padding-inline-start: 4rem; }
    .ps-md-7 { padding-inline-start: 5rem; }
    .pe-md-0 { padding-inline-end: 0; }
    .pe-md-1 { padding-inline-end: .25rem; }
    .pe-md-2 { padding-inline-end: .5rem; }
    .pe-md-3 { padding-inline-end: 1rem; }
    .pe-md-4 { padding-inline-end: 1.5rem; }
    .pe-md-5 { padding-inline-end: 3rem; }
    .pe-md-6 { padding-inline-end: 4rem; }
    .pe-md-7 { padding-inline-end: 5rem; }
    .px-md-0 { padding-inline: 0; }
    .px-md-1 { padding-inline: .25rem; }
    .px-md-2 { padding-inline: .5rem; }
    .px-md-3 { padding-inline: 1rem; }
    .px-md-4 { padding-inline: 1.5rem; }
    .px-md-5 { padding-inline: 3rem; }
    .px-md-6 { padding-inline: 4rem; }
    .px-md-7 { padding-inline: 5rem; }
    .py-md-0 { padding-top: 0; padding-bottom: 0; }
    .py-md-1 { padding-top: .25rem; padding-bottom: .25rem; }
    .py-md-2 { padding-top: .5rem; padding-bottom: .5rem; }
    .py-md-3 { padding-top: 1rem; padding-bottom: 1rem; }
    .py-md-4 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
    .py-md-5 { padding-top: 3rem; padding-bottom: 3rem; }
    .py-md-6 { padding-top: 4rem; padding-bottom: 4rem; }
    .py-md-7 { padding-top: 5rem; padding-bottom: 5rem; }
  }
  @media (min-width: 992px) {
    .m-lg-0 { margin: 0; }
    .m-lg-1 { margin: .25rem; }
    .m-lg-2 { margin: .5rem; }
    .m-lg-3 { margin: 1rem; }
    .m-lg-4 { margin: 1.5rem; }
    .m-lg-5 { margin: 3rem; }
    .m-lg-6 { margin: 4rem; }
    .m-lg-7 { margin: 5rem; }
    .m-lg-auto { margin: auto; }
    .mt-lg-0 { margin-top: 0; }
    .mt-lg-1 { margin-top: .25rem; }
    .mt-lg-2 { margin-top: .5rem; }
    .mt-lg-3 { margin-top: 1rem; }
    .mt-lg-4 { margin-top: 1.5rem; }
    .mt-lg-5 { margin-top: 3rem; }
    .mt-lg-6 { margin-top: 4rem; }
    .mt-lg-7 { margin-top: 5rem; }
    .mt-lg-auto { margin-top: auto; }
    .mb-lg-0 { margin-bottom: 0; }
    .mb-lg-1 { margin-bottom: .25rem; }
    .mb-lg-2 { margin-bottom: .5rem; }
    .mb-lg-3 { margin-bottom: 1rem; }
    .mb-lg-4 { margin-bottom: 1.5rem; }
    .mb-lg-5 { margin-bottom: 3rem; }
    .mb-lg-6 { margin-bottom: 4rem; }
    .mb-lg-7 { margin-bottom: 5rem; }
    .mb-lg-auto { margin-bottom: auto; }
    .ms-lg-0 { margin-inline-start: 0; }
    .ms-lg-1 { margin-inline-start: .25rem; }
    .ms-lg-2 { margin-inline-start: .5rem; }
    .ms-lg-3 { margin-inline-start: 1rem; }
    .ms-lg-4 { margin-inline-start: 1.5rem; }
    .ms-lg-5 { margin-inline-start: 3rem; }
    .ms-lg-6 { margin-inline-start: 4rem; }
    .ms-lg-7 { margin-inline-start: 5rem; }
    .ms-lg-auto { margin-inline-start: auto; }
    .me-lg-0 { margin-inline-end: 0; }
    .me-lg-1 { margin-inline-end: .25rem; }
    .me-lg-2 { margin-inline-end: .5rem; }
    .me-lg-3 { margin-inline-end: 1rem; }
    .me-lg-4 { margin-inline-end: 1.5rem; }
    .me-lg-5 { margin-inline-end: 3rem; }
    .me-lg-6 { margin-inline-end: 4rem; }
    .me-lg-7 { margin-inline-end: 5rem; }
    .me-lg-auto { margin-inline-end: auto; }
    .mx-lg-0 { margin-inline: 0; }
    .mx-lg-1 { margin-inline: .25rem; }
    .mx-lg-2 { margin-inline: .5rem; }
    .mx-lg-3 { margin-inline: 1rem; }
    .mx-lg-4 { margin-inline: 1.5rem; }
    .mx-lg-5 { margin-inline: 3rem; }
    .mx-lg-6 { margin-inline: 4rem; }
    .mx-lg-7 { margin-inline: 5rem; }
    .mx-lg-auto { margin-inline: auto; }
    .my-lg-0 { margin-top: 0; margin-bottom: 0; }
    .my-lg-1 { margin-top: .25rem; margin-bottom: .25rem; }
    .my-lg-2 { margin-top: .5rem; margin-bottom: .5rem; }
    .my-lg-3 { margin-top: 1rem; margin-bottom: 1rem; }
    .my-lg-4 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
    .my-lg-5 { margin-top: 3rem; margin-bottom: 3rem; }
    .my-lg-6 { margin-top: 4rem; margin-bottom: 4rem; }
    .my-lg-7 { margin-top: 5rem; margin-bottom: 5rem; }
    .my-lg-auto { margin-top: auto; margin-bottom: auto; }
    .p-lg-0 { padding: 0; }
    .p-lg-1 { padding: .25rem; }
    .p-lg-2 { padding: .5rem; }
    .p-lg-3 { padding: 1rem; }
    .p-lg-4 { padding: 1.5rem; }
    .p-lg-5 { padding: 3rem; }
    .p-lg-6 { padding: 4rem; }
    .p-lg-7 { padding: 5rem; }
    .pt-lg-0 { padding-top: 0; }
    .pt-lg-1 { padding-top: .25rem; }
    .pt-lg-2 { padding-top: .5rem; }
    .pt-lg-3 { padding-top: 1rem; }
    .pt-lg-4 { padding-top: 1.5rem; }
    .pt-lg-5 { padding-top: 3rem; }
    .pt-lg-6 { padding-top: 4rem; }
    .pt-lg-7 { padding-top: 5rem; }
    .pb-lg-0 { padding-bottom: 0; }
    .pb-lg-1 { padding-bottom: .25rem; }
    .pb-lg-2 { padding-bottom: .5rem; }
    .pb-lg-3 { padding-bottom: 1rem; }
    .pb-lg-4 { padding-bottom: 1.5rem; }
    .pb-lg-5 { padding-bottom: 3rem; }
    .pb-lg-6 { padding-bottom: 4rem; }
    .pb-lg-7 { padding-bottom: 5rem; }
    .ps-lg-0 { padding-inline-start: 0; }
    .ps-lg-1 { padding-inline-start: .25rem; }
    .ps-lg-2 { padding-inline-start: .5rem; }
    .ps-lg-3 { padding-inline-start: 1rem; }
    .ps-lg-4 { padding-inline-start: 1.5rem; }
    .ps-lg-5 { padding-inline-start: 3rem; }
    .ps-lg-6 { padding-inline-start: 4rem; }
    .ps-lg-7 { padding-inline-start: 5rem; }
    .pe-lg-0 { padding-inline-end: 0; }
    .pe-lg-1 { padding-inline-end: .25rem; }
    .pe-lg-2 { padding-inline-end: .5rem; }
    .pe-lg-3 { padding-inline-end: 1rem; }
    .pe-lg-4 { padding-inline-end: 1.5rem; }
    .pe-lg-5 { padding-inline-end: 3rem; }
    .pe-lg-6 { padding-inline-end: 4rem; }
    .pe-lg-7 { padding-inline-end: 5rem; }
    .px-lg-0 { padding-inline: 0; }
    .px-lg-1 { padding-inline: .25rem; }
    .px-lg-2 { padding-inline: .5rem; }
    .px-lg-3 { padding-inline: 1rem; }
    .px-lg-4 { padding-inline: 1.5rem; }
    .px-lg-5 { padding-inline: 3rem; }
    .px-lg-6 { padding-inline: 4rem; }
    .px-lg-7 { padding-inline: 5rem; }
    .py-lg-0 { padding-top: 0; padding-bottom: 0; }
    .py-lg-1 { padding-top: .25rem; padding-bottom: .25rem; }
    .py-lg-2 { padding-top: .5rem; padding-bottom: .5rem; }
    .py-lg-3 { padding-top: 1rem; padding-bottom: 1rem; }
    .py-lg-4 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
    .py-lg-5 { padding-top: 3rem; padding-bottom: 3rem; }
    .py-lg-6 { padding-top: 4rem; padding-bottom: 4rem; }
    .py-lg-7 { padding-top: 5rem; padding-bottom: 5rem; }
  }
  @media (min-width: 1200px) {
    .m-xl-0 { margin: 0; }
    .m-xl-1 { margin: .25rem; }
    .m-xl-2 { margin: .5rem; }
    .m-xl-3 { margin: 1rem; }
    .m-xl-4 { margin: 1.5rem; }
    .m-xl-5 { margin: 3rem; }
    .m-xl-6 { margin: 4rem; }
    .m-xl-7 { margin: 5rem; }
    .m-xl-auto { margin: auto; }
    .mt-xl-0 { margin-top: 0; }
    .mt-xl-1 { margin-top: .25rem; }
    .mt-xl-2 { margin-top: .5rem; }
    .mt-xl-3 { margin-top: 1rem; }
    .mt-xl-4 { margin-top: 1.5rem; }
    .mt-xl-5 { margin-top: 3rem; }
    .mt-xl-6 { margin-top: 4rem; }
    .mt-xl-7 { margin-top: 5rem; }
    .mt-xl-auto { margin-top: auto; }
    .mb-xl-0 { margin-bottom: 0; }
    .mb-xl-1 { margin-bottom: .25rem; }
    .mb-xl-2 { margin-bottom: .5rem; }
    .mb-xl-3 { margin-bottom: 1rem; }
    .mb-xl-4 { margin-bottom: 1.5rem; }
    .mb-xl-5 { margin-bottom: 3rem; }
    .mb-xl-6 { margin-bottom: 4rem; }
    .mb-xl-7 { margin-bottom: 5rem; }
    .mb-xl-auto { margin-bottom: auto; }
    .ms-xl-0 { margin-inline-start: 0; }
    .ms-xl-1 { margin-inline-start: .25rem; }
    .ms-xl-2 { margin-inline-start: .5rem; }
    .ms-xl-3 { margin-inline-start: 1rem; }
    .ms-xl-4 { margin-inline-start: 1.5rem; }
    .ms-xl-5 { margin-inline-start: 3rem; }
    .ms-xl-6 { margin-inline-start: 4rem; }
    .ms-xl-7 { margin-inline-start: 5rem; }
    .ms-xl-auto { margin-inline-start: auto; }
    .me-xl-0 { margin-inline-end: 0; }
    .me-xl-1 { margin-inline-end: .25rem; }
    .me-xl-2 { margin-inline-end: .5rem; }
    .me-xl-3 { margin-inline-end: 1rem; }
    .me-xl-4 { margin-inline-end: 1.5rem; }
    .me-xl-5 { margin-inline-end: 3rem; }
    .me-xl-6 { margin-inline-end: 4rem; }
    .me-xl-7 { margin-inline-end: 5rem; }
    .me-xl-auto { margin-inline-end: auto; }
    .mx-xl-0 { margin-inline: 0; }
    .mx-xl-1 { margin-inline: .25rem; }
    .mx-xl-2 { margin-inline: .5rem; }
    .mx-xl-3 { margin-inline: 1rem; }
    .mx-xl-4 { margin-inline: 1.5rem; }
    .mx-xl-5 { margin-inline: 3rem; }
    .mx-xl-6 { margin-inline: 4rem; }
    .mx-xl-7 { margin-inline: 5rem; }
    .mx-xl-auto { margin-inline: auto; }
    .my-xl-0 { margin-top: 0; margin-bottom: 0; }
    .my-xl-1 { margin-top: .25rem; margin-bottom: .25rem; }
    .my-xl-2 { margin-top: .5rem; margin-bottom: .5rem; }
    .my-xl-3 { margin-top: 1rem; margin-bottom: 1rem; }
    .my-xl-4 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
    .my-xl-5 { margin-top: 3rem; margin-bottom: 3rem; }
    .my-xl-6 { margin-top: 4rem; margin-bottom: 4rem; }
    .my-xl-7 { margin-top: 5rem; margin-bottom: 5rem; }
    .my-xl-auto { margin-top: auto; margin-bottom: auto; }
    .p-xl-0 { padding: 0; }
    .p-xl-1 { padding: .25rem; }
    .p-xl-2 { padding: .5rem; }
    .p-xl-3 { padding: 1rem; }
    .p-xl-4 { padding: 1.5rem; }
    .p-xl-5 { padding: 3rem; }
    .p-xl-6 { padding: 4rem; }
    .p-xl-7 { padding: 5rem; }
    .pt-xl-0 { padding-top: 0; }
    .pt-xl-1 { padding-top: .25rem; }
    .pt-xl-2 { padding-top: .5rem; }
    .pt-xl-3 { padding-top: 1rem; }
    .pt-xl-4 { padding-top: 1.5rem; }
    .pt-xl-5 { padding-top: 3rem; }
    .pt-xl-6 { padding-top: 4rem; }
    .pt-xl-7 { padding-top: 5rem; }
    .pb-xl-0 { padding-bottom: 0; }
    .pb-xl-1 { padding-bottom: .25rem; }
    .pb-xl-2 { padding-bottom: .5rem; }
    .pb-xl-3 { padding-bottom: 1rem; }
    .pb-xl-4 { padding-bottom: 1.5rem; }
    .pb-xl-5 { padding-bottom: 3rem; }
    .pb-xl-6 { padding-bottom: 4rem; }
    .pb-xl-7 { padding-bottom: 5rem; }
    .ps-xl-0 { padding-inline-start: 0; }
    .ps-xl-1 { padding-inline-start: .25rem; }
    .ps-xl-2 { padding-inline-start: .5rem; }
    .ps-xl-3 { padding-inline-start: 1rem; }
    .ps-xl-4 { padding-inline-start: 1.5rem; }
    .ps-xl-5 { padding-inline-start: 3rem; }
    .ps-xl-6 { padding-inline-start: 4rem; }
    .ps-xl-7 { padding-inline-start: 5rem; }
    .pe-xl-0 { padding-inline-end: 0; }
    .pe-xl-1 { padding-inline-end: .25rem; }
    .pe-xl-2 { padding-inline-end: .5rem; }
    .pe-xl-3 { padding-inline-end: 1rem; }
    .pe-xl-4 { padding-inline-end: 1.5rem; }
    .pe-xl-5 { padding-inline-end: 3rem; }
    .pe-xl-6 { padding-inline-end: 4rem; }
    .pe-xl-7 { padding-inline-end: 5rem; }
    .px-xl-0 { padding-inline: 0; }
    .px-xl-1 { padding-inline: .25rem; }
    .px-xl-2 { padding-inline: .5rem; }
    .px-xl-3 { padding-inline: 1rem; }
    .px-xl-4 { padding-inline: 1.5rem; }
    .px-xl-5 { padding-inline: 3rem; }
    .px-xl-6 { padding-inline: 4rem; }
    .px-xl-7 { padding-inline: 5rem; }
    .py-xl-0 { padding-top: 0; padding-bottom: 0; }
    .py-xl-1 { padding-top: .25rem; padding-bottom: .25rem; }
    .py-xl-2 { padding-top: .5rem; padding-bottom: .5rem; }
    .py-xl-3 { padding-top: 1rem; padding-bottom: 1rem; }
    .py-xl-4 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
    .py-xl-5 { padding-top: 3rem; padding-bottom: 3rem; }
    .py-xl-6 { padding-top: 4rem; padding-bottom: 4rem; }
    .py-xl-7 { padding-top: 5rem; padding-bottom: 5rem; }
  }
  @media (min-width: 1400px) {
    .m-xxl-0 { margin: 0; }
    .m-xxl-1 { margin: .25rem; }
    .m-xxl-2 { margin: .5rem; }
    .m-xxl-3 { margin: 1rem; }
    .m-xxl-4 { margin: 1.5rem; }
    .m-xxl-5 { margin: 3rem; }
    .m-xxl-6 { margin: 4rem; }
    .m-xxl-7 { margin: 5rem; }
    .m-xxl-auto { margin: auto; }
    .mt-xxl-0 { margin-top: 0; }
    .mt-xxl-1 { margin-top: .25rem; }
    .mt-xxl-2 { margin-top: .5rem; }
    .mt-xxl-3 { margin-top: 1rem; }
    .mt-xxl-4 { margin-top: 1.5rem; }
    .mt-xxl-5 { margin-top: 3rem; }
    .mt-xxl-6 { margin-top: 4rem; }
    .mt-xxl-7 { margin-top: 5rem; }
    .mt-xxl-auto { margin-top: auto; }
    .mb-xxl-0 { margin-bottom: 0; }
    .mb-xxl-1 { margin-bottom: .25rem; }
    .mb-xxl-2 { margin-bottom: .5rem; }
    .mb-xxl-3 { margin-bottom: 1rem; }
    .mb-xxl-4 { margin-bottom: 1.5rem; }
    .mb-xxl-5 { margin-bottom: 3rem; }
    .mb-xxl-6 { margin-bottom: 4rem; }
    .mb-xxl-7 { margin-bottom: 5rem; }
    .mb-xxl-auto { margin-bottom: auto; }
    .ms-xxl-0 { margin-inline-start: 0; }
    .ms-xxl-1 { margin-inline-start: .25rem; }
    .ms-xxl-2 { margin-inline-start: .5rem; }
    .ms-xxl-3 { margin-inline-start: 1rem; }
    .ms-xxl-4 { margin-inline-start: 1.5rem; }
    .ms-xxl-5 { margin-inline-start: 3rem; }
    .ms-xxl-6 { margin-inline-start: 4rem; }
    .ms-xxl-7 { margin-inline-start: 5rem; }
    .ms-xxl-auto { margin-inline-start: auto; }
    .me-xxl-0 { margin-inline-end: 0; }
    .me-xxl-1 { margin-inline-end: .25rem; }
    .me-xxl-2 { margin-inline-end: .5rem; }
    .me-xxl-3 { margin-inline-end: 1rem; }
    .me-xxl-4 { margin-inline-end: 1.5rem; }
    .me-xxl-5 { margin-inline-end: 3rem; }
    .me-xxl-6 { margin-inline-end: 4rem; }
    .me-xxl-7 { margin-inline-end: 5rem; }
    .me-xxl-auto { margin-inline-end: auto; }
    .mx-xxl-0 { margin-inline: 0; }
    .mx-xxl-1 { margin-inline: .25rem; }
    .mx-xxl-2 { margin-inline: .5rem; }
    .mx-xxl-3 { margin-inline: 1rem; }
    .mx-xxl-4 { margin-inline: 1.5rem; }
    .mx-xxl-5 { margin-inline: 3rem; }
    .mx-xxl-6 { margin-inline: 4rem; }
    .mx-xxl-7 { margin-inline: 5rem; }
    .mx-xxl-auto { margin-inline: auto; }
    .my-xxl-0 { margin-top: 0; margin-bottom: 0; }
    .my-xxl-1 { margin-top: .25rem; margin-bottom: .25rem; }
    .my-xxl-2 { margin-top: .5rem; margin-bottom: .5rem; }
    .my-xxl-3 { margin-top: 1rem; margin-bottom: 1rem; }
    .my-xxl-4 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
    .my-xxl-5 { margin-top: 3rem; margin-bottom: 3rem; }
    .my-xxl-6 { margin-top: 4rem; margin-bottom: 4rem; }
    .my-xxl-7 { margin-top: 5rem; margin-bottom: 5rem; }
    .my-xxl-auto { margin-top: auto; margin-bottom: auto; }
    .p-xxl-0 { padding: 0; }
    .p-xxl-1 { padding: .25rem; }
    .p-xxl-2 { padding: .5rem; }
    .p-xxl-3 { padding: 1rem; }
    .p-xxl-4 { padding: 1.5rem; }
    .p-xxl-5 { padding: 3rem; }
    .p-xxl-6 { padding: 4rem; }
    .p-xxl-7 { padding: 5rem; }
    .pt-xxl-0 { padding-top: 0; }
    .pt-xxl-1 { padding-top: .25rem; }
    .pt-xxl-2 { padding-top: .5rem; }
    .pt-xxl-3 { padding-top: 1rem; }
    .pt-xxl-4 { padding-top: 1.5rem; }
    .pt-xxl-5 { padding-top: 3rem; }
    .pt-xxl-6 { padding-top: 4rem; }
    .pt-xxl-7 { padding-top: 5rem; }
    .pb-xxl-0 { padding-bottom: 0; }
    .pb-xxl-1 { padding-bottom: .25rem; }
    .pb-xxl-2 { padding-bottom: .5rem; }
    .pb-xxl-3 { padding-bottom: 1rem; }
    .pb-xxl-4 { padding-bottom: 1.5rem; }
    .pb-xxl-5 { padding-bottom: 3rem; }
    .pb-xxl-6 { padding-bottom: 4rem; }
    .pb-xxl-7 { padding-bottom: 5rem; }
    .ps-xxl-0 { padding-inline-start: 0; }
    .ps-xxl-1 { padding-inline-start: .25rem; }
    .ps-xxl-2 { padding-inline-start: .5rem; }
    .ps-xxl-3 { padding-inline-start: 1rem; }
    .ps-xxl-4 { padding-inline-start: 1.5rem; }
    .ps-xxl-5 { padding-inline-start: 3rem; }
    .ps-xxl-6 { padding-inline-start: 4rem; }
    .ps-xxl-7 { padding-inline-start: 5rem; }
    .pe-xxl-0 { padding-inline-end: 0; }
    .pe-xxl-1 { padding-inline-end: .25rem; }
    .pe-xxl-2 { padding-inline-end: .5rem; }
    .pe-xxl-3 { padding-inline-end: 1rem; }
    .pe-xxl-4 { padding-inline-end: 1.5rem; }
    .pe-xxl-5 { padding-inline-end: 3rem; }
    .pe-xxl-6 { padding-inline-end: 4rem; }
    .pe-xxl-7 { padding-inline-end: 5rem; }
    .px-xxl-0 { padding-inline: 0; }
    .px-xxl-1 { padding-inline: .25rem; }
    .px-xxl-2 { padding-inline: .5rem; }
    .px-xxl-3 { padding-inline: 1rem; }
    .px-xxl-4 { padding-inline: 1.5rem; }
    .px-xxl-5 { padding-inline: 3rem; }
    .px-xxl-6 { padding-inline: 4rem; }
    .px-xxl-7 { padding-inline: 5rem; }
    .py-xxl-0 { padding-top: 0; padding-bottom: 0; }
    .py-xxl-1 { padding-top: .25rem; padding-bottom: .25rem; }
    .py-xxl-2 { padding-top: .5rem; padding-bottom: .5rem; }
    .py-xxl-3 { padding-top: 1rem; padding-bottom: 1rem; }
    .py-xxl-4 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
    .py-xxl-5 { padding-top: 3rem; padding-bottom: 3rem; }
    .py-xxl-6 { padding-top: 4rem; padding-bottom: 4rem; }
    .py-xxl-7 { padding-top: 5rem; padding-bottom: 5rem; }
  }

  /* ---- Helpers (the set the SOP names + audit-confirmed) --------- */
  .w-25 { width: 25%; } .w-50 { width: 50%; } .w-75 { width: 75%; } .w-100 { width: 100%; }
  .h-100 { height: 100%; }
  .mw-100 { max-width: 100%; }
  .overflow-hidden { overflow: hidden; }
  .position-relative { position: relative; }
  .position-absolute { position: absolute; }
  /* ---- Radius, GENERATED from the role list (ticket 21) -----------
     Six radius ROLES shipped and four radius UTILITIES did, so --radius-xs and
     --radius-xl had no stop at all. Pilot 02 had five consumers and §13 wrote
     `.rounded-xl`, checked, and removed it — the class resolved to nothing and
     would have failed silently, which is the same defect class as a contrast
     ratio typed into a comment.
     Generated by iterating the roles, so the set cannot go partial again: a
     new radius role gets a utility in the same commit by construction, which
     is the property that makes this whole file trustworthy. `.rounded` stays
     as the unsuffixed alias for md, because that is the Bootstrap name the
     team types from habit. */
  .rounded-xs { border-radius: var(--radius-xs); }
  .rounded-sm { border-radius: var(--radius-sm); }
  .rounded-md { border-radius: var(--radius-md); }
  .rounded-lg { border-radius: var(--radius-lg); }
  .rounded-xl { border-radius: var(--radius-xl); }
  .rounded-pill { border-radius: var(--radius-pill); }
  .rounded { border-radius: var(--radius-md); }

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

  /* ---- The link-affordance opt-out (v0.4.0, ticket 20) -------------
     Anchors underline by default (see @layer base). A site-owned anchor that
     is a CONTROL rather than prose — a nav item, a card wrapper, a lone icon
     link — says so here. Named, so it greps; one rule, so the Bootstrap
     alias cannot drift away from it.
     This is not a way out of SC 1.4.1: the rendered gate measures every
     anchor in running text and fails a bare one, whichever class removed the
     underline. The hatch exists for anchors that are not prose, and the
     prose predicate — not the author — decides which those are. */
  .link-bare, .text-decoration-none { text-decoration: none; }
  .link-underline, .text-decoration-underline { text-decoration-line: underline; }
  .visually-hidden,
  .visually-hidden-focusable:not(:focus):not(:focus-within) {
    position: absolute !important; /* the one deliberate !important in the core:
      a11y helper must survive any layered override; documented exception */
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip-path: inset(50%);
    white-space: nowrap; border: 0;
  }
  .stretched-link::after {
    position: absolute; inset: 0; z-index: 1; content: "";
  }
  /* Was a hand-installed per-project patch; now core (retires SOP guardrail).
     44×44 is WCAG 2.1 SC 2.5.5 Target Size (AAA) — the same criterion .btn and
     every form control clear. This utility is the floor for the controls the
     core does NOT own: a bare icon link, a site-authored toggle. It shipped at
     40px through v0.3.0 because it lives in a different file from the seven
     floors that moved, and nothing measured it — no demo carried the class, so
     the rendered gate had nothing to find. The gate now measures it. */
  .tap-target { position: relative; }
  .tap-target::before {
    content: ""; position: absolute; inset: 50% auto auto 50%;
    translate: -50% -50%; width: max(100%, 44px); height: max(100%, 44px);
  }

  /* ---- Added v0.3.1 by Pilot 02 (Climatech on core) ----------------
     These four were NOT in the known-gaps table, and that is the finding:
     Pilot 01 built this same comp and hand-rolled all four inside its
     261-line site.css — object-fit 5x, list-style:none 3x, aspect-ratio
     3x, white-space:nowrap 1x. Twelve re-implementations of primitives
     the framework should own, on ONE page, invisible to every instrument
     because nothing measures "how often did site.css rebuild a utility."
     They are Bootstrap-compatible names on purpose, like the rest of this
     file: the team types them from habit and they must not silently
     resolve to nothing.
     Of the two Helpers-ticket primitives, .vr has now landed (below) and the
     stacks have not. Nothing in four pilots needs a stack, and a utility
     nobody has needed is a guess. */
  .list-unstyled { padding-left: 0; list-style: none; }

  /* .vr — v0.4.1, Pilot 04 (E-111), and the rule it was held under is the
     reason it is here now: "add them when a build actually needs one." A build
     needs one. Pilot 04's Hearth band separates six cells with a 2px rule down
     the right of every cell but the last, which is the ordinary divider-rail
     shape, and the core had neither this nor a per-side border utility — so it
     cost site CSS. The per-side borders live beside .border in
     core.surfaces.css; this is the primitive the design actually asked for.

     Bootstrap draws it with a background and an opacity. This draws it with a
     BORDER in --border, so it is the same ink as every other divider the core
     paints and it follows whichever publisher it lands inside — a rail on a
     brand band gets that band's ink, not the page ground's grey at 1.46:1,
     which is the defect ticket 19 fixed for the accordion. An opacity would
     have re-created it: a faded copy of a colour chosen for a different
     surface is still a colour chosen for a different surface.

     align-self: stretch is what makes it a rail rather than a 1px dot — it has
     no content, so without it a flex item collapses to zero height. */
  .vr {
    display: inline-block;
    align-self: stretch;
    width: 0;
    min-height: 1em;
    border-inline-start: 1px solid var(--border);
  }

  /* Bootstrap 5.3's full value set, deliberately. A PARTIAL set is worse
     than none: `object-fit-none` would resolve to nothing and fail silent,
     which is the same class of defect as a contrast ratio typed into a
     comment. Dimensions stay the caller's job (w-100 / h-100) — object-fit
     with no box to fit is a no-op, and conflating them would hide that. */
  .object-fit-contain { object-fit: contain; }
  .object-fit-cover   { object-fit: cover; }
  .object-fit-fill    { object-fit: fill; }
  .object-fit-scale   { object-fit: scale-down; }
  .object-fit-none    { object-fit: none; }

  /* Real `aspect-ratio`, not Bootstrap's padding-top ::before hack. The
     core already requires color-mix(), oklch(), :user-invalid and the
     `translate` property, so the browser floor is well above this.
     .ratio must be declared BEFORE its modifiers — they are the same
     specificity, so source order is what lets .ratio-16x9 win. */
  .ratio { position: relative; width: 100%; aspect-ratio: 1 / 1; }
  .ratio > * { position: absolute; inset: 0; width: 100%; height: 100%; }
  .ratio-1x1  { aspect-ratio: 1 / 1; }
  .ratio-4x3  { aspect-ratio: 4 / 3; }
  .ratio-16x9 { aspect-ratio: 16 / 9; }
  .ratio-21x9 { aspect-ratio: 21 / 9; }

  .text-nowrap { white-space: nowrap; }
}

/* ---------- core.typography.css ---------- */
/* ==========================================================================
   RBMG Core — typography (heading aliases, body defaults, type utilities)
   core.typography.css · v0.2.0-draft
   Consumes only Tier 0B tokens. API mirrors the shipped sites' utility
   surface (audited on the reference brand), so existing habits transfer.
   ========================================================================== */

@layer base {
  /* Heading class aliases — same treatment as the elements (foundation) */
  .h1, .h2, .h3, .h4, .h5, .h6 {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-bold);
    text-transform: var(--heading-transform, uppercase);
    margin-bottom: 0.5rem;
  }
  h1, h2, h3, h4, h5, h6 { margin-bottom: 0.5rem; }
  .h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); }
  .h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); }
  .h3 { font-size: var(--fs-h3); line-height: var(--lh-h3); }
  .h4 { font-size: var(--fs-h4); line-height: var(--lh-h4); }
  .h5 { font-size: var(--fs-h5); line-height: var(--lh-h5); }
  h6, .h6 {
    font-size: var(--fs-h5); line-height: var(--lh-h5);
    font-weight: var(--font-weight-semibold);
  }

  /* THE `.regular` HEADING VARIANT IS GONE — v0.4.1, Pilot 04 (E-091), and it
     never worked. This block used to read:

       h1.regular, .h1.regular, … h5.regular, .h5.regular
         { font-weight: var(--font-weight-semibold); }

     commented "the Figma Semibold pair, every level" — in @layer base, while
     @layer utilities declares `.regular { font-weight: var(--font-weight-
     regular) }`. A later layer wins REGARDLESS OF SPECIFICITY, so `.h4.regular`
     has measured 400 at every level since the file was written. Two core rules,
     one class name, opposite intent, and the one that loses is the documented
     one. Same shape as a core utility outranking a core component (E-073) and
     as `.btn` inside a dropdown menu — one file over.

     DELETING IT IS THE ONLY BEHAVIOUR-IDENTICAL FIX, which is what makes it
     safe to do without auditing every project: `.h4.regular` rendered 400
     before and renders 400 after. Nothing can be leaning on the semibold
     behaviour because nothing has ever been able to obtain it. The variant that
     was meant is `.semibold`, which exists three rules down, which Pilot 04
     used, and which measures 600.

     roles.test.mjs now asserts the general case — no selector in @layer base
     may set a property that a same-named class in @layer utilities also sets —
     because the defect is not this class, it is that two layers can disagree
     about one name and nothing says so. */

  /* Default body copy sizing.

     TICKET 18 — `ul li, ol li` USED TO BE IN THIS SELECTOR, and it made every
     container-level size utility silently inert for list items. A directly
     matched rule beats inheritance from any container in any layer, so
     `.label-sm` or `.p-sm` on a <ul> did nothing to its <li>s and the author
     had to restate the size on the item. Pilot 02 shipped that wrong in two
     sections — a plan list at 16px against a 14px intent for four commits,
     and a location list whose .label-sm never applied.

     Items now INHERIT, which is all they ever needed: an <li> inherits from
     its list, which inherits from <body>, which sets exactly these values. The
     rule was not providing the size; it was preventing the override.

     `p` and `table` are the same shape and are deliberately left: the
     container-utility case for them has not been OBSERVED, and this file's
     house rule is to seed from measured violations rather than a theoretical
     complete set. If a build hits it on a <p>, that is the same fix. */
  p, table {
    font-size: var(--fs-p-md);
    line-height: var(--lh-p-md);
    font-weight: var(--font-weight-regular);
  }
}

@layer utilities {
  /* ---- Display sizes --------------------------------------------- */
  .display-lg {
    font-size: var(--fs-display-lg); line-height: var(--lh-display-lg);
    font-family: var(--font-heading); font-weight: var(--font-weight-bold);
    text-transform: var(--heading-transform, uppercase);
  }
  .display-sm {
    font-size: var(--fs-display-sm); line-height: var(--lh-display-sm);
    font-family: var(--font-heading); font-weight: var(--font-weight-bold);
    text-transform: var(--heading-transform, uppercase);
  }

  /* ---- Paragraph sizes (suffix distinguishes from padding .p-N) --- */
  .p-lg { font-size: var(--fs-p-lg); line-height: var(--lh-p-lg); }
  .p-md { font-size: var(--fs-p-md); line-height: var(--lh-p-md); }
  .p-sm { font-size: var(--fs-p-sm); line-height: var(--lh-p-sm); }
  .p-xs { font-size: var(--fs-p-xs); line-height: var(--lh-p-xs); }

  /* ---- Labels (label font, bold, uppercase) ----------------------- */
  .label-lg, .label-md, .label-sm, .label-xs {
    font-family: var(--font-label);
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
  }
  .label-lg { font-size: var(--fs-label-lg); line-height: var(--lh-label-lg); }
  .label-md { font-size: var(--fs-label-md); line-height: var(--lh-label-md); }
  .label-sm { font-size: var(--fs-label-sm); line-height: var(--lh-label-sm); }
  .label-xs { font-size: var(--fs-label-xs); line-height: var(--lh-label-xs); }

  /* ---- Overlines (the eyebrow above a heading) ---------------------
     v0.3.0 — family, slant, weight, tracking and transform are all roles
     now (Pilot 01, F-008). Label and overline utilities both bound to
     --font-label, and the pilot's brand used two visually unrelated
     families for those jobs in every section of its comp: a black sans
     for labels, a black extended ITALIC SERIF for eyebrows. One role
     cannot answer both, a brand may not define a fourth role, and the
     italic is structure so BRAND could not add it either. The eyebrow
     appeared in ~8 sections and was logged unbuildable at brand tier.

     --font-overline falls back to --font-label, so a brand with one
     family answers nothing and gets exactly today's behaviour. The style
     knobs follow the --heading-transform precedent: value-shaped levers
     into core structure, so "our eyebrow is a different typeface" stops
     being a reason to write CSS in site.css. */
  .overline-lg, .overline-sm {
    font-family: var(--font-overline, var(--font-label));
    font-weight: var(--overline-weight, var(--font-weight-black));
    font-style: var(--overline-style, normal);
    letter-spacing: var(--overline-tracking, normal);
    text-transform: var(--overline-transform, uppercase);
  }
  .overline-lg { font-size: var(--fs-overline-lg); line-height: var(--lh-overline-lg); }
  .overline-sm { font-size: var(--fs-overline-sm); line-height: var(--lh-overline-sm); }

  /* ---- Families --------------------------------------------------
     v0.4.1, Pilot 04 (E-117) — the axis that had no utility.

     Three core controls set their typography from a font role: .btn and the
     collapse toggle take --font-heading, .nav-link takes --font-label. Two
     tickets said that was a defect and both were WRONG: E-068 and E-075 died
     because `.label-md` sets family, weight, size and transform and beats the
     component by layer, and the build's own service-area pills had been using
     it the whole time.

     E-117 is the one that survives, and re-testing it the same way is what
     shows why. Pilot 04's FAQ question is Paragraph/Large — the BODY family —
     and a question IS body copy. `.p-lg` recovers the size, `.bold` recovers
     the weight, and `.label-*` cannot help because it supplies --font-label,
     not --font-body. FAMILY IS THE ONE AXIS OF THE FOUR WITH NO UTILITY AT
     ALL, so the cost was one declaration of site CSS and no way to avoid it.

     So the components are fine and the diagnosis in the ticket ("the core
     routes every control's typography through the heading roles") is the wrong
     frame: the utility surface was incomplete. All four roles ship, not the
     one that was needed — the .object-fit-* precedent. A partial set is worse
     than none, because `.font-overline` resolving to nothing fails silently,
     which is the same class of defect as a ratio typed into a comment. */
  .font-heading  { font-family: var(--font-heading); }
  .font-body     { font-family: var(--font-body); }
  .font-label    { font-family: var(--font-label); }
  .font-overline { font-family: var(--font-overline, var(--font-label)); }

  /* ---- Weights ---------------------------------------------------- */
  .black    { font-weight: var(--font-weight-black); }
  .bold     { font-weight: var(--font-weight-bold); }
  .semibold { font-weight: var(--font-weight-semibold); }
  .medium   { font-weight: var(--font-weight-medium); }
  .regular  { font-weight: var(--font-weight-regular); }

  /* ---- Icon sizes (v0.4.0, ticket 8) -----------------------------
     A glyph is not text, so it does not take a --fs-* role. See
     core.tokens.css §ICON SIZE for why binding one to a heading size is the
     --on-inverse-as-a-fill mistake one level over.

     `font-size` sizes an icon-font <i> and an SVG authored at 1em alike, which
     is both idioms the estate uses. An SVG with fixed width/height attributes
     is sized by those, not by this — give it width:1em;height:1em or use the
     .ratio utility.

     inline-size and centring come with the size on purpose. Nine of the
     pilot's ten instances paired a glyph size with a slightly larger box so a
     column of icons optically aligns whatever the glyph's own width is; that
     pairing was hand-written every time, which is what made it ten findings
     instead of one. --icon-box scales with the glyph rather than being a
     second literal to keep in sync. */
  .icon-xs, .icon-sm, .icon-md, .icon-lg, .icon-xl, .icon-2xl {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--icon-box, 1.25em);
    flex-shrink: 0;
  }
  .icon-xs  { font-size: var(--icon-xs); }
  .icon-sm  { font-size: var(--icon-sm); }
  .icon-md  { font-size: var(--icon-md); }
  .icon-lg  { font-size: var(--icon-lg); }
  .icon-xl  { font-size: var(--icon-xl); }
  .icon-2xl { font-size: var(--icon-2xl); }   /* v0.4.1, E-110 — 48 */

  /* ---- Legacy Bootstrap-extension sizes (all six ship them) ------- */
  .fs-7 { font-size: var(--fs-p-sm); line-height: var(--lh-p-sm); }
  .fs-8 { font-size: var(--fs-p-xs); line-height: var(--lh-p-xs); }

  /* ---- Misc ------------------------------------------------------- */
  .text-uppercase { text-transform: uppercase; }
  .text-none      { text-transform: none; }
  .text-truncate  { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* ---------- core.surfaces.css ---------- */
/* ==========================================================================
   RBMG Core — color & surface engine
   core.surfaces.css · v0.2.0-draft

   Three jobs:
     1. Role-generated color utilities. No build step, no per-brand
        generation: utilities reference roles, roles resolve per brand at
        runtime. The same static file serves every brand — stronger than
        the Sass approach that motivated "generated" in the tier map.
     2. The surface-aware PUBLISHER side. Every core background utility
        publishes --link / --link-hover / --icon-accent values legible
        against itself, at :where() specificity 0 (a pattern already
        proven on audited production sites). The consumer side (bare `a`) lives in
        foundation's base layer; with @layer, any utility or component
        color wins over it by cascade position — no :not([class])
        gymnastics needed.
     3. Contrast by default. Publishers move hover toward MORE contrast
        (darken on light, fade on dark), and on-colors are contrast-gated
        roles, so the per-site "WCAG safety net" install becomes a
        verification instead of a patch.

   Deliberately not carried: `text-bg-*` (the combo-utility trap — asked
   for text color, silently sets a surface). Its jobs are .bg-* (which
   handles its own text) and .text-* (which touches only text).
   ========================================================================== */

@layer utilities {
  /* ---- Text ------------------------------------------------------- */
  .text-primary   { color: var(--brand-primary); }
  .text-secondary { color: var(--brand-secondary); }
  .text-accent    { color: var(--brand-accent); }
  .text-body      { color: var(--on-surface); }
  .text-muted     { color: var(--muted); }
  .text-inverse   { color: var(--on-inverse); }
  .text-inverse-muted { color: var(--on-inverse-muted); }
  /* THE TWO PUBLISHED ROLES THAT HAD NO UTILITY — v0.4.1, Pilot 04 (E-067).
     The publishers below set three surface-aware values: --link, --icon-accent
     and --muted. Only --muted had a utility, and nothing followed the
     precedent, so a site wanting the accent ink the core had already computed
     for the band it is standing on had to write the rule itself. Pilot 04
     wrote it twice (`.gsp-overline`, `.gsp-icon-accent`), which is the tell:
     the core had the RIGHT VALUE and no selector for it.

     A BRAND CANNOT CLOSE THIS AND THAT IS WHY IT IS A UTILITY. Answering a
     role with `var(--icon-accent)` at :root looks equivalent and is the
     --on-gradient failure: a custom property is substituted on the element
     that DECLARES it, so the page ground's accent freezes on <html> and every
     band inherits it already-resolved. Read at the point of use, it follows
     whichever publisher is nearest, which is the whole point of the system. */
  .text-icon-accent { color: var(--icon-accent); }
  .text-link        { color: var(--link); }

  /* ---- Surfaces --------------------------------------------------
     Each sets its own readable text color and publishes the link/icon
     pair below. Composition never pairs them by hand. */
  .bg-surface        { background-color: var(--surface);         color: var(--on-surface); }
  .bg-surface-raised { background-color: var(--surface-raised);  color: var(--on-surface); }
  .bg-surface-sunken { background-color: var(--surface-sunken);  color: var(--on-surface); }
  /* The second sunken step — v0.4.1, Pilot 04 (E-114). See core.tokens.css
     §THE SECOND SUNKEN STEP: one role, and a comp that paints two greys in the
     two sections bracketing the page's lower half. */
  .bg-surface-sunken-strong { background-color: var(--surface-sunken-strong); color: var(--on-surface); }
  .bg-inverse        { background-color: var(--surface-inverse); color: var(--on-inverse); }
  .bg-primary        { background-color: var(--brand-primary);   color: var(--on-primary); }
  .bg-secondary      { background-color: var(--brand-secondary); color: var(--on-secondary); }
  .bg-accent         { background-color: var(--brand-accent);    color: var(--on-accent); }

  /* ---- Gradient surfaces ------------------------------------------
     v0.3.0 — REBUILT (Pilot 01, F-009). See core.tokens.css §GRADIENT for
     why the core stopped modelling the image and models only the colour
     text has to survive.

     .bg-gradient paints whatever --gradient holds — a two-stop linear, a
     16-stop conic, an SVG fill, anything CSS can express. The angle
     utilities below are the common two-stop case kept as a convenience;
     they now only SET --gradient, so every gradient on the site renders
     and publishes through one code path.

     A section needing a brand's named gradient selects it in site.css:

         .hero { --gradient: var(--gradient-hero);
                 --gradient-ref: var(--gradient-hero-ref); }

     — composition choosing among values the brand declared, which is
     exactly what the SITE tier is for. No colour stop ever appears in
     site.css.

     NOTE what changed in the label colour. v0.1.0 set `color:
     var(--on-secondary)` here — a text role borrowed from an unrelated
     hue, correct only while --gradient-from happened to be the secondary.
     That is the same shape as the chain that rendered Pilot 01's mobile
     hamburger invisible, still present a layer up. It now derives from
     --gradient-ref, which is the thing it is actually measured against. */
  .bg-gradient,
  .bg-gradient-0, .bg-gradient-45, .bg-gradient-90,
  .bg-gradient-135, .bg-gradient-180, .bg-gradient-270 {
    background-image: var(--gradient);
    color: var(--on-gradient);
  }
  /* THE ANGLE UTILITIES SET AN ANGLE, not a whole image — v0.4.0, ticket 11.

     They used to rebuild the two-stop default from scratch, which meant they
     worked ONLY on the default: a section selecting a brand's named gradient
     and then asking for it at 90deg silently got the core's secondary→primary
     ramp instead, losing the brand entirely. Pilot 02 wanted the same named
     gradient at two angles and had no way to say so.

     Setting --gradient-angle instead makes the utility compose with whatever
     --gradient holds. A brand opts a named gradient into being re-angleable by
     writing the variable into its own declaration:

         --gradient-steel: linear-gradient(var(--gradient-angle, 135deg), …);

     and a conic or 16-stop gradient that cannot meaningfully take an angle
     simply does not write it — the utility then has nothing to act on, which
     is the honest outcome rather than a broken one. No colour stop moves into
     site.css either way, which is the constraint that matters. */
  .bg-gradient-0   { --gradient-angle: 0deg; }
  .bg-gradient-45  { --gradient-angle: 45deg; }
  .bg-gradient-90  { --gradient-angle: 90deg; }
  .bg-gradient-135 { --gradient-angle: 135deg; }
  .bg-gradient-180 { --gradient-angle: 180deg; }
  .bg-gradient-270 { --gradient-angle: 270deg; }

  /* ---- Borders -----------------------------------------------------
     PER-SIDE, v0.4.1 — Pilot 04 (E-111). The set was `.border` (all four
     sides) plus five COLOUR modifiers, and nothing for a single edge. Pilot
     04's Hearth band separates six cells with a 2px rule on the right of every
     cell but the last, which is the commonest divider-rail shape there is, and
     it cost site CSS.

     The gap sits inside a promise the core already made: Bootstrap ships
     .border-top / -end / -bottom / -start and their -0 forms, and this core's
     stated convention is Bootstrap-compatible names for grid and utilities ON
     PURPOSE, so the team types them from habit. A name that resolves to
     nothing fails silently, which is why the full eight ship rather than the
     one a build needed — the .object-fit-* precedent.

     Logical properties, matching the rest of the file (.breadcrumb, the tab
     rail, the drawer all use inline/block), so a divider rail is correct in a
     right-to-left document without a second set of names. */
  .border           { border: 1px solid var(--border); }
  .border-0         { border: 0; }
  .border-top       { border-block-start:  1px solid var(--border); }
  .border-end       { border-inline-end:   1px solid var(--border); }
  .border-bottom    { border-block-end:    1px solid var(--border); }
  .border-start     { border-inline-start: 1px solid var(--border); }
  .border-top-0     { border-block-start:  0; }
  .border-end-0     { border-inline-end:   0; }
  .border-bottom-0  { border-block-end:    0; }
  .border-start-0   { border-inline-start: 0; }
  .border-strong    { border-color: var(--border-strong); }
  .border-primary   { border-color: var(--brand-primary); }
  .border-secondary { border-color: var(--brand-secondary); }
  .border-accent    { border-color: var(--brand-accent); }
  /* currentColor, not --on-inverse: same reasoning as .btn-outline-inverse.
     A border meant to read against its surface should adopt that
     surface's text colour rather than name one polarity. */
  .border-inverse   { border-color: currentColor; }

  /* ==================================================================
     PUBLISHERS — link & icon by parent surface
     Custom properties inherit, so the NEAREST surface always wins: a
     white card nested in a dark band gets light-surface links (the
     production failure this retires: flat .bg-* rules painting nested
     card links white-on-white). :where() keeps specificity at 0.
     Site sheets with bespoke full-bleed surfaces don't extend these
     selector lists (that's how one audited production publisher list
     grew 14 site classes long) — a
     bespoke section gets a core .bg-* utility alongside its site class,
     and publishing comes with it.
     ================================================================== */

  /* v0.2.0 — each publisher re-derives --contrast-toward FROM ITS OWN
     FILL, replacing two hardcoded light/dark selector lists.

     Pilot 01 broke the old model twice. `.bg-surface` was hardcoded
     "light" and always darkened, so on a dark-ground brand every hover
     REDUCED contrast (--link 16.30:1 → --link-hover 10.15:1) while still
     clearing 4.5:1 — invisible to any threshold gate. And `.bg-inverse`
     was hardcoded "dark", so a brand whose inverse surface is a light
     island published white links onto it at 1.14:1, with no brand-tier
     fix available because .bg-primary, .bg-secondary and all six
     gradients shared that list. */

  /* Page ground and neutral surfaces — derive from --surface.

     v0.4.0 — THE COMPONENT SELECTORS ARE NEW, and ticket 5 turned out to be
     bigger than "the dropdown menu paints --surface-raised from no publisher
     list." THREE core components paint a surface role and published nothing:
     the dropdown menu, the modal dialog, and .card. So a bare
     <div class="card"> inside a .bg-primary band painted a white fill while
     INHERITING that band's published --link — which is --on-primary, i.e.
     white. A white link on a white card: the exact production failure the
     publisher system was built to retire, reintroduced by the core itself one
     layer down.

     demo.html hid it by writing `class="card bg-surface"` by hand, and that is
     the tell — the harness was working around the defect it exists to expose.

     The rule this generalises to, and a structural test now enforces it: a
     rule that paints a --surface* role IS a surface, and a surface publishes.
     There is no third option where something paints a fill and leaves its
     contents reading whatever the fill above it published.

     KNOWN RESIDUAL, stated rather than hidden: this group derives direction
     from --surface, so a brand tinting --surface-raised DARK would get
     page-ground links on a dark card. That gap predates this change —
     .bg-surface-raised was already in this list — and closing it needs a
     --link-on-raised answer that no build has yet needed. */
  :where(body, .bg-surface, .bg-surface-raised, .bg-surface-sunken,
         .bg-surface-sunken-strong,
         .card,
         rbmg-dropdown [data-dropdown-menu],
         rbmg-modal [data-modal-dialog],
         rbmg-drawer [data-drawer-body],
         .icon-stacked.base-surface) {
    --contrast-toward: color(from var(--surface) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--link-on-surface);
    --link-hover:  color-mix(in srgb, var(--link-on-surface), var(--contrast-toward) 20%);
    --icon-accent: var(--icon-on-surface);
    --muted:         var(--on-surface-muted);
    /* THE SEAM, v0.4.1 — E-118. This block matches `body`, and a declaration on
       body beats an inherited :root value, so it was silently overriding every
       brand's answer to --border for the whole document. Reading the seam here
       is what makes a brand's answer actually paint. */
    --border:        var(--border-on-surface,         color-mix(in srgb, var(--contrast-toward), var(--surface) 90%));
    --border-strong: var(--border-strong-on-surface,  color-mix(in srgb, var(--contrast-toward), var(--surface) 80%));
    --border-control: var(--border-control-on-surface, var(--muted));
  }

  /* The inverse surface — derives from --surface-inverse whatever its
     brightness, so a light inverse island now publishes correctly. */
  :where(.bg-inverse, .icon-stacked.base-inverse) {
    --contrast-toward: color(from var(--surface-inverse) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--link-on-inverse);
    --link-hover:  color-mix(in srgb, var(--link-on-inverse), var(--contrast-toward) 20%);
    --icon-accent: var(--icon-on-inverse);
    --muted:         var(--on-inverse-muted);
    /* E-118 as filed: this block published four brand-facing values and only
       three were answerable. --link, --icon-accent and --muted each read a
       seam; the borders were a hard-coded derivation, so a comp drawing a rule
       a reader can actually see on a dark band (Pilot 04's FAQ: #DCDFE7,
       11.12:1 on its navy, against the core's 1.36:1) had no legal way to say
       so. The derivation stays as the default, so nothing shipped moves. */
    --border:        var(--border-on-inverse,         color-mix(in srgb, var(--contrast-toward), var(--surface-inverse) 90%));
    --border-strong: var(--border-strong-on-inverse,  color-mix(in srgb, var(--contrast-toward), var(--surface-inverse) 80%));
    --border-control: var(--border-control-on-inverse, var(--muted));
  }

  /* THE FILLED ACTIVE TAB — v0.4.0, Pilot 03.

     `rbmg-tabs[active="fill"]` lifts the selected tab onto its own fill, so
     it IS a surface and a surface publishes. It derives from
     --tab-active-fill rather than joining the --surface group above, for the
     reason the .bg-inverse block gives one comment up: a group that hardcodes
     its direction is wrong the moment a brand answers the seam with the other
     polarity, and that is the failure --surface-raised is still carrying.

     WHY THIS IS NOT COVERED BY THE RULE'S OWN `color`. The tab sets
     `color: var(--on-tab-active-fill)`, and that covers every descendant
     resolving currentColor — the label, an .icon-* glyph. It does NOT cover
     descendants that read a PUBLISHED role directly, and two of those land
     inside a tab in practice:

       --muted   a secondary line under the label. On a .bg-primary rail this
                 resolves to color-mix(--contrast-toward, --brand-primary 15%)
                 — a light ink mixed for the BAND — painted onto the pill.
       --border  a rule or chip edge inside the tab, same derivation, same
                 mistake.

     Checked and NOT the reason, stated so the next person does not re-derive
     it: every .icon-stacked.base-* variant appears in a publisher list of its
     own, so an <fa-stack> inside the tab re-publishes from its own fill and
     was never exposed. The honest justification is --muted and --border, not
     the icon chip — which is what the comp made me look at first.

     THE PUBLISHER IS SCOPED TO THE SELECTED TAB, not the rail. An unselected
     tab has no fill of its own and must keep reading the band it sits on. */
  :where(rbmg-tabs[active="fill"] [role="tab"][aria-selected="true"]) {
    /* --on-tab-active-fill IS RE-DECLARED HERE, and its absence was E-145 —
       Pilot 04, a live 1.6:1 painted under a green gate.

       core.tokens.css derives it at :root from var(--tab-active-fill,
       var(--surface)). At :root nothing answers the seam, so it resolved
       against the page ground — WHITE — yielding a dark label which was then
       inherited already-resolved. A site answering the DOCUMENTED seam moved
       the fill and left the label behind: measured black on Guardian's navy,
       ~1.6:1, on a control whose whole job is to say which step you are
       reading. Following the instruction is what produced the failure.

       THIRD INSTANCE OF THE FAMILY --on-gradient AND --focus-ring ALREADY
       NAME, and the guard written after those two could not see it. That test
       derives its list from roles whose value mentions --contrast-toward; this
       one mentions --tab-active-fill. The predicate that actually holds is
       broader, and roles.test.mjs now uses it: A ROLE DERIVED AT :root FROM AN
       UNDECLARED SEAM MUST BE RE-DECLARED WHERE THAT SEAM IS ANSWERED, because
       an undeclared seam exists precisely to be answered below :root.

       The three roles below it read it — --link, --link-hover, --icon-accent —
       were inheriting the same stale value, so this one line fixes four. */
    --on-tab-active-fill: color(from var(--tab-active-fill, var(--surface)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --contrast-toward: color(from var(--tab-active-fill, var(--surface)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--on-tab-active-fill);
    --link-hover:  color-mix(in srgb, var(--on-tab-active-fill), var(--contrast-toward) 20%);
    --icon-accent: var(--on-tab-active-fill);
    --muted:         color-mix(in srgb, var(--contrast-toward), var(--tab-active-fill, var(--surface)) 15%);
    --border:        color-mix(in srgb, var(--contrast-toward), var(--tab-active-fill, var(--surface)) 15%);
    --border-strong: color-mix(in srgb, var(--contrast-toward), var(--tab-active-fill, var(--surface)) 15%);
    --border-control: var(--muted);
  }

  /* Brand-colored surfaces — links adopt the gated on-colour of the fill
     they sit on, and direction derives from that same fill.

     THE MUTED SEAM — v0.4.0, Pilot 03. The 15% mix in each --muted below is
     the third tuning of that constant, and the gate's own comment records the
     history: 25% failed the dark reference at 4.32:1, 20% passed both
     references and failed Pilot 02's brand at 4.30:1, 15% cleared all three.
     Pilot 03's brand is the FOURTH, and its secondary (#D8352C, a
     mid-luminance red) breaks 15% at 3.78:1 — the fill's TOTAL headroom is
     4.7:1, so a constant that eats 20% of the ink's contrast cannot survive
     it, and no constant can: each retune was just moving which brand fails.

     So the derivation stays as the default and each band gains an UNDECLARED
     seam, fallback at the point of use, --on-inverse rule observed:

         --on-primary-muted / --on-secondary-muted / --on-accent-muted

     named to match --on-surface-muted / --on-inverse-muted, which are the
     same role on the neutral surfaces. A brand whose band has the headroom
     answers nothing; one whose band cannot afford a second ink tier answers
     the seam — and on a fill with 4.7:1 total, the only legal answer IS the
     on-colour itself, which is what "muted cannot exist here" looks like as
     a value. The gate resolves through the seam, so an answered value is
     measured and a bad one fails. */
  :where(.bg-primary, .icon-stacked.base-primary) {
    --contrast-toward: color(from var(--brand-primary) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--on-primary);
    --link-hover:  color-mix(in srgb, var(--on-primary), var(--contrast-toward) 20%);
    --icon-accent: var(--on-primary);
    --muted:         var(--on-primary-muted, color-mix(in srgb, var(--contrast-toward), var(--brand-primary) 15%));
    --border:        color-mix(in srgb, var(--contrast-toward), var(--brand-primary) 15%);
    --border-strong: color-mix(in srgb, var(--contrast-toward), var(--brand-primary) 15%);
    --border-control: var(--muted);
  }
  :where(.bg-secondary, .icon-stacked.base-secondary) {
    --contrast-toward: color(from var(--brand-secondary) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--on-secondary);
    --link-hover:  color-mix(in srgb, var(--on-secondary), var(--contrast-toward) 20%);
    --icon-accent: var(--on-secondary);
    --muted:         var(--on-secondary-muted, color-mix(in srgb, var(--contrast-toward), var(--brand-secondary) 15%));
    --border:        color-mix(in srgb, var(--contrast-toward), var(--brand-secondary) 15%);
    --border-strong: color-mix(in srgb, var(--contrast-toward), var(--brand-secondary) 15%);
    --border-control: var(--muted);
  }
  :where(.bg-accent, .icon-stacked.base-accent) {
    --contrast-toward: color(from var(--brand-accent) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--on-accent);
    --link-hover:  color-mix(in srgb, var(--on-accent), var(--contrast-toward) 20%);
    --icon-accent: var(--on-accent);
    --muted:         var(--on-accent-muted, color-mix(in srgb, var(--contrast-toward), var(--brand-accent) 15%));
    --border:        color-mix(in srgb, var(--contrast-toward), var(--brand-accent) 15%);
    --border-strong: color-mix(in srgb, var(--contrast-toward), var(--brand-accent) 15%);
    --border-control: var(--muted);
  }

  /* Gradients publish from --gradient-ref: the stop where text has the
     LEAST contrast, declared by the brand alongside the gradient itself.
     A gradient is the one surface whose fill is not a single colour, so
     "which colour is this surface?" has to be answered rather than read —
     and answering it with the worst stop is what makes the answer safe. */
  :where(.bg-gradient,
         .bg-gradient-0, .bg-gradient-45, .bg-gradient-90,
         .bg-gradient-135, .bg-gradient-180, .bg-gradient-270) {
    /* --on-gradient is RE-DECLARED here, not merely referenced, and that is
       load-bearing. A custom property's var() is substituted on the element
       that DECLARES it: --on-gradient at :root resolves against :root's
       --gradient-ref and is then inherited as an already-resolved colour. A
       section setting its own --gradient-ref would not re-derive it, so a
       brand's light gradient would keep the page ground's white label —
       white on a pale fill, which is the invisible-button failure again.
       Re-deriving here binds it to whichever ref actually applies.

       Caught in a browser, not by the gate: the gate resolves ROLE values
       and cannot see a per-element inheritance failure, so it happily
       measured the intent while the cascade did something else. Same shape
       as the lesson that started this file — a check can only fail on what
       it models. Every publisher re-derives --contrast-toward for exactly
       this reason; this is that rule applied to a second derived role. */
    --on-gradient: color(from var(--gradient-ref) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --contrast-toward: color(from var(--gradient-ref) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--on-gradient);
    --link-hover:  color-mix(in srgb, var(--on-gradient), var(--contrast-toward) 20%);
    --icon-accent: var(--on-gradient);
    --muted:         color-mix(in srgb, var(--contrast-toward), var(--gradient-ref) 15%);
    --border:        color-mix(in srgb, var(--contrast-toward), var(--gradient-ref) 15%);
    --border-strong: color-mix(in srgb, var(--contrast-toward), var(--gradient-ref) 15%);
    --border-control: var(--muted);
  }

  /* ==================================================================
     SCRIM — text over imagery (v0.2.0)

     Pilot 01 put text over photography in four of fourteen sections and
     found NO compliant path: every core surface was opaque, so the only
     expressible option was an rgba() in site.css — a raw colour outside
     the brand sheet, i.e. a Tier-2 violation. The most common
     composition pattern in the comp had no legal expression.

     A scrim is a translucent veil of the page ground. It publishes like
     any other surface, so text over a photo is governed by the same
     system as text anywhere else, and the mechanism keeping it legible
     is a measurable token rather than a hand-picked rgba().
     ================================================================== */
  .bg-scrim, .bg-scrim-strong, .bg-scrim-gradient {
    color: var(--on-surface);
    --contrast-toward: color(from var(--surface) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --focus-ring: var(--contrast-toward);
    --link:        var(--link-on-surface);
    --link-hover:  color-mix(in srgb, var(--link-on-surface), var(--contrast-toward) 20%);
    --icon-accent: var(--icon-on-surface);
    --muted:         var(--on-surface-muted);
    /* the same seam the neutral group reads — a scrim IS a veil of the page
       ground, so it takes the page ground's answer (E-118) */
    --border:        var(--border-on-surface,         color-mix(in srgb, var(--contrast-toward), var(--surface) 90%));
    --border-strong: var(--border-strong-on-surface,  color-mix(in srgb, var(--contrast-toward), var(--surface) 80%));
    --border-control: var(--border-control-on-surface, var(--muted));
  }
  .bg-scrim        { background-color: var(--scrim); }
  .bg-scrim-strong { background-color: var(--scrim-strong); }
  /* Fades from opaque at the text end to clear at the image end — the
     hero pattern, where the photo stays visible but the copy must not
     depend on which pixel sits behind it. */
  .bg-scrim-gradient {
    /* the midpoint is a role for the same reason the alphas are: it is the
       shape of the ramp, and a comp decides it (ticket 6) */
    background-image: linear-gradient(var(--scrim-angle, 0deg),
      var(--scrim-strong) 0%, var(--scrim) var(--scrim-midpoint, 45%), transparent 100%);
  }
}

/* ---------- core.components.css ---------- */
/* ==========================================================================
   RBMG Core — static components (buttons, cards, icon-stacked, the navbar
   shell, breadcrumb)
   core.components.css · v0.4.0-draft

   Design test for everything here (tier map §Tier 1): what makes the
   wrong version inexpressible?
     · .btn clears 44×44 at EVERY size — retires the .tap-target patch-
       work and the "reach for .btn-lg" guardrail
     · variants generate from roles — there is no path to a hex-named
       one-off (the .btn-<hex> fossil) or a per-brand button vocabulary
     · button text colors are the gated on-color roles — the white-on-
       light-accent CTA failure (measured live in the audit at ~2.2:1)
       cannot be authored
     · card hover reads --brand-accent — the shop-wide guardrail that was
       written in one client's colors becomes true for every brand
     · breadcrumb divider is a token — no pseudo-caret to hide, and its
       links clear 44×44 like every other control the core owns
     · the navbar shell exists at all (v0.4.0) — the core shipped nav
       BEHAVIOUR and nav COMPONENTS and no navbar CSS, so every site rebuilt
       ~150 lines of it. .nav-link is real height, never a pseudo hit area,
       because stacked pseudo hit areas OVERLAP
     · .card is positioned, so .stretched-link cannot escape to the viewport
   ========================================================================== */

@layer components {
  /* ====================================================================
     BUTTONS — shape 1: the standard button
     ==================================================================== */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-height: 44px;               /* tap target by construction — */
    min-width: 44px;                /* holds at EVERY size variant   */
    padding: 0.5rem 1.25rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    font-family: var(--font-heading);
    font-weight: var(--font-weight-semibold);
    font-size: var(--fs-label-md);
    line-height: var(--lh-label-md);
    text-transform: var(--heading-transform, uppercase);
    text-align: center;
    text-decoration: none;
    /* NO `white-space: nowrap` — v0.4.0, Pilot 03.
       It shipped here for three versions and it made a button SILENTLY CLIP
       ITS OWN LABEL. Measured on Pilot 03 §7: "Ask About Our Maintenance Plan"
       in a 246px card column at 390px painted a correctly-sized 246px pill
       with the label overflowing both ends of it — the leading "A" cut off and
       the trailing icon outside the fill. The pill's BOX was contained, which
       is why a containment check over bounding boxes reported zero problems;
       what overflowed was the text inside a box that had stopped being able to
       grow. That is the failure mode this file exists to make inexpressible:
       nothing about the wrong version looks wrong to any instrument.

       `nowrap` was protecting a cosmetic property — a two-word label staying on
       one line — at the price of a correctness one. A label that does not fit
       must wrap; `min-height: 44px` is a floor, not a fixed height, so a
       wrapped button grows taller and keeps its tap target.

       A label that genuinely must not break says so IN ITS OWN CONTENT with a
       non-breaking space (a phone number is the case that matters, and Pilot 03
       carries U+00A0 in `company_phone` for exactly this). That is the right
       layer for it: "these two words belong together" is a fact about the
       string, not about the component. */
    border: 1px solid transparent;
    /* TICKET 12 — the seam. A brand's button radius and its general small
       radius are routinely different numbers (a square button on a rounded
       card), and without a seam the only way to say so was a site rule
       restyling a core component. Same device as --btn-<role>: undeclared,
       with the fallback at the point of use, so a brand answering nothing gets
       exactly today's shape. --btn-pill and --btn-rect still override it
       outright, because those are shape MODIFIERS rather than the default. */
    border-radius: var(--btn-radius, var(--radius-sm));
    cursor: pointer;
    user-select: none;
    transition: background-color var(--transition-standard) var(--ease-standard),
                border-color     var(--transition-standard) var(--ease-standard),
                color            var(--transition-standard) var(--ease-standard);
  }
  /* NO `outline: … currentColor` HERE. This rule drew the focus ring in the
     button's own LABEL colour — but the ring is painted 2px OUTSIDE the
     button, on whatever surface the button sits on. So a filled button with
     a white label painted a white ring on the white page ground: measured
     1.00:1, invisible, on the most common interactive element in the system.
     `.btn-inverse` on a dark band measured 2.00:1 the other way.

     This is the `.btn-outline-inverse` defect exactly — "currentColor
     resolves against the element's OWN computed color" — moved from
     `background-color` to `outline`, which is why the F-019 structural test
     could not see it: that test looks for a currentColor FILL beside a
     `color`, and this is a currentColor RING. The ring belongs to the
     surface, not to the element, so it takes the surface's published
     --focus-ring from :where(:focus-visible) like everything else. */
  .btn:focus-visible {
    outline-offset: 2px;
  }
  .btn:disabled, .btn.disabled {
    opacity: 0.65;
    pointer-events: none;
  }

  /* Sizes — padding and type scale change; the 44×44 floor does not */
  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
  .btn-sm { padding: 0.35rem 0.85rem; font-size: var(--fs-label-sm); line-height: var(--lh-label-sm); }
  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
  .btn-lg { padding: 0.75rem 1.75rem; font-size: var(--fs-label-lg); line-height: var(--lh-label-lg); }

  /* Filled variants — generated from roles; text is the gated on-color.

     v0.3.0 — each fill is a component-scoped token with a role fallback
     (Pilot 01, F-010). --brand-secondary is documented as "the deep
     supporting hue behind bands and inverse surfaces" AND generates
     .btn-secondary, and the pilot's brand assigned those two jobs
     different values: bands at #2E414C, the secondary button at #5E6B78.
     Answering the role with either made the other wrong, and since the
     button is generated from the role there was no compliant override —
     the build shipped without a secondary button rather than a wrong one.

     This is the seam --btn-outline-primary already had, for the same
     reason one row down: a role that serves two jobs needs a way to say
     so. A brand using one value answers nothing and nothing changes; a
     brand that splits them answers one line. The variant list is still
     generated from roles, so there is still no path to a hex-named
     one-off — which is the guarantee that matters here.

     The -hover tints follow the seam rather than the base role, or a
     brand that overrode the fill would keep the base hue's hover: the
     stale-hover failure this project was commissioned over, rebuilt.

     THE LABEL IS RE-DERIVED IN EACH RULE — v0.4.1, and it is E-145's family
     rather than a style choice. core.tokens.css derives --on-btn-<role> at
     :root and argues it is safe there because the --btn-* seams are BRAND-tier.
     That is the same sentence that was true of --tab-active-fill until Pilot 03
     documented it as a per-section seam, and Pilot 04 shows a site scoping a
     --btn-* value on a section today (E-144). A role derived at :root from an
     UNDECLARED SEAM resolves on <html> and inherits already-resolved, so the
     moment anyone answers the seam lower down, the fill moves and the label
     stays — which is exactly the 1.6:1 the tab shipped.

     Re-deriving on the element that paints the fill is value-identical when
     the seam is answered at :root, so nothing moves; it simply stops being
     wrong when the seam is answered anywhere else. roles.test.mjs enforces it
     for every role of this shape rather than for the two we have been bitten
     by. */
  .btn-primary {
    --on-btn-primary: color(from var(--btn-primary, var(--brand-primary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: var(--btn-primary, var(--brand-primary));
    border-color:     var(--btn-primary, var(--brand-primary));
    color: var(--on-btn-primary);
  }
  .btn-secondary {
    --on-btn-secondary: color(from var(--btn-secondary, var(--brand-secondary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: var(--btn-secondary, var(--brand-secondary));
    border-color:     var(--btn-secondary, var(--brand-secondary));
    color: var(--on-btn-secondary);
  }
  .btn-accent {
    --on-btn-accent: color(from var(--btn-accent, var(--brand-accent)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: var(--btn-accent, var(--brand-accent));
    border-color:     var(--btn-accent, var(--brand-accent));
    color: var(--on-btn-accent);
  }
  /* THE HOVER FILL MOVES AWAY FROM ITS OWN LABEL — v0.4.0, ticket 15, carried
     since session 10 and the last of Pilot 02's colour items.

     It used to mix 20% toward --contrast-toward, which is the SURFACE's
     direction, while the label stayed derived from the REST fill. So the pair
     was computed in one state and painted in another, and nothing measured the
     state that moved. That is the stale-hover failure this whole project was
     commissioned over, surviving inside the component built most carefully
     against it.

     Measured, on the dark-ground reference brand, at the page ground where
     --contrast-toward is WHITE:
         .btn-secondary hover  4.34:1   FAIL
         .btn-primary hover    4.52:1   scraping past
     The fill lightened toward the surface while its label stayed white.

     Note what does NOT fix it: deriving the label from the hover fill. A 20%
     mix rarely crosses the 0.5637 pivot, so the derived label is the same white
     and the ratio is the same 4.34:1. The direction is the defect, not the
     staleness of the label.

     `color(from var(--on-btn-<role>) …)` is the auto-contrast pattern applied to the LABEL, and since a label is white or black, its auto-contrast is the other one. So this reads as "deepen the fill, away from whatever its label turned out to be": a dark fill darkens, a light fill lightens, and the label's contrast can only INCREASE. That is the law the core already declares for links — "a hover must never reduce contrast" — finally true for the most common interactive element in the system rather than only for the one that had a gate row. It also survives a brand overriding --on-btn-<role> with an off-white literal: the auto-contrast of an off-white is still black, so the fill still deepens, and the gate measures the literal against the hover fill. NOT the same derivation as --brand-<role>-hover, and do not unify them. Those answer "this hue as TEXT on the page ground", where the surface's direction is exactly right and a regression test pins it. A fill and a label on that fill are a different question with a different answer. */ .btn-primary:is(:hover, :focus-visible, :active) { --on-btn-primary: oklch(from var(--btn-primary, var(--brand-primary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --btn-hover-away: color(from var(--on-btn-primary) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: color-mix(in srgb, var(--btn-primary, var(--brand-primary)), var(--btn-hover-away) 20%);
    border-color:     color-mix(in srgb, var(--btn-primary, var(--brand-primary)), var(--btn-hover-away) 20%);
    color: var(--on-btn-primary);
  }
  .btn-secondary:is(:hover, :focus-visible, :active) {
    --on-btn-secondary: color(from var(--btn-secondary, var(--brand-secondary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --btn-hover-away: color(from var(--on-btn-secondary) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: color-mix(in srgb, var(--btn-secondary, var(--brand-secondary)), var(--btn-hover-away) 20%);
    border-color:     color-mix(in srgb, var(--btn-secondary, var(--brand-secondary)), var(--btn-hover-away) 20%);
    color: var(--on-btn-secondary);
  }
  .btn-accent:is(:hover, :focus-visible, :active) {
    --on-btn-accent: color(from var(--btn-accent, var(--brand-accent)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    --btn-hover-away: color(from var(--on-btn-accent) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: color-mix(in srgb, var(--btn-accent, var(--brand-accent)), var(--btn-hover-away) 20%);
    border-color:     color-mix(in srgb, var(--btn-accent, var(--brand-accent)), var(--btn-hover-away) 20%);
    color: var(--on-btn-accent);
  }

  /* Inverse — a button that reads as the page ground, placed on a
     contrasting band.
     v0.2.0 — FIXED. Its fill used to be `var(--on-inverse)`: a TEXT role
     consumed as a SURFACE. A dark-ground brand must set --on-inverse
     dark, so the button rendered dark-on-dark and its own label measured
     1.53:1 against its own fill. The fill is now a surface role with its
     own override seam — the same seam .btn-outline-primary and
     -secondary already had for the light-hue trap, omitted here on the
     one variant a dark-ground brand depends on. */
  .btn-inverse {
    background-color: var(--btn-inverse, var(--surface));
    border-color: var(--btn-inverse, var(--surface));
    color: var(--on-btn-inverse, var(--on-surface));
  }
  /* THE ONE VARIANT THAT MOVES THE OTHER WAY, and it is the exception that
     proves the rule rather than a hole in it. Found while fixing ticket 15.

     Every other filled variant deepens away from its own label. .btn-inverse
     cannot: its fill is a SURFACE role, so it is normally at the extreme
     already — pure white on a light brand. It sits on a dark band, where the
     published --contrast-toward is white, so the old rule mixed 8% white INTO
     WHITE and the hover was a complete no-op on every light-ground brand.
     "Away from its own label" would be the identical no-op, for the identical
     reason: there is nowhere further to go.

     So this one moves TOWARD its label — a white button darkens under the
     pointer, which is the conventional treatment anyway. That reduces the
     pair's contrast, which is only acceptable because it starts at 12–21:1 and
     because the gate now MEASURES the hover pair instead of trusting that
     sentence. A brand whose --btn-inverse is mid-tone gets a gate failure. */
  .btn-inverse:is(:hover, :focus-visible, :active) {
    background-color: color-mix(in srgb, var(--btn-inverse, var(--surface)),
                                var(--on-btn-inverse, var(--on-surface)) 8%);
    border-color: transparent;
    color: var(--on-btn-inverse, var(--on-surface));
  }

  /* Outline variants — the outline color is a component-scoped token
     with a role fallback, because a LIGHT lead hue as text on white
     carries the same contrast trap as links (measured ~2.2:1 in the
     audit). A brand with a light hue points --btn-outline-<role> at its
     deep hue; the role-satisfaction check gates it. */
  .btn-outline-primary {
    background-color: transparent;
    border-color: var(--btn-outline-primary, var(--brand-primary));
    color: var(--btn-outline-primary, var(--brand-primary));
  }
  .btn-outline-secondary {
    background-color: transparent;
    border-color: var(--btn-outline-secondary, var(--brand-secondary));
    color: var(--btn-outline-secondary, var(--brand-secondary));
  }
  /* Hover fills with the SAME seam the filled variant uses, so
     .btn-outline-primary:hover and .btn-primary always agree. Reading
     --brand-primary here instead would split them the moment a brand
     answered --btn-primary — a stale pairing of exactly the kind the
     derived -hover tints exist to prevent. */
  .btn-outline-primary:is(:hover, :focus-visible, :active) {
    --on-btn-primary: color(from var(--btn-primary, var(--brand-primary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: var(--btn-primary, var(--brand-primary));
    border-color:     var(--btn-primary, var(--brand-primary));
    color: var(--on-btn-primary);
  }
  .btn-outline-secondary:is(:hover, :focus-visible, :active) {
    --on-btn-secondary: color(from var(--btn-secondary, var(--brand-secondary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: var(--btn-secondary, var(--brand-secondary));
    border-color:     var(--btn-secondary, var(--brand-secondary));
    color: var(--on-btn-secondary);
  }
  /* Outline-inverse — an outline button that ADOPTS whatever surface it
     sits on, via currentColor.
     v0.2.0 — FIXED, and this is the headline failure from Pilot 01. It
     used to hardcode `var(--on-inverse)` for both border and label. On a
     dark-ground brand that resolved dark, and the mobile hamburger —
     sitting on the dark page ground, not inside a light band — rendered
     at 1.00:1. Completely invisible. Nothing failed and nothing warned;
     all six declared gates reported PASS.
     currentColor is correct by construction: inside .bg-inverse it is
     --on-inverse, on the page ground it is --on-surface, inside a scrim
     it is the scrim's text colour. There is no list to disagree with
     reality, and no polarity it can get wrong. */
  .btn-outline-inverse {
    background-color: transparent;
    border-color: var(--btn-outline-inverse, currentColor);
    color: var(--btn-outline-inverse, currentColor);
  }
  /* v0.3.0 — FIXED, and it is the v0.1.0 hamburger failure returning
     inside the fix for the v0.1.0 hamburger failure. This block read:

         background-color: currentColor;
         color: var(--surface);

     `currentColor` in background-color resolves against the SAME
     element's computed `color` — including a `color` set in the same
     block. So the fill became var(--surface) too: fill and label the
     identical colour, measured 1.00:1 on all three instances, at every
     surface, on both reference brands. Invisible on hover.

     currentColor is still right for the REST state, where it adopts
     whatever surface the button sits on and cannot get polarity wrong.
     It cannot be used for a fill in a rule that also sets the label.
     --contrast-toward is the published per-surface answer to "which ink
     does this surface take?", so the fill reads it and the label derives
     from the fill's own lightness — computed at both ends, no
     self-reference, and the seam still overrides both. */
  .btn-outline-inverse:is(:hover, :focus-visible, :active) {
    background-color: var(--btn-outline-inverse, var(--contrast-toward));
    border-color:     var(--btn-outline-inverse, var(--contrast-toward));
    color: color(from var(--btn-outline-inverse, var(--contrast-toward)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
  }

  /* ====================================================================
     BUTTONS — shape 2: the CTA (the pattern three sites hand-rolled
     independently; icon + label, pill silhouette, bolder)
     ==================================================================== */
  .btn-cta {
    gap: 0.5rem;
    padding: 0.75rem 1.25rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    font-weight: var(--font-weight-bold);
    font-size: var(--fs-label-lg);
    line-height: var(--lh-label-lg);
    border-radius: var(--radius-pill);
  }
  /* Shape modifiers, either direction */
  .btn-pill { border-radius: var(--radius-pill); }
  .btn-rect { border-radius: var(--btn-radius, var(--radius-sm)); }

  /* ====================================================================
     CARDS
     ==================================================================== */
  /* `position: relative` is TICKET 23's fix, and it is ticket 3's fix on a
     second component. `.stretched-link::after` is `position: absolute; inset:
     0`, so with no positioned ancestor the overlay escapes to the initial
     containing block and covers the viewport instead of the card — the same
     defect, the same silence, and the mega panel's version already cost a
     render. A card IS the ancestor that pattern means, in every use of it
     anywhere, so the core supplies it rather than documenting a precondition
     nobody reads. The general case stays a documented one in
     core-manifest.json; this makes the case that actually happens correct. */
  .card {
    position: relative;
    display: flex;
    flex-direction: column;
    background-color: var(--surface-raised);
    color: var(--on-surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;               /* clips media bands to the radius */
  }
  /* PADDING IS A SEAM — v0.4.0, Pilot 03. Undeclared, fallback at the point
     of use, the --btn-radius shape: a site that needs a denser card SELECTS a
     value rather than reaching into .card-body, which is restyling a core
     component's internals and is the one thing the layer rule forbids.

     It is here because two sections of one build needed it: the opener's
     tombstones (padding-block-start, shipped as an internals override before
     this existed) and the About cards, whose comp draws 12px. Card padding is
     per-comp by nature — 1.5rem is a good default and a bad law, and the
     spacing scale has no 0.75rem stop to reach for instead. */
  .card-body  { padding: var(--card-padding, 1.5rem); }
  .card-title { margin-bottom: 0.5rem; }

  /* Hover treatment — was a shop-wide SOP rule written in one client's
     brand colors (unfollowable on six of seven sites); retokenized it is
     true everywhere: border → accent + lift. */
  /* AN INSET SHADOW, NOT A BORDER — v0.4.1, from the pressure test. This read
     `border: 2px solid var(--border)`, and `.card` has no border, so adding the
     class narrowed the card's CONTENT BOX by 4px. Measured: 416px inside a
     plain card and 412px inside a hoverable one in the same row — so a pricing
     row where one card is linked and the rest are not has one column of copy
     set 4px narrower than its neighbours, wrapping differently, for a reason
     nothing on the page explains.

     An inset box-shadow paints the same 2px edge inside the padding box with no
     layout effect at all, follows the card's radius, and is clipped by the
     `overflow: hidden` .card already carries. `outline` would also have no
     layout effect and was rejected: the card may be given `tabindex` (demo.html
     does), and then the focus ring — also an outline — would replace the edge
     rather than sit outside it.

     STILL A COMPOSITION QUESTION THE CORE CANNOT ANSWER: :focus-within needs
     something focusable inside the card. A .card-hoverable containing no
     control paints a pointer affordance a keyboard user can never reach — the
     same shape as <fa-stack hoverable>, and unlike that one there is no JS here
     to warn from. Compose it around a .stretched-link. */
  .card-hoverable {
    box-shadow: inset 0 0 0 2px var(--border);
    transition: box-shadow var(--transition-standard) var(--ease-standard),
                transform  var(--transition-standard) var(--ease-standard);
  }
  .card-hoverable:is(:hover, :focus-within) {
    box-shadow: inset 0 0 0 2px var(--brand-accent), var(--shadow-md);
    transform: translateY(-2px);
  }

  /* Docked header — shrinks to its own text against the card's top edge.
     Structural draft; final metrics land from patterns.md at the pilot. */
  .card-header-docked {
    align-self: flex-start;
    width: fit-content;
    padding: 0.5rem 1rem;
    background-color: var(--brand-secondary);
    color: var(--on-secondary);
    border-radius: 0 0 var(--radius-sm) 0;
    font-family: var(--font-label);
    font-weight: var(--font-weight-bold);
    font-size: var(--fs-label-md);
    line-height: var(--lh-label-md);
    text-transform: uppercase;
  }

  /* ====================================================================
     ICON-STACKED — canonical CSS-background implementation.
     The backdrop is a native CSS background, NOT a Font Awesome glyph:
     pseudo-element FA icons render unreliably in Safari with the SVG+JS
     kit (the bug three audited sites still carry). The icon itself is
     the <i>/<svg> child.
     ==================================================================== */
  .icon-stacked {
    display: inline-flex;
    position: relative;
    width: 2em;
    height: 2em;
    align-items: center;
    justify-content: center;
    vertical-align: middle;
    border-radius: var(--radius-pill);
    /* THE BASE IS A SEAM — v0.4.1, Pilot 04 (E-144's residual half).
       The fill was a bare var(--brand-primary) with the .base-* variants each
       restating a fill/label PAIR, so a STATE — the client asked this backdrop
       to shift red→blue on hover — could not be expressed by answering
       anything. A site's only move was a rule repainting a core component,
       which is the one thing the layer rule forbids, and the build reached for
       --btn-accent instead: a BUTTON seam consumed as a graphic colour, which
       is the "is this role being consumed as the kind of thing it names"
       failure the core names elsewhere.

       Undeclared seam, fallback at the point of use, the --btn-<role> shape.
       A site now writes `--icon-stacked-bg: var(--brand-accent)` on its own
       hover rule and the component keeps its own structure.

       THE INK FOLLOWS THE SEAM rather than sitting beside it: answering the
       fill alone cannot leave a stale label, which is the whole reason
       --on-btn-* derives from --btn-* and not from --brand-*. The two variants
       whose ink is NOT auto-contrast say so explicitly below — .base-surface
       takes the published accent (a surface-aware glyph) and .base-inverse
       takes --on-inverse. Every variant is value-identical to what it painted
       before this seam existed. */
    background-color: var(--icon-stacked-bg, var(--brand-primary));
    color: var(--icon-stacked-ink,
                color(from var(--icon-stacked-bg, var(--brand-primary)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick)));
    /* The transform leg was 0.15s ease — a second duration and a second
       curve, both literals, in the one component that happened to want a
       snappier lift. A brand retuning --transition-standard moved the
       other two legs and left this one behind, so the three properties
       drifted out of sync by design. One duration, one curve. */
    transition: background-color var(--transition-standard) var(--ease-standard),
                box-shadow       var(--transition-standard) var(--ease-standard),
                transform        var(--transition-standard) var(--ease-standard);
  }
  .icon-stacked > i,
  .icon-stacked > svg {
    position: relative;
    z-index: 1;
    font-size: 1em;
  }

  /* Bases — generated from roles (no per-brand base vocabulary). Each answers
     the seam above rather than restating a pair, so the ink follows the fill.

     A CHIP IS NOT REQUIRED TO BE VISIBLE AGAINST ITS BAND, and nothing checks
     it — v0.4.1, from the <fa-stack> pressure test. Measured across every
     variant × every ground on both reference brands: 9 of 36 pairs on the
     light brand and 7 of 36 on the dark are indistinguishable, four of them at
     exactly 1:1. `base-surface` on `.bg-surface`, `base-primary` on
     `.bg-primary` and so on are the obvious ones; the ones that catch people
     are near-misses — `base-surface` on `.bg-surface-sunken` is 1.08:1, and on
     the new `.bg-surface-sunken-strong` 1.01:1.

     NOT a defect and deliberately not fixed. A chip painted the same colour as
     the band under it is the composition the site asked for, the glyph inside
     it still clears 3:1 against the chip, and a hairline border added here
     would change every chip on three shipped sites to defend against a choice
     nobody made by accident twice. What was missing is that nothing SAID so:
     these are publisher entries, which reads as "a chip is a surface in its own
     right", and a surface you cannot see is a design decision rather than a
     property. `npm run gate:fa-stack` reports the pairs; the manifest carries
     the warning; a build pairing a chip with its own band should reach for
     `shadow` or a different base. */
  .icon-stacked.base-primary   { --icon-stacked-bg: var(--brand-primary); }
  .icon-stacked.base-secondary { --icon-stacked-bg: var(--brand-secondary); }
  .icon-stacked.base-accent    { --icon-stacked-bg: var(--brand-accent); }
  /* surface-aware glyph: the ink is the PUBLISHED accent, not auto-contrast */
  .icon-stacked.base-surface   { --icon-stacked-bg: var(--surface);         --icon-stacked-ink: var(--icon-accent); }
  .icon-stacked.base-inverse   { --icon-stacked-bg: var(--surface-inverse); --icon-stacked-ink: var(--on-inverse); }

  /* Shape + sizes + effects */
  .icon-stacked.square { border-radius: var(--radius-sm); }
  .icon-stacked.sharp  { border-radius: 0; }
  .icon-stacked.xs  { font-size: 0.5rem; }
  .icon-stacked.sm  { font-size: 0.75rem; }
  .icon-stacked.md  { font-size: 1rem; }
  .icon-stacked.lg  { font-size: 1.5rem; }
  .icon-stacked.xl  { font-size: 2rem; }
  .icon-stacked.xxl { font-size: 3rem; }
  .icon-stacked.shadow { box-shadow: var(--shadow-md); }
  .icon-stacked.hoverable { cursor: pointer; }
  .icon-stacked.hoverable:hover { transform: scale(1.1); }

  /* ====================================================================
     NAVBAR SHELL — v0.4.0 (Pilot 02, tickets 2, 3, 4)

     The core shipped the nav BEHAVIOUR (shrink-menu, hero-offset) and the
     nav COMPONENTS (rbmg-dropdown, rbmg-drawer) and no navbar styling at
     all — not one `.shrink-menu` rule, and the reference demo positioned
     its own header with an inline style. So Pilot 02's site.css carried
     ~150 lines of it, the largest block in the sheet, and it recurs on
     EVERY site including Pilot 03 on day one.

     WHAT IS DELIBERATELY NOT HERE: the arrangement. Pilot 02's header is a
     two-row grid with the logo spanning both, and that is one comp. Shipping
     it as core would be inventing a universal shape from a sample of one —
     the mistake the v0.1.0 gradient model made and the reason it was rebuilt.
     Arrangement is composition; a site sets grid-template-* on .navbar-inner
     and that is the SITE tier working correctly.

     What IS here is what every header needs and nobody should re-derive: the
     fixed placement, the full-bleed inset formula, and a nav link that clears
     44×44. All three were defects on the pilot before they were rules.
     ==================================================================== */
  .navbar {
    position: fixed;
    inset-inline: 0;
    top: 0;
    /* Also the fix for TICKET 3. rbmg-dropdown[mega] sets `position: static`
       on itself so CSS alone can place a full-bleed panel, and the panel is
       absolute with top:100% — which needs a positioned ancestor that the core
       never supplied and never documented. Pilot 02's mega panel resolved
       against the initial containing block and rendered 904px down the page.
       A fixed navbar IS that containing block, so the case that actually
       recurs is answered by construction rather than by a note. The general
       case is a documented precondition in core-manifest.json, and the
       component warns when it opens without one. */
    z-index: var(--z-navbar, 1040);
    /* THE SHADOW IS A SEAM — v0.4.1, Pilot 04 (E-076). The core supports a
       TRANSPARENT header in JS and could not express one in CSS:
       core.behaviors.js grew `data-hero-pad` specifically for the case and
       documents it at length ("a TRANSPARENT header floats over the hero
       instead… That is padding"), while this rule hard-coded the elevation, so
       building the thing the behaviour exists to serve meant overriding a core
       declaration.

       Note what is NOT a seam and never needed to be: the BACKGROUND. This
       rule sets none, so the shell is transparent by default and a site opts
       INTO a ground by composing a .bg-* utility onto it (demo.html has always
       read `class="navbar bg-surface"`). Half of E-076 dissolved on reading
       the rule rather than the symptom — a reminder that "the core publishes
       no --navbar-bg" and "the core imposes a background" are different
       claims, and only the second would have been a defect. */
    box-shadow: var(--navbar-shadow, var(--shadow-md));

    /* THE FULL-BLEED INSET, HOISTED — v0.4.1, Pilot 04 (E-082).
       It lived as an inline clamp() on .navbar-inner and nowhere else, so
       rbmg-dropdown[mega] could not align with it. The panel sets itself
       `position: static` so it resolves against .navbar and goes full-bleed —
       that is the design — which means it cannot inherit .navbar-inner's
       padding, and the pilot's only compliant move was to restate the formula
       verbatim in site.css and label it duplication. Trying the .container
       utility first put the panel's first column 82px inside the nav link that
       opened it, because .container caps at 1320 against this header's 1460.

       Declared HERE and not at :root: this is per-header geometry, and a
       section (a second header, a docked sub-nav) may rebind it. A length has
       no contrast to go stale, so the :root-inheritance trap that governs
       colour roles does not apply — but the reason it is on the element is the
       same one. */
    --navbar-pad-x: clamp(var(--container-pad-x, 0.75rem),
                          (100vw - var(--navbar-fit, 1460px)) / 2,
                          var(--navbar-inset, 5rem));
  }

  /* FULL-BLEED, and the inset is a formula rather than a taste — this is the
     one piece of Pilot 02's header worth generalising verbatim.

     The pilot used a .container until a render measured the header: its
     content needed 1420px and .container caps at 1320 at EVERY width, because
     xxl is the last step. The grid had 100px less than it needed, `auto 1fr`
     took it from the only track that could yield — the logo's — and
     max-width:100% squashed the brand mark 46.5%. Identical at 1440 and 1920:
     not an edge case, every desktop width.

     So the header gets exactly the room its content needs and spends the rest
     on the brand's inset. --navbar-fit is what the nav must not go below;
     leave slack in it, because a header that fits to the pixel breaks on a
     longer phone number. */
  .navbar-inner {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    /* one value, read here and by anything that has to line up with it */
    padding-inline: var(--navbar-pad-x, clamp(var(--container-pad-x, 0.75rem),
                                              (100vw - var(--navbar-fit, 1460px)) / 2,
                                              var(--navbar-inset, 5rem)));
  }

  /* The brand mark is WIDTH-driven, and the swap is the actual defect fix.
     `width: auto; height: <n>` lets the width be whatever a track allows while
     pinning the height — precisely the shape that squashes, because
     max-width:100% clamps the width and nothing recomputes the height. Stating
     the width and taking `height: auto` inverts it: a short track makes the
     mark SMALLER at the right ratio instead of distorting it. Same trap as the
     reset's fluid-image rule (ticket 13), one component along. */
  .navbar-brand {
    display: inline-flex;
    align-items: center;
    min-height: 44px;             /* a brand mark IS a link to home — a control */
    min-width: 44px;
    text-decoration: none;
  }
  .navbar-brand img { display: block; width: var(--navbar-brand-width, 12rem); height: auto; }

  /* .nav-link is the class the INTERACTIVE CENSUS demanded. Every control in
     Pilot 02's header was site-authored, so the 44×44 floor was the site's to
     supply — and the gate reached none of them. A standalone text link is a
     CONTROL, not prose: it opts out of the underline (ticket 20) and takes the
     floor instead, which is the same complementary pair the breadcrumb makes.

     REAL HEIGHT, never a .tap-target pseudo-element, and that is measured
     rather than preferred. .tap-target centres a 44×44 ::before with no layout
     effect; on the pilot's footer list, 22.5px links on a 24px pitch would
     have had hit areas OVERLAPPING by 20px, and a tap between two labels lands
     on whichever painted last. Overlapping targets are worse than small ones —
     wrong rather than merely hard. Real height makes the rows TILE, so no
     separation rule is needed: SC 2.5.5's spacing clause exists for
     undersized targets and these are not undersized.

     min-width matters as much as min-height. The pilot's "Blog" was the only
     label short enough to expose a height-only rule and shipped 38×44 for two
     weeks under a comment claiming both axes. */
  .nav-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-height: 44px;
    min-width: 44px;
    padding-inline: 0.5rem;
    font-family: var(--font-label);
    font-weight: var(--font-weight-bold);
    font-size: var(--fs-label-md);
    line-height: var(--lh-label-md);
    text-transform: var(--heading-transform, uppercase);
    color: var(--link);
    text-decoration: none;
    background: none;
    border: 0;
    cursor: pointer;
    white-space: nowrap;
    transition: color var(--transition-standard) var(--ease-standard);
  }
  .nav-link:is(:hover, :focus-visible) { color: var(--link-hover); }
  /* A stacked list of them — a footer column, a drawer, a related-links card.
     `display: flex` rather than inline so the target spans the column: a
     385×44 row is easier to hit than a 385×22 one, and a wrapped label still
     grows the box because min-height is a floor and not a height.

     white-space: normal is what makes that last clause TRUE. .nav-link's
     nowrap is the navbar's (a horizontal bar must not fold mid-label), but a
     stacked column owns its width, and inheriting nowrap there CLIPS a long
     label at the drawer edge instead of wrapping it — Pilot 03's "Tankless
     Water Heater Maintenance" shipped cut to "MAINTENAN". This comment
     claimed wrapping for two versions while the code forbade it. */
  .nav-link-block { display: flex; justify-content: flex-start; white-space: normal; }

  .navbar-nav { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }

  /* The shrink hook. The behaviour that adds this class has shipped since
     v0.1.0 with nothing in the core reacting to it — every site wrote its own
     rule, which is how four divergent implementations happened. The near-
     universal treatment is that the utility row leaves and the mark gets
     smaller; both are structure, both belong here, and the sizes stay roles.

     THE CORE OWNS THE PROPERTY, THE SITE OWNS THE VALUE — v0.4.1, Pilot 04
     (E-077). The hook used to be `.shrink-menu .navbar-utility { display:
     none }` and it lost, silently, to any site rule at all: it lives in
     @layer components, site.css lands in @layer utilities and loads last, so
     a bare `.navbar-utility { display: flex }` beats it NO MATTER THE
     SPECIFICITY. Pilot 04 hit that twice in one session in opposite
     directions — first by accident, from a rule written only to centre an
     icon, with every gate green because no gate opens a scrolled page; then
     deliberately, when the client asked for exactly that behaviour.

     The trap is that the core DEFINED this class and never styled it.
     Arrangement is site work on purpose, so the site was REQUIRED to write the
     one declaration that breaks the hook, and had no way to learn it needed a
     `:not(.shrink-menu)` guard.

     So the core now ships the display and routes it through a variable. A site
     arranging the row answers --navbar-utility-display and composes with the
     hook; a site writing `display:` directly is overriding a documented core
     declaration, which is nameable, greppable and reviewable rather than
     invisible. Same device as --card-padding and --btn-radius: the value is the
     site's, the structure is not.

     THE TWO VARIABLES ARE THE FIX, AND ONE WOULD NOT HAVE BEEN. The first
     version of this shipped a single knob — the site answered
     --navbar-utility-display and `.shrink-menu` set the same property to none —
     and a browser showed it was the SAME BUG in a new spelling: the shrink rule
     lives in @layer components and a site's answer lives in @layer utilities,
     so the site's value outranks the state exactly as its `display` used to.
     Worse, a site answering it INLINE beats every selector there is. Measured
     on demo.html: the utility row stayed visible through the whole scroll, with
     the class correctly applied.

     Splitting them makes the two answers unable to collide, because they are
     no longer the same declaration. --navbar-utility-shrunk is the OUTER value
     and is core-internal; --navbar-utility-display is the site's and sits in
     the fallback. A site answering its knob — in a sheet, inline, at any
     specificity, in any layer — cannot reach the outer one, so the hook wins
     whenever the state is on and the site's arrangement applies whenever it is
     not. That is the property the ticket asked for and the single knob only
     appeared to have. */
  .navbar-utility {
    display: var(--navbar-utility-shrunk, var(--navbar-utility-display, flex));
    align-items: center;
    gap: 0.5rem;
  }
  .shrink-menu .navbar-utility { --navbar-utility-shrunk: none; }
  .shrink-menu .navbar-brand img { width: var(--navbar-brand-width-shrunk, 7rem); }

  /* ====================================================================
     BREADCRUMB — divider is a token; there is no pseudo-caret to hide
     ==================================================================== */
  .breadcrumb {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-label-md);
    line-height: var(--lh-label-md);
  }
  .breadcrumb-item { display: inline-flex; align-items: center; gap: 0.5rem; }
  .breadcrumb-item + .breadcrumb-item::before {
    content: var(--breadcrumb-divider, "\203A");   /* › — a brand/site may retoken */
    opacity: 0.6;
  }
  .breadcrumb-item.active { color: var(--muted); }

  /* THE 44×44 FLOOR, added v0.3.2 — and the reason it was missing until a
     build measured it is the point.

     This component shipped from v0.1.0 with a 24px-tall link. The core claims
     44×44 as a core property, `.btn` clears it at every size, every form
     control clears it, every overlay row clears it — and .breadcrumb, sitting
     in the same file as the button that made the claim, cleared nothing.
     Pilot 02's sub-page is the first page in any RBMG build to render one, and
     its three trail links measured 45×24, 65×24 and 121×24.

     THE RENDERED GATE REPORTED PASS THE WHOLE TIME, because TAP_RULES had no
     row for it — demo.html has carried a breadcrumb since v0.1.0 and nothing
     ever measured it. That is the "a threshold written about one component
     cannot see a second component repeating it" lesson, one component further
     on: the knowledge was in this file, four rules up, and the assertion was
     not. A row is added to tools/measure-rendered.mjs in the same commit, so
     this floor is measured rather than asserted here in prose.

     REAL HEIGHT, NOT A .tap-target PSEUDO-ELEMENT, and that choice is
     measured too. .tap-target centres a 44×44 ::before with no layout effect,
     which is right for a lone icon link and WRONG here: .breadcrumb is
     `flex-wrap: wrap` with `gap: 0.5rem`, so a wrapped trail (which is what a
     phone gets — Pilot 02's wraps to two lines at 390) puts its rows 32px
     apart, and 44px hit areas centred on 32px pitch OVERLAP by 12px. A tap
     between two crumbs would land on whichever painted last. Overlapping hit
     areas are worse than small ones: they are wrong rather than merely hard.

     Giving the anchor real height makes the flex line 44px, so the rows TILE
     at 44 + 8 gap and no separation rule is needed — the same arithmetic the
     footer-link fix used, and the reason SC 2.5.5's spacing clause does not
     apply once targets are no longer undersized.

     min-width matters as much as min-height: a short crumb ("AC", a year, a
     SKU) is undersized on the axis nobody checks. justify-content centres the
     label inside it so a short crumb reads as centred rather than left-hung.
     The .active item is not a link and needs no floor; it inherits the 44px
     line from its row and centres in it. */
  /* No underline, and the two halves of that decision are the same decision.
     Anchors underline by default from v0.4.0 (ticket 20) because a link in
     running text has no other affordance. A crumb is not running text: it is
     a navigation control in a row of controls, `inline-flex`, with a divider
     glyph between each pair — so it opts out of the underline and takes the
     44×44 floor instead. Those are complements, not two independent choices.
     The rendered gate decides which side an anchor is on by measuring whether
     it has text beside it, so a component cannot land in neither. */
  .breadcrumb-item a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    text-decoration: none;
  }
}

/* ---------- core.forms.css ---------- */
/* ==========================================================================
   RBMG Core — forms
   core.forms.css · v0.2.0-draft

   Net-new: the audit found forms almost untouched across the seven sites
   (five total rule hits), i.e. shipped Bootstrap defaults plus three
   identical per-site patches — sunken input fill, muted placeholder,
   taller textarea. All three are core defaults here, so the patch
   disappears rather than being re-applied per project.

   What is made inexpressible:
     · UNREADABLE PLACEHOLDERS. The audited hand-written placeholder
       (neutral-500 on a neutral-100 fill) measures 1.85:1 — shipped, and
       failing. Placeholder is now a role (--on-surface-muted), measured
       5.11:1 on the sunken fill for the demo brand.
     · INVISIBLE CONTROL EDGES. Control borders use --border-control, a
       FUNCTIONAL role gated at 3:1 (WCAG 1.4.11), not the decorative
       --border-strong which measures ~1.5:1 on white.
     · SUB-44px TARGETS. Every control clears 44×44 — WCAG 2.1 SC 2.5.5
       Target Size, the AAA threshold — and checks and radios carry a
       44×44 hit area around their 20px visual box. (v0.1.0 put the floor
       on the WRAPPER only, so the input a user actually aims at was 20×20
       while the README claimed otherwise.)
     · PREMATURE ERROR STATES. Validation styling keys off :user-invalid,
       so a field is never painted red before the user has interacted
       with it. `.is-invalid` / `.is-valid` remain for server-rendered
       round-trips.
     · COLOR-ONLY ERROR SIGNALLING. An invalid control gets a thicker
       edge as well as a color change (WCAG 1.4.1).

   SETTLED — do not re-derive.
   The QA rule about contact-form ids targets DUPLICATE / anchor-colliding
   ids on sections and wrappers. It does NOT mean "strip ids from form
   fields": a field id is what <label for> binds to, and removing it
   breaks both the accessible name and the form's own data submission.
   Every control below assumes a real id + a real <label for>. Placeholder
   text is never a substitute for a label.
   ========================================================================== */

@layer components {
  /* ====================================================================
     LABELS & LAYOUT
     ==================================================================== */
  .form-label {
    display: inline-block;
    margin-bottom: 0.25rem;
    font-family: var(--font-label);
    font-weight: var(--font-weight-bold);
    font-size: var(--fs-label-md);
    line-height: var(--lh-label-md);
    color: inherit;
  }
  .form-text {
    margin-top: 0.25rem;
    font-size: var(--fs-p-sm);
    line-height: var(--lh-p-sm);
    color: var(--muted);
  }
  /* Required marker — carried by the label, not by color alone */
  .form-label.required::after {
    content: " *";
    color: var(--danger);
  }

  /* ====================================================================
     CONTROLS — text inputs, textareas, selects
     ==================================================================== */
  .form-control,
  .form-select {
    display: block;
    width: 100%;
    min-height: 44px;                      /* tap target by construction */
    padding: 0.5rem 0.875rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    font-family: var(--font-body);
    font-size: var(--fs-p-md);
    line-height: var(--lh-p-md);
    color: var(--on-surface);
    background-color: var(--surface-sunken);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-sm);
    transition: border-color var(--transition-standard) var(--ease-standard);
    appearance: none;
  }
  .form-control::placeholder {
    color: var(--muted);        /* role, not a hand-picked gray */
    opacity: 1;                            /* Firefox dims placeholders by default */
  }
  /* NO `outline: none` HERE. This block used to kill the global gated ring
     and replace it with a border-colour change plus a 25%-alpha glow —
     measured at 1.53:1 (Ridgeline) and 1.96:1 (Ironwood) against the ground
     it sits on, where a state indicator needs 3:1. The change itself was
     worse: resting border → focused border is 1.67:1 on the light brand.
     Every other focusable element in the core takes `--focus-ring` at
     16.78:1 / 19.51:1, so form controls — the single most important thing to
     be able to find with a keyboard — had the weakest indicator in the
     system, and were the only place that opted out of the measured one.

     "Don't author a focus ring; that is the per-project accessibility
     install this core exists to retire" is this repo's own rule, and the
     core broke it on itself. The border-colour change stays as a second,
     supplementary signal. The ring comes from :where(:focus-visible) in
     core.foundation.css, like everywhere else, and the rendered gate now
     measures every indicator against its ground so this cannot come back. */
  .form-control:focus-visible,
  .form-select:focus-visible {
    border-color: var(--link);
  }
  .form-control:disabled,
  .form-select:disabled {
    opacity: 0.65;
    cursor: not-allowed;
  }
  textarea.form-control {
    min-height: 110px;                     /* the per-site patch, promoted */
    resize: vertical;
  }

  /* Select caret — a CSS background, matching the .icon-stacked rationale
     (pseudo-element FA glyphs render unreliably in Safari with the SVG+JS
     kit). currentColor keeps it legible on any surface. */
  .form-select {
    padding-right: 2.5rem;  /* scale-exempt: clearance reserved for the caret art drawn below, not rhythm */
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 1.15rem) center, calc(100% - 0.85rem) center;
    background-size: 0.3rem 0.3rem, 0.3rem 0.3rem;
    background-repeat: no-repeat;
  }

  /* ====================================================================
     CHECKS & RADIOS
     The visual box stays small; the INPUT ITSELF carries a 44×44 hit area.

     v0.2.0 — Pilot 01 (F-018). The wrapper had the min-height, but the
     input a user actually aims at was 20×20, which made the README's
     "every form control clears the floor" false for the smallest target in
     the system. The claim and the code disagreed, and the code was the
     one that shipped.
     ==================================================================== */
  .form-check {
    display: flex;
    align-items: center;
    gap: 0.625rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    min-height: 44px;
    padding: 0.25rem 0;
  }
  .form-check-input {
    position: relative;
    flex-shrink: 0;
    width: 1.25rem;
    height: 1.25rem;
    margin: 0;
    appearance: none;
    background-color: var(--surface);
    border: 2px solid var(--border-control);
    transition: background-color var(--transition-standard) var(--ease-standard),
                border-color     var(--transition-standard) var(--ease-standard);
    cursor: pointer;
  }
  /* Hit area expanded around the visual box — same technique as
     .tap-target, so the pointer target is 44×44 while the control still
     draws at 20×20. */
  .form-check-input::after {
    content: "";
    position: absolute;
    inset: 50% auto auto 50%;
    translate: -50% -50%;
    width: 44px;
    height: 44px;
  }
  .form-check-input[type="checkbox"] { border-radius: var(--radius-xs); }
  .form-check-input[type="radio"]    { border-radius: 50%; }
  .form-check-input:checked {
    background-color: var(--brand-primary);
    border-color: var(--brand-primary);
  }
  /* Marks are drawn with the gated on-color, so they stay visible on a
     light lead hue (where a white tick would vanish) */
  .form-check-input[type="checkbox"]:checked {
    background-image: linear-gradient(45deg, transparent 45%, var(--on-primary) 45%, var(--on-primary) 55%, transparent 55%),
                      linear-gradient(-45deg, transparent 45%, var(--on-primary) 45%, var(--on-primary) 55%, transparent 55%);
    background-size: 100% 100%;
  }
  .form-check-input[type="radio"]:checked {
    box-shadow: inset 0 0 0 4px var(--brand-primary),
                inset 0 0 0 10px var(--on-primary);
  }
  /* NO AUTHORED FOCUS RING HERE EITHER — v0.4.1, and its presence was a
     finding. This rule read `outline: 2px solid var(--link)`, which is a SECOND
     ring vocabulary in the same file whose comment forty lines up records the
     core breaking that rule on itself for `.form-control` and fixing it. The
     fix was applied to one component and not its two siblings, which is the
     shape this repo keeps naming: a fix applied to one member of a set is a
     list, and a list is the blind spot.

     Neither ring was a contrast failure on the reference brands — `--link` is
     gated at 4.5:1 against the page ground, and it measured 9.22:1 / 12.37:1 —
     but it is strictly weaker than the global ring's 21:1 / 18.16:1 for no
     stated reason, and it means "what colour is a focus ring here?" had two
     answers. `--focus-ring` is the one. */
  .form-check-label { cursor: pointer; }

  /* Switch — same input, different silhouette.

     THE KNOB MOVES WITH background-position, NOT WITH THE GRADIENT — v0.4.1,
     and the previous version is this repo's own motion lesson in a component
     that had not learned it. The knob's position was baked into the image
     (`circle at 0.5rem` → `circle at 1.75rem`), so the thing that changed was
     `background-image`, which does not interpolate — while the rule declared a
     transition on `background-position`, which never changed at all. A
     transition naming a property nothing moves, over a property that cannot
     move: the collapse panel's `hidden` defect exactly, one component over. The
     knob snapped, and the declaration said otherwise for three versions.

     Now the gradient is position-independent and `background-size` gives it a
     knob-sized box to sit in, so `background-position` carries the travel and
     the declared transition is the one that runs. The COLOUR still swaps
     discretely, which is correct — a knob changing colour mid-slide would be
     two animations disagreeing about one state — and `background-color` handles
     the track behind it. */
  .form-switch .form-check-input {
    width: 2.5rem;
    border-radius: var(--radius-pill);
    --switch-knob: var(--border-control);
    background-image: radial-gradient(circle, var(--switch-knob) 0.4rem, transparent 0.4rem);
    background-size: 1.25rem 100%;
    background-repeat: no-repeat;
    background-position: 0 50%;
    transition: background-color var(--transition-standard) var(--ease-standard),
                background-position var(--transition-standard) var(--ease-standard);
  }
  /* THE IMAGE IS RESTATED ON :checked, and leaving it out was a regression this
     file's own gate caught within a minute. `.form-check-input[type="checkbox"]
     :checked` paints the two-layer TICK gradient at (0,3,0); the switch's knob
     rule is (0,2,0). While the knob was declared on `.form-switch
     .form-check-input:checked` — also (0,3,0), later in source — it won. The
     moment the position moved out of the image and this rule stopped setting
     `background-image`, the checkbox tick rule became the winner and a checked
     switch painted a TICK instead of a knob.

     So the image is declared in both states. It is the same gradient either
     way, differing only in --switch-knob, so `background-position` still
     carries the travel and the colour still swaps discretely.

     `background-size` IS RESTATED FOR THE SAME REASON, AND LEAVING IT OUT PUT
     THE KNOB IN THE MIDDLE FOR A RELEASE. The rule above solved exactly this
     hazard for `background-image` and stopped one property short. The tick rule
     also sets `background-size: 100% 100%`, and the unchecked switch rule that
     sets `1.25rem 100%` is only (0,2,0) — so on :checked the tick's value won,
     the gradient was handed the FULL 40px track as its box, and a circle
     centred in a box the size of the positioning area cannot be moved by
     `background-position` at all: `100%` resolves to an offset of
     (40 − 40) × 100% = 0. Measured in Chrome before the fix: `background-size`
     100% 100%, `background-position` 100% 50%, knob painted dead centre.

     The instrument agreed with the CSS and both were wrong: `gate:forms` read
     `background-position` going 0% → 100% and called it travel. **A declared
     position is not a moved knob** — the gate now samples the painted pixels,
     which is the range-thumb lesson arriving in the component next door. */
  .form-switch .form-check-input:checked {
    --switch-knob: var(--on-primary);
    background-image: radial-gradient(circle, var(--switch-knob) 0.4rem, transparent 0.4rem);
    background-size: 1.25rem 100%;
    background-position: 100% 50%;
  }

  /* ====================================================================
     RANGE
     ==================================================================== */
  .form-range {
    width: 100%;
    min-height: 44px;
    background: transparent;
    appearance: none;
    cursor: pointer;
  }
  .form-range::-webkit-slider-runnable-track {
    height: 0.35rem;
    background-color: var(--border-control);
    border-radius: var(--radius-pill);
  }
  .form-range::-moz-range-track {
    height: 0.35rem;
    background-color: var(--border-control);
    border-radius: var(--radius-pill);
  }
  .form-range::-webkit-slider-thumb {
    appearance: none;
    width: 1.25rem;
    height: 1.25rem;
    margin-top: -0.45rem;  /* scale-exempt: centres the range thumb on its track, geometry */
    background-color: var(--range-thumb, var(--brand-primary));
    /* THE RING IS THE PUBLISHED INK, NOT --on-primary — v0.4.1. The thumb's
       FILL is a brand hue, and a brand hue on a band that may BE that hue is
       the trap this core documents at .btn-outline-* and again at rbmg-tabs:
       measured on a .bg-primary band, thumb and ground are the same colour at
       1:1 on both reference brands. The ring is what identifies the control
       (WCAG 1.4.11), so it takes --contrast-toward, which every publisher
       re-derives from its own fill — maximum contrast against whatever band the
       slider was dropped on, by construction, with no list of bands to keep in
       step. --on-primary was only ever right on a primary fill.
       --range-thumb is there for a brand whose slider is not the lead hue. */
    border: 2px solid var(--contrast-toward);
    border-radius: 50%;
  }
  .form-range::-moz-range-thumb {
    width: 1.25rem;
    height: 1.25rem;
    background-color: var(--range-thumb, var(--brand-primary));
    /* THE RING IS THE PUBLISHED INK, NOT --on-primary — v0.4.1. The thumb's
       FILL is a brand hue, and a brand hue on a band that may BE that hue is
       the trap this core documents at .btn-outline-* and again at rbmg-tabs:
       measured on a .bg-primary band, thumb and ground are the same colour at
       1:1 on both reference brands. The ring is what identifies the control
       (WCAG 1.4.11), so it takes --contrast-toward, which every publisher
       re-derives from its own fill — maximum contrast against whatever band the
       slider was dropped on, by construction, with no list of bands to keep in
       step. --on-primary was only ever right on a primary fill.
       --range-thumb is there for a brand whose slider is not the lead hue. */
    border: 2px solid var(--contrast-toward);
    border-radius: 50%;
  }
  /* the ring comes from :where(:focus-visible) like every other control — see
     the note on .form-check-input */

  /* ====================================================================
     INPUT GROUP
     ==================================================================== */
  .input-group {
    display: flex;
    align-items: stretch;
    width: 100%;
  }
  /* .btn joins the flatten list, because a control and a button is the
     commonest group there is (search-and-go) and a button kept --btn-radius in
     the middle of a joined row. */
  .input-group > .form-control,
  .input-group > .form-select,
  .input-group > .btn {
    flex: 1 1 auto;
    width: 1%;
    border-radius: 0;
  }
  .input-group > .btn { flex: 0 0 auto; width: auto; }
  .input-group > :first-child { border-start-start-radius: var(--radius-sm); border-end-start-radius: var(--radius-sm); }
  .input-group > :last-child  { border-start-end-radius: var(--radius-sm);   border-end-end-radius: var(--radius-sm); }
  .input-group-text {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0.5rem 0.875rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    font-size: var(--fs-p-md);
    color: var(--on-surface);
    background-color: var(--surface-sunken);
    border: 1px solid var(--border-control);
  }
  /* EVERY ADJACENCY, NOT TWO OF THEM — v0.4.1, and the two were a list.
     The rule named `.form-control + .input-group-text` and its mirror, so those
     two pairs collapsed their shared edge and every other pair kept both
     borders: measured 2px at the seam on select+text, text+text and
     control+.btn, which is the search-and-go pattern every dealer site has. A
     group is a row of ARBITRARY children; `* + *` is the property, and the pair
     list was a sample of it. */
  /* scale-exempt: sub-pixel geometry, not rhythm */
  .input-group > * + * { margin-inline-start: -1px; }
  /* A focused control must paint its ring OVER its neighbour, not under it —
     the -1px overlap above means the next sibling is painted later and would
     clip a ring drawn outside the border box. */
  .input-group > :is(:focus, :focus-within, :focus-visible) { position: relative; z-index: 1; }

  /* ====================================================================
     FLOATING LABELS
     The label is a real <label for>, only visually repositioned — it is
     never replaced by placeholder text, and never removed from the
     accessibility tree.
     ==================================================================== */
  .form-floating {
    position: relative;
  }
  .form-floating > .form-control,
  .form-floating > .form-select {
    height: 3.5rem;
    padding: 1.5rem 0.875rem 0.5rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
  }
  .form-floating > label {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    padding: 1rem 0.875rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    font-size: var(--fs-p-md);
    line-height: var(--lh-p-md);
    color: var(--muted);
    pointer-events: none;
    transform-origin: 0 0;
    transition: transform var(--transition-standard) var(--ease-standard),
                opacity   var(--transition-standard) var(--ease-standard);
  }
  /* :has(), NOT `~` — v0.4.1. The sibling combinator can only see a label
     written AFTER its control, and `<label>` before `<input>` is the order this
     core uses everywhere else (.form-check, .form-label, every field in
     demo.html). So the conventional order silently never floated: the label sat
     absolutely positioned over the field, permanently, on top of whatever the
     user typed. Nothing said the order mattered.

     :has() reads the container instead of a sibling chain, so both orders work.
     It introduces no new browser floor — the core already requires relative
     colour syntax, which shipped later in all three engines.

     STILL A REQUIREMENT, and it is documented at the top of this block rather
     than fixed: the control needs a `placeholder` attribute. Without one,
     :placeholder-shown can never match, so :not(:placeholder-shown) always
     does, and the label is permanently floated over an empty field. There is no
     pure-CSS test for "is this input empty" that does not go through the
     placeholder. */
  .form-floating:has(> .form-control:focus) > label,
  .form-floating:has(> .form-control:not(:placeholder-shown)) > label,
  .form-floating:has(> .form-select) > label {
    transform: scale(0.8) translateY(-0.5rem);
  }

  /* ====================================================================
     VALIDATION
     :user-invalid fires only AFTER interaction, so nothing is painted red
     on first paint. The .is-* classes cover server-rendered round-trips.
     Color is never the only signal — the edge thickens too.
     ==================================================================== */
  .form-control:user-invalid,
  .form-select:user-invalid,
  .form-control.is-invalid,
  .form-select.is-invalid {
    border-color: var(--danger);
    border-width: 2px;
  }
  .form-control:user-valid,
  .form-control.is-valid {
    border-color: var(--success);
    border-width: 2px;
  }
  .form-check-input:user-invalid,
  .form-check-input.is-invalid { border-color: var(--danger); }

  /* Feedback messages. Hidden by default; shown by the matching state.
     Pair with aria-describedby on the control so it is announced. */
  .invalid-feedback,
  .valid-feedback {
    display: none;
    margin-top: 0.25rem;
    font-size: var(--fs-p-sm);
    line-height: var(--lh-p-sm);
  }
  .invalid-feedback { color: var(--danger); }
  .valid-feedback   { color: var(--success); }
  :is(.form-control, .form-select):is(:user-invalid, .is-invalid) ~ .invalid-feedback { display: block; }
  :is(.form-control, .form-select):is(:user-valid, .is-valid)     ~ .valid-feedback   { display: block; }

  /* ====================================================================
     ON INVERSE SURFACES
     Controls inside a dark band keep a legible edge and fill without a
     per-site override.

     THE FILL IS A SEAM — v0.4.1, Pilot 04 (E-098), and the clause above used
     to end "…without a per-site override" as a statement of policy rather than
     of fact. It is still the DEFAULT and still contrast-safe by construction;
     it is no longer the only expressible language.

     A translucent white field on a dark band is one visual idiom. The other —
     an opaque LIGHT field on a dark card, which is what Pilot 04's comp draws
     (#EEF0F5 field, #E4E9F0 border, #A9ABB4 placeholder, on a navy card) — had
     NO compliant expression at all. The fill derives from --on-inverse, so the
     only way to move it was to answer a TEXT role in order to change a FILL,
     which is the --on-inverse-as-a-fill failure the core documents at length.
     The build correctly declined to override it from site.css even though a
     site rule would have won (E-073), and shipped the core's treatment.

     It closes as a seam rather than a WONTFIX because the core's stated reason
     for refusing was CONTRAST SAFETY, and safety is exactly what a derived ink
     preserves. --field-inverse moves the fill and THE LABEL DERIVES FROM THE
     SEAM IN THIS SAME RULE, so answering the fill cannot leave a white label
     on a light field — the --on-btn-* lesson, one component over.

     WHY THE INK DERIVES FROM THE SEAM AND NOT FROM THE PAINTED FILL. The
     default fill is translucent, and relative colour syntax inherits the
     ORIGIN's alpha when none is given: `color(from <12% white> … 0 0)` would produce a label at alpha 0.12 — an invisible label, which is the exact failure class this core exists to end. So the derivation reads the seam with --surface-inverse as its opaque fallback: unanswered, that is the band's own ink and identical to what shipped; answered, it is the ink for whatever fill was chosen. Border and placeholder are their own seams with today's values as fallbacks, because a light field on a dark card needs all three moved together and the core cannot guess two of them from the first. All three are gated: gate rows measure the ink AND the border against the field's own fill, so a brand that answers one and forgets another fails rather than ships. ==================================================================== */ :where(.bg-inverse, .bg-primary, .bg-secondary) :is(.form-control, .form-select, .input-group-text) { color: oklch(from var(--field-inverse, var(--surface-inverse)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick));
    background-color: var(--field-inverse, color-mix(in srgb, var(--on-inverse), transparent 88%));
    border-color: var(--field-border-inverse, var(--on-inverse-muted));
  }
  /* THE PLACEHOLDER DERIVES FROM THE FIELD, NOT FROM THE BAND — v0.4.1, and
     this one is a defect the new gate row found rather than a ticket.

     It read var(--on-inverse-muted): a role meaning "muted text on the INVERSE
     SURFACE", painted on a field that is a 12% veil of the ink sitting ON that
     surface — a different, lighter ground. Measured on the light reference
     brand: 3.94:1, a 1.4.3 failure, on the commonest CTA pattern there is (a
     form on a dark band). Nothing saw it because no gate row measured a field
     ink against a FIELD FILL; the muted rows measure bands, and a field is not
     a band. Exactly the shape of the ticket that commissioned the row.

     15% is not a new constant: it is the one the brand-band publishers already
     use for muted ink on a fill, chosen by measurement in v0.4.0 (25% failed
     the dark reference, 20% failed Pilot 02's brand, 15% cleared all three).
     A field on a dark band IS a fill, so it takes the fill answer. Measured
     here: 5.19:1 light / 15.4:1 dark, against 3.94 / 5.21 before.

     Mixed with the SEAM rather than with the painted fill, for the reason the
     ink above is: the default fill is translucent, so mixing with it yields a
     translucent placeholder whose real colour depends on compositing — and an
     unmeasurable colour is how every failure in this file's history shipped. */
  :where(.bg-inverse, .bg-primary, .bg-secondary) .form-control::placeholder {
    color: var(--field-placeholder-inverse,
                color-mix(in srgb,
                          color(from var(--field-inverse, var(--surface-inverse)) srgb-linear var(--contrast-pick) var(--contrast-pick) var(--contrast-pick)),
                          var(--field-inverse, var(--surface-inverse)) 15%));
  }
}

/* ---------- core.overlays.css ---------- */
/* ==========================================================================
   RBMG Core — interactive component styling
   css/core.overlays.css · v0.2.0-draft

   The visual counterpart to the behavior components in js/components/.
   Split from core.components.css because these are the pieces with
   transitions, stacking, and off-canvas geometry — the parts a brand is
   most likely to want to inspect, and the parts a site must not redefine.

   All motion routes through --transition-standard for duration and through
   --ease-enter / --ease-exit / --ease-standard for curve, and collapses
   under prefers-reduced-motion at the bottom of this file. No transition in
   this file names a timing keyword directly — a structural test enforces
   that, because the previous version's easing was three hand-typed
   `ease-out`s and ten silent defaults. Every color is a role; every radius
   and shadow is a token.

   Enter vs exit: whichever state the element rests in when CLOSED carries
   --ease-exit, and the .is-open rule overrides the curve to --ease-enter.
   One declaration each, and the pair can never disagree about duration.
   ========================================================================== */

@layer components {
  /* ====================================================================
     DROPDOWN
     ==================================================================== */
  rbmg-dropdown {
    position: relative;
    display: inline-block;
  }
  rbmg-dropdown [data-dropdown-menu] {
    position: absolute;
    z-index: 1000;
    min-width: 12rem;
    margin-top: 0.25rem;
    padding: 0.5rem;
    background-color: var(--surface-raised);
    color: var(--on-surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-dropdown);
    /* --dropdown-shift is set by the component only when the menu would
       overflow the viewport; CSS decides how the correction is applied. */
    translate: var(--dropdown-shift, 0) 0;
  }
  /* PLACEMENT IS AN ATTRIBUTE ON THE MENU, not on an ancestor — v0.4.1.
     These read `rbmg-dropdown[data-placement="…"] [data-dropdown-menu]`, and a
     descendant combinator matches a NESTED dropdown's menu as happily as its
     own. Two dropdowns with different placements therefore both styled the
     inner menu, and because they set different properties neither won:
     measured with inset-inline-start AND inset-inline-end both at 0, stretched
     across its container. The component stamps the menu it owns, so an
     ancestor's rule has nothing to match. */
  [data-dropdown-menu][data-placement="bottom-start"] { inset-inline-start: 0; top: 100%; }
  [data-dropdown-menu][data-placement="bottom-end"]   { inset-inline-end: 0;   top: 100%; }
  [data-dropdown-menu][data-placement="top-start"]    { inset-inline-start: 0; bottom: 100%; margin: 0 0 0.25rem; }
  [data-dropdown-menu][data-placement="top-end"]      { inset-inline-end: 0;   bottom: 100%; margin: 0 0 0.25rem; }

  /* A FLYOUT OPENS TO THE SIDE. A nested menu used to open at `top: 100%` like
     a top-level one, which puts it directly over the items below its own
     toggle — measured covering its parent menu's "Maintenance". Every submenu
     in every menu system opens inline-end of its parent for this reason.

     BOTH AXES ARE RESET EXPLICITLY, including the ones this rule does not
     want. The placement rules above still stamp the same element, and an
     `auto` here is what makes "over-constrained" impossible rather than
     dependent on which of two equal-specificity rules came last — which is the
     defect this pair of rules just had. */
  /* A NESTED DROPDOWN SPANS ITS PARENT MENU, and without this the flyout's
     `100%` is the wrong 100%. `rbmg-dropdown` is `inline-block`, so nested
     inside a menu it shrinks to its toggle — and `inset-inline-start: 100%`
     then offsets the flyout by the TOGGLE's width, leaving it on top of the
     parent's own full-width items. Measured: it still covered "Maintenance"
     after the flyout rule landed. Spanning the menu is also what makes the
     toggle behave like the menu item it visually is. */
  [data-dropdown-menu] rbmg-dropdown { display: block; }
  [data-dropdown-menu][data-nested] {
    inset-inline-start: 100%;
    inset-inline-end: auto;
    top: 0;
    bottom: auto;
    margin: 0;
    margin-inline-start: 0.25rem;
  }

  /* :not(.btn) — THE SAME GUARD THE DRAWER PANEL CARRIES, and the fact that
     it took a second build to reach here is the finding. `5b1405b` added it
     to rbmg-drawer when Pilot 03's drawer CTA rendered as a bare link; this
     selector has the identical shape (specificity 0,2,1 against .btn-<role>'s
     0,1,0, same layer) and was left alone because nothing had yet composed a
     .btn into a MENU. Pilot 03's mega panel then did, and the outlined service
     buttons came out with `border: 0`, `background: none` and `color:
     inherit` — a button stripped of every property that makes it one.

     A fix applied to one component and not its sibling is a list, and a list
     is the blind spot: both are "a container whose default child is a nav
     link, into which a site may compose a real button." The pinned test now
     asserts BOTH, so a third container of this kind cannot ship without one. */
  rbmg-dropdown [data-dropdown-menu] a:not(.btn),
  rbmg-dropdown [data-dropdown-menu] button:not(.btn) {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 44px;                      /* menu items are tap targets too */
    padding: 0.5rem 0.75rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    border: 0;
    background: none;
    text-align: start;
    text-decoration: none;
    border-radius: var(--radius-sm);
    color: inherit;
    transition: background-color var(--transition-standard) var(--ease-standard);
  }
  /* :not(.btn) HERE TOO — and this one is worse than the rest state it
     mirrors, which is why it gets its own note. The base rule's guard leaves
     a composed .btn looking correct at rest, so the omission here is silent
     until a pointer touches it: the menu's own hover paints --surface-sunken
     at (0,2,2) while .btn-outline-primary:hover's `color: var(--on-btn-primary)`
     (0,2,0) still lands, and the button hovers to a WHITE label on light grey.
     Measured on Pilot 03's mega panel: 1.33:1.

     Fixing a rule and not its :hover sibling is the same list-shaped mistake
     as fixing the drawer and not the dropdown — made twice in one change, one
     level down, which is the argument for the guard being on every selector
     in this block rather than remembered per rule. */
  /* THE HOVER GROUND DERIVES FROM THE ITEM'S OWN INK, not from --surface-sunken.
     Naming that role assumed every menu item sits on the page ground: it is
     the PAGE's sunken tone, and no publisher re-declares it (publishers
     re-declare --link, --icon-accent, --border, --muted — not the surface
     roles). So a menu item inside a band-published card hovered to light grey
     on blue. That is E-033 item 3 exactly, one property over: the tab hover
     named --on-surface and went near-black on a blue band, and the fix there
     was the same — stop naming the page ground.

     currentColor is the item's own label, so an 8% veil of it always moves
     AWAY from whatever ground the item is actually on: 8% ink over a white
     panel is the light grey this always painted, and 8% white over a blue
     card is a lighter blue. One declaration, correct on every surface,
     with no list of bands to keep in step.

     This is the legitimate use of currentColor the core documents — a REST/
     hover state adopting its surface. It is NOT the .btn-outline-inverse
     trap: that rule set `color` in the same breath, so its fill became its
     own label at 1.00:1. This sets background only, and the label stays at
     full strength over an 8% veil.

     THE VEIL MOVES THE GROUND TOWARD THE LABEL, so contrast falls slightly —
     state it rather than let a reader assume otherwise. Measured on Pilot 03
     by compositing through a canvas: white panel 18.73:1 → 15.86:1 (ground
     #ECECEC), blue card 8.73:1 → 7.12:1 (ground rgb(37,87,160)). Both stay
     far above 4.5:1, and the direction is unavoidable for a HIGHLIGHT —
     the alternative, always darkening, is invisible on a dark ground. That
     is a different question from --link-hover, where the hue itself carries
     the state and must deepen; here the state is carried by a ground
     appearing at all. */
  rbmg-dropdown [data-dropdown-menu] :is(a, button):not(.btn):is(:hover, :focus-visible) {
    background-color: color-mix(in srgb, currentColor 8%, transparent);
  }

  /* Mega panel — full-bleed, so CSS alone places it. This is the case
     that made Popper painful; here it is ordinary layout. */
  rbmg-dropdown[mega] { position: static; }
  /* stamped on the menu for the same reason placement is: a dropdown nested
     inside a mega panel used to inherit the full-bleed rule */
  [data-dropdown-menu][data-mega] {
    inset-inline: 0;
    top: 100%;
    width: 100%;
    min-width: 0;
    border-radius: 0 0 var(--radius-md) var(--radius-md);
    translate: none;
  }

  /* ====================================================================
     TABS
     ==================================================================== */
  rbmg-tabs [role="tablist"] {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    border-bottom: 1px solid var(--border);
  }
  rbmg-tabs [role="tab"] {
    /* FLEX, because the rest of this component already assumed it: stack mode
       sets `justify-content: flex-start` and a <button> is not a flex
       container by default, so that declaration did nothing and neither did
       `gap` or an `margin-inline-start: auto` pushing a trailing icon to the
       far edge. Pilot 03's rail wanted a leading glyph, a label, and an arrow
       held right; it got three inline boxes in a row. */
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0.5rem 1rem;
    border: 0;
    border-bottom: 3px solid transparent;
    background: none;
    color: var(--muted);
    font-family: var(--font-label);
    font-weight: var(--font-weight-bold);
    font-size: var(--fs-label-md);
    text-transform: uppercase;
    cursor: pointer;
    transition: color var(--transition-standard) var(--ease-standard),
                border-color var(--transition-standard) var(--ease-standard);
  }
  /* `inherit`, NOT var(--on-surface). A tab paints no surface of its own — it
     sits on whatever band the site put it on — and NO publisher re-declares
     --on-surface (they republish --muted, --link, --border, --icon-accent,
     --contrast-toward). So on a .bg-primary band this resolved to the PAGE
     ground's ink and hovering a tab turned its label near-black on blue,
     about 1.9:1, while the rest state was correct because it reads --muted,
     which publishers do re-declare. `inherit` takes the band's own ink and
     is right on every surface including the page ground, where it is the
     same value this used to name. Pilot 03, on a blue rail. */
  rbmg-tabs [role="tab"]:is(:hover, :focus-visible) { color: inherit; }

  /* THE RAIL'S DISTRIBUTION — v0.4.0, ticket 10.
     Pilot 02's comp drew an equal-width, divider-separated rail. The build
     wrote out what a site MAY and MAY NOT change about a core component,
     concluded correctly that this is a tab VARIANT rather than a local patch,
     shipped the core's rail centred, and raised a ticket. Same shape as the
     chevron indicator, and the same answer: the variant belongs here.

     A SECOND AXIS, not a second value of `overflow`. `overflow` answers "what
     happens when the tabs do not fit" (wrap / scroll / stack); `rail` answers
     "how is the space divided when they do". Folding them into one attribute
     would make `equal` and `scroll` mutually exclusive by accident rather than
     by reason.

     And they ARE mutually exclusive by reason, which is why the selector says
     so. Scroll mode's `flex: 0 0 auto` is what makes the rail scrollable —
     equal-width would override it to flex: 1 and silently DELETE the mode.
     Pilot 02 identified that exact trap while deciding not to patch locally;
     :not([overflow="scroll"]) is that reasoning made unwritable. */
  rbmg-tabs[rail="equal"]:not([overflow="scroll"]) [role="tablist"] { flex-wrap: nowrap; }
  rbmg-tabs[rail="equal"]:not([overflow="scroll"]) [role="tab"] {
    flex: 1 1 0;
    min-width: 0;          /* a flex item's min-content floor would break the equality */
  }

  /* Dividers are their own boolean, because "equal width" and "separated by a
     rule" are two decisions a comp makes independently. The divider reads
     --border, which every publisher now re-declares from its own fill (ticket
     19), so a rail on a red band gets a red-ink divider rather than the page
     ground's grey at 1.46:1.

     THE DIVIDER FOLLOWS THE RAIL'S AXIS. `border-inline-start` is the rule
     between two tabs sitting side by side; `overflow="stack"` turns the rail
     vertical (aria-orientation, Up/Down keys), and a left border between
     stacked tabs draws a 1px tick down the left edge of each one instead of a
     rule between them. Pilot 03's comp draws a divided vertical rail, found
     the divider on the wrong axis, and logged it rather than redrawing the
     rule in site.css. The layout owns the axis, so the selector says which. */
  rbmg-tabs[divided]:not([overflow="stack"]) [role="tab"] + [role="tab"] {
    border-inline-start: 1px solid var(--border);
  }
  rbmg-tabs[divided][overflow="stack"] [role="tab"] + [role="tab"] {
    border-block-start: 1px solid var(--border);
  }

  /* ACTIVE — through a seam, v0.3.1 (Pilot 02).
     This shipped as a bare var(--brand-primary) and was wrong on BOTH
     reference brands at once, which is the tell that it was structural
     rather than a brand's problem to solve.

     A brand hue is a FILL. Here it was consumed as TEXT sitting directly on
     the page ground, plus a 3px state indicator — and a lead hue chosen to
     carry white/dark labels has no obligation to be legible ON the ground.
     Measured: Ridgeline's ridge-teal on white 2.20:1, Ironwood's iron-red
     on its dark ground 2.56:1, Pilot 02's #C02021 on #0F222E 2.69:1. Three
     brands, three failures, zero instruments that could see one.

     This is the SAME SHAPE as .btn-outline-primary, which was given the
     --btn-outline-* seam and a COMPONENT_PAIRS row for exactly this reason.
     Both reference brands already answer that seam, and their comments
     already record these ratios — four lines from where the tab needed
     them. The tab had neither seam nor gate, so the failure was not merely
     unfixed, it was unfalsifiable.

     ONE role for the label and the indicator, deliberately: two would let a
     brand answer the fill and leave a stale indicator behind, which is the
     stale-hover failure one level over. Answering the seam moves both.

     The seam is UNDECLARED, with the fallback at the point of use — never
     a [DEFAULT] declared at :root chaining to a role a brand overrides.
     That is the --on-inverse rule, and it is why this reads as a fallback
     rather than a declaration. Existing markup is unaffected: a brand whose
     primary clears 4.5:1 on its ground answers nothing. */
  rbmg-tabs [role="tab"][aria-selected="true"] {
    color:               var(--tab-active, var(--brand-primary));
    border-bottom-color: var(--tab-active, var(--brand-primary));
  }

  /* ACTIVE AS A SURFACE — v0.4.0, Pilot 03.
     `active="fill"` swaps the colour treatment for a filled pill. See
     core.tokens.css §THE FILLED ACTIVE TAB for why the label is derived
     from the fill rather than sharing --tab-active's single role.

     The fill IS the indicator, so the edge marker is cleared in both
     directions — the bottom border the default rail draws, and the
     inline-start border `overflow="stack"` moves it to. Leaving either
     behind paints a 3px bar in --tab-active along the edge of a pill that
     is already announcing itself by its fill, which is two indicators
     disagreeing about which one means "selected".

     A NOTE ON WHY THIS IS AN ATTRIBUTE AND NOT A TOKEN. Same reasoning as
     indicator="chevron" (ticket 16): "the active tab is a filled pill" is
     STRUCTURE — it changes what the element is, not what colour it is. A
     brand answering a token cannot be allowed to turn an underline into a
     pill, because the site's layout has to know which one it is getting.

     The radius is a token with the button's stop as its fallback, because a
     comp that draws pill tabs has almost always drawn pill buttons, and
     making a site restate that is how a literal gets written. */
  rbmg-tabs[active="fill"] [role="tab"][aria-selected="true"] {
    background-color: var(--tab-active-fill, var(--surface));
    color: var(--on-tab-active-fill);
    border-color: transparent;
    border-radius: var(--tab-active-radius, var(--btn-radius, var(--radius-md)));
  }
  rbmg-tabs [role="tabpanel"] { padding-block: 1.5rem; }

  /* ---- Overflow modes (v0.3.0, Pilot 01 F-016) ---------------------
     Nine tabs need ~1,440px of rail; a phone has 375. The component had
     no overflow handling at all, so every site would have decided this
     for itself — the mechanism that produced seven divergent stylesheets.
     `wrap` is the default and is what the core always did, so existing
     markup is untouched.

     SCROLL — a rail that must stay on one line. The edge fade IS the
     affordance: without it a cut-off rail looks like a rail that ends,
     which is the failure mode of every hand-rolled version. Same
     mask-image technique the drawer already uses on its scroll region,
     driven by classes the component publishes. The fades appear only
     where there is more rail — no classes means it all fits and no fade
     is drawn. */
  rbmg-tabs[overflow="scroll"] [role="tablist"] {
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scrollbar-width: none;                 /* the fade is the affordance */
    mask-image: linear-gradient(to right, black 0, black calc(100% - 2rem), transparent 100%);
  }
  rbmg-tabs[overflow="scroll"] [role="tablist"]::-webkit-scrollbar { display: none; }
  rbmg-tabs[overflow="scroll"] [role="tab"] {
    flex: 0 0 auto;
    scroll-snap-align: start;
  }
  /* The focus ring goes INSIDE the tab in scroll mode. An outline is
     painted outside the border box, and `overflow-x: auto` clips it — the
     first and last tabs lose part of their ring against the container
     edge. Measured on the end tab: the ring wanted 1162px inside a
     container ending at 1158px, so 4px of it was cut.
     That is WCAG 2.4.11 damage introduced BY adding scroll mode, which is
     the kind of thing a feature quietly brings with it. An inset ring
     cannot be clipped by its own scroll container. */
  rbmg-tabs[overflow="scroll"] [role="tab"]:focus-visible {
    outline-offset: -3px;
  }
  rbmg-tabs[overflow="scroll"] [role="tablist"].is-scrolled-from-start {
    mask-image: linear-gradient(to right, transparent 0, black 2rem,
                                black calc(100% - 2rem), transparent 100%);
  }
  rbmg-tabs[overflow="scroll"] [role="tablist"].is-scrolled-from-start.is-scrolled-to-end {
    mask-image: linear-gradient(to right, transparent 0, black 2rem, black 100%);
  }
  rbmg-tabs[overflow="scroll"] [role="tablist"].is-scrolled-to-end:not(.is-scrolled-from-start) {
    mask-image: none;
  }

  /* STACK — vertical, for long labels or a narrow column. The active
     indicator moves to the inline-start edge: a bottom border on a
     stacked item reads as a divider rather than a selection. The
     component also sets aria-orientation="vertical", which moves the
     arrow keys to Up/Down (APG) without the site having to know. */
  rbmg-tabs[overflow="stack"] [role="tablist"] {
    flex-direction: column;
    align-items: stretch;
    border-bottom: 0;
    border-inline-start: 1px solid var(--border);
  }
  rbmg-tabs[overflow="stack"] [role="tab"] {
    justify-content: flex-start;
    text-align: start;
    border-bottom: 0;
    border-inline-start: 3px solid transparent;
    margin-inline-start: -1px;  /* scale-exempt: sub-pixel geometry, not rhythm */  /* sit the indicator on the rail */
  }
  rbmg-tabs[overflow="stack"] [role="tab"][aria-selected="true"] {
    border-inline-start-color: var(--tab-active, var(--brand-primary));
  }

  /* A FILLED active tab IS the indicator, so the rail's apparatus is
     redundant: the 1px track the marker slides along, and the 3px gutter
     every tab reserves for it. The core already blanks the marker on the
     selected tab for exactly this reason (border-color: transparent, below)
     — this finishes the thought instead of leaving a track with nothing on
     it and a 1px negative margin holding the tabs off the edge. Found on
     Pilot 03, whose comp draws neither. */
  rbmg-tabs[overflow="stack"][active="fill"] [role="tablist"] { border-inline-start: 0; }
  rbmg-tabs[overflow="stack"][active="fill"] [role="tab"] {
    border-inline-start-width: 0;
    margin-inline-start: 0;
  }

  /* ====================================================================
     COLLAPSE / ACCORDION
     ==================================================================== */
  rbmg-collapse { display: block; }
  rbmg-accordion { display: block; }
  rbmg-accordion rbmg-collapse + rbmg-collapse { border-top: 1px solid var(--border); }

  rbmg-collapse [data-collapse-toggle] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    width: 100%;
    min-height: 44px;
    padding: 1rem 0;
    border: 0;
    background: none;
    color: inherit;
    text-align: start;
    font-family: var(--font-heading);
    font-weight: var(--font-weight-semibold);
    font-size: var(--fs-p-lg);
    cursor: pointer;
  }
  /* Plus/minus mark — the near-universal override, now the default.
     No chevron, and nothing to hide.

     Drawn as two bars rather than swapping a "+" glyph for a "−" glyph, so
     it can MORPH: the vertical bar shrinks to nothing as the panel opens,
     on the panel's own duration and curve. The glyph version carried a
     `transition: transform` that could never fire — nothing it applied to
     ever transformed — which is the panel's own defect in miniature: a
     declaration that names motion over something that snaps.

     THE INK IS A SEAM — v0.4.1, Pilot 04 (E-100). The bars painted
     --icon-accent while the toggle's LABEL inherits, so the indicator could
     not follow the control it belongs to. Pilot 04's comp draws both in the
     same ink (#0F2848, sampled off the comp's own raster, byte-identical to
     the label beside it) and the build measured a red `+` next to a navy
     question. There was no compliant fix: the pseudo-element is a core
     component internal, and a site rule would have WON without being allowed.

     No reference brand could have exposed it, which is the reason it took four
     pilots: on an inverse band --icon-accent resolves white and the label is
     white too, so the two agree by accident. It only separates on a light
     ground.

     WHY THIS DOES NOT CONTRADICT rbmg-tabs three hundred lines up, which
     argues the opposite about itself ("ONE role for the label and the
     indicator, deliberately: two would let a brand answer the fill and leave a
     stale indicator behind"). Both are right, because they mark different
     things. A tab's indicator is a STATE marker and has to move with whatever
     colour says "selected". A collapse's +/− is part of the toggle's own label
     line, and a comp routinely draws it in the label's ink. The seam lets a
     brand say which, and defaults to today's behaviour.

     The bars take a role rather than currentColor by default, and a brand that
     wants them to follow the label answers `--collapse-indicator: currentColor`
     — which is SAFE here and it is worth saying why, because the core bans
     currentColor elsewhere. This rule sets `background` and never `color`, so
     currentColor adopts the toggle's inherited label rather than resolving
     against a `color` set in its own block. That is the documented legitimate
     use (a rest state adopting its surface), not the .btn-outline-inverse trap
     that painted a fill and its label the same colour at 1.00:1. This file is
     one of the four the F-019 structural test scans for precisely that pair. */
  rbmg-collapse [data-collapse-toggle]::after {
    content: "";
    flex-shrink: 0;
    width: 1rem;
    height: 1rem;
    background: linear-gradient(var(--collapse-indicator, var(--icon-accent)) 0 0) center / 100% 2px no-repeat,
                linear-gradient(var(--collapse-indicator, var(--icon-accent)) 0 0) center / 2px 100% no-repeat;
    transition: background-size var(--transition-standard) var(--ease-exit);
  }
  rbmg-collapse.is-open [data-collapse-toggle]::after {
    background-size: 100% 2px, 2px 0%;
    transition-timing-function: var(--ease-enter);
  }

  /* THE CHEVRON VARIANT — v0.4.0, ticket 16.
     Plus/minus is the near-universal default and stays the default. Pilot 02's
     comp drew a chevron, there was no seam, and the build correctly declined to
     restyle a core component's internals from site.css — so it shipped the
     wrong indicator and raised a ticket instead. That is the process working;
     this is the other half of it.

     An ATTRIBUTE, not a token. A token would let a brand answer "chevron" as
     though it were a value, and the indicator is STRUCTURE — it changes what
     the pseudo-element is, not what colour it takes. An attribute on the
     component is how this file already spells a structural variant
     (rbmg-tabs[overflow], rbmg-drawer[mode]), so the vocabulary is consistent.

     It ROTATES rather than swapping glyphs, for the same reason the bars morph
     rather than swapping "+" for "−": a transition needs a property that can
     actually transition, and `content` cannot. Drawn with borders so it takes
     the icon role and scales with its box exactly as the bars do. */
  rbmg-collapse[indicator="chevron"] [data-collapse-toggle]::after {
    width: 0.5rem;
    height: 0.5rem;
    margin-inline-end: 0.25rem;      /* smaller box — keep the optical edge */
    background: none;
    border-inline-end: 2px solid var(--collapse-indicator, var(--icon-accent));
    border-block-end: 2px solid var(--collapse-indicator, var(--icon-accent));
    rotate: 45deg;                   /* points down = closed */
    transition: rotate var(--transition-standard) var(--ease-exit);
  }
  rbmg-collapse[indicator="chevron"].is-open [data-collapse-toggle]::after {
    rotate: -135deg;                 /* points up = open */
    transition-timing-function: var(--ease-enter);
  }

  /* The panel animates its height, and `hidden` lands only once it is
     fully closed. `hidden` is display:none and cannot be transitioned, so
     the previous version snapped open and shut while #afterTransition
     waited two animation frames and fired `hidden.bs.collapse` with no
     transition having run at all — a method named for motion that never
     waited on any.

     max-height, not a grid 0fr→1fr wrapper. The grid technique is tidier
     on paper but needs an inner element held at `overflow: hidden` for as
     long as the panel is open, which clips the focus ring of any link
     inside the panel and any dropdown flyout it contains — the same WCAG
     2.4.11 damage F-016 found on the scrolling tab rail, reintroduced by
     the fix for something else. Here the component measures once and
     clears BOTH max-height and overflow on transitionend, so a settled
     open panel is an ordinary box with nothing constraining it. It also
     needs no wrapper, so no site's markup changes.

     The padding animates WITH the height. max-height clips the content box,
     and padding lives outside it, so a panel at max-height 0 is still
     `padding-bottom` tall — measured in Chrome at 16px, which the panel
     jumped through instantly at both ends of every transition. A 16px pop
     bracketing a 250ms glide is precisely the jar the animation was added
     to remove, and it is invisible to every test that does not have layout. */
  rbmg-collapse [data-collapse-panel] {
    padding-bottom: 1rem;
    transition: max-height     var(--transition-standard) var(--ease-exit),
                padding-bottom var(--transition-standard) var(--ease-exit);
  }
  rbmg-collapse.is-open [data-collapse-panel] {
    transition-timing-function: var(--ease-enter);
  }

  /* ====================================================================
     DRAWER — one component, two modes
     ==================================================================== */
  rbmg-drawer {
    position: fixed;
    inset: 0;
    z-index: 1050;
    display: block;
    pointer-events: none;
    visibility: hidden;
    /* Duration 0s, delay --transition-standard: visibility flips only after
       the slide-out finishes. No timing keyword — a curve over a 0s step
       means nothing, and naming one here would be the only literal left. */
    transition: visibility 0s var(--transition-standard);
  }
  rbmg-drawer.is-open {
    pointer-events: auto;
    visibility: visible;
    transition-delay: 0s;
  }

  rbmg-drawer [data-drawer-backdrop] {
    position: absolute;
    inset: 0;
    background-color: rgb(0 0 0 / 0.5);
    opacity: 0;
    transition: opacity var(--transition-standard) var(--ease-exit);
  }
  rbmg-drawer [data-drawer-backdrop].is-visible {
    opacity: 1;
    transition-timing-function: var(--ease-enter);
  }

  rbmg-drawer [data-drawer-body] {
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    width: min(22rem, 85vw);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background-color: var(--surface);
    color: var(--on-surface);
    box-shadow: var(--shadow-dropdown);
    translate: -100% 0;
    transition: translate var(--transition-standard) var(--ease-exit);
  }
  rbmg-drawer[side="end"] [data-drawer-body] {
    inset-inline-start: auto;
    inset-inline-end: 0;
    translate: 100% 0;
  }
  rbmg-drawer.is-open [data-drawer-body] {
    translate: 0 0;
    transition-timing-function: var(--ease-enter);
  }

  /* Panels stack in the same box; only the active one is in flow. */
  rbmg-drawer[mode="panels"] [data-drawer-body] { position: absolute; }
  rbmg-drawer [data-drawer-panel] {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex: 1;
  }
  rbmg-drawer [data-drawer-panel][hidden] { display: none; }

  /* A FADE MEANS "THERE IS MORE THIS WAY", so each edge is driven by whether
     content is hidden PAST it — never by which edge you happen to be at.
     Every one of these four rules used to say the opposite of that, and all
     four were wrong at once: a panel that did not overflow faded both ends, a
     region resting at the top faded its top, one mid-scroll stopped fading
     its bottom, and one scrolled to the bottom faded nothing. Nothing caught
     it because no build had ever put a [data-drawer-scroll] in a drawer —
     Pilot 03's mobile menu is the first, three versions in.

     The state names are accurate and unchanged; only the mapping moves.
       is-scrolled-from-top  = scrolled away from the top -> content ABOVE
       is-scrolled-to-bottom = the bottom is reached      -> nothing BELOW

     Read the four together, because they are one truth table and a change to
     any one of them has to be checked against the other three:

       (no class)  scrollable, resting at the top   fade bottom
       from-top    scrollable, mid-scroll           fade both
       to-bottom   NOT scrollable at all            fade neither
       both        scrollable, at the bottom        fade top

     The third row is the one that reads oddly and it is the load-bearing one:
     when a region does not overflow, syncScrollFade sets to-bottom and leaves
     from-top clear, so `to-bottom alone` IS the no-overflow state. A short
     panel must show no fade at all. */
  rbmg-drawer [data-drawer-scroll] {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    mask-image: linear-gradient(to bottom, black 0, black calc(100% - 1.5rem), transparent 100%);
  }
  rbmg-drawer [data-drawer-scroll].is-scrolled-from-top {
    mask-image: linear-gradient(to bottom,
      transparent 0, black 1.5rem,
      black calc(100% - 1.5rem), transparent 100%);
  }
  rbmg-drawer [data-drawer-scroll].is-scrolled-to-bottom { mask-image: none; }
  rbmg-drawer [data-drawer-scroll].is-scrolled-from-top.is-scrolled-to-bottom {
    mask-image: linear-gradient(to bottom, transparent 0, black 1.5rem, black 100%);
  }

  /* :not(.btn): a panel's DEFAULT anchor is a nav link, but a .btn composed
     into a panel (the drawer-CTA pattern every dealer site uses) must stay a
     button. Without the guard this selector's specificity (0,2,2) silently
     beats .btn-<role>'s fill (0,1,0) in the same layer, and a drawer CTA
     renders as a bare link — found by Pilot 03's drawer, invisible to the
     role gate (a component STRIPPED of its role reads as no gate row at all). */
  rbmg-drawer :is([data-panel-target], [data-panel-back], [data-drawer-panel] a:not(.btn)) {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    min-height: 44px;
    padding: 0.75rem 1.25rem;  /* scale-exempt: optical padding inside a control whose size is guaranteed by its own min-height:44px floor and measured by gate:rendered — not layout rhythm */
    border: 0;
    background: none;
    color: inherit;
    text-align: start;
    text-decoration: none;
    font-size: var(--fs-p-md);
  }
  rbmg-drawer [data-panel-target]::after { content: "›"; margin-inline-start: auto; color: var(--icon-accent); }
  rbmg-drawer [data-panel-back]::before  { content: "‹"; color: var(--icon-accent); }
  rbmg-drawer [data-drawer-close] {
    align-self: flex-end;
    min-width: 44px;
    min-height: 44px;
    border: 0;
    background: none;
    color: inherit;
    font-size: 1.5rem;
    cursor: pointer;
  }

  /* Body scroll lock — set by lockScroll()/unlockScroll() */
  body.is-scroll-locked {
    position: fixed;
    inset-inline: 0;
    overflow: hidden;
  }

  /* ====================================================================
     MODAL
     ==================================================================== */
  rbmg-modal {
    position: fixed;
    inset: 0;
    z-index: 1060;
    display: grid;
    place-items: center;
    padding: 1rem;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition-standard) var(--ease-exit);
  }
  rbmg-modal.is-open {
    opacity: 1;
    pointer-events: auto;
    transition-timing-function: var(--ease-enter);
  }
  rbmg-modal[hidden] { display: none; }

  rbmg-modal [data-modal-backdrop] {
    position: absolute;
    inset: 0;
    background-color: rgb(0 0 0 / 0.6);
  }
  rbmg-modal [data-modal-dialog] {
    position: relative;
    width: min(48rem, 100%);
    max-height: 85vh;
    overflow: auto;
    padding: 1.5rem;
    background-color: var(--surface-raised);
    color: var(--on-surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-dropdown);
    translate: 0 1rem;
    transition: translate var(--transition-standard) var(--ease-exit);
  }
  rbmg-modal.is-open [data-modal-dialog] {
    translate: 0 0;
    transition-timing-function: var(--ease-enter);
  }
  rbmg-modal [data-modal-close] {
    position: absolute;
    inset-block-start: 0.5rem;
    inset-inline-end: 0.5rem;
    min-width: 44px;
    min-height: 44px;
    border: 0;
    background: none;
    color: inherit;
    font-size: 1.5rem;
    cursor: pointer;
  }
  /* TICKET 14 — `height: auto` assumes a REPLACED element with an intrinsic
     size. An <img> or <video> has one; an <iframe> does not, so `height: auto`
     resolves to the CSS default 150px and a video embed — the single most
     common thing anyone puts in a modal — rendered as a 150px letterbox.

     aspect-ratio supplies the ratio the element cannot supply itself, and it
     is a seam because 16/9 is the common case and not the only one. Scoped to
     the elements that actually lack an intrinsic size, so an <img> keeps
     sizing from its own file rather than being forced into a ratio it does not
     have — which would be ticket 13's distortion, reintroduced deliberately. */
  rbmg-modal [data-modal-media] { width: 100%; height: auto; border-radius: var(--radius-md); }
  rbmg-modal :is(iframe, embed, object)[data-modal-media] {
    aspect-ratio: var(--modal-media-ratio, 16 / 9);
  }
}

@layer components {
  /* ====================================================================
     ROTATOR — the visual counterpart to initRotators()

     v0.4.1, and its absence was the finding. `initRotators` has shipped since
     v0.2.0, auto-initialising on DOM ready, toggling `.active` on an <img> —
     and the core painted NOTHING for it. The only `.active` rule anywhere in
     the core was `.breadcrumb-item.active`. Measured on a bare rotator: three
     of three frames visible at once, stacked in normal flow, with the
     behaviour faithfully moving a class that changed nothing.

     That is the navbar gap exactly (Pilot 02, ticket 2 — the core shipped the
     nav BEHAVIOUR and no navbar CSS, so every site rebuilt ~150 lines of it),
     in a second behaviour, and it survived four pilots because no build ever
     used a rotator: it was rendered on no page and executed by no test.

     WHAT IS HERE AND WHAT IS NOT. The stack and the crossfade are here,
     because "one frame at a time, faded on the core's own clock" is not a
     design decision — every implementation of this is the same three
     declarations, and the alternative is seven sites writing them differently.
     SIZE IS NOT: a rotator's aspect ratio and dimensions come from the comp,
     so a site gives it `.ratio`, an aspect-ratio, or a height. With none it
     collapses to the height of its absolutely-positioned frames, which is
     zero — the same contract `.ratio` already documents.
     ==================================================================== */
  [data-rotator] { position: relative; }
  [data-rotator] > img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0;
    transition: opacity var(--transition-standard) var(--ease-standard);
  }
  [data-rotator] > img.active { opacity: 1; }
  /* A single frame is never advanced by the behaviour, so it must not be
     hidden by the resting state either — the class is what shows a frame, and
     a one-frame rotator has no class to receive. */
  [data-rotator] > img:only-of-type { opacity: 1; }
}

/* ==========================================================================
   REDUCED MOTION — v0.4.1, and the rewrite is Pilot 04's E-106.

   THIS BLOCK USED TO BE A LIST, under the heading "one place, covering every
   component above". It enumerated eight selectors, all in this file, and the
   heading was false in two directions at once:

     · SITE motion was outside it. A site composing a transition — which the
       core encourages, on its own --ease-* roles — silently animated for a
       user who had asked it not to, and nothing in the file or the SOP said
       the site owed its own block. Every site would have had to remember
       independently.
     · CORE motion was outside it too, and this is the part the ticket did not
       know. Grepping every core stylesheet for `transition:` finds NINE more
       rules the list never covered: .btn, .card-hoverable, .icon-stacked,
       .nav-link, four rules in core.forms.css, and the global `a` transition
       in core.foundation.css. So under the old block a user with the
       preference set still got animated buttons, links, cards, icon chips and
       form controls, FROM THE CORE, while the heading claimed full coverage.

   That is the TAP_RULES lesson exactly — a list that measured seven selectors
   and said nothing whatever about the eighth, under a green PASS — so the fix
   cannot be a longer list. It has to be a rule that cannot be incomplete.

   WHY IT NEEDS ITS OWN LAYER. A universal selector cannot do this job from
   @layer components: `.btn`'s own `transition` is (0,1,0) against `*`'s
   (0,0,0) in the same layer, so the shorthand wins and the reset does nothing.
   `!important` is banned. `@layer a11y` sits after `utilities`, so a
   zero-specificity universal rule beats every core component AND every site
   rule — which is what closes the ticket as filed instead of exporting it to
   the SOP as a thing to remember.

   `animation-*` comes with it. The old block covered transitions only, so a
   keyframed spinner or a marquee was exempt by construction; a user asking for
   no motion means the motion, not one mechanism for producing it. The JS
   honours the same preference for auto-advancing content.

   0.01ms rather than 0: a zero-duration transition never fires transitionend,
   and afterTransition() in core.util.js waits for exactly that event before
   landing `hidden` on a collapse panel. Killing the event would leave panels
   visible in the a11y tree forever — an accessibility fix breaking
   accessibility, which is the kind of thing this layer must not do.
   ========================================================================== */
@layer a11y {
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      transition-duration: 0.01ms;
      animation-duration: 0.01ms;
      animation-iteration-count: 1;
    }
  }
}
