/* ============================================================================
 * S8 Insights — Brand colour/token set (NO font import)
 * ============================================================================
 * THE single source of the brand colour + structural tokens (--s8-* colours,
 * lines, status, easings, radii, spacing, layout). Fonts-free on purpose.
 *
 * Consumed two ways:
 *   - The public matchup page links this file directly (colours) alongside
 *     aeo-tokens.css (Source Serif 4 + IBM Plex Mono) + matchup.css, so it pulls
 *     only the AEO fonts, not the old Google Fonts payload.
 *   - brand-tokens.css @imports this file, then adds the font-family vars, the
 *     font @import, the engine/tag badge palettes, light-mode overrides, reset,
 *     and utilities. Un-migrated pages keep linking brand-tokens.css and get
 *     these colours through that import.
 *
 * Deliberately omits the --s8-font-* family variables (the matchup surface uses
 * --aeo-font-*). Edit colour values HERE only; do not re-declare them elsewhere.
 * ============================================================================ */

:root {
  /* ── Surface · navy stack ── */
  --s8-navy:        #0A1628;
  --s8-navy-2:      #0F2138;
  --s8-navy-3:      #162B44;
  --s8-card-bg:     #0F2138;

  /* ── Accent · gold scale ── */
  --s8-gold:        #C9A961;
  --s8-gold-light:  #E8C776;
  --s8-gold-dark:   #9E7F3E;

  /* ── Accent · terminal purple (Elite+ / S8 Terminal tier identity) ──
     Gradient runs deep → terminal. Ink is the text colour ON the purple:
     terminal #7C4FC0 vs ink #F0E8FF measures 4.73:1 (WCAG AA for normal
     text); the deep end measures 9.08:1. Do not lighten --s8-terminal or
     dim the ink without re-checking that ratio. */
  --s8-terminal:       #7C4FC0;
  --s8-terminal-deep:  #4A2D7A;
  --s8-terminal-ink:   #F0E8FF;
  --s8-terminal-line:  rgba(180, 140, 220, 0.2);

  /* ── Text ── */
  --s8-cream:       #FAFAF7;
  --s8-slate:       #8A9BA8;
  --s8-slate-dim:   #7c88a0;

  /* ── Lines ── */
  --s8-line:        rgba(201, 169, 97, 0.18);
  --s8-line-strong: rgba(201, 169, 97, 0.32);

  /* ── Status ── */
  --s8-green:       #4ADE80;
  --s8-amber:       #F59E0B;
  --s8-red:         #F26D6D;
  /* Warning FILL role: stays bright amber in BOTH themes. In light mode
     --s8-amber re-roles to a dark text amber (#8A5100) for AA on cream, which
     would fail as a banner fill under dark ink (3.0:1); banner backgrounds use
     this token instead (#06101E ink on #F59E0B = 8.9:1). */
  --s8-warn-fill:   #F59E0B;

  /* ── Elevation ──
     Shadow is the one part of the visual language that cannot simply be
     re-coloured between themes: on a dark ground a black shadow reads as
     depth, on paper it reads as dirt, and the alpha that looks right in one is
     wrong in the other. Both values are therefore declared per theme rather
     than derived. --s8-shadow-edge is the vertical edge a pinned table column
     casts over the columns scrolling beneath it. */
  --s8-shadow-card: 0 10px 28px rgba(0, 0, 0, 0.22);
  --s8-shadow-edge: 6px 0 8px -6px rgba(0, 0, 0, 0.45);

  /* Hairline around a piece of MEDIA (a flag, a club crest): deliberately
     neutral rather than gold, because an image is not a brand surface. It has
     to be the opposite of the page or it disappears, and a crest with a white
     field needs it most, so it flips with the theme like any other ink. */
  --s8-media-ring: rgba(255, 255, 255, 0.16);

  /* Gold WASH: the faint tint behind a promoted panel (the Elite+ callout).
     Pages used to carry these as literals because no token existed, which
     meant every one of them kept the DARK gold in light mode and painted a
     warm patch on cool paper. Derived from the identity gold rgb(201,168,76);
     it differs from --s8-gold above only until the C1-a swap lands, and at
     these alphas the difference is not perceptible. */
  --s8-gold-wash:        rgba(201, 168, 76, 0.06);
  --s8-gold-wash-strong: rgba(201, 168, 76, 0.12);

  /* ── Animation easings ── */
  --ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);

  /* ── Radii ── */
  --s8-radius-sm:   6px;
  --s8-radius-md:   10px;
  --s8-radius-lg:   14px;
  --s8-radius-pill: 999px;

  /* ── Spacing scale (multiples of 4) ── */
  --s8-space-1: 4px;
  --s8-space-2: 8px;
  --s8-space-3: 12px;
  --s8-space-4: 16px;
  --s8-space-5: 24px;
  --s8-space-6: 32px;
  --s8-space-7: 48px;
  --s8-space-8: 64px;

  /* ── Layout ── */
  --s8-max-width: 1320px;

  /* ── Type scale ─────────────────────────────────────────────────────────
     Added 2026-08-11 (responsive pass 1). Until now NO type token existed
     anywhere in the repo — a grep for --fs-*, --type-*, --font-size-* and
     --step-* returned nothing, and all 1,086 font-size declarations across
     the in-scope surfaces were px literals at 40 distinct values.

     These values are NOT a new design. They are the existing clusters with
     their half-pixel neighbours folded in (12/12.5, 13/13.5, 14/14.5,
     15/15.5, 9/9.5, 10/10.5, 11/11.5), so adopting a token is a visual no-op
     to within half a pixel and desktop does not move. The scale exists so the
     NEXT size added to this codebase has somewhere to come from, and so a
     floor can be stated rather than assumed.

     THE FLOOR. --s8-fs-micro is 10px and --s8-fs-nano (9px) exists only to
     name what is already shipping on the public pages: 515 of those 1,086
     declarations are at or below 11px. The member portal already holds an
     11px floor (board v4 D6, verified 0 sub-11px text); the marketing pages
     do not. Closing that gap is a design decision, not a responsive fix, and
     is logged in docs/ui-foundations.md for the founder rather than taken
     here. Do not introduce a NEW size below --s8-fs-micro. */
  --s8-fs-nano:    9px;    /* legacy public eyebrows only — do not extend */
  --s8-fs-micro:  10px;    /* the floor for new work */
  --s8-fs-label:  11px;    /* app-surface floor (board v4 D6) */
  --s8-fs-meta:   12px;
  --s8-fs-sm:     13px;
  --s8-fs-body:   14px;
  --s8-fs-body-lg:15px;
  --s8-fs-base:   16px;    /* also the iOS no-zoom floor for form controls */
  --s8-fs-lg:     18px;
  --s8-fs-xl:     20px;
  --s8-fs-2xl:    22px;
  --s8-fs-3xl:    26px;
  --s8-fs-4xl:    32px;
  --s8-fs-5xl:    42px;
  --s8-fs-6xl:    58px;

  /* ── Breakpoints ────────────────────────────────────────────────────────
     Added 2026-08-11. Custom properties cannot be used inside a @media
     condition, so these are DOCUMENTATION AND A CONTRACT, not something the
     media queries can interpolate. They exist because the in-scope surfaces
     carried 28 distinct breakpoint values with four off-by-one duplicate
     pairs (599/600, 639/640, 767/768, 899/900), three separate house styles
     (public 900/700/440, gated 1024/768/640, engine-run its own seven), and
     NOTHING below 400px against a 320px worst case.

     The six below are NOT invented round numbers. Each is the value the
     existing rules already cluster on, so folding a rule onto its canonical
     neighbour moves the boundary by at most a pixel:
       phone-sm  440px  the 400/440 cluster (16 rules)
       phone     600px  the 519/520/560/599/600 cluster (27 rules)
       phone-lg  640px  the 639/640 cluster — where engine-run swaps its chip
                        rail for a native select
       tablet    768px  the 700/720/760/767/768 cluster
       desktop   900px  the 819/840/899/900 cluster — where the public nav
                        collapses to a hamburger
       wide     1100px  the 980/1024/1100 cluster

     Write max-width queries at (value - 0.02px) so a max and a min pair can
     never both match at the boundary, e.g. @media (max-width: 899.98px). That
     off-by-one is why 599/600, 639/640, 767/768 and 899/900 all exist as
     separate values today.

     NOTHING IN THIS CODEBASE HAS A RULE BELOW 400px and the matrix's worst
     case is 320px. There is deliberately no token for it: 320-439 is not a
     breakpoint, it is the floor every layout must already survive.

     docs/ui-foundations.md maps all 28 existing values onto this set. */
  --s8-bp-phone-sm:  440px;
  --s8-bp-phone:     600px;
  --s8-bp-phone-lg:  640px;
  --s8-bp-tablet:    768px;
  --s8-bp-desktop:   900px;
  --s8-bp-wide:     1100px;

  /* ── Touch and control sizing ───────────────────────────────────────────
     --s8-tap is the WCAG 2.5.8 / iOS floor for anything a finger must hit.
     --s8-ctl-h is the visual height of a dense control; where it is smaller
     than --s8-tap the control must still carry a --s8-tap hit area (padding,
     or a ::before that expands the target without moving the box). */
  --s8-tap:      44px;
  --s8-ctl-h:    38px;
  --s8-ctl-h-lg: 44px;

  /* ── Z-index scale ──────────────────────────────────────────────────────
     Added 2026-08-11. The surfaces carried 68 hardcoded z-index values across
     22 tiers with no token: -1, 0, 1, 2, 3, 5, 8, 60, 100, 200, 300, 490,
     500, 900, 999, 1000, 8500, 8999, 9000, 9999, 99998, 99999.

     WHY IT CLIMBED THAT HIGH. A decorative grain layer (body::after,
     pointer-events:none, opacity ~0.022) sits at 9999 on five public pages.
     Nothing on those pages can paint above it without going to five digits,
     which is why the floating theme toggle is at 8500, the cookie banner at
     9000, and the age gate at 99999. The grain has no reason to be above
     anything; moving it to the bottom is what makes the rest of this scale
     possible. Full rationale and the per-declaration mapping:
     docs/ui-foundations.md. */
  --s8-z-below:      -1;   /* a scrim a surface draws for itself */
  --s8-z-base:        0;   /* decorative atmosphere, grain, background media */
  --s8-z-raise:       1;   /* content lifted over its own background */
  --s8-z-film:       50;   /* the full-viewport grain film: above ALL page
                              content, below every piece of chrome. It cannot
                              live at --s8-z-base, because hero, footer and
                              step-number blocks carry z-index:1 and a
                              positioned layer at 0 paints BENEATH them —
                              measured, that removed the noise from most of the
                              homepage at a max channel delta of 126. */
  --s8-z-sticky-cell: 2;   /* sticky table header / pinned column */
  --s8-z-sticky-corner:3;  /* where a sticky header and pinned column cross */
  --s8-z-dock:       10;   /* a component's own sticky sub-bar or tab rail */

  /* A TAB'S OWN INTERNAL STACK (2026-08-26, ratified). The run board is a
     full-page takeover, so everything inside it stacks in THAT context and
     never against the page. These four order one row's controls against each
     other, and they come from the tab mockups, whose raw 3 / 5 / 6 / 7 carried
     the ordering but not a name.

     --s8-z-tab-tip SHARES ITS VALUE with --s8-z-sticky-corner. That is not a
     collision to tidy away: the two can never meet, because one is inside the
     takeover and the other is a table's pinned header on a page underneath it.
     Same number, different stacking context, different meaning. */
  --s8-z-tab-tip:     3;   /* a tooltip inside a tab body */
  --s8-z-tab-menu:    5;   /* a row control's menu */

  /* THE CONTROL SITS ABOVE THE MENU IT OPENED, and that is deliberate. The
     menu fans out from BEHIND the button — the button stays whole and on top,
     so it can be pressed again to close what it opened, and the menu appears
     to come out from under it rather than to cover it. Ordering these the
     "obvious" way round puts the first menu item over the button and the
     control becomes unpressable while its own menu is open. Do not swap. */
  --s8-z-tab-ctl:     6;   /* the control, above the menu it opened */
  --s8-z-tab-form:    7;   /* a form popped from that control, above both */
  --s8-z-chrome:    100;   /* the app header / the public nav bar */
  --s8-z-skip:      150;   /* skip link: above chrome, below anything modal */
  --s8-z-dropdown:  300;   /* menus and popovers anchored to a trigger */
  --s8-z-sheet:     400;   /* drawers and bottom sheets */
  --s8-z-modal:     500;   /* modals and full-page takeovers, and their scrim */
  --s8-z-modal-pop: 600;   /* a popover opened from inside a modal */
  --s8-z-toast:     700;   /* transient notifications */
  --s8-z-system:    800;   /* the view-as bar and other operator chrome */
  --s8-z-consent:   900;   /* the cookie banner and its settings link */
  --s8-z-gate:     1000;   /* the 18+ age gate: blocks everything, always top */
}


/* ── Site-wide light palette · "Daylight" ─────────────────────────────────
 * THE single definition of the light theme for the --s8-* vocabulary. Every
 * light surface on the site resolves through this block: colour-only consumers
 * (the server-rendered matchup page) load brand-colors.css directly, and
 * brand-tokens.css imports it, so the app, admin and public --s8 pages all
 * inherit from here. brand-tokens.css adds only the BRIDGE names on top; it no
 * longer restates these values, because two copies of one palette drift.
 *
 * NOT AN INVERSION. Until 2026-08-11 this block flipped the navy stack onto a
 * warm cream stack (#FAF8F2 / #F3EFE5 / #ECE7DA), measured at hue 89-91deg in
 * OKLCH against the dark identity's 267deg. That is a different brand, not the
 * same brand in daylight, and it produced the three symptoms the founder
 * named: beige panels that read as washed rather than raised, mono data that
 * sits dull on cream, and a gold that had to be desaturated so far to survive
 * on cream (#7E5F16, 81% of the dark gold's chroma) that gold TEXT was quietly
 * re-roled to navy blue on the public pages and gold stopped being the brand.
 *
 * The stack below is the dark stack's own hue, lightened: 264-265deg against
 * 267deg. Elevation direction is unchanged from the shipped light mode (page
 * lightest, raised surfaces progressively deeper) so nothing that already
 * assumes it has to be re-authored.
 *
 * Measured, 2026-08-11 (WCAG 2.1 relative luminance; --s8-navy-3 is the
 * deepest surface small text sits on, so it is the binding case):
 *   ink #0A1628      15.17:1 on navy-3 · 17.66:1 on navy
 *   slate            6.79:1  on navy-3 · 7.91:1  on navy
 *   slate-dim        5.74:1  on navy-3 · 6.68:1  on navy
 *   gold             4.72:1  on navy-3 · 5.50:1  on navy   (was 4.81 / 5.59)
 *   gold-light       4.51:1  on navy-3
 *   green/red/amber/blue  5.18 / 5.23 / 5.40 / 5.31 on navy-3
 * navy-4 is structural (scrollbar thumb, deep inset); no small text sits on it.
 * -------------------------------------------------------------------------- */
:root[data-theme="light"] {
  color-scheme: light;

  /* Surface · daylight stack (page → deepest). Same hue family as the navy
     stack, not a warm-paper substitute. */
  --s8-navy:        #FBFCFE;
  --s8-navy-2:      #F1F4FA;
  --s8-navy-3:      #E7EBF3;
  --s8-navy-4:      #D9DFEB;
  --s8-card-bg:     #F1F4FA;

  /* Text · brand navy as ink (the --s8-cream NAME is kept for compatibility) */
  --s8-cream:       #0A1628;
  --s8-slate:       #42506A;
  --s8-slate-dim:   #4F5B70;

  /* Accent · gold, INK role. Darkening a gold costs chroma, and chroma is what
     stops it reading as olive: the retired #7E5F16 kept 81% of the dark gold's
     chroma, this keeps 92% at hue 80deg (pulled warm, away from olive-yellow).
     It is the most chromatic gold that still clears AA on the page ground, so
     do not lighten it without re-measuring 4.5:1 against --s8-navy-3.
     Filled gold controls do NOT use these: see --s8-gold-fill* in
     brand-tokens.css, which is a separate role with its own floors. */
  --s8-gold:        #886000;
  --s8-gold-light:  #8C6300;   /* 4.51:1 on navy-3 — the AA ceiling; no room above it */
  --s8-gold-dark:   #755100;   /* 5.99:1 on navy-3 */

  /* Lines · ink at low alpha. Dark draws hairlines in gold on navy at
     1.28-1.30:1 against their surface; these land at 1.39-1.41:1 and 1.93-1.96:1,
     a touch stronger because a light surface needs more line weight to hold a
     table together. */
  --s8-line:        rgba(10, 22, 40, 0.16);
  --s8-line-strong: rgba(10, 22, 40, 0.30);

  /* Status · hues preserved, darkened to clear AA on the deepest text surface */
  --s8-green:       #0F7038;
  --s8-amber:       #8A5100;
  --s8-red:         #B23125;
  --s8-blue:        #1F5BBF;

  /* Elevation · tinted with the navy rather than neutral black, and much
     lighter: the dark shadows (0.22 / 0.45 black) sat on paper as grey smears,
     most visibly on the pinned column of the /app/intelligence table. */
  --s8-shadow-card: 0 1px 2px rgba(10, 22, 40, 0.05), 0 10px 28px rgba(10, 22, 40, 0.07);
  --s8-shadow-edge: 6px 0 8px -6px rgba(10, 22, 40, 0.18);
  --s8-media-ring:  rgba(10, 22, 40, 0.18);

  /* Derived from the LIGHT gold ink, and lifted slightly: the same alphas that
     read as a wash on navy are nearly nothing on paper. */
  --s8-gold-wash:        rgba(136, 96, 0, 0.05);
  --s8-gold-wash-strong: rgba(136, 96, 0, 0.10);

  /* ── Shared-chrome bridge, light half ─────────────────────────────────────
     MOVED HERE 2026-08-11 (responsive pass 1). These five names are what the
     shared public nav and footer are written against, and they used to live
     only in brand-tokens.css and public-theme.css. Neither is loaded by every
     page that CARRIES that chrome, and the failure is silent: the markup reads
     var(--text2, #8A9BA8), so on a page without them the fallbacks resolve to
     the DARK literals — correct in dark, and dark-grey-on-white in light.

     That is exactly what /elite-plus-terms was doing. It links only this file
     (correctly: the 2026-08-09 migration dropped brand-tokens.css to escape the
     legacy Cormorant/DM Mono font payload), so in light mode its whole footer
     measured 1.89:1 and its responsible-gambling line and BeGambleAware link,
     which the constitution forbids removing, were unreadable — on the one page
     an anonymous prospect must read before waiving a cooling-off right.

     They belong here because the chrome they style is shared, so any page may
     carry it, and this file is the only sheet every such page is guaranteed to
     load. Values are identical to public-theme.css's light block and to
     brand-tokens.css's bridge, which both still declare them later in the
     cascade; nothing else moves. The DARK half stays as the markup fallbacks
     and the seven per-page :root blocks, because those already agree and
     restating them here would fight the marketing pages' own palettes. */
  --ink:   10, 22, 40;
  --text:  #0A1628;
  --text2: #42506A;
  --dim:   #4F5B70;
  --gold:  #886000;
  --gi:    #886000;
}

/* ── Paired theme assets ─────────────────────────────────────────────────────
 * Static backup of the display rules shared/site-theme.js injects at runtime,
 * so paired dark/light artwork degrades correctly even if the controller
 * script never runs (blocked, failed load, JS off → dark artwork shows). */
.s8-theme-asset--light { display: none !important; }
:root[data-theme="light"] .s8-theme-asset--dark { display: none !important; }
:root[data-theme="light"] .s8-theme-asset--light { display: block !important; }

/* ── Body scroll lock behind a blocking overlay ──────────────────────────────
 * THE SOURCE OF THIS RULE. shared/public-theme.css carries a byte-identical
 * copy, for the same reason as the zoom floor below: the seven marketing pages
 * load that sheet and never this one.
 *
 * WHAT IT FIXES. Measured across the overlay lifecycle pass: the page scrolled
 * 300px behind EVERY blocking overlay on the site — the bankroll modals, all
 * three tracker modals, the match-intel bottom sheet (1141px behind it), and
 * the 18+ AGE GATE, which is compliance furniture a visitor could simply
 * scroll past and read the site behind. Exactly one surface in the codebase
 * locked scroll (the run takeover, in JS); every other one never implemented it.
 *
 * WHY :has() AND NOT JAVASCRIPT. A JS lock means editing the open/close path of
 * five separate modal implementations that share no code, and each would then
 * own its own scroll-restoration bug. This is one declarative rule that keys on
 * the state the markup already sets. :has() has been baseline since 2023.
 *
 * WHY overflow:hidden AND NOT position:fixed. The position:fixed technique has
 * to store and restore scrollY by hand, and getting that wrong is what jumps
 * the reader to the top on close — a defect this pass also measured. Leaving
 * the document where it is means there is nothing to restore.
 *
 * ADDING AN OVERLAY: give it [data-s8-lock-scroll] rather than extending this
 * list. The named selectors are the surfaces that predate the convention. */
html:has(.modal-overlay.open),
html:has(.br-modal-overlay.open),
html:has(#ageOverlay:not(.hidden)),
html:has([data-s8-lock-scroll]),
body:has(.modal-overlay.open),
body:has(.br-modal-overlay.open),
body:has(#ageOverlay:not(.hidden)),
body:has([data-s8-lock-scroll]) {
  overflow: hidden;
  /* iOS momentum scrolling can still carry the document under a locked body;
     touch-action stops the gesture reaching it at all. */
  touch-action: none;
}


/* The match-intel date picker is a bottom SHEET only below 640px; above that
   it is a small popover anchored to its trigger, and locking the page behind a
   popover strands a desktop reader — measured as a Critical when this selector
   was unconditional. The breakpoint is the component's own (match-intel.css:503). */
@media (max-width: 639.98px) {
  html:has(.mi-datepop:not([hidden])),
  body:has(.mi-datepop:not([hidden])) { overflow: hidden; touch-action: none; }
}

/* ── iOS text-entry zoom floor ───────────────────────────────────────────────
 * THE SOURCE OF THIS RULE. shared/public-theme.css carries a byte-identical
 * copy because the two sheets are the site's two independent roots — the seven
 * marketing pages load public-theme.css and never this file. Keep them in step;
 * netlify/tests/input-zoom-guard.test.mjs asserts they are identical, and the
 * responsive harness re-measures every focusable control at runtime.
 *
 * WHAT IT FIXES. iOS Safari zooms the page when a focused text control renders
 * below 16px, and does not zoom back. Guards for this already existed in four
 * places (app-shell.css:64, intelligence.html, record.html, pricing.html), all
 * written as the bare type selector `input, textarea, select` — specificity
 * (0,0,1). EVERY input rule in this codebase is a class, (0,1,0) or higher, so
 * those guards lost the cascade in every place that mattered and had never
 * done anything. Sign in, sign up, password reset, the /record filters, the
 * Elite+ waitlist field, the whole personal tracker, the bankroll modals, the
 * stake calculator, the intelligence scope selects and the engine-run player
 * search and sort were all zooming.
 *
 * HOW IT WINS WITHOUT !important. `:root:root` is (0,2,0), so with the element
 * this rule is (0,2,1) — above every offending rule in the repo, the most
 * specific of which is (0,1,1). Specificity beats source order, so it does not
 * matter which sheet loads last. The exclusion list is wrapped in :where(),
 * which contributes ZERO specificity, so the list can grow without the rule
 * silently getting stronger.
 *
 * WHY (pointer: coarse) AND NOT A WIDTH. The zoom is a property of the input
 * method, not the viewport: a width query would miss a touch laptop and would
 * change desktop for no reason. Under a fine pointer nothing here applies and
 * desktop stays pixel-identical.
 *
 * The excluded types either cannot receive typed text or are buttons whose
 * label size is a deliberate design choice. */
@media (pointer: coarse) {
  :root:root input:not(:where(
    [type="checkbox"], [type="radio"], [type="range"], [type="color"],
    [type="file"], [type="submit"], [type="button"], [type="image"], [type="hidden"]
  )),
  :root:root select,
  :root:root textarea {
    font-size: var(--s8-fs-base, 16px);
  }
}
