A font filename encodes more than a weight, but only the weight was ever read
out of it, so two faces of one family collapsed onto a single slot.
Google Fonts ships Newsreader as Newsreader-Italic-VariableFont_opsz,wght.ttf
and Newsreader-VariableFont_opsz,wght.ttf. The italic sorts first, both scored
400, so the italic claimed the family's only 400 slot, the upright was dropped
as a duplicate, and the surviving face was declared with no font-style at all.
@font-face is deliberately global — the composition CSS scoper exempts it,
since a face declaration cannot be scoped — so mounting captions re-pointed the
whole document's Newsreader at the italic file and every sibling composition
rendered in italics.
Faces now carry a font-style descriptor and dedupe on weight AND style.
The same fix had to land in build-frame.mjs, which renames captured fonts
BEFORE captions.mjs sees them. It dropped the style token while renaming, so an
italic file arrived as "Newsreader-Regular.ttf" and was then asserted upright —
leaving the global normal slot pointing at italic bytes even once brandFontFaces
understood styles. The staged filename is a contract: it must carry every axis
that distinguishes one face from another, and the dedup key must be the whole
face.
Second axis, same misparse: weight parsing matched WORDS only, so a Fontsource
capture (inter-latin-500-normal.woff2) scored a whole family 400 and shipped one
of its faces. A numeric axis in the filename now wins over the word heuristic,
anchored so it is not read out of the middle of a hash-named capture file —
a non-digit before it and no alphanumeric after, which keeps both the 4-digit
guard and separator-free names like Roboto900.ttf.
Tests pin both ends of the contract: a round-trip asserting the names
build-frame stages map back to the right weight+style through the real
brandFontFaces, plus a source check that no copy reverts to a weight-only name
or a hardcoded font-style:normal. captions.mjs also gains a parity pin across
the three workflows that ship it.
Not addressed: a VariableFont file is still declared at a single font-weight
rather than its range, so weights it could interpolate are still synthesized.