/* =============================================================================
   qs-a11y.css — focus-visible ring + touch-target primitives
   =============================================================================
   Audit findings closed: G3 (no focus-visible anywhere, 0 hits repo-wide before
   this file — not even in the vendor template) and most of G4 (touch targets;
   118 of 130 controls under 44x44, permission checkboxes fixed at 15x15).

   LOAD ORDER: this file must be LAST in <head> — after qs-theme-dark.css and
   qs-theme-dark-pages.css (see _Layout.cshtml / _LayoutAccount.cshtml). Among
   competing !important declarations of equal specificity, the LAST one in the
   cascade wins ties — the same mechanism qs-theme-dark.css's own header comment
   documents and relies on.

   WHY THIS SURVIVES THE !important WAR WITHOUT REMOVING A SINGLE ONE OF THE
   ~1191 EXISTING !important RULES: grep confirms the only `outline:` declarations
   anywhere in app CSS are site.css:959 and qs-signature.css:712, and both are
   plain `outline: none` with NO !important. So an !important outline declared
   here wins unconditionally — G3 needed one new file, not a de-!important pass.
   ========================================================================== */

/* ---------- Focus ring — two-tone so it stays >=3:1 against ANY backdrop ----------
   This app puts controls on a white card, a --qs-primary sidebar, a gradient
   navbar, and a dark canvas. A single-color ring fails WCAG 1.4.11 against at
   least one of those. A two-tone ring (colored outline + contrasting halo) is
   guaranteed to contrast against any one of them, because one band always
   differs from the surface underneath it. */
:root {
    --qs-focus-ring-w: 3px;
    --qs-focus-ring-offset: 2px;
}

a[href]:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible,
[contenteditable="true"]:focus-visible,
[role="button"]:focus-visible,
[role="link"]:focus-visible,
[role="tab"]:focus-visible,
[role="checkbox"]:focus-visible,
[role="switch"]:focus-visible,
[role="menuitem"]:focus-visible,
[role="option"]:focus-visible {
    outline: var(--qs-focus-ring-w) solid var(--qs-focus-ring) !important;
    outline-offset: var(--qs-focus-ring-offset) !important;
    box-shadow: 0 0 0 calc(var(--qs-focus-ring-w) + var(--qs-focus-ring-offset)) var(--qs-focus-ring-contrast) !important;
}

/* Component overrides — a handful of 2-class vendor selectors carry enough
   specificity to beat the plain selectors above even with !important on both
   sides (equal specificity resolves by source order, and some vendor rules are
   later in a different file). :focus-visible:focus-visible is a legal,
   spec-compliant duplication that raises specificity to (0,2,0) without an
   ancestor combinator and without further !important escalation. Deliberately
   NOT :where() — that zeroes specificity, the opposite of what's needed. */
.btn:focus-visible:focus-visible,
.btn-icon:focus-visible:focus-visible,
.page-link:focus-visible:focus-visible,
.paginate_button:focus-visible:focus-visible,
.dt-button:focus-visible:focus-visible,
.nav-link:focus-visible:focus-visible,
.dropdown-item:focus-visible:focus-visible,
.select2-selection:focus-visible:focus-visible,
.qs-bulk-btn:focus-visible:focus-visible,
.qs-theme-toggle:focus-visible:focus-visible,
.qs-act:focus-visible:focus-visible {
    outline: var(--qs-focus-ring-w) solid var(--qs-focus-ring) !important;
    outline-offset: var(--qs-focus-ring-offset) !important;
    box-shadow: 0 0 0 calc(var(--qs-focus-ring-w) + var(--qs-focus-ring-offset)) var(--qs-focus-ring-contrast) !important;
}

/* Mouse/touch users keep the clean look — this is the correct idiom that makes
   the ring acceptable to a design team, not a regression of the previous
   `outline: none` sites (site.css:959, qs-signature.css:712). */
:focus:not(:focus-visible) {
    outline: none;
}

/* ---------- Skip link (G3) ---------- */
.qs-skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: var(--qs-z-toast);
    background: var(--qs-primary);
    color: var(--qs-on-primary) !important;
    padding: 10px 18px;
    border-radius: 0 0 var(--qs-radius-md) var(--qs-radius-md);
    font-weight: var(--qs-fw-semibold);
    text-decoration: none;
}
[dir="rtl"] .qs-skip-link { left: auto; right: -9999px; border-radius: 0 0 0 var(--qs-radius-md); }
.qs-skip-link:focus {
    left: 12px;
}
[dir="rtl"] .qs-skip-link:focus { left: auto; right: 12px; }

/* ---------- Visually-hidden but announced (accessible-name pattern) ---------- */
.qs-visually-hidden {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---------- Touch targets (G4) ----------
   Two techniques, deliberately not a blanket min-height:44px (that would blow
   up dense table rows and worsen J1's mobile density problem):
     1. Layout-changing, where there's room.
     2. Layout-neutral expanded hit area, where there isn't — WCAG 2.5.5/2.5.8
        measure the POINTER TARGET, not the painted box, so a 32px icon with a
        44px invisible ::after conforms without touching table density. */
.btn,
.qs-bulk-btn,
.page-link,
.paginate_button,
.form-control,
.qs-check-row {
    min-height: var(--qs-tap-min);
}

.btn-icon.btn-action,
.qs-check {
    position: relative;
}
.btn-icon.btn-action::after,
.qs-check::after {
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    width: var(--qs-tap-min);
    height: var(--qs-tap-min);
    transform: translate(-50%, -50%);
}

.qs-check {
    width: var(--qs-checkbox-size);
    height: var(--qs-checkbox-size);
    cursor: pointer;
    flex-shrink: 0;
}
.qs-check-row {
    display: flex;
    align-items: center;
    gap: var(--qs-space-2);
    padding: var(--qs-space-2) var(--qs-space-1);
    cursor: pointer;
}

/* site.css:1597-1600 SHRINKS action buttons at <=767px (padding:5px 7px,
   font-size:12px, both !important) — actively causing G4 at the exact 390px
   viewport the audit tests. Neutralize it here, loaded after site.css. */
@media (max-width: 767px) {
    .btn-action {
        padding: 10px var(--qs-space-3) !important;
        font-size: 14px !important;
    }

    /* 2026-08-20 — MEASURED at 390x844, which is the first time this band was actually
       looked at rather than reasoned about. Padding alone does NOT reach the floor:
         .btn-icon.btn-action    36 x 44   (height fine, WIDTH 8px short)
         .qs-translate-btn       38 x 38   (short on both axes)
         .dt-buttons .btn        38 x 44   (WIDTH short)
       A 36px-wide target is the G4 defect again, just on the other axis - and padding
       cannot fix it, because the glyph is narrower than the floor. min-width/min-height
       is the only thing that states the floor directly.

       WHY IT IS SAFE: verified in the same run that no page gains horizontal overflow at
       390px afterwards (checks-mobile.js asserts page scrollWidth == clientWidth). The
       actions column already scrolls inside .dt-table-scroll, and below 576px the table
       becomes stacked cards where there is width to spare.

       !important is required on min-width for .btn-icon: components.min.css pins
       `.btn-icon { width: 38px !important }` and a min-width without it loses to the
       fixed width in the vendor sheet.

       SCOPE: `.content-wrapper .btn` rather than a list of component classes. The first
       attempt enumerated `.dt-buttons .btn`, `.dataTables_wrapper .btn` and friends and
       STILL missed a plain `.btn-outline-secondary` on the dashboard at 38x44 - because
       enumerating component classes is a losing game. The rule is stated once for the
       page body; the sidebar, top bar and modals keep their own sizing. */
    .content-wrapper .btn,
    .btn-icon.btn-action,
    .qs-act,
    .qs-translate-btn {
        min-width: var(--qs-tap-min) !important;
        min-height: var(--qs-tap-min) !important;
    }

    /* The row-select checkbox has no wrapping label (unlike the permission matrix), so
       its target is the 44px ::after overlay defined above. At <=575px the table becomes
       stacked cards and each checkbox sits in its own row, so those overlays cannot
       collide the way the permission matrix's did — verified by hit-testing four points
       around every checkbox in checks-mobile.js. */
    .qs-check {
        margin: var(--qs-space-2);
    }
}

/* ============================================================================
   SELECTED TEXT — user-reported: selecting a row in a dark-mode table (order
   details > Products, and any other list) turned the row into a pale block with
   the text gone.

   ROOT CAUSE: nothing in this app had ever declared ::selection, so selection was
   the browser's. Chrome paints a LIGHT highlight (lighter still when the window
   is not focused) and deliberately leaves `color` alone, so the run keeps the ink
   it already had. That is safe for dark-on-white body copy and ruinous for every
   light-on-dark run in the app: dark-theme table cells and headings (--qs-text is
   #f5f8fc), the sidebar, the navbar, the login brand panel. White ink on a white
   highlight is a blank row, which is exactly what was reported.

   Declaring BOTH halves fixes it for good, because the pair is then guaranteed to
   contrast no matter what was underneath. Not scoped to dark: light mode has the
   same failure on its own dark surfaces (white sidebar/navbar/brand-panel text).

   TWO THINGS HERE ARE NOT STYLE PREFERENCES:
   * ::-moz-selection MUST stay in its own rule - one unknown selector invalidates
     the whole selector list, so pairing them would drop the rule in EVERY browser.
   * the dark rule repeats the same tokens purely to carry a dark FALLBACK. var()
     inside a highlight pseudo has a patchy history (WebKit has shipped builds that
     drop it), and a fallback is the only thing that renders if substitution fails.
     One shared literal pair would then paint dark-mode selection in the light
     colours - still legible, since both halves are set, but not the intended look.
   ========================================================================== */
::selection {
    background-color: var(--qs-selection-bg);
    color: var(--qs-selection-fg);
}
::-moz-selection {
    background-color: var(--qs-selection-bg);
    color: var(--qs-selection-fg);
}
html[data-theme="dark"] ::selection {
    background-color: var(--qs-selection-bg);
    color: var(--qs-selection-fg);
}
html[data-theme="dark"] ::-moz-selection {
    background-color: var(--qs-selection-bg);
    color: var(--qs-selection-fg);
}

/* ============================================================================
   prefers-reduced-motion — one global, selector-agnostic rule
   2026-08 UI/UX audit remediation.

   WHY THIS EXISTS when three files already had reduced-motion blocks:
   every one of them is a HAND-MAINTAINED SELECTOR LIST (auth-shell.css names 13
   selectors; qs-signature.css has two more lists). A list like that is wrong the
   moment anyone adds an animated element and forgets to extend it - and the L4
   extraction moved 22 rules into auth-shell.css, any of which could have carried
   an animation outside the list. Coverage that depends on remembering is not
   coverage.

   This rule needs no maintenance: it matches everything, including elements that
   do not exist yet. It lives in qs-a11y.css because that file is linked LAST, so
   it wins the cascade against every vendor and theme sheet above it.

   WHY near-zero duration rather than `animation: none`:
   `none` freezes an element in its UNANIMATED base state, which can be wrong -
   the orbit rings, for example, get their centring from `translate(-50%,-50%)`
   INSIDE the keyframes, which is exactly why auth-shell.css has to re-apply that
   transform by hand after killing the animation. Running the animation to
   completion in 0.01ms instead lands every element on its final keyframe, so the
   intended end state is preserved with no per-element patching.

   PRACTICAL SIDE EFFECT, worth knowing: the auth shell ran sheen and orbit
   animations with `infinite`, so nothing on those screens was ever visually
   stable. That made the login page effectively undriveable by Playwright, whose
   actionability check waits for an element to hold still across two animation
   frames - `page.click()` simply timed out. With this rule, a test run (or a CI
   browser) that sets prefers-reduced-motion gets a static page it can drive.
   ============================================================================ */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        animation-delay: 0ms !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0ms !important;
        scroll-behavior: auto !important;
    }
}

/* ============================================================================
   G1 — WCAG 1.4.3 contrast on semantic *text* colours (2026-08-20)

   MEASURED on /AdminProviderFinancial/Index against its real backgrounds
   (#fff and #fafbfd), light theme:

     .text-info    rgb(0,188,212)   2.30:1 / 2.22:1   <- the two values named in the report
     .text-success rgb(76,175,80)   2.78:1 / 2.68:1
     .text-warning rgb(255,112,67)  2.74:1 / 2.65:1
     .text-danger  rgb(244,67,54)   3.68:1 / 3.56:1
     .stat-sub     rgb(148,163,184) 2.56:1
     .attn-count (Home, .attn-warning) rgb(221,162,10) 2.27:1  <- fails even the 3:1
                                                                  large-text threshold

   Those raw values come from the vendor Limitless sheet (Material 500 ramp), which is
   tuned for coloured FILLS, not for text on white. This app already owns a solved
   version of the same palette: qs-tokens.css layer 2 ships --qs-*-solid values chosen
   for, and documented with, their on-white ratio (success 5.07:1, danger 5.62:1,
   info 5.80:1). The Orders status-badge fix the report cites as the reference pattern
   is those same tokens. So this is not a new palette - it is pointing four utility
   classes at the palette the rest of the app already passes with.

   HUE FAMILY IS PRESERVED (the acceptance criterion): green stays green, red stays
   red - only the shade darkens.

   WHY IT LIVES IN THIS FILE AND WHY IT IS SCOPED :not([data-theme="dark"]):
   qs-theme-dark.css:541-545 already re-points these four utilities at the --qs-d-*
   dark ramp with !important. Repainting them unconditionally here would fight that
   file for the same declaration, so the light fix is explicitly light-only and the
   dark ramp is left as the single owner of dark.

   .text-warning IS THE ONE JUDGEMENT CALL: --qs-warning-solid is amber-500, kept
   deliberately light in qs-tokens.css because it is used as a FILL. Amber text on
   white cannot reach 4.5:1 while still reading as amber, so text use resolves to
   --qs-warning-subtle-fg (the token that exists precisely for amber-as-text).
   ============================================================================ */
html:not([data-theme="dark"]) .text-success { color: var(--qs-success-solid) !important; }
html:not([data-theme="dark"]) .text-danger  { color: var(--qs-danger-solid) !important; }
html:not([data-theme="dark"]) .text-info    { color: var(--qs-info-solid) !important; }
html:not([data-theme="dark"]) .text-warning { color: var(--qs-warning-subtle-fg) !important; }

/* Money renders through these two helpers on the financial screens; both resolved to
   the raw vendor greens/reds above. */
html:not([data-theme="dark"]) .qs-money--pos { color: var(--qs-money-pos) !important; }
html:not([data-theme="dark"]) .qs-money--neg { color: var(--qs-money-neg) !important; }

/* Supporting text under each finance stat tile - 2.56:1 as shipped. */
html:not([data-theme="dark"]) .stat-sub { color: var(--qs-text-muted) !important; }

/* DataTables' "Showing 1-10 of 53" line: rgb(122,132,153) = 3.76:1. */
html:not([data-theme="dark"]) .dataTables_info { color: var(--qs-text-muted) !important; }

/* Home dashboard "needs attention" counters USED TO be color-tinted per variant
   (.attn-warning/.attn-info/.attn-danger/.attn-success) in views/home-index.css, which is
   what these four rules were re-pointing to a passing-contrast ramp. The GPG redesign
   retired that tinting entirely — .attn-count is now always var(--qs-text) (matches the
   reference, which never color-codes these), so the contrast failure these rules fixed no
   longer exists, and forcing a tint back on here would reintroduce the mismatch these
   rules were never meant to cause. */

/* .text-muted resolves to rgb(153,153,153) from the vendor sheet = 2.85:1 on white and
   2.75:1 on the striped-row #fafbfd — measured on /AdminProviderFinancial/Refunds, where
   it carries every transaction timestamp at 10.4px. --qs-text-muted (#5b6478) is 5.9:1 on
   white and is what the rest of the app already uses for secondary text.
   Dark mode keeps its own owner (qs-theme-dark.css), hence the same light-only scope. */
html:not([data-theme="dark"]) .text-muted { color: var(--qs-text-muted) !important; }

/* ============================================================================
   C7 — the "this action is unavailable, and here is why" placeholder
   (2026-08-20, second attempt — the first one shipped a visible defect)

   WHAT WENT WRONG THE FIRST TIME, because it explains every rule below.

   Attempt 1 put the "i" affordance on `.qs-act--disabled::after` in
   qs-list-density.css. That collides with the rule ~40 lines above this one:

       .btn-icon.btn-action::after { content:""; top:50%; left:50%;
                                     width:44px; height:44px;
                                     transform:translate(-50%,-50%) }

   Same pseudo-element, and the cascade resolves PER PROPERTY. At (0,2,0) the
   touch-target rule won `content`, `width`, `height`, `top` and `transform`;
   the (0,1,0) attempt won only `background` and `border-radius`. The result was
   a 44x44 SOLID GREY CIRCLE centred on the button, painted over the icon —
   which is precisely the grey blob the client screenshotted, four per row on
   BannerRequests/Index. The measurements passed because they read the button's
   own background and geometry, never what a pseudo-element painted on top.

   Two rules follow, and they are the point of this comment:
     1. `::after` on `.btn-icon.btn-action` BELONGS TO THE TOUCH TARGET. Never
        style it from another sheet. The info affordance is now a real <span> in
        the markup (qsDisabledAction), where nothing can silently reinterpret it.
     2. The reason text is NOT an absolutely-positioned child any more. The
        actions cell lives inside `.dt-table-scroll { overflow-x: auto }`, which
        is a clipping container — the client's second screenshot shows the bubble
        sliced in half. It is now drawn by qs-a11y.js into a single
        position:fixed node appended to <body>, so no ancestor can clip it.
   ============================================================================ */

.qs-act--disabled {
    /* THE DESIGN, third and final shape. The two rejected attempts are recorded because
       each one was rejected for a different, specific reason:

         attempt 1  filled grey button + inline sentence
                    -> a wall of grey chrome that buried the one live action, and the
                       sentence overflowed the column onto the next icon.
         attempt 2  no chrome at all + a floating 12px "i" badge per slot
                    -> read as dirt. On BannerRequests that is FOUR detached specks per
                       row hovering beside four glyphs; the client's word for it was
                       "artifacts", and they were right. A marker that has to float
                       outside its own control is not a marker, it is noise.

       What works: THE SLOT ITSELF IS THE SIGNAL. A dashed outline is the universal
       "empty / placeholder" idiom - it says "a control belongs here and it is not
       available" in one glance, with no extra element, no floating speck, and it GROUPS
       when there are four of them instead of multiplying. A solid pill still means
       "press me"; a dashed outline never does.

       Three states, and each earns its keep:
         rest   dashed hairline + muted glyph, no fill        -> quiet, groups, readable
         hover  fill + stronger border + full-contrast glyph  -> shows WHICH slot the
                                                                 tooltip is explaining
         focus  the app's standard focus ring                 -> reachable by keyboard

       Contrast is deliberately NOT the de-emphasis: --qs-text-faint measures 2.9:1 on
       white, under the 3:1 WCAG 1.4.11 floor for a graphical object. The glyph keeps
       --qs-text-muted (5.9:1 light, 7.7:1 dark). Quiet must never mean unreadable.

       The slot keeps its full footprint, so the Actions column is identical on every row
       - which is the actual C7 requirement. */
    background: transparent !important;                  /* vs the inline style= on live actions */
    border: 1px dashed var(--qs-border-strong) !important;
    box-shadow: none !important;                          /* the Limitless button shadow reads as chrome */
    color: var(--qs-text-muted) !important;
    cursor: help;
    opacity: 1;
    position: relative;
}

/* Wakes up under the pointer / on focus, so it is unambiguous which slot the tooltip
   belongs to when four of them sit side by side. */
.qs-act--disabled:hover,
.qs-act--disabled:focus-visible {
    background: var(--qs-surface-2) !important;
    border-color: var(--qs-text-muted) !important;
    border-style: solid !important;
    color: var(--qs-text) !important;
}

/* The glyph has to be visible. Attempt 1 left it at .75 opacity underneath a grey blob,
   i.e. invisible twice over. */
.qs-act--disabled > i {
    opacity: 1;
    color: inherit;
}

/* The old inline label is redundant now that the reason is a real tooltip, but it stays in
   the DOM as the no-JS and screen-reader fallback. */
.qs-act--disabled .qs-act-label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* A hover-only explanation is unreachable by keyboard, so the placeholder is focusable
   (tabindex in qsDisabledAction) and shows the same focus ring as every other control. */
.qs-act--disabled:focus-visible {
    outline: 3px solid var(--qs-focus-ring);
    outline-offset: 2px;
}

/* ── the shared tooltip node, drawn by qs-a11y.js ─────────────────────────────
   ONE node for the whole document, position:fixed, appended to <body>. Fixed
   positioning takes it out of every ancestor's overflow, which is the only way to
   survive `.dt-table-scroll` and the sticky last column. */
.qs-tip {
    position: fixed;
    z-index: 2000;
    max-width: min(22rem, calc(100vw - 2rem));
    padding: var(--qs-space-2) var(--qs-space-3);
    border-radius: var(--qs-radius-sm);
    background: var(--qs-c-slate-900);
    color: var(--qs-inverse-fg);
    font-size: var(--qs-fs-xs);
    font-weight: var(--qs-fw-regular);
    line-height: 1.5;
    text-align: start;
    box-shadow: var(--qs-shadow-3);
    opacity: 0;
    visibility: hidden;
    transition: opacity .12s ease;
    pointer-events: none;   /* the tooltip must never steal the hover that opened it */
}
.qs-tip[data-open="1"] {
    opacity: 1;
    visibility: visible;
}
@media (prefers-reduced-motion: reduce) {
    .qs-tip { transition: none; }
}

/* ============================================================================
   CONTRAST FLOOR — the 24 distinct failures found by checks-layout.js
   (2026-08-20, sweep over 8 pages x 4 viewports x Arabic RTL)

   G1 closed the finance MONEY values. This closes everything else the sweep found,
   because "the money is fixed" is not the same claim as "nothing on the screen is
   under the threshold". Every value below was measured against its REAL rendered
   background, and every replacement was computed to clear its threshold before it
   was written — the ratio is in the comment beside each group.

   Three sources of these failures, worth naming because they recur:

     1. BOOTSTRAP `.bg-*` UTILITIES CARRY !important. site.css already maps
        `.badge.bg-info` and friends onto the semantic tokens, but without
        !important — so `.bg-info { background-color: <cyan-500> !important }` from the
        vendor sheet won every time and the mapping was dead code. White text on
        that cyan measures 2.30:1. This is the single biggest group: 248 badge
        occurrences across the sweep.
     2. VENDOR OUTLINE BUTTONS use the Material 500 ramp as TEXT colour. Tuned for
        fills, 2.78:1 as text on white — the same root cause as G1, on a different
        component.
     3. HAND-PICKED GREYS in the page stylesheets (#999, #adb5bd, #d1d5db,
        #8a94a6, #777) — each one chosen by eye, each one between 1.47:1 and
        4.48:1.

   All light-theme-scoped: qs-theme-dark.css is the single owner of dark, and it
   already re-points these families at the --qs-d-* ramp.
   ============================================================================ */

/* ── 1. solid badges: white text on a vendor Material fill ────────────────────
   measured before -> after, white glyph on the fill:
     bg-info      2.30:1 -> 5.36:1     bg-success   2.78:1 -> 5.07:1
     bg-danger    3.68:1 -> 6.50:1     bg-secondary 4.48:1 -> 5.40:1
   !important is mandatory here: it is fighting Bootstrap's own !important. */
html:not([data-theme="dark"]) .badge.bg-info,
html:not([data-theme="dark"]) .badge.badge-info {
    background-color: var(--qs-info-solid) !important;
    color: var(--qs-on-info) !important;
}
html:not([data-theme="dark"]) .badge.bg-success {
    background-color: var(--qs-success-solid) !important;
    color: var(--qs-on-success) !important;
}
html:not([data-theme="dark"]) .badge.bg-danger {
    background-color: var(--qs-danger-solid) !important;
    color: var(--qs-on-danger) !important;
}
html:not([data-theme="dark"]) .badge.bg-secondary {
    background-color: var(--qs-neutral-solid) !important;
    color: var(--qs-on-neutral) !important;
}
html:not([data-theme="dark"]) .badge.bg-warning {
    background-color: var(--qs-warning-solid) !important;
    color: var(--qs-on-warning) !important;
}

/* ── 3. vendor outline buttons — Material 500 used as text ────────────────────
     btn-outline-success   2.78:1 -> 5.07:1
     btn-outline-danger    3.68:1 -> 6.50:1
     btn-outline-secondary 4.48:1 -> 5.93:1
   The border takes the same value so the button still reads as one object. */
html:not([data-theme="dark"]) .btn-outline-success   { color: var(--qs-success-solid); border-color: var(--qs-success-solid); }
html:not([data-theme="dark"]) .btn-outline-danger    { color: var(--qs-danger-solid);  border-color: var(--qs-danger-solid); }
html:not([data-theme="dark"]) .btn-outline-info      { color: var(--qs-info-solid);    border-color: var(--qs-info-solid); }
html:not([data-theme="dark"]) .btn-outline-warning   { color: var(--qs-warning-subtle-fg); border-color: var(--qs-warning-subtle-fg); }
html:not([data-theme="dark"]) .btn-outline-secondary { color: var(--qs-text-muted);    border-color: var(--qs-border-strong); }
/* On hover these fill, so the text must flip to the on-colour or it disappears. */
html:not([data-theme="dark"]) .btn-outline-success:hover   { background-color: var(--qs-success-solid); color: var(--qs-on-success); }
html:not([data-theme="dark"]) .btn-outline-danger:hover    { background-color: var(--qs-danger-solid);  color: var(--qs-on-danger); }
html:not([data-theme="dark"]) .btn-outline-info:hover      { background-color: var(--qs-info-solid);    color: var(--qs-on-info); }
html:not([data-theme="dark"]) .btn-outline-secondary:hover { background-color: var(--qs-neutral-solid); color: var(--qs-on-neutral); }

/* ── 4. hand-picked greys ─────────────────────────────────────────────────────
   Every one goes to --qs-text-muted (#5b6478), which measures 5.93:1 on white and
   5.24:1 on the palest surface it lands on. Before -> after:
     .addedit-bc-sep               1.47:1 -> 5.93:1   (a 1.47:1 glyph is invisible)
     .dash-updated-sep             2.07:1 -> 5.93:1
     .addedit-bc-link              2.54:1 -> 5.93:1
     .select2-...__placeholder     2.85:1 -> 5.93:1
     .dash-filter-label            3.06:1 -> 5.93:1
     .attn-jump                    3.06:1 -> 5.93:1
     .dataTables_length label      4.47:1 -> 5.48:1
     .qs-filter-toggle-text        4.48:1 -> 5.93:1
   The separators are decorative, but they are TEXT NODES: a screen reader reads
   them and a low-vision user still has to see the crumb trail break. */
html:not([data-theme="dark"]) .addedit-bc-sep,
html:not([data-theme="dark"]) .addedit-bc-link,
html:not([data-theme="dark"]) .dash-updated-sep,
html:not([data-theme="dark"]) .dash-filter-label,
html:not([data-theme="dark"]) .attn-jump,
html:not([data-theme="dark"]) .select2-selection__placeholder,
html:not([data-theme="dark"]) .dataTables_length label,
html:not([data-theme="dark"]) .dataTables_filter label,
html:not([data-theme="dark"]) .qs-filter-toggle-text {
    color: var(--qs-text-muted) !important;
}

/* ── 4b. ...but that muted grey must NOT survive the hover fill ───────────────
   Reported defect: hovering the filter disclosure pill made its own label vanish.
   Two rules in THIS FILE collide on one element. Section 3 flips
   `.btn-outline-secondary:hover` to a SOLID --qs-neutral-solid fill and relies on
   the button's `color` going to the on-colour; section 4 immediately above pins
   the label span to --qs-text-muted with `!important`, which that hover has no
   way to unset. So the fill flipped and the text did not: #5b6478 on #546e7a
   measures ~1.1:1 - invisible. The `icon-filter3` glyph is NOT inside the span,
   so it inherited the on-colour and stayed visible; that asymmetry is exactly
   what the report described - an icon and a count badge with a blank gap between.

   `:not([data-theme="dark"])` does NOT mean "light only": with no explicit toggle
   and a dark OS preference it still matches while the tokens resolve dark
   (--qs-text-muted #a7b2c6 on --qs-neutral-solid #94a3b8, ~1.3:1), so the same
   collision lands there too. The fix is token-based rather than a fixed colour so
   it holds in both token sets.

   The fill is replaced as well. The solid slate slab was the generic outline-button
   treatment inherited by accident, not a deliberate skin for this control, and it
   read heavier than the pills it sits beside. --qs-surface-2 with a primary border
   ring matches `.qs-filter-pill:hover` (qs-list-density.css) one row below, and
   --qs-text is the maximum-contrast text token in BOTH token sets.

   `[aria-expanded="true"]` is included because qs-filters.js toggles that attribute
   (qs-filters.js:106,256) - a pill that snaps back to flat grey while its filter
   row is open reads as closed. */
.qs-filter-toggle:hover,
.qs-filter-toggle:focus-visible,
.qs-filter-toggle[aria-expanded="true"] {
    background-color: var(--qs-surface-2) !important;
    border-color: var(--qs-primary-600) !important;
    color: var(--qs-text) !important;
}

.qs-filter-toggle:hover .qs-filter-toggle-text,
.qs-filter-toggle:focus-visible .qs-filter-toggle-text,
.qs-filter-toggle[aria-expanded="true"] .qs-filter-toggle-text {
    color: var(--qs-text) !important;
}

/* ── 5. the SOFT badge variant, scoped — and why a global version was deleted ──
   FIRST ATTEMPT WAS WRONG. It read four failures on the Orders grid (badge-warning
   3.18:1, badge-success 3.71:1, badge-danger 3.84:1, badge-secondary 4.20:1) and
   concluded `.badge-*` were soft badges app-wide, so it set only their `color`.
   They are NOT: site.css:1717-1722 maps `.badge.badge-*` to a SOLID token fill with
   the matching on-colour text. Setting only the foreground made text and fill the
   same value — measured 1:1, INVISIBLE text on 80 badges across the app. Caught by
   the very next sweep, which is the only reason this is a footnote and not a defect.

   The soft variant lives ONLY inside .qs-ops-module (qs-ops-module.css:87-120
   re-skins those four as tinted pills for the Orders and Bookings grids), so the fix
   belongs there, where the tint actually exists.

   The lesson is the grey blob's lesson again: A CLASS NAME IS NOT A COMPONENT. The
   same `.badge-success` is a solid fill on eight screens and a tinted pill on two,
   and a rule generalised from one screen's reading breaks the other eight. */
html:not([data-theme="dark"]) .qs-ops-module .badge-warning   { color: var(--qs-warning-subtle-fg); }
html:not([data-theme="dark"]) .qs-ops-module .badge-success   { color: var(--qs-success-solid); }
html:not([data-theme="dark"]) .qs-ops-module .badge-danger    { color: var(--qs-danger-solid); }
html:not([data-theme="dark"]) .qs-ops-module .badge-secondary { color: var(--qs-text-muted); }

/* `.qs-act-label` was in the grey list above for the same bad reason: one reading of
   the Clear-Search button (4.48:1 on white) generalised to every action label. Those
   labels sit INSIDE solid coloured buttons, so a fixed grey put #5b6478 on #c62828 —
   1.06:1. It must inherit from its button, always. The Clear-Search button is handled
   by the .btn-outline-secondary rule in group 3 instead. */
html:not([data-theme="dark"]) .qs-act .qs-act-label { color: inherit; }

/* ── 6. the same three groups, DARK ───────────────────────────────────────────
   Groups 1-5 above are light-scoped on purpose, because qs-theme-dark.css is the
   single owner of dark. But the both-themes sweep found dark carrying its own
   failures, and two of them for a reason worth writing down.

   (a) THE VENDOR `.bg-*` UTILITIES ARE THEME-BLIND. `.bg-info` is cyan-500 with
       !important in every theme, so dark inherited the exact same 2.30:1 white-on-cyan
       badge that light had. The dark theme never mapped `.bg-*` at all - it maps
       `.badge-*`. Same fix, dark ramp.

   (b) --qs-d-primary IS TOO LIGHT TO CARRY WHITE TEXT. #4c7dff with white measures
       3.69:1, and it is the fill under the ACTIVE filter chip and `.badge-primary`
       (13px and 12px text, so the 4.5 threshold applies, not 3). It stays as the
       accent for borders and rails - that is a graphical-object use at 3:1 - but any
       surface that carries text gets the darker --qs-c-blue-600 (#2150b8), which is
       6.9:1 with white and still reads as the same blue.
   ---------------------------------------------------------------------------- */
html[data-theme="dark"] .badge.bg-info {
    background-color: var(--qs-info-solid) !important;
    color: var(--qs-on-info) !important;
}
html[data-theme="dark"] .badge.bg-success {
    background-color: var(--qs-success-solid) !important;
    color: var(--qs-on-success) !important;
}
html[data-theme="dark"] .badge.bg-danger {
    background-color: var(--qs-danger-solid) !important;
    color: var(--qs-on-danger) !important;
}
html[data-theme="dark"] .badge.bg-secondary {
    background-color: var(--qs-neutral-solid) !important;
    color: var(--qs-on-neutral) !important;
}
html[data-theme="dark"] .badge.bg-warning {
    background-color: var(--qs-warning-solid) !important;
    color: var(--qs-on-warning) !important;
}

/* (b) text-bearing surfaces built on the dark accent. 3.69:1 -> 6.90:1 with white. */
html[data-theme="dark"] .badge.badge-primary,
html[data-theme="dark"] .qs-ops-module .nav-tabs .nav-link.active,
html[data-theme="dark"] .nav-tabs .nav-link.active .badge,
html[data-theme="dark"] .qs-ops-module .nav-tabs .nav-link.active .badge {
    background-color: var(--qs-c-blue-600) !important;
    color: var(--qs-inverse-fg) !important;
}

/* ============================================================================
   BUTTON CONTENT MUST NOT SPILL OUT OF ITS BUTTON  (2026-08-20)

   REPORTED: at a narrower window the search button rendered as a blue square with
   the Search label hanging out below it, half-covered by the button.

   MEASURED on /AdminProviderFinancial/Refunds at 768-992px:
       <button class="btn btn-primary flex-fill" style="height:44px;">
           <i class="icon-search4 mr-1"></i>Search
       </button>
     box 56x44, content 66x56  ->  the label spilled 13px below the border box.

   TWO CAUSES, both in that one line of markup:
     1. `style="height:44px"` is a HARD height. It was added to line the button up with
        the input beside it, but it also forbids the button from growing, so anything
        that does not fit is simply painted outside.
     2. A `.btn` is display:block/inline-block, so the icon and the label are ordinary
        inline content — once the button gets narrow they break onto a SECOND LINE.
        Two lines of 23px do not fit in 44px.

   This is not one screen: `style="height:44px"` sits on the search and clear buttons of
   31 views. Fixing it in the views would mean 31 edits and would leave the same trap for
   the next person, so it is fixed once here, in the sheet that loads last.

     display:inline-flex   puts the icon and the label on ONE flex line, so the pair can
                           no longer break in two. This is the actual fix; the height
                           rule below only stops the symptom from being hidden.
     height:auto           lets the button grow if its content genuinely needs more room.
                           !important because an inline style attribute cannot be beaten
                           any other way. The 44px floor is NOT lost - `min-height:
                           var(--qs-tap-min)` above still guarantees it, so every button
                           still lines up with its input and still meets the touch target.

   Deliberately NOT `white-space: nowrap`: that would trade a vertical spill for a
   horizontal one on genuinely long labels. Buttons may still wrap - they will just get
   taller instead of leaking.
   ============================================================================ */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: auto !important;   /* vs style="height:44px" hard-coded in 31 views */
}
