  :root {
    /* ── PRIMITIVE TOKENS ─────────────────────────────────────────────────────
       Raw scales. Components MUST NOT reference these directly — go through the
       semantic layer below. Published here so palette work happens in one place. */

    /* Coral (brand) */
    --coral-50:  #FFE9D0;
    --coral-100: #FFDDCA;
    --coral-200: #FFCBAA;
    --coral-300: #FFC49A;
    --coral-400: #FFB186;
    --coral-500: #FFAF85;  /* legacy primitive — unreferenced; --accent is the canonical brand accent */
    --coral-600: #FF9966;
    --coral-700: #F09060;
    --coral-800: #E5754A;
    --coral-900: #2A1A0C;  /* on-coral text */

    /* Cream (warm neutral, paired with coral) */
    --cream-50:  #FFF2EB;  /* on-dark text */
    --cream-100: #DECCC0;

    /* Blue (surfaces & cool accents) */
    --blue-200:  #B8CFEE;  /* cool */
    --blue-300:  #9CBDE7;  /* jordy / muted */
    --blue-400:  #7FA5D9;  /* rain */
    --blue-500:  #4A90E2;
    --blue-700:  #204983;  /* glass gradient mid */
    --blue-800:  #111E38;  /* surface */
    --blue-900:  #111E38;  /* canvas */
    --blue-950:  #0A1830;

    /* Status (used ad-hoc today; tokenized for the next migration pass) */
    --green-500: #64FFB4;
    --red-500:   #FF6B6B;
    --red-600:   #DC2626;
    --red-900:   #7F1D1D;
    /* Semantic role colours (DESIGN.md → Button roles). Default to the shades
       that read on DARK surfaces (where most destructive/success actions live:
       admin, inbox, modals); the light RSVP receive card overrides --color-error
       to --red-600 for contrast on its cream frost. */
    --color-error:   var(--red-500);
    --color-success: var(--green-500);
    /* Faint red wash for the destructive role's hover fill (.d-pill). */
    --color-error-dim: rgba(255,107,107,0.14);

    /* Weather palette — three sun bands (yellow gradient) + one cool grey
       overcast + rain + night. Aligned with Slate + Honey: clear sun =
       honey, overcast = cool slate, rain = slate-blue. */
    --weather-clear:      #F5C25E;   /* matches --accent — clear sun */
    --weather-clear-soft: #E8B870;   /* mostly clear — softer honey */
    --weather-partly:     #D5B068;   /* partly cloudy — clearly yellow (was reading too grey) */
    --weather-overcast:   #9A8E7E;   /* overcast — warm grey, harmonized with cream map */
    --weather-rain:       #4A5E82;   /* rain — Delft-blue family (no longer slate) */
    --weather-night:      #0F1B2A;   /* deeper than --bg for visibility */

    /* (Legacy numeric --space-1..12 ladder removed 2026-05-24 — 0 consumers,
       superseded by the semantic --space-2xs…4xl scale below.) */

    /* Radius — MIGRATED: sm/md/lg/pill/none are consumed and border-radius is fully
       tokenized (0 raw). (--radius-xs / --radius-circle currently unused.) */
    --radius-xs:    4px;
    --radius-sm:    8px;
    --radius-md:    12px;
    --radius-lg:    20px;
    --radius-pill:  999px;
    --radius-circle: 50%;

    /* (Legacy numeric --text-xs..3xl scale removed 2026-05-24 — 0 consumers,
       superseded by the semantic --text-{display,title,subtitle,body,label,caption}
       roles below.) */

    /* Motion vocabulary (DESIGN.md → "Motion tokens & mobile interaction").
       Easing: standard = two-way micro; decelerate = premium ease-out for
       entrances/fades; accelerate = exits; emphasized = sheet/large moves;
       spring = gentle controlled overshoot (CSS approx — real springs are JS).
       Duration: fast micro · base values/chips · slow sheets · slower grows. */
    --ease-standard:   cubic-bezier(0.4, 0, 0.2, 1);
    --ease-decelerate: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-accelerate: cubic-bezier(0.55, 0, 1, 0.45);
    --ease-emphasized: cubic-bezier(0.32, 0.72, 0, 1);
    --ease-spring:     cubic-bezier(0.34, 1.3, 0.64, 1);
    --dur-fast:   120ms;
    --dur-base:   180ms;
    --dur-slow:   320ms;
    --dur-slower: 480ms;

    /* ── SEMANTIC TOKENS ─────────────────────────────────────────────────────
       What components consume. Components SHOULD reference these (or the
       component-level tokens below), never the primitives directly.

       Brand: Slate + Honey (experimental swap from Polynesian + Coral).
       Primitive layer (--coral-*, --blue-*) intentionally unchanged so the
       direct primitive consumers (e.g. tokens.js JS bridge) keep their
       legacy values. Promote honey/slate to proper primitives once locked. */

    --bg:         #111E38;                     /* Delft Blue (was #1A2C42 Slate) */
    --panel-text: #111E38;                     /* dark text for transparent panel surfaces */
    --panel:      #111E38;
    --accent:        #F5C25E;                     /* honey gold (was --coral-500) */
    --accent-hover:  #FAD17C;                     /* lighter honey for :hover */
    --accent-active: #D9A845;                     /* darker honey for :active */
    --accent-dim:    rgba(245,194,94,0.16);
    --accent-on:     #2C1F02;                     /* warm dark, drawn ON --accent */
    --text:       #FFF4E0;                     /* warm cream */
    --text-secondary: rgba(255,244,224,0.78);  /* cream @ 0.78 — secondary text on dark (DESIGN.md principle 3, same hue as --text) */
    --muted:      #9BA9BC;                     /* cool grey (was Jordy blue) — cool-weather chips only; NOT secondary text */
    --card-bg:    rgba(17,30,56,0.40);
    --border:     rgba(17,30,56,0.12);         /* dark subtle outline for cream map */
    --pill-bg:    rgba(17,30,56,0.45);

    /* Glass levels — cream-map recipe. FLAT solid fills only — no gradients,
       no saturate (saturate amplified the map's pin colors into a blue cast).
       Panel  = transparent lens (Tier 1), depth comes from a drop shadow.
       Card   = fully opaque Delft Blue island (Tier 2).
       Action = semi-dark raised pill for glass-on-glass controls (Tier 3). */
    --glass-panel-bg:  rgba(17,30,56,0.00);
    --glass-card-bg:   rgba(17,30,56,1.00);
    --glass-action-bg: rgba(17,30,56,0.55);
    --glass-blur-panel: blur(6px);
    --glass-blur-card:  blur(6px);
    --glass-blur-action: blur(8px);
    --glass-blur-pill:  blur(8px);
    /* Heavy "first-impression" frost — the accept panel + (now) the venue
       list and top bar. Pure blur over an alpha-0 Delft base. */
    --glass-blur-frost: blur(22px);
    --glass-border: 1px solid rgba(17,30,56,0.12);
    --glass-border-light: 1px solid rgba(17,30,56,0.08);
    /* Null inset — no sheen on transparent panels. Using a transparent shadow
       rather than `none` so it remains valid in comma-separated box-shadow lists. */

    /* ── Cream-redesign surface system ──────────────────────────────────────
       THREE surface roles. Every element belongs to exactly one. Text/icon
       colour is determined by the role, not by the element:

         A · Floating bar (transparent)  → #top-strip, #panel, #detail-panel,
              FTS track. Backdrop = blurred map. Direct text = DARK (#111E38).
              Controls sitting on it use the glass-on-glass pill below.
         B · Card (opaque Delft Blue)     → .venue-card, .dp-card.
              Text/icons = CREAM (--text). Never put dark text here.
         C · Content panel (cream frosted)→ calendar, invite, accept/plan-preview,
              profile, search/bell dropdowns, sort. Predictable backdrop, so
              text = DARK (#111E38), muted = rgba(17,30,56,0.55). */
    --content-bg:    rgba(255,242,235,0.80);   /* cream @ 80% — 50% was too murky to read */
    --content-blur:  blur(6px);
    --content-text:  #111E38;
    --content-muted: rgba(17,30,56,0.55);

    /* Glass-on-glass control pill — for controls on a transparent bar (Surface
       A). Flat semi-dark fill that PROTRUDES via a drop shadow; presses IN via
       an inset shadow on :active (same language as the FTS thumb). Icons/text
       on these = CREAM. No gradients. */
    --ctl-bg:        rgba(17,30,56,0.50);
    --ctl-bg-hover:  rgba(17,30,56,0.62);
    --ctl-icon:      #FFF4E0;
    --ctl-raise:     0 2px 6px rgba(17,30,56,0.22);
    --ctl-press:     inset 0 2px 5px rgba(17,30,56,0.40);

    /* Transparent glass control — a clear frosted bubble on a glass bar with a
       DARK icon. For see-through utility controls (top-strip buttons, filter
       pills). Reads as glass, lifted by a bright rim + soft shadow. */
    --glassctl-bg:        rgba(255,250,244,0.28);
    --glassctl-bg-hover:  rgba(255,250,244,0.50);
    --glassctl-border:    1px solid rgba(255,255,255,0.45);
    --glassctl-icon:      rgba(17,30,56,0.82);
    --glassctl-raise:     0 1px 4px rgba(17,30,56,0.13);
    --glassctl-press:     inset 0 2px 4px rgba(17,30,56,0.18);

    /* Drop shadow lifting a transparent floating bar off the cream map.
       Two layers: a tight contact shadow + a soft ambient lift so panels
       read as clearly floating, not flat. */
    --panel-shadow:  0 2px 6px rgba(17,30,56,0.14), 0 10px 28px rgba(17,30,56,0.18);
    /* Upward shadow for bottom sheets (invite, accept, confirm). */
    --sheet-shadow:  0 -8px 32px rgba(17,30,56,0.18);
    /* Draggable glass-bead thumb (FTS slider + zoom-jog) — ONE shared lens
       material so both thumbs read identically. Rim + top-light catch + bottom
       inner shade + tight contact + soft lift define the bead at 0% fill. */
    --thumb-bead-border:        1px solid rgba(17,30,56,0.25);
    --thumb-bead-shadow:        inset 0 1px 0 rgba(255,250,235,0.22), inset 0 -1px 0 rgba(15,30,55,0.42), 0 0 0 0.5px rgba(15,27,42,0.50), 0 1px 2px rgba(0,0,0,0.28), 0 4px 10px rgba(0,0,0,0.32);
    --thumb-bead-shadow-active: inset 0 1px 0 rgba(255,250,235,0.18), inset 0 -1px 2px rgba(15,30,55,0.50), 0 0 0 0.5px rgba(15,27,42,0.55), 0 1px 2px rgba(0,0,0,0.30);
    --thumb-bead-press-fill:    rgba(17,30,56,0.12);
    /* Flat zoom-jog thumb — a simple ring (no bead shadow), so its border
       carries more weight than the shadow-backed bead rim above. */
    --thumb-flat-border:        1.5px solid rgba(17,30,56,0.40);
    /* Floating-label halo — white text legible over any map band. CSS mirror
       of render-pins' 4×-stacked canvas glow (the venue name labels). Used by
       the brand wordmark; reuse for any DOM label floating over the map. */
    --label-halo: 0 0 3px rgba(0,0,0,0.60), 0 0 6px rgba(0,0,0,0.32);  /* softer than the canvas labels' 4× glow — toned down per review */
    --label-text: #FFFFFF;                       /* pure white — matches render-pins LIGHT_FILL */
    --label-mark-shadow: drop-shadow(0 1px 2px rgba(0,0,0,0.28));  /* light lift for the mark over the map */
    /* Dark muted text for transparent (A) + cream (C) surfaces — replaces
       --muted (#9BA9BC slate) which is illegible on light backgrounds.
       0.70 (was 0.55) so it clears WCAG AA on the cream dropdown + Jordy-25%
       chrome — measured in scripts/contrast-audit.mjs (Phase 6). */
    --ink-muted:     rgba(17,30,56,0.70);

    /* Card-pill subject colors — kept distinct from --muted/--accent so the
       "Skyer" (cool gray) and "Regn" (blue) pill text reads cleanly on glass.
       Slate-tuned: cool = lighter slate-grey, rain = mid slate-blue. */
    --cool: #B5BCC8;
    --rain: #6F8AA8;

    /* ── DESIGN.md surface system + scales (final · 2026-05-21) ───────────────
       Added in Phase 0 of the design-system overhaul. ADD-ONLY: NOTHING
       consumes these yet — components are repointed surface-by-surface in
       Phase 2, so this block is a zero-visual-change addition. Values are the
       locked DESIGN.md spec — do NOT dial them here. The legacy --glass-*,
       --space-1..12, --text-xs..3xl, --content-*, --ctl-*, --glassctl-* and
       --radius-lg(16px) tokens stay until their consumers migrate away.
       (--radius-lg becomes 20px in the Phase 2 radius pass; left at 16 here so
       its current consumers don't shift.) */

    /* Surface fills — one per surface; colour is role-assigned, never dialed. */
    --surface-chrome:  rgba(156,189,231,0.25);  /* Jordy 25% — chrome panel */
    --surface-sheet:   rgba(156,189,231,0.25);  /* Jordy 25% — sheet (raised by shadow) */
    --surface-content: rgba(17,30,56,0.90);     /* Delft Blue 90% — content card */
    --surface-modal:   rgba(17,30,56,0.90);     /* Delft Blue 90% — modal, cream text */
    --surface-raised:  #FFF2EB;                  /* Cream, opaque — dropdown / popover */
    --surface-control: rgba(156,189,231,0.01);  /* Jordy ~1% — outline-defined chip */
    --scrim:           rgba(17,30,56,0.55);      /* Delft Blue 55% — behind modals/sheets */

    /* Blur — small, subtle frost. */
    --blur-control: blur(2px);
    --blur-surface: blur(4px);

    /* Shadow — flat lift; strength tracks elevation. */
    --shadow-1: 0 1px 2px rgba(0,0,0,0.18), 0 4px 14px rgba(0,0,0,0.30);  /* resting: chrome panel, card */
    --shadow-2: 0 8px 28px rgba(0,0,0,0.40);                              /* raised: sheet, dropdown */
    --shadow-3: 0 16px 48px rgba(0,0,0,0.50);                             /* pop: modal */
    /* Legibility casing — tight dark halo (drop-shadow) so a thin outline glyph
       stays readable over ANY background (weather over the map / FTS). */
    --icon-casing: drop-shadow(0 0 1.5px rgba(0,0,0,0.85)) drop-shadow(0 1px 1px rgba(0,0,0,0.5));

    /* ── Cream-frost content world (2026-05-27) ───────────────────────────────
       The detail + invite + accept SHEETS are pure-frost (transparent fill +
       blur, ink text — like .dpacc-panel). CARDS inside them step up off the
       frost as a translucent CREAM tile (not the dark Delft of the old content
       world). Rolling out panel-by-panel; the venue-list cards + modals stay
       Delft until their own pass. See DESIGN.md "Cream-frost content world". */
    /* TWO glass hues (tunable in glass-lab.html). CARD hue = content surfaces
       (dp-card, plans, info) — more opaque so dense text stays legible. BUTTON
       hue = the detail-panel action controls (directions/share/fav/bell), the
       lighter top-bar .ts-btn glass-control look (transparent + bright rim). */
    --card-cream-bg:     rgba(253,244,234,0.70);  /* card fill (lab-tuned 2026-05-27) */
    --card-cream-blur:   blur(10px);
    --card-cream-border: rgba(255,255,255,0.87);   /* bright lens rim (lab-tuned) */
    --card-cream-bg-hover: rgba(255,250,244,0.88);  /* brighter cream on list-card hover */
    --paper-cream:       #FDF4EA;             /* SOLID cream — opaque sheets that stack over other panels (no bleed, no costly blur) */
    --card-cream-shadow: 0 1px 2px rgba(17,30,56,0.10), 0 8px 22px rgba(17,30,56,0.14);  /* soft lift off the frost */
    --rain-ink:          #3F6088;             /* deeper rain blue — legible as pill text on a cream card */
    --skel-sheen-light:  rgba(17,30,56,0.05); /* skeleton sweep on a cream card */
    /* BUTTON hue — mirrors the top-bar control (.ts-btn / --glassctl-*) but kept
       separate so tuning the detail buttons doesn't move the top bar. */
    --btn-glass-bg:        rgba(255,250,244,0.28);
    --btn-glass-bg-hover:  rgba(255,250,244,0.50);
    --btn-glass-border:    rgba(255,255,255,0.45);
    --btn-glass-border-active: rgba(255,255,255,0.78);  /* brighter rim on an active toggle (fav/bell on) */
    --btn-glass-blur:      blur(8px);
    --btn-glass-icon:      rgba(17,30,56,0.82);
    --btn-glass-raise:     0 1px 4px rgba(17,30,56,0.13);
    --btn-glass-press:     inset 0 2px 4px rgba(17,30,56,0.18);
    /* Honey on a LIGHT surface. The problem: honey + cream sit at the same
       lightness, so #F5C25E washes out on cream. TWO tools:
       · --accent-on-light — a DEEPER amber gold for honey TEXT / glyphs on a
         light card. This is the primary fix (a colour shift reads crisp; a
         drop-shadow on light text just looks blurry). Same hue, deeper value.
       · --accent-casing — a tight dark drop-shadow, only for honey FILLS/icons
         where the colour can't change (a solid honey CTA on light, a glyph). */
    --accent-on-light:   #B8830C;
    --accent-casing:     drop-shadow(0 1px 1.5px rgba(17,30,56,0.32));
    /* Soft dark bloom for the few honey TEXTS that must stay honey on a light
       card (sun-hours, the "+Xh" opportunity) — a gentler, Delft-toned cousin of
       --label-halo (the map venue-label glow). Dark halo behind light honey =
       legible on cream without a hard outline. */
    --accent-bloom:      0 0 2px rgba(17,30,56,0.45), 0 0 5px rgba(17,30,56,0.26);

    /* Borders — Jordy on DARK surfaces, Delft Blue on LIGHT; compose `1px solid var(--line-*)`. */
    --line-d-faint:  rgba(156,189,231,0.08);   /* divider on dark */
    --line-d:        rgba(156,189,231,0.18);   /* edge on dark (content card, modal) */
    --line-d-strong: rgba(156,189,231,0.30);
    --line-l-faint:  rgba(17,30,56,0.08);      /* divider on light */
    --line-l:        rgba(17,30,56,0.18);      /* edge on light (chrome, sheet, dropdown) */
    --line-l-strong: rgba(17,30,56,0.30);      /* control silhouette — outline chip on chrome */

    /* Element-opacity states (applied to the whole element). */
    --o-disabled:       0.40;
    --o-muted:          0.55;
    --o-secondary-text: 0.78;   /* cream secondary text */

    /* Skeleton loading — cream-at-low-opacity blocks read as *absence of
       content* (not blue content); sheen is the moving highlight. Keep OUT
       the diagonal stripe (stripe = shade, not loading). DESIGN-FIXES → Skeletons. */
    --skel-block: rgba(255,244,224,0.09);  /* placeholder block on the Delft skeleton card */
    --skel-sheen: rgba(255,244,224,0.06);  /* sweeping highlight mid-stop */
    --fill-track: rgba(255,242,235,0.06);  /* card sun-fill bar track (cream wash on Delft) */

    /* Radius — DESIGN.md set. --radius-sm/md/pill already match above; only
       --radius-none is genuinely new. --radius-lg stays 16 here (→ 20 in Phase 2). */
    --radius-none: 0;

    /* Spacing — 4px base + 2px/6px fine sub-steps. New names; replaces the old
       --space-1..12 (which omitted 2/6) once consumers migrate. */
    --space-2xs: 2px;
    --space-xs:  4px;
    --space-sm:  6px;
    --space-md:  8px;
    --space-lg:  12px;
    --space-xl:  16px;
    --space-2xl: 24px;
    --space-3xl: 32px;
    --space-4xl: 48px;

    /* Type — six roles (Inter). DESIGN.md specifies the role VALUES (size /
       weight / tracking) but not variable names; these role-based names are a
       Phase 0 proposal, wired in the Phase 2 type pass (rename then if desired).
       Inputs 16px (prevents iOS auto-zoom). Tabular figures are applied per
       element via `font-variant-numeric: tabular-nums`, not a custom property. */
    --text-display:  28px;  --fw-display:  900;  --tracking-display: -0.03em;
    --text-title:    22px;  --fw-title:    700;  --tracking-title:   -0.01em;
    --text-subtitle: 18px;  --fw-subtitle: 600;  /* tracking: normal */
    --text-body:     15px;  --fw-body:     400;  /* tracking: normal */
    --text-label:    13px;  --fw-label:    500;  /* tracking: normal */
    --text-caption:  11px;  --fw-caption:  400;  /* tracking: normal */
    --text-input:    16px;

    /* ────────────────────────────────────────────────────────────────────
       VIEWPORT + SAFE-AREA TOKENS — read this before adding any panel
       ────────────────────────────────────────────────────────────────────
       Standards-based (2026) cross-host adaptation. The runtime no longer
       needs custom JS to figure out the visible viewport — modern CSS
       primitives handle every host correctly. The contract:

         1. <meta viewport> declares `viewport-fit=cover` (unlocks
            env(safe-area-inset-*) on iOS) and `interactive-widget=
            resizes-content` (Android keyboard now shrinks the layout
            viewport — same as iOS — so svh/dvh/% all reflect it).
         2. Bottom-anchored sheets use `bottom: 0` + `max-height: Xsvh`
            (small viewport, never clipped by chrome) + `padding-bottom:
            var(--app-pad-b)`.
         3. `--app-pad-b` collapses while a form field is focused — see
            the :has(:focus-visible) rule below — so the sheet hugs the
            keyboard instead of leaving a stale safe-area gap above it.

       --app-pad-b — `max(env(safe-area-inset-bottom), 12px)`.
                     iOS PWA: env() ≈ 34px (home indicator). Android
                     Chrome 135+ PWA: env() ≈ 24–48px (gesture chin —
                     edge-to-edge migration now reports it). Older
                     Android / desktop: env() = 0, falls back to 12px.
                     This single signal is correct on every host — no
                     more JS-tracked --app-bottom-inset hack required.

       --app-h      — visualViewport.height in px (JS-tracked, 100svh
                     fallback). Retained for legacy panel rules and the
                     few JS measurements that need exact pixel math.
                     New code should prefer `svh` directly. See JS
                     viewport tracker block in <head>.

       Drag-to-dismiss math (NOT layout) still uses window.visualViewport
       in JS — that API is the only way to read pinch-zoom + keyboard
       offsets for gesture handlers. Layout no longer needs it.
    */
    --app-pad-b: max(env(safe-area-inset-bottom, 0px), 12px);
  }
  /* Keyboard-open override — when a form field is focused, env(safe-
     area-inset-bottom) doesn't change but the keyboard now overlaps the
     home-indicator zone. The 34px (iOS) or ~30px (Android) inset becomes
     dead space ABOVE the keyboard, leaving the sheet floating mid-screen.
     Collapse to 8px so the sheet hugs the keyboard. (Webventures 2025
     pattern, https://webventures.rejh.nl/blog/2025/safe-area-inset-
     bottom-does-not-update/) */
  :root:has(:is(input, textarea, [contenteditable]):focus-visible) {
    --app-pad-b: 8px;
  }

