/* ============================================================================
   base.css: v2. Reset, element roles, accessibility, utilities, and THE
   REVEAL ENGINE. Consumes tokens.css ONLY (no raw hex/ms, suite-enforced).
   Laws of every stylesheet in this system (DESIGN.md §8/§11, frontend.md):
   - logical properties only (RTL-first);
   - animate transform/opacity only (compositor budget);
   - nothing loops or idles (sole exception: the board-swap skeleton shimmer in
     market.css, a working indicator, hard-disabled under reduced motion);
   - every entrance happens ONCE, then stillness.
   ========================================================================== */

/* ---- Reset ---- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* THE STABLE GUTTER (operator): the scrollbar column is reserved on EVERY page,
     scrolling or not. Without it a short page (/contacts) is a full scrollbar wider than a
     long one, so `100%` resolves 10px larger there and EVERY centered rail — the header
     logo/marks, the back arrow, the .bottomnav, each .container — lands 5px further out; the
     chrome visibly jumps as you move between pages. Measured in the operator's Brave at 1440:
     clientWidth 1440 vs 1430, chrome at 97.6 vs 92.6. Headless Chromium uses OVERLAY
     scrollbars and cannot see this class at all. The classic no-jump fix is `overflow-y:
     scroll`, which paints a dead track on short pages; `stable` reserves the space without
     drawing one, and where it is unsupported the page degrades to today's behaviour.
     .lenis-stopped (below) already relied on it for the modal-lock case. */
  scrollbar-gutter: stable;
  /* THE SCROLL-BAND LAW (app-wide): EVERY scroll-into-view (the browser's own (a focus jump, an
     anchor), HTMX's `show:…:top`, and any scrollIntoView) reserves the STICKY chrome's band, so a
     scrolled-to target can never land UNDER the topbar (the class the licensing wizard once patched
     per-page with its own scroll-margin). One reservation on the scroll container replaces every
     per-target band-aid. var(--topbar-h) IS the bar's box (components.css block-size rides the
     same token; the 80px chrome-literal mirror retired when the landing began ramping
     the bar: a literal twin would tuck anchor targets under the taller bar there). */
  scroll-padding-block-start: calc(var(--topbar-h) + var(--space-3));
}

/* Where the bar is NOT sticky (MEMBER sessions, every width: the liquid-glass nav is the chrome
   and the topbar scrolls away; components.css .topbar), there is no band to reserve, just
   breathing air. */
html:has(.bottomnav) { scroll-padding-block-start: var(--space-3); }

/* THE SWAPPED-REGION ANCHOR LAW. A region marked data-scroll-top gets REPLACED by a taller step, and
   browser scroll anchoring, which exists to hold your place when content above you grows, reads
   that as "everything above shifted" and yanks the scroll down (to the page BOTTOM on a phone, the
   register type-pick's whole bug). It is the wrong instinct here: a step advance is a NEW screen, not
   a reflow of the one you are reading, and app.js glides the region to the top itself. Excluding the
   region from anchor selection leaves the scroll still through the swap, so that glide starts from
   where the user actually was, no cut to the bottom first. */
[data-scroll-top] { overflow-anchor: none; }

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto; /* animated anchor scrolling is motion too (§11) */
  }
}

body {
  margin: 0;
  /* the shell column: <main> grows, so the footer meets the viewport bottom
     even on SHORT pages (login), no stray body-background strip below it */
  min-block-size: 100dvh;
  display: flex;
  flex-direction: column;
  /* THE UNIVERSAL PAGE FLOOR (the owner's unification): EVERY page,
     dashboard and every feature, rides THIS one paint; a feature sheet must NOT
     re-paint the floor (only a section that must cover a hero re-uses the SAME token). */
  background: var(--navy);
  color: var(--white);
  font-family: var(--font-ui);
  font-size: var(--text-body);
  font-weight: var(--w-body);
  line-height: var(--leading);
  overscroll-behavior-y: none; /* app-feel: no rubber-banding the page */
}

main {
  flex: 1 0 auto; /* the page body owns the leftover height (the shell column above) */
  /* THE CHROME ANCHOR: main is the containing block the floating chrome measures from
     (the community .cgrad key; the .profile-topbar / .seller-chrome rows) — the back
     arrow itself flows in its own band since the ruling (components.css
     .app-back) — and flow-root PINS main's top at the topbar's bottom; without it a
     page-top negative-margin bleed (.subx-page) would COLLAPSE through and drag main
     (band and chrome alike) up behind the bar. Children's own pull-ups still bleed
     (a negative margin inside a BFC renders above the box edge). */
  position: relative;
  display: flow-root;
}

/* ---- NUMBER FIELDS ARE PLAIN TYPEABLE WELLS (the unified-labels law, the owner's
   simplification) ---- the native ±1 spin buttons are gone APP-WIDE (they
   crowded the well + collided with resting labels/adornments; was: hidden-at-rest,
   shown-on-focus). ONE element-role rule: the scoped copies in components.css are
   retired (one canonical source). Keyboard arrows still step via the `step` attr. */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield; /* the Firefox arm (ignored elsewhere) */
}

/* THE ONE PLACEHOLDER VOICE (operator, the AA/AAA placeholder call): every real
   placeholder app-wide reads the muted text tier at FULL opacity — without a rule the UA's
   dim graytext fails AA on the dark wells, and Firefox additionally mutes ::placeholder via
   a ~0.54 UA opacity, so the tier alone is not enough. ONE element-role rule (the
   spin-button precedent above): the per-sheet copies that restated this exact pair are
   retired; a surface that wants a LOUDER voice (the assistant prompt's --ink) still
   overrides locally. The .field system's placeholder=" " float-armers are unaffected
   (a space paints nothing). */
::placeholder {
  color: var(--slate-on-dark);
  opacity: 1;
}

/* LENIS smooth-scroll core CSS (the library's own recommended rules; js/app.js armSmoothScroll).
   `scroll-behavior: auto !important` while smooth = Lenis owns scrolling so the CSS `scroll-behavior:
   smooth` (above) can't fight its rAF lerp; `[data-lenis-prevent]` contains overscroll on the nested
   messages-thread log; `lenis-stopped` locks the page under a viewport overlay (lenis.stop()). */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
/* scrollbar-gutter keeps the 10px scrollbar COLUMN reserved while stopped;
   without it, every lenis.stop()/start() cycle (the register entrance hold,
   a viewport overlay) collapses and restores the column, shifting centered
   content ~5px sideways at the release (owner-caught at the entrance's end;
   headed-traced: clientWidth 1440→1430 on the exact release frame). */
.lenis.lenis-stopped { overflow: hidden; scrollbar-gutter: stable; }

/* SMOOTHNESS: the liquid-glass nav KEEPS its blur while scrolling (the user's call:
   the drop-and-solidify swap read as a distracting color flash, worse than the blur cost). The
   bar pays its way instead: the blur stays TRIMMED to 14px on the nav's own compositor layer
   (components.css), and the pane is small fixed chrome, never a full-viewport overlay. Re-open
   only against a measured scroll-jank regression. */

/* CUSTOM SCROLLBAR: minimal, CREAM thumb, TRANSPARENT track, SHOWS ONLY ON SCROLL (the flecto.io
   auto-hide). Driven by Lenis's own .lenis-scrolling class on <html> (js/app.js armSmoothScroll, the
   smooth-scroll law): the thumb is invisible at rest and fades to cream while scrolling. App-wide +
   locked-in (base.css is always loaded). The messages-thread log opts to `scrollbar-width: none` in its
   own rule (unaffected). Reduced-motion keeps this (a thumb is not vestibular motion). */
* {
  scrollbar-width: thin;                              /* Firefox: slim */
  scrollbar-color: transparent transparent;           /* Firefox: hidden at rest */
}
html.lenis-scrolling, html.lenis-scrolling * {
  scrollbar-color: var(--white) transparent;          /* Firefox: the PRIMARY-TEXT-tone thumb while scrolling (light on dark, dark on beige) */
}
/* the webkit scrollbar pseudo honors physical width/height only (not logical props): side-agnostic
   thickness, fine in RTL (the bar sits on the inline-start edge either way). */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background-color: transparent;                       /* hidden at rest */
  border-radius: var(--r-full);
  border: 3px solid transparent;                       /* inset → a slim pill, not a fat bar */
  background-clip: padding-box;
  transition:
    background-color var(--motion-state) var(--ease-standard),
    box-shadow var(--motion-state) var(--ease-standard);
}
/* show ON SCROLL: cream fill + a FAINT inset navy outline so the cream thumb stays visible over the
   content in BOTH themes (the thumb rides the primary-text tone; the inset outline rides the
   floor tone, so the pair contrast-inverts with the theme). */
html.lenis-scrolling ::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover {
  background-color: var(--white);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--navy-deep) 22%, transparent);
}

h1,
h2,
h3,
p {
  margin: 0;
}

/* role defaults: components re-role deliberately, never ad-hoc */
h1,
h2,
h3 {
  font-weight: var(--w-heading);
  line-height: var(--leading-heading);
}

/* feature titles: Rubik tracking tightened to -2%, TITLES ONLY (h1 = every page's feature
   headline); body/labels keep normal tracking, and the a11y-sp* spacing modes outrank this. */
h1 {
  letter-spacing: -0.02em;
}

img {
  max-inline-size: 100%;
  block-size: auto;
}

a {
  color: var(--gold-bright); /* on dark surfaces (the default canvas) */
}

/* on light bands, links wear the designated link color: gold-bright would
   sit at 3:1 on cream (AA fail); --blue is the token whose JOB is links */
.section-light a:not(.btn) {
  color: var(--blue);
}

[x-cloak] {
  display: none !important;
}

[hidden] {
  /* the UA's [hidden] rule has NO priority: any author `display` (e.g. a grid
     form) silently defeats the attribute. This makes `hidden` mean hidden,
     app-wide, like x-cloak above. */
  display: none !important;
}

/* ---- Accessibility ---- */
::selection {
  background: var(--gold);
  color: var(--on-accent);
}

/* THE ONE canonical keyboard-focus ring (WCAG 2.0 2.4.7 Focus Visible, AA, IS 5568; a launch
   gate). The ring rides `outline`, NOT `box-shadow`.

   WHY (the defect this closes, measured on rendered pixels): a box-shadow ring is silently deleted
   by ANY rule that owns the element's resting box-shadow. Two shapes of that, both real:
     (a) no `:focus-visible` rule at all: `.btn-primary`'s resting `box-shadow` is (0,1,0) in a
         LATER sheet than this (0,1,0) rule, so the rest state simply wins;
     (b) the component HAS a `:focus-visible` rule and a LATER rule at equal-or-higher specificity
         re-skins its box-shadow: the same `.theme-toggle` rings on `/` and does not on
         `/dashboard`, because `dashboard.css`'s `.dash-greet-meta .theme-toggle` glass re-skin ties
         its specificity and wins on source order.
   `outline` is the only property no resting/decorative style claims, so expressing the ring there
   makes it un-annullable BY CONSTRUCTION rather than by per-surface discipline. The box-shadow
   below stays as the brand GLOW; where a component also keeps its own `--focus-ring` box-shadow the
   two merge into ONE crisp band (verified zoomed), never a doubled ring. A focus rule that
   suppresses this outline is a defect; the walker in tests/test_focus_visibility.py holds that
   line with a named, reasoned exception list.

   OFFSET 2px, matching the app's OWN `html.a11y-focus` recipe below (one recipe, not two): flush at
   0 the ring merges into a same-hue filled control: the gold `.btn-primary` on dark and its brown
   beige twin both read as "the button grew" rather than "the button is focused". The 2px gap puts
   the page floor between control and ring, which is also what carries the ≥3:1 contrast
   (DESIGN.md §10) the tokens are tuned for. */
:focus-visible {
  outline: 3px solid var(--focus-edge);
  outline-offset: 2px;
  box-shadow: var(--focus-ring-lit); /* the ring composed WITH the gold glow: keyboard focus reads LIT */
  border-radius: var(--r-sm);
}

.skip-link {
  position: absolute;
  inset-block-start: var(--space-2);
  inset-inline-start: var(--space-2);
  z-index: 100;
  background: var(--navy-4);
  color: var(--white);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-sm);
  transform: translateY(-200%);
}

.skip-link:focus-visible {
  transform: none;
}

/* sr-only: off-screen for sighted users, read by AT. The ONE canonical copy lives here in the
   always-loaded base sheet (not a page sheet): the badge (topbar/bottom_nav, every member page),
   the thread speaker prefixes and the composer file input rely on it on pages whose page-sheet is
   NOT profile/search, so a page-sheet-local copy left the text VISIBLE there. */
.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: calc(var(--space-1) * -1);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- Layout utilities ---- */
.container {
  max-inline-size: 1080px;
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.muted {
  color: var(--slate);
}

.section-title {
  /* no underline bar: the gold active mark lives in the TOPBAR alone
     (one wayfinding language; titles stay clean) */
  font-size: var(--text-h1);
  font-weight: var(--w-display);
  text-align: center;
}

@media (min-width: 768px) {
  .section-title {
    /* desktop: band titles step up to ~1.3× h1 (≈42px), clearly above the
       body, a notch below the display-scale card headlines (derived from
       the role token, so the ladder retunes as one) */
    font-size: calc(var(--text-h1) * 1.3);
  }
}

/* ---- The signature gradient border (DESIGN.md §4: BOLD is the brand) ----
   --grad-surface lets any component keep the ring while changing its fill. */
.grad-border {
  --grad-surface: var(--navy-2);
  background:
    linear-gradient(var(--grad-surface), var(--grad-surface)) padding-box,
    var(--grad-gold) border-box;
  border: 3px solid transparent;
  border-radius: var(--r);
}

.grad-border-hairline {
  border-width: 2px;
}

.grad-border-bold {
  border-width: 4px;
}

/* Gradient-ink display text (ONE word of a headline, never body copy).
   Worst-case ink #FFB54D on navy = 9.1:1 (measured), AA with room. */
.text-grad {
  background: var(--grad-gold);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ============================================================================
   THE REVEAL ENGINE: the Phantom/Apple/Duolingo scroll choreography, made
   LAWFUL (DESIGN.md §11: opacity, translate ≤32px, scale ≥0.95; one-shot;
   nothing replays on scroll-up). One pattern serves BOTH the page-load beat
   and every scroll entrance:

   - Markup opts in with data-reveal="0..5" (the stagger order) and an
     optional data-reveal-from="rise|fade|zoom|start|end" variant: Phantom's
     variety without Phantom's parallax.
   - js/app.js observes each element (IntersectionObserver) and adds
     .is-revealed ONCE on first intersection, then unobserves.
   - The hidden initial state exists ONLY when scripting is enabled AND the
     user accepts motion. No JS → content visible. Reduced motion → content
     visible. Nothing to unbreak, ever.
   ========================================================================== */
@media (scripting: enabled) and (prefers-reduced-motion: no-preference) {
  [data-reveal]:not(.is-revealed) {
    opacity: 0;
    transform: translateY(var(--reveal-travel)); /* rise, the default */
    /* TRANSIENT layer hint, exactly as web.dev prescribes: it applies only
       while the element is ABOUT to animate and expires with this selector
       the moment .is-revealed lands, no permanent layers, no JS bookkeeping */
    will-change: transform, opacity;
  }

  [data-reveal-from="rise-lg"]:not(.is-revealed) {
    transform: translateY(var(--reveal-travel-lg)); /* display-scale entrances */
  }

  [data-reveal-from="fade"]:not(.is-revealed) {
    transform: none;
  }

  [data-reveal-from="zoom"]:not(.is-revealed) {
    transform: scale(0.95); /* the lawful floor */
  }

  /* slides are tuned for THIS RTL page: "start" enters from the inline start
     (the right), "end" from the inline end (no logical translate exists in CSS) */
  [data-reveal-from="start"]:not(.is-revealed) {
    transform: translateX(var(--reveal-travel));
  }

  [data-reveal-from="end"]:not(.is-revealed) {
    transform: translateX(calc(var(--reveal-travel) * -1));
  }

}

[data-reveal] {
  transition:
    opacity var(--motion-reveal) var(--ease-emphasized),
    transform var(--motion-reveal) var(--ease-emphasized);
}


/* ===== THE FEATURE-HEADER ENTRANCE: the ONE shared header choreography =====
   A head wears `data-reveal-group` and its own children ride the staged rise, so a page
   opts in with ONE attribute and carries no per-line numbers (they had drifted per page:
   0/1/1/3 on /projects, a lone 0 on /today, none at all on half the features).
   THE HIDDEN STATE LIVES ON THE CONTAINER, which is why this is CSS and not JS: the
   container ships in the server HTML, so children are hidden at FIRST PAINT. A JS pass
   that stamped data-reveal per child was tried and removed the same day (app.js records
   it) — the attributes landed ~140ms late, after the observer had already revealed the
   head, and the entrance simply never played. :nth-child owns the stagger; a child may
   still carry data-reveal-from to pick its own travel. */
@media (scripting: enabled) and (prefers-reduced-motion: no-preference) {
  [data-reveal-group]:not(.is-revealed) > * {
    opacity: 0;
    transform: translateY(var(--reveal-travel));
    will-change: transform, opacity;
  }
  [data-reveal-group]:not(.is-revealed) > [data-reveal-from="zoom"] { transform: scale(0.95); }
  [data-reveal-group]:not(.is-revealed) > [data-reveal-from="fade"] { transform: none; }
  [data-reveal-group]:not(.is-revealed) > [data-reveal-from="rise-lg"] {
    transform: translateY(var(--reveal-travel-lg));
  }
}
[data-reveal-group] > * {
  transition:
    opacity var(--motion-reveal) var(--ease-emphasized),
    transform var(--motion-reveal) var(--ease-emphasized);
}
/* the same --stagger schedule the numbered rules below use; the last step repeats so a
   long head degrades to "all together at the end" instead of jumping the queue with no
   delay rule at all */
[data-reveal-group] > :nth-child(2) { transition-delay: calc(var(--stagger, 90ms) * 1); }
[data-reveal-group] > :nth-child(3) { transition-delay: calc(var(--stagger, 90ms) * 2); }
[data-reveal-group] > :nth-child(4) { transition-delay: calc(var(--stagger, 90ms) * 3); }
[data-reveal-group] > :nth-child(5) { transition-delay: calc(var(--stagger, 90ms) * 4); }
[data-reveal-group] > :nth-child(n + 6) { transition-delay: calc(var(--stagger, 90ms) * 5); }

/* the --stagger fallback is the load-bearing resilience pattern (var() fallbacks
   are exempt from the tokens-only law): a token-sheet skew can never silently
   collapse the whole stagger choreography to 0s again. */
[data-reveal="1"] { transition-delay: calc(var(--stagger, 90ms) * 1); }
[data-reveal="2"] { transition-delay: calc(var(--stagger, 90ms) * 2); }
[data-reveal="3"] { transition-delay: calc(var(--stagger, 90ms) * 3); }
[data-reveal="4"] { transition-delay: calc(var(--stagger, 90ms) * 4); }
[data-reveal="5"] { transition-delay: calc(var(--stagger, 90ms) * 5); }

/* ---- Theme shift (the Phantom background morph, js/app.js armThemeShifts):
   a band darkens while it holds the viewport center, then lightens back.
   Color is §11-allow-listed; the duration token collapses under reduced
   motion, so the morph degrades to an instant, meaning-free color switch. ---- */
[data-theme-shift] {
  /* fallbacks are LOAD-BEARING (MDN: an undefined var() makes the declaration
     `unset` → transition: all 0s → an INSTANT snap). They guard against any
     stylesheet-version skew ever reproducing that bug. */
  transition:
    background-color var(--motion-morph, 2000ms) var(--ease-emphasized, ease),
    color var(--motion-morph, 2000ms) var(--ease-emphasized, ease);
}

[data-theme-shift].theme-dark {
  background-color: var(--navy);
  color: var(--white);
}

/* ---- HTMX swap lifecycle (frontend.md): exits faster than entries ---- */
.htmx-swapping {
  opacity: 0;
  transition: opacity var(--motion-state) var(--ease-standard);
}

.htmx-added {
  opacity: 0;
  transform: translateY(8px);
}

.htmx-settling {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity var(--motion-swap) var(--ease-emphasized),
    transform var(--motion-swap) var(--ease-emphasized);
}

/* ---- THE TIER-1 MICRO-WIN (DESIGN.md §8): a gold-glow pulse on EVERY completed
   action, the constant "the app responds to ME" dopamine baseline the whole
   celebration ladder rests on. Fired by app.js pulseSuccess(el); glow-led with a
   tiny scale lift (compositor-only). pulseSuccess returns early under reduced
   motion, so this never runs there (the meaning rides the text/state change). ---- */
.is-pulsing { animation: pulse-success var(--motion-micro) var(--ease-spring); }
@keyframes pulse-success {
  0% { box-shadow: 0 0 0 color-mix(in srgb, var(--gold-bright) 0%, transparent); transform: scale(1); }
  45% { box-shadow: var(--glow-gold); transform: scale(1.02); }
  100% { box-shadow: 0 0 0 color-mix(in srgb, var(--gold-bright) 0%, transparent); transform: scale(1); }
}

/* ============================================================================
   THE ACCESSIBILITY ENGINE (IS 5568 / WCAG AA, the a11y-menu build;
   docs/frontend.md "The accessibility engine"). Every effect is a ROOT CLASS
   the base.html allow-list stamps from the `a11y` cookie (zero flash) and the
   dashboard a11yMenu island toggles. Structural effects live HERE (always
   loaded); TOKEN-level effects (motion collapse, contrast boost, readable
   font) live in tokens.css beside the themes they must compose with.
   `!important` is deliberate on the override class of an a11y layer: a user
   preference must beat any component style (the a11y-widget convention).
   ========================================================================== */

/* ---- text-size scale (WCAG 1.4.4 resize text): the rem type scale rides the
   root font-size, so text scales app-wide while px paddings/hit-areas hold
   their geometry (targets only ever grow with their text). Four steps to
   150%; browser zoom stacks on top for the full 200%. ---- */
html.a11y-fs1 { font-size: 112.5%; }
html.a11y-fs2 { font-size: 125%; }
html.a11y-fs3 { font-size: 137.5%; }
html.a11y-fs4 { font-size: 150%; }

/* ---- text spacing (WCAG 1.4.12: sp1 IS the criterion's exact metrics;
   sp2 the stronger step for low-vision/dyslexia comfort). Inherited from
   body; components with their own line-height keep their heading rhythm. ---- */
html.a11y-sp1 :is(p, li, dd, dt, a, button, label, td, th, h1, h2, h3, h4, h5, h6, span) {
  letter-spacing: 0.12em;
  word-spacing: 0.16em;
}
html.a11y-sp1 :is(p, li, dd, dt, td, th) { line-height: 1.75; }
html.a11y-sp1 p:not(:last-child) { margin-block-end: 2em; }
html.a11y-sp2 :is(p, li, dd, dt, a, button, label, td, th, h1, h2, h3, h4, h5, h6, span) {
  letter-spacing: 0.18em;
  word-spacing: 0.24em;
}
html.a11y-sp2 :is(p, li, dd, dt, td, th) { line-height: 2; }
html.a11y-sp2 p:not(:last-child) { margin-block-end: 2em; }

/* ---- REFLOW UNDER SCALED TEXT (WCAG 1.4.10 Reflow · 1.4.12 Text Spacing) ----
   A surface whose ONE-ROW law is a deliberate design call (DESIGN.md §13) holds that row on real
   devices because its compression clamps are VIEWPORT-driven. Those clamps never fire when this
   menu scales the CONTENT instead of the viewport: measured, the dashboard's 3-tab strip then
   dragged the document to 505px inside a 390px phone, so the chrome and the greeting could never
   be on screen together. While a text-size or text-spacing preference is on, the one-row law
   yields: the strip wraps and its labels fold. Every other reflow fix is unconditional (a track
   that can shrink, a row that can wrap) because those are inert at rest; the two below are not, so
   they are scoped to the preference that needs them, and THIS is their one home. Specificity
   (0,3,2) deliberately outranks the components.css `main[data-dashboard] .dash-tabs` (0,2,1) and
   the landing.css `.hero-ctas .btn` (0,2,0) rules they override. */
html:is(.a11y-fs1, .a11y-fs2, .a11y-fs3, .a11y-fs4, .a11y-sp1, .a11y-sp2)
  main[data-dashboard] .dash-tabs {
  flex-wrap: wrap;
}
html:is(.a11y-fs1, .a11y-fs2, .a11y-fs3, .a11y-fs4, .a11y-sp1, .a11y-sp2)
  main[data-dashboard] .dash-tabs a {
  white-space: normal;
}
/* the landing hero's CTA labels: `nowrap` floors the whole hero COLUMN at the button's min-content
   width, and pushed the anonymous acquisition page to 409px inside a 390px phone. */
html:is(.a11y-fs1, .a11y-fs2, .a11y-fs3, .a11y-fs4, .a11y-sp1, .a11y-sp2) .hero-ctas .btn {
  white-space: normal;
}

/* ---- highlight links (WCAG 1.4.1 use-of-color support): every anchor gains
   a real underline so links never ride color alone. ---- */
html.a11y-links a[href]:not(.btn):not(.feature-btn):not(.app-back):not([class*="-btn"]):not([class*="-key"]) {
  text-decoration: underline !important;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.18em;
}

/* ---- highlight headings (structure made visible, cognitive/low-vision): the
   real document outline surfaces as a calm gold underline on every heading. ---- */
html.a11y-headings :is(h1, h2, h3, h4, h5, h6) {
  text-decoration: underline 2px;
  text-decoration-color: var(--gold-bright);
  text-underline-offset: 0.22em;
}

/* ---- focus emphasis (WCAG 2.4.7 amplified): a thick always-on ring for
   EVERY focus, not only :focus-visible; keyboard/motor users who asked for
   it get an unmissable position marker. ---- */
html.a11y-focus :focus {
  outline: 3px solid var(--gold-bright) !important;
  outline-offset: 2px !important;
}

/* ---- grayscale (color-blindness / visual-noise reduction): the filter sits
   on the ROOT element deliberately: the Filter Effects spec EXEMPTS the root
   from the "filter creates a containing block for fixed descendants" rule, so
   the fixed bottom-nav/modals keep their viewport anchoring (a body-level
   filter would break them). ---- */
html.a11y-gray { filter: grayscale(1); }

/* ---- big cursor (low-vision / motor tracking): one large high-contrast
   arrow everywhere; !important beats component cursor:pointer (the a11y
   layer wins); the keyword fallback is mandatory per the CSS spec. ---- */
html.a11y-cursor,
html.a11y-cursor * { cursor: url("/static/img/a11y/cursor-big.png") 7 4, auto !important; }

/* ---- the reading MASK (the UserWay grammar): two fixed scrims dim everything
   but a clear horizontal band riding the pointer; the island manages the pair
   with the same rAF glide as the guide bar. ---- */
.a11y-mask-top,
.a11y-mask-bottom {
  position: fixed;
  inset-inline: 0;
  background: color-mix(in srgb, var(--navy-deep) 55%, transparent);
  z-index: 11990;
  pointer-events: none;
  will-change: transform;
}
.a11y-mask-top { inset-block-start: 0; block-size: 100vh; transform: translateY(-100vh); }
.a11y-mask-bottom { inset-block-start: 0; block-size: 100vh; }

/* ---- the reading guide (focus ruler for cognitive/low-vision reading): a
   fixed gold rule the a11yMenu island glides under the pointer (transform-
   only, rAF-throttled); pointer-events:none so it never blocks a tap. ---- */
.a11y-guide-bar {
  position: fixed;
  inset-inline: 0;
  inset-block-start: 0;
  block-size: 4px;
  background: var(--gold-bright);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--navy-deep) 65%, transparent),
    0 2px 12px color-mix(in srgb, var(--navy-deep) 45%, transparent);
  z-index: 12000;
  pointer-events: none;
  will-change: transform;
}

/* ---- motion off (the class twin of prefers-reduced-motion, WCAG 2.2.2/
   2.3.3): the token collapse lives in tokens.css; here the scroll-reveal
   hidden state is force-released so content is simply VISIBLE (the media-gate
   grammar, as a class), and smooth scrolling falls back to native (the island
   retires Lenis). ---- */
html.a11y-motion [data-reveal],
html.a11y-motion [data-reveal-group] > * {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
  animation: none !important;
}
html.a11y-motion .lenis { scroll-behavior: auto !important; }

/* ---- QUICK NAVIGATION (ניווט מהיר, WCAG 2.4.1 Bypass Blocks as a persistent,
   first-in-tab-order surface): server-rendered at the very top of body when the
   preference is on; STICKY in normal flow (pushes content, never covers it).
   NAMING LAW (live-caught): the ROOT marker class is `a11y-quicknav` (the cookie
   token, stamped on <html>), so the STRIP component class MUST be a different
   token: a bare `.a11y-quicknav` rule would restyle the ROOT (html became a
   flex row; body shrank to fit). Component classes never reuse a marker token. ---- */
.a11y-quicknav-strip {
  position: sticky;
  inset-block-start: 0;
  z-index: 1300;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-2) clamp(var(--space-3), 4vw, var(--space-5));
  background: var(--navy-4);
  border-block-end: 2px solid var(--gold-bright);
  font-size: var(--text-caption);
}
.a11y-quicknav-name {
  color: var(--slate-on-dark);
  font-weight: var(--w-medium);
}
.a11y-quicknav-strip a {
  display: inline-flex;
  align-items: center;
  min-block-size: 36px;
  padding-inline: var(--space-2);
  border-radius: var(--r-full);
  color: var(--gold-bright);
  font-weight: var(--w-medium);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}
.a11y-quicknav-strip a:hover { background: var(--state-hover); }
.a11y-quicknav-strip a:focus-visible { box-shadow: var(--focus-ring); }

/* ---- KEYBOARD NAVIGATION (ניווט מקלדת): the UNMISSABLE focus frame, a thick
   double ring on EVERY focus (stronger than the a11y-focus ring), plus
   scroll-margin so a keyboard jump lands the target away from the viewport
   edges. The heading/landmark shortcuts (Alt+arrows / Alt+1-3) arm in app.js
   while the class is on, modifier combos only (no screen-reader single-key
   collisions; the WCAG 2.1.4 class avoided entirely). ---- */
html.a11y-kbd :focus {
  outline: 4px solid var(--gold-bright) !important;
  outline-offset: 3px !important;
  box-shadow: 0 0 0 8px color-mix(in srgb, var(--gold-bright) 30%, transparent) !important;
}
html.a11y-kbd :is(main, header, h1, h2, h3, h4, a, button, input, select, textarea, [tabindex]) {
  scroll-margin-block: 22vh;
}
