/* =============================================================
   IMPOSTER PLAY LAB — TOKENS
   The only file where raw values are allowed. Everything else
   references these.

   RE-CUT 2026-09-06 — "premium". The owner's note on the previous
   build was that it had steered too far into playful and read as
   childish. The reference is the DISCIPLINE of the flyer in
   01-client/references/flyer-2026-09-05.jpg — restraint, editorial
   type, a quiet ground, contained motion — not its choices. The
   ground stays the white the client settled on; the copy stays
   theirs; the decoration is gone entirely. Premium here comes from
   type, space and the stage's motion. Nothing else.

   Palette source: "IPL Brand Colour Guide" (deep green anchor, four
   pastels). The one departure the guide forces: a pastel cannot be
   TEXT on a light ground, so each hue has two text-grade tones —
   `-display` for large type (≥24px, 3:1) and `-ink` for anything
   smaller (4.5:1). Full-strength pastels appear on the green only.
   ============================================================= */

/* -------------------------------------------------------------
   METRIC-MATCHED FALLBACKS — added 2026-09-06 (design-rules L16).

   `font-display: swap` means every cold visitor renders the
   fallback face for a moment, and a blocked or proxied font makes
   that state permanent. Raw Georgia is 14.8% wider than Fraunces
   per unit of type, so the headline wrapped to FOUR lines instead
   of two and the hero overflowed `100svh` by up to 45.8px.

   These two faces re-cut the local fallback to the webfont's own
   metrics, so the fallback text occupies the same box. Derived by
   measurement, not taste — advance width of the hero headline and
   the join sentence, and the faces' own ascent/descent read back
   through `TextMetrics`. Note `size-adjust` SCALES the metric
   overrides (verified in all three engines: 50% + 100%/20% yields
   0.5*120%, not 120%), so each override below is the target
   divided by the size-adjust.

   They are NOT in `fonts.css`: that file is overwritten wholesale
   by `assets/_source/fetch-fonts.py`, and these are hand-derived.

   Georgia and Arial were chosen over `Iowan Old Style` because
   BOTH ship on Windows and macOS and could therefore actually be
   measured here — `Iowan Old Style` is absent on this machine and
   resolves to the generic serif. A platform with neither falls
   through to the raw stack below exactly as it did before: no
   regression, and no number invented for a face this pass could
   not open.
   ------------------------------------------------------------- */

@font-face {
  font-family: "Fraunces fallback";
  src: local("Georgia");
  size-adjust: 87.114%;      /* 12.5133em / 14.3643em, the headline's own advance */
  ascent-override: 112.266%; /* 97.8%  / 0.87114 */
  descent-override: 29.272%; /* 25.5%  / 0.87114 */
  line-gap-override: 0%;
}

/* THE ITALIC NEEDS ITS OWN FACE AND ITS OWN NUMBER, and forgetting it is why
   the first cut of this fix still wrapped: the headline's second line ends in
   `<em class="hero__accent">matter most.</em>`, an italic run, and a face
   declared only at `font-style: normal` does not match italic text — the
   cascade skipped straight past it to raw Georgia. Fraunces' italic is a much
   narrower cut than its roman relative to Georgia's: 82.540% where the roman
   wants 87.114%. One number for both would miss by 5.5%. */
@font-face {
  font-family: "Fraunces fallback";
  font-style: italic;
  /* THREE names, because the engines disagree about which one names an italic
     and they must all land on the SAME physical face. Measured advance of
     "matter most." at 1000px: real Georgia Italic is 5785.16, the roman is
     5676.28, and a miss falls through to 6067.88. Chromium and Firefox match
     "Georgia Italic"; WEBKIT MATCHES NEITHER EXPLICIT NAME and would fall
     through — but it reads bare `local("Georgia")` as the FAMILY and picks
     its italic member, where Chromium and Firefox read the same string as
     the roman. Ordered this way each engine takes a different entry and all
     three arrive at 5785.16. */
  src: local("Georgia Italic"), local("Georgia-Italic"), local("Georgia");
  size-adjust: 82.540%;      /* 4.7751em / 5.7852em, the accent's own advance */
  ascent-override: 118.488%; /* 97.8%  / 0.82540 */
  descent-override: 30.894%; /* 25.5%  / 0.82540 */
  line-gap-override: 0%;
}

@font-face {
  font-family: "Inter fallback";
  src: local("Arial");
  size-adjust: 108.514%;     /* 21.2909em / 19.6204em, the join sentence's advance */
  ascent-override: 89.297%;  /* 96.9%  / 1.08514 */
  descent-override: 22.209%; /* 24.1%  / 1.08514 */
  line-gap-override: 0%;
}

:root {

  /* ---------- 1. COLOUR ------------------------------------ */

  /* brand values — the only place hex codes belong */
  --green:        #1B4D3E;   /* anchor / trust                */
  --green-deep:   #123328;   /* the shade under the anchor    */
  --green-soft:   #2C6A57;   /* hovers on green surfaces      */

  --coral:        #F5B7A6;   /* warmth, energy, courage       */
  --gold:         #F6D08A;   /* confidence, achievement, CTAs */
  --teal:         #9BC7BE;   /* curiosity, the "lab" spirit   */
  --lilac:        #C9B7D9;   /* openness, imagination         */

  /* DISPLAY tones — the pastels deepened only as far as large type needs.
     Measured on white (WCAG 2.x; the ≥24px / ≥18.66px-bold bar is 3:1):
     coral 3.57, gold 3.43, teal 3.53, lilac 3.41. Still recognisably the
     pastel, and legal — which is how "soft pastels for headings" is met on a
     light ground. Never under 24px. */
  --coral-display: #C8704F;
  --gold-display:  #AE8428;
  --teal-display:  #57938A;
  --lilac-display: #9583B0;

  /* INK tones — the same hues deepened until they pass 4.5:1 on white, for
     anything smaller than display size: coral 4.88, gold 5.31, teal 7.03,
     lilac 6.14. */
  --coral-ink:    #B4553B;
  --gold-ink:     #8A6516;
  --teal-ink:     #29625A;
  --lilac-ink:    #6B5B7E;
  /* the error message is the one line a visitor is REQUIRED to read */
  --coral-alert:  #7A3320;

  /* button hovers — the same hue, lit */
  --gold-lit:     #FFDD9E;
  --teal-lit:     #B4D6CE;

  /* neutrals — pushed a few % toward the green, never pure grey */
  --white:        #FFFFFF;
  --ink-900:      #111111;   /* the guide's body black        */
  --ink-500:      #4F5A54;   /* 7.18:1 on white               */
  --ink-200:      #E1E5E2;   /* the hairline                  */

  /* roles — components reference these, never the values above */
  --surface:        var(--white);
  --surface-invert: var(--green);
  --ink:            var(--ink-900);
  --ink-muted:      var(--ink-500);
  --ink-invert:     var(--white);      /* the guide: white on dark, never a tint */
  /* THERE IS NO `--ink-invert-muted`, and the reasoning is kept so nobody adds
     one back. The guide is white on dark and never a tint, so on the green band
     there is no quieter grade to demote supporting copy into — muted copy takes
     the same white and is made quiet by size and weight instead. A token was
     once defined here as `#FFFFFF`, i.e. as a second name for `--ink-invert`,
     which reads as a real distinction in every stylesheet that references it
     while being none. Its two consumers now point at `--ink-invert` directly:
     `.on-invert` in base.css and `.on-invert .btn--ghost` in components.css.
     `--placeholder-invert` below is the related, REAL case: the inverted
     placeholder needed its own value precisely because taking white here made
     it render identically to a typed address. */
  --heading:        var(--green);      /* the anchor carries the headings */
  --heading-invert: var(--gold);       /* the guide's own dark example */
  --line:           var(--ink-200);
  --line-invert:    rgb(255 255 255 / 0.18);

  /* the puff a word kicks up as it lands — the pastels at full brand value */
  --dust-coral:     rgb(245 183 166 / 0.8);
  --dust-gold:      rgb(246 208 138 / 0.8);
  --dust-teal:      rgb(155 199 190 / 0.8);
  --dust:           var(--dust-coral);

  /* white films on the deep-green panel: the field, its focus state, its
     border. On an inverted ground a tint of the ground reads as dirt. */
  --film-invert:       rgb(255 255 255 / 0.04);
  --film-invert-lit:   rgb(255 255 255 / 0.08);
  --line-invert-field: rgb(255 255 255 / 0.30);
  --placeholder-invert: #B0CAC1;   /* 5.54:1 on green; 1.74:1 against white body text, so it cannot pass for a typed value */

  /* The wall. Was ONE greyscale texture multiplied over --surface, so the
     token owned the colour and the file owned only the modulation
     (design-rules L3/L4, and the ramped veil below). FLATTENED TO PURE
     --surface ON 2026-09-20 at the client's ask — the ground now paints
     no image at all, and every section's veil becomes visually neutral
     (surface over surface = surface). The chalk asset stays in the ship
     set (`stage.py`) and preloaded (index.html / welcome.html) so a
     reinstate is a one-line token flip; retiring both is tracked at
     05-open. */
  --texture:        none;
  /* a film of the surface over the wall scales the modulation down by
     `1 - strength`. 1 = the asset as cut. Exists because the ground was
     briefly cream (2026-09-06) and the asset read as dirt there; on white
     it is not needed and is left at 1.
     Lowered to 0.55 on 2026-09-20 — see the veil below for why. */
  --texture-strength: 0.55;

  /* ---------- the veil ----------
     A second film of --surface over the wall, but ramped DOWN THE PAGE
     instead of set once, so the ground can be brushed where there is nothing
     to read and paper where there is.

     Why it exists: the client asked on 2026-09-20 for a flat white ground —
     the texture "takes too much attention". Their own brief opens the build
     with "a subtle texture rather than a flat fill", and pass-bands.md turns
     that sentence into a measured floor the first build failed. Both cannot
     hold at one amplitude, so they are held at different heights of the page.

     The rule it encodes: WHERE THE BRAND PERFORMS THE GROUND IS BRUSHED, AND
     WHERE THERE ARE WORDS TO READ IT IS PAPER. That is also why the whole
     confirmation page sits at `--veil-page` — it is all reading.

     These are the four stops of one ramp, and each section paints a gradient
     from its own stop to the next section's. Contiguous sections therefore
     meet at a shared number and the page is one continuous ramp with no seam
     to hide. Re-ordering or re-sizing a section cannot break it: the ramp is
     measured in sections, not pixels, so it holds on a phone too.

     0% = the wall as cut. 100% = flat --surface. `--texture-strength` scales
     the whole wall FIRST, so what a visitor sees at any height of the page is
     `strength × (1 − veil)`. The rules are in base.css under `.wall`. */
  --veil-hero:   0%;    /* the top of the page — the wall as cut       */
  --veil-stage: 14%;    /* the three words                            */
  --veil-read:  62%;    /* reading begins                             */
  --veil-page:  88%;    /* where it settles, and the whole of welcome */


  /* ---------- 2. TYPE --------------------------------------
     Fraunces (display) — at weight 500, with SOFT and WONK at zero
     and the optical size allowed to rise, it is a crisp old-style
     serif with a true italic. The wonky, heavy cut the previous
     build used is what read as childish.
     Inter (text) — crisp, gallery-neutral, gets out of the way. */

  /* the metric-matched face sits FIRST among the fallbacks, so the box the
     text occupies is the same whether or not the webfont has arrived */
  --font-display: "Newsreader", "Fraunces fallback", "Iowan Old Style", Georgia, serif;
  --font-body:    "Inter", "Inter fallback", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* modular scale, ratio 1.2 (360px) → 1.28 (1440px) */
  --step--2: clamp(0.750rem, 0.740rem + 0.05vw, 0.781rem);
  --step--1: clamp(0.888rem, 0.869rem + 0.08vw, 0.938rem);
  --step-0:  clamp(1.063rem, 1.021rem + 0.19vw, 1.188rem);  /* body */
  --step-1:  clamp(1.275rem, 1.194rem + 0.36vw, 1.519rem);
  --step-2:  clamp(1.531rem, 1.394rem + 0.61vw, 1.944rem);
  --step-3:  clamp(1.838rem, 1.621rem + 0.96vw, 2.488rem);
  --step-4:  clamp(2.200rem, 1.871rem + 1.46vw, 3.188rem);
  --step-5:  clamp(2.644rem, 2.164rem + 2.13vw, 4.081rem);  /* the headline */
  --step-6:  clamp(3.000rem, 2.400rem + 2.67vw, 4.800rem);

  --lh-display: 1.08;
  --lh-tight:   1.24;
  --lh-body:    1.62;

  --ls-display: -0.015em;
  --ls-body:     0;
  --ls-caps:     0.18em;

  --weight-body:    400;
  --weight-medium:  500;
  --weight-semi:    600;
  --weight-display: 500;   /* was 600–700; regular weight is the premium half */
  --weight-heavy:   600;   /* the three words — the heaviest anything gets now */

  /* Fraunces variable axes.
     `opsz` is no longer pinned at 24. That pin existed because at weight
     700–800 the lowercase `f` loses its terminal as the optical size rises
     (design-rules, Type). At the weights used now (400–600) the terminal
     survives the axis — re-verified on "often", "off" and "Confidence." on
     2026-09-06 at 96 — so the display cut can be used for what it is for.
     SOFT and WONK at zero: the irregularity was the childish half. */
  --wonk-display: "opsz" 96, "SOFT" 0, "WONK" 0;
  --wonk-text:    "opsz" 24, "SOFT" 0, "WONK" 0;

  --measure:         64ch;
  --measure-lead:    46ch;
  --measure-heading: 22ch;


  /* ---------- 3. SPACE ------------------------------------- */

  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs:  0.75rem;
  --space-s:   1rem;
  --space-m:   1.5rem;
  --space-l:   2.25rem;
  --space-xl:  3.5rem;
  --space-2xl: 5rem;
  --space-3xl: clamp(4.5rem, 3rem + 6.5vw, 9rem);


  /* ---------- 4. LAYOUT ------------------------------------ */

  --container:        1240px;
  --container-narrow: 720px;
  --container-mid:    920px;
  --gutter:           clamp(1.25rem, 4.5vw, 4rem);


  /* ---------- 5. SHAPE -------------------------------------
     Square-cornered. Fields and buttons take a hair of radius.  */

  --radius-s:    3px;
  --radius-m:    6px;
  --radius-l:    12px;
  --radius-pill: 999px;

  --shadow-1: 0 1px 2px rgb(20 24 22 / 0.04),
              0 2px 10px rgb(20 24 22 / 0.05);
  --shadow-2: 0 2px 6px rgb(20 24 22 / 0.05),
              0 14px 38px rgb(20 24 22 / 0.09);


  /* ---------- 6. MOTION ------------------------------------
     Contained. State changes 150–420ms; the reveals ~2.04s on an
     expo-out; the three words keep their own clock in stage.css. */

  --dur-fast: 160ms;
  --dur-base: 260ms;
  --dur-slow: 420ms;
  /* THE REVEAL CLOCK, AND NOTHING ELSE. Every rise-from-below on the page is
     a multiple of this one number — the base reveal, the sign-up card's lift,
     the serif headings' line-by-line, the hairlines drawing in — and nothing
     outside components.css §1 reads it, so it is the single dial for how the
     page arrives. The stage's three words keep their own clock in stage.css
     and the figure's own clock is written into its markup by
     `assets/_source/figure/build-figure.js` (4.63s, and two acts — see
     design-rules L24); neither moves with this.
     1200ms until 2026-09-06, when the owner asked for the pop 1.7× slower:
     1200 × 1.7 = 2040. The per-element STAGGERS are deliberately left where
     they were — the 120/240/360ms on 01–04, the 260ms on the paragraph, the
     300ms on the card — because they are the composition's arrival ORDER, in
     absolute time, not part of the pop. The one exception is the beat between
     the lines of a serif heading, which is internal to this primitive's own
     rhythm and rides the same 1.7 (150 → 255ms in components.css §1). */
  --dur-cine: 2040ms;

  --ease-out:    cubic-bezier(0.2, 0.8, 0.2, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-fall:   cubic-bezier(0.40, 0.02, 0.72, 0.40);
  /* expo-out: almost all of the distance in the first third, then a long
     settle — what makes a 2s move read as deliberate rather than slow */
  --ease-cine:   cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0ms;
    --dur-base: 0ms;
    --dur-slow: 0ms;
    --dur-cine: 0ms;
  }
}
