/* fonts.css — the site's text faces, in one document-scoped place (106 §3.5).

   ⚠️ MOVED HERE FROM projects.css (lines 25-39) ON 2026-08-31, AND THIS IS NOT
   TIDYING. @font-face is document-scoped, so a hero heading in Blinker worked
   before this move — but it worked BECAUSE THE PROJECTS SECTION'S STYLESHEET
   HAPPENS TO LOAD, and projects.css is linked after hero.css. Delete or defer
   that sheet and the largest text on the site silently falls back.

   102 §8 records the cost of silent font fallback on this project already:
   'Blinker' and 'Poppins' shipped instead of 'Blinker Subset' and 'Poppins
   Subset', and both card faces AND the bar's name rendered in a fallback for a
   whole session without anyone seeing it. The hero's h1 is now the largest
   text on the site, so it gets the loudest possible failure instead: C9
   asserts the RENDERED WIDTH of a known string, because a family-name string
   can be right while the file never loaded (105 §3.3).

   ⚠️ THE url() PATHS ARE ONE DIRECTORY SHALLOWER THAN THEY WERE. In
   css/sections/projects.css they read ../../assets/fonts/; from css/fonts.css
   they must read ../assets/fonts/. Moving the blocks verbatim would 404 every
   face and fall back silently — the exact failure this move exists to prevent.

   Linked FIRST in index.html, ahead of every section sheet, so no section can
   own a face another section depends on.

   base.css is deliberately not the destination: its own header states "no
   design system, no fonts, no palette," and putting fonts under that sentence
   would make the file lie.

   ── Provenance, moved with the blocks (076 §7) ────────────────────────────
   Blinker for headings, Poppins for body. Both OFL, both SELF-HOSTED, never a
   font CDN: 000 §3.5 confirmed mainland-China reachability empirically FOR OUR
   ORIGIN, and fonts.googleapis.com is not reliably reachable there — the same
   reasoning that keeps the INSPIRE subset local and that ruled out a CDN for
   ogl in 062 §3.1.

   ⚠️ THE HEADING FACE IS BLINKER BECAUSE IT WAS MEASURED, NOT GUESSED.
   068 §8 read jingjinghan.com by eye and offered Open Sans or Noto Sans at
   "moderate confidence"; none of its four candidates was on the page. 071 §2
   read the computed font-family off the rendered h1 and the two heading lines:
   all three are Blinker. Gilroy is the body default there and is commercial;
   Blinker is not, so the licence problem 070 §5 anticipated does not arise.

   ⚠️ SUBSET TO PRINTABLE ASCII PLUS — – … ' ' " " · ° ×, NOT to the placeholder
   string. A text-exact subset would break the day real copy lands, which is the
   same trap hero.css records for the 7-glyph INSPIRE cut ("a later INSPIRE ME
   would collapse on the missing space"). Added payload: 18,624 bytes for three
   faces, against a previous whole-site font budget of 1,052.

   ⚠️ blinker-400-subset.woff2 CARRIES ALL 32 CHARACTERS THE HERO'S SIX PHRASES
   USE, verified glyph-by-glyph with a bounds pen rather than by cmap presence:
   105 §2.1 found 0 missing and 0 blank outlines, including + | , and -. It is
   105 codepoints — full printable ASCII plus extras — not a tight cut, so a
   copy session can change the phrases without re-cutting the file.

   ── ⚠️ THE RECIPE FOR CUTTING ANOTHER SUBSET, AND THE FLAG THAT IS EASY TO MISS ──
   All four faces here were cut with pyftsubset at its DEFAULT layout flags —
   NOT --layout-features='*', which keeps ~18 extra glyphs and inflates the
   file. Defaults reproduce the existing files exactly: 107 glyphs, 13 tables
   and an identical glyph order for the Poppins pair.

       pyftsubset <Face>.ttf --unicodes="U+0020-007E,U+00B0,U+00B7,U+00D7,\
                  U+2013,U+2014,U+2018,U+2019,U+201C,U+201D,U+2026" \
                  --flavor=woff2 --name-IDs+=14 --no-hinting \
                  --output-file=<face>-subset.woff2

   ⚠️ --no-hinting IS THE SECOND LOAD-BEARING FLAG AND THIS RECIPE WAS MISSING IT
   UNTIL 2026-09-03 (148 §1.4, measured in 149). Every upstream Blinker TTF ships
   cvt/fpgm/prep — the TrueType hinting tables — and 18 tables in total. The
   shipped blinker-400-subset.woff2 has 14 tables and NONE of the three, so it
   was cut with hinting stripped and the recipe never recorded how.

   ⚠️ SO THE SENTENCE ABOVE — "Defaults reproduce the existing files exactly" —
   IS FALSE FOR BLINKER, measured: re-cutting Blinker-Regular.ttf with the recipe
   as it stood gives 11,920 B and 17 tables against the shipped 8,908 B and 14.
   Adding --no-hinting gives 7,696 B and a table set identical to the shipped
   file. The claim may still hold for the Poppins pair, which is what it cites
   and what was not re-tested; do not read this correction as covering them.

   Also changed 2026-09-03: --unicodes= with the literal set replaces
   --unicodes-file=. The 105 codepoints compress exactly to the string above
   (printable ASCII plus the ten extras), which removes the chicken-and-egg the
   old wording created — "take the set from an existing subset" needs fontTools
   installed to read one, and 147 §5 found it was not.

   ⚠️ --name-IDs+=14 IS LOAD-BEARING AND NOTHING STRUCTURAL WILL TELL YOU IT IS
   MISSING. pyftsubset's default name-ID set keeps 0-6 and DROPS 14, which is
   the OFL licence URL. All four faces here carry it. The 300 cut on
   2026-09-01 was made without it and compared against the 400 file on glyph
   count, glyph order and table list — all three agreed, and the licence
   record was still gone. These fonts are REDISTRIBUTED under the OFL; the
   notice has to travel with them.
   Check it directly, because no diff of the outline data will:
       nameIDs must be [0, 1, 2, 3, 4, 5, 6, 14] and 14 must read
       https://scripts.sil.org/OFL   (Blinker's is http://, not https://)

   Take the codepoint set FROM AN EXISTING SUBSET rather than retyping it —
   `sorted(TTFont(existing).getBestCmap())` — so a new face cannot quietly
   diverge from the 105 the others carry.

   ⚠️ POPPINS IS A STATIC FAMILY. There is no variable axis and no [wght] file
   in google/fonts; METADATA.pb declares no axes. The weights that exist are
   static cuts, and going lighter than 300 means cutting ExtraLight — which
   declares usWeightClass 275, not 200. Measured stem-to-cap: ExtraLight
   0.0643, Light 0.1000, Regular 0.1304. There is nothing between 300 and 400.

   font-display: swap, not block — 000 §3.4's fail-open rule prefers text a
   visitor can read in a fallback over no text at all. ⚠️ Two consequences now
   that the hero's h1 uses this face: the projects curve is derived from the
   copy's box, so projects/index.js rebuilds on document.fonts.ready; and the
   hero heading would flash a fallback, except that main.js's boot race awaits
   document.fonts.ready before the loader hides, so the swap resolves behind
   the overlay. Neither is a reason to switch to block. */

@font-face {
  font-family: 'Blinker Subset';
  src: url('../assets/fonts/blinker-400-subset.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
/* ── BLINKER 200 AND 300 (146 §3.2, cut 2026-09-03, measured in 149) ──────
   For the strip's phrases, which Xinlu wanted lighter than the hero's 400
   (145 §5.3). Both cut from google/fonts/ofl/blinker at the digests recorded
   in 148 §1.3, with the corrected recipe above.

   ⚠️ THE 200 FACE SHIPPED AND WAS THEN DROPPED. Xinlu compared the two on
   hardware and settled on 300 (152 §1.3), so blinker-200-subset.woff2 and its
   @font-face are gone as of 2026-09-03. Recorded rather than erased because
   the finding survives the file: Blinker's ExtraLight declares usWeightClass
   200 and does NOT have the problem Poppins has, where ExtraLight declares 275
   and there is nothing between 300 and 400 (see the Poppins note above).
   Anyone re-cutting it can trust that.

   ⚠️ Blinker-Light upstream is fontRevision 1.016 while every other face,
   including our shipped 400, is 1.015. An ordinary point release of one
   weight; recorded so nobody later reads the mismatch as a bad cut.

   ⚠️ ?stripweight= IS GONE TOO, and this sentence used to name it. With only
   one light face left the flag would have parsed, clamped, set the property
   and moved no pixel, because CSS font matching resolves 200 up to the 300
   face — 086 §6's defect, which has shipped three times. Removed the way
   ?cuey= and ?cuefade= were: out of the parser, not left inert. */
@font-face {
  font-family: 'Blinker Subset';
  src: url('../assets/fonts/blinker-300-subset.woff2') format('woff2');
  font-weight: 300; font-style: normal; font-display: swap;
}

/* ⚠️ 300 EXISTS BECAUSE font-weight: 300 WITHOUT IT IS SILENT (110 §1.3).
   Browsers synthesise BOLD; they do not synthesise LIGHT. A `font-weight: 300`
   with no 300 face declared resolves to the nearest available — 400 — with no
   error, no console warning and a completely plausible render. That is the
   entry-10 sub-case the catalogue gained this session, arriving in the very
   next task after it was written.

   So C16 does not read a family name or a computed font-weight, either of
   which reports 300 whether or not a 300 face exists. It measures the
   RENDERED WIDTH of the body sentence and asserts two things: that it equals
   the 300 face's advance within 0.1 %, and that it DIFFERS from the 400
   face's by more than 0.1 %. The second is the arm that catches the silent
   fallback, and it is only trustworthy because the separation was measured
   first: 67.1160 em against 67.8240 em, a gap of -1.0439 %, ten times the
   band. Had it come back under 0.1 % the answer was to find another
   discriminator, not to narrow the threshold.

   Cut from Poppins-Light.ttf 4.004 — the same upm, the same version and the
   same 105-codepoint set as the other two, with pyftsubset's default layout
   flags, which reproduce the reference cut exactly: 107 glyphs, 13 tables and
   an identical glyph order to poppins-400-subset.woff2. Not a text-exact cut,
   for the reason this file's header already gives. */
@font-face {
  font-family: 'Poppins Subset';
  src: url('../assets/fonts/poppins-300-subset.woff2') format('woff2');
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Poppins Subset';
  src: url('../assets/fonts/poppins-400-subset.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Poppins Subset';
  src: url('../assets/fonts/poppins-500-subset.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
