/* ==========================================================================
   Design tokens and fonts

   Every colour, and the six self-hosted woff2 faces. Nothing here selects an
   element. Loaded first so every later file can read the tokens.
   ========================================================================== */


/* ---- Self-hosted faces ---- */
/* ---- Fonts ----
   Self-hosted, not fetched from fonts.googleapis.com. A blocked third-party
   font request used to swap the whole site to its fallback: Firefox rendered
   every serif heading in Georgia while Chrome rendered Instrument Serif,
   because an extension blocked that one gstatic path. Same-origin files
   cannot fail that way.

   Geist and Geist Mono are the variable builds. They are declared `400 700`
   even though the axis runs 100-900. The old static set only carried
   400/500/600/700, so `.stat-box .growth`'s `font-weight: 300` rounded up to
   400. The narrower range keeps it rounding up instead of suddenly rendering
   Light.

   Only latin and latin-ext ship; the site has no Cyrillic or Vietnamese text.
   U+2192 (the arrow in 8451.html) is in no Google subset and already came
   from a system font.

   To refresh: re-fetch the css2 URL for the three families, keep the latin
   and latin-ext blocks, and repoint src at assets/fonts/. Do not re-encode
   the woff2 files. */
@font-face {
  font-family: "Geist";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../assets/fonts/geist-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Geist";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../assets/fonts/geist-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../assets/fonts/geist-mono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../assets/fonts/geist-mono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Instrument Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/instrument-serif-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Instrument Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/instrument-serif-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Metric-matched stand-ins, used only while the real file loads or if the
   reader has web fonts turned off. Instrument Serif is a condensed display
   face: at the same font-size it sets about 24% narrower than Georgia and
   about 18% narrower than Times. Dropping in an unadjusted system serif
   therefore reflows every heading. size-adjust cancels that, so the swap
   changes the letterforms without moving a line.

   Measured with canvas TextMetrics at 1000px in Chrome on Windows, over
   every text run that actually resolves to the serif stack across all five
   pages -- 85 runs, 1969 characters -- not over a synthetic sample. Tuning
   on the real copy matters: a generic pangram gave 73.62%, which carried a
   1.7% width bias against the site's own headings.

     face              width/em   x-height   ascent   descent
     Instrument Serif    46.745     0.5156    0.990    0.310
     Georgia             63.499     0.4844    0.917    0.219
     Times New Roman     59.497     0.4531    0.891    0.216

   size-adjust is the ratio of summed Instrument Serif width to summed
   fallback width. The overrides are Instrument Serif's own ascent and
   descent divided by that same factor, so the line box matches too. Residual
   per-run width error is -5.6% to +10.0% for Georgia, mean +0.9%; one scalar
   cannot match every string, because the two faces differ per glyph.

   The trade is apparent size: the stand-in renders about a quarter smaller
   than it otherwise would. Layout stability is worth more, because the flash
   lasts a frame and the reflow does not.

   Geist and Geist Mono get no equivalent. Geist against Arial measured
   100.39% -- already a match -- and Geist Mono against Consolas 109.13%,
   which moves monospace labels by a few pixels. Only the serif was far
   enough off to be worth correcting.

   Recompute both values if the serif copy changes materially. */
@font-face {
  font-family: "Instrument Serif Georgia Fallback";
  src: local("Georgia");
  size-adjust: 75.59%;
  ascent-override: 130.98%;
  descent-override: 41.01%;
}
@font-face {
  font-family: "Instrument Serif Times Fallback";
  src: local("Times New Roman"), local("Liberation Serif"), local("Tinos");
  size-adjust: 82.35%;
  ascent-override: 120.22%;
  descent-override: 37.65%;
}


/* ---- Design tokens ---- */
:root {
  /* Every colour on the site is declared here. The `-rgb` twins exist only
     because rgba() cannot take a hex custom property; they must stay in step
     with the hex above them. */
  --bg: #fdfaf2;
  --bg-rgb: 253, 250, 242;
  --bg-dark: #221d19;
  --bg-dark-rgb: 34, 29, 25;
  /* The 2026 designs (nodes 429:143615, 431:173065, 433:180214, 436:183904)
     use a single cream, #FDFAF2, for both the page ground and the cards. The
     old palette had two, and --paper was the lighter of the pair. The name is
     kept because the rules that mean "card, not ground" still read better
     with it, but it is now an alias -- do not give it a second value. */
  --paper: var(--bg);
  --paper-rgb: var(--bg-rgb);
  /* Text and rules on the dark sections. */
  --on-dark: #f1ece0;
  --on-dark-rgb: 241, 236, 224;
  --text: #2a241e;
  --text-body: #6b5c4a;
  --text-muted: #8c8477;
  /* One token used to cover two roles that the 2026 designs separate, which
     is why it kept getting flipped between blue and grey. Re-measured
     2026-09-09 against the Calendly frame (429:143615) and the Key Stages
     frame (436:188670):

       hero tag pills (429:143637)   stroke #3185FC   -> --tag, the accent
       idle stage rows (436:188682)  fill   #9C8C78   -> --idle
       idle pager numbers (429:153856) fill #9C8C78   -> --idle

     Only the idle state is warm grey. Keeping them apart means a palette
     change moves the pills and leaves the idle greys alone, which is what
     the designs do. */
  --tag: var(--accent);
  /* The unselected state, on the stage rows and the "what shipped" pager. Its
     selected twin is --accent, so hue as well as weight marks the choice. */
  --idle: #9c8c78;
  /* 2026 redesign: the accent moved from terracotta (#C15F2C) to blue. It
     drives the active nav link, the hero breadcrumb, the callout strokes and
     -- at 0.6 -- every hairline rule on the site. */
  --accent: #3185fc;
  --accent-rgb: 49, 133, 252;
  /* Rules and section dividers. The designs draw these as a 1px accent stroke
     at 60%, which over the cream ground composites to about #83B4F8. */
  --border: rgba(var(--accent-rgb), 0.6);
  /* Kept at 30% on purpose, and not folded into --border. The two Key Stages
     frames disagree: Calendly (429:155978) strokes the box at
     rgba(49,133,252,0.3), 84.51 (436:188670) at 0.6. 0.3 is what ships,
     because it is the value on the page this component was built from. Left
     alone pending a decision on which frame is current. */
  --stage-border: rgba(var(--accent-rgb), 0.3);

  /* Measured off the Figma Results export (node 289:166306) rather than
     guessed. The rule is a 1px stroke of rgba(253,250,242,0.3), which the
     0.749x export renders as 0.228 alpha-px across two device pixels --
     exactly what a 0.3 stroke scaled to 0.749 gives. The old #3a332a was
     roughly half that brightness and read as no line at all on the dark
     sections.

     The hatch measured 0.44 alpha-px per stripe on a 8.7px perpendicular
     period, about 11x the ink of the old 1px/11px at 0.05 that it replaces.
     Stripe direction confirmed by tracing a peak down 12 rows: x falls as y
     rises, so the stripes run "/", which is what -45deg produces. */
  --border-dark: rgba(var(--paper-rgb), 0.3);
  /* The Results table's success green, read off the check-circle component
     (262:79546). It is the one colour in that section that is neither the
     accent nor the off-white. */
  --icon-success: #10934d;
  --hatch-dark: repeating-linear-gradient(
    -45deg,
    rgba(var(--paper-rgb), 0.44) 0 1px,
    transparent 1px 8.7px
  );

  /* The dark sections carry TWO rule colours, not one. Results (289:166306)
     rules in cream; Impact (429:153991) rules in this desaturated blue. Read
     off the Impact export: every hairline and every hatch stripe there
     composites to (57,67,77) over the #221D19 ground, which is #6D9DC5 at
     0.3 -- a cream stroke would land near (100,95,90). Impact's eyebrow and
     its three column headings are the same #6D9DC5 solid.

     Do not fold this into --border-dark. Results was measured independently
     and is cream; changing that token to blue would move a calibration that
     is not wrong.

     Hatch weight taken the same way --hatch-light was, by mean channel
     offset from the ground over whole periods rather than per-pixel alpha.
     The sampled band means (37.262, 34.424, 32.533) against (34,29,25) give
     offsets of 3.262/5.424/7.533 over a line-minus-ground of 75/128/172 --
     0.0435, 0.0424, 0.0438, agreeing to 3%, which is what confirms the line
     colour. Peaks recur every 12.47px along a row, so the perpendicular
     pitch is 8.82px, and coverage 1/8.82 puts the stroke alpha at 0.38.
     Direction traced down 12 rows: x falls as y rises, so "/" -- -45deg. */
  --accent-dark: #6d9dc5;
  --accent-dark-rgb: 109, 157, 197;
  --border-dark-accent: rgba(var(--accent-dark-rgb), 0.3);
  --hatch-dark-accent: repeating-linear-gradient(
    -45deg,
    rgba(var(--accent-dark-rgb), 0.38) 0 1px,
    transparent 1px 8.82px
  );

  /* The light-ground twin, measured off the Figma Context side band (node
     385:61323) the same way --hatch-dark was measured off the Results export.
     Both land on the same 45-degree "/" stripe at essentially one pitch --
     8.7px there, 8.76px here -- from two independent nodes.

     The line colour tracks --accent by design, so the palette moves it. The
     Figma source draws it in #C4622D; what is calibrated here is the stripe's
     weight, not its hue, and weight is what must be preserved when the accent
     changes.

     Match that weight by MEAN LUMINANCE DROP against the ground, never by
     solving each pixel for alpha against an assumed line colour. That solve
     divides by (ground - line) per channel, so it is wrecked by a
     near-degenerate channel -- a blue line on cream differs by 10 in blue --
     and by any error in the assumed ground. Both traps bit here, and together
     they reported a stripe at 46% of the design's weight as a match.
     Luminance drop needs no line-colour reference and cannot be biased that
     way.

     Calibrate on the real page, never on a synthetic swatch: the ground here
     is (253,250,242), and running the ladder over #faf7f0 instead picked 0.42,
     which measures 6.8% heavy in place. On the real ground, against the
     design's 5.874, alpha 0.39 gives 5.812 (1.0% low) and 0.42 gives 6.272.
     Response is near-linear in alpha and passes near the origin. Re-run the
     ladder if the accent or the ground changes; do not carry 0.39 across
     such a change. */
  --hatch-light: repeating-linear-gradient(
    -45deg,
    rgba(var(--accent-rgb), 0.39) 0 1px,
    transparent 1px 8.76px
  );

  /* ---- Layout ----
     The content column every page section is held to, and the gutter outside
     it. .site-header runs full width but aligns its contents to the same
     column, which is what its max()/calc() pair resolves. Twelve wrappers read
     --content-width; six of them also read --gutter. */
  --content-width: 1280px;
  --gutter: 40px;

  --font-serif: "Instrument Serif", "Instrument Serif Georgia Fallback",
                "Instrument Serif Times Fallback", Georgia, serif;
  --font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "Geist Mono", "SFMono-Regular", Consolas, monospace;
}
