/**
 * Gymnastics Plus — Site-wide Base Layer (gp-base)
 *
 * WS-2 Phase 1 (ADDITIVE). First-party base reset that will REPLACE the Kadence
 * parent's base CSS at the Phase 2 cutover. Unlike gp-core.css (which is scoped to
 * body.gp-app and only enqueued on page-template / gymnast_profile routes), this is
 * loaded UNSCOPED on EVERY page so non-template routes (default Pages, blog single,
 * archives, search, 404, the Kadence-block pages) get a first-party base.
 *
 * Authored to MIRROR Kadence's base (kadence/assets/css/all.min.css + Kadence's
 * system-font defaults, theme typography mods are unset) so that — with the parent
 * still enqueued in Phase 1 — this layers on top with NO visible change on pages
 * that already render styled. Pages that currently render bare (search, 404, the
 * abandoned podcast/founding-members block pages) may GAIN this base; that is the
 * expected, signed-off improvement, not a regression.
 *
 * Font is Poppins, SELF-HOSTED as of F3 (2026-07-26, docs/plans/2026-07-26-app-native-ux.md
 * "F3. Type and shape") — see the @font-face block immediately below. This replaces the WS-2
 * Phase 5 Google Fonts CDN load (gp-fonts-poppins in inc/gp-asset-enqueue.php); self-hosting
 * means no third-party fetch inside the native app-door WKWebView shell (a CSP/offline win)
 * and collapses what had drifted into three separate Google Fonts requests (the site-wide
 * handle, a redundant duplicate in gp-template-functions.php, and several page templates'
 * own <link> tags) down to ONE load path. The system stack remains the fallback below.
 *
 * BASE ONLY. No component/layout/page styles — those live in gp-core.css and the
 * per-page stylesheets.
 *
 * @package Gymnastics_Plus
 */

/* Poppins @font-face (F3) — self-hosted, latin subset only, 400/500/600/700, NO italic (the
   slant lives in the logo per DESIGN.md "Type — Poppins"). Files live in assets/fonts/ (theme-
   relative from this file: assets/css/ -> ../fonts/). font-display: swap avoids invisible text
   while the file loads; the 400/600 weights also get a <link rel=preload> from
   inc/gp-asset-enqueue.php (gp_fonts_preload) since they are what most first paints need. */
@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/poppins-v24-latin-400.woff2') format('woff2');
}
@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../fonts/poppins-v24-latin-500.woff2') format('woff2');
}
@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../fonts/poppins-v24-latin-600.woff2') format('woff2');
}
@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../fonts/poppins-v24-latin-700.woff2') format('woff2');
}

/* Site-wide font-family token (F3) — the ONE place the Poppins + fallback stack is spelled
   out. body{} below and gp-core.css's --gp-font-sans/--gp-font-display both resolve through
   this so there is a single stack to edit. No italic anywhere in the stack (see @font-face
   above) — the fallback system fonts don't get italicized either, by design. */
:root {
    --gp-font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
        Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}

/* Type ramp (F3, DESIGN.md "Type — Poppins" + docs/plans/2026-07-26-app-native-ux.md "F3. Type
   and shape") — the Apple-style scale, in REM so browser text-scaling is respected (px shown
   in comments is the visual reference only, per MOBILE_DESIGN_SYSTEM.md §3/§12). This is the
   canonical scale for anything that isn't a heading (captions, labels, badges, the body copy
   below); the --gp-h1..h6 tokens right after it resolve ONTO these same rungs so there is
   exactly one scale in the codebase, not two that can drift apart. */
:root {
    --gp-t-xl:   2.125rem;  /* 34px */
    --gp-t-lg:   1.75rem;   /* 28px */
    --gp-t-md:   1.375rem;  /* 22px */
    --gp-t-sm:   1.25rem;   /* 20px */
    --gp-t-body: 1.0625rem; /* 17px */
    --gp-t-sub:  0.9375rem; /* 15px */
    --gp-t-foot: 0.8125rem; /* 13px */
    --gp-t-cap:  0.6875rem; /* 11px */
}

/* Heading-scale tokens (WS-2.5 Task 4; sizes repointed onto the --gp-t-* ramp above by F3,
   2026-07-26) — the structured Poppins ramp, defined ONCE here and consumed by the bare h1–h6
   below AND by per-surface heading classes (portal/profile/single) so the scale lives in one
   place. Names are collision-free (taken: --gp-text-*, --gp-font, --gp-gray-*). Colors stay
   here for gp-base's own headings; surface COLOR unification is deferred to the --gp-color-*
   semantic layer (Task 6).
   F3 mapping (descending, hierarchy rule intact — MOBILE_DESIGN_SYSTEM.md §3): h1=t-xl(34),
   h2=t-lg(28, unchanged), h3=t-md(22, was 24), h4=t-sm(20, was 22), h5=t-body(17, was 20 —
   now the SAME size as body copy, differentiated by its 600 weight + darker ink, not size),
   h6=t-sub(15, was 18). --gp-t-foot/--gp-t-cap (13/11) are intentionally NOT mapped to a
   heading — they are the caption/label rungs, used directly where needed. */
:root {
    --gp-h1-size: var(--gp-t-xl);   --gp-h1-line: 1.15; --gp-h1-weight: 700; --gp-h1-tracking: -0.02em;  --gp-h1-color: #1A202C;
    --gp-h2-size: var(--gp-t-lg);   --gp-h2-line: 1.2;  --gp-h2-weight: 700; --gp-h2-tracking: -0.015em; --gp-h2-color: #1A202C;
    --gp-h3-size: var(--gp-t-md);   --gp-h3-line: 1.25; --gp-h3-weight: 600; --gp-h3-tracking: -0.01em;  --gp-h3-color: #1A202C;
    --gp-h4-size: var(--gp-t-sm);   --gp-h4-line: 1.3;  --gp-h4-weight: 600; --gp-h4-tracking: -0.01em;  --gp-h4-color: #2D3748;
    --gp-h5-size: var(--gp-t-body); --gp-h5-line: 1.35; --gp-h5-weight: 600; --gp-h5-tracking: normal;   --gp-h5-color: #2D3748;
    --gp-h6-size: var(--gp-t-sub);  --gp-h6-line: 1.4;  --gp-h6-weight: 600; --gp-h6-tracking: normal;   --gp-h6-color: #4A5568;
}

/* SPACING SCALE (UB coordinated token slice 2026-08-06, WS-14) — promoted, not invented.
   This scale ALREADY EXISTS in the theme and is already consumed 194 times. It was simply never
   given a home: css/gp-single.css declares it in its own :root (10 rungs), and css/gp-membership.css
   (74 uses), css/gp-onboarding.css (38 uses) and css/gp-profile.css (5 uses) consume it while
   declaring NOTHING. Those 117 declarations resolve today only because gp-single.css's :root leaks
   globally whenever that stylesheet happens to be enqueued. On any route where it is not, all 117
   silently collapse to zero spacing. Moving the scale here — gp-base is the one stylesheet loaded on
   every route and both doors — closes that hole.
   VALUES ARE COPIED EXACTLY from gp-single.css:53-62, so every existing consumer is byte-identical
   and this slice cannot move a shipped pixel. In rem rather than px so browser text-scaling carries
   layout with it, matching the --gp-t-* ramp above (px shown for reference only).
   The per-file :root forks are NOT deleted here: retiring them is a separate coordinated de-fork,
   and while both exist they declare the same values so there is nothing to diverge. */
:root {
    --gp-space-1:  0.25rem;  /* 4px  */
    --gp-space-2:  0.5rem;   /* 8px  */
    --gp-space-3:  0.75rem;  /* 12px */
    --gp-space-4:  1rem;     /* 16px */
    --gp-space-5:  1.25rem;  /* 20px */
    --gp-space-6:  1.5rem;   /* 24px */
    --gp-space-8:  2rem;     /* 32px */
    --gp-space-10: 2.5rem;   /* 40px */
    --gp-space-12: 3rem;     /* 48px */
    --gp-space-16: 4rem;     /* 64px */
    /* Two rungs the marketing surface needs that the app scale never did: section-scale vertical
       rhythm. Same 4px grid, continuing the existing naming. */
    --gp-space-20: 5rem;     /* 80px */
    --gp-space-28: 7rem;     /* 112px */
}

/* Display rung (WS-14 plan decision 6, UB coordinated token slice 2026-08-06) — the ONE size
   ABOVE h1, for oversized marketing display type (the public homepage hero). It sits deliberately
   OUTSIDE the --gp-t-* app ramp: that ramp is the Apple/iOS scale for product UI and tops out at
   34px on purpose, while a marketing hero needs to scale with the viewport. So this is the single
   exception, and it is fluid rather than a fixed rung — the alternative the plan explicitly warned
   against is a per-page clamp() forked into feature CSS, which calcifies.

   Structure mirrors --gp-h1-* exactly (size/line/weight/tracking/color) so consumers use one
   familiar shape. clamp() floor 40px (phone, one clear step above h1's 34) → ceiling 72px
   (desktop), crossing over around a 1040px viewport. In rem so browser text-scaling is respected;
   the vw term keeps a long headline from overflowing a 390px phone (the failure mode a fixed
   display size has).

   ADDITIVE + dark-safe by construction: nothing in the theme consumed a display size before this,
   so every existing surface is byte-identical, and --gp-display-color resolves THROUGH
   --gp-h1-color, which the [data-theme="dark"] block below already repoints to
   --gp-color-text-strong. No dark-mode entry is needed here, and none may be added — that would
   fork the heading ink into two places that can drift. */
:root {
    --gp-display-size: clamp(2.5rem, 1.6rem + 4.5vw, 4.5rem); /* 40px → 72px */
    --gp-display-line: 1.05;
    --gp-display-weight: 700;
    --gp-display-tracking: -0.03em;
    /* Resolves to --gp-color-text, NOT --gp-h1-color. The two are different blacks in light mode:
       --gp-h1-color is the raw literal #1A202C (blue-leaning, outside the --gp-gray-* ramp) while
       --gp-color-text is #171717. Measured in the browser, pointing display at --gp-h1-color made
       the largest element on the page a visibly different black from every heading under it. That
       #1A202C/#171717 reconciliation is a known open item in the WS-2.5 notes; until it is settled
       this rung takes the semantic token so a page using display + headings has ONE ink.
       Dark-safe: --gp-color-text is already redefined under [data-theme="dark"]. */
    --gp-display-color: var(--gp-color-text);
}

/* ============================================================================
   COLOR TOKEN SYSTEM (WS-2.5 Task 6, Phase 1) — 3-tier, site-wide, dark-ready.
   Defined HERE in gp-base (loaded on EVERY page), not gp-core.css (which is
   scoped to .gp-app templates and only enqueued on some routes). ADDITIVE: in
   this phase NOTHING consumes these tokens, so light mode is byte-identical —
   this just establishes the seam. Tier-1 values mirror gp-core.css :root exactly;
   on .gp-app pages both files define them to the SAME values (no conflict).
   Later phases delete the ~24 local --gp-gold forks and repoint surfaces onto
   the Tier-2 semantics; the #1A202C/#171717 text reconciliation + the
   --gp-text-primary collision are folded in at the consume phase (Phase 3).
   ============================================================================ */
:root {
    /* Tier 1 — PRIMITIVES (raw palette; the ONLY place raw hex lives) */
    --gp-white: #ffffff;
    --gp-black: #000000;
    --gp-gray-50: #fafafa;
    --gp-gray-100: #f5f5f5;
    --gp-gray-200: #e5e5e5;
    --gp-gray-300: #d4d4d4;
    --gp-gray-400: #a3a3a3;
    --gp-gray-500: #737373;
    --gp-gray-600: #525252;
    --gp-gray-700: #404040;
    --gp-gray-800: #262626;
    --gp-gray-900: #171717;
    --gp-gold: #DFB859;
    --gp-gold-hover: #c9a64e;
    /* The desktop portal frame's near-black (UB 2026-08-12). A NEW primitive rather than a reach
       for an existing rung, because the owner locked this exact value on the prototype
       (docs/plans/2026-07-27-desktop-look-frame-locked.md, "Tokens in play": --chrome-bg #0e0f13)
       and nothing already in the ramp is it: --gp-gray-900 is #171717, the dark canvas is #0b0c0e
       and the dark surface is #16181d. It sits deliberately between the last two so the chrome
       reads as a frame around the dark canvas rather than as another panel on it. Consumed only
       through --gp-chrome-bg at the bottom of this block. */
    --gp-chrome-black: #0e0f13;
    --gp-gold-light: rgba(223, 184, 89, 0.1);
    --gp-gold-text: #7a5c14;       /* text-safe gold: the brand gold darkened until it clears AA as TEXT on a light
                                      surface (6.23:1 on white, 5.72:1 on the gray-100 canvas). --gp-gold itself is a
                                      FILL color and measures 1.89:1 / 1.73:1 as ink, far under the 4.5:1 body-text
                                      floor. Consumed only through --gp-color-accent-text below. */
    --gp-gold-text-hover: #5f470f; /* one clear step darker for the hover/active state (8.77:1 / 8.04:1) */
    --gp-purple-400: #a855f7;
    --gp-purple-500: #7c3aed;
    --gp-purple-600: #6d28d9;
    --gp-success: #22c55e;
    --gp-error: #ef4444;
    --gp-warning: #f59e0b;
    --gp-info: #3b82f6;
    /* THE BRAND GRADIENT, and its two hues promoted to primitives.
       Owner decision 2026-08-31: the gradient behind a gymnast's initials is the
       one he wants on buttons and on any large block of purple, "just the purple
       is okay but when its a large block... it needs to be the gradient".

       These two hues were already the platform's most-used gradient (profile
       initials and cover, Community, Settings, messaging, notifications) and the
       only one carrying a documented accessibility decision. They were locked
       inside --gp-avatar-ph-bg, which is named for a PLACE; a colour used on
       buttons and surfaces needs a name for its JOB, so they are lifted here and
       every gradient below is composed from them. Nothing is re-picked. */
    --gp-brand-violet: #667eea;                 /* the pale, periwinkle end */
    --gp-brand-plum:   #764ba2;                 /* the deep end white text sits on */

    --gp-avatar-ph-bg: linear-gradient(315deg, var(--gp-brand-violet) 0%, var(--gp-brand-plum) 100%); /* placeholder-avatar gradient (matches the profile .gp-cover-photo). Angle rotated 135deg to 315deg per the owner decision of 2026-07-26 (gymnast-portal record, amendment 3): same two brand colors, but the white initials now sit over the darker end of the sweep, taking their contrast from 3.66:1 (below AA) to 6.37:1. The colors themselves are unchanged and deliberately so. */
    --gp-media-ph-bg: radial-gradient(120% 95% at 27% 13%, rgba(255, 255, 255, 0.10), transparent 60%),
                      linear-gradient(158deg, #2c3039, #1a1d23 55%, #23262d); /* placeholder-MEDIA fill (G1 token slice, 2026-07-27) — the dark sheen a video card paints before, or instead of, a real poster frame. Sits here beside --gp-avatar-ph-bg because it is the same kind of thing: a fixed decorative placeholder gradient whose raw hex belongs in Tier-1. Deliberately NOT theme-aware — a video poster reads as a dark letterbox in light and dark alike, which is why docs/portal-shell/gp-shell.css declares --media once and never overrides it in its dark block. Components consume the Tier-2 --gp-color-media below, never this. */

    /* Tier 2 — SEMANTICS (--gp-color-*; the seam for one-place color + dark mode).
       Every value maps to a Tier-1 primitive, so light mode is byte-identical. */
    --gp-color-bg: var(--gp-gray-100);
    --gp-color-surface: var(--gp-white);
    --gp-color-surface-2: var(--gp-gray-50);
    --gp-color-text: var(--gp-gray-900);
    /* UB (2026-07-27, G1) — CONTRAST FIX, light only. This was --gp-gray-500 (#737373) and it
       failed WCAG AA as body text on most of the surfaces it actually sits on: 4.35:1 on the page
       canvas --gp-color-bg, 4.23:1 on --gp-color-bg-profile, and 3.76:1 on both --gp-color-surface-3
       and --gp-color-skeleton (it cleared 4.5:1 only on pure white and on --gp-color-surface-2).
       The F6 state kit's .gp-state__body is one of the surfaces that inherited the failure.
       Repointed one rung down the EXISTING primitive ramp rather than introducing a new grey:
       #666666 is the true arithmetic minimum but clears --gp-color-surface-3 by only 0.06, and it
       would be a brand-new Tier-1 primitive, which is a DESIGN.md slice of its own. --gp-gray-600
       is already in the ramp and clears every surface with real headroom.
       Measured, after: bg 7.17:1, surface 7.81:1, surface-2 7.49:1, surface-3 6.20:1,
       skeleton 6.20:1, bg-profile 6.97:1, highlight-bg 7.54:1. Hierarchy is intact — the primary
       ink --gp-color-text is still 17.93:1 on white against this token's 7.81:1, so "muted" still
       reads a clear step quieter, it just also reads. DARK IS UNTOUCHED: #cfd4db already measures
       11.92:1 on --gp-color-surface and was never the problem. */
    --gp-color-text-muted: var(--gp-gray-600);
    --gp-color-text-inverse: var(--gp-white);
    --gp-color-border: var(--gp-gray-200);
    --gp-color-border-strong: var(--gp-gray-300);
    --gp-color-primary: var(--gp-gray-900);        /* neutral default action */
    --gp-color-on-primary: var(--gp-white);
    --gp-color-accent: var(--gp-gold);             /* gold accent — a FILL/CTA intent (backgrounds, dots, bars, borders, icons) */
    --gp-color-accent-hover: var(--gp-gold-hover);

    /* UB (2026-07-26, look-lab finding R0) — accent as TEXT. The gold above is a fill color, and
       the site-wide `a` rule below plus a handful of other rules were painting READ TEXT with it:
       1.89:1 on a white surface, 1.73:1 on the gray-100 page canvas, against WCAG AA's 4.5:1 for
       body text. Light is the platform default, so every text link on the website was failing AA.
       Splitting the role (fill gold stays --gp-color-accent; ink gets its own semantic) is the fix
       the new portal shell already shipped: docs/portal-shell/gp-shell.css defines --accent-text
       for exactly this reason, and this token IS that token under the theme's Tier-2 naming
       (--gp-color-accent-text ↔ the shell's --accent-text, same #7a5c14 light value, same flip
       back to plain gold in dark, where gold measures 10.38:1 on canvas / 9.42:1 on surface and
       was never the problem). Any rule that paints glyphs a member READS consumes this; anything
       painting a surface, dot, bar, border or icon keeps --gp-color-accent. */
    --gp-color-accent-text: var(--gp-gold-text);
    --gp-color-accent-text-hover: var(--gp-gold-text-hover);

    --gp-color-on-accent: var(--gp-gray-900);
    --gp-cta: var(--gp-gold);                       /* KEY CTA = gold */
    --gp-cta-on: var(--gp-gray-900);
    /* ---- THE BRAND PURPLE'S FOUR JOBS (owner slice, 2026-08-31) ------------
       He chose the avatar gradient for buttons and for any large block of purple,
       and flat purple for the small marks. Four semantics rather than one, because
       the same two hues need different treatment depending on how much ROOM they
       have and on WHAT SITS ON THEM. Size decides gradient or flat; what sits on
       top decides which gradient.

       IT DOES NOT COMPETE WITH GOLD, which was his condition. Gold stays the one
       action per screen; these are surfaces, progress and the occasional primary
       button on a screen where no gold is present. Two filled buttons asking to be
       pressed is the failure mode, not the colour. */

    /* A SURFACE with nothing on it, or with text placed deliberately at the dark
       end (the avatar initials). Full sweep, both hues visible. */
    --gp-color-brand-surface: linear-gradient(135deg, var(--gp-brand-violet) 0%, var(--gp-brand-plum) 100%);

    /* A BUTTON, where white text spans the whole width and cannot be placed.
       MEASURED: white on the pale end is 3.66:1 and FAILS AA; across this variant
       it runs 4.73:1 to 6.37:1 and passes everywhere. The negative stop slides the
       pale end off the element, so both of the owner's hues stay literally in the
       declaration and nothing is re-picked to buy the contrast. */
    --gp-color-brand-action: linear-gradient(135deg, var(--gp-brand-violet) -80%, var(--gp-brand-plum) 100%);
    --gp-color-brand-action-on: var(--gp-white);

    /* SMALL MARKS — flat purple, NOT the gradient. Owner ruling 2026-08-31, on
       seeing the gradient rendered on a 7px progress dash: "for the small purple
       like this i think would look better just as purple". Below roughly 40px a
       two-stop sweep has no room to read AS a sweep; it only muddies a shape that
       wanted to be one clean colour, and it reads as a rendering artefact rather
       than as an intention.

       Flat, but still HIS purple: this is one END of his gradient, never a new hue
       mixed out of it. Which end depends on the theme, because neither carries on
       both. Measured against the real canvases (#f5f5f5 light, #0b0c0e dark): plum
       is 5.84:1 on light but only 3.07:1 on dark; violet is 5.35:1 on dark but
       3.36:1 on light. So each theme takes the end that reads, and both remain
       literally the two hues he picked. WCAG 1.4.11 asks 3:1 of a non-text
       graphic; 5.84 and 5.35 clear it with real room, where a single shared value
       would have scraped it in one theme.
       Consumers: progress dashes, dots, thin bars, small icons, tiny badges. */
    --gp-color-brand: var(--gp-brand-plum);

    /* PROGRESS is the first consumer of that rule and the reason for it. It keeps
       its own semantic so a later progress treatment (a track, a determinate bar,
       a ring) can change on its own without dragging every other small mark with
       it. */
    --gp-color-progress: var(--gp-color-brand);

    --gp-cta-premium: linear-gradient(135deg, var(--gp-purple-500), var(--gp-purple-400)); /* checkout/membership */
    --gp-cta-premium-on: var(--gp-white);
    --gp-color-success: var(--gp-success);
    --gp-color-danger: var(--gp-error);
    --gp-color-warning: var(--gp-warning);
    --gp-color-info: var(--gp-info);
    --gp-color-focus: var(--gp-gold);
    --gp-color-shadow: rgba(0, 0, 0, 0.08);

    /* D2 (2026-07-23) — legacy-fork reconciliation semantics. gp-base's own bare `body` (#17)
       and the gp-profile.css legacy fork (Section 4.2 de-fork) each used a near-black/near-white
       shade that is NOT byte-identical to any existing Tier-1 primitive (a few hex units off —
       e.g. #1A202C vs --gp-gray-900's #171717). Per the byte-identical-light rule, these get their
       OWN semantic (light = the exact pre-existing value, so the de-fork is a zero-pixel-diff
       swap) rather than being force-mapped onto a near-but-not-exact existing token. Consolidating
       these near-duplicate shades onto ONE canonical value is a future Phase-4.9 owner call (a small
       deliberate pixel shift), not this slice's job. */
    --gp-color-text-strong: #1A202C;          /* gp-base's own body/heading ink (was raw in the body rule) */
    --gp-color-text-profile: #1c1e21;         /* gp-profile.css --gp-text-primary */
    --gp-color-text-muted-profile: #65676b;   /* gp-profile.css --gp-text-secondary */
    --gp-color-border-profile: #e4e6eb;       /* gp-profile.css --gp-border-color */
    --gp-color-bg-profile: #f0f2f5;           /* gp-profile.css --gp-bg-gray (page-canvas level, one step BELOW card-bg) */
    --gp-color-fill-ink: #1c1e21;             /* always-ink pill/chip fill (e.g. .gp-filter-btn.active) — intentionally NO dark
                                                 redefinition: the fill stays ink in both themes (already high-contrast white-on-ink
                                                 in both, so it was never part of the readability gap below) */
    --gp-color-fill-black: var(--gp-black);   /* TRUE black chrome bar, distinct from --gp-color-fill-ink above (B2 2026-09-09,
                                                 owner: "instead of making the single post headers off black lets just make them
                                                 black"). The ink fill is #1c1e21, which reads as very dark gray beside a phone's
                                                 own black status bar; this is the token for a bar the owner wants to MERGE with
                                                 that edge. Points at the Tier-1 primitive rather than repeating #000000, so the
                                                 one place black is defined stays the one place. Same "always" contract as
                                                 fill-ink: NO [data-theme="dark"] redefinition, because a bar asked for as black
                                                 is black in both themes, and its content is already white-on-black in both. */

    /* UB (2026-07-24) — "selected fill" pair: the chip/ink-fill family gap flagged by D1/D2/CT
       (DS-2 #8/#9 + the --gp-color-fill-ink precedent above). Three ACTIVE-STATE surfaces (portal
       sidebar nav pill, College Tracker Board/List toggle, profile tab bar) each fill their selected
       state with an ink-toned or raw-gray background that goes dark-on-dark once the surface itself
       goes dark, instead of reading as a clearly ELEVATED chip. Light is untouched everywhere (each
       surface's existing light declarations are left exactly as they were — this pair is only ever
       consumed inside a [data-theme="dark"] override in the surface file, never in a light-affecting
       position). Dark flips the fill light-on-dark (gray-50 fill / gray-900 ink) — a clearly elevated
       chip against any gray-800/900 surround. Contrast: 17.2:1 label-to-fill (#171717 ink on #fafafa
       fill, WCAG AAA text) — the same pairing also clears the fill-vs-surround boundary against a
       gray-900 page canvas (17.2:1) and a gray-800 card surface (14.5:1); see the PR body for the
       full ratio table. */
    --gp-color-fill-selected: var(--gp-color-primary);      /* light default (unconsumed by the 3 fixed surfaces; matches the existing gp-core .gp-nav-item.active convention) */
    --gp-color-on-fill-selected: var(--gp-color-on-primary);

    /* UB (2026-07-25) — Podium interaction-state token (docs/plans/2026-07-25-gp-design-system-podium.md
       "Interaction states": hover is pointer-only, one wash token shared by both themes). Purely
       additive — no existing rule consumes this yet, so it changes NOTHING visually until a future
       slice opts a control into it; light value is Podium's Chalk-surface wash. */
    --gp-color-hover-wash: rgba(16, 22, 40, 0.05);

    /* UB (2026-07-26) — F6 shared state kit (docs/plans/2026-07-26-app-native-ux.md "F6. The shared
       state kit"). The placeholder fill a loading skeleton paints, i.e. the shape of content that has
       not arrived yet. It needed its OWN semantic because none of the existing Tier-2 names fits the
       role in BOTH themes: --gp-color-surface-2 is all but invisible on a white card in light,
       --gp-color-border is a hairline role being misused as a fill, and a Tier-1 primitive is off
       limits to components. Light maps to the gray-200 primitive (the conventional skeleton grey,
       reads clearly on both the white card surface and the gray page canvas); dark is redefined
       below as a translucent white so it sits correctly on canvas, surface and surface-2 alike
       rather than solving for one fixed dark background. Purely additive: the only consumer is
       css/gp-state-kit.css, which did not exist before this slice, so nothing else moves. */
    --gp-color-skeleton: var(--gp-gray-200);

    /* ------------------------------------------------------------------------------------------
       UB (2026-07-27) — G1 APP-SHELL TOKEN SLICE (docs/plans/2026-07-26-g1-app-shell-brief.md §2.5,
       recorded in DESIGN.md "The G1 additions" as part of this same slice, which is the standing rule
       for a new custom property). The brief measured docs/portal-shell/gp-shell.css against this file
       and found the shell's token block is overwhelmingly a second SPELLING of what gp-base already
       has — the type ramp, radii and press physics are byte-identical, and most colour intents map one
       to one. What follows is the short list that genuinely had no counterpart. Naming is the theme's
       (--gp-* Tier-2), because gp-base is the one token home and G1 renames the SHELL's component rules
       onto these names rather than teaching the theme a second vocabulary (brief §2.3/§2.4).

       Additive: nothing consumes any of these yet, so light mode is byte-identical on every shipped
       surface. The parked brand decision (brief §5.3) is untouched — --gp-cta does not appear here.

       One row of §2.5 is already CLOSED and so is deliberately absent below: the readable gold TEXT
       tier the brief asks for is --gp-color-accent-text, defined further up this file by the R0 fix
       that shipped the same day the brief was written. G1 maps the shell's --accent-text onto it. Do
       not add a second gold-ink token.
       ------------------------------------------------------------------------------------------ */

    /* The third surface step (the shell's --panel-3). gp-base had two, and the shell needs a fill that
       sits ON a panel rather than beside it: a progress-bar track, a meta pill, a badge ground, the
       segments of a level meter. Light maps to the gray-200 primitive, which is NOT an arbitrary pick —
       it is the same rung, for the same reason, that --gp-color-skeleton above had to reach for: on a
       white card --gp-color-surface-2 (#fafafa) is all but invisible, and --gp-color-border is a
       hairline role being misused as a fill. Two roles landing on one light value is fine; they are
       different intents and they diverge in dark (skeleton goes translucent, this stays opaque).
       Dark continues THIS file's own ramp rather than importing the shell's, one clear step above
       --gp-color-surface-2 and still below --gp-color-fill-selected (#2a2e36), so "selected" keeps
       reading as the brightest step. Measured on the dark value: white 15.17:1, --gp-color-text-muted
       (#cfd4db) 10.18:1. */
    --gp-color-surface-3: var(--gp-gray-200);

    /* Recruiting-journey STATUS colours (the shell's --st-*). Owner direction of 2026-07-26 keeps the
       three stage hues, so they need a real semantic home instead of living only as a component-local
       alias. That alias is the point of care here: css/gp-college-tracker.css ALREADY ships this family
       as --ct-interested / --ct-pursuing / --ct-committed, pointing at --gp-color-info /
       --gp-color-accent-purple / --gp-color-success. The shell had drifted to slightly different hues
       (#2563eb / #7c3aed / #16a34a). Per brief §2.2 the theme wins wherever it has already shipped, so
       these resolve to the tracker's LIVE values: a later de-fork of the --ct-* aliases onto these
       names is then a zero-pixel swap, and the palette gains no third blue and no third green. Feature
       CSS consumes these; the --ct-* aliases stay exactly as they are until that de-fork slice runs. */
    --gp-color-status-interested: var(--gp-color-info);
    --gp-color-status-pursuing:   var(--gp-color-accent-purple);
    --gp-color-status-committed:  var(--gp-color-success);

    /* Video/media poster fill (the shell's --media, a literal gradient in its component rule until
       now). The seam components consume; the gradient itself is the Tier-1 --gp-media-ph-bg above.
       No [data-theme="dark"] entry by design — see that primitive's note. */
    --gp-color-media: var(--gp-media-ph-bg);
    /* Ink ON that media fill (or a real poster frame): overlay labels and play glyphs a video tile
       carries (G3 mirror slice, 2026-08-07; G5 Videos and G6 Coach view consume it next). Theme-
       CONSTANT white for the same reason its background is theme-constant dark — a letterbox is a
       letterbox in both themes, so this pairs with --gp-color-media and, like it, has no
       [data-theme="dark"] entry by design. NOT --gp-color-text-inverse (flips dark, would go
       dark-on-dark) and NOT the nav's --gp-nav-ink (different surface, different owner). */
    --gp-color-on-media: var(--gp-white);
    /* Backplate under third-party marks (university logos in the tracker rows, the directory and
       the college detail next): theme-CONSTANT white, because school logos are drawn for white and
       a theme-following surface turns them illegible in dark (owner catch, 2026-08-07). Same
       mechanism as the two constants above; no [data-theme="dark"] entry by design. */
    --gp-color-logo-plate: var(--gp-white);

    /* D3 (2026-07-23) — College Tracker de-fork (WS-2.5 deferred slice, css/gp-college-tracker.css).
       That file's own local gray-700/purple/blue/ink values have no exact Tier-2 counterpart (either a
       genuinely distinct shade, like the D2 shades above, or a role gp-base has never named before, like
       the tracker's iOS-style blue "interactive accent" — the app's gold CTA intent doesn't fit a
       secondary-blue link/focus color). Each gets ONE semantic here, light = the exact pre-existing
       value (byte-identical), dark = a value verified against the plan's WCAG-AA contrast gate
       (docs/plans/2026-07-23-app-experience-motion.md §3.6) — see the PR body for the ratio table. */
    --gp-color-text-secondary: #404040;                 /* tracker's gray-700 text tier (between text-muted/500 and text/900) */
    --gp-color-accent-blue: #007AFF;                    /* tracker's iOS-blue interactive accent (links, focus rings, filled buttons) — distinct from the gold CTA */
    --gp-color-accent-blue-hover: #0066D6;
    --gp-color-accent-blue-tint: #EAF3FF;               /* pale-blue border/wash paired with the accent-blue family */
    --gp-color-accent-purple: #8b5cf6;                  /* tracker's "Pursuing" stage purple */
    --gp-color-highlight-bg: #fffbeb;                   /* "current step" tile highlight wash (checklist) */
    --gp-color-icon-ink: #000000;                       /* modal close-icon ink (a true #000, not gray-900 — kept its own token per the byte-identical rule) */
    --gp-color-btn-ink-bg: #111827;                     /* tracker's local ink-filled "Save" button (a Tailwind near-black, not gray-900 — same reconciliation pattern as D2) */
    --gp-color-btn-ink-bg-hover: #000000;

    /* ADDITIVE port from develop's token ramp (release/app-legal-2026-09-04): ONLY the
       definitions the app legal stylesheet consumes and main lacked, values byte-copied
       from develop's gp-base.css. New NAMES only — no existing token changes value, so
       nothing on the live site restyles. Named side effect, on purpose: gp-state-kit.css
       already consumed --gp-t-sub and --gp-r-xs undefined, so its skeleton/empty states
       now get their INTENDED size and radius instead of the browser fallback. */
    --gp-t-md:   1.375rem;  /* 22px */
    --gp-t-body: 1.0625rem; /* 17px */
    --gp-t-sub:  0.9375rem; /* 15px */
    --gp-t-foot: 0.8125rem; /* 13px */
    --gp-r-xs:   0.5rem;   /* 8px */
    --gp-r-full: 999px;
    --gp-gold-text: #7a5c14;       /* text-safe gold: dark enough to clear AA as TEXT on light */
    --gp-color-accent-text: var(--gp-gold-text);

    /* Tier 3 — COMPONENT tokens (resolve to Tier-2; re-skin a component in one line) */
    --gp-btn-bg: var(--gp-color-primary);
    --gp-btn-on: var(--gp-color-on-primary);
    --gp-card-bg: var(--gp-color-surface);

    /* Shape — radii (F3, DESIGN.md "Shape & elevation"). One scale, in rem; gp-core.css
       repoints its existing --gp-card-radius plus a handful of exact/near-match component
       radii onto these (see the F3 PR body for the full mapping + near-match deltas). Not
       theme-aware — radius doesn't change between light/dark. */
    --gp-r-xs:   0.5rem;   /* 8px */
    --gp-r-sm:   0.75rem;  /* 12px */
    --gp-r-md:   1rem;     /* 16px */
    --gp-r-lg:   1.375rem; /* 22px */
    --gp-r-xl:   1.75rem;  /* 28px */
    --gp-r-full: 999px;

    /* Shape — elevation (F3, DESIGN.md "Shape & elevation": "offset + blur; light mode uses
       cool-tinted shadows"). Three depths — e1 soft (resting card), e2 lifted (hover/raised),
       e3 floating (toast/sheet/popover) — cool-tinted rgba(16,22,40,*) rather than neutral
       black, matching the tint already used for --gp-color-hover-wash above. [data-theme=dark]
       redefines these below to a plain, deeper black (a cool tint reads muddy on a dark
       surface that's already cool-neutral). */
    --gp-e1: 0 1px 3px rgba(16, 22, 40, 0.08), 0 1px 2px rgba(16, 22, 40, 0.06);
    --gp-e2: 0 4px 6px rgba(16, 22, 40, 0.10), 0 2px 4px rgba(16, 22, 40, 0.06);
    --gp-e3: 0 10px 28px rgba(16, 22, 40, 0.18), 0 4px 12px rgba(16, 22, 40, 0.12);

    /* Glass material (WS-2.5 liquid-glass pilot) — frosted translucency for floating CHROME
       only (sheets, popovers, nav/utility bars, toasts), NEVER dense lists/tables. The rgba
       tints live here in the token home (Tier-1 exempt); components consume var(--gp-glass-*)
       with no raw hex. The opacity floor keeps text legible; saturate() gives the rich iOS look. */
    --gp-glass-bg: rgba(255, 255, 255, 0.6);
    --gp-glass-border: rgba(255, 255, 255, 0.5);
    --gp-glass-highlight: rgba(255, 255, 255, 0.85);   /* 1px top rim-light */
    --gp-glass-blur: blur(22px) saturate(180%);
    --gp-glass-shadow: 0 12px 40px rgba(28, 40, 80, 0.18);

    /* Always-dark glass — overlay chrome that carries LIGHT (white) content: the floating
       tab bar, control bars. Does NOT flip with theme, so white icons stay legible in both
       light and dark (the way iOS keeps its tab bar dark-translucent even in light mode). */
    --gp-glass-bg-dark: rgba(20, 22, 28, 0.58);     /* tint floor: lower = less "cloudy"/milky */
    --gp-glass-border-dark: rgba(255, 255, 255, 0.12);
    --gp-glass-highlight-dark: rgba(255, 255, 255, 0.16);
    --gp-glass-shadow-dark: 0 10px 34px rgba(0, 0, 0, 0.34);
    --gp-glass-blur-dark: blur(20px) saturate(180%) brightness(0.86);  /* saturate restores color (anti-cloudy); brightness deepens */
    --gp-glass-active: rgba(255, 255, 255, 0.18);   /* active "chip" fill on dark-glass chrome (nav tabs) */

    /* Floating tab bar — ALWAYS DARK frosted glass (Control Center / Instagram look), even on the
       light theme. Owner decision 2026-06-13: a dark anchor reads more premium and matches the refs.
       NOT theme-aware — renders dark glass + white ink regardless of [data-theme]. */
    --gp-nav-glass: rgba(22, 24, 30, 0.6);   /* LIQUID GLASS — translucent + backdrop-filter; hide-on-scroll keeps the blur off-screen mid-scroll so it doesn't smear */
    --gp-nav-blur: blur(20px) saturate(180%) brightness(0.9);   /* saturate restores color (anti-cloudy); brightness < 1 deepens the dark glass */
    --gp-nav-border: rgba(255, 255, 255, 0.1);
    --gp-nav-highlight: rgba(255, 255, 255, 0.16);
    --gp-nav-shadow: 0 -2px 12px rgba(0, 0, 0, 0.22);   /* upward (flush bottom bar); tight blur to avoid iOS scroll-smear */
    --gp-nav-ink: #ffffff;                       /* white icons always — dark glass anchors regardless of theme */
    --gp-nav-chip: rgba(255, 255, 255, 0.18);    /* active "glider" pill fill */

    /* SHELL GEOMETRY (UB 2026-07-27, G1 token slice — the last row of brief §2.5, recorded in
       DESIGN.md "Shell geometry"). Layout constants rather than design tokens, as the brief says
       plainly, but the shell measures against them in a dozen rules (the scroll container's top and
       bottom padding, the floating bar's height and inset, the header's height) so they must live
       somewhere named. Here, because gp-base is the only stylesheet loaded on EVERY route and both
       doors, so one name is guaranteed to resolve wherever a shell rule lands.

       ADOPTED BY THE NAV SEAM, 2026-07-27 (G1, the slice this comment reserved). When these tokens
       landed they were additive and the legacy mobile-nav constants were left alone, because moving
       live chrome was out of that slice's scope. G1 is the nav-seam migration, so it has now done it:
       css/gp-mobile-core.css repoints --gp-mobile-header-height onto --gp-shell-header-height (56px ->
       52px) and redefines --gp-bottom-nav-height as the floating bar's total FOOTPRINT,
       calc(--gp-shell-bar-height + --gp-shell-bar-gap) = 74px, so the dozen surfaces that reserve room
       with calc(var(--gp-bottom-nav-height) + safe-area) follow the bar automatically. The legacy names
       survive as the compatibility seam their consumers already spell; the VALUES now come from here.
       (css/gp-portal-sidebar.css still re-declares its own 60px header height for the DESKTOP portal
       header, which is a different bar and is deliberately untouched.)

       The safe-area pair reads env() here while docs/portal-shell/gp-shell.css pins both to 0px. That
       difference is correct and must not be "fixed": a phone frame drawn in a desktop browser has no
       inset, so the lab's token block supplies 0px as standalone scaffolding, and the theme supplies
       the real device value (brief §2.4). Spelling matches the env(safe-area-inset-*, 0px) form already
       used across the app-door CSS. */
    --gp-shell-bar-height:    62px;   /* floating bottom tab bar (66 -> 62, JP ruling 2026-09-08 after a
                                         side-by-side: the industry-standard step, 24px icons + 10px
                                         labels, quieter chrome with the same tap targets. The bar
                                         shrinks by exactly the content's 4px so the label keeps its
                                         clearance over the glider bubble. History: 60 -> 66 on
                                         2026-09-05 ("modern tech company"), then this settle. Every
                                         clearance site follows via the --gp-bottom-nav-height calc.) */
    --gp-shell-header-height: 52px;   /* app header (the bar carrying back, wordmark, messages, notifications) */
    --gp-shell-bar-gap:       12px;   /* the tab bar's inset from the bottom edge (it floats, it is not flush).
                                         Two jobs since 2026-09-09: the MINIMUM float on a device with no
                                         safe-area inset, and the amount the bar dips back INTO that inset on
                                         a home-indicator phone, via max(gap, inset - gap) at .gp-bottom-nav.
                                         It is also the bar's left and right inset, so one number keeps the
                                         capsule reading as one shape. */
    --gp-safe-top:    env(safe-area-inset-top, 0px);
    --gp-safe-bottom: env(safe-area-inset-bottom, 0px);

    /* ------------------------------------------------------------------------------------------
       UB (2026-08-12) — DESKTOP PORTAL CHROME. The owner locked the desktop portal frame on
       2026-07-25 and confirmed the chrome on 2026-07-26: a dark rail plus a dark in-content
       header, both the same near-black, wrapping LIGHT content
       (docs/plans/2026-07-27-desktop-look-frame-locked.md, "CHROME CONFIRMED").

       WHY THIS IS A NEW INTENT AND NOT A REUSE. The chrome is deliberately NOT theme-following:
       it stays near-black in light mode and in dark mode alike, exactly the way the floating tab
       bar's --gp-nav-* family below is always dark. It therefore cannot consume --gp-color-surface
       (flips with the theme) and it cannot consume the --gp-glass-*-dark family either, because
       that family is TRANSLUCENT and the desktop frame is a flat opaque panel. So it gets its own
       Tier-2 names, and like --gp-nav-* it has NO [data-theme="dark"] entry by design: a dark
       redefinition here would be the bug, not the fix.

       Ink is shared with the tab bar on purpose (--gp-nav-ink) rather than spelled a second time:
       white-on-always-dark-chrome is one role, and two spellings of it can drift.

       Measured against the chrome fill these tokens actually sit on, #0e0f13: white 19.2:1,
       --gp-color-accent (#DFB859) 11.1:1, --gp-color-danger (#ef4444, the count badge) 4.8:1.
       The white figure read 18.9:1 before 2026-09-09, which is full white over #111111, a
       lighter near-black than the one these tokens are used on.

       TWO CORRECTIONS TO THIS NOTE, 2026-09-09, comment only, no value changed:

       (a) THE MUTED RUNG IS 7.6:1, NOT THE 8.6:1 THIS NOTE USED TO CLAIM. Composited and then
           read back off the rendered pixels of the More sheet on deployed dev: rgba(255,255,255,
           0.62) over the chrome fill #0e0f13 lands on rgb(163,163,165), which is 7.61:1 measured
           and 7.67:1 by arithmetic. 8.6:1 is not reachable over ANY of the near-blacks in this
           file: the ceiling is 7.85:1 over pure black. The conclusion the note drew is unchanged,
           it clears AA with headroom, but the figure was wrong and a wrong number in a token
           comment gets cited.

           WHERE 8.6 CAME FROM, recorded so nobody "restores" it: it is the 0.65 NAV rung over
           PURE BLACK: 8.63:1 rounding the composited channel first, which is the rendered
           figure, or 8.60:1 carried unrounded. The old sentence described the nav rung, on a surface nothing
           here uses, while wearing this token's name, and quoted white on a third background
           again. ONE conflation produced both errors, which is why (a) and (b) are a single
           mistake rather than two.

       (b) IT DOES NOT CARRY THE RESTING NAV LABELS, and did not when this was written. The bottom
           bar's resting item is --gp-nav-ink at opacity 0.65 (css/gp-mobile-core.css, the
           .gp-bottom-nav-item rule), which composites to 8.35:1: a near neighbour of this rung,
           which is probably how the two got conflated, but a different mechanism. What actually
           consumes this token is the portal sidebar, the utility bar, and the More sheet's icons.
           Its labels moved to full ink on 2026-09-09 by owner ruling.
       ------------------------------------------------------------------------------------------ */
    --gp-chrome-bg:        var(--gp-chrome-black);
    --gp-chrome-border:    rgba(255, 255, 255, 0.08);
    --gp-chrome-ink:       var(--gp-nav-ink);            /* the one white-on-dark-chrome ink */
    --gp-chrome-ink-muted: rgba(255, 255, 255, 0.62);    /* resting nav label / icon */
    --gp-chrome-hover:     rgba(255, 255, 255, 0.07);    /* pointer hover wash on the chrome */
    --gp-chrome-chip:      rgba(255, 255, 255, 0.12);    /* the active item's soft chip (no gold stripe) */

    /* Geometry the rail and the header both measure against, so they cannot drift apart. */
    --gp-chrome-rail:           260px;   /* rail, expanded */
    --gp-chrome-rail-collapsed:  74px;   /* rail, collapsed */
    --gp-chrome-header-height:   60px;   /* the in-content header */
}

/* Dark mode — redefine Tier-2 ONLY (Tier-1 + every component rule stay byte-identical).
   INERT until a surface opts in via [data-theme="dark"]; the user toggle ships in a later phase.
   UB (2026-07-25) — repointed to the "Podium" Graphite palette, the owner-approved canonical dark
   direction (DESIGN.md + docs/plans/2026-07-25-gp-design-system-podium.md). Same seam, same
   Tier-2 names, same consumers — only the VALUES change, from placeholder --gp-gray-* re-tints to
   Podium's measured Graphite hex/rgba.

   ITERATION 2 (UB, 2026-07-25, same day) — the owner + his partner reviewed the pass directly
   above on dev and called it "almost there" with three precise problems: (1) body/secondary text
   still read as "gray on gray," not enough pop; (2) every active nav/tab/toggle got a solid gold
   FILL, which read "muddy," not premium, once stacked on the gray surfaces below it — their
   reference (a Lovable app sidebar) uses a clean elevated NEUTRAL pill + bright white text, gold
   nowhere in the fill; (3) panel/card colors still clashed. This block supersedes the OWNER
   OVERRIDE A/B values from the pass above with the values below — same seam, same consumers, only
   the numbers move again. Every pair is re-verified AA below (all comfortably ≥10:1 — see the PR
   body's full ratio table); gold is now reserved for primary-action fills, the progress-bar fill,
   and a single small accent detail on the active nav item (see css/gp-portal-sidebar.css) — never
   a large active-state fill. */
[data-theme="dark"] {
    --gp-color-bg: #0b0c0e;              /* iter 2: was #0c0d10 — a hair deeper + slightly desaturated canvas floor, part of the calmer low-chroma ramp requested */
    --gp-color-surface: #16181d;         /* iter 2: was #1e2129 (owner override B's raised panel) — the owner's partner still read panel-vs-canvas as "clashing"; this settles on a cleaner, closer-to-canvas step so TEXT (now much brighter, below) carries the contrast instead of the boxes fighting each other */
    --gp-color-surface-2: #1e2127;       /* iter 2: was #262b34 — one clear, calmer step above the new surface */
    --gp-color-text: #ffffff;            /* iter 2: was #f5f6f8 (Podium --ink) — owner: "why are we just gray text on a dark background." Primary content now reads pure white; 17.76:1 vs the new surface, 16.13:1 vs new surface-2 */
    --gp-color-text-muted: #cfd4db;      /* iter 2: was #b2b8c2 (Podium --ink-2) — brightened so secondary/body text stops reading gray-on-gray; 11.92:1 vs the new surface, 10.82:1 vs new surface-2 (still a clear step down from --gp-color-text for hierarchy) */
    --gp-color-text-inverse: #0b0c0e;    /* dark ink for a light-color-filled badge (paired with --gp-color-text/-success/-danger/-info AS backgrounds, not with --gp-color-primary — that pairing is --gp-color-on-primary below); matches the iter-2 canvas */
    --gp-color-border: rgba(255, 255, 255, 0.12);        /* iter 2: was 0.15 — the brighter text above no longer needs as strong a hairline to read panel edges, and a lighter line reads calmer against the new surface */
    --gp-color-border-strong: rgba(255, 255, 255, 0.18); /* iter 2: was 0.20 — keeps the same two-tier step relationship at the new, subtler hairline */
    --gp-color-primary: var(--gp-gold);            /* on dark, primary goes gold — already matched Podium's "black on light, gold on dark" action law before Podium existed; unchanged. Gold stays HERE (real primary-action buttons) — it no longer floods active nav/tab/toggle fills, see --gp-color-fill-selected below */
    --gp-color-on-primary: #1b1306;       /* Podium --action-ink — the exact ink tuned to sit on gold (was the old gray-900 placeholder) */
    --gp-color-accent-text: var(--gp-gold);            /* accent text goes back to plain gold on dark, where it already clears AA comfortably: 10.38:1 vs the canvas, 9.42:1 vs surface, 8.55:1 vs surface-2. Same flip the portal shell makes (gp-shell.css sets --accent-text:#DFB859 in its dark block) */
    --gp-color-accent-text-hover: var(--gp-gold-hover); /* 8.43:1 / 7.65:1 / 6.95:1 against the same three */
    --gp-color-danger: #ff5a4d;           /* Podium Graphite --danger (was unset here, silently inheriting the light #ef4444 Tier-1) */
    --gp-color-success: #34c759;          /* Podium Graphite --success (was unset here, silently inheriting the light #22c55e Tier-1) */
    --gp-color-shadow: rgba(0, 0, 0, 0.4);

    /* Shape — elevation, dark side (F3). Plain black, deeper than the light cool-tint values
       above (a dark surface is already cool-neutral, so tinting the shadow too reads muddy;
       DESIGN.md only calls for the cool tint "in light mode"). Same three-depth scale. */
    --gp-e1: 0 1px 3px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.18);
    --gp-e2: 0 4px 6px rgba(0, 0, 0, 0.32), 0 2px 4px rgba(0, 0, 0, 0.20);
    --gp-e3: 0 10px 28px rgba(0, 0, 0, 0.46), 0 4px 12px rgba(0, 0, 0, 0.30);

    /* Podium interaction-state token, dark side (see the :root definition above) — pointer-hover
       wash on a dark surface. Additive; no consumer yet. */
    --gp-color-hover-wash: rgba(255, 255, 255, 0.06);

    /* F6 skeleton fill, dark side (see the :root definition above). Deliberately a TRANSLUCENT
       white rather than a fixed dark grey: a skeleton renders on the canvas, on a card surface and
       on the raised surface-2 alike, and one opaque value cannot read correctly on all three. At
       10% it lands one clear step above whichever dark background sits behind it, which is the
       "elevation, not inversion" reading the Podium dark palette is built on. */
    --gp-color-skeleton: rgba(255, 255, 255, 0.10);

    /* D2 legacy-fork reconciliation — dark side, repointed onto the Podium ink/canvas/line values
       above (same role as before: gp-base's own body ink, the gp-profile.css de-fork, its
       page-canvas). Iter 2: brightened alongside --gp-color-text/-text-muted above (same "text must
       pop" fix — these are the same ink role under a byte-identical-light-mode exemption, not a
       genuinely different color, so they track the primary pair). */
    --gp-color-text-strong: #ffffff;
    --gp-color-text-profile: #ffffff;
    --gp-color-text-muted-profile: #cfd4db;
    --gp-color-border-profile: rgba(255, 255, 255, 0.12);
    --gp-color-bg-profile: #0b0c0e;       /* page-canvas: same dark floor as the iter-2 --gp-color-bg */

    /* ITERATION 2 (UB, 2026-07-25) — replaces the OWNER OVERRIDE A solid-gold fill from the pass
       above. The owner + partner judged that pairing (gold bg, dark ink) as "muddy," not premium,
       once every active nav item / tab / toggle across the app filled solid gold on top of the
       gray surfaces below it — their reference (a Lovable app sidebar) uses a clean ELEVATED
       NEUTRAL pill + bright white text instead, no color flood. Redefining ONLY these two tokens
       fixes every wired consumer in one place: the portal sidebar active nav item
       (css/gp-portal-sidebar.css — which also gets the one remaining gold accent, the active
       item's icon, so gold isn't eliminated entirely, just no longer the fill), the profile tab bar
       (css/gp-profile.css), the College Tracker Board/List toggle (css/gp-college-tracker.css), the
       Recruiting Portal "Foundation" section tab + coach-dashboard "On Watchlist" KPI tile
       (css/gp-recruiting-portal.css, page-templates/page-recruiter.php), and Mind Lab's tag filter
       pill (page-templates/page-mind-lab.php) — all six already consumed
       var(--gp-color-fill-selected)/var(--gp-color-on-fill-selected) rather than hardcoding their
       own fill, so this one-place repoint fixes every one of them with no per-surface edit needed.
       Fill is a neutral step ABOVE --gp-color-surface-2 (elevation, not color, signals "selected");
       text is pure white. 13.61:1 white-to-fill (verified), AAA. */
    --gp-color-fill-selected: #2a2e36;
    --gp-color-on-fill-selected: var(--gp-color-text);

    /* D3 (2026-07-23) — College Tracker de-fork, dark side. Out of THIS slice's scope (College
       Tracker deep de-fork is explicitly deferred) and not part of Podium's color-law table, so
       left byte-identical; the darker Graphite canvas/panel behind these can only raise their
       already-verified contrast ratios, never lower them. */
    --gp-color-text-secondary: var(--gp-gray-300);   /* 12.1:1 vs page bg, 10.2:1 vs card surface */
    --gp-color-accent-blue: #409CFF;                 /* 6.3:1 vs page bg, 5.4:1 vs card surface (AA text) */
    --gp-color-accent-blue-hover: #0A84FF;           /* Apple's dark-mode systemBlue; a deliberately deeper "pressed" tone */
    --gp-color-accent-blue-tint: rgba(64, 156, 255, 0.4);   /* translucent so it reads on any dark surface, no fixed bg to solve for */
    --gp-color-accent-purple: #A78BFA;               /* 6.6:1 vs page bg, 5.6:1 vs card surface (AA text) */

    /* The flat brand purple takes the OTHER end of the gradient in dark. Same two
       hues the owner picked, each theme using the one that carries on it: plum
       measures 3.07:1 on the dark canvas and violet 5.35:1. The gradients above
       are deliberately NOT flipped — a sweep containing both hues reads on either
       canvas, which is exactly why only the flat token needs a dark entry. */
    --gp-color-brand: var(--gp-brand-violet);
    --gp-color-highlight-bg: rgba(245, 158, 11, 0.12);      /* amber wash (an existing --gp-warning tint) reads as "in progress" on a dark card */
    --gp-color-icon-ink: var(--gp-gray-50);          /* pure #000 on a dark surface would be invisible; matches --gp-color-text's dark value */
    --gp-color-accent-text: var(--gp-gold);          /* additive port with the app-legal release: accent text goes back to plain gold on dark, where it clears AA (develop's value, byte-copied) */
    --gp-color-btn-ink-bg: var(--gp-gold);           /* the app's existing dark-mode "primary action goes gold" convention (matches --gp-color-primary) */
    --gp-color-btn-ink-bg-hover: var(--gp-gold-hover);

    /* Glass material — dark variant (near-black tint + faint rim = the Control Center look).
       Chrome-only per Podium principle 4; out of this token-value slice's scope (glass is a
       separate material system, not the Tier-2 semantic color table) — left byte-identical. */
    --gp-glass-bg: rgba(30, 33, 42, 0.55);
    --gp-glass-border: rgba(255, 255, 255, 0.1);
    --gp-glass-highlight: rgba(255, 255, 255, 0.14);
    --gp-glass-shadow: 0 14px 44px rgba(0, 0, 0, 0.55);
    /* NOTE: the floating tab bar is ALWAYS dark glass now (defined once in :root), so it needs
       no [data-theme="dark"] override here — it already renders dark on every theme. */

    /* UB (2026-07-25) — leak-sweep root-cause fix. The bare h1-h6 heading rules below (Phase 5b)
       consume --gp-h1-color..--gp-h6-color, which were tokenized off raw hex at :root but NEVER
       given a [data-theme="dark"] override — every plain heading tag site-wide (no page/component
       color override of its own) rendered fixed near-black ink regardless of theme. This is why
       Mind Lab's page title (h1), module headers (h2), and lesson titles (h3) read as dark-on-dark
       even after the Podium dark palette shipped (PR #479): those elements have no explicit
       `color`, so the h1/h2/h3 element rule — not inherited body color — wins the cascade.
       Repointed onto --gp-color-text-strong (h1-h5; already dark-aware, PR #479) / --gp-color-
       text-muted (h6, to keep its original slightly-dimmer step vs h1-h5). Additive only inside
       this existing dark block — the :root literals above are untouched, so light is byte-identical. */
    --gp-h1-color: var(--gp-color-text-strong);
    --gp-h2-color: var(--gp-color-text-strong);
    --gp-h3-color: var(--gp-color-text-strong);
    --gp-h4-color: var(--gp-color-text-strong);
    --gp-h5-color: var(--gp-color-text-strong);
    --gp-h6-color: var(--gp-color-text-muted);

    /* UB (2026-07-27) — G1 app-shell token slice, dark side (see the :root definitions above for the
       full rationale). Only two of the five new entries need a dark value:

       surface-3 continues THIS file's Graphite ramp rather than importing the shell's #22262e-by-
       coincidence — canvas #0b0c0e, surface #16181d, surface-2 #1e2127, and now #22262e — one clear
       step up, and still below --gp-color-fill-selected (#2a2e36) so a SELECTED chip stays the
       brightest thing on a dark card. Measured against it: white 15.17:1 (AAA), --gp-color-text-muted
       #cfd4db 10.18:1 (AAA).

       status-interested is the one stage hue that needs a dark override. The other two inherit a dark
       value already: --gp-color-accent-purple flips to #A78BFA and --gp-color-success to #34c759 in
       this same block, both of which the status semantics resolve through. --gp-color-info has no dark
       entry, so interested would otherwise stay the light #3b82f6 and measure a thin 4.83:1 on
       --gp-color-surface; #5b8dff (the value the portal shell measured for exactly this) lifts it to
       5.66:1 on surface and 6.24:1 on the canvas. Nothing consumes it yet, so this changes no pixel
       today; it is what keeps the token honest when G1's shell components adopt it. */
    --gp-color-surface-3: #22262e;
    --gp-color-status-interested: #5b8dff;
}

/* ============================================================================
   MOTION TOKEN SYSTEM (WS-12 app-experience, M0) — the standardized motion
   vocabulary. Defined HERE in gp-base (loaded on EVERY page, zero added bytes
   per surface) beside the color tokens above, and follows the SAME discipline:
   feature CSS consumes var(--gp-motion-*), never a raw duration or curve.
   --gp-motion-ease-spring is NOT a new curve — it is the literal
   cubic-bezier(0.32, 0.72, 0, 1) already hardcoded in css/gp-surface.css
   (cover/sheet transitions); this reconciles that value onto a named token so
   gp-surface.css can consume it instead of forking a second spring. See
   docs/plans/2026-07-23-app-experience-motion.md §1.2 for the full rationale
   and citations. Only transform/opacity are animated anywhere motion tokens
   are used — see MOBILE_DESIGN_SYSTEM.md and the motion plan §1.1/§1.3.
   ============================================================================ */
:root {
    /* Durations — short by design; <100ms reads as instant, <250ms as responsive. */
    --gp-motion-duration-instant: 100ms;  /* tap feedback, micro-state; the "did it register" range */
    --gp-motion-duration-fast:    200ms;  /* tab crossfade, small state change, hover/focus */
    --gp-motion-duration-base:    300ms;  /* screen enter (cover push / sheet rise) — matches shipped gp-surface */
    --gp-motion-duration-slow:    400ms;  /* rare: large/complex reveal, app-load handoff only */

    /* Easing — the allowed curves. ease-out is the 80% default. */
    --gp-motion-ease-out:    cubic-bezier(0.4, 0, 0.2, 1);    /* default; decisive start, soft settle */
    --gp-motion-ease-in:     cubic-bezier(0.4, 0, 1, 1);      /* exits only (element leaving screen) */
    --gp-motion-ease-in-out: cubic-bezier(0.42, 0, 0.58, 1);  /* symmetric moves */
    --gp-motion-ease-spring: cubic-bezier(0.32, 0.72, 0, 1);  /* iOS-sheet emphasis; ALREADY in gp-surface — adopt, do not fork */
    /* The only curve here that OVERSHOOTS its target and settles back. ease-spring above does not:
       cubic-bezier(0.32, 0.72, 0, 1) approaches 1 without ever passing it, which is exactly right
       for a sheet and exactly wrong for something that should read as having mass. Promoted from
       the literal that was component-scoped in css/gp-mobile-core.css (the bottom-nav elastic
       bubble, native-UX F8) with the value unchanged, so that component is byte-identical and the
       next surface that needs overshoot adopts this instead of forking a second curve. Reserve it
       for a travelling/landing object; it is wrong for a fade or a colour change. */
    --gp-motion-ease-overshoot: cubic-bezier(0.34, 1.56, 0.5, 1);

    /* Composite shorthands (optional convenience; resolve to the above) */
    --gp-motion-tap:  transform var(--gp-motion-duration-instant) var(--gp-motion-ease-out);
    --gp-motion-fade: opacity var(--gp-motion-duration-fast) var(--gp-motion-ease-out);
}

/* ============================================================================
   PRESS & TOUCH PHYSICS TOKENS (F2, 2026-07-26 app-native-ux, Wave 0) — the
   Podium press-scale trio from DESIGN.md "Interaction states": a scale-down on
   :active, on every device (touch included), immediate and physical. Feature
   CSS consumes these var()s, never a raw scale() number — this is what let the
   18 bespoke per-feature forks (six different raw values: 0.94, 0.95, 0.97,
   0.98, 0.985, 0.99) converge onto one shared physical feel. Paired with the
   --gp-motion-duration-instant token above for the transition duration.
   ============================================================================ */
:root {
    --gp-press-scale-button: 0.96;  /* .gp-btn and every variant, FAB, tappable cards, filled CTAs */
    --gp-press-scale-chip:   0.95;  /* pills/chips, segmented controls */
    --gp-press-scale-icon:   0.90;  /* round/square icon-only buttons (close, back, etc.) */
}

/* Global tap-highlight suppression (F2) — ships in the SAME PR as the :active
   press states in gp-core.css (and every converged feature-CSS fork), never
   alone: a suppression with no replacement feedback is a regression, per the
   plan. The 32 scattered local `-webkit-tap-highlight-color: transparent`
   declarations are harmless duplicates now and are removed one file at a time
   as each is next touched — not swept in this PR. */
* {
    -webkit-tap-highlight-color: transparent;
}

/* Reduced motion — global, site-wide (harmless on the website, load-bearing in
   the app). Collapses every transition/animation to near-instant for users who
   set Reduce Motion, WITHOUT breaking end states: the opened sheet, the visible
   content, the error banner are all still fully present, just without the
   in-between motion. Generalizes the per-block reduced-motion guards already in
   css/gp-surface.css so no future animation can forget it. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* Box-sizing — matches Kadence: html{box-sizing:border-box} *{box-sizing:inherit} */
html {
    box-sizing: border-box;
}
*, *::before, *::after {
    box-sizing: inherit;
}

/* Body base — Poppins (self-hosted, F3), system stack as fallback.
   DS-2 #17 (2026-07-23): retokenized off raw hex — the page root now resolves through Tier-2
   (byte-identical in light: #1A202C/#ffffff are still exactly what render; dark-capable now that
   a surface opts into [data-theme="dark"]). Most templates override this with their own
   higher-specificity background (--gp-app, --gp-onboarding-page, --gp-profile-wrapper, etc.), so
   this fix mainly helps the routes that fall through to the bare gp-base layer (search, 404,
   plain Pages) — it does not change anything on already-styled templates.
   F3 (2026-07-26): font-family now resolves through --gp-font-family (one stack, one place to
   edit); font-size moved from a flat 1rem (16px) to --gp-t-body (17px-equivalent, DESIGN.md's
   Apple ramp "body" rung) — the plan's "body size on both doors goes to the 17px-equivalent
   rem" instruction. This is a real, visible size bump on every route that falls through to this
   bare layer; part of the owner screenshot gate, not a silent change. */
body {
    margin: 0;
    font-family: var(--gp-font-family);
    font-size: var(--gp-t-body);
    line-height: 1.6;
    color: var(--gp-color-text-strong); /* Kadence "Strongest text" (palette3) */
    background-color: var(--gp-color-surface);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* Media — matches Kadence: img{display:block;height:auto;max-width:100%} */
img {
    display: block;
    height: auto;
    max-width: 100%;
}

/* Headings — tight, structured scale tuned for Poppins (WS-2 Phase 5b).
   Owner-preferred compact sizes (h1 32 / h2 28 / h3 24 — the original Kadence sizes) with
   the modern treatment layered on, so they read STRUCTURED rather than shrunk:
     • TIGHT, descending line-heights (1.15 → 1.4) instead of the old uniform loose 1.5;
     • a WEIGHT LADDER — 700 for display h1/h2, 600 (semibold) for h3–h6, body stays 400;
     • slight NEGATIVE letter-spacing on the display sizes so Poppins reads crisp, not airy.
   Bare element selectors keep low specificity so per-page/block CSS still overrides.
   Colors retain the Phase-4 palette (palette3/4/5). WS-2.5 Task 4 TOKENIZED this — the values now
   live in the :root block at the top of this file; the rules below consume them. */
h1, h2, h3, h4, h5, h6 {
    padding: 0;
    margin: 0;
}
h1 { font-size: var(--gp-h1-size); line-height: var(--gp-h1-line); font-weight: var(--gp-h1-weight); letter-spacing: var(--gp-h1-tracking); color: var(--gp-h1-color); } /* palette3 */
h2 { font-size: var(--gp-h2-size); line-height: var(--gp-h2-line); font-weight: var(--gp-h2-weight); letter-spacing: var(--gp-h2-tracking); color: var(--gp-h2-color); }
h3 { font-size: var(--gp-h3-size); line-height: var(--gp-h3-line); font-weight: var(--gp-h3-weight); letter-spacing: var(--gp-h3-tracking); color: var(--gp-h3-color); }
h4 { font-size: var(--gp-h4-size); line-height: var(--gp-h4-line); font-weight: var(--gp-h4-weight); letter-spacing: var(--gp-h4-tracking); color: var(--gp-h4-color); } /* palette4 */
h5 { font-size: var(--gp-h5-size); line-height: var(--gp-h5-line); font-weight: var(--gp-h5-weight); color: var(--gp-h5-color); }
h6 { font-size: var(--gp-h6-size); line-height: var(--gp-h6-line); font-weight: var(--gp-h6-weight); color: var(--gp-h6-color); } /* palette5 */

/* Flow elements — mirror Kadence base spacing */
p {
    margin: 0 0 1.5em;
}
hr {
    height: 0;
    border: 0;
    border-bottom: 2px solid var(--gp-color-border); /* DS-2 #18: was raw #e5e5e5 — exact match to --gp-gray-200, byte-identical in light */
}
ul, ol {
    margin: 0 0 1.5em 1.5em;
    padding: 0;
}
ul { list-style: disc; }
ol { list-style: decimal; }
li > ul, li > ol {
    margin-bottom: 0;
    margin-left: 1.5em;
}
dt { font-weight: 700; }
dd { margin: 0 1.5em 1.5em; }
figure { margin: 0.5em 0; }
table {
    width: 100%;
    margin: 0 0 1.5em;
}

/* Links — accent gold, TOKENIZED 2026-06-13 (one-gold sweep; was the Kadence raw gold + a darker raw hover).
   R0 2026-07-26: repointed off the FILL gold onto --gp-color-accent-text. This one rule colors every text
   link on the website, and the fill gold measured 1.89:1 on a white surface / 1.73:1 on the page canvas
   against AA's 4.5:1. The hover state additionally reached past Tier-2 straight into the --gp-gold-hover
   primitive (2.32:1 / 2.13:1), so it now consumes the paired semantic instead. Dark is unchanged in
   practice: the token resolves back to plain gold under [data-theme="dark"]. Focus is deliberately left
   sharing the hover declaration here — the focus RING is a separate slice's concern, and this change does
   not touch --gp-color-focus. */
a {
    color: var(--gp-color-accent-text);
    text-underline-offset: 0.1em;
    transition: color 0.1s linear;
}
a:hover, a:focus, a:active {
    color: var(--gp-color-accent-text-hover);
    text-underline-offset: 0.25em;
}

/* Forms & buttons — inherit type, sane normalization */
button, input, select, textarea {
    font: inherit;
    color: inherit;
}
button {
    cursor: pointer;
}

/* Shared avatar-initials placeholder tile — the purple-gradient (--gp-avatar-ph-bg)
   fallback rendered by gp_render_avatar() (inc/gp-helper-functions.php) everywhere a
   user has no real photo: never a gray/Gravatar silhouette, never a blank white box.
   gp_render_avatar() inlines width/height/font-size/background/color per-instance (the
   surfaces it drops into each already declare their OWN "no photo" background/sizing,
   so inlining is what makes this reliable regardless of any one surface's CSS); this
   class exists as a documented hook/marker, not the source of truth for those values. */
.gp-avatar--ph {
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    letter-spacing: 0.02em;
    background: var(--gp-avatar-ph-bg);
    color: var(--gp-white);
}

/* ============================================================================
   APP-DOOR CHROME-LEAK REMOVAL (F5, 2026-07-26 app-native-ux "Web-chrome leak
   removal") — this file has no conditional enqueue (loaded on EVERY route, both
   doors), so this is the one place a fix reaches every app-door screen at once,
   including the ones with no mobile-nav chrome at all (login, onboarding, the
   events archive). Every rule below is scoped to html.gp-app, added by
   gpa_app_html_class() (inc/gp-app-bootstrap.php, APP DOOR ONLY). Do NOT scope
   door behavior to body.gp-app: that class is ALSO hardcoded as a portal
   styling class on both doors (e.g. body_class('gp-app gp-portal-page') in
   portal templates), so it leaks to the website (2026-07-25 scope-leak fix).
   ============================================================================ */

/* 1) No rubber-band white band. inc/gp-capacitor-bridge.php already sets this
   INSIDE the real Capacitor shell (.gp-native-app, JS-detected); this is the
   same fix for the app DOOR itself, so a plain mobile browser on
   app(-dev).gymnasticsplus.org is corrected too, not only the installed app. */
html.gp-app,
html.gp-app body {
    overscroll-behavior: none;
}

/* 2) Canvas pinned to the resolved theme so no white frame can show between
   documents (a navigation, or the overscroll gutter above). Same --gp-color-bg
   token the dark override earlier in this file redefines under
   [data-theme="dark"] — no new color introduced, light is unaffected. */
html.gp-app {
    background-color: var(--gp-color-bg);
}

/* 3) iOS auto text-size adjustment is a Safari-only affordance; a native app
   doesn't get bumped font sizes out from under its own layout. */
html.gp-app {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

/* 4) Scrollbar chrome hidden on the app door — a phone app has no visible
   scrollbar track/thumb. Content still scrolls; only the OS-drawn indicator
   is suppressed. */
html.gp-app,
html.gp-app body {
    scrollbar-width: none;      /* Firefox-engine WebViews */
    -ms-overflow-style: none;   /* legacy, harmless elsewhere */
}
html.gp-app::-webkit-scrollbar,
html.gp-app body::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}

/* 5) Callout + selection suppressed on CHROME, LABELS and CONTROLS only — a
   long-press "Copy"/"Share" menu on a nav label or a button is a web tell.
   Deliberately narrow: real content (a message, a bio, a post) is NOT in this
   list and stays selectable. Nav-specific chrome (.gp-mobile-header,
   .gp-bottom-nav, .gp-more-sheet, the portal sidebar's own nav links) is
   covered here rather than in gp-mobile-core.css so this one block reaches
   every app-door page, whether or not that page's mobile-nav CSS is enqueued. */
html.gp-app .gp-mobile-header,
html.gp-app .gp-bottom-nav,
html.gp-app .gp-more-sheet,
html.gp-app .gp-portal-nav,
html.gp-app .gp-btn,
html.gp-app .gp-pill,
html.gp-app label {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}
