/*
 * =============================================================================
 * Frihat Legal Platform (FLP) - Global Design System
 * File   : 04-buttons.css        Slice: A3 - Button system
 * Owner  : Web tier
 * Depends: 00-tokens.css (fx design tokens), 01-base.css (shared keyframes),
 *          Bootstrap 5.3 (loaded earlier), LeptonXLite 5.4 chrome.
 * =============================================================================
 *
 * WHY THIS FILE IS SHAPED THE WAY IT IS
 * -------------------------------------
 * Bootstrap 5.3 does NOT read `bs-primary` for its button colours. Every
 * `.btn-*` class hardcodes its own set of component locals:
 *     bs-btn-bg / -color / -border-color
 *     bs-btn-hover-bg / -hover-color / -hover-border-color
 *     bs-btn-active-bg / -active-color / -active-border-color
 *     bs-btn-disabled-bg / -disabled-color / -disabled-border-color
 *     bs-btn-focus-box-shadow / -padding-x / -padding-y / -font-size / -radius
 * Therefore the ONLY maintainable way to rebrand the whole button family is to
 * REDECLARE those locals from our own tokens, never to override
 * `background-color` / `color` directly. One declaration block per variant then
 * makes hover, active, focus and disabled follow automatically, instead of
 * needing eight extra rules each.
 *
 * This file used to have to out-specify `wwwroot/global-styles.css`, whose
 * `.btn-primary` and `.btn-outline-primary` blocks set the same locals at
 * (0,1,0). THAT FILE NO LONGER EXISTS - the slice set replaced it - so the
 * (0,2,0) selectors below are no longer defending against it. They are kept
 * anyway, because they are still what makes this file win over Bootstrap's own
 * (0,1,0) rules and over LeptonXLite's chrome regardless of the order in which
 * the ABP bundler concatenates them. That is a specificity decision, not an
 * accident, and it is why no rule in this file uses `!important`.
 *
 *
 * THE LINEAR PASS - WHAT REPLACED THE "FANCY" SYSTEM
 * --------------------------------------------------
 * Every previous pass tuned this file toward ornament: a shine sweep, hover
 * lifts, brand glows, spring overshoot, alpha-black depth gradients and a
 * focus bloom. All of it is GONE. The system is now Linear's, which is defined
 * by four things and nothing else:
 *
 *   SHAPE     Every button reads `fx-radius-control` at every size, and THIS
 *             FILE STATES NO OPINION ABOUT WHAT THAT TOKEN RESOLVES TO. It used
 *             to: this line read "every button is a FULL PILL (9999px)" as
 *             though the pill were a law of the button system. It is not, and
 *             writing it here made the token look like a restatement of a
 *             decision this file had already made rather than the one place the
 *             decision actually lives. See THE PILL, RULED AGAINST below.
 *             The exceptions are shapes that are genuinely something else:
 *             `.fx-fab` (a circle), `.fx-fab-extended` (a capsule, and it takes
 *             `fx-radius-pill` rather than the control token - see section 12),
 *             and row/menu controls that are LIST ROWS rather than buttons and
 *             therefore take `fx-radius-item`. `.btn-icon` is a square on the
 *             control token, so its rendered shape follows that token too.
 *
 *   TEXT      Weight 500 everywhere. Hierarchy is carried by COLOUR TIER, never
 *             by bolding: primary label on the brand fill, secondary label at
 *             `--fx-color-text`, tertiary and ghost labels at
 *             `--fx-color-text-subtle` until hovered. `--fx-tracking-ui` is
 *             applied on the base and auto-zeroes for Arabic in the token layer.
 *             `line-height` equals the button HEIGHT, so the label is centred by
 *             its own line box and a text-only button and an icon+label button
 *             are always the same height even if flex centring is overridden.
 *
 *   SURFACE   Flat fills. No gradients anywhere - not a depth overlay, not a
 *             shine, not a gloss. The primary button changes state through a
 *             `filter`, not through a second colour token. The secondary button
 *             is the only one with modelling, and that modelling is a BEVEL
 *             (inset hairlines plus a ring plus a weak float), not a gradient.
 *
 *   MOTION    Under the cursor: 160ms, `--fx-ease-standard` (ease-out-quad), on
 *             ENUMERATED properties. Press is a scale to `--fx-press-scale` at
 *             `--fx-dur-instant` (60ms) - exits are faster than entrances.
 *             Nothing lifts. Nothing overshoots. Nothing sweeps.
 *
 *
 * THE PILL, RULED AGAINST - 2026-08-03, MEASURED ON THE RUNNING APP
 * -----------------------------------------------------------------
 * `fx-radius-control` was adopted at 9999px from Linear, whose buttons genuinely
 * are pills. The recommendation of this pass is that it moves to
 * `fx-radius-md` (8px), THE INPUT RADIUS. The token change is one line in
 * 00-tokens and is NOT made here; nothing in this file hard codes a corner, and
 * nothing in this file should, which is the property that makes the reversal
 * cheap. What follows is the evidence, because a shape decision reversed without
 * one is just the next person's turn.
 *
 * THE LADDER'S OWN PREMISE IS WHAT THE PILL BREAKS. 00-tokens 4 states it in
 * capitals: RADIUS GROWS WITH THE SURFACE. A corner is a statement about how big
 * a thing is. Measured live, in both themes, that premise is contradicted at a
 * CONSTANT height, which is the one case no size argument can rescue:
 *
 *   /Account/Login   the submit button and the field above it are the SAME BOX:
 *                    40px tall, 362px wide, stacked 40px apart. The field draws
 *                    8px. The button draws 9999px, which on a 40px box is an
 *                    effective 20px. Identical geometry, 2.5x the corner. It is
 *                    also the only element on that screen off the ladder - the
 *                    card is 8, the fields 8, the checkbox 4, the logo 6.
 *   /Identity/Users  THREE controls at exactly 28px tall carry THREE radii:
 *                    the search field 6px, the page size select 8px, and the
 *                    two `.btn-primary.btn-sm` buttons 9999px, i.e. 14px.
 *   /Account/Manage  the submit is 32px tall at an effective 16px; the fields
 *                    above it are 40px tall at 8px. The SMALLER control carries
 *                    the BIGGER corner, which is the ladder read backwards.
 *   /Cases           a 32px `.frihat-btn` at an effective 16px sits in one row
 *                    with two 32px selects and a 32px search field, all at 8px,
 *                    all sharing a top edge at y=130. Same row, same height,
 *                    double the corner.
 *
 * THE COUNTER-ARGUMENT, TAKEN SERIOUSLY. A pill is the clearest "this is an
 * action, not a field" signal there is, and shape is a non-colour channel that
 * survives greyscale. Three reasons it does not carry here:
 *
 *   1. THIS FILE ALREADY NAMES ITS GREYSCALE CHANNEL, AND IT IS NOT THE CORNER.
 *      See DESTRUCTIVE ACTIONS below: "a destructive confirm is a filled surface
 *      where its neighbour is a bevel, which is the non-colour channel that
 *      survives greyscale". FILL and ELEVATION are the channel. On the login
 *      screen the field is a near-white recessed box and the button is a solid
 *      dark green raised one carrying `fx-shadow-stack`; in greyscale those two
 *      are further apart than any corner value could put them. The pill is a
 *      redundant third signal, and it is the one that costs alignment.
 *   2. THE PILL IS NOT EXCLUSIVE HERE, SO IT IS NOT A SIGNAL. On Linear the
 *      stadium means "button" because nothing else is one. In this product the
 *      shell rail toggle, the user profile chip, the page header rule, the
 *      toolbar divider, the section title marker and the scrollbar thumbs are
 *      all drawn at a pill radius. A shape that means six things means none.
 *   3. THE COSTS ARE NOT SYMMETRIC. The category cue is wanted once per screen
 *      region; the misalignment is paid on every dense row, every dialog footer
 *      and every form in an Arabic form tool. That is a systemic cost for a
 *      benefit the fill already delivers.
 *
 * WHY `fx-radius-md` AND NOT `fx-radius-lg`. md is documented as THE input
 * radius, and the whole defect is a button disagreeing with the input beside it;
 * md makes the login button and the login field the same object twice, which is
 * what their identical geometry already says they are. lg (10px) is the CARD
 * rung - it would put a 32px button and the 881px content card on one corner,
 * which breaks the ladder's premise a second way, and a 2px gap from the fields
 * would read as a wobble rather than as a distinction.
 *
 * WHAT MUST TRAVEL WITH THE TOKEN CHANGE, because a half converted product is
 * worse than either end state: `fx-radius-control` is also read by 01-base (the
 * bare button element), 03-leptonx (the shell chrome), 08-feedback (the
 * SweetAlert2 footer) and 09-tabulator. Those are correct consumers and they
 * move for free. 03-leptonx line 599 is NOT: it maps
 * `bs-border-radius-pill` to this token, so Bootstrap's `.rounded-pill`
 * utility would start drawing 8px rectangles. That line wants `fx-radius-pill`.
 *
 *
 * ACCENT DISCIPLINE - THE SITE MAP FOR THIS FILE
 * ----------------------------------------------
 * RE-DERIVED 2026-08-03 against the REWRITTEN 00-tokens 3.9. That section is no
 * longer a closed list of six sites; it is a DERIVATION - Q1 identity or
 * position, Q2 pointer or keyboard, Q3 neither - with a list kept underneath as
 * the record of what has been derived so far. The numbering moved when it was
 * rewritten, and every site number in this header was stale until this pass.
 * The current mapping, which is what a reviewer checks each brand reference in
 * this file against:
 *
 *   SITE 5   THE FOCUS RING            `--fx-color-focus-ring`
 *            `.btn`, `.btn-check` + label, `.btn-close`, `.toast .btn-close`,
 *            `.fx-fab`, `.frihat-btn`, `.frihat-row-action`.
 *            IT IS BRAND IN BOTH THEMES NOW, not dark only - light joined dark
 *            on brand in the third pass. The token layer picks the ramp step;
 *            this file only consumes the token.
 *   SITE 6   THE PRIMARY BUTTON FILL   `--fx-color-primary`
 *            `.btn-primary`; `.fx-fab`, which is the same button drawn as a
 *            circle; and the `.frihat-btn` family - the base, `.frihat-save`,
 *            `.frihat-confirm` and `[data-role="save"]`. 3.9 qualifies this site
 *            "one per screen region; a form has one", and that parenthetical is
 *            what the ruling below turns on.
 *   SITE 8   LINK TEXT                 `--fx-color-link` / `-link-hover`
 *            `.btn-link` only, which IS link text wearing a button class.
 *
 * `--fx-color-text-on-primary` appears in two further places and is NOT a brand
 * reference: it is the ink measured to sit ON a brand or a status fill (see
 * section 1a), so it travels with site 6 and with the four status fills rather
 * than being a site of its own.
 *
 * NOTHING ELSE IN THIS FILE IS BRAND. No indicator bar, no kicker, no sort arrow
 * - those marks belong to the nav, page and grid slices - and no SURFACE: 3.9's
 * three brand surfaces are the shell band, the shell rail and the dashboard icon
 * tiles, owned by 03-leptonx and 11-home. This file paints none of them.
 *
 *
 * THE TONAL BRAND FAMILY - RULED AGAIN 2026-08-03, AND THE ANSWER IS UNCHANGED
 * ---------------------------------------------------------------------------
 * `.btn-outline-primary`, `.btn-soft-primary` and `.btn-ghost-primary` are
 * NEUTRAL. They were made neutral during the Linear pass on the reasoning that
 * "a tonal brand chip is not one of the then-six sanctioned sites, and there is
 * no seventh". THAT REASONING IS NOW WORTHLESS. The list was extended three
 * times in a single day, and 3.9 was rewritten precisely because a list that
 * moves that often is not the rule. So the question was RE-OPENED and put
 * through the derivation instead - these three classes are markup that Bootstrap
 * and ABP emit by default, and demoting them by inertia is not an answer.
 *
 *   Q1  Does a tonal primary button tell me WHO THIS PRODUCT IS, or WHERE I
 *       CURRENTLY AM? No, on both halves. Identity on any screen is already
 *       carried by the shell band and by the ONE filled `.btn-primary`. And a
 *       button is an action OFFERED, never a report of where the user is - a
 *       tab, a nav row and a sort arrow answer that half; a button does not.
 *   Q2  Does it respond to my pointer or my keyboard? Its REST state does not.
 *       Its hover and active states do, and they already take `--fx-color-hover`
 *       and `--fx-color-active`, which IS the tint and is correct.
 *   Q3  Neither. Reach for weight, size, space and a text tier - which is what
 *       the family already does: one hairline, a page-coloured fill and the
 *       tertiary ink tier.
 *
 * So the family stays neutral, and it now stays neutral for a reason that will
 * survive the next rewrite of 3.9. Three things confirm the answer rather than
 * carry it:
 *   · Q1a IS NEVER REACHED, so the surface clause never opens. Had it been
 *     reached, S1 fails (a button is an action, not identity), S2 fails - 3.9
 *     forbids a brand plane behind "a TOOLBAR, FORM, MODAL or SECTION BODY ...
 *     where the user works" - and S3 fails outright on frequency, because an
 *     outline button recurs per toolbar and per grid row and S3 disqualifies
 *     anything whose count comes from the data.
 *   · SITE 6 READS "one per screen region; a form has one". A tonal green chip
 *     standing beside the filled green button is a second brand fill in that
 *     region, which the site's own qualifier excludes.
 *   · THE FORBIDDEN HALF IS EXPLICIT that a diluted brand is not a tint: "not a
 *     lighter brand, not brand at 10 percent alpha". A tonal chip is exactly
 *     that, and the fifth pass restated the prohibition by name.
 *
 * WHAT ACTUALLY CHANGED UNDER THIS FILE, so nobody mistakes the ruling for
 * inertia: the third pass ADDED five brand sites and the fifth pass added three
 * brand surfaces, and not one of the eight reaches a button. All five additions
 * are MARKS - an indicator bar, a tab underline, a page kicker, a sort arrow,
 * the focus ring. All three surfaces are CHROME - the band, the rail, the
 * dashboard tiles. The green came back to the product in the shell and the
 * navigation, which is where the client said it had gone missing. It did not
 * come back to the button family, and that is the correct reading of a contract
 * whose load-bearing half is still the forbidden list.
 *
 * A reviewer counting brand references in this file should therefore find them
 * only in `.btn-primary`, the `.frihat-btn` primary aliases, `.fx-fab`,
 * `.btn-link`, and the focus ring.
 *
 * `.frihat-row-action:hover` is `--fx-color-hover` - THE TINT, which has been
 * the client's mint ramp since the fourth pass and has not been grey since. See
 * the note over section 6.
 *
 *
 * THE VARIANT SET, AND HOW IT MAPS ONTO THE CLASSES BOOTSTRAP AND ABP EMIT
 * -----------------------------------------------------------------------
 *   primary    `.btn-primary` · `.frihat-btn` (base) · `.frihat-save`
 *              `.frihat-confirm` · `[data-role="save"]` · `.fx-fab`
 *              Brand fill, no border, no shadow but the physical card edge.
 *              Hover = `filter: brightness(...)`. Active = a deeper filter plus
 *              the press scale.
 *
 *   secondary  `.btn-secondary` · `.btn-outline-secondary` · `.btn-light`
 *              `.btn-soft-*` · `.frihat-cancel` · `[data-role="cancel"]`
 *              Neutral surface plus a simulated bevel. Hover steps the fill up
 *              to `--fx-color-level-3`.
 *
 *   tertiary   `.btn-outline-*` (the whole ex-tonal family)
 *              One real hairline border, page-coloured fill, tertiary ink at
 *              rest. Hover promotes the ink to primary and steps the border and
 *              the fill up one notch.
 *
 *   ghost      `.btn-ghost*` · `.fx-btn-ghost*` · toolbar loose buttons ·
 *              `.dropdown-menu .btn` · `.frihat-row-action`
 *              No border, no fill, tertiary ink. Hover = `--fx-color-hover`
 *              plus primary ink. Active = `--fx-color-active`.
 *
 *   danger     Linear ships no danger variant. This platform has destructive
 *              actions, so one exists - built in the Linear idiom. See below.
 *
 *   link       `.btn-link`. Link text that happens to be a button.
 *
 *
 * DESTRUCTIVE ACTIONS - THE TRIGGER IS NOT FILLED, THE CONFIRM IS
 * ---------------------------------------------------------------
 * A filled red button is a loud object, and the previous pass put one on every
 * delete affordance in the platform - including grid row glyphs, where a wall
 * of red boxes reads as an error state rather than as an action list. Red is
 * now a TEXT and BORDER colour on a ghost or tertiary base for the TRIGGER, and
 * a filled surface only on the FINAL CONFIRMATION:
 *
 *   trigger  `.btn-outline-danger` · `.btn-ghost-danger` · `.btn-soft-danger`
 *            `.frihat-row-action.frihat-delete`
 *   confirm  `.btn-danger` · `.frihat-btn.frihat-delete`
 *            `.frihat-delete-confirm` · `[data-role="confirm"]`
 *
 * `.frihat-btn.frihat-delete` is in the CONFIRM list on purpose. Read the
 * emitters before "fixing" it: FLP.Courts/Pages/Courts/Index.js line 213 emits
 * `class="frihat-btn frihat-delete" data-role="confirm"` for the modal's final
 * Delete, while the same file's row formatter (line 25) emits
 * `class="frihat-row-action frihat-delete"` for the trigger. The same modifier
 * means two different things depending on the base class it is attached to, and
 * both are skinned here accordingly.
 *
 * COLOUR IS STILL NOT THE ONLY SIGNAL. The previous pass leaned on a bold
 * weight for this, which this file can no longer use - hierarchy comes from
 * colour tier and weight is 500 everywhere. The non-colour channels are now
 * SHAPE and POSITION: a destructive confirm is a filled surface where its
 * neighbour is a bevel, and it is the only filled surface in a footer that
 * otherwise has one. Markup MUST additionally carry a trash icon and an
 * explicit verb; that obligation is stronger here than it was, not weaker, and
 * CSS cannot enforce it.
 *
 *
 * FOCUS - ONE EXPRESSION, ONE EXCEPTION
 * -------------------------------------
 * `outline: var(--fx-focus-width) solid var(--fx-color-focus-ring)` at
 * `outline-offset: var(--fx-focus-offset)`, on `:focus-visible` only. Never a
 * box-shadow, never a glow, never a border-colour change, never a lift. The
 * tokens resolve to 2px/2px and re-tier themselves under `prefers-contrast`, so
 * the literal values the brief specifies are what ships.
 *
 * `--fx-shadow-focus-glow` is DEPRECATED and is referenced nowhere in this file.
 * NEITHER IS `fx-shadow-focus`, AS OF 2026-08-02. It used to appear twice, on
 * `.frihat-row-action` and on `.toast .btn-close`, on the reasoning that each
 * lives inside a container that clips. That reasoning does not hold: the token
 * is reserved for an element that clips ITS OWN overflow, and in both of those
 * cases the clipper is an ANCESTOR - a Tabulator cell and a toast. A box-shadow
 * is cropped by an ancestor's overflow in precisely the way an outline is, so
 * the token changed nothing and the ring stayed cut on the trailing side.
 *
 * The exception that survives is therefore to the OFFSET, not to the outline.
 * Both controls now take a real outline at a NEGATIVE `outline-offset`, drawn
 * just inside their own border box where no ancestor can reach it. That is the
 * pattern `.fx-chip-remove` established in 08-feedback and the flush list row
 * and accordion header use in 07-surfaces. It also survives forced-colors mode,
 * which discards shadows outright and would have left both controls with no
 * indicator at all.
 *
 *
 * WHAT WAS DELETED IN THIS PASS, AND WHY IT CANNOT COME BACK
 * ---------------------------------------------------------
 *   - THE SHINE SYSTEM. `.btn::before`, `.fx-fab::before` and
 *     `.frihat-btn::before` were three copies of one translucent white band,
 *     with `--fx-btn-shine-peak`, `--fx-btn-shine-angle`, `--fx-btn-shine-dur`,
 *     an `isolation: isolate` stacking context on every button to host it, an
 *     RTL angle mirror, and suppression rules in eight other places. Linear has
 *     no gloss sweep. All of it is gone, along with the `isolation` and the
 *     `z-index: -1` machinery that existed only to support it.
 *   - THE DEPTH GRADIENTS. `--fx-btn-depth-top` / `-bottom` painted
 *     `linear-gradient(180deg, rgba(0,0,0,a), rgba(0,0,0,b))` on `.btn`,
 *     `.fx-fab` and `.frihat-btn`, and about twenty rules existed only to set
 *     those two numbers per state. Linear has no gradient fills. Gone.
 *   - THE RAW rgba COMPOSITES. `--fx-btn-depth-rest` / `-hover` /
 *     `-brand-rest` / `-brand-hover` / `-tonal-rest` / `-tonal-hover` were
 *     built from hand-written `rgba(0,0,0,.07/.10/.08/.16/.10/.22)` fallbacks
 *     and from `--fx-color-primary-rgb`. They are replaced by `--fx-shadow-stack`
 *     (the physical card edge, for a control at rest on a surface) and the
 *     `--fx-shadow-float-*` tier (for genuinely floating layers - the FAB).
 *   - THE LIFTS AND THE SPRING. `--fx-btn-lift-sm/-md/-focus`, every
 *     `translateY`, and `--fx-ease-spring`. A Linear button does not move on
 *     hover; it presses.
 *   - THE FOCUS BLOOM. `--fx-btn-focus-glow` and the `fx-focus-bloom`
 *     animation.
 *   - `--fx-btn-pad-scale`. Padding now comes straight from the control-pad
 *     tokens with no multiplier, which is what makes the size ladder below
 *     readable against the spec.
 *
 *
 * RTL: this is an Arabic-first platform. Only logical properties are used.
 * Physical values appear exclusively where they are genuinely physical: shadow
 * y-offsets and the safe-area maximum in the FAB. With the shine gone there is
 * no direction-aware declaration left in the file at all.
 *
 * ACCESSIBILITY CONTRACT (WCAG 2.2 AA)
 *   - Every size keeps a computed height >= 24 CSS px (SC 2.5.8 Target Size
 *     Minimum), held by an explicit `max()` floor against `--fx-btn-target-min`
 *     rather than by arithmetic that happens to work. The mini tier sits
 *     exactly ON the floor at 24px; every other tier clears it.
 *   - Coarse-pointer sizing is inherited from the token layer, which re-tiers
 *     the control heights to 28/32/40/44/48. This file adds no pointer
 *     overrides.
 *   - Every label clears 4.5:1 against every pixel its own surface can render
 *     in any state. See the measured note above `--fx-btn-primary-hover-filter`
 *     for the one place where that constraint changed a specified value.
 *   - Focus is `:focus-visible` only. Never a bare `:focus`.
 *   - Hover treatments are gated behind `@media (any-hover: hover)`, so a touch
 *     device never sticks in a hover state after a tap.
 *   - prefers-reduced-motion removes the press scale and the opt-in entrance.
 *     prefers-contrast thickens borders and restores an outline on ghosts.
 *
 * DURATION CONTRACT
 *   - Not one hardcoded millisecond value appears in this file, and no duration
 *     anywhere lands in the 200-400ms dead band. Everything under the cursor is
 *     `--fx-dur-base` (160ms) or `--fx-dur-instant` (60ms).
 *
 * FLUIDITY CONTRACT
 *   - No fixed px layout widths anywhere. Widths are 100%, `min()`, `max()`,
 *     `clamp()` or a control-height token.
 *   - No bare `100vh` anywhere. The only viewport unit is `dvmin` inside the
 *     FAB's clamp, so a mobile browser collapsing its URL bar cannot make the
 *     button jump.
 *   - Action rows and toolbars wrap; button groups scroll on the inline axis
 *     rather than overflow, because a wrapped group cannot keep its logical
 *     end-cap radii correct per line (see section 10 for the opt-in that can).
 *   - Long Arabic labels shrink and ellipsise instead of pushing the layout.
 * =============================================================================
 */


/* ── 1) Local aliases ─────────────────────────────────────────────────────────
 *
 * Six locals, down from nineteen. Everything the deleted ornament system needed
 * went with it; what is left is the ink alias, the target floor, the two
 * primary-state filters and the secondary bevel.
 * ------------------------------------------------------------------------- */
:root {
    /* 1a. LABEL INK ON A STATUS FILL. The shared contract ships
       `color-text-on-primary` but no `-on-success` / `-on-warning` /
       `-on-danger` / `-on-info`. The solid status fills are emerald-600,
       amber-600, red-600 and blue-600 - all dark, and all identical in BOTH
       themes by deliberate token design - so one near-white ink serves all
       four, and `text-on-primary` is a frozen brand constant that is near-white
       in both themes. Measured on the fills: 5.82 / 5.15 / 5.93 / 5.76. */
    --fx-btn-ink-light: var(--fx-color-text-on-primary);

    /* WCAG 2.2 SC 2.5.8 "Target Size (Minimum)". A normative constant, not a
       design choice, so it is a literal length rather than a token a future
       rescale could drag below it. Every size is clamped against it. */
    --fx-btn-target-min: 24px;

    /* 1b. THE PRIMARY STATE FILTERS.
     *
     * Linear's primary button changes state through `filter: brightness()`, not
     * through a second colour token, and this file follows that verbatim in the
     * LIGHT theme. `filter` applies to the element as a group, so it moves the
     * fill AND the label together; that is what makes the effect read as light
     * falling on one object rather than as two properties animating.
     *
     * DARK IS 108 PERCENT, NOT 115, AND THAT IS AN AA CONSTRAINT RATHER THAN A
     * TASTE DECISION. Brightening the group pushes both luminances up, but the
     * label CLAMPS at pure white while the fill keeps climbing, so contrast
     * FALLS the harder you push. The ceiling therefore moves with the ink, and
     * the ink changed: `--fx-color-text-on-primary` is now pure white in dark
     * (00-tokens 3.2), which lifts rest from 4.61:1 to 5.16:1 and buys back the
     * headroom the old 105 cap was protecting. Re-measured on the dark theme's
     * green-700 with WHITE ink:
     *     rest  5.16:1     105%  4.76:1     108%  4.54:1
     *     110%  4.40:1     115%  4.08:1
     * 108 is the LAST step that clears 4.5:1 and is where the cap now sits;
     * 110 is already a live AA failure, so this is a ceiling and not a target
     * to round up from. The light theme has no such ceiling - its deeper green
     * measures 7.38:1 at rest and 6.13:1 at 115 percent, because there the
     * label clamps to white while the fill is still far too dark to catch up.
     *
     * THE DECLARATION LAGGED THIS NOTE BY A PASS, AND IT IS CORRECTED HERE
     * (2026-08-03). The dark block below still read `brightness(105%)` - the OLD
     * cap, the one that existed to protect a 4.61:1 rest against the previous
     * near-white ink. The ink moved to pure white, this note was rewritten to
     * record the new 108 ceiling, and the value itself was never changed. All
     * five figures above were re-verified this pass against the current tokens
     * (dark `fx-color-primary` is still green-700 #1e7d43, dark
     * `fx-color-text-on-primary` is still `fx-p-white`), so nothing has moved
     * underneath the arithmetic; only the declaration was stale. 105 was not an
     * accessibility failure - it was simply a quieter hover than the file says
     * it ships.
     *
     * Active is 98 percent in BOTH themes: darkening the group moves both
     * luminances toward zero, which compresses contrast, so it is the value that
     * has to be checked in the tighter theme. Dark measures 4.57:1 at 98%. */
    --fx-btn-primary-hover-filter: brightness(115%);
    --fx-btn-primary-active-filter: brightness(98%);

    /* 1c. THE SECONDARY BEVEL.
     *
     * LIGHT collapses to what Linear's light secondary actually is: one hairline
     * ring plus the weakest float. There is nothing to simulate - a white-ish
     * surface on a near-white page is already legible, and inset light hairlines
     * on it would be invisible at best and dirty at worst.
     *
     * DARK is the full four-layer simulation, because on a near-black page a
     * flat neutral fill has no edge at all: two INSET light hairlines (the top
     * one catching an imagined overhead light, the surrounding one closing the
     * box), a 1px ring outside them, and a drop shadow underneath. The hairlines
     * come from the TRANSLUCENT border family, which inverts with the theme, so
     * "light hairline" is what they actually paint in dark. In light the same
     * tokens are dark ink, which is precisely why the light variant does not use
     * the inset layers. */
    --fx-btn-bevel:
        0 0 0 var(--fx-border-width) var(--fx-color-border-emphasis),
        var(--fx-shadow-float-sm);
}

html[data-bs-theme="dark"] {
    /* 108%, the AA ceiling derived in 1b. Do NOT raise it: 110% measures 4.40:1
       with white ink on green-700 and is a live SC 1.4.3 failure. */
    --fx-btn-primary-hover-filter: brightness(108%);

    --fx-btn-bevel:
        inset 0 var(--fx-border-width) 0 0 var(--fx-color-border-emphasis),
        inset 0 0 0 var(--fx-border-width) var(--fx-color-border-subtle),
        0 0 0 var(--fx-border-width) var(--fx-color-border),
        var(--fx-shadow-float-sm);
}


/* ── 2) Base `.btn` ───────────────────────────────────────────────────────────
 *
 * ONE LOCAL DRIVES THE WHOLE SIZE LADDER. `--fx-btn-h` is read by `block-size`,
 * `min-block-size`, `min-inline-size`, `line-height` and - via section 8 -
 * `inline-size` on an icon button. A size class therefore only has to reset
 * that one local plus its font, padding, icon and gap; it never restates a
 * dimension. This is also why `.btn-icon` needs no per-size rules at all.
 *
 * `line-height` IS the height. Linear centres a button label by its line box
 * rather than by flex, so the text sits identically whether or not the button
 * is a flex container and whether or not a consumer overrides `align-items`.
 * The flex centring is kept as well, because it is what positions the ICON.
 *
 * `padding-block` is ZERO by design. With a fixed `block-size` and a line box
 * of the same height, vertical padding can only push the label out of the box.
 * ------------------------------------------------------------------------- */
.btn {
    /* The size ladder's single source of truth. `max()` makes the SC 2.5.8
       floor a guarantee rather than an arithmetic coincidence. */
    --fx-btn-h: max(var(--fx-control-h-md), var(--fx-btn-target-min));
    --fx-btn-icon: var(--fx-icon-md);
    --fx-btn-shadow: none;
    --fx-btn-filter: none;
    --fx-btn-press-scale: var(--fx-press-scale);
    /* Ink actually rendered right now. The loading state needs to know it,
       because it hides the label by turning `color` transparent. */
    --fx-btn-ink-current: var(--bs-btn-color, currentColor);

    /* --- Bootstrap component locals, restated from our tokens --- */
    --bs-btn-padding-x: var(--fx-control-pad-x-md);
    --bs-btn-padding-y: 0;
    --bs-btn-font-family: var(--fx-font-body);
    --bs-btn-font-size: var(--fx-text-sm);
    /* 500. Not 600, and not a per-variant weight anywhere in this file:
       hierarchy is carried by colour tier. */
    --bs-btn-font-weight: var(--fx-weight-medium);
    --bs-btn-line-height: var(--fx-btn-h);
    /* Most variants are borderless. TERTIARY (section 5) is the one family that
       raises this to a real hairline. The `--bs-btn-*-border-color` locals stay
       declared everywhere because an undefined one resolves to `currentColor`,
       which would paint a stroke the instant any consumer restored a width. */
    --bs-btn-border-width: 0;
    --bs-btn-border-radius: var(--fx-radius-control);
    /* Focus is an outline. Bootstrap's focus box-shadow is neutralised here
       rather than overridden per state. */
    --bs-btn-focus-box-shadow: none;

    /* DISABLED, IN FOUR LINES INSTEAD OF SIXTY. Custom property references are
       resolved against the element's FINAL custom-property cascade, so each of
       these picks up whatever the variant block set on the same element. Every
       variant therefore disables to its own colours at half opacity, and no
       variant has to restate a `--bs-btn-disabled-*` local. Adding a new variant
       gets a correct disabled state for free. */
    --bs-btn-disabled-color: var(--bs-btn-color);
    --bs-btn-disabled-bg: var(--bs-btn-bg);
    --bs-btn-disabled-border-color: var(--bs-btn-border-color);
    --bs-btn-disabled-opacity: 0.5;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    column-gap: var(--fx-control-gap-md);
    /* MIN-block-size, not block-size, and it does two different jobs.
       For a LABELLED button the label is an anonymous flex item whose line box
       is already exactly `--fx-btn-h` tall, so this is merely a floor that the
       content meets on its own. For an ICON-ONLY button there is no text and
       therefore no line box at all - the only flex item is a 16px glyph - so
       here it is the whole geometry.
       It is a floor rather than a hard `block-size` because of
       `prefers-contrast: more`, where a 2px border appears: with
       `box-sizing: border-box` a fixed height would push the line box out of the
       padding box and clip Arabic descenders, whereas a floor simply lets the
       control grow by the border it just gained. */
    min-block-size: var(--fx-btn-h);
    /* A pill needs at least its own height to look like a pill rather than like
       a lozenge, and this doubles as the comfortable minimum for very short
       labels ("OK"). It is a control height, never a layout width. */
    min-inline-size: var(--fx-btn-h);
    max-inline-size: 100%;
    white-space: nowrap;
    /* A long Arabic label shrinks and clips instead of pushing the layout. See
       the ellipsis rule below and the `.btn-wrap` opt-out. */
    overflow: hidden;
    vertical-align: middle;
    text-decoration: none;
    letter-spacing: var(--fx-tracking-ui);
    cursor: pointer;
    position: relative;
    /* No gradient. Anywhere. Stated explicitly so a Bootstrap or LeptonX rule
       that paints one cannot survive into this system. */
    background-image: none;
    box-shadow: var(--fx-btn-shadow);
    filter: var(--fx-btn-filter);
    /* `will-change: transform` WAS HERE AND IS GONE (2026-08-03). THIS NOTE IS
       THE RECORD, and it covers the three other sites in this file that were
       removed in the same pass: `.fx-fab`, `.frihat-btn` and
       `.frihat-row-action`. There is now no `will-change` anywhere in the button
       system.

       THE ARGUMENT IT WAS DEFENDED WITH WAS "every variant presses, so every
       variant is promoted for the press". Both halves fail.

       IT IS NOT TRUE THAT EVERY VARIANT PRESSES. The press is
       `transform: scale(var(--fx-btn-press-scale))`, and a later rule zeroes
       that local to 1 for a large share of the buttons this class covers:
       `.dropdown-menu .btn`, `td .btn.btn`, `.tabulator-cell .btn.btn`,
       `.abp-entity-action-container .btn.btn`, `:disabled`, `.is-loading`, and
       EVERY button in the file under `prefers-reduced-motion: reduce`. For all
       of those the press changes colour and nothing else, so the declaration was
       promising the compositor a transform that can never arrive. A promise the
       element cannot keep is the definition of a speculative one.

       AND PROMOTION IS NOT FREE WHERE IT IS TRUE EITHER. `will-change` costs
       memory and forces a stacking context whether or not the browser honours
       the hint, and it does so for the ELEMENT'S WHOLE LIFE — here, on every
       button on every page, permanently, to buy a 60ms scale that happens on at
       most ONE of them at a time and only while a finger is down. `.btn` is the
       most numerous interactive class in the product; a dense toolbar or a grid
       of row controls was paying the cost dozens of times over for a transform
       that runs once.

       THE TEST THIS FILE NOW APPLIES is the one 08-feedback states at
       `.fx-toast-progress` and at the indeterminate progress bar, which are the
       only two `will-change` declarations left in the design system: a transform
       must genuinely run on the element, UNINTERRUPTED, for the entire time it
       is mounted. Those two animate continuously from mount to unmount and earn
       it. A press does not qualify and neither does a hover. If a future site
       wants one, it has to meet that test or set the hint from script
       immediately before the change and clear it after, which is what the
       property was designed for. */

    transition:
        background-color var(--fx-dur-base) var(--fx-ease-standard),
        border-color var(--fx-dur-base) var(--fx-ease-standard),
        box-shadow var(--fx-dur-base) var(--fx-ease-standard),
        color var(--fx-dur-base) var(--fx-ease-standard),
        filter var(--fx-dur-base) var(--fx-ease-standard),
        opacity var(--fx-dur-base) var(--fx-ease-standard),
        transform var(--fx-dur-base) var(--fx-ease-standard);
}

/* LONG LABEL HANDLING.
   `text-overflow` only applies to a block container, and `.btn` is a flex
   container, so an ellipsis cannot be painted on a bare text node: that case
   degrades to a clean clip (still no layout push). Wrapping the label in a
   <span> - which the Razor pages should prefer for long Arabic strings - gets
   the real ellipsis. `min-inline-size: 0` is what allows a flex item to shrink
   below its content width at all. */
.btn > span,
.btn > .fx-btn-label {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Opt-out: let the label wrap onto multiple lines and grow the button. Use on
   wide, standalone calls to action, never inside a toolbar or a grid row. The
   height-matched line box has to be released or the second line would be set on
   32px leading; block padding comes back so the two lines are not flush against
   the pill's edge. */
.btn.btn-wrap,
.btn.fx-btn-wrap {
    --bs-btn-line-height: var(--fx-leading-normal);

    padding-block: var(--fx-space-2);
    white-space: normal;
    overflow: visible;
    text-align: center;
}

.btn.btn-wrap > span,
.btn.fx-btn-wrap > span {
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
}

/* Icons track the icon scale rather than the font size, so a 16px glyph never
   drifts when the label size changes. `fill: currentColor` is what makes an
   inline SVG follow the label through every colour-tier change - rest, hover,
   disabled - without a single per-variant icon rule. */
.btn > svg,
.btn > .fx-btn-icon {
    inline-size: var(--fx-btn-icon);
    block-size: var(--fx-btn-icon);
    fill: currentColor;
    flex: 0 0 auto;
}

.btn > img {
    inline-size: var(--fx-btn-icon);
    block-size: var(--fx-btn-icon);
    flex: 0 0 auto;
}

.btn > i,
.btn > .fx-icon {
    font-size: var(--fx-btn-icon);
    line-height: 1;
    flex: 0 0 auto;
}

/* FOCUS - one expression for the whole file.
   An outline, not a box-shadow: an outline is not clipped by a `border-radius`
   the way an inset shadow is, it survives `forced-colors`, and it is the only
   indicator that cannot be confused with elevation. `:focus-visible` only, so a
   pointer user never sees it. `z-index` lifts the ring above a grouped
   neighbour that would otherwise overlap it. */
.btn:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: var(--fx-focus-offset);
    z-index: 3;
}

/* PRESS. Written as `.btn.btn` (0,4,0) so it beats every `.btn.btn-x:hover`
   (0,3,0) variant rule below regardless of source order - the press must always
   win when a pointer is both hovering and down.

   The scale is the ONLY transform in the file. The duration drops to
   `--fx-dur-instant` so the press is immediate, and the release is governed by
   the base rule at `--fx-dur-base` - which is the "exits faster than entrances"
   contract expressed in the direction that actually matters here: the press
   itself is the fast leg. */
.btn.btn:not(:disabled):not(.disabled):active,
.btn.btn:not(:disabled):not(.disabled).is-pressed {
    transform: scale(var(--fx-btn-press-scale));
    transition-duration: var(--fx-dur-instant);
}


/* ── 3) Sizes ─────────────────────────────────────────────────────────────────
 *
 * The Linear ladder, mapped onto the classes this codebase already emits:
 *
 *   class       tier      height   font        icon   padding-x   gap
 *   .btn-xs     mini      24       text-xs     12     10          4
 *   .btn-sm     small     28       text-sm     16     12          8
 *   .btn        medium    32       text-sm     16     16          8
 *   .btn-lg     default   40       text-base   18     16          6
 *   .btn-xl     large     44       text-lg     18     20          6
 *
 * Every value is a token: `--fx-control-h-*`, `--fx-text-*`, `--fx-icon-*`,
 * `--fx-control-pad-x-*`, `--fx-control-gap-*`. The one place the ladder and
 * the token set disagree is the medium tier's padding - the spec says 14px and
 * the pad scale offers 12 or 16 - and it is resolved UP to 16 rather than by
 * inventing a token. See the report note.
 *
 * `.btn-xl` is new in this pass; the 44px tier had no class before and the
 * ladder needs one. Nothing emits it yet, which is why it is safe to add.
 *
 * RADIUS IS CONSTANT ACROSS SIZES. Every tier reads `fx-radius-control` and no
 * size class restates a corner, which is what keeps the shape decision in one
 * place. While that token is a pill the constancy is free, because a pill is
 * scale-invariant. While it is a fixed length the constancy is a CHOICE, and it
 * is the right one for the same reason the ladder exists: a 24px chip and a 44px
 * hero action are both CONTROLS, they sit in rows with the same 8px fields, and
 * a corner that drifted per tier would make a toolbar of mixed sizes read as
 * mixed components. The ladder's "radius grows with the surface" governs the
 * step from a control to a card to a panel, not the step from a small button to
 * a large one.
 *
 * ══ RE-EXAMINED 2026-08-03 AND UPHELD. THE ANSWER IS NO CHANGE ═════════════
 *
 * The paragraph above was written the same day the pill was retired, and within
 * hours a review found what looked like the same defect one rung lower: on
 * /Identity/Users, "مستخدم جديد" and "أجراءات" draw 8px at 28px tall while the
 * search box, the page-length select and the pager beside them draw 6px. The
 * question was handed here on the correct grounds — if the small button rung is
 * wrong at 8px it is wrong everywhere, and that is one decision for this file
 * rather than a patch in a grid file. It was measured on the running app rather
 * than argued. IT IS NOT A DEFECT, AND THE REASONING IS RECORDED SO THE NEXT
 * READER DOES NOT RE-DERIVE IT FROM THE SAME SCREENSHOT.
 *
 * ONE. THE COMPARISON THAT LOOKED WORST IS NOT A SIZE COMPARISON AT ALL. The
 * pager rows are `fx-radius-ITEM`, and that token is 6px at EVERY height — on
 * /Cases the same pager draws 6px at 32px tall, sitting under a toolbar where
 * the button, both selects, the search box and the page-size control are ALL
 * 32px and ALL 8px. A pager row is a list row that happens to be clickable, and
 * measuring a button's corner against it compares a control to an ITEM. Two
 * thirds of the reported crossover dissolves on that alone.
 *
 * TWO. AT THE PLATFORM'S DEFAULT HEIGHT THE TWO LADDERS ALREADY AGREE. 32px
 * button 8px, 32px field 8px; 40px button 8px, 40px field 8px; 44px likewise.
 * The defect the pill retirement fixed — a button disagreeing with the field
 * beside it on a shared top edge — is fully fixed, and /Cases is the evidence.
 * What remains is exactly ONE pair at ONE rung: `.btn-sm` against
 * `.form-control-sm`, both 28px, 8 against 6.
 *
 * THREE. THAT LAST PAIR IS NOT FINDABLE AT REAL SIZE, which the review that
 * raised it recorded in the same breath — and it is the difference between this
 * and the case it replaced. 9999 against 6 was a stadium beside a rectangle and
 * read across a room. 8 against 6 on a 28px box is a two pixel difference on a
 * corner arc, on two controls that in this product do not even share a band.
 *
 * FOUR. AND THE COST IS REAL WHERE THE BENEFIT IS NOT. Stepping the small rung
 * makes `fx-radius-control` a FUNCTION OF A SIZE CLASS, which no other shape
 * token in this system is. Section 8's button group writes the token ONCE on
 * the group and `fx-radius-xs` on every interior edge, so a mixed-size group, a
 * split button and 09-tabulator's row-action trigger would each have to
 * re-derive a corner they currently inherit. That is three new arithmetic sites
 * bought with a difference nobody can see.
 *
 * FIVE. THIS SYSTEM ALREADY ACCEPTS THAT A COMPONENT'S CORNER NEED NOT TRACK
 * ITS OWN HEIGHT. The badge set in 08-feedback is 4 / 7 / 6 at 20 / 24 / 28,
 * deliberately non-monotonic, on measured reference values. "Radius grows with
 * the surface" was never a per-component rule.
 *
 * IF THE 28px RUNG IS EVER MADE TO AGREE, IT SHOULD MOVE THE OTHER WAY. The
 * correct edit is `.form-control-sm` stepping UP to 8px, which puts the whole
 * field ladder from 28px upward on one corner and leaves `fx-radius-control` a
 * single value. That is 05-forms' decision and its ladder note, not this file's,
 * and it is reported there rather than assumed here. Nothing in this file
 * changes either way.
 *
 * Coarse pointers are handled entirely by the token layer, which re-tiers
 * control-h-xs..xl to 28/32/40/44/48 and tiers the padding and icon tokens with
 * them. This file writes NO pointer-based override of its own; consuming the
 * tokens gets tablet and phone sizing for free and keeps one source of truth.
 * ------------------------------------------------------------------------- */
.btn.btn-xs {
    --fx-btn-h: max(var(--fx-control-h-xs), var(--fx-btn-target-min));
    --fx-btn-icon: var(--fx-icon-xs);

    --bs-btn-padding-x: var(--fx-control-pad-x-xs);
    --bs-btn-font-size: var(--fx-text-xs);

    column-gap: var(--fx-control-gap-xs);
}

.btn.btn-sm {
    --fx-btn-h: max(var(--fx-control-h-sm), var(--fx-btn-target-min));

    --bs-btn-padding-x: var(--fx-control-pad-x-sm);
    --bs-btn-font-size: var(--fx-text-sm);

    column-gap: var(--fx-control-gap-sm);
}

.btn.btn-lg {
    --fx-btn-h: max(var(--fx-control-h-lg), var(--fx-btn-target-min));
    --fx-btn-icon: var(--fx-icon-lg);

    --bs-btn-padding-x: var(--fx-control-pad-x-md);
    --bs-btn-font-size: var(--fx-text-base);

    column-gap: var(--fx-control-gap-lg);
}

.btn.btn-xl,
.btn.fx-btn-xl {
    --fx-btn-h: max(var(--fx-control-h-xl), var(--fx-btn-target-min));
    --fx-btn-icon: var(--fx-icon-lg);

    --bs-btn-padding-x: var(--fx-control-pad-x-lg);
    --bs-btn-font-size: var(--fx-text-lg);

    column-gap: var(--fx-control-gap-lg);
}

/* Full-bleed helper for stacked mobile layouts and dialog footers. */
.btn.btn-block,
.btn.fx-btn-block {
    inline-size: 100%;
}


/* ── 4) Solid variants ────────────────────────────────────────────────────────
 *
 * Selectors are `.btn.btn-x` (0,2,0) so they beat both Bootstrap's own (0,1,0)
 * rules and the legacy global-styles.css overrides no matter which file the ABP
 * bundler emits first.
 *
 * Every hover rule in this section lives in the single
 * `@media (any-hover: hover)` block at the end of it. A touch device gets rest
 * and press only, which is what stops a tapped button from sticking in a hover
 * skin until the next tap lands somewhere else.
 * ------------------------------------------------------------------------- */

/* --- PRIMARY: the Frihat green, and the ONLY brand-filled control in the
       platform. No border. No brand-tinted shadow - the elevation is
       `--fx-shadow-stack`, the neutral physical card edge, which is what a
       control at rest on a surface gets. The brand does not glow, bloom or
       tint anything; it is a fill and nothing else.
 *
 * State changes are a FILTER, per section 1b. Because `filter` moves the fill
 * and the label together, there is no second colour token here at all: the
 * `--bs-btn-hover-*` and `--bs-btn-active-*` locals are pinned to the resting
 * colours so Bootstrap's own state rules paint nothing new and the filter is
 * the whole effect. --- */
.btn.btn-primary {
    --bs-btn-color: var(--fx-color-text-on-primary);
    --bs-btn-bg: var(--fx-color-primary);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-color-text-on-primary);
    --bs-btn-hover-bg: var(--fx-color-primary);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-color-text-on-primary);
    --bs-btn-active-bg: var(--fx-color-primary);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: var(--fx-shadow-stack);
}

.btn.btn-primary:not(:disabled):not(.disabled):active,
.btn.btn-primary:not(:disabled):not(.disabled).is-pressed {
    --fx-btn-filter: var(--fx-btn-primary-active-filter);
}

/* --- SECONDARY: the neutral surface with the simulated bevel. This is the
       companion to the primary in every dialog footer, and the pairing is the
       whole point of the variant: one filled brand object beside one modelled
       neutral object. It carries no accent at any state.
 *
 * The fill is level 2 (the raised plane) and steps to level 3 (the floating /
 * white plane) on hover. Body ink throughout - 13.04:1 light, 12.14:1 dark. --- */
.btn.btn-secondary,
.btn.btn-outline-secondary,
.btn.btn-light,
.btn.btn-soft-primary,
.btn.btn-soft-secondary {
    --bs-btn-color: var(--fx-color-text);
    --bs-btn-bg: var(--fx-color-level-2);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-color-text);
    --bs-btn-hover-bg: var(--fx-color-level-3);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-color-text);
    --bs-btn-active-bg: var(--fx-color-level-3);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: var(--fx-btn-bevel);
}

/* --- DARK: an inked neutral slab. Not a third neutral tier so much as the
       inverse of one - it exists because ABP emits `.btn-dark` for a handful of
       high-emphasis neutral actions and a permanently black button on a dark
       page is a bug. Both fills are theme-aware, so it is contrast-safe by
       construction in either theme.
 *
 * Hover does not change the fill: `--fx-color-text` has no "slightly different
 * ink" sibling that is darker in BOTH themes (text-muted is LIGHTER than text
 * in light mode and the direction flips in dark), so a fill step here would be
 * correct in one theme and wrong in the other. The feedback is the bevel
 * strengthening to the float tier. --- */
.btn.btn-dark {
    --bs-btn-color: var(--fx-color-text-inverse);
    --bs-btn-bg: var(--fx-color-text);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-color-text-inverse);
    --bs-btn-hover-bg: var(--fx-color-text);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-color-text-inverse);
    --bs-btn-active-bg: var(--fx-color-text);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: var(--fx-shadow-stack);
}

/* --- STATUS FILLS. All four solid status colours are dark and identical in
       both themes, so all four take the near-white ink (section 1a).
 *
 * `.btn-danger` is the DESTRUCTIVE CONFIRM, not the destructive trigger. See
 * the header. It is a filled surface where its footer neighbour is a bevel,
 * which is the non-colour channel that survives greyscale. --- */
.btn.btn-success {
    --bs-btn-color: var(--fx-btn-ink-light);
    --bs-btn-bg: var(--fx-color-success);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-btn-ink-light);
    --bs-btn-hover-bg: var(--fx-color-success);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-btn-ink-light);
    --bs-btn-active-bg: var(--fx-color-success);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: var(--fx-shadow-stack);
}

.btn.btn-danger {
    --bs-btn-color: var(--fx-btn-ink-light);
    --bs-btn-bg: var(--fx-color-danger);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-btn-ink-light);
    --bs-btn-hover-bg: var(--fx-color-danger);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-btn-ink-light);
    --bs-btn-active-bg: var(--fx-color-danger);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: var(--fx-shadow-stack);
}

.btn.btn-warning {
    --bs-btn-color: var(--fx-btn-ink-light);
    --bs-btn-bg: var(--fx-color-warning);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-btn-ink-light);
    --bs-btn-hover-bg: var(--fx-color-warning);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-btn-ink-light);
    --bs-btn-active-bg: var(--fx-color-warning);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: var(--fx-shadow-stack);
}

.btn.btn-info {
    --bs-btn-color: var(--fx-btn-ink-light);
    --bs-btn-bg: var(--fx-color-info);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-btn-ink-light);
    --bs-btn-hover-bg: var(--fx-color-info);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-btn-ink-light);
    --bs-btn-active-bg: var(--fx-color-info);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: var(--fx-shadow-stack);
}

/* A status fill presses the same way the primary does - the filter is the
   feedback, and darkening a dark fill only raises its label contrast. */
.btn.btn-success:not(:disabled):not(.disabled):active,
.btn.btn-danger:not(:disabled):not(.disabled):active,
.btn.btn-warning:not(:disabled):not(.disabled):active,
.btn.btn-info:not(:disabled):not(.disabled):active,
.btn.btn-dark:not(:disabled):not(.disabled):active {
    --fx-btn-filter: var(--fx-btn-primary-active-filter);
}

@media (any-hover: hover) {
    .btn.btn-primary:hover,
    .btn.btn-success:hover,
    .btn.btn-danger:hover,
    .btn.btn-warning:hover,
    .btn.btn-info:hover,
    .btn.btn-dark:hover {
        --fx-btn-filter: var(--fx-btn-primary-hover-filter);
    }

    /* The neutral family's feedback is the fill step (level 2 to level 3, set
       in the block above through Bootstrap's own hover locals) plus the bevel
       strengthening by one float tier. No filter: brightening a near-white
       surface does nothing, and brightening a dark one washes it out. */
    .btn.btn-secondary:hover,
    .btn.btn-outline-secondary:hover,
    .btn.btn-light:hover,
    .btn.btn-soft-primary:hover,
    .btn.btn-soft-secondary:hover,
    .btn.btn-dark:hover {
        --fx-btn-shadow: var(--fx-shadow-float-md);
    }
}


/* ── 5) The ex-outline family - TERTIARY ──────────────────────────────────────
 *
 * OUTLINED AGAIN, AND FOR THE FIRST TIME HONESTLY. Two passes ago these were
 * strokes; one pass ago they were low-alpha tonal tints with no stroke at all,
 * on a directive that said borders looked cheap. Linear does have an outlined
 * tertiary and it is one of the most-used controls in the product, so the
 * family comes back as the real thing:
 *
 *     one hairline border · a PAGE-COLOURED fill · TERTIARY ink at rest
 *
 * The fill is `--fx-color-level-0`, the page itself. That is deliberate: at
 * rest the button is a rule drawn around a hole, which is the quietest a
 * control can be while still being unmistakably a control. On hover the ink is
 * promoted to primary, the border steps from `--fx-color-border` to
 * `--fx-color-border-emphasis`, and the fill steps to `--fx-color-hover` - THE
 * TINT. Quiet at rest, loud under the cursor.
 *
 * THE TINTS ARE GONE AND SO IS THE ELEVATION THEY NEEDED. A 10-16 percent tint
 * measures 1.03:1 to 1.40:1 against the page, which is why the previous pass had
 * to float the entire family one elevation tier above the solids just to give it
 * an edge - `--fx-btn-depth-tonal-rest`, a shadow standing in for a border. A
 * border is a better border. The family now rests flat, which is also what makes
 * a toolbar of eight of them read as a toolbar rather than as eight cards.
 *
 * `.btn-outline-primary` IS NEUTRAL. It is the tertiary, full stop. It answers
 * Q3 of 00-tokens 3.9 - neither identity nor position - so it is not coloured at
 * all; and site 6 is qualified "one per screen region", which the `.btn-primary`
 * beside this one already spends. THE OLD REASONING HERE WAS A LIST LOOKUP ("not
 * one of the six sanctioned sites, and there is no seventh") and 3.9 has since
 * been rewritten as a derivation, so it was re-run rather than inherited. The
 * full ruling, including why none of the eight sites added in the third and
 * fifth passes reaches a button, is in the header.
 *
 * REST CONTRAST, MEASURED against the page in both themes:
 *   neutral (text-subtle)   5.19 light   5.67 dark
 *   danger  (danger-fg)     5.80 light   5.90 dark
 *   success (success-fg)    5.69 light   7.70 dark
 *   warning (warning-fg)    5.20 light   8.06 dark
 *   info    (info-fg)       5.59 light   7.87 dark
 *
 * THE FILL IS `transparent`, NOT A PAINTED PAGE COLOUR, and that is the
 * difference between "page-coloured" being true and being true only on the
 * page. Painting `--fx-color-level-0` would make a tertiary sitting on a level-1
 * card a visibly darker plate - a recessed well in dark, a grey patch in light.
 * Letting the surface show through is page-coloured ON the page, card-coloured
 * on a card, and modal-coloured in a dialog, from one declaration. It also makes
 * the 5 percent hover wash composite over whatever is actually behind it, which
 * is the only way that wash reads identically everywhere.
 *
 * THE BASE SELECTOR IS ENUMERATED, NOT `[class*="btn-outline-"]`. Bootstrap
 * ships exactly eight outline classes and two of them - secondary and light -
 * belong to SECONDARY in section 4. An attribute selector would be (0,2,0), tie
 * with those, and win on source order, silently dragging both back out of the
 * neutral family. Enumerating is also what lets the per-hue blocks below sit at
 * the same (0,2,0) and win purely by coming later, which is the resolution order
 * this section needs.
 * ------------------------------------------------------------------------- */
.btn.btn-outline-primary,
.btn.btn-outline-success,
.btn.btn-outline-danger,
.btn.btn-outline-warning,
.btn.btn-outline-info,
.btn.btn-outline-dark,
.btn.btn-tertiary,
.btn.fx-btn-tertiary {
    --bs-btn-border-width: var(--fx-border-width);
    --bs-btn-color: var(--fx-color-text-subtle);
    --bs-btn-bg: transparent;
    --bs-btn-border-color: var(--fx-color-border);
    --bs-btn-hover-color: var(--fx-color-text);
    --bs-btn-hover-bg: var(--fx-color-hover);
    --bs-btn-hover-border-color: var(--fx-color-border-emphasis);
    --bs-btn-active-color: var(--fx-color-text);
    --bs-btn-active-bg: var(--fx-color-active);
    --bs-btn-active-border-color: var(--fx-color-border-emphasis);
}

/* --- STATUS TERTIARIES. Only the ink and the border carry the hue; the fill
       stays the page in every one of them, and steps to THE TINT on hover -
       never to a wash in the status hue. A red-tinted plate under a red label is
       two signals for one meaning, and it is what made a grid full of delete
       buttons read as an error report.
 *
 * `.btn-outline-danger` and `.btn-soft-danger` are the destructive TRIGGER, not
 * the confirmation. See the header. --- */
.btn.btn-outline-danger,
.btn.btn-soft-danger,
.btn.btn-tertiary-danger {
    --bs-btn-color: var(--fx-color-danger-fg);
    --bs-btn-border-color: var(--fx-color-danger-border);
    --bs-btn-hover-color: var(--fx-color-danger-fg);
    --bs-btn-hover-border-color: var(--fx-color-danger-fg);
    --bs-btn-active-color: var(--fx-color-danger-fg);
    --bs-btn-active-border-color: var(--fx-color-danger-fg);
}

.btn.btn-outline-success,
.btn.btn-soft-success {
    --bs-btn-color: var(--fx-color-success-fg);
    --bs-btn-border-color: var(--fx-color-success-border);
    --bs-btn-hover-color: var(--fx-color-success-fg);
    --bs-btn-hover-border-color: var(--fx-color-success-fg);
    --bs-btn-active-color: var(--fx-color-success-fg);
    --bs-btn-active-border-color: var(--fx-color-success-fg);
}

.btn.btn-outline-warning,
.btn.btn-soft-warning {
    --bs-btn-color: var(--fx-color-warning-fg);
    --bs-btn-border-color: var(--fx-color-warning-border);
    --bs-btn-hover-color: var(--fx-color-warning-fg);
    --bs-btn-hover-border-color: var(--fx-color-warning-fg);
    --bs-btn-active-color: var(--fx-color-warning-fg);
    --bs-btn-active-border-color: var(--fx-color-warning-fg);
}

.btn.btn-outline-info,
.btn.btn-soft-info {
    --bs-btn-color: var(--fx-color-info-fg);
    --bs-btn-border-color: var(--fx-color-info-border);
    --bs-btn-hover-color: var(--fx-color-info-fg);
    --bs-btn-hover-border-color: var(--fx-color-info-fg);
    --bs-btn-active-color: var(--fx-color-info-fg);
    --bs-btn-active-border-color: var(--fx-color-info-fg);
}

/* The status softs are tertiaries too - they need the border width and the page
   fill that the `[class*="btn-outline-"]` selector gave their outline twins.
   `.btn-soft-primary` is NOT here: it is a neutral secondary, in section 4. */
.btn.btn-soft-danger,
.btn.btn-soft-success,
.btn.btn-soft-warning,
.btn.btn-soft-info {
    --bs-btn-border-width: var(--fx-border-width);
    --bs-btn-bg: transparent;
    --bs-btn-hover-bg: var(--fx-color-hover);
    --bs-btn-active-bg: var(--fx-color-active);
}

/* `.btn-outline-dark` resolves to the inked slab on hover rather than to a hue -
   the one tertiary that promotes into a solid. Kept because ABP emits it for
   "select all" style actions where that escalation is the intent. */
.btn.btn-outline-dark {
    --bs-btn-hover-color: var(--fx-color-text-inverse);
    --bs-btn-hover-bg: var(--fx-color-text);
    --bs-btn-hover-border-color: var(--fx-color-text);
    --bs-btn-active-color: var(--fx-color-text-inverse);
    --bs-btn-active-bg: var(--fx-color-text);
    --bs-btn-active-border-color: var(--fx-color-text);
}


/* ── 6) Ghost variants ────────────────────────────────────────────────────────
 *
 * No border, no fill, tertiary ink. This is the default for toolbars, table row
 * actions and any dense cluster, where eight bordered buttons would turn into
 * visual noise and eight filled ones into a mosaic.
 *
 * Hover promotes the ink to primary and lays down `--fx-color-hover`; active
 * deepens to `--fx-color-active`. BOTH ARE THE TINT, AND THE TINT IS THE
 * CLIENT'S MINT RAMP - not a grey, and not a percentage of ink. This note said
 * "NEUTRAL washes at 5 and 8 percent ... the same grey everywhere" and both
 * halves went stale in the fourth pass: 00-tokens 2.8 / 3.8 moved the pair onto
 * the client-supplied mint ramp from 1.2.1 - opaque mint-100 and a mint-200 mix
 * in light, mint-500 at 6 and 10 percent alpha in dark. What the note was
 * actually protecting is unchanged and still true: a hovered toolbar button, a
 * hovered nav item, a hovered table row and a keyboard-highlighted list item are
 * the SAME TINT everywhere in the platform. The hue moved; the discipline did
 * not. A tint is still not a diluted brand, which 3.9 forbids by name.
 *
 * `.fx-btn-ghost` is a synonym for markup written against the fx namespace.
 * `.btn-soft-*` for the status hues is a TERTIARY (section 5), not a ghost -
 * softs came from a tinted-chip era that no longer exists, and the class names
 * are kept only because module markup emits them.
 * ------------------------------------------------------------------------- */
.btn.btn-ghost,
.btn.fx-btn-ghost,
.btn[class*="btn-ghost-"],
.btn[class*="fx-btn-ghost-"] {
    --bs-btn-border-width: 0;
    --bs-btn-color: var(--fx-color-text-subtle);
    --bs-btn-bg: transparent;
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-color-text);
    --bs-btn-hover-bg: var(--fx-color-hover);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-color-text);
    --bs-btn-active-bg: var(--fx-color-active);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: none;
}

/* THE HUED GHOSTS. Only the INK is coloured; the wash stays neutral in every
   state, for the same reason the status tertiaries keep a neutral plate. The
   hover ink is the SAME `-fg` token as the rest ink rather than a deeper one -
   a ghost that changes hue under the cursor reads as a different control.

   `.btn-ghost-primary` is absent by design and RE-CONFIRMED 2026-08-03 under the
   rewritten 3.9: it answers Q3, so it is not coloured at all. It falls through to
   the neutral ghost above, which is what it should be. See the header ruling. */
.btn.btn-ghost-danger,
.btn.fx-btn-ghost-danger {
    --bs-btn-color: var(--fx-color-danger-fg);
    --bs-btn-hover-color: var(--fx-color-danger-fg);
    --bs-btn-active-color: var(--fx-color-danger-fg);
}

.btn.btn-ghost-success,
.btn.fx-btn-ghost-success {
    --bs-btn-color: var(--fx-color-success-fg);
    --bs-btn-hover-color: var(--fx-color-success-fg);
    --bs-btn-active-color: var(--fx-color-success-fg);
}

.btn.btn-ghost-warning,
.btn.fx-btn-ghost-warning {
    --bs-btn-color: var(--fx-color-warning-fg);
    --bs-btn-hover-color: var(--fx-color-warning-fg);
    --bs-btn-active-color: var(--fx-color-warning-fg);
}

.btn.btn-ghost-info,
.btn.fx-btn-ghost-info {
    --bs-btn-color: var(--fx-color-info-fg);
    --bs-btn-hover-color: var(--fx-color-info-fg);
    --bs-btn-active-color: var(--fx-color-info-fg);
}


/* ── 7) `.btn-link` ───────────────────────────────────────────────────────────
 *
 * A button that reads as a link, and the file's one legitimate use of
 * `--fx-color-link` - accent site 3. The underline stays on at rest so the
 * control is not identified by colour alone (SC 1.4.1), with a generous offset
 * so Arabic descenders are not clipped.
 *
 * It keeps the pill radius but never paints a fill, so the radius is only ever
 * visible as the shape of the focus outline. `min-inline-size` is released
 * because a link has no target box to fill - forcing one would leave a wide
 * click area around a short word.
 * ------------------------------------------------------------------------- */
.btn.btn-link {
    --bs-btn-border-width: 0;
    --bs-btn-color: var(--fx-color-link);
    --bs-btn-bg: transparent;
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-color-link-hover);
    --bs-btn-hover-bg: transparent;
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-color-link-hover);
    --bs-btn-active-bg: transparent;
    --bs-btn-active-border-color: transparent;
    --bs-btn-padding-x: var(--fx-space-1);

    --fx-btn-shadow: none;

    text-decoration: underline;
    text-underline-offset: var(--fx-space-1);
    text-decoration-thickness: var(--fx-border-width);
    min-inline-size: auto;
}

@media (any-hover: hover) {
    .btn.btn-link:hover {
        text-decoration-thickness: var(--fx-border-width-strong);
    }
}


/* ── 8) Icon-only buttons ─────────────────────────────────────────────────────
 *
 * ACCESSIBILITY REQUIREMENT, NOT OPTIONAL: an icon-only button has no
 * accessible name. Every `.btn-icon` in Razor or JS MUST carry `aria-label`
 * (or `aria-labelledby`). A `title` attribute alone is not a substitute for
 * screen-reader users on touch devices. CSS cannot enforce this, so it is
 * called out here and must be checked in review.
 *
 * `inline-size == block-size`, both read from `--fx-btn-h`, so ONE rule sizes
 * every tier: `.btn-icon.btn-xs` is 24 square, `.btn-icon.btn-lg` is 40 square,
 * and a future `.btn-icon.btn-xl` is 44 square with nothing added here. The
 * four per-size blocks the previous pass needed are gone.
 *
 * `.btn-circle` IS LOAD BEARING AGAIN, AND THIS NOTE USED TO SAY THE OPPOSITE.
 * It read "a square on `--fx-radius-control` is already a circle, so
 * `.btn-circle` is a no-op kept only so existing markup keeps parsing
 * meaningfully". That was true ONLY while the control token was a pill: a 32px
 * square at 9999px renders as a 32px disc, so the class changed nothing. The
 * moment `fx-radius-control` becomes a fixed length (see THE PILL, RULED
 * AGAINST in the header) a square on it is a squircle, and `.btn-circle` is the
 * only thing left that produces an actual circle. It was already declared
 * correctly - it sets `fx-radius-circle` explicitly rather than relying on the
 * geometry - so nothing here changes. What changes is that markup wanting a
 * round icon button must now SAY SO, and any `.btn-icon` that was silently
 * getting a disc will render as an 8px square instead. That is the intended
 * outcome: an icon button in a toolbar beside 8px fields should be an 8px
 * square, and a genuine circle is a deliberate shape that asks for itself.
 * ------------------------------------------------------------------------- */
.btn.btn-icon {
    --bs-btn-padding-x: 0;

    inline-size: var(--fx-btn-h);
    min-inline-size: var(--fx-btn-h);
    column-gap: 0;
    /* flex-shrink is disabled so a dense toolbar never squashes an icon button
       below its target size when the row runs out of room; the toolbar wraps. */
    flex: 0 0 auto;
}

.btn.btn-icon.btn-circle,
.btn.btn-icon.fx-btn-circle {
    --bs-btn-border-radius: var(--fx-radius-circle);
}

/* COARSE POINTER: intentionally NOT handled here. The token layer re-tiers
 * control-h-xs..xl to 28/32/40/44/48 (and the padding and icon tokens with
 * them) inside its own `@media (pointer: coarse)` block, so every rule above
 * widens and heightens automatically on a tablet or phone. Writing a second set
 * of pointer overrides here would duplicate that decision in two files and
 * guarantee they drift apart.
 * ------------------------------------------------------------------------- */


/* ── 9) States ────────────────────────────────────────────────────────────────
 *
 * Disabled, toggled (aria-pressed / .active / .btn-check), and loading.
 * ------------------------------------------------------------------------- */

/* --- DISABLED: `opacity: .5` and nothing else, which is the whole Linear
       treatment. The colours come from the variant itself through the four
       `--bs-btn-disabled-*` locals on `.btn`, so a disabled primary is a faded
       primary rather than a grey box that looks like a different component.
       The physics locals are zeroed as well so a future variant that adds one
       is disabled correctly without anyone remembering to come back here. --- */
.btn:disabled,
.btn.disabled,
fieldset:disabled .btn {
    --fx-btn-shadow: none;
    --fx-btn-filter: none;
    --fx-btn-press-scale: 1;

    cursor: not-allowed;
    transform: none;
    /* Keeps the tab-order story honest: a disabled control is not focusable, so
       the pointer cue is all we have. */
    pointer-events: auto;
}

/* --- TOGGLED / PRESSED ---
 *
 * A toggle that is ON is a SELECTED item, and section 3.9 is explicit that a
 * selected thing is `--fx-color-active` - THE TINT, the client's mint ramp - with
 * the ink promoted to primary. It is not the brand colour and it is not a recess:
 * the previous pass expressed it as an inset press shadow, which reads as
 * "currently being clicked" rather than as "currently on".
 *
 * THIS COMMENT USED TO CLOSE ITSELF HERE, ON THE LINE ABOVE, AND THAT WAS A
 * PARSE ERROR RATHER THAN AN UNTIDINESS (fixed 2026-08-03). A stray close
 * delimiter ended the block early and left the seven prose lines below it as
 * top-level CSS. A parser reading `border-color is deliberately NOT written
 * here...` treats it as a qualified-rule prelude and swallows everything up to
 * the next brace pair - which is the toggled-state rule itself. THE ENTIRE
 * TOGGLED / PRESSED SKIN WAS DEAD: `[aria-pressed="true"]`, `.active` and
 * `.btn-check:checked + .btn` painted no fill and no ink at all. The prose is
 * retained verbatim below because it is still the correct reasoning; only the
 * delimiter was wrong.
 *
 * DO NOT WRITE A COMMENT CLOSE DELIMITER INSIDE A COMMENT, not even quoted or
 * inside backticks. CSS has no escape for it and no nesting - the first one wins
 * and the rest of the block becomes markup. That is exactly how this defect was
 * introduced, and it was reproduced once while writing this very note.
 *
 * `border-color` is deliberately NOT written here. A bare `.btn` with no variant
 * class has no `--bs-btn-active-border-color`, and an undefined custom property
 * makes the whole declaration invalid at computed-value time - which resolves
 * `border-color` to its initial `currentColor` and would paint a stroke the
 * moment any consumer restored a width. The variants that DO have a border (the
 * tertiary family) keep their resting border through the toggled state, which is
 * the correct reading anyway: toggling changes the fill, not the outline. */
.btn[aria-pressed="true"],
.btn.active,
.btn-check:checked + .btn,
.btn-check:active + .btn {
    background-color: var(--fx-color-active);
    color: var(--fx-color-text);
}

/* The checkbox/radio is visually hidden, so focus has to be expressed on its
   sibling label. Same single indicator as `.btn:focus-visible`. */
.btn-check:focus-visible + .btn {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: var(--fx-focus-offset);
    position: relative;
    z-index: 3;
}

/* --- LOADING --------------------------------------------------------------
 * `.btn.is-loading` (or `[data-loading]`) keeps the label in normal flow and
 * merely turns it transparent, so the button's measured width never changes.
 * A width jump mid-submit is a real usability failure: it moves the very
 * target the user just aimed at, and in a form footer it re-flows every
 * sibling. Do not "fix" this by replacing the label text.
 *
 * DEVELOPER CONTRACT: the JS that adds `is-loading` MUST also set
 * `aria-busy="true"` on the button, and SHOULD set `disabled` once the request
 * is in flight. `pointer-events: none` stops clicks but tells assistive tech
 * nothing on its own.
 * ------------------------------------------------------------------------- */
.btn.is-loading,
.btn[data-loading],
.btn[aria-busy="true"] {
    --fx-btn-filter: none;
    --fx-btn-press-scale: 1;

    color: transparent;
    pointer-events: none;
    transform: none;
    /* Stops a text-selection flash while the label is invisible. */
    user-select: none;
}

.btn.is-loading::after,
.btn[data-loading]::after,
.btn[aria-busy="true"]::after {
    content: "";
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    inline-size: var(--fx-icon-sm);
    block-size: var(--fx-icon-sm);
    /* Centring offsets; direction-agnostic on both axes here. */
    margin-block-start: calc(var(--fx-icon-sm) / -2);
    margin-inline-start: calc(var(--fx-icon-sm) / -2);
    border: var(--fx-border-width-strong) solid var(--fx-color-border-emphasis);
    /* The ink was captured on `.btn` before `color` went transparent. */
    border-block-start-color: var(--fx-btn-ink-current);
    border-radius: var(--fx-radius-circle);
    animation: fx-btn-spin var(--fx-dur-slower) linear infinite;
}

@keyframes fx-btn-spin {
    to {
        transform: rotate(360deg);
    }
}

/* A non-rotating alternative used under reduced-motion (see section 16). */
@keyframes fx-btn-pulse {
    50% {
        opacity: 0.25;
    }
}


/* ── 10) Button groups and toolbars ───────────────────────────────────────────
 *
 * A GROUP NEEDS END-CAP LOGIC. A segmented control is one object with two
 * rounded ends, not N rounded chips: the FIRST member keeps its full
 * `fx-radius-control` corner on the inline-START side only, the LAST member on
 * the inline-END side only, and every interior edge drops to `fx-radius-xs`.
 *
 * XS IS THE RIGHT INTERIOR VALUE UNDER EITHER SHAPE DECISION, which is worth
 * recording because it is the kind of thing a reader assumes was tuned for the
 * pill and then "fixes". It is 4px, not zero: against a 16px pill cap it is the
 * floor that stops a square interior corner reading as two components welded
 * together, and against an 8px cap it is exactly the nesting step from 00-tokens
 * 4, where an inner corner steps DOWN one rung from its outer one rather than
 * sideways. Both readings land on the same token, so nothing here moves when
 * `fx-radius-control` does.
 *
 * Bootstrap 5.3's default (non-RTL) build does this flattening with PHYSICAL
 * properties: `border-top-right-radius` and friends on `:not(:last-child)`,
 * plus `margin-left: calc(var(--bs-border-width) * -1)`. Under `dir="rtl"` that
 * is exactly backwards - the group ends up rounded on the inside seams and
 * square on the outer edges. The fix is two clean steps: NEUTRALISE Bootstrap
 * physically, once, with the `border-radius` shorthand (which resets all four
 * physical longhands) at a specificity that beats its (0,4,0); then flatten
 * LOGICALLY only, and only for groups that are actually joined.
 *
 * Specificity note: Bootstrap's rules are (0,3,0) because `:not(:first-child)`
 * contributes its argument's weight. Each override here is (0,4,0) or higher,
 * so bundle order cannot break it.
 *
 * OVERFLOW STRATEGY. A `.btn-group` cannot both wrap AND keep its end caps
 * correct: CSS has no way to know which child ended a visual line, so a wrapped
 * group always shows a flattened cap mid-air. The default is therefore to
 * SCROLL on the inline axis - the group never causes horizontal page overflow,
 * and every radius stays exactly right. `.fx-btn-group-wrap` is the opt-in for
 * cases where wrapping is genuinely preferable; it restores a full pill and a
 * real gap on every child, which is the only rendering that stays correct on
 * each line.
 * ------------------------------------------------------------------------- */
.btn-group,
.btn-group-vertical {
    position: relative;
    display: inline-flex;
    vertical-align: middle;
    max-inline-size: 100%;
    /* `overflow-x` is the physical axis, but in a horizontal writing mode it IS
       the inline axis in both LTR and RTL. Scroll position simply starts at the
       inline start in either direction. */
    overflow-x: auto;
    /* NO `scrollbar-width` HERE. Since Chrome 121 that property makes Chrome
       discard every `::-webkit-scrollbar-*` rule for the element and substitute
       its own built in scrollbar, so declaring it was the one thing stopping
       this group from getting the tokenised skin 01-base section 11 applies
       globally. The base layer owns scrollbars entirely; components never
       restate them. */
    scroll-snap-type: inline proximity;
    /* The seam. One hairline wide, but nothing is drawn - this is a sliver of
       the group's own backdrop showing through between two fills. */
    column-gap: var(--fx-border-width);
    /* Setting overflow on one axis forces the other axis to compute to `auto`
       as well, which would clip the focus outline on the block axis. The group
       carries exactly enough internal block padding for the outline and cancels
       it again with a negative block margin, so surrounding layout is
       unaffected. */
    padding-block: calc(var(--fx-focus-offset) + var(--fx-focus-width));
    margin-block: calc(-1 * (var(--fx-focus-offset) + var(--fx-focus-width)));
}

.btn-group > .btn {
    scroll-snap-align: start;
    /* Members keep their intrinsic width and scroll, rather than compressing
       until the labels are unreadable. */
    flex: 0 0 auto;
}

/* Opt-in wrapping group: a real gap and full pills, so each visual line is
   self-consistent. No per-child reset is needed - the neutralising shorthand
   below gives every member its pill back, and the flattening rules exclude this
   variant by `:not()` rather than by an override. */
.btn-group.fx-btn-group-wrap {
    display: flex;
    flex-wrap: wrap;
    overflow-x: visible;
    gap: var(--fx-space-1);
}

.btn-group > .btn.btn.btn,
.btn-group > .btn-group > .btn.btn.btn {
    margin-inline-start: 0;
    margin-left: 0;
    border-radius: var(--fx-radius-control);
}

/* END CAPS, LOGICALLY. `border-start-start` / `border-end-start` are the two
   corners on the inline-START side; the `-end` pair is the inline-END side.
   Both resolve correctly in Arabic and English from one declaration. */
.btn-group:not(.fx-btn-group-wrap):not(.fx-btn-group-divided) > .btn.btn.btn:not(:first-child),
.btn-group:not(.fx-btn-group-wrap):not(.fx-btn-group-divided) > .btn-group:not(:first-child) > .btn.btn.btn {
    border-start-start-radius: var(--fx-radius-xs);
    border-end-start-radius: var(--fx-radius-xs);
}

.btn-group:not(.fx-btn-group-wrap):not(.fx-btn-group-divided) > .btn.btn.btn:not(:last-child):not(.dropdown-toggle),
.btn-group:not(.fx-btn-group-wrap):not(.fx-btn-group-divided) > .btn-group:not(:last-child) > .btn.btn.btn {
    border-start-end-radius: var(--fx-radius-xs);
    border-end-end-radius: var(--fx-radius-xs);
}

/* A split button's caret half is the LAST member, so it keeps the end cap and
   flattens only its start side - excluded from the rule above by
   `:not(.dropdown-toggle)`, and given the start flattening by the first rule
   because it is never `:first-child`. Nothing extra is needed. */

/* An engaged member must float above its neighbours or its outline and its
   border highlight get painted under the next button's fill. This sets stacking
   order only - no skin - which is why the `:hover` arm is split into the
   any-hover block below rather than left to fire on a touch tap. */
.btn-group > .btn:focus-visible,
.btn-group > .btn:active,
.btn-group > .btn.active {
    z-index: 3;
}

@media (any-hover: hover) {
    .btn-group > .btn:hover {
        z-index: 3;
    }
}

/* `.fx-btn-group-divided`: the opt-in for a group whose members need MORE
   separation than the one-pixel seam - typically a ghost group, where every
   member is transparent at rest and the default sliver has nothing to show
   against. Members keep their full pill because the `:not()` above excludes
   this variant from the end-cap flattening. */
.btn-group.fx-btn-group-divided {
    column-gap: var(--fx-space-2);
}

/* Vertical groups flatten on the block axis, which is physical and identical in
   both directions, so no logical/physical collision arises here. */
.btn-group-vertical {
    row-gap: var(--fx-border-width);
}

.btn-group-vertical > .btn:not(:first-child) {
    margin-block-start: 0;
    border-start-start-radius: var(--fx-radius-xs);
    border-start-end-radius: var(--fx-radius-xs);
}

.btn-group-vertical > .btn:not(:last-child) {
    border-end-start-radius: var(--fx-radius-xs);
    border-end-end-radius: var(--fx-radius-xs);
}

.btn-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--fx-space-2);
    justify-content: flex-start;
    /* A toolbar wraps; it never widens its parent. Combined with the shrinkable
       `.btn` above, this is what keeps a dense Arabic toolbar from producing a
       horizontal page scrollbar at 1366x768 or on a phone. */
    max-inline-size: 100%;
    min-inline-size: 0;
}

/* Toolbars default their LOOSE buttons - the ones with no variant class at all -
   to the ghost treatment, so a dense action bar stays calm. Anything that opts
   into an explicit variant keeps it: this is scoped by
   `:not([class*="btn-"])`, so it can only ever match a bare `.btn`. */
.btn-toolbar > .btn:not([class*="btn-"]):not(.btn-link),
.fx-toolbar > .btn:not([class*="btn-"]):not(.btn-link) {
    --bs-btn-color: var(--fx-color-text-subtle);
    --bs-btn-bg: transparent;
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--fx-color-text);
    --bs-btn-hover-bg: var(--fx-color-hover);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--fx-color-text);
    --bs-btn-active-bg: var(--fx-color-active);
    --bs-btn-active-border-color: transparent;

    --fx-btn-shadow: none;
}


/* ── 11) Dropdown toggles and split buttons ───────────────────────────────────
 *
 * Bootstrap places the caret with `margin-left: .255em`, which lands on the
 * wrong side in RTL. Re-expressed logically. The caret rotates on `.show`
 * through the standard ease - the same 160ms ease-out-quad as everything else,
 * because there is one motion vocabulary in this file now and no springs in it.
 *
 * A caret is METADATA, not a label, so it takes the quaternary ink tier. It is
 * the only place in this file that uses `--fx-color-text-faint`, and it is the
 * exact case the tier exists for: a chevron that must be present without being
 * read. `currentColor` would have made it as loud as the label.
 * ------------------------------------------------------------------------- */
.btn.dropdown-toggle::after {
    margin-inline-start: var(--fx-space-1);
    margin-inline-end: 0;
    margin-left: 0;
    margin-right: 0;
    color: var(--fx-color-text-faint);
    transition: transform var(--fx-dur-base) var(--fx-ease-standard);
}

.btn.dropdown-toggle.show::after,
.show > .btn.dropdown-toggle::after {
    transform: rotate(180deg);
}

/* Split button: the caret half is a narrow square with no label, so its padding
   comes off the inline axis only. Its end cap follows the group (section 10). */
.btn.dropdown-toggle-split {
    --bs-btn-padding-x: var(--fx-space-2);

    min-inline-size: auto;
}

.btn.dropdown-toggle-split::after {
    margin-inline-start: 0;
    margin-inline-end: 0;
}

/* Bootstrap's `.dropdown-toggle-no-caret` companion, kept working. */
.btn.dropdown-toggle-no-caret::after {
    display: none;
}


/* ── 12) Floating action button (`.fx-fab`) ───────────────────────────────────
 *
 * A primary action rendered as a circle, so it is accent site 1 wearing a
 * different shape - brand fill, `filter` state changes, nothing else.
 *
 * Anchored to the inline-end / block-end corner, so it sits bottom-left in
 * Arabic and bottom-right in English without a second rule.
 *
 * Safe-area: `env(safe-area-inset-*)` is inherently physical, and which physical
 * side "inline-end" maps to depends on the document direction. Taking the max of
 * the left and right insets is correct in both directions and costs at most a
 * few pixels of extra breathing room.
 *
 * TOAST COLLISION - RESOLVED: slice A5 moved the toast stack, so the
 * FAB KEEPS this corner and applies no toast offset of its own. A5 anchors its
 * stack above the FAB using `--fx-fab-clearance` (owned by 00-tokens.css and
 * READ-ONLY from here). This file's obligation is the reciprocal one: the FAB
 * must fit entirely inside that clearance envelope.
 *
 * THE ENVELOPE, MADE SELF-ENFORCING:
 *     occupied = diameter + inset-block-end + shadow allowance <= fab-clearance
 * The diameter is not an independent number; it is DERIVED by subtracting the
 * offset and the shadow allowance from the clearance token. If A1 ever retunes
 * `--fx-fab-clearance`, the FAB resizes to match and the contract cannot
 * silently break.
 *
 * With fab-clearance = 4.5rem (72px):
 *     offset           = min(space-4, 1rem)          = 16px
 *     shadow allowance =                               8px
 *     diameter         = clamp(44px, 6dvmin, 48px)   = 44px..48px
 *     occupied         = 48 + 16 + 8                 = 72px = 4.5rem  (exact)
 * The rem literals here are deliberate and are the only ones in the file: an
 * envelope guarantee has to be arithmetically checkable, not dependent on token
 * values this slice cannot see. `clamp()` returns its minimum if the computed
 * maximum ever falls below it, so the tap target survives any retune.
 *
 * The FAB is the ONE control in the file that is genuinely a floating layer
 * rather than a control at rest on a surface, so it is also the one that takes
 * `--fx-shadow-float-*` instead of `--fx-shadow-stack`. It no longer grows on
 * hover: a scale on a fixed element that already sits at the edge of the
 * clearance envelope is the one motion that can break the envelope, and the
 * filter carries the state perfectly well without it.
 * ------------------------------------------------------------------------- */
.fx-fab {
    /* Bounded so the sum above is provable regardless of the space scale. */
    --fx-fab-offset: min(var(--fx-space-4), 1rem);
    --fx-fab-shadow-allowance: 0.5rem;
    --fx-fab-size: clamp(
        var(--fx-tap-min),
        6dvmin,
        calc(var(--fx-fab-clearance, 4.5rem) - var(--fx-fab-offset) - var(--fx-fab-shadow-allowance))
    );

    --fx-btn-filter: none;
    --fx-btn-press-scale: var(--fx-press-scale);

    position: fixed;
    inset-block-end: calc(var(--fx-fab-offset) + env(safe-area-inset-bottom, 0px));
    inset-inline-end: calc(
        var(--fx-fab-offset)
        + max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))
    );
    z-index: var(--fx-z-fixed, 1030);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--fx-fab-size);
    block-size: var(--fx-fab-size);
    min-inline-size: var(--fx-tap-min);
    min-block-size: var(--fx-tap-min);
    padding: 0;
    border: 0;
    border-radius: var(--fx-radius-circle);
    background-color: var(--fx-color-primary);
    background-image: none;
    color: var(--fx-color-text-on-primary);
    box-shadow: var(--fx-shadow-float-md);
    filter: var(--fx-btn-filter);
    cursor: pointer;
    overflow: hidden;
    /* No `will-change` — see the note on `.btn` in section 2. The FAB does press
       for real, so this one was not a false promise; it fails the other half of
       the test. A press is a 60ms transition on demand, not a transform running
       for the element's mounted life, and the FAB is mounted for the whole
       session. It is also already `position: fixed` with a `z-index`, so it has
       a stacking context regardless and the hint was buying nothing it did not
       already have. */
    transition:
        box-shadow var(--fx-dur-base) var(--fx-ease-standard),
        filter var(--fx-dur-base) var(--fx-ease-standard),
        opacity var(--fx-dur-base) var(--fx-ease-standard),
        transform var(--fx-dur-base) var(--fx-ease-standard);
}

/* Like `.btn-icon`, a FAB has no accessible name of its own: `aria-label` on
   the element is mandatory. */
.fx-fab > svg,
.fx-fab > .fx-btn-icon {
    inline-size: var(--fx-icon-lg);
    block-size: var(--fx-icon-lg);
    fill: currentColor;
}

.fx-fab > img {
    inline-size: var(--fx-icon-lg);
    block-size: var(--fx-icon-lg);
}

.fx-fab > i,
.fx-fab > .fx-icon {
    font-size: var(--fx-icon-lg);
    line-height: 1;
}

@media (any-hover: hover) {
    .fx-fab:hover {
        --fx-btn-filter: var(--fx-btn-primary-hover-filter);

        box-shadow: var(--fx-shadow-float-lg);
    }
}

.fx-fab:not(:disabled):active {
    --fx-btn-filter: var(--fx-btn-primary-active-filter);

    transform: scale(var(--fx-btn-press-scale));
    transition-duration: var(--fx-dur-instant);
}

.fx-fab:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: var(--fx-focus-offset);
}

.fx-fab:disabled,
.fx-fab.disabled {
    --fx-btn-filter: none;
    --fx-btn-press-scale: 1;

    opacity: 0.5;
    box-shadow: none;
    cursor: not-allowed;
    transform: none;
}

/* Extended FAB: a capsule with a visible label, so no aria-label is required.
 *
 * IT READS `fx-radius-pill`, NOT `fx-radius-control`, AND THAT IS A CORRECTION
 * (2026-08-03). It used to read the control token, which meant a decision about
 * the shape of FORM BUTTONS silently reshaped a floating capsule. The two are
 * not the same decision, and 00-tokens says so in both directions: the control
 * token is the ONE REVERSIBLE choice about buttons and nav pills, while
 * `fx-radius-pill` is the token for things whose shape is a pill BY NATURE -
 * avatars, toggles, switch tracks, scrollbar thumbs.
 *
 * This is the second kind. The extended FAB is the CIRCULAR `.fx-fab` above it
 * with a label let into it: same derived `--fx-fab-size` height, same fixed
 * position, same float shadow, same clearance envelope. Its sibling is drawn
 * with `fx-radius-circle` and cannot move, so routing this one through a
 * reversible token guaranteed the pair would eventually disagree - a disc and
 * an 8px rectangle presented as two forms of one control. A capsule is a
 * stretched circle and is the only shape that keeps the family readable.
 *
 * It is also the one place in this file where a stadium is unambiguously right:
 * a FAB floats OVER content at the viewport corner with no field, no row and no
 * neighbour to align to, so none of the evidence in THE PILL, RULED AGAINST
 * reaches it. Nothing sits beside it to disagree with. */
.fx-fab.fx-fab-extended {
    inline-size: auto;
    /* Never wider than the viewport gutter allows, so a long Arabic label
       cannot push the FAB off screen. */
    max-inline-size: calc(100% - var(--fx-space-6));
    /* Same derived height as the circular form, so the extended variant obeys
       the identical clearance envelope. */
    block-size: var(--fx-fab-size);
    border-radius: var(--fx-radius-pill);
    padding-inline: var(--fx-control-pad-x-lg);
    column-gap: var(--fx-control-gap-lg);
    font-family: var(--fx-font-body);
    font-weight: var(--fx-weight-medium);
    font-size: var(--fx-text-base);
    letter-spacing: var(--fx-tracking-ui);
    white-space: nowrap;
    overflow: hidden;
}


/* ── 13) `.fx-btn-row` - the standard action row ──────────────────────────────
 *
 * Form footers, dialog footers, wizard steps. `flex-end` in a flex container
 * follows the writing direction, so this aligns to the physical left in Arabic
 * and the physical right in English with no direction-specific rule.
 *
 * STAGGERED ENTRANCE - DECISION: OPT-IN, NOT DEFAULT.
 * `.fx-btn-row` is the footer of essentially every form and modal in the
 * platform, and `.modal-footer` below inherits its behaviour. A default
 * entrance animation would replay every time Bootstrap shows a modal, every
 * time a wizard advances a step, and every time a partial re-renders - three
 * moments where the user has already decided what to click and is waiting on the
 * app, not admiring it. Motion there reads as lag, not polish.
 *
 * So the stagger is available as `.fx-btn-row.fx-stagger` and is meant for
 * first-paint hero rows and empty-state calls to action. It consumes 01-base's
 * shared `fx-rise-in` keyframe and the token layer's `--fx-stagger-step`; if the
 * keyframe is absent the rule is simply inert.
 *
 * `.btn-group` gets NO stagger at all, by the same reasoning taken further: a
 * segmented control is one object, and animating its segments in one at a time
 * dismantles the object it is trying to present.
 * ------------------------------------------------------------------------- */
.fx-btn-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--fx-space-3);
    margin-block-start: var(--fx-space-5);
    /* An action row must never be the thing that makes a page scroll
       horizontally, at any resolution. */
    max-inline-size: 100%;
    min-inline-size: 0;
}

/* Start-aligned and space-between variants for wizard-style footers. */
.fx-btn-row.fx-btn-row-start {
    justify-content: flex-start;
}

.fx-btn-row.fx-btn-row-between {
    justify-content: space-between;
}

/* Anything marked as the spacer pushes the remaining actions to the end. */
.fx-btn-row > .fx-btn-row-spacer {
    margin-inline-end: auto;
}

/* Opt-in staggered entrance. `both` fill mode holds the from-state during the
   delay, so a button does not flash at full opacity before its turn. Six steps
   is deliberate: an action row with a seventh button is a design problem, and
   the seventh onwards simply enter with the sixth. */
.fx-btn-row.fx-stagger > .btn,
.fx-btn-row.fx-stagger > .btn-group,
.fx-btn-row.fx-stagger > .frihat-btn {
    animation: fx-rise-in var(--fx-dur-enter) var(--fx-ease-standard) both;
}

.fx-btn-row.fx-stagger > :nth-child(1) {
    animation-delay: calc(var(--fx-stagger-step) * 0);
}

.fx-btn-row.fx-stagger > :nth-child(2) {
    animation-delay: calc(var(--fx-stagger-step) * 1);
}

.fx-btn-row.fx-stagger > :nth-child(3) {
    animation-delay: calc(var(--fx-stagger-step) * 2);
}

.fx-btn-row.fx-stagger > :nth-child(4) {
    animation-delay: calc(var(--fx-stagger-step) * 3);
}

.fx-btn-row.fx-stagger > :nth-child(5) {
    animation-delay: calc(var(--fx-stagger-step) * 4);
}

.fx-btn-row.fx-stagger > :nth-child(n + 6) {
    animation-delay: calc(var(--fx-stagger-step) * 5);
}

/* Below 768px: the standard mobile action-sheet pattern. The row stacks, and
   `column-reverse` puts the LAST button in source order on top. Razor and JS
   footers in this codebase are all written as [Cancel, Save] / [Cancel,
   Delete], so the primary or destructive confirmation lands on top, full width,
   with the secondary beneath it, exactly as intended.
   If a future footer is authored primary-first, mark it `.fx-btn-row-inorder`
   below rather than reordering the DOM, because DOM order is what the keyboard
   and screen reader follow. */
@media (max-width: 767.98px) {
    .fx-btn-row {
        flex-direction: column-reverse;
        align-items: stretch;
        gap: var(--fx-space-2);
    }

    .fx-btn-row.fx-btn-row-inorder {
        flex-direction: column;
    }

    .fx-btn-row > .btn,
    .fx-btn-row > .btn-group,
    .fx-btn-row > .frihat-btn {
        inline-size: 100%;
        max-inline-size: 100%;
    }

    .fx-btn-row > .fx-btn-row-spacer {
        margin-inline-end: 0;
    }
}


/* ── 14) ABP / LeptonX framework button surfaces ──────────────────────────────
 *
 * The framework emits buttons we do not author: modal footers, ABP entity
 * action dropdowns, and grid toolbars. These rules make those inherit the
 * system sensibly without touching any .cshtml or .js.
 * ------------------------------------------------------------------------- */

/* Modal footers behave exactly like an fx-btn-row. Bootstrap 5.3 exposes the
   gap as a modal local, so we set the token rather than fighting its margins. */
.modal-footer {
    --bs-modal-footer-gap: var(--fx-space-3);

    gap: var(--fx-space-3);
}

.modal-footer > .btn {
    margin: 0;
    max-inline-size: 100%;
}

/* Same mobile action-sheet treatment as `.fx-btn-row`, at the same breakpoint,
   so an ABP modal and a hand-written dialog behave identically. */
@media (max-width: 767.98px) {
    .modal-footer {
        flex-direction: column-reverse;
        flex-wrap: nowrap;
        align-items: stretch;
    }

    .modal-footer > .btn {
        inline-size: 100%;
    }
}

/* Buttons rendered INSIDE a dropdown menu are MENU ROWS, not standalone
   actions: full width, start-aligned text, ghost colouring, and
   `--fx-radius-item` rather than the pill, because that is the token for a
   menu row and a stack of pills does not tile. No press scale either - a menu
   item that shrinks under the finger turns a list into a fairground. */
.dropdown-menu .btn {
    --bs-btn-border-width: 0;
    --bs-btn-bg: transparent;
    --bs-btn-border-color: transparent;
    --bs-btn-color: var(--fx-color-text);
    --bs-btn-hover-bg: var(--fx-color-hover);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-hover-color: var(--fx-color-text);
    --bs-btn-active-bg: var(--fx-color-active);
    --bs-btn-active-border-color: transparent;
    --bs-btn-active-color: var(--fx-color-text);
    --bs-btn-border-radius: var(--fx-radius-item);

    --fx-btn-shadow: none;
    --fx-btn-filter: none;
    --fx-btn-press-scale: 1;

    inline-size: 100%;
    justify-content: flex-start;
    text-align: start;
}

/* --- `.btn-close` SKIN (colour only) --------------------------------------
 * Slice A5 owns the GEOMETRY of `.btn-close` inside `.modal-header`,
 * `.offcanvas-header`, `.alert-dismissible` and `.toast` (margin-inline-start,
 * min sizes). Nothing below sets a size, a margin or a position: this is
 * strictly the skin, per the handover. The radius follows the rest of the file
 * to `--fx-radius-item` - a dismiss control is a row-level affordance, not a
 * button in the pill family.
 *
 * The problem being fixed: Bootstrap paints `.btn-close` with a data-URI SVG
 * whose stroke is hardcoded near-black. On a dark modal surface that glyph is
 * effectively invisible. The fix must not ship a second SVG, so it goes through
 * `filter`, which is Bootstrap's own supported mechanism for this
 * (`--bs-btn-close-white-filter`). Everything else is token-driven.
 *
 * Contrast: at rest opacity 0.75 the near-black glyph measures about 5.9:1 on
 * the light surface, comfortably past the 3:1 SC 1.4.11 asks of a non-text
 * control; hover takes it to full opacity.
 * ------------------------------------------------------------------------- */
.btn-close {
    --bs-btn-close-color: var(--fx-color-text);
    --bs-btn-close-opacity: 0.75;
    --bs-btn-close-hover-opacity: 1;
    --bs-btn-close-focus-opacity: 1;
    --bs-btn-close-focus-shadow: none;
    --bs-btn-close-disabled-opacity: 0.35;

    border-radius: var(--fx-radius-item);
    cursor: pointer;
    transition:
        background-color var(--fx-dur-base) var(--fx-ease-standard),
        opacity var(--fx-dur-base) var(--fx-ease-standard);
}

/* A neutral hover plate gives the control a visible hit area, which a bare
   glyph otherwise lacks entirely. */
@media (any-hover: hover) {
    .btn-close:hover,
    .alert-dismissible .btn-close:hover,
    .toast .btn-close:hover {
        background-color: var(--fx-color-hover);
        opacity: 1;
    }
}

.btn-close:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: var(--fx-focus-offset);
    opacity: 1;
}

/* EXCEPTION 1 OF 2 to the OFFSET, not to the outline. A toast clips ITS OWN
   overflow - section 3 of 08-feedback sets `overflow: hidden` on `.toast` - and
   the close button sits hard against the clipped inline end, so a ring drawn at
   a positive offset loses its trailing edge.

   THIS USED TO REACH FOR `--fx-shadow-focus`, AND THAT WAS WRONG. The token is
   reserved for an element that clips its own overflow and therefore cannot
   render an outline outside its own border box. Here the clipper is the
   ANCESTOR, and a box-shadow is cropped by an ancestor's overflow in exactly the
   way an outline is - 08-feedback states this outright on `.fx-chip-remove`. The
   token bought nothing and the ring stayed cut.

   The working precedent in this codebase is `.fx-chip-remove`: a REAL outline at
   a NEGATIVE offset, drawn just inside the control, where no ancestor's overflow
   can reach it. Same width, same ring colour, same 3:1 step, and it survives
   forced-colors because it is an outline rather than a shadow. */
.toast .btn-close:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: calc(var(--fx-focus-offset) * -1);
}

.btn-close:disabled,
.btn-close.disabled {
    background-color: transparent;
    cursor: not-allowed;
}

/* Dark surfaces: invert the hardcoded dark glyph. Bootstrap 5.3 ships this
   filter for `.btn-close-white` and applies it under `[data-bs-theme=dark]`,
   but LeptonXLite repaints modal, offcanvas and toast chrome with its own rules
   and the attribute lives on `html` rather than on the component, so the
   inversion is restated here at descendant specificity that cannot lose. The
   literal filter is Bootstrap's own value, used only as a fallback if the
   variable is absent. */
html[data-bs-theme="dark"] .btn-close,
html[data-bs-theme="dark"] .modal-header .btn-close,
html[data-bs-theme="dark"] .offcanvas-header .btn-close,
html[data-bs-theme="dark"] .alert-dismissible .btn-close,
html[data-bs-theme="dark"] .toast .btn-close {
    filter: var(--bs-btn-close-white-filter, invert(1) grayscale(100%) brightness(200%));
}

/* ABP entity action containers and the LeptonX page toolbar: keep the cluster
   tight and consistently spaced. */
.abp-entity-action-container,
.abp-toolbar,
.lpx-content-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--fx-space-2);
    max-inline-size: 100%;
    min-inline-size: 0;
}

/* ABP's "actions" split button inside a Tabulator or table cell keeps its
   colour feedback but loses the press scale and the elevation. A grid row is a
   dense field of small targets, the row clips on overflow, and a shadow under a
   button inside a cell reads as a rendering fault rather than as depth.

   `.btn.btn` rather than `.btn` in the compound is load-bearing: `td .btn` is
   only (0,1,1), which LOSES to a variant block's (0,2,0) for `--fx-btn-shadow`,
   so the previous pass's claim that this rule won was arithmetically wrong and
   in-cell primaries kept their elevation. (0,2,1) and (0,3,0) beat every variant
   in the file. */
.abp-entity-action-container .btn.btn,
td .btn.btn,
.tabulator-cell .btn.btn {
    --fx-btn-shadow: none;
    --fx-btn-press-scale: 1;
}


/* ── 15) Legacy Frihat button classes - preserved as token-based aliases ──────
 *
 * These classes are emitted by hand-written DOM in the module scripts and by
 * three Razor pages. They must keep working verbatim:
 *
 *   .frihat-btn         modules/FLP.CaseManagement/.../Cases/Index.cshtml + Index.js
 *                       modules/FLP.Courts/.../Courts/Index.cshtml + Index.js
 *                       modules/FLP.Courts/.../CourtBranches/Index.cshtml + Index.js
 *   .frihat-row-action  the edit / delete glyph buttons in all three Index.js
 *                       grid formatters
 *
 * They were ALSO defined in `wwwroot/css/frihat-tabulator.css`, where
 * `.frihat-btn` was scoped as `.frihat-grid-toolbar .frihat-btn` at (0,2,0) -
 * which meant the dialog footers in those same scripts, which render
 * `.frihat-btn` OUTSIDE the toolbar, received no styling at all. That file has
 * since been retired into the slice set and this is now the only definition,
 * so the unscoped rules below are what those footers finally get.
 *
 * SPECIFICITY: the repeated class selector `.frihat-btn.frihat-btn.frihat-btn`
 * is (0,3,0). It is kept even though the file it was defending against is gone,
 * because 09-tabulator.css still themes the grid these buttons live in and a
 * (0,3,0) anchor makes this file's ownership of the BUTTON classes independent
 * of bundle order - WITHOUT resorting to `!important`, which this file reserves
 * for the utilities layer.
 *
 * MODIFIER COVERAGE. The scripts emit five class modifiers and three
 * `[data-role]` values. Every one has a skin:
 *
 *   primary      .frihat-save   .frihat-confirm   [data-role="save"]
 *   secondary    .frihat-cancel                   [data-role="cancel"]
 *   destructive  .frihat-delete .frihat-delete-confirm   [data-role="confirm"]
 *
 * `.frihat-delete` means two different things depending on its base class, and
 * both are correct: on `.frihat-btn` it is the modal's FINAL Delete
 * (FLP.Courts/Pages/Courts/Index.js line 213, emitted alongside
 * `data-role="confirm"`) and is therefore a filled red; on `.frihat-row-action`
 * it is the grid TRIGGER (same file, line 25) and is therefore a ghost with red
 * ink. Read the emitters before "fixing" this.
 *
 * OWNERSHIP: agent A6 owns the Tabulator grid theme in 09-tabulator.css. These
 * BUTTON classes are owned HERE. If a `.frihat-row-action` or `.frihat-btn`
 * block ever reappears in the grid slice, it is a duplicate and belongs in this
 * file, not there.
 * ------------------------------------------------------------------------- */

/* --- `.frihat-btn`: the primary pill, in its non-Bootstrap form. Same brand
       fill, same pill, same `--fx-shadow-stack` edge, same brightness filters,
       same press scale as `.btn.btn-primary`, because a dialog footer built by
       hand in JS should be indistinguishable from one built with Bootstrap
       classes. --- */
.frihat-btn.frihat-btn.frihat-btn {
    --fx-btn-h: max(var(--fx-control-h-md), var(--fx-btn-target-min));
    --fx-btn-filter: none;
    --fx-btn-press-scale: var(--fx-press-scale);
    --fx-btn-ink-current: var(--fx-color-text-on-primary);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    column-gap: var(--fx-control-gap-md);
    min-block-size: var(--fx-btn-h);
    min-inline-size: var(--fx-btn-h);
    max-inline-size: 100%;
    padding-block: 0;
    padding-inline: var(--fx-control-pad-x-md);
    border: 0;
    border-radius: var(--fx-radius-control);
    font-family: var(--fx-font-body);
    font-size: var(--fx-text-sm);
    font-weight: var(--fx-weight-medium);
    line-height: var(--fx-btn-h);
    letter-spacing: var(--fx-tracking-ui);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: pointer;
    position: relative;
    color: var(--fx-color-text-on-primary);
    background-color: var(--fx-color-primary);
    background-image: none;
    box-shadow: var(--fx-shadow-stack);
    filter: var(--fx-btn-filter);
    /* No `will-change` — see the note on `.btn` in section 2. This class exists
       so a dialog footer built by hand in JS is indistinguishable from one built
       with Bootstrap classes, and that has to hold for what the two DO NOT do as
       well as for what they do. */
    transition:
        background-color var(--fx-dur-base) var(--fx-ease-standard),
        box-shadow var(--fx-dur-base) var(--fx-ease-standard),
        color var(--fx-dur-base) var(--fx-ease-standard),
        filter var(--fx-dur-base) var(--fx-ease-standard),
        opacity var(--fx-dur-base) var(--fx-ease-standard),
        transform var(--fx-dur-base) var(--fx-ease-standard);
}

@media (any-hover: hover) {
    .frihat-btn.frihat-btn.frihat-btn:hover {
        --fx-btn-filter: var(--fx-btn-primary-hover-filter);
    }
}

.frihat-btn.frihat-btn.frihat-btn:not(:disabled):active {
    --fx-btn-filter: var(--fx-btn-primary-active-filter);

    transform: scale(var(--fx-btn-press-scale));
    transition-duration: var(--fx-dur-instant);
}

/* Same single indicator as `.btn`, so a hand-built dialog footer and a Bootstrap
   one are indistinguishable on the keyboard as well as by eye. */
.frihat-btn.frihat-btn.frihat-btn:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: var(--fx-focus-offset);
}

.frihat-btn.frihat-btn.frihat-btn:disabled,
.frihat-btn.frihat-btn.frihat-btn.disabled {
    --fx-btn-filter: none;
    --fx-btn-press-scale: 1;

    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
}

/* --- PRIMARY affirmatives. Declared explicitly rather than left to inherit the
       base, so the Save button in the two FLP.Courts dialogs is skinned by a
       rule that names it and a future change to the base cannot silently demote
       it. --- */
.frihat-btn.frihat-save.frihat-save,
.frihat-btn.frihat-confirm.frihat-confirm,
.frihat-btn.frihat-btn[data-role="save"] {
    color: var(--fx-color-text-on-primary);
    background-color: var(--fx-color-primary);
    box-shadow: var(--fx-shadow-stack);
}

/* --- SECONDARY cancel: the bevelled neutral, exactly as `.btn-secondary` reads
       beside `.btn-primary`. The pairing IS the hierarchy - one filled brand
       object, one modelled neutral object - which is why this needs no border,
       no tint and no weight change to be unmistakably the quieter of the two. --- */
.frihat-btn.frihat-cancel.frihat-cancel,
.frihat-btn.frihat-btn[data-role="cancel"] {
    color: var(--fx-color-text);
    background-color: var(--fx-color-level-2);
    box-shadow: var(--fx-btn-bevel);
}

@media (any-hover: hover) {
    .frihat-btn.frihat-cancel.frihat-cancel:hover,
    .frihat-btn.frihat-btn[data-role="cancel"]:hover {
        --fx-btn-filter: none;

        background-color: var(--fx-color-level-3);
        box-shadow: var(--fx-shadow-float-md);
    }
}

/* --- DESTRUCTIVE dialog confirmations - the FINAL step only. A filled red
       surface standing beside a bevelled neutral Cancel: the fill itself is the
       non-colour signal, because it is the only filled object in the footer.
       Ink is the near-white one at 5.93:1 on the red.
 *
 * `.frihat-delete` is in this list because on a `.frihat-btn` base it IS the
 * confirmation (see the section note); the grid TRIGGER of the same name is
 * skinned as a ghost further down, and the two never collide because the two
 * rules are anchored on different base classes. --- */
.frihat-btn.frihat-delete.frihat-delete,
.frihat-btn.frihat-delete-confirm.frihat-delete-confirm,
.frihat-btn.frihat-btn[data-role="confirm"] {
    color: var(--fx-btn-ink-light);
    background-color: var(--fx-color-danger);
    box-shadow: var(--fx-shadow-stack);
}

@media (any-hover: hover) {
    .frihat-btn.frihat-delete.frihat-delete:hover,
    .frihat-btn.frihat-delete-confirm.frihat-delete-confirm:hover,
    .frihat-btn.frihat-btn[data-role="confirm"]:hover {
        --fx-btn-filter: var(--fx-btn-primary-hover-filter);
    }
}

/* --- `.frihat-row-action`: the ghost icon button used in grid row formatters.
       It sits inside a Tabulator cell, so it follows the same rule as every
       other in-cell control: colour feedback and the press scale only, no
       elevation. `--fx-radius-item` rather than the pill for the same reason
       `.dropdown-menu .btn` uses it - this is a row control, and a row of
       circles does not tile.
 *
 * The markup is a bare glyph, so `aria-label` is mandatory; FLP.Courts already
 * sets it, FLP.CaseManagement currently sets only `title` and should be updated
 * by whoever next touches that file. --- */
.frihat-row-action.frihat-row-action {
    --fx-btn-press-scale: var(--fx-press-scale);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: max(var(--fx-control-h-sm), var(--fx-btn-target-min));
    /* 28px square on the current control scale, clamped so it can never fall
       under the 24px SC 2.5.8 floor. */
    block-size: max(var(--fx-control-h-sm), var(--fx-btn-target-min));
    flex: 0 0 auto;
    padding: 0;
    /* No transparent border. `prefers-contrast: more` restores a real one as a
       full shorthand in section 16, so nothing depends on a width being here. */
    border: 0;
    border-radius: var(--fx-radius-item);
    background-color: transparent;
    /* TERTIARY ink - a glyph is metadata beside the row's text, and the token
       layer's third tier is exactly that. This was `text-muted`, which is the
       BODY tier after the retune and made a row of icons as loud as the data. */
    color: var(--fx-color-text-subtle);
    font-size: var(--fx-icon-md);
    line-height: 1;
    cursor: pointer;
    /* No `will-change`, and THIS was the costliest of the four — see the note on
       `.btn` in section 2. A row action is emitted once per action per grid row,
       so the number of promoted layers scaled with the SIZE OF THE DATASET: a
       200 row Tabulator page held several hundred of them, permanently, so that
       one 60ms scale could run when a finger came down on one. That is the exact
       shape the property's own guidance warns against. */
    transition:
        background-color var(--fx-dur-base) var(--fx-ease-standard),
        color var(--fx-dur-base) var(--fx-ease-standard),
        opacity var(--fx-dur-base) var(--fx-ease-standard),
        transform var(--fx-dur-base) var(--fx-ease-standard);
}

.frihat-row-action.frihat-row-action + .frihat-row-action {
    margin-inline-start: var(--fx-space-1);
}

@media (any-hover: hover) {
    /* THE TINT and the primary ink - the same hover a toolbar ghost, a nav item
       and a table row get. It used to be a brand tint with brand ink, which put
       an accent on every editable row in the platform. The tint is the client's
       mint ramp, not a grey; what makes it a tint is its WEIGHT against the
       plane (1.03 to 1.27), never its hue. */
    .frihat-row-action.frihat-row-action:hover,
    .frihat-row-action.frihat-edit.frihat-edit:hover {
        background-color: var(--fx-color-hover);
        color: var(--fx-color-text);
    }

    /* The destructive TRIGGER: red ink on the same neutral wash. No red plate -
       see the header note on destructive actions. */
    .frihat-row-action.frihat-delete.frihat-delete:hover {
        background-color: var(--fx-color-hover);
        color: var(--fx-color-danger-fg);
    }
}

/* The press scale is the one motion a row action gets. It is entirely contained
   inside the button's own box, so it cannot be clipped by the cell and cannot
   shift the row. */
.frihat-row-action.frihat-row-action:not(:disabled):active {
    background-color: var(--fx-color-active);
    transform: scale(var(--fx-btn-press-scale));
    transition-duration: var(--fx-dur-instant);
}

/* EXCEPTION 2 OF 2 to the OFFSET, not to the outline. A row action sits inside a
   Tabulator cell, and the CELL is what clips - the button clips nothing. That
   distinction decides the fix: `--fx-shadow-focus` is the token layer's answer
   for an element that clips ITS OWN overflow, and a box-shadow is cropped by an
   ancestor's overflow exactly as an outline is, so pointing at the token here
   left the ring just as cut as before while reading as though it were solved.

   Same answer as the toast close button and as `.fx-chip-remove` in 08: a real
   outline at a NEGATIVE offset. The button is at least 24px square - see the
   `max()` on its block size - so a 2px ring drawn inside it stays clear of the
   glyph, and nothing outside the border box means nothing for the cell to clip. */
.frihat-row-action.frihat-row-action:focus-visible {
    outline: var(--fx-focus-width) solid var(--fx-color-focus-ring);
    outline-offset: calc(var(--fx-focus-offset) * -1);
}

.frihat-row-action.frihat-row-action:disabled,
.frihat-row-action.frihat-row-action.disabled {
    --fx-btn-press-scale: 1;

    opacity: 0.5;
    background-color: transparent;
    cursor: not-allowed;
    transform: none;
}

/* Coarse pointer: nothing to write. Both legacy aliases size off control-h-sm /
   control-h-md, which the token layer already re-tiers to 32 / 40 on a touch
   device. */


/* ── 16) User preference media queries ────────────────────────────────────────
 *
 * REDUCED MOTION. With the lifts, the shine, the spring and the focus bloom all
 * deleted, there is exactly one movement left in the button system - the press
 * scale - plus the opt-in entrance on `.fx-btn-row.fx-stagger`. Both go. Colour,
 * filter and focus feedback are untouched, because removing those would remove
 * the affordance itself rather than its decoration.
 *
 * The `--fx-btn-press-scale` local is zeroed at the top of the cascade, which
 * neutralises every variant that opted in through it including any added later;
 * `transform` is then cleared explicitly as a belt-and-braces second mechanism,
 * because a single missed selector here is an accessibility defect rather than a
 * cosmetic one. The loading spinner swaps from a rotation to an opacity pulse so
 * the "work in progress" signal survives without movement on screen.
 *
 * INCREASED CONTRAST: borders thicken, the primary state filters are cut (a
 * brightness shift is a low-contrast signal by construction), and ghost buttons
 * gain a visible border so they stop relying on a 5 percent hover wash to be
 * discoverable. The token layer additionally collapses every text tier onto
 * `--fx-color-text` and every line token onto `--fx-color-border-strong`, so the
 * tertiary and ghost inks promote to full contrast without a rule here.
 * ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    .btn,
    .btn-close,
    .fx-fab,
    .frihat-btn.frihat-btn.frihat-btn,
    .frihat-row-action.frihat-row-action,
    .btn.dropdown-toggle::after {
        --fx-btn-press-scale: 1;

        transition-duration: var(--fx-dur-instant);
    }

    .btn.btn:not(:disabled):not(.disabled):active,
    .btn.btn:not(:disabled):not(.disabled).is-pressed,
    .fx-fab:not(:disabled):active,
    .frihat-btn.frihat-btn.frihat-btn:not(:disabled):active,
    .frihat-row-action.frihat-row-action:not(:disabled):active {
        transform: none;
    }

    /* No staggered entrance either. */
    .fx-btn-row.fx-stagger > .btn,
    .fx-btn-row.fx-stagger > .btn-group,
    .fx-btn-row.fx-stagger > .frihat-btn {
        animation: none;
    }

    /* THE PULSE MUST NOT READ ITS PERIOD FROM A DURATION TOKEN, AND THIS WAS A
       STROBE UNTIL 2026-08-03. MEASURED, not inferred: on a machine reporting
       this preference the rule below computed to `fx-btn-pulse` at 0.00001s with
       an iteration count of `infinite` — an opacity flash at roughly 100kHz on
       every button that reports work in progress.

       WHY IT HAPPENED, because the shape recurs. 01-base section 15 damps every
       `fx-dur-*` token to 0.01ms inside this same query, and its universal
       backstop that would otherwise force `animation-iteration-count: 1` is
       (0,0,0). This rule is (0,2,1) and writes the `animation` SHORTHAND, so it
       set the period and the count together and won both — which is precisely
       the limitation section 15 documents and says must be answered by the file
       that owns the loop. This file owns it.

       SLOWED, NOT STOPPED, and the period is DERIVED rather than typed.
       fx-toast-duration is the one time token 00-tokens deliberately leaves
       undamped, because it measures human perception rather than animation, so
       it is the only honest source for a period that has to stay human under
       this preference. Half of it gives one pulse every two and a half seconds,
       identical to the four spinners 08-feedback slows in its own branch, so a
       loading button and a loading dialog now breathe at the same rate. A still
       indicator cannot be told from a hung request, which is why this does not
       simply become `animation: none`. */
    .btn.is-loading::after,
    .btn[data-loading]::after,
    .btn[aria-busy="true"]::after {
        animation: fx-btn-pulse calc(var(--fx-toast-duration, 5s) / 2) var(--fx-ease-standard) infinite;
        border-color: var(--fx-btn-ink-current);
    }
}

@media (prefers-contrast: more) {
    /* A brightness shift is a low-contrast signal by construction, so the
       primary's state changes stop being a filter and fall back to the fill
       step Bootstrap's own hover locals already carry. `.btn.btn` is (0,2,0)
       because a media query adds no specificity and these locals are set at
       (0,2,0) in the variant blocks. */
    .btn.btn,
    .fx-fab,
    .frihat-btn.frihat-btn.frihat-btn {
        --fx-btn-primary-hover-filter: none;
        --fx-btn-primary-active-filter: none;
    }

    /* The border is thickened and promoted to the opaque strong line ONLY where
       one already exists or where the control is otherwise edgeless. A solid
       variant is deliberately left alone: its fill is already a 4.5:1 boundary,
       and a grey ring around a green button buys nothing while costing two
       pixels of the target box. */
    .btn.btn-outline-primary,
    .btn.btn-outline-success,
    .btn.btn-outline-danger,
    .btn.btn-outline-warning,
    .btn.btn-outline-info,
    .btn.btn-outline-dark,
    .btn.btn-tertiary,
    .btn.fx-btn-tertiary,
    .btn.btn-soft-danger,
    .btn.btn-soft-success,
    .btn.btn-soft-warning,
    .btn.btn-soft-info,
    .btn.btn-ghost,
    .btn.fx-btn-ghost,
    .btn[class*="btn-ghost-"],
    .btn[class*="fx-btn-ghost-"],
    .btn-toolbar > .btn:not([class*="btn-"]):not(.btn-link),
    .dropdown-menu .btn {
        --bs-btn-border-width: var(--fx-border-width-strong);
        --bs-btn-border-color: var(--fx-color-border-strong);
        --bs-btn-hover-border-color: var(--fx-color-border-strong);
        --bs-btn-active-border-color: var(--fx-color-border-strong);
    }

    /* `.frihat-row-action` is not a `.btn`, so it does not read
       `--bs-btn-border-width` and needs the full shorthand to get an edge. */
    .frihat-row-action.frihat-row-action {
        border: var(--fx-border-width-strong) solid var(--fx-color-border-strong);
    }
}

/* ── 16.1) Forced colours (Windows High Contrast) ─────────────────────────────
 *
 * In forced-colours mode the user agent discards author colours AND box-shadows
 * outright. Every fill, level step, bevel and float in this file therefore
 * evaporates at once, which would leave a button system with no visible buttons.
 * Outlines and borders are the only things that survive, so they carry the whole
 * structure here. System keywords, never tokens: a token resolves to an author
 * colour, which is exactly what is being discarded.
 *
 * Focus needs no rule of its own in this mode - it is already an `outline` in
 * the base cascade, which survives. Only the ring COLOUR has to be forced to a
 * system keyword.
 *
 * THE SELECTORS BELOW CARRY THEIR CALL SITE'S SPECIFICITY, AND TWO OF THEM DID
 * NOT UNTIL 2026-08-03. A media query adds no specificity, so a forced-colors
 * rule only lands if it out-weighs whatever set the property in the normal
 * cascade. Both legacy aliases defend themselves against bundle order with
 * repeated class selectors and both write `border: 0` in their base block:
 *     `.frihat-btn.frihat-btn.frihat-btn`      (0,3,0)  vs a bare `.frihat-btn`
 *     `.frihat-row-action.frihat-row-action`   (0,2,0)  vs a bare selector
 * so the restore below lost to them and the two controls kept `border: 0` in
 * high contrast. Since this mode ALSO discards the fill and the box-shadow, the
 * hand-built dialog footer buttons - Save, Cancel and the delete confirmation in
 * all three module dialogs - rendered as invisible rectangles with a floating
 * label, and the grid row glyphs lost the edge that section 16 gives them under
 * prefers-contrast. Matched to their defining specificity here. A NEW ALIAS THAT
 * REPEATS ITS CLASS MUST BE ADDED TO THIS BLOCK AT THE SAME WEIGHT.
 *
 * `.btn`, `.fx-fab` and `.btn-close` need no escalation: nothing in this file
 * sets their border above (0,1,0), and this block is the last word in source
 * order for all three.
 * ------------------------------------------------------------------------- */
@media (forced-colors: active) {
    .btn,
    .fx-fab,
    .frihat-btn.frihat-btn.frihat-btn,
    .frihat-row-action.frihat-row-action,
    .btn-close {
        border: var(--fx-border-width) solid ButtonBorder;
    }

    .btn:focus-visible,
    .btn-check:focus-visible + .btn,
    .btn-close:focus-visible,
    .fx-fab:focus-visible,
    .frihat-btn.frihat-btn.frihat-btn:focus-visible {
        outline: var(--fx-focus-width) solid Highlight;
        outline-offset: var(--fx-focus-offset);
    }

    /* The two clipped controls keep their NEGATIVE offset here. This block used
       to hand them the positive offset alongside everyone else and then zero a
       box-shadow that no longer exists; both halves were wrong. Forced-colors
       does not switch off an ancestor's `overflow: hidden`, so a ring pushed 2px
       outward is cropped in this mode exactly as it is in the normal one. Only
       the COLOUR is forced to a system keyword. */
    .toast .btn-close:focus-visible,
    .frihat-row-action.frihat-row-action:focus-visible {
        outline: var(--fx-focus-width) solid Highlight;
        outline-offset: calc(var(--fx-focus-offset) * -1);
    }

    /* A toggled button must stay distinguishable once its fill is discarded. */
    .btn[aria-pressed="true"],
    .btn.active,
    .btn-check:checked + .btn {
        border: var(--fx-border-width-strong) solid ButtonText;
        forced-color-adjust: none;
    }
}


/* ── 17) Print ────────────────────────────────────────────────────────────────
 *
 * A printed case sheet or client statement is a legal artefact. Controls are
 * not part of it, so every button surface is removed from the printed flow.
 *
 * THREE SELECTORS HERE WERE INERT UNTIL 2026-08-03, AND THE CLAIM THAT USED TO
 * SIT IN THIS PARAGRAPH — that they "match the display-setting rules above at
 * equal or higher specificity" — WAS SIMPLY UNTRUE. A media query adds no
 * specificity, so a print rule only lands if it out-weighs whatever set the
 * property in the normal cascade, and three of these did not:
 *
 *     `.frihat-btn`      (0,1,0)  vs  `.frihat-btn.frihat-btn.frihat-btn`
 *                                      at (0,3,0), which sets inline-flex
 *     `.frihat-row-action` (0,1,0) vs `.frihat-row-action.frihat-row-action`
 *                                      at (0,2,0), which sets inline-flex
 *     `.btn-group`       (0,1,0)  vs  `.btn-group.fx-btn-group-wrap`
 *                                      at (0,2,0), which sets flex
 *
 * So the hand-built dialog footer buttons (Save, Cancel and the delete
 * confirmation in all three module dialogs), the grid row action glyphs, and any
 * wrapping button group PRINTED ONTO THE PAGE — on a platform whose printed
 * output is a legal artefact. This is the same escalation section 16.1 already
 * had to make for the two legacy aliases in forced colours, and it was missed
 * here because a print defect is invisible in normal browsing and in every
 * screenshot. Matched to their defining specificity now.
 *
 * A NEW ALIAS THAT REPEATS ITS CLASS MUST BE ADDED TO THIS BLOCK AT THE SAME
 * WEIGHT, exactly as section 16.1 states for itself. `.btn`, `.btn-close`,
 * `.fx-fab` and the four container selectors need no escalation: nothing in this
 * file sets their `display` above (0,1,0), and this block is the last word in
 * source order for all of them.
 * ------------------------------------------------------------------------- */
@media print {
    .btn,
    .btn-close,
    .btn-group.btn-group,
    .btn-group-vertical,
    .btn-toolbar,
    .fx-btn-row,
    .fx-fab,
    .frihat-btn.frihat-btn.frihat-btn,
    .frihat-row-action.frihat-row-action,
    .modal-footer,
    .abp-entity-action-container,
    .abp-toolbar,
    .lpx-content-toolbar {
        display: none;
    }
}


/* -- 18) ROW ACTION OVERFLOW: the kebab and its menu -------------------------
 *
 * OWNER'S RULING, 2026-09-18: at most three row-action buttons stay visible and
 * the remainder fold behind one vertical three-dots button. The behaviour lives
 * in `wwwroot/frihat/frihat-grid-tools.js` (`frihat.gridTools.rowActions`); the
 * SKIN LIVES HERE, because section 15's header already states that the
 * `.frihat-row-action` family is owned by this file and not by the grid slice.
 * A `.frihat-row-menu` block appearing in 09-tabulator.css or in any page-level
 * `Index.css` is a duplicate and belongs in this file.
 *
 * THE MENU IS NOT A CHILD OF THE GRID. The script portals it to <body> and
 * gives it two viewport coordinates, so every selector below is written against
 * a TOP-LEVEL element. That is the whole point of the task: a menu rendered
 * inside a Tabulator cell is clipped by `.tabulator-tableholder`'s scroll
 * overflow and, on a locked column, buried under the frozen region's own
 * stacking context. Nothing here may be nested under `.tabulator`.
 *
 * SIXTEEN THEMES. Every colour below is a semantic `--fx-color-*` token. A raw
 * hex or an `--fx-p-*` primitive would be correct in two of the sixteen
 * family-by-mode combinations and wrong in fourteen.
 * ------------------------------------------------------------------------- */

/* --- the kebab trigger. It IS a `.frihat-row-action` and inherits the whole
       glyph-button skin from section 15; only the two things that are
       specifically its own are written here. --- */
.frihat-row-action.frihat-row-more {
    /* It must not be squeezed below the shared 24px target when it lands at the
       end of a tight actions column. */
    flex: 0 0 auto;
}

/* THE OPEN STATE IS VISIBLE ON THE TRIGGER, not only in the menu. A user who
   tabs away and back must be able to see which button owns the list that is on
   screen. Keyed on the ARIA state rather than a class of our own, so the visual
   and the accessibility tree can never disagree. */
.frihat-row-action.frihat-row-more[aria-expanded="true"] {
    background-color: var(--fx-color-active);
    color: var(--fx-color-text);
}

/* --- the optional COUNT on a visible action --------------------------------
 *
 * OUT OF FLOW, AND THAT IS ARITHMETIC RATHER THAN TASTE. Every page sizes its
 * actions column from the count of controls — `30 + 40 * <controls>` and its
 * variants — on the assumption that a control is the 28px square section 15
 * defines. A badge laid out IN the flow would widen the button, push the sum
 * past the column and clip the last action on every row of every grid that
 * ever shows one. Absolutely positioned it costs exactly zero layout, so the
 * footprint those sums depend on is unchanged.
 *
 * AND IT STAYS INSIDE THE BORDER BOX, hung in the corner rather than off it,
 * for the same reason section 15 draws the focus outline at a negative offset:
 * the Tabulator CELL clips, the button does not. A bubble poking past the
 * button would be cropped on the last visible column exactly as that ring was.
 *
 * FILLED PRIMARY, not one of the outline badge variants in 08-feedback. That
 * section is right that a screen of filled badges is a screen of brand, but
 * this thing is ~10px of text: a subtle wash behind it fails contrast at that
 * size, and `--fx-color-text-on-primary` is the one pair the token layer
 * guarantees is legible. Semantic tokens throughout, so it is correct in all
 * sixteen family-by-mode combinations with no hex anywhere.
 *
 * It is NOT a `.badge`: that component is a 24px-tall pill built to be read as
 * a status on its own line, and it would not fit inside a 28px button.
 * The script marks it `aria-hidden` and folds the count into the button's
 * `aria-label`, so nothing here has to carry meaning for a screen reader. --- */
.frihat-row-action.has-badge {
    /* The containing block for the badge, and the ONLY thing that changes on a
       badged button: no size, no padding, no margin. Scoped to the modifier so
       the platform's other row actions keep the exact box they already had. */
    position: relative;
}

.frihat-row-action-badge {
    position: absolute;
    inset-block-start: 0;
    /* LOGICAL, so the bubble sits on the trailing corner in English and on the
       leading one in Arabic without a second rule. */
    inset-inline-end: 0;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Sized in `em` off its own font size: the pill is a circle on one digit
       and grows into a lozenge on two, and it re-tiers with the control scale
       on a coarse pointer for free. */
    min-inline-size: 1.25em;
    block-size: 1.25em;
    padding-inline: 0.25em;

    border-radius: var(--fx-radius-pill);
    background-color: var(--fx-color-primary);
    color: var(--fx-color-text-on-primary);

    font-size: var(--fx-text-2xs);
    font-weight: var(--fx-weight-semibold);
    line-height: 1;
    letter-spacing: 0;
    /* Tabular figures so 11 and 99 occupy the same width and the bubble does
       not breathe as a count ticks over. */
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;

    /* Decoration drawn over a control the pointer is aiming at. It must never
       become the event target, or a click near the corner would miss the
       button and `closest('button[data-action]')` would still find it — but
       the hover and press states would flicker on the way in. */
    pointer-events: none;
}

/* The glyph is dimmed on a disabled action; its count has to dim WITH it or a
   bright bubble advertises an action the row cannot perform. The opacity in
   section 15 is on the button, so this only has to stop the filled plate from
   reading as live. */
.frihat-row-action:disabled .frihat-row-action-badge,
.frihat-row-action.disabled .frihat-row-action-badge {
    background-color: var(--fx-color-disabled-fg);
}


/* --- the portaled menu ---------------------------------------------------- */

.frihat-row-menu {
    /* FIXED, because the element is parented to <body> while its anchor is a
       cell deep inside a scrolling grid. The two custom properties are written
       by the script from `getBoundingClientRect`, which is already a viewport
       coordinate, so a fixed box needs no further correction; an absolute one
       would need the document scroll added back on both axes.

       PHYSICAL `left`/`top`, NOT the logical insets. The RTL decision is made in
       the script's arithmetic by aligning the menu's inline END edge with the
       kebab's; handing that already-resolved number to `inset-inline-start`
       would mirror it a second time in Arabic. */
    position: fixed;
    left: var(--fx-row-menu-x, 0);
    top: var(--fx-row-menu-y, 0);
    z-index: var(--fx-z-popover);

    /* HIDDEN BUT LAID OUT. The script appends the menu, MEASURES it, positions
       it, and only then adds `.is-open`. `visibility` keeps a real width and
       height to measure; `display: none` would report zero for both and the menu
       would open at the wrong corner on every RTL screen and on every row near
       the bottom of the viewport. */
    visibility: hidden;

    display: flex;
    flex-direction: column;
    min-inline-size: 11rem;
    max-inline-size: min(20rem, calc(100vw - 1rem));
    padding-block: var(--fx-space-1);
    padding-inline: 0;
    margin: 0;

    background-color: var(--fx-color-surface-raised);
    border: var(--fx-border-width) solid var(--fx-color-border);
    border-radius: var(--fx-radius-md);
    box-shadow: var(--fx-shadow-float-md);

    font-family: var(--fx-font-body);
    font-size: var(--fx-text-sm);
    color: var(--fx-color-text);

    /* The list can outgrow the viewport on a row with many actions. It scrolls
       inside itself rather than running off screen, and this is the ONLY
       overflow clip in the whole control: on the element that owns it. */
    max-block-size: min(60vh, 24rem);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.frihat-row-menu.is-open {
    visibility: visible;
}

/* --- one item ------------------------------------------------------------- */

.frihat-row-menu-item {
    display: flex;
    align-items: center;
    column-gap: var(--fx-control-gap-md);
    inline-size: 100%;
    min-block-size: var(--fx-control-h-sm);
    padding-block: var(--fx-space-1-5);
    padding-inline: var(--fx-control-pad-x-sm);

    border: 0;
    border-radius: 0;
    background-color: transparent;
    color: inherit;

    font: inherit;
    font-weight: var(--fx-weight-medium);
    letter-spacing: var(--fx-tracking-ui);

    /* START, NOT LEFT. The row reads from the inline start in both directions,
       which is what makes one rule correct in Arabic and in English. */
    text-align: start;
    cursor: pointer;

    transition: background-color var(--fx-dur-fast) var(--fx-ease-standard),
                color var(--fx-dur-fast) var(--fx-ease-standard);
}

/* The icon column is a fixed width so the labels line up down the list however
   wide each individual glyph happens to be. */
.frihat-row-menu-item > i {
    flex: 0 0 1.25em;
    text-align: center;
    color: var(--fx-color-text-muted);
}

.frihat-row-menu-label {
    flex: 1 1 auto;
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The same count on the FOLDED-AWAY form of the action. In the list it is a
   real flex item rather than an overlay: there is no 28px box to protect here
   and no cell to clip it, and the label above it already takes the space it
   needs. Same plate and same figures as the button copy, one step larger
   because the menu row is, so the two read as one object that moved. */
.frihat-row-menu-badge {
    flex: 0 0 auto;
    margin-inline-start: var(--fx-space-2);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: 1.5em;
    padding-block: 0.1em;
    padding-inline: 0.4em;

    border-radius: var(--fx-radius-pill);
    background-color: var(--fx-color-primary);
    color: var(--fx-color-text-on-primary);

    font-size: var(--fx-text-xs);
    font-weight: var(--fx-weight-semibold);
    line-height: 1;
    letter-spacing: var(--fx-tracking-ui);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/* Unavailable on this row: the plate goes flat for the same reason the glyph
   does two rules down, so the count stops advertising a live action. */
.frihat-row-menu-item[aria-disabled="true"] .frihat-row-menu-badge {
    background-color: var(--fx-color-disabled-fg);
}

.frihat-row-menu-item:hover {
    background-color: var(--fx-color-hover);
}

.frihat-row-menu-item:hover > i {
    color: var(--fx-color-text);
}

.frihat-row-menu-item:active {
    background-color: var(--fx-color-active);
}

/* THE DESTRUCTIVE ITEM IS RED INK ON THE NEUTRAL WASH, never a red plate. That
   is the same ruling section 15 makes for the destructive row-action TRIGGER,
   applied to its folded-away form so an action does not change character merely
   because it moved into a list. */
.frihat-row-menu-item.frihat-delete:hover,
.frihat-row-menu-item.frihat-delete:hover > i {
    color: var(--fx-color-danger-fg);
}

/* --- focus: a GLOW, never a fence ------------------------------------------
 *
 * Section 15 draws a NEGATIVE-offset outline on `.frihat-row-action` for one
 * specific reason: that button sits inside a Tabulator cell and the CELL clips.
 * NEITHER CONDITION HOLDS HERE. This menu is parented to <body> and clipped by
 * nothing, so it gets the platform's real focus treatment, the soft glow the
 * owner asked for twice, and no ring at all. An inset outline here would be
 * copying a workaround out of the context that justified it. */
.frihat-row-menu-item:focus {
    outline: none;
}

.frihat-row-menu-item:focus-visible {
    outline: none;
    background-color: var(--fx-color-hover);
    box-shadow: var(--fx-shadow-focus);
}

/* --- unavailable on THIS row -----------------------------------------------
 *
 * `aria-disabled`, not `:disabled`, because the script keeps these items
 * FOCUSABLE on purpose: a real `disabled` button leaves the focus order, so an
 * arrow key would skip straight past it and the user would never hear why the
 * action is unavailable. The pointer is refused visually and the click is
 * refused in script. */
.frihat-row-menu-item[aria-disabled="true"] {
    color: var(--fx-color-disabled-fg);
    cursor: not-allowed;
}

.frihat-row-menu-item[aria-disabled="true"] > i {
    color: var(--fx-color-disabled-fg);
}

.frihat-row-menu-item[aria-disabled="true"]:hover,
.frihat-row-menu-item[aria-disabled="true"]:active {
    background-color: transparent;
}

/* --- reduced motion -------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    .frihat-row-menu-item {
        transition: none;
    }
}

/* --- print -------------------------------------------------------------------
   The print block in section 17 hides the row actions themselves. The menu is a
   top-level element and matches none of those selectors, so it is named here; a
   menu left open when a user prints would otherwise land a floating box across
   the page. */
@media print {
    .frihat-row-menu {
        display: none;
    }
}
