Syne's g: not a bug but the design, and I still replaced two letters
The lowercase g and ğ of Syne, the site's display font, looked broken to us at large sizes. I checked the font file; it was not a bug but the design itself. The fix was a patch of a few KB that takes only these two letters from Archivo, plus the browser choosing fonts letter by letter.
01A letter that looked broken
The site's headings are set in Syne, mostly at weight 800. It is a wide font with a lot of character, and it carries much of the site's identity. But the lowercase g always looked odd to us at large sizes. At first we took it for a rendering bug: the letter looked as if a part were missing or misplaced. Since ğ is common in Turkish, it stood out not only in English words but in Turkish headings too.
02First I looked for the bug on my side
The usual suspects were clear: the subset Google Fonts sends, the browser's synthetic bold, or a setting in my own CSS. I ruled them out one by one. I compared the letter with the full-charset Syne TTF that I also use for the share cards; the same shape was there. I looked at the font's stylistic sets as well and did not find the kind of alternative g I was hoping for.
The conclusion was clear: this is not a bug, it is Syne's original design. Knowing that mattered, because if it had been a bug, loading the right file or fixing a setting would have been enough. Since it was the design, I had three options: change the display font entirely, accept the letter as it is, or take only these two letters from another font. The first would have changed the look of the site from scratch, the second would have kept bothering us. I chose the third.
03A two-letter font
I picked Archivo as the stand-in. Archivo has a width axis, and wdth 125 corresponds to its Expanded cut; to my eye these wide proportions were the option that sat most quietly next to Syne. The Google Fonts css2 endpoint supports a text parameter: it returns a font file containing only the requested characters. text=g%C4%9F means gğ. Since --f-display is used at weights 800 and 600 on the site, I ask for 600, 700 and 800. The stylesheet loads with the media="print" and onload trick, without blocking the first paint.
<!-- index.html -->
<link rel="stylesheet" media="print" onload="this.media='all'"
href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@125,600;125,700;125,800&text=g%C4%9F&display=swap" />
/* src/styles/tokens.css */
--f-display: 'Archivo', 'Syne', 'Space Grotesk', sans-serif;The CSS Google returns for this request is instructive. There is one @font-face rule per weight: font-family 'Archivo', font-stretch 125% and unicode-range: U+67, U+11F, U+306. The first two are g and ğ. The third is the combining breve; since ğ can also be written as g followed by that mark, it is in the file too. The result is a font of a few KB for two letters.
04Fallback works letter by letter
The browser's font matching rules do the rest. Archivo is first in the --f-display list. When drawing text, the browser looks for the first suitable face in the list for each character; if the character is outside a @font-face rule's unicode-range, or the font has no glyph for it, it moves on to the next family. So in the word "doğru", d, o, r and u come from Syne and ğ comes from Archivo. Uppercase G is not in the range, so it stays in Syne as well.
Weights match by the same logic. Since Archivo has a separate @font-face rule per weight, the g in a heading set at 800 comes from Archivo's 800 cut, and in text set at 600, such as .ablead, from the 600 cut. Space Grotesk, third in the list, is a safety net for when Syne has not loaded yet or cannot load at all; the final sans-serif is its own fallback.
unicode-range has another benefit: the browser only downloads this font file when a character in that range is about to be drawn with this font stack. For a heading without g or ğ, Archivo is never requested. To be honest, there is a small cost too: since the stylesheet loads without blocking and with display=swap, the g can briefly show in Syne before Archivo arrives and then switch.
The boot screen is outside this rule. Its DEVILYXRD word uses Syne directly rather than --f-display; it is uppercase and has no g, so it does not need the patch.
05The same patch on the share cards
Satori, which draws the share cards, has no unicode-range and no per-glyph fallback, so the same split has to be made by hand. At build time, scripts/og.mjs downloads the same Archivo subset from Google Fonts as TrueType. displayText splits the title into words, and each word into runs of g and ğ and everything else; the g and ğ runs are drawn with the Archivo family. Without a network the patch is skipped and the cards are drawn with Syne's g.
const runs = hasPatch ? w.split(/([gğ]+)/).filter(Boolean) : [w]
return h('span', { display: 'flex' },
...runs.map((r) => (/^[gğ]+$/.test(r)
? h('span', { fontFamily: 'Archivo' }, r)
: r)))You can change two letters of a font without changing the font; the browser already picks letter by letter.