Skip to main content

max / makenotwork

Put the MNW server on the house fonts, and stop fetching any of them from Google The web half of the typography standard. --font-mono and --font-sans now come from the generated typography.css, which build.rs writes from makeover and whose faces build.rs cuts with quasi-type into static/fonts/. Four static files covering two weights (IBM Plex Mono and Lato, regular and bold, 101 KB of woff2) become two variable ones carrying wght 200-800, 81 KB. --font-body is gone: it and --font-sans were the same need under two names, which is the split this deletes. --font-heading stays and is Young Serif, the brand tier, exempt per product. The embeds inline the layer alongside the theme and spacing they already inline. Their font-family literals named Lato and IBM Plex Mono, neither of which any embed document ever declared, so all five have been rendering in the visitor's system faces since they were written. The error pages were pulling three families off fonts.googleapis.com. 404 and 500 are rendered by the app and self-host now; 502 is the app-is-down page, cannot reach /static by construction, and drops the webfonts rather than depending on a third party during an outage. lint-frontend now reads typography.css for definitions, which is what caught this commit the first time: it is where --font-mono and --font-sans live now.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-17 16:17 UTC
Signed with PGP, not checked
Commit: 451fd2238014531b8f47f4c6c5296aa9e5d9b889
Parent: b30ad28
31 files changed, +504 insertions, -315 deletions
M .gitignore +9
@@ -13,6 +13,15 @@
13 13 server/static/geometry.css
14 14 server/static/layout.css
15 15 server/static/embed-geometry.css
16 + server/static/typography.css
17 +
18 + # The house faces, cut by quasi-type in build.rs. Same rule as the stylesheets
19 + # above and the same rule shop and the Alloy image follow: the pipeline is the
20 + # source, and a committed face is a second one that nothing rebuilds.
21 + server/static/fonts/QuasiMono.woff2
22 + server/static/fonts/QuasiBody.woff2
23 + server/static/fonts/OFL-QuasiMono.txt
24 + server/static/fonts/OFL-QuasiBody.txt
16 25
17 26 # Environment files (contain secrets)
18 27 server/.env
M server/Cargo.lock +169 -19
@@ -104,6 +104,21 @@
104 104 "memchr",
105 105 ]
106 106
107 + [[package]]
108 + name = "alloc-no-stdlib"
109 + version = "2.0.4"
110 + source = "registry+https://github.com/rust-lang/crates.io-index"
111 + checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
112 +
113 + [[package]]
114 + name = "alloc-stdlib"
115 + version = "0.2.4"
116 + source = "registry+https://github.com/rust-lang/crates.io-index"
117 + checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
118 + dependencies = [
119 + "alloc-no-stdlib",
120 + ]
121 +
107 122 [[package]]
108 123 name = "allocator-api2"
109 124 version = "0.2.21"
@@ -1432,6 +1447,27 @@
1432 1447 "cfg_aliases",
1433 1448 ]
1434 1449
1450 + [[package]]
1451 + name = "brotli"
1452 + version = "8.0.4"
1453 + source = "registry+https://github.com/rust-lang/crates.io-index"
1454 + checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
1455 + dependencies = [
1456 + "alloc-no-stdlib",
1457 + "alloc-stdlib",
1458 + "brotli-decompressor",
1459 + ]
1460 +
1461 + [[package]]
1462 + name = "brotli-decompressor"
1463 + version = "5.0.3"
1464 + source = "registry+https://github.com/rust-lang/crates.io-index"
1465 + checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
1466 + dependencies = [
1467 + "alloc-no-stdlib",
1468 + "alloc-stdlib",
1469 + ]
1470 +
1435 1471 [[package]]
1436 1472 name = "bs58"
1437 1473 version = "0.5.1"
@@ -1466,6 +1502,20 @@
1466 1502 version = "1.25.0"
1467 1503 source = "registry+https://github.com/rust-lang/crates.io-index"
1468 1504 checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
1505 + dependencies = [
1506 + "bytemuck_derive",
1507 + ]
1508 +
1509 + [[package]]
1510 + name = "bytemuck_derive"
1511 + version = "1.12.0"
1512 + source = "registry+https://github.com/rust-lang/crates.io-index"
1513 + checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f"
1514 + dependencies = [
1515 + "proc-macro2",
1516 + "quote",
1517 + "syn 3.0.3",
1518 + ]
1469 1519
1470 1520 [[package]]
1471 1521 name = "byteorder"
@@ -2845,6 +2895,15 @@
2845 2895 "windows-sys 0.48.0",
2846 2896 ]
2847 2897
2898 + [[package]]
2899 + name = "euclid"
2900 + version = "0.22.14"
2901 + source = "registry+https://github.com/rust-lang/crates.io-index"
2902 + checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
2903 + dependencies = [
2904 + "num-traits",
2905 + ]
2906 +
2848 2907 [[package]]
2849 2908 name = "event-listener"
2850 2909 version = "5.4.1"
@@ -3001,6 +3060,15 @@
3001 3060 source = "registry+https://github.com/rust-lang/crates.io-index"
3002 3061 checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
3003 3062
3063 + [[package]]
3064 + name = "font-types"
3065 + version = "0.12.3"
3066 + source = "registry+https://github.com/rust-lang/crates.io-index"
3067 + checksum = "75382bc7392ef10aad10935f92fc3db36d2d4dad0e5d96d8d65e04f89a07ec39"
3068 + dependencies = [
3069 + "bytemuck",
3070 + ]
3071 +
3004 3072 [[package]]
3005 3073 name = "form_urlencoded"
3006 3074 version = "1.2.2"
@@ -3598,7 +3666,7 @@
3598 3666 checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1"
3599 3667 dependencies = [
3600 3668 "bstr",
3601 - "hashbrown 0.15.5",
3669 + "hashbrown 0.17.1",
3602 3670 ]
3603 3671
3604 3672 [[package]]
@@ -4956,6 +5024,18 @@
4956 5024 "typewit",
4957 5025 ]
4958 5026
5027 + [[package]]
5028 + name = "kurbo"
5029 + version = "0.13.1"
5030 + source = "registry+https://github.com/rust-lang/crates.io-index"
5031 + checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
5032 + dependencies = [
5033 + "arrayvec",
5034 + "euclid",
5035 + "polycool",
5036 + "smallvec",
5037 + ]
5038 +
4959 5039 [[package]]
4960 5040 name = "lazy_static"
4961 5041 version = "1.5.0"
@@ -5257,6 +5337,7 @@
5257 5337 "quasi-axum",
5258 5338 "quasi-basics",
5259 5339 "quasi-router",
5340 + "quasi-type",
5260 5341 "quasi-webview",
5261 5342 "rand 0.10.2",
5262 5343 "regex",
@@ -5306,9 +5387,7 @@
5306 5387
5307 5388 [[package]]
5308 5389 name = "makeover"
5309 - version = "2.6.0"
5310 - source = "registry+https://github.com/rust-lang/crates.io-index"
5311 - checksum = "63f2504fc551cc50ca2766440ee1addd2452bfad02ec203411afe94a7d0a47a7"
5390 + version = "2.7.0"
5312 5391 dependencies = [
5313 5392 "include_dir",
5314 5393 "serde",
@@ -5317,9 +5396,7 @@
5317 5396
5318 5397 [[package]]
5319 5398 name = "makeover-build"
5320 - version = "0.39.0"
5321 - source = "registry+https://github.com/rust-lang/crates.io-index"
5322 - checksum = "52baae176ab62ac8d79238301be5dc112f1615630f647ab1465b57b5872edee0"
5399 + version = "0.40.0"
5323 5400 dependencies = [
5324 5401 "makeover",
5325 5402 "makeover-geometry",
@@ -6228,6 +6305,15 @@
6228 6305 "universal-hash 0.6.1",
6229 6306 ]
6230 6307
6308 + [[package]]
6309 + name = "polycool"
6310 + version = "0.4.0"
6311 + source = "registry+https://github.com/rust-lang/crates.io-index"
6312 + checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
6313 + dependencies = [
6314 + "arrayvec",
6315 + ]
6316 +
6231 6317 [[package]]
6232 6318 name = "polyval"
6233 6319 version = "0.6.2"
@@ -6583,6 +6669,22 @@
6583 6669 "makeover-layout",
6584 6670 ]
6585 6671
6672 + [[package]]
6673 + name = "quasi-type"
6674 + version = "0.1.0"
6675 + dependencies = [
6676 + "brotli",
6677 + "font-types",
6678 + "kurbo",
6679 + "read-fonts",
6680 + "serde",
6681 + "sha2 0.10.9",
6682 + "skrifa",
6683 + "toml 1.1.3+spec-1.1.0",
6684 + "write-fonts",
6685 + "zip 6.0.0",
6686 + ]
6687 +
6586 6688 [[package]]
6587 6689 name = "quasi-webview"
6588 6690 version = "0.18.0"
@@ -6871,6 +6973,17 @@
6871 6973 "cipher 0.4.4",
6872 6974 ]
6873 6975
6976 + [[package]]
6977 + name = "read-fonts"
6978 + version = "0.43.0"
6979 + source = "registry+https://github.com/rust-lang/crates.io-index"
6980 + checksum = "a6b5cae85882cfb0167038e7726a426f47ebd808e67af78c2d6bcbb02acbf3ce"
6981 + dependencies = [
6982 + "bytemuck",
6983 + "font-types",
6984 + "once_cell",
6985 + ]
6986 +
6874 6987 [[package]]
6875 6988 name = "redox_syscall"
6876 6989 version = "0.5.18"
@@ -7884,6 +7997,16 @@
7884 7997 source = "registry+https://github.com/rust-lang/crates.io-index"
7885 7998 checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
7886 7999
8000 + [[package]]
8001 + name = "skrifa"
8002 + version = "0.46.0"
8003 + source = "registry+https://github.com/rust-lang/crates.io-index"
8004 + checksum = "de0bbcc507e438bfe765692ae5228059959781db7430fe36e1cfd569e7c88c89"
8005 + dependencies = [
8006 + "bytemuck",
8007 + "read-fonts",
8008 + ]
8009 +
7887 8010 [[package]]
7888 8011 name = "slab"
7889 8012 version = "0.4.12"
@@ -10144,6 +10267,19 @@
10144 10267 source = "registry+https://github.com/rust-lang/crates.io-index"
10145 10268 checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
10146 10269
10270 + [[package]]
10271 + name = "write-fonts"
10272 + version = "0.52.0"
10273 + source = "registry+https://github.com/rust-lang/crates.io-index"
10274 + checksum = "843cebb7bd36359861dbce43d54ac8b33dfdf7b80a95913adc1ef7c7bb881e62"
10275 + dependencies = [
10276 + "font-types",
10277 + "indexmap 2.14.0",
10278 + "kurbo",
10279 + "log",
10280 + "read-fonts",
10281 + ]
10282 +
10147 10283 [[package]]
10148 10284 name = "writeable"
10149 10285 version = "0.6.3"
@@ -10532,6 +10668,20 @@
10532 10668 "zopfli",
10533 10669 ]
10534 10670
10671 + [[package]]
10672 + name = "zip"
10673 + version = "6.0.0"
10674 + source = "registry+https://github.com/rust-lang/crates.io-index"
10675 + checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b"
10676 + dependencies = [
10677 + "arbitrary",
10678 + "crc32fast",
10679 + "flate2",
10680 + "indexmap 2.14.0",
10681 + "memchr",
10682 + "zopfli",
10683 + ]
10684 +
10535 10685 [[package]]
10536 10686 name = "zip"
10537 10687 version = "8.6.0"
@@ -10623,12 +10773,16 @@
10623 10773 ]
10624 10774
10625 10775 [[patch.unused]]
10626 - name = "synckit-client"
10627 - version = "0.8.0"
10776 + name = "quasi-immediate"
10777 + version = "0.18.0"
10628 10778
10629 10779 [[patch.unused]]
10630 - name = "synckit-config"
10631 - version = "0.2.0"
10780 + name = "quasi-store"
10781 + version = "0.1.0"
10782 +
10783 + [[patch.unused]]
10784 + name = "quasi-tauri"
10785 + version = "0.18.0"
10632 10786
10633 10787 [[patch.unused]]
10634 10788 name = "kberg"
@@ -10643,13 +10797,9 @@
10643 10797 version = "0.1.0"
10644 10798
10645 10799 [[patch.unused]]
10646 - name = "quasi-immediate"
10647 - version = "0.18.0"
10800 + name = "synckit-client"
10801 + version = "0.8.0"
10648 10802
10649 10803 [[patch.unused]]
10650 - name = "quasi-store"
10651 - version = "0.1.0"
10652 -
10653 - [[patch.unused]]
10654 - name = "quasi-tauri"
10655 - version = "0.18.0"
10804 + name = "synckit-config"
10805 + version = "0.2.0"
@@ -245,11 +245,22 @@
245 245 # Emits static/geometry.css (makeover-geometry) and static/layout.css
246 246 # (makeover-webview). The same generator GO and BB run; only the output paths
247 247 # differ, since the server serves its stylesheets rather than bundling them.
248 - makeover-build = "0.39.0"
248 + makeover-build = "0.40.0"
249 249 # Read directly for the embeds' own copy of the spacing layer, which they need
250 250 # because an iframe cannot link a stylesheet. makeover-build does not re-export
251 251 # it, so the pin lives here and has to track the one makeover-build resolves.
252 252 makeover-geometry = "0.7"
253 + # Cuts the two house faces into `static/fonts/` at build time. It is a git
254 + # dependency because quasi-type is `publish = false`, which is also why
255 + # makeover-build cannot do this and only emits the CSS that fetches the result.
256 + #
257 + # The rev is pinned, and it is the same rev shop's `shop-font` and Alloy's
258 + # `QUASI_TYPE_REV` name. That is the point of pinning it here: the house face is
259 + # one face, so the one a browser downloads has to be the one the image installs
260 + # and the terminal bundles. The three move together, in one pass, or a user sees
261 + # two Quasi Monos. Under the tree's `[patch]` block the rev is ignored and the
262 + # working copy is what builds.
263 + quasi-type = { git = "https://makenot.work/git/max/quasi-type.git", version = "0.1", rev = "cbbb9e3dd6faa29582b9437c0685ff91af97afdb" }
253 264
254 265 [dev-dependencies]
255 266 tower = { version = "0.5.3", features = ["util"] }
@@ -47,6 +47,16 @@
47 47 )
48 48 .expect("write static/embed-geometry.css");
49 49
50 + // The typography layer, and the faces it fetches.
51 + //
52 + // Two halves that have to agree: makeover_build writes the `@font-face`
53 + // rules and the two tokens, and `cut_house_faces` puts the woff2 files
54 + // where those rules point. Both name the file through
55 + // `makeover::WEBFONT_*_FILE`, so the agreement is a constant rather than a
56 + // string typed twice.
57 + makeover_build::typography_css("static/typography.css", "/static/fonts");
58 + cut_house_faces();
59 +
50 60 println!("cargo::rerun-if-changed=build.rs");
51 61
52 62 // The landing shots' own dimensions, so the description can reserve their
@@ -166,6 +176,66 @@
166 176 write_if_changed(Path::new("templates/_sheet.html"), &sheet_partial);
167 177 }
168 178
179 + /// Cut Quasi Mono and Quasi Body into `static/fonts/`, as woff2.
180 + ///
181 + /// Cut rather than committed, which is the same rule `shop-font` and the Alloy
182 + /// image follow: a face in the repository is a second source of truth that
183 + /// nothing rebuilds, so the glyph set and the shipped face drift and nobody
184 + /// finds out until a mark looks wrong. The pipeline is the source; these are
185 + /// its output, and they are gitignored.
186 + ///
187 + /// # woff2 only, and no ttf beside it
188 + ///
189 + /// The old pairs here offered `woff2` with a `truetype` fallback, which was
190 + /// already dead weight: every browser that can run this site has supported
191 + /// woff2 since 2018, and the ttf is 2-3x the bytes for a case that does not
192 + /// arrive. `makeover::font_face_css` declares woff2 alone, so writing a ttf
193 + /// would put a file in `static/` that nothing ever fetches.
194 + ///
195 + /// # The base is fetched once
196 + ///
197 + /// `quasi_type::cut` downloads the pinned base into the cache directory below
198 + /// and checksums it, so a warm target directory needs no network. `offline` is
199 + /// false because the first build on a fresh machine has to be able to fetch;
200 + /// a failure here is fatal rather than skipped, since a missing face is a site
201 + /// rendering in the fallback with nothing to say so.
202 + fn cut_house_faces() {
203 + let dir = Path::new("static/fonts");
204 + fs::create_dir_all(dir).expect("create static/fonts");
205 + let cache = Path::new(&std::env::var("OUT_DIR").expect("cargo sets OUT_DIR")).join("bases");
206 +
207 + for (slot, file) in [
208 + ("quasi-mono", makeover_build::WEBFONT_MONO_FILE),
209 + ("quasi-body", makeover_build::WEBFONT_SANS_FILE),
210 + ] {
211 + let cut = quasi_type::cut(slot, &cache, false)
212 + .unwrap_or_else(|e| panic!("could not cut {slot}: {e}"));
213 + let face = cut
214 + .faces
215 + .first()
216 + .unwrap_or_else(|| panic!("{slot} cut no faces"));
217 + // One file per slot, and it carries the whole `wght` 200-800 axis. The
218 + // four static files this replaces (Plex Mono and Lato, regular and
219 + // bold) were 101 KB of woff2 between them and covered two weights.
220 + write_bytes_if_changed(&dir.join(file), &face.woff2);
221 + write_bytes_if_changed(
222 + &dir.join(format!("OFL-{}.txt", cut.family.replace(' ', ""))),
223 + &cut.license,
224 + );
225 + }
226 + }
227 +
228 + /// `write_if_changed` for bytes.
229 + ///
230 + /// Same reason as the text version: rewriting an identical file bumps its mtime
231 + /// and makes every downstream step think the fonts moved.
232 + fn write_bytes_if_changed(path: &Path, contents: &[u8]) {
233 + if fs::read(path).is_ok_and(|existing| existing == contents) {
234 + return;
235 + }
236 + fs::write(path, contents).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
237 + }
238 +
169 239 /// Intrinsic dimensions of the landing screenshots, read from the files.
170 240 ///
171 241 /// A picture that cannot say how big it is cannot have its space reserved, so
@@ -11,10 +11,12 @@
11 11 set -u
12 12 ROOT="$(cd "$(dirname "$0")/.." && pwd)"
13 13 STATIC="$ROOT/static"
14 - # The site's own sheets. The generated ones (geometry.css, layout.css) are
15 - # makeover's output and are never linted; they are the standard, not the code.
14 + # The site's own sheets. The generated ones (geometry.css, layout.css,
15 + # typography.css) are makeover's output and are never linted; they are the
16 + # standard, not the code. They are still READ, because they are where the
17 + # tokens the site's sheets spend are defined.
16 18 SITE_SHEETS="$STATIC/style.css $STATIC/wizard.css $STATIC/media-player.css"
17 - GENERATED_SHEETS="$STATIC/geometry.css $STATIC/layout.css"
19 + GENERATED_SHEETS="$STATIC/geometry.css $STATIC/layout.css $STATIC/typography.css"
18 20
19 21 violations=0
20 22
@@ -52,9 +52,16 @@
52 52 // Whole value is being early: a preload discovered after the sheets
53 53 // it races bought nothing.
54 54 .with_head_first(
55 - "<link rel=\"preload\" href=\"/static/fonts/Lato-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\
55 + "<link rel=\"preload\" href=\"/static/fonts/QuasiBody.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\
56 56 <link rel=\"preload\" href=\"/static/fonts/ysrf.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>",
57 57 )
58 + // First, and before the sheets that use the tokens it defines. The
59 + // `@font-face` rules take no part in the cascade so their position
60 + // buys nothing there; what it buys is discovery, since a face the
61 + // parser has not reached yet is a face the browser has not started
62 + // fetching. `style.css` still wins every contest it won before:
63 + // this file defines two tokens and matches no element.
64 + .styled(format!("/static/typography.css?v={V}"))
58 65 .styled(format!("/static/geometry.css?v={V}"))
59 66 .styled(format!("/static/layout.css?v={V}"))
60 67 .styled(format!("/static/style.css?v={V}"))
@@ -131,7 +138,7 @@
131 138 let stmt = head
132 139 .find("@layer makeover, base, components, responsive;")
133 140 .expect("the order is stated");
134 - for sheet in ["geometry.css", "layout.css", "style.css"] {
141 + for sheet in ["typography.css", "geometry.css", "layout.css", "style.css"] {
135 142 assert!(stmt < head.find(sheet).expect("the sheet is linked"));
136 143 }
137 144 }
@@ -148,7 +155,11 @@
148 155 #[test]
149 156 fn the_fonts_are_preloaded_before_the_sheets_that_race_them() {
150 157 let head = head();
151 - assert!(head.find("Lato-Regular.woff2") < head.find("style.css"));
158 + assert!(head.find("QuasiBody.woff2") < head.find("style.css"));
159 + // The retired pair, checked by absence: a preload for a face nothing
160 + // declares is a download the browser makes and never uses.
161 + assert!(!head.contains("Lato"));
162 + assert!(!head.contains("IBMPlexMono"));
152 163 }
153 164
154 165 #[test]
@@ -264,7 +264,7 @@
264 264
265 265 /* Description */
266 266 .media-description {
267 - font-family: var(--font-body);
267 + font-family: var(--font-sans);
268 268 font-size: var(--text-lead);
269 269 line-height: 1.9;
270 270 margin-bottom: var(--gap-page);
@@ -316,7 +316,7 @@
316 316 }
317 317
318 318 .chapter-title {
319 - font-family: var(--font-body);
319 + font-family: var(--font-sans);
320 320 color: var(--content);
321 321 }
322 322
@@ -97,37 +97,17 @@
97 97 font-display: swap;
98 98 }
99 99
100 - @font-face {
101 - font-family: "IBM Plex Mono";
102 - src: url("/static/fonts/IBMPlexMono-Regular.woff2") format("woff2"),
103 - url("/static/fonts/IBMPlexMono-Regular.ttf") format("truetype");
104 - font-weight: normal;
105 - font-display: swap;
106 - }
100 + /* The mono and body faces are NOT declared here any more. They are Quasi Mono
101 + and Quasi Body, cut by quasi-type and fetched by the generated
102 + `typography.css`, which also defines `--font-mono` and `--font-sans`. What
103 + left: four `@font-face` blocks for IBM Plex Mono and Lato, regular and bold,
104 + 101 KB of woff2 covering two weights. The two that replaced them are 81 KB
105 + and carry the whole 200-800 axis.
107 106
108 - @font-face {
109 - font-family: "IBM Plex Mono";
110 - src: url("/static/fonts/IBMPlexMono-Bold.woff2") format("woff2"),
111 - url("/static/fonts/IBMPlexMono-Bold.ttf") format("truetype");
112 - font-weight: bold;
113 - font-display: swap;
114 - }
115 -
116 - @font-face {
117 - font-family: "Lato";
118 - src: url("/static/fonts/Lato-Regular.woff2") format("woff2"),
119 - url("/static/fonts/Lato-Regular.ttf") format("truetype");
120 - font-weight: normal;
121 - font-display: swap;
122 - }
123 -
124 - @font-face {
125 - font-family: "Lato";
126 - src: url("/static/fonts/Lato-Bold.woff2") format("woff2"),
127 - url("/static/fonts/Lato-Bold.ttf") format("truetype");
128 - font-weight: bold;
129 - font-display: swap;
130 - }
107 + Young Serif stays above, and stays here rather than moving with them. It is
108 + the brand tier: per product, exempt by decision, and named by
109 + `--font-heading` below, which is MNW's alone. When the app-override layer
110 + lands (makeover 174ab3c1) this is the declaration that moves into it. */
131 111
132 112 /* Everything below is in the `components` cascade layer.
133 113
@@ -151,10 +131,17 @@
151 131 overriding this file's :root without needing to know the order. */
152 132 @layer components {
153 133 :root {
154 - /* Typography (three-tier system) */
134 + /* Typography: the brand tier only.
135 +
136 + `--font-mono` and `--font-sans` are the house tokens and come from
137 + typography.css, which loads before this file. Do not redefine either here
138 + -- that is the divergence the unification deleted, and this file is the
139 + one that would win.
140 +
141 + `--font-heading` is MNW's own and stays: the display tier is exempt per
142 + product, no renderer emits it, and no described screen resolves a token to
143 + it. */
155 144 --font-heading: "Young Serif", serif;
156 - --font-mono: "IBM Plex Mono", monospace;
157 - --font-body: "Lato", sans-serif;
158 145
159 146 /* --------------------------------------------------------------------
160 147 INTENT LAYER, the shared theme vocabulary (themeable)
@@ -309,7 +296,7 @@
309 296 margin: 0;
310 297 background-color: var(--surface-page);
311 298 color: var(--content);
312 - font-family: var(--font-body);
299 + font-family: var(--font-sans);
313 300 line-height: 1.6;
314 301 }
315 302
@@ -391,7 +378,7 @@
391 378 }
392 379
393 380 p {
394 - font-family: var(--font-body);
381 + font-family: var(--font-sans);
395 382 color: var(--content);
396 383 }
397 384
@@ -896,7 +883,7 @@
896 883 width: 100%;
897 884 border-collapse: collapse;
898 885 font-size: var(--text-note);
899 - font-family: var(--font-body);
886 + font-family: var(--font-sans);
900 887 margin-bottom: var(--gap-section);
901 888 }
902 889
@@ -1226,7 +1213,7 @@
1226 1213 .card-description {
1227 1214 font-size: var(--text-note);
1228 1215 opacity: 0.8;
1229 - font-family: var(--font-body);
1216 + font-family: var(--font-sans);
1230 1217 }
1231 1218
1232 1219 /* ===========================================
@@ -1370,7 +1357,7 @@
1370 1357 padding: var(--gap-section);
1371 1358 margin-bottom: var(--gap-section);
1372 1359 font-size: var(--text-note);
1373 - font-family: var(--font-body);
1360 + font-family: var(--font-sans);
1374 1361 }
1375 1362
1376 1363 .info-box h3 {
@@ -1399,7 +1386,7 @@
1399 1386 padding: var(--gap-section);
1400 1387 margin-bottom: var(--gap-section);
1401 1388 font-size: var(--text-note);
1402 - font-family: var(--font-body);
1389 + font-family: var(--font-sans);
1403 1390 }
1404 1391
1405 1392 .error-message {
@@ -1625,7 +1612,7 @@
1625 1612 font-size: var(--text-body);
1626 1613 opacity: 0.6;
1627 1614 border-bottom: 2px solid transparent;
1628 - font-family: var(--font-body);
1615 + font-family: var(--font-sans);
1629 1616 color: var(--content);
1630 1617 }
1631 1618
@@ -2201,7 +2188,7 @@
2201 2188 }
2202 2189
2203 2190 .article-page .article-body {
2204 - font-family: var(--font-body);
2191 + font-family: var(--font-sans);
2205 2192 font-size: var(--text-lead);
2206 2193 line-height: 1.9;
2207 2194 }
@@ -5280,7 +5267,7 @@
5280 5267 }
5281 5268
5282 5269 .paywall-desc {
5283 - font-family: var(--font-body);
5270 + font-family: var(--font-sans);
5284 5271 color: var(--content-muted);
5285 5272 margin-bottom: var(--gap-pane);
5286 5273 }
@@ -5449,7 +5436,7 @@
5449 5436 min-height: 400px;
5450 5437 padding: var(--gap-pane);
5451 5438 background: var(--surface-overlay);
5452 - font-family: var(--font-body);
5439 + font-family: var(--font-sans);
5453 5440 line-height: 1.8;
5454 5441 }
5455 5442
@@ -5933,7 +5920,7 @@
5933 5920 }
5934 5921
5935 5922 .founder-tagline {
5936 - font-family: var(--font-body);
5923 + font-family: var(--font-sans);
5937 5924 font-size: var(--text-body);
5938 5925 line-height: 1.55;
5939 5926 margin: var(--gap-section) auto 0;
@@ -6109,7 +6096,7 @@
6109 6096 padding: var(--gap-peer) var(--gap-section);
6110 6097 border: 1px solid var(--border);
6111 6098 border-radius: var(--radius-control);
6112 - font-family: var(--font-body);
6099 + font-family: var(--font-sans);
6113 6100 font-size: var(--text-note);
6114 6101 background: var(--surface-page);
6115 6102 color: var(--content);
@@ -6683,7 +6670,7 @@
6683 6670 }
6684 6671
6685 6672 .doc-body {
6686 - font-family: var(--font-body);
6673 + font-family: var(--font-sans);
6687 6674 font-size: var(--text-lead);
6688 6675 line-height: 1.9;
6689 6676 }
@@ -6824,7 +6811,7 @@
6824 6811 }
6825 6812
6826 6813 .docs-section a {
6827 - font-family: var(--font-body);
6814 + font-family: var(--font-sans);
6828 6815 font-size: var(--text-lead);
6829 6816 color: var(--content);
6830 6817 text-decoration: none;
@@ -6876,7 +6863,7 @@
6876 6863 width: 100%;
6877 6864 max-width: 320px;
6878 6865 padding: var(--gap-peer) var(--gap-section);
6879 - font-family: var(--font-body);
6866 + font-family: var(--font-sans);
6880 6867 font-size: var(--text-body);
6881 6868 background: var(--surface-sunken);
6882 6869 border: 1px solid var(--border);
@@ -6913,7 +6900,7 @@
6913 6900 }
6914 6901 .docs-search-result:last-child { border-bottom: none; }
6915 6902 .docs-search-result:hover { background: var(--surface-sunken); }
6916 - .docs-search-result-title { font-family: var(--font-body); }
6903 + .docs-search-result-title { font-family: var(--font-sans); }
6917 6904 .docs-search-result-section { font-family: var(--font-mono); font-size: var(--text-fine); opacity: 0.5; }.alert .alert-title {
6918 6905 font-family: var(--font-mono);
6919 6906 font-size: var(--text-note);
@@ -8217,7 +8204,7 @@
8217 8204 padding: var(--gap-peer);
8218 8205 border: 1px solid var(--border);
8219 8206 background: var(--surface-overlay);
8220 - font-family: var(--font-body);
8207 + font-family: var(--font-sans);
8221 8208 resize: vertical;
8222 8209 }
8223 8210
@@ -9558,7 +9545,7 @@
9558 9545
9559 9546 .item-page .license-text {
9560 9547 white-space: pre-wrap;
9561 - font-family: var(--font-body);
9548 + font-family: var(--font-sans);
9562 9549 font-size: var(--text-note);
9563 9550 margin-top: var(--gap-section);
9564 9551 padding: var(--gap-section);
@@ -9885,7 +9872,7 @@
9885 9872 }
9886 9873 .insertion-list-table {
9887 9874 width: 100%;
9888 - font-family: var(--font-body);
9875 + font-family: var(--font-sans);
9889 9876 }
9890 9877 .insertion-list-table th {
9891 9878 text-align: left;
@@ -9963,7 +9950,7 @@
9963 9950 }
9964 9951 .placement-list-table {
9965 9952 width: 100%;
9966 - font-family: var(--font-body);
9953 + font-family: var(--font-sans);
9967 9954 margin-bottom: var(--gap-section);
9968 9955 }
9969 9956 .placement-list-table th {
@@ -10977,7 +10964,7 @@
10977 10964 text-align: left;
10978 10965 background: none;
10979 10966 border: none;
10980 - font-family: var(--font-body);
10967 + font-family: var(--font-sans);
10981 10968 font-size: var(--text-note);
10982 10969 color: var(--content);
10983 10970 cursor: pointer;
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file