blog.git

blog

~ Personal research notes. blog.sqzer.com
git clone https://github.com/debuman/blog.git

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 }