src/styles/fonts.css (12426B)
1 /* Two files, two scripts. Every byte and every ratio below was measured on the shipped
2 files; `python scripts/subset-fonts.py` regenerates them and re-prints the hashes.
3
4 Lora is the only Latin face on the site. Body, headings, links, kickers, nav and the
5 wordmark all resolve to it, at 400, 500 and 700 off one weight axis. Pretendard covers
6 Hangul, which Lora does not have at all, so "one face" means one Latin face: the
7 Korean gate below is not a second opinion about type, it is the other half of the
8 character set.
9
10 latin lora 84,764 -> 22,300 wght 400-700, 105 codepoints, kern + liga
11 ko pretendard 210,008 -> 118,136 wght 400-700, 963 Hangul codepoints, no
12 OpenType features retained (Pretendard's
13 GSUB carries no liga to keep)
14
15 One Latin file at 22,300B, where the Newsreader/Inter pair it replaced cost 43,268B,
16 and one font request on an English route instead of two. Its 105 codepoints are
17 printable ASCII plus the ten non-ASCII marks the corpus painted when the subset was
18 cut, U+2022 and U+2248 among them (LATIN in scripts/subset-fonts.py). Codepoints the
19 built pages paint that neither file covers - arrows, box drawing, circled digits,
20 three hiragana, one CJK ideograph, a handful of emoji, and the footer's © - fall
21 back to a system face.
22
23 liga is kept. Lora is a serif whose f hook runs into a following dotted i, and the
24 built text holds 49 `fi` and 46 `fl` pairs outside code (visible text of dist/, tags
25 stripped, diagram labels included). Lora ships exactly f+i and f+l; keeping both costs 396B.
26
27 No italic is shipped. dist/ holds 14 <em>, 0 <i> and 0 <cite>; the 14 are on two
28 articles, both <article class="post" lang="ko">, and [slug].astro sets
29 `.post[lang='ko'] .prose em { font-style: normal; font-weight: 500 }` because
30 Pretendard has no italic to shear. So italic renders on no built page, and nothing
31 is synthesised either, because nothing asks for it. Lora-Italic would be ~23KB that
32 is never fetched. To bring it back: drop Lora-Italic-wght.ttf in fonts-src/ and add
33 one JOBS row in scripts/subset-fonts.py.
34
35 The Korean face is not sliced by unicode-range beyond the Hangul gate. Slicing was
36 measured: cutting it into 4/8/12/16 frequency-ordered ranges and resolving each built
37 page against its own syllables leaves the median page pulling 132-140KB at every N,
38 because a Korean post spreads up to 658 distinct syllables across the whole frequency
39 curve. Per-file woff2 overhead then makes the total worse, not better (118,136 as one
40 file becomes ~152,000 across eight). 118KB is the price of the script.
41
42 woff2 encoding is not byte-deterministic, so the ko figure drifts by ~100B per
43 rebuild, and so does its content-hashed filename: re-running the script renames the
44 file even when its input has not changed. */
45
46 /* One face, one axis, one @font-face. The display role needs 500 and the body role
47 needs 400, 500 and 700; splitting them into two pinned faces would ship 36,180B in
48 three files against 22,300B in one, and buy nothing, because both roles want the same
49 range. Lora's axis is exactly 400-700, so all three points are real instances and
50 nothing is ever synthesised. There is no optical-size axis on this face: the 38px h1
51 is the 16px letter enlarged, with identical stroke modulation. That is the look this
52 unification buys, and it is cheap to live with because the ladder tops out at 38px,
53 well short of display sizes, where an unmodulated enlargement shows most.
54
55 No size-adjust. With a single Latin face there is no second Latin face to reconcile
56 with, and the type scale in tokens.css is derived from Lora's own outlines at its own
57 scale. Lora ships at that scale, and the Korean face is matched to it below. */
58 @font-face {
59 font-family: 'Lora';
60 src: url('/fonts/latin-9bba8f25.woff2') format('woff2-variations');
61 font-weight: 400 700;
62 font-style: normal;
63 font-display: swap;
64 }
65
66 /* Hangul only - no Latin outlines survive in this file. The unicode-range gate makes
67 them unreachable, and carrying them would cost 12,316B.
68
69 Read that first line before reading the arithmetic: this face draws 963 Hangul
70 codepoints and nothing else, so its own Latin metrics never paint. What size-adjust
71 does here is scale Hangul against Lora's Latin. The x-height ratio is how the number
72 is derived, not what it matches.
73
74 Lora x-height .50000 / Pretendard x-height .53027 = 94.29%
75
76 Why that ratio and not another: Pretendard's Latin is Inter's, uniformly scaled, so
77 Inter .54590 / Pretendard .53027 = 102.95% is the scale at which Pretendard's Latin
78 equals Inter's, and therefore the scale at which its Hangul sits as its designer drew
79 it against that Latin - the factor this face carried beside Inter. Substituting Lora
80 for Inter in that chain gives 102.95% x (.50000 / .54590) = .50000 / .53027, with the
81 Inter term cancelling. It is the same method, applied to Lora.
82
83 Checked against what is actually drawn, measured on the shipped file at wght 400:
84 Hangul ink tops at .7881em (median of 961 syllables, max .8027) and bottoms at
85 -.0742em. At 94.29% that top lands at .7431em, against Lora's tallest lowercase at
86 .7600em and its capitals at .7000em - Hangul between cap and ascender, which is where
87 a syllable block belongs. The other candidate ratios overshoot: cap-height parity
88 (.70000 / .70703) is 99.01% and puts Hangul at .7803em, over Lora's ascenders;
89 lowercase-ascender parity (.75500 / .70703) is 106.78% and is further still.
90
91 Note what this costs. Article prose in dist/, code excluded, is 53,523 Hangul letters
92 against 14,717 Latin, so 94.29% shrinks the four fifths of the prose that is Korean
93 to match the fifth that is not: Hangul's ink top is .7431em against .8109em at the
94 Inter-matched 102.9%, 8.4% smaller. That is the honest consequence of unifying on a
95 face with no Hangul. If Korean should give up less, o-height parity (.51600 / .53711)
96 = 96.07% is the defensible middle - and it is a two-number change, this size-adjust
97 and KoFallback's (.8642578 x the new factor), because the two KoFallback overrides
98 below do not move with it. */
99 @font-face {
100 font-family: 'KoBody';
101 src: url('/fonts/ko-02f8cebd.woff2') format('woff2-variations');
102 font-weight: 400 700;
103 font-style: normal;
104 font-display: swap;
105 size-adjust: 94.29%;
106 unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7A3, U+D7B0-D7FF;
107 }
108 /* One Korean declaration serves headings and body alike: with one Latin face there is no
109 separate display face for a second Korean face to track. */
110
111 /* ── Fallback metrics ───────────────────────────────────────────────────────────────
112 118KB arrives after HTML, after CSS, after layout. Until it lands, every page that
113 sets Hangul - each article and the Writing index - paints it in a system Korean face,
114 and without these overrides the swap reflows every Korean paragraph.
115
116 The numbers are exact, not fitted. Measured on pretendard-var.woff2: all 963 Hangul
117 codepoints advance 1770/2048 - the histogram has a single entry, no exceptions - with
118 ascent 1950, descent -494, lineGap 0, unitsPerEm 2048.
119
120 advance 1770/2048 = .8642578 x size-adjust .9429 = .8149080
121 ascent 1950/2048 = .9521484 x .9429 = .8977807
122 descent 494/2048 = .2412109 x .9429 = .2274378
123
124 Malgun Gothic, Apple SD Gothic Neo and Noto Sans KR all advance Hangul at exactly
125 1.0em, so the fallback is 12.4% wide uncorrected. Scaling it to .8149080 makes the
126 swap a glyph substitution with no line-break movement anywhere.
127
128 size-adjust .8149080 / 1.0 = 81.49%
129 ascent-override .8977807 / .8149080 = 110.17%
130 descent-override .2274378 / .8149080 = 27.91%
131
132 Only the first number depends on KoBody's size-adjust. The two overrides are ratios
133 in which the KoBody factor appears in both numerator and denominator and cancels
134 exactly - they reduce to 1950/1770 and 494/1770 - so 110.17% and 27.91% hold for any
135 KoBody value. Do not recompute them.
136
137 One rule with every platform font in its src list, not one rule per platform. With
138 several same-family rules Chrome builds the fallback from every face whose local()
139 resolves and loads each installed one again for every size and weight it sets Hangul
140 at, before first paint. A machine with Malgun Gothic and Noto Sans KR both installed,
141 which recent Korean Windows is, pays for both. In a single src list only the first
142 local() that resolves is loaded.
143
144 The order puts Malgun Gothic first wherever it is installed (Windows, and Macs with
145 Office), then Apple SD Gothic Neo (macOS, iOS), then Noto Sans KR or Nanum Gothic.
146 One set of overrides serves all of them: the ascent and descent overrides carry
147 Pretendard's metrics whichever face they land on, and the size-adjust assumes the
148 1.0em Hangul advance the first three share. Malgun's hhea (2229/-495) and typo
149 (1638/-410) metrics disagree; the overrides make it irrelevant which one the browser
150 reads. */
151 @font-face {
152 font-family: 'KoFallback';
153 src: local('Malgun Gothic'), local('Apple SD Gothic Neo'),
154 local('Noto Sans KR'), local('NotoSansKR-Regular'), local('Nanum Gothic');
155 size-adjust: 81.49%; ascent-override: 110.17%; descent-override: 27.91%; line-gap-override: 0%;
156 unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7A3, U+D7B0-D7FF;
157 }
158
159 /* Latin fallback. Lora is preloaded on every route, so this is slow-link insurance -
160 but a sans stack (Helvetica, Arial, Segoe UI) would make the unstyled flash sans and
161 the swap sans-to-serif. Against a serif it has to be a serif. Georgia ships on
162 Windows and macOS both and is the closest of the system serifs.
163
164 Matched on mean lowercase advance from hmtx, which is the criterion that keeps the
165 swap from reflowing a line (x-height would give 103.85% and move line breaks). The
166 two overrides are divided by the declared size-adjust, not by the raw ratio, because
167 size-adjust scales the overridden metrics too - the browser computes the final ascent
168 as ascent-override x size-adjust:
169
170 size-adjust Lora .52404 / Georgia .49914 = 104.99% -> 105%
171 ascent-override Lora sTypoAscender 1.006em / 1.05 = 95.81%
172 descent-override Lora sTypoDescender .274em / 1.05 = 26.10%
173
174 For comparison: Times New Roman would need 114.09% and Arial 107.05%. Only Georgia is
175 measured here, so platforms without it fall through to the unadjusted generic at the
176 end of the stack rather than to a guessed number. */
177 @font-face {
178 font-family: 'LatinFallback';
179 src: local('Georgia');
180 size-adjust: 105%; ascent-override: 95.81%; descent-override: 26.10%; line-gap-override: 0%;
181 }
182
183 :root {
184 /* One stack under two names. The templates ask for a display face and a body face;
185 there is one Latin face, so both resolve to Lora and the distinction survives only
186 in the variable names the page templates use. */
187 --font-display: 'Lora', 'KoBody', 'KoFallback', 'LatinFallback', Georgia, 'Times New Roman', serif;
188 --font-body: 'Lora', 'KoBody', 'KoFallback', 'LatinFallback', Georgia, 'Times New Roman', serif;
189 --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
190
191 /* src/pages/writing/index.astro is written as a portable component: it re-declares
192 --font-body and --font-display from `var(--font-sans, ...)` and `var(--font-serif,
193 ...)`, whose fallbacks name Inter and Newsreader as *installed local families*, not
194 as the @font-face families this file defines. Without these two hooks the Writing
195 index - every entry title on the site - would paint in whatever Inter or Newsreader
196 the reader happens to have installed, and otherwise in system-ui and Georgia, while
197 still paying for the preload. Defining them here is what puts the index on the
198 shipped face, without editing the component. */
199 --font-sans: 'Lora', 'KoBody', 'KoFallback', 'LatinFallback', Georgia, 'Times New Roman', serif;
200 --font-serif: 'Lora', 'KoBody', 'KoFallback', 'LatinFallback', Georgia, 'Times New Roman', serif;
201 }