/* ==========================================================================
   Persona Studio v2 — tokens.css
   The design primitives. Nothing downstream declares a raw colour, a raw px
   spacing value, a raw radius, a raw duration or a raw easing curve.

   GENERATED. Source of truth: scratchpad/contrast.mjs (the palette objects)
   via scratchpad/gen-tokens.mjs. Every colour token below has been through
   the WCAG verifier in that file — 354 pairings, 0 failing — and the four
   theme blocks are emitted from one palette so they cannot drift apart. Edit
   the palette and regenerate; do not hand-edit the colour sections.

   Every token is prefixed --ps-. This is not cosmetic: persona-studio.php
   loads the shared site nav on the same document, which owns unprefixed names
   (--transition-speed, --text-tertiary, --primary-color …). An unprefixed
   token here would be silently overwritten by, or would silently overwrite,
   the site chrome.

   Theme resolution order — data-theme wins in BOTH directions (SPEC §8):
     1. :root                                        -> dark (default)
     2. @media (prefers-color-scheme: light)
          :root:not([data-theme="dark"])              -> light, unless forced dark
     3. :root[data-theme="light"]                     -> light, always
     4. :root[data-theme="dark"]                      -> dark, always
   Blocks 2 and 4 have equal specificity (0,2,0), so 4 is placed last and wins
   on source order. That ordering is load-bearing; do not reorder the file.

   Count of "important" overrides in this file: zero. The token layer has no
   cascade to fight, so it never needs one.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. NON-COLOUR PRIMITIVES  (theme independent)
   -------------------------------------------------------------------------- */
:root {

  /* --- 1.1 Font stacks -----------------------------------------------------
     System faces only: nothing is fetched, so there is no font in the render
     path and font-display is moot. v1 pulled 100KB+ of Font Awesome CSS for a
     handful of glyphs; icons become an inline SVG sprite instead. --- */
  --ps-font-sans:
      -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", Roboto,
      "Helvetica Neue", Arial, sans-serif;
  --ps-font-mono:
      ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
      "Liberation Mono", monospace;
  /* Persona names and the hero: a touch more character than the UI face. */
  --ps-font-display: var(--ps-font-sans);

  /* --- 1.2 Type scale ------------------------------------------------------
     Ten steps replacing v1's 29 ad-hoc sizes, which included seven
     near-identical values inside a 4px range (0.85/0.875/0.9/0.95/1/1.05/1.1rem)
     and freely mixed px, rem and em. Steps are far enough apart to read as
     intentional: 12 13 14 16 18 20 24 30 38 48.

     The floor is 13px for anything a user reads. v1 set tag chips at 11.2px
     AND at 3.78:1 — a compound failure on the most repeated element on the
     page. --ps-fs-2xs (12px) is reserved for uppercase overlines with
     letter-spacing, where the caps height carries the legibility, and is
     never used for prose. --- */
  --ps-fs-2xs: 0.75rem;    /* 12 — overlines/eyebrows ONLY, with --ps-ls-label */
  --ps-fs-xs:  0.8125rem;  /* 13 — floor for readable text: timestamps, tag chips */
  --ps-fs-sm:  0.875rem;   /* 14 — secondary UI: labels, helper text, meta rows */
  --ps-fs-md:  1rem;       /* 16 — BODY DEFAULT: chat messages, descriptions */
  --ps-fs-lg:  1.125rem;   /* 18 — card titles, lead paragraph */
  --ps-fs-xl:  1.25rem;    /* 20 — persona name in the chat header, h3 */
  --ps-fs-2xl: 1.5rem;     /* 24 — dialog + section headings, h2 */
  --ps-fs-3xl: 1.875rem;   /* 30 — view heading, h1 on interior views */
  --ps-fs-4xl: 2.375rem;   /* 38 — hero, small viewports */
  --ps-fs-5xl: 3rem;       /* 48 — hero, desktop */
  /* One fluid step so the hero needs no breakpoint of its own. */
  --ps-fs-hero: clamp(2.375rem, 1.55rem + 2.6vw, 3rem);

  /* --- 1.3 Line height ----------------------------------------------------
     v1 had zero tokens and only 18 line-height declarations in 3822 lines, so
     most components inherited 'normal' — which is why persona names crowded
     when they wrapped. Display text gets tighter leading as it gets larger. */
  --ps-lh-flat:    1;      /* icon boxes, numerals, single-line badges */
  --ps-lh-tight:   1.15;   /* --ps-fs-4xl and above */
  --ps-lh-snug:    1.3;    /* headings, card titles, persona names */
  --ps-lh-normal:  1.5;    /* UI text, labels, buttons */
  --ps-lh-relaxed: 1.65;   /* chat messages and long-form prose */

  /* --- 1.4 Weight --------------------------------------------------------- */
  --ps-fw-normal:    400;
  --ps-fw-medium:    500;  /* default for UI labels: 400 reads thin on dark */
  --ps-fw-semibold:  600;  /* card titles, buttons, headings */
  --ps-fw-bold:      700;  /* view headings */
  --ps-fw-extrabold: 800;  /* hero display only */

  /* --- 1.5 Letter spacing -------------------------------------------------
     Optical correction, not decoration: large text needs negative tracking,
     uppercase micro-labels need positive. */
  --ps-ls-display: -0.022em;  /* --ps-fs-4xl / 5xl / hero */
  --ps-ls-tight:   -0.011em;  /* --ps-fs-2xl / 3xl */
  --ps-ls-normal:  0;
  --ps-ls-label:    0.06em;   /* uppercase overlines at --ps-fs-2xs */

  /* --- 1.6 Spacing: a strict 4px grid ------------------------------------
     v1 declared six --space-* tokens and used them ZERO times, spending 39
     distinct raw values instead (0.35rem, 0.45rem, 0.6rem, 0.625rem, 0.65rem,
     15px, 18px, 22px, 26px, 28px …). There was no rhythm because there was no
     scale in practice. This scale is deliberately complete at the top end —
     a consumer product needs 64/80/96 for hero and section rhythm, and if the
     scale stops at 48 authors will write 80px by hand. --- */
  --ps-s-0:  0;
  --ps-s-h:  2px;    /* half step: hairline gutters and optical nudges only */
  --ps-s-1:  4px;
  --ps-s-2:  8px;
  --ps-s-3:  12px;
  --ps-s-4:  16px;
  --ps-s-5:  20px;
  --ps-s-6:  24px;
  --ps-s-7:  32px;
  --ps-s-8:  40px;
  --ps-s-9:  48px;
  --ps-s-10: 64px;
  --ps-s-11: 80px;
  --ps-s-12: 96px;

  /* --- 1.7 Radii ---------------------------------------------------------
     Agent Rooms caps at 6px because it is an instrument. This is a warm
     consumer chat product, so the scale runs to 16px (SPEC §8 ceiling) and
     stays on the 4px grid. v1 had 6 tokens plus 13 raw values including
     20/22/25px near-duplicates of its own 16/24px tokens, and a 1.5px typo. */
  --ps-r-1:      4px;
  --ps-r-2:      8px;
  --ps-r-3:      12px;
  --ps-r-4:      16px;
  --ps-r-pill:   999px;
  --ps-r-circle: 50%;
  /* Semantic aliases. Authors reach for these; the numbered steps exist so
     the aliases have something to point at. */
  --ps-r-chip:    var(--ps-r-pill);
  --ps-r-control: var(--ps-r-2);   /* inputs, buttons, selects */
  --ps-r-card:    var(--ps-r-4);   /* persona cards, panels */
  --ps-r-modal:   var(--ps-r-4);
  --ps-r-avatar:  var(--ps-r-circle);
  /* Chat bubbles: three soft corners and one tight corner on the tail side,
     which is what makes a bubble read as spoken rather than as a box. */
  --ps-r-bubble:      var(--ps-r-4);
  --ps-r-bubble-tail: var(--ps-r-1);

  /* --- 1.8 Borders ------------------------------------------------------- */
  --ps-bw:        1px;
  --ps-bw-strong: 2px;   /* selected chip, invalid field, active tab */

  /* --- 1.9 Icon sizes ---------------------------------------------------
     v1 sized icons at 12 different values with no fixed box, so the sidebar's
     icon column did not align (measured 26/20/20px at 390px wide).
     --ps-icon-box is that missing fixed column. */
  --ps-icon-sm:  16px;
  --ps-icon-md:  20px;
  --ps-icon-lg:  24px;
  --ps-icon-xl:  32px;
  --ps-icon-box: 24px;   /* fixed-width square every nav/list icon sits in */

  /* --- 1.10 Layout constants -------------------------------------------
     --ps-nav-h is the shared site nav, which is NOT ours: measured 65px at
     >=1440 (64px + a 1px border) and 64px below. v1 hardcoded calc(100vh -
     64px), so at desktop the shell was 1px too tall and the page grew a second
     scrollbar. app.js sets this from the real element on load and resize; the
     fallback is deliberately the LARGER value, because a stale token that is
     too big leaves a 1px gap while one that is too small reproduces the bug.

     --ps-card-min is chosen so the grid lands exactly 4 across at 1440:
     1440 - 264 sidebar - (2 x 32) gutters = 1112 = (4 x 260) + (3 x 24 gap). */
  --ps-nav-h:       65px;
  --ps-sidebar-w:   264px;  /* 240 in v1 cramped the icon column; 264 = 8px grid */
  /* ADDED BY css/app.css (see DESIGN.md §1): the app shell needs a measure, or
     at 2560px the discover grid keeps adding columns until the cards are the
     only thing on screen and the eye has nowhere to land. 1600 = --ps-bp-2xl,
     which is where the last column stops being useful. */
  --ps-shell-max:   1600px;
  --ps-card-min:    260px;
  --ps-prose-max:   68ch;   /* v1 centred prose at a ~105-character measure */
  /* ADDED BY css/app.css (see DESIGN.md §1). Two dialog measures, because the
     nine dialogs in partials/ are two shapes: a list/consent column and the
     two-column creator form. v1 sized each of its seven separately (620/700/
     800/900px + two at 90%), which is why no two lined up. */
  --ps-dialog-w:    720px;  /* leaderboard, history, my personas, consent */
  --ps-dialog-w-lg: 960px;  /* the creator form + the chat surface */
  /* ADDED BY css/app.css (see DESIGN.md §1): a third measure for js/modal.js's
     confirm() dialog, which is one sentence and two buttons. At --ps-dialog-w it
     was a 720px panel around nine words. */
  --ps-dialog-w-sm: 480px;
  --ps-chat-max:    760px;
  --ps-composer-max: 820px;
  --ps-tap-min:     44px;   /* SPEC §9 minimum touch target */

  /* --- 1.11 Breakpoints -------------------------------------------------
     Custom properties cannot be used inside a @media query, so these are
     documentation plus a JS-readable source of truth — app.js reads them so
     that a matchMedia in script can never disagree with the stylesheet.
     THE CANONICAL SET IS THESE FIVE. v1 spread 15 media queries across ten
     values (480, 520, 640, 768, 859, 860, 867, 1024, 1280, 1400): the nav
     switched at 859/860 and the sidebar at 867/868, leaving a real 7px band
     where the page had neither a hamburger nor a sidebar nor any menu at all. */
  --ps-bp-sm: 480px;
  --ps-bp-md: 768px;
  --ps-bp-lg: 1024px;
  --ps-bp-xl: 1280px;
  --ps-bp-2xl: 1600px;

  /* --- 1.12 Motion ------------------------------------------------------
     v1: --transition-fast and --transition-slow had 0 uses, canonical
     --transition had 10, and the compat alias --transition-speed had 40 — the
     alias had become the real API. There were no easing tokens at all, so
     every transition was bare 'ease'. Most of the perceived quality of a
     well-built product lives in these curves. --- */
  --ps-dur-1: 90ms;    /* colour/opacity on small controls */
  --ps-dur-2: 140ms;   /* default: hover, focus, chip toggle */
  --ps-dur-3: 200ms;   /* dropdowns, tooltips, chip reflow */
  --ps-dur-4: 280ms;   /* dialogs, drawers, sheet entry */
  --ps-dur-5: 420ms;   /* full-screen or celebratory transitions only */

  /* Entrances decelerate; exits accelerate; anything reversible is symmetric.
     --ps-ease-spring overshoots and is reserved for affordances the user
     directly caused (send, like, avatar pop) — expressive, but rationed. */
  --ps-ease-out:    cubic-bezier(0.22, 1, 0.36, 1);
  --ps-ease-in:     cubic-bezier(0.55, 0, 1, 0.45);
  --ps-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ps-ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --ps-ease-linear: linear;   /* progress + shimmer only; never for movement */

  /* Composed shorthands, so a component writes one token not two. */
  --ps-t-fast:   var(--ps-dur-1) var(--ps-ease-out);
  --ps-t-base:   var(--ps-dur-2) var(--ps-ease-out);
  --ps-t-slow:   var(--ps-dur-3) var(--ps-ease-out);
  --ps-t-modal:  var(--ps-dur-4) var(--ps-ease-out);
  --ps-t-spring: var(--ps-dur-3) var(--ps-ease-spring);

  /* Long-running animation periods. Tokenised so §6 can stop them too: a
     blanket transition-duration override does not stop an infinite keyframe
     animation, which is how v1's sparkle survived reduced-motion in spirit. */
  --ps-dur-shimmer: 1400ms;   /* skeleton sweep */
  --ps-dur-typing:  1200ms;   /* three-dot typing indicator */
  --ps-dur-aurora:  24s;      /* ambient background drift */

  /* --- 1.13 Z-index ----------------------------------------------------
     Two hard external constraints, neither of which this product owns:
     the shared site nav is z-index 100, and the shared TPIUpgradeModal
     overlay is 1000000. Everything in-page therefore sits BELOW 100, and
     every layer that must cover the nav sits between 300 and 800.
     v1's live values were 9998, 9999, 99998, 100000 and 1000000 —
     escalation by increment rather than a scale. */
  --ps-z-base:     0;
  --ps-z-raised:   10;
  --ps-z-sidebar:  80;    /* below the shared nav (100), deliberately */
  --ps-z-sticky:   90;    /* sticky category bar: tucks under the nav */
  --ps-z-dropdown: 300;
  --ps-z-popover:  400;
  --ps-z-overlay:  500;   /* modal scrim */
  --ps-z-modal:    600;
  --ps-z-chat:     700;
  /* The ONE deliberate exception. Toasts must outrank the shared upgrade
     modal (1000000, external, not ours to change) or a confirmation is
     invisible behind it — v1's toasts were unreachable for exactly this
     reason. This is a named decision with a reason, not an increment. */
  --ps-z-toast:    1000010;

  /* --- 1.14 Focus ring geometry ----------------------------------------
     v1 had no focus token at all and consequently three competing focus
     languages plus controls with none. Two shapes are provided: an outline
     for elements that can afford the extra 4px, and a box-shadow ring for
     elements inside an overflow:hidden or tightly packed context, where an
     outline would be clipped. --- */
  --ps-focus-w:      2px;
  --ps-focus-offset: 2px;
  --ps-focus-outline: var(--ps-focus-w) solid var(--ps-focus);
  --ps-focus-ring:
      0 0 0 var(--ps-focus-w) var(--ps-focus),
      0 0 0 calc(var(--ps-focus-w) + var(--ps-focus-offset)) var(--ps-focus-halo);
  /* For controls sitting on a persona photo or the brand gradient. */
  --ps-focus-ring-media:
      0 0 0 var(--ps-focus-w) var(--ps-focus-media),
      0 0 0 calc(var(--ps-focus-w) + var(--ps-focus-offset)) var(--ps-focus-media-halo);

  /* --- 1.15 Truncation fade: the structural fix ------------------------
     v1 faded a clamped description to var(--bg-primary) #0D0C1D while the card
     it sat on composited to #141324. The scrim was DARKER than its own
     background, so instead of dissolving it painted a hard grey rectangle
     across the text — the single most visible defect in the product. It also
     ran 2.2rem tall over a 1.275rem line-height, veiling 1.7 of 3 lines.

     The fix is structural, not a corrected hex: fade with a MASK, not with a
     painted gradient. A mask removes pixels, so it has no colour of its own
     and CANNOT mismatch the surface underneath it — the bug is unexpressible.
     It also works unchanged in both themes and over the aurora and imagery.

         .persona-desc {
           -webkit-mask-image: var(--ps-fade-b);
                   mask-image: var(--ps-fade-b);
         }

     --ps-fade-h is one line-height, not two: a fade should suggest a next
     line, not bury half the paragraph. --- */
  --ps-fade-h: 1.5em;
  --ps-fade-b: linear-gradient(to bottom,
      #000 0,
      #000 calc(100% - var(--ps-fade-h)),
      transparent 100%);
  /* ADDED BY css/app.css (see DESIGN.md §7). The horizontal falloff is a
     PARAMETER, not a constant, because the right distance depends on how wide
     the things being dissolved are. Custom properties resolve at use time, so an
     element that needs a longer dissolve sets --ps-fade-w on itself and
     --ps-fade-r follows; nothing needs a second gradient token.

     32px (--ps-s-7) is right for the chat starters strip, whose chips are a
     whole sentence wide. It was measurably WRONG for the category rail, whose
     chips are ~150px: 32px of falloff over a 150px pill cut the third chip
     ("Coding & Dev") through the middle of a word with most of the pill still at
     full opacity, so it read as a clipping fault rather than as a fade. The rail
     raises it to 48px, which dissolves a third of a chip. */
  --ps-fade-w: var(--ps-s-7);
  --ps-fade-r: linear-gradient(to right,
      #000 0,
      #000 calc(100% - var(--ps-fade-w)),
      transparent 100%);

  /* --- 1.16 Control marks (ADDED BY css/app.css — see DESIGN.md §7) ----
     Two SHAPE tokens, not colour tokens. A checkbox has to paint a tick inside
     itself, and the only way to do that on an `appearance: none` input without
     a pseudo-element on a replaced element (which Firefox does not render) is a
     background image. An SVG data URI cannot read currentColor, so the stroke
     has to be baked in — and a baked hex belongs in the token layer, never in
     app.css, which is why these live here.

     Two variants, because a tick has to sit on two kinds of fill:
       --ps-img-check       white stroke, for a tick on --ps-accent / --ps-error
                            (--ps-on-accent is #FFFFFF in BOTH themes, so one
                            token covers both).
       --ps-img-check-dark  near-black stroke, for a tick on --ps-warm, whose
                            on-colour (--ps-on-warm #231402) is dark in both
                            themes. Used by the Pro-only filter, where a violet
                            tick inside an amber pill fought the pill.
     The stroke geometry (viewBox 16, width 2.4, round caps) is tuned to read at
     16px; do not scale the artwork by changing background-size alone. --- */
  --ps-img-check:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M13.2 4.4 6.3 11.5 2.8 8' fill='none' stroke='%23FFFFFF' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --ps-img-check-dark:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M13.2 4.4 6.3 11.5 2.8 8' fill='none' stroke='%23231402' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  /* The dash a partially-selected (indeterminate) box shows. */
  --ps-img-dash:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.6 8h8.8' fill='none' stroke='%23FFFFFF' stroke-width='2.4' stroke-linecap='round'/%3E%3C/svg%3E");

  /* --- 1.17 Aurora geometry -------------------------------------------
     The backdrop the glass refracts. v1's aurora targeted a .persona-studio
     class that appears ZERO times in the markup (the element is
     .persona-studio-main), so ~45 lines of the signature background never
     rendered in production — which is why the page was flat and why
     backdrop-filter over a flat colour read grey rather than luminous.
     Blur is large and size is generous because a small blurred blob reads as
     a smudge; at this scale it reads as light. --- */
  --ps-aurora-blur: 100px;
  --ps-aurora-size: 46vw;
  --ps-glass-blur:  20px;
}

/* --------------------------------------------------------------------------
   2. DARK THEME  (default, and forced via data-theme="dark")
   The default because the product is a character chat surface used at night
   as much as by day, and because large avatar imagery sits better on a dark
   ground. The ground is violet-black (#0A0714), not neutral black: a warm
   product on a cold grey ground reads as a dashboard.

   This single block is resolution steps 1 AND 4. The selector list gives the
   bare :root default (0,1,0) and the forced-dark override (0,2,0) the same
   declarations without duplicating 60 colours, and it is exactly equivalent:
   :root:not([data-theme="dark"]) in block 3 and :root[data-theme="dark"] here
   are mutually exclusive, so they can never both match and source order
   between them is irrelevant; and against block 4's :root[data-theme="light"]
   (0,2,0) only the bare :root arm of this list matches, at (0,1,0), so forced
   light still wins. Verified in tokens-proof.html in all four combinations.
   -------------------------------------------------------------------------- */
:root,
:root[data-theme="dark"] {
  color-scheme: dark;

  /* --- Aurora backdrop. The opacity is applied to the WRAPPER, never to the
      individual blobs, so overlapping blobs cannot compound and the brightest
      point on the page is bounded at --ps-aurora-max. That bound is what makes
      text-over-aurora contrast provable rather than sampled. --- */
  --ps-aurora-1: #6D3BFF;
  --ps-aurora-2: #B23BFF;
  --ps-aurora-3: #FF4D80;
  --ps-aurora-opacity: 0.18;
  --ps-aurora-max: #3D1931;   /* computed, for reference + fallbacks */

  /* --- Page ground --- */
  /* Two grounds, not one. --ps-bg-deep is the page behind everything and the
     only surface the aurora paints on; --ps-bg is the app shell. The
     separation is what gives the aurora somewhere to live without bleeding
     under every panel. */
  --ps-bg-deep: #0A0714;
  --ps-bg:      #120E20;
  --ps-sunken:  #0D0A1A;

  /* --- Surface layers --- */
  /* The ladder is NESTING DEPTH, not luminance and not elevation. surface-1 is
     a card or panel on the page; -2 is a panel nested inside one; -3 is a chip
     or well inside that. Which DIRECTION that moves is theme-dependent and
     deliberately so: on dark, nesting gets lighter; on light, a card goes up to
     white (#FFFFFF) while nested wells go down into violet-tinted greys. Any
     system that forces one monotonic direction ends up with either grey cards
     on light or glowing wells on dark.
     
     The consequence to hold on to: DO NOT read surface-2 as "higher than
     surface-1". Elevation is carried by --ps-e-1..4 and by --ps-border, never
     by the surface index. On light in particular, one step of the ladder is a
     small luminance move (surface-2 #F0EBFA against --ps-bg #F6F3FC), so a
     control that must read as a control needs a border or a shadow as well as
     a surface — verified by rendering, where surface-2 buttons on the light
     page ground disappeared until they were given --ps-border.
     
     Every one is a flat opaque colour. v1 built surfaces from rgba() over the
     page, which is why nobody could predict what a card actually composited to
     (#141324, measured) and why the description scrim ended up darker than the
     card it was drawn on. */
  --ps-surface-1: #1B1533;
  --ps-surface-2: #241C42;
  --ps-surface-3: #2F2554;

  /* --- Surface interaction states --- */
  /* Opaque hover/active variants rather than a translucent veil. Because they
     are flat colours, the contrast of text on a hovered row is a measured
     number in the table, not a compositing accident. Use --ps-overlay-hover
     ONLY where the thing underneath is an image. */
  --ps-surface-1-hover:  #221A3E;
  --ps-surface-2-hover:  #2C2250;
  --ps-surface-3-hover:  #382C63;
  --ps-surface-1-active: #191430;
  --ps-surface-2-active: #211A3C;
  --ps-surface-3-active: #2A2049;

  /* --- Text layers --- */
  /* Three content tiers plus a disabled tier. All three content tiers clear
     4.5:1 on EVERY surface token above, including the glass composites and the
     brightest point of the aurora — so an author cannot pick a legal text
     token and an illegal background. v1 had five text colours of which three
     failed AA; this ramp is four, and --ps-text-disabled is the only one that
     is allowed to fail (WCAG 1.4.3 exempts disabled controls) and it is never
     permitted to carry content. */
  --ps-text-1:        #F7F4FF;
  --ps-text-2:        #CAC1E6;
  --ps-text-3:        #A79CC7;
  --ps-text-disabled: #7E7399;

  /* --- Borders --- */
  /* --ps-border is decorative separation and is NOT a control boundary: it
     sits around 1.5:1 by design, because a hairline that meets 3:1 on a dark
     ground looks like a wire cage. Anything that bounds a control or conveys
     state uses --ps-border-strong, which clears 3:1 (WCAG 1.4.11) on every
     surface. Choosing between them is the whole point of having two. */
  --ps-border:        #322A52;
  --ps-border-strong: #8C7FBA;

  /* --- Brand accent --- */
  /* One violet family, one gradient. v1 shipped a second, off-brand purple
     (#667eea -> #764ba2) injected from JavaScript behind nine "important"
     overrides — which is how a product ends up with two brands. --ps-accent is
     the fill; --ps-accent-text is a LIGHTER step, because the fill colour does
     not itself clear 4.5:1 as text on a dark ground, and using one value for
     both is the mistake that made v1 stat numbers 2.91:1. --ps-accent-ui is
     the brand as a non-text mark, at the 3:1 bar. */
  --ps-accent:        #7C4DFF;
  --ps-accent-hover:  #6234E8;
  --ps-accent-active: #4A24BF;
  --ps-accent-text:   #BBA0FF;
  --ps-accent-ui:     #BBA0FF;
  --ps-on-accent:     #FFFFFF;
  --ps-accent-tint:   #221152;
  --ps-accent-edge:   #4A24BF;

  /* --- Warm / Pro --- */
  /* The amber used for Pro, crowns and upgrade affordances. Warm is the
     product’s second voice: it marks aspiration, never error. */
  --ps-warm:      #FFB454;
  --ps-warm-text: #FFC978;
  --ps-on-warm:   #231402;
  --ps-warm-tint: #33240E;
  --ps-warm-edge: #6E4E1B;

  /* --- State colours --- */
  /* Each state ships five tokens, not one: the fill, a text-safe step, the
     on-fill label colour, a tint for banner backgrounds and an edge for their
     border. v1 had four bare state colours and consequently seven ad-hoc
     rgba()/hex state colours invented at the call site. */
  --ps-success:      #22C58A;
  --ps-success-text: #4FDBA8;
  --ps-on-success:   #04231A;
  --ps-success-tint: #0F2C24;
  --ps-success-edge: #1D5A45;
  --ps-warning:      #F5A524;
  --ps-warning-text: #FFC65C;
  --ps-on-warning:   #241503;
  --ps-warning-tint: #33260C;
  --ps-warning-edge: #6B4E14;
  --ps-error:        #F2555A;
  --ps-error-text:   #FF8A8E;
  --ps-on-error:     #2A0508;
  --ps-error-tint:   #331319;
  --ps-error-edge:   #6E2530;
  --ps-info:         #4C9BFF;
  --ps-info-text:    #8FC0FF;
  --ps-on-info:      #04162C;
  --ps-info-tint:    #10233D;
  --ps-info-edge:    #1E4470;

  /* --- Focus --- */
  /* Two-band ring. The inner band is brand-coloured and clears 3:1 against
     every surface token; the outer halo separates it from whatever it lands
     on. Over IMAGERY neither band can be guaranteed by theme, so
     --ps-focus-media / --ps-focus-media-halo lock to white-on-near-black: a
     backdrop cannot be within 3:1 of both white and black at once, so that
     pair is provably visible on any photo. See DESIGN.md §6. */
  --ps-focus:            #C0A6FF;
  --ps-focus-halo:       #0A0714;
  --ps-focus-media:      #FFFFFF;
  --ps-focus-media-halo: #07040F;

  /* --- Persona / category hues --- */
  /* Six distinguishable hues for category chips and avatar fallbacks, each
     with a tint to sit on. Every hue clears 4.5:1 as text on surface-1, on
     --ps-bg and on its own tint, so a category chip is legible whichever way
     it is composed. Never the sole carrier of meaning (SPEC §9). */
  --ps-hue-1:      #C0A0FF;
  --ps-hue-2:      #6FD3E8;
  --ps-hue-3:      #7BE0A8;
  --ps-hue-4:      #FFC978;
  --ps-hue-5:      #FF9EC4;
  --ps-hue-6:      #A8C7FF;
  --ps-hue-1-tint: #251A44;
  --ps-hue-2-tint: #0F2B33;
  --ps-hue-3-tint: #112E22;
  --ps-hue-4-tint: #33260C;
  --ps-hue-5-tint: #331624;
  --ps-hue-6-tint: #16233C;

  /* --- Leaderboard medals --- */
  /* Gold/silver/bronze are literal by nature and cannot be derived from the
     brand ramp. --ps-medal-edge gives ranks 1-3 a boundary that clears 3:1 on
     a pale surface, and --ps-on-medal is the numeral colour. Ranks 4+ use
     surface-3 with --ps-text-2 (v1 had no treatment past 3rd). */
  --ps-medal-1:    #E8B93B;
  --ps-medal-2:    #BFC7D2;
  --ps-medal-3:    #D08B57;
  --ps-on-medal:   #1A1206;
  --ps-medal-edge: #8A6A12;

  /* --- Text over imagery --- */
  /* A painted scrim, because the photo underneath is unknown. The floor
     opacity is set so --ps-on-scrim clears 4.5:1 even over a pure WHITE
     image, which is the hardest possible case rather than a sampled one. */
  --ps-on-scrim: #FFFFFF;

  /* --- Selection --- */
  --ps-select-bg: #331A85;
  --ps-select-fg: #F7F4FF;

  /* --- Skeletons --- */
  /* Shimmer stops. Skeletons match the shape of the content they replace
     (SPEC §8), so these are surface-adjacent, not grey. */
  --ps-skeleton-a: #241C42;
  --ps-skeleton-b: #2F2554;

  --ps-scrim-image-opacity: 0.78;
  --ps-scrim-image: linear-gradient(to top,
      rgba(7, 4, 15, 0.78) 0%,
      rgba(7, 4, 15, 0.42900000000000005) 45%,
      rgba(7, 4, 15, 0) 100%);

  /* --- Elevation --- */
  /* Four steps, tuned per theme. Elevation maps to the z-index tiers in §1:
     e-1 raised, e-2 dropdown/hover, e-3 popover/sticky, e-4 modal. v1 had 4
     shadow tokens used 12 times against 37 bespoke box-shadows. */
  --ps-e-1:     inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 1px 2px rgba(6, 3, 16, 0.50), 0 2px 8px rgba(6, 3, 16, 0.36);
  --ps-e-2:     inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 2px 6px rgba(6, 3, 16, 0.54), 0 10px 24px rgba(6, 3, 16, 0.46);
  --ps-e-3:     inset 0 1px 0 rgba(255, 255, 255, 0.09), 0 4px 10px rgba(6, 3, 16, 0.58), 0 18px 40px rgba(6, 3, 16, 0.54);
  --ps-e-4:     inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 10px 20px rgba(6, 3, 16, 0.62), 0 36px 72px rgba(6, 3, 16, 0.66);
  --ps-e-inset: inset 0 1px 3px rgba(6, 3, 16, 0.55);
  --ps-e-glow:  0 6px 24px rgba(124, 77, 255, 0.42);

  /* --- Scrim + glass --- */
  /* The aurora (§1.16) is what these refract. In v1 the aurora targeted a
     .persona-studio class that appears nowhere in the markup, so
     backdrop-filter: blur(20px) was blurring a flat colour and the glass read
     grey. Glass without a backdrop is just a lighter box. */
  --ps-scrim:          rgba(7, 4, 15, 0.72);
  --ps-glass-tint:     rgba(255, 255, 255, 0.08);
  --ps-glass-sheen:    rgba(255, 255, 255, 0.05);
  --ps-glass-hairline: rgba(255, 255, 255, 0.1);

  /* --- Overlays for imagery --- */
  /* Translucent, so ONLY for use where the thing underneath is an image or
     the aurora. On any solid surface use the opaque --ps-surface-N-hover. */
  --ps-overlay-hover: rgba(255, 255, 255, 0.07);
  --ps-overlay-press: rgba(7, 4, 15, 0.16);

  /* --- Gradients. Accents only: SPEC §8 forbids a gradient under text. ---
     …except --ps-grad-brand, which .ps-btn--primary puts under white label text,
     so BOTH of its stops have to clear 4.5:1 against --ps-on-accent (#FFFFFF).
     Contrast interpolates monotonically between two stops, so checking the two
     ends checks the whole ramp — the lightest end is the worst case.

     It did not. The end stop was #9A72FF: white on it is 3.37:1, and the Search
     button measured 4.14:1 composited. axe could not catch this — it reports
     `color-contrast` as INCOMPLETE ("background could not be determined due to a
     background gradient") and an incomplete is not a violation, so the page
     looked clean. Found by screenshotting with the glyphs made transparent and
     sampling the real composited pixels. Every primary button was affected, not
     just Search.

     Start darkened to give the ramp somewhere to travel while both ends pass:
       #6234E8 -> 6.61:1   (this is --ps-accent-hover, so it stays on-brand)
       #8256EA -> 4.70:1 */
  --ps-grad-brand: linear-gradient(135deg, #6234E8 0%, #8256EA 100%);
  --ps-grad-warm:  linear-gradient(135deg, #FFB454 0%, #E8B93B 100%);
  --ps-grad-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0) 100%);
}

/* --------------------------------------------------------------------------
   3. LIGHT THEME  (OS preference, unless dark is forced)
   Not an inversion. Same hue families, re-luminanced: the text ramp is
   re-derived against paper-white rather than lightened, and every state and
   hue colour is darkened until it clears 4.5:1 as text. Warm/Pro amber has to
   move furthest — #FFB454 is unusable as text on white, so light theme uses
   #8C5406 and keeps the amber only as a fill.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;

    /* --- Aurora backdrop --- */
    --ps-aurora-1: #6D3BFF;
    --ps-aurora-2: #B23BFF;
    --ps-aurora-3: #FF4D80;
    --ps-aurora-opacity: 0.1;
    --ps-aurora-max: #E8E1FC;   /* computed, for reference + fallbacks */

    /* --- Page ground --- */
    --ps-bg-deep: #EFEAF9;
    --ps-bg:      #F6F3FC;
    --ps-sunken:  #FBF9FE;

    /* --- Surface layers --- */
    --ps-surface-1: #FFFFFF;
    --ps-surface-2: #F0EBFA;
    --ps-surface-3: #E5DDF6;

    /* --- Surface interaction states --- */
    --ps-surface-1-hover:  #F7F3FD;
    --ps-surface-2-hover:  #E8E1F6;
    --ps-surface-3-hover:  #DBD1F0;
    --ps-surface-1-active: #F0EAFB;
    --ps-surface-2-active: #DFD6F1;
    --ps-surface-3-active: #D1C5EA;

    /* --- Text layers --- */
    --ps-text-1:        #1A1030;
    --ps-text-2:        #463A66;
    --ps-text-3:        #594B78;
    --ps-text-disabled: #807598;

    /* --- Borders --- */
    --ps-border:        #DFD6F0;
    --ps-border-strong: #7C6DA2;

    /* --- Brand accent --- */
    --ps-accent:        #6A34E0;
    --ps-accent-hover:  #5A2BC4;
    --ps-accent-active: #42199B;
    --ps-accent-text:   #5A2BC4;
    --ps-accent-ui:     #6A34E0;
    --ps-on-accent:     #FFFFFF;
    --ps-accent-tint:   #EDE4FF;
    --ps-accent-edge:   #C0A5FA;

    /* --- Warm / Pro --- */
    --ps-warm:      #F0A32B;
    --ps-warm-text: #8C5406;
    --ps-on-warm:   #231402;
    --ps-warm-tint: #FDF1DE;
    --ps-warm-edge: #B07A12;

    /* --- State colours --- */
    --ps-success:      #0B7351;
    --ps-success-text: #0A6E4D;
    --ps-on-success:   #FFFFFF;
    --ps-success-tint: #E4F6EF;
    --ps-success-edge: #A9DCC8;
    --ps-warning:      #8C5200;
    --ps-warning-text: #8A5200;
    --ps-on-warning:   #FFFFFF;
    --ps-warning-tint: #FDF0DC;
    --ps-warning-edge: #EBC894;
    --ps-error:        #D33A3F;
    --ps-error-text:   #B02328;
    --ps-on-error:     #FFFFFF;
    --ps-error-tint:   #FDEBEC;
    --ps-error-edge:   #F1BABC;
    --ps-info:         #1F6FD8;
    --ps-info-text:    #1859B0;
    --ps-on-info:      #FFFFFF;
    --ps-info-tint:    #E8F1FD;
    --ps-info-edge:    #B5D2F5;

    /* --- Focus --- */
    --ps-focus:            #5B2BD1;
    --ps-focus-halo:       #FFFFFF;
    --ps-focus-media:      #FFFFFF;
    --ps-focus-media-halo: #07040F;

    /* --- Persona / category hues --- */
    --ps-hue-1:      #6A34E0;
    --ps-hue-2:      #0A6B7C;
    --ps-hue-3:      #0A6E4D;
    --ps-hue-4:      #8C5606;
    --ps-hue-5:      #B02A66;
    --ps-hue-6:      #1859B0;
    --ps-hue-1-tint: #EDE4FF;
    --ps-hue-2-tint: #DDF1F4;
    --ps-hue-3-tint: #DFF3E9;
    --ps-hue-4-tint: #FBEEDB;
    --ps-hue-5-tint: #FCE7EF;
    --ps-hue-6-tint: #E5EEFB;

    /* --- Leaderboard medals --- */
    --ps-medal-1:    #E8B93B;
    --ps-medal-2:    #BFC7D2;
    --ps-medal-3:    #D08B57;
    --ps-on-medal:   #1A1206;
    --ps-medal-edge: #8A6A12;

    /* --- Text over imagery --- */
    --ps-on-scrim: #FFFFFF;

    /* --- Selection --- */
    --ps-select-bg: #D9C7FF;
    --ps-select-fg: #1A1030;

    /* --- Skeletons --- */
    --ps-skeleton-a: #F0EBFA;
    --ps-skeleton-b: #E5DDF6;

    --ps-scrim-image-opacity: 0.78;
    --ps-scrim-image: linear-gradient(to top,
        rgba(7, 4, 15, 0.78) 0%,
        rgba(7, 4, 15, 0.42900000000000005) 45%,
        rgba(7, 4, 15, 0) 100%);

    /* --- Elevation --- */
    --ps-e-1:     0 1px 2px rgba(42, 22, 88, 0.06), 0 2px 8px rgba(42, 22, 88, 0.05);
    --ps-e-2:     0 2px 4px rgba(42, 22, 88, 0.07), 0 8px 20px rgba(42, 22, 88, 0.08);
    --ps-e-3:     0 4px 8px rgba(42, 22, 88, 0.08), 0 16px 36px rgba(42, 22, 88, 0.10);
    --ps-e-4:     0 8px 16px rgba(42, 22, 88, 0.10), 0 32px 64px rgba(42, 22, 88, 0.14);
    --ps-e-inset: inset 0 1px 2px rgba(42, 22, 88, 0.08);
    --ps-e-glow:  0 6px 24px rgba(106, 52, 224, 0.28);

    /* --- Scrim + glass --- */
    --ps-scrim:          rgba(26, 16, 48, 0.42);
    --ps-glass-tint:     rgba(255, 255, 255, 0.72);
    --ps-glass-sheen:    rgba(255, 255, 255, 0.55);
    --ps-glass-hairline: rgba(255, 255, 255, 0.8);

    /* --- Overlays for imagery --- */
    --ps-overlay-hover: rgba(26, 16, 48, 0.05);
    --ps-overlay-press: rgba(26, 16, 48, 0.1);

    /* --- Gradients ---
       Same defect as the dark theme's: the end stop was #9B72F0, white on which
       is 3.47:1. It measured 4.66:1 composited and so looked like a pass, but
       only because averaging the button box weighted the darker end — the right
       side of every primary button was failing. Both stops now clear 4.5:1:
         #6A34E0 -> 6.60:1   #8256EA -> 4.70:1  */
    --ps-grad-brand: linear-gradient(135deg, #6A34E0 0%, #8256EA 100%);
    --ps-grad-warm:  linear-gradient(135deg, #F0A32B 0%, #E8B93B 100%);
    --ps-grad-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0) 100%);
  }
}

/* --------------------------------------------------------------------------
   4. LIGHT THEME, FORCED  (must win when the OS prefers dark)
   Rationale for every value is in block 2 and in DESIGN.md §3; the token
   names are identical, so it is not repeated here.
   -------------------------------------------------------------------------- */
:root[data-theme="light"] {
  color-scheme: light;

  /* --- Aurora backdrop --- */
  --ps-aurora-1: #6D3BFF;
  --ps-aurora-2: #B23BFF;
  --ps-aurora-3: #FF4D80;
  --ps-aurora-opacity: 0.1;
  --ps-aurora-max: #E8E1FC;   /* computed, for reference + fallbacks */

  /* --- Page ground --- */
  --ps-bg-deep: #EFEAF9;
  --ps-bg:      #F6F3FC;
  --ps-sunken:  #FBF9FE;

  /* --- Surface layers --- */
  --ps-surface-1: #FFFFFF;
  --ps-surface-2: #F0EBFA;
  --ps-surface-3: #E5DDF6;

  /* --- Surface interaction states --- */
  --ps-surface-1-hover:  #F7F3FD;
  --ps-surface-2-hover:  #E8E1F6;
  --ps-surface-3-hover:  #DBD1F0;
  --ps-surface-1-active: #F0EAFB;
  --ps-surface-2-active: #DFD6F1;
  --ps-surface-3-active: #D1C5EA;

  /* --- Text layers --- */
  --ps-text-1:        #1A1030;
  --ps-text-2:        #463A66;
  --ps-text-3:        #594B78;
  --ps-text-disabled: #807598;

  /* --- Borders --- */
  --ps-border:        #DFD6F0;
  --ps-border-strong: #7C6DA2;

  /* --- Brand accent --- */
  --ps-accent:        #6A34E0;
  --ps-accent-hover:  #5A2BC4;
  --ps-accent-active: #42199B;
  --ps-accent-text:   #5A2BC4;
  --ps-accent-ui:     #6A34E0;
  --ps-on-accent:     #FFFFFF;
  --ps-accent-tint:   #EDE4FF;
  --ps-accent-edge:   #C0A5FA;

  /* --- Warm / Pro --- */
  --ps-warm:      #F0A32B;
  --ps-warm-text: #8C5406;
  --ps-on-warm:   #231402;
  --ps-warm-tint: #FDF1DE;
  --ps-warm-edge: #B07A12;

  /* --- State colours --- */
  --ps-success:      #0B7351;
  --ps-success-text: #0A6E4D;
  --ps-on-success:   #FFFFFF;
  --ps-success-tint: #E4F6EF;
  --ps-success-edge: #A9DCC8;
  --ps-warning:      #8C5200;
  --ps-warning-text: #8A5200;
  --ps-on-warning:   #FFFFFF;
  --ps-warning-tint: #FDF0DC;
  --ps-warning-edge: #EBC894;
  --ps-error:        #D33A3F;
  --ps-error-text:   #B02328;
  --ps-on-error:     #FFFFFF;
  --ps-error-tint:   #FDEBEC;
  --ps-error-edge:   #F1BABC;
  --ps-info:         #1F6FD8;
  --ps-info-text:    #1859B0;
  --ps-on-info:      #FFFFFF;
  --ps-info-tint:    #E8F1FD;
  --ps-info-edge:    #B5D2F5;

  /* --- Focus --- */
  --ps-focus:            #5B2BD1;
  --ps-focus-halo:       #FFFFFF;
  --ps-focus-media:      #FFFFFF;
  --ps-focus-media-halo: #07040F;

  /* --- Persona / category hues --- */
  --ps-hue-1:      #6A34E0;
  --ps-hue-2:      #0A6B7C;
  --ps-hue-3:      #0A6E4D;
  --ps-hue-4:      #8C5606;
  --ps-hue-5:      #B02A66;
  --ps-hue-6:      #1859B0;
  --ps-hue-1-tint: #EDE4FF;
  --ps-hue-2-tint: #DDF1F4;
  --ps-hue-3-tint: #DFF3E9;
  --ps-hue-4-tint: #FBEEDB;
  --ps-hue-5-tint: #FCE7EF;
  --ps-hue-6-tint: #E5EEFB;

  /* --- Leaderboard medals --- */
  --ps-medal-1:    #E8B93B;
  --ps-medal-2:    #BFC7D2;
  --ps-medal-3:    #D08B57;
  --ps-on-medal:   #1A1206;
  --ps-medal-edge: #8A6A12;

  /* --- Text over imagery --- */
  --ps-on-scrim: #FFFFFF;

  /* --- Selection --- */
  --ps-select-bg: #D9C7FF;
  --ps-select-fg: #1A1030;

  /* --- Skeletons --- */
  --ps-skeleton-a: #F0EBFA;
  --ps-skeleton-b: #E5DDF6;

  --ps-scrim-image-opacity: 0.78;
  --ps-scrim-image: linear-gradient(to top,
      rgba(7, 4, 15, 0.78) 0%,
      rgba(7, 4, 15, 0.42900000000000005) 45%,
      rgba(7, 4, 15, 0) 100%);

  /* --- Elevation --- */
  --ps-e-1:     0 1px 2px rgba(42, 22, 88, 0.06), 0 2px 8px rgba(42, 22, 88, 0.05);
  --ps-e-2:     0 2px 4px rgba(42, 22, 88, 0.07), 0 8px 20px rgba(42, 22, 88, 0.08);
  --ps-e-3:     0 4px 8px rgba(42, 22, 88, 0.08), 0 16px 36px rgba(42, 22, 88, 0.10);
  --ps-e-4:     0 8px 16px rgba(42, 22, 88, 0.10), 0 32px 64px rgba(42, 22, 88, 0.14);
  --ps-e-inset: inset 0 1px 2px rgba(42, 22, 88, 0.08);
  --ps-e-glow:  0 6px 24px rgba(106, 52, 224, 0.28);

  /* --- Scrim + glass --- */
  --ps-scrim:          rgba(26, 16, 48, 0.42);
  --ps-glass-tint:     rgba(255, 255, 255, 0.72);
  --ps-glass-sheen:    rgba(255, 255, 255, 0.55);
  --ps-glass-hairline: rgba(255, 255, 255, 0.8);

  /* --- Overlays for imagery --- */
  --ps-overlay-hover: rgba(26, 16, 48, 0.05);
  --ps-overlay-press: rgba(26, 16, 48, 0.1);

  /* --- Gradients --- */
  --ps-grad-brand: linear-gradient(135deg, #6A34E0 0%, #9B72F0 100%);
  --ps-grad-warm:  linear-gradient(135deg, #F0A32B 0%, #E8B93B 100%);
  --ps-grad-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0) 100%);
}

/* --------------------------------------------------------------------------
   6. REDUCED MOTION
   Declared here rather than in app.css so it still applies if app.css fails
   to load, and expressed by collapsing the DURATION TOKENS rather than by a
   blanket * { transition: none }: that way a component that reads a token
   needs no reduced-motion branch of its own, and nothing that is merely a
   state change (colour, opacity) is broken.

   The infinite ambient animations are stopped outright, because collapsing
   their duration to 1ms would spin them, not stop them. v1 caught its
   sparkle and pulse-glow only by the blanket duration rule; they are stopped
   by name here. v1's reduced-motion block was otherwise correct, and the
   parts of it that worked are preserved.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  :root {
    --ps-dur-1: 1ms;
    --ps-dur-2: 1ms;
    --ps-dur-3: 1ms;
    --ps-dur-4: 1ms;
    --ps-dur-5: 1ms;
    --ps-ease-spring: var(--ps-ease-out);   /* no overshoot: overshoot IS motion */
    --ps-dur-shimmer: 0s;
    --ps-dur-typing:  0s;
    --ps-dur-aurora:  0s;
    /* The aurora stops drifting but stays visible: it is the backdrop the
       glass needs, and removing it would flatten the UI rather than calm it. */
  }
}

/* --------------------------------------------------------------------------
   7. INCREASED CONTRAST
   v1 had a prefers-contrast block that lifted --text-secondary, the one token
   that already passed, and left --text-muted, --text-tertiary and --primary
   untouched: it strengthened the wrong thing. This raises the two tiers that
   are genuinely nearest a bound — the tertiary text tier and the decorative
   hairline, which is the only token in the system deliberately under 3:1.
   -------------------------------------------------------------------------- */
@media (prefers-contrast: more) {
  :root {
    --ps-text-3: var(--ps-text-2);
    --ps-border: var(--ps-border-strong);
    --ps-aurora-opacity: 0.08;
  }
  :root[data-theme="light"] {
    --ps-text-3: var(--ps-text-2);
    --ps-border: var(--ps-border-strong);
    --ps-aurora-opacity: 0.04;
  }
}

/* --------------------------------------------------------------------------
   8. FORCED COLOURS
   Under Windows High Contrast the browser replaces our palette wholesale.
   The one thing that must be surrendered explicitly is the aurora, because a
   forced-colours backdrop plus a decorative one is unreadable; and the glass
   veil, which becomes an opaque block. Text and surfaces are left to the
   system on purpose.
   -------------------------------------------------------------------------- */
@media (forced-colors: active) {
  :root {
    --ps-aurora-opacity: 0;
    --ps-glass-blur: 0px;
    --ps-e-1: none;
    --ps-e-2: none;
    --ps-e-3: none;
    --ps-e-4: none;
    --ps-e-glow: none;
    --ps-focus: Highlight;
    --ps-focus-halo: Canvas;
  }
}
