diff --git a/public/fonts/fraunces-OFL.txt b/public/fonts/v1/fraunces-OFL.txt similarity index 100% rename from public/fonts/fraunces-OFL.txt rename to public/fonts/v1/fraunces-OFL.txt diff --git a/public/fonts/fraunces-latin-ext-wght-normal.woff2 b/public/fonts/v1/fraunces-latin-ext-wght-normal.woff2 similarity index 100% rename from public/fonts/fraunces-latin-ext-wght-normal.woff2 rename to public/fonts/v1/fraunces-latin-ext-wght-normal.woff2 diff --git a/public/fonts/fraunces-latin-wght-normal.woff2 b/public/fonts/v1/fraunces-latin-wght-normal.woff2 similarity index 100% rename from public/fonts/fraunces-latin-wght-normal.woff2 rename to public/fonts/v1/fraunces-latin-wght-normal.woff2 diff --git a/public/fonts/inter-OFL.txt b/public/fonts/v1/inter-OFL.txt similarity index 100% rename from public/fonts/inter-OFL.txt rename to public/fonts/v1/inter-OFL.txt diff --git a/public/fonts/inter-latin-ext-wght-normal.woff2 b/public/fonts/v1/inter-latin-ext-wght-normal.woff2 similarity index 100% rename from public/fonts/inter-latin-ext-wght-normal.woff2 rename to public/fonts/v1/inter-latin-ext-wght-normal.woff2 diff --git a/public/fonts/inter-latin-wght-normal.woff2 b/public/fonts/v1/inter-latin-wght-normal.woff2 similarity index 100% rename from public/fonts/inter-latin-wght-normal.woff2 rename to public/fonts/v1/inter-latin-wght-normal.woff2 diff --git a/public/fonts/jetbrains-mono-OFL.txt b/public/fonts/v1/jetbrains-mono-OFL.txt similarity index 100% rename from public/fonts/jetbrains-mono-OFL.txt rename to public/fonts/v1/jetbrains-mono-OFL.txt diff --git a/public/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 b/public/fonts/v1/jetbrains-mono-latin-ext-wght-normal.woff2 similarity index 100% rename from public/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 rename to public/fonts/v1/jetbrains-mono-latin-ext-wght-normal.woff2 diff --git a/public/fonts/jetbrains-mono-latin-wght-normal.woff2 b/public/fonts/v1/jetbrains-mono-latin-wght-normal.woff2 similarity index 100% rename from public/fonts/jetbrains-mono-latin-wght-normal.woff2 rename to public/fonts/v1/jetbrains-mono-latin-wght-normal.woff2 diff --git a/public/fonts/source-sans-3-OFL.txt b/public/fonts/v1/source-sans-3-OFL.txt similarity index 100% rename from public/fonts/source-sans-3-OFL.txt rename to public/fonts/v1/source-sans-3-OFL.txt diff --git a/public/fonts/source-sans-3-latin-ext-wght-normal.woff2 b/public/fonts/v1/source-sans-3-latin-ext-wght-normal.woff2 similarity index 100% rename from public/fonts/source-sans-3-latin-ext-wght-normal.woff2 rename to public/fonts/v1/source-sans-3-latin-ext-wght-normal.woff2 diff --git a/public/fonts/source-sans-3-latin-wght-normal.woff2 b/public/fonts/v1/source-sans-3-latin-wght-normal.woff2 similarity index 100% rename from public/fonts/source-sans-3-latin-wght-normal.woff2 rename to public/fonts/v1/source-sans-3-latin-wght-normal.woff2 diff --git a/public/fonts/source-serif-4-OFL.txt b/public/fonts/v1/source-serif-4-OFL.txt similarity index 100% rename from public/fonts/source-serif-4-OFL.txt rename to public/fonts/v1/source-serif-4-OFL.txt diff --git a/public/fonts/source-serif-4-latin-ext-wght-normal.woff2 b/public/fonts/v1/source-serif-4-latin-ext-wght-normal.woff2 similarity index 100% rename from public/fonts/source-serif-4-latin-ext-wght-normal.woff2 rename to public/fonts/v1/source-serif-4-latin-ext-wght-normal.woff2 diff --git a/public/fonts/source-serif-4-latin-wght-normal.woff2 b/public/fonts/v1/source-serif-4-latin-wght-normal.woff2 similarity index 100% rename from public/fonts/source-serif-4-latin-wght-normal.woff2 rename to public/fonts/v1/source-serif-4-latin-wght-normal.woff2 diff --git a/public/fonts/space-grotesk-OFL.txt b/public/fonts/v1/space-grotesk-OFL.txt similarity index 100% rename from public/fonts/space-grotesk-OFL.txt rename to public/fonts/v1/space-grotesk-OFL.txt diff --git a/public/fonts/space-grotesk-latin-ext-wght-normal.woff2 b/public/fonts/v1/space-grotesk-latin-ext-wght-normal.woff2 similarity index 100% rename from public/fonts/space-grotesk-latin-ext-wght-normal.woff2 rename to public/fonts/v1/space-grotesk-latin-ext-wght-normal.woff2 diff --git a/public/fonts/space-grotesk-latin-wght-normal.woff2 b/public/fonts/v1/space-grotesk-latin-wght-normal.woff2 similarity index 100% rename from public/fonts/space-grotesk-latin-wght-normal.woff2 rename to public/fonts/v1/space-grotesk-latin-wght-normal.woff2 diff --git a/scripts/copy-studio.mjs b/scripts/copy-studio.mjs index ddebc14..57034f5 100644 --- a/scripts/copy-studio.mjs +++ b/scripts/copy-studio.mjs @@ -9,7 +9,10 @@ const from = path.join(cli, "dist/assets/studio"); const pub = new URL("../public/", import.meta.url).pathname; await rm(path.join(pub, "assets"), { recursive: true, force: true }); await cp(from, pub, { recursive: true, filter: (src) => !src.endsWith(".map") }); -// Studio's own headers (CSP etc.) for its files; the site's pages set their own. -const headers = await readFile(path.join(from, "_headers"), "utf8").catch(() => ""); -if (headers) await writeFile(path.join(pub, "_headers"), headers); +// Studio's own headers (CSP etc.) for its files; the site's pages set their own. The theme's +// fonts never change under a path (a new set gets /fonts/v2/): cached for a year, so a reload +// draws the text in its font from the first paint instead of swapping it in later. +const studio = await readFile(path.join(from, "_headers"), "utf8").catch(() => ""); +const fonts = "/fonts/v1/*\n Cache-Control: public, max-age=31536000, immutable\n"; +await writeFile(path.join(pub, "_headers"), `${studio.trimEnd()}\n\n${fonts}`.trimStart()); console.log(`Studio copied from ${from}`); diff --git a/src/fonts.ts b/src/fonts.ts index a75d746..a48cca9 100644 --- a/src/fonts.ts +++ b/src/fonts.ts @@ -4,12 +4,18 @@ * (Turkish and other accented letters) load on demand through fonts.css's unicode ranges. */ export const PAIRING_PRELOADS: Readonly> = { - editorial: ["/fonts/fraunces-latin-wght-normal.woff2", "/fonts/inter-latin-wght-normal.woff2"], - modern: ["/fonts/space-grotesk-latin-wght-normal.woff2", "/fonts/inter-latin-wght-normal.woff2"], - humanist: ["/fonts/source-sans-3-latin-wght-normal.woff2"], + editorial: [ + "/fonts/v1/fraunces-latin-wght-normal.woff2", + "/fonts/v1/inter-latin-wght-normal.woff2", + ], + modern: [ + "/fonts/v1/space-grotesk-latin-wght-normal.woff2", + "/fonts/v1/inter-latin-wght-normal.woff2", + ], + humanist: ["/fonts/v1/source-sans-3-latin-wght-normal.woff2"], technical: [ - "/fonts/jetbrains-mono-latin-wght-normal.woff2", - "/fonts/inter-latin-wght-normal.woff2", + "/fonts/v1/jetbrains-mono-latin-wght-normal.woff2", + "/fonts/v1/inter-latin-wght-normal.woff2", ], - classic: ["/fonts/source-serif-4-latin-wght-normal.woff2"], + classic: ["/fonts/v1/source-serif-4-latin-wght-normal.woff2"], }; diff --git a/src/styles/features/tokens.css b/src/styles/features/tokens.css index 5eb4c95..7d65a1f 100644 --- a/src/styles/features/tokens.css +++ b/src/styles/features/tokens.css @@ -55,29 +55,41 @@ font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace; } /* - * Font pairings (RFC-0023 §2): the families come from fonts.css and load only when used. They win + * Font pairings (RFC-0023 §2): the families come from fonts.css and load only when used; each is + * followed by its metric-matched fallback (font-fallbacks.css), so the swap does not move the + * text. They win * over the older typeface setting; `system` keeps it. */ [data-ch-type-pairing="editorial"] { - --ch-font-display: "Fraunces Variable", ui-serif, Georgia, serif; - --ch-font-text: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --ch-font-display: "Fraunces Variable", "Fraunces Variable Fallback", ui-serif, Georgia, serif; + --ch-font-text: "Inter Variable", "Inter Variable Fallback", ui-sans-serif, system-ui, sans-serif; } [data-ch-type-pairing="modern"] { - --ch-font-display: "Space Grotesk Variable", ui-sans-serif, system-ui, sans-serif; - --ch-font-text: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --ch-font-display: + "Space Grotesk Variable", "Space Grotesk Variable Fallback", ui-sans-serif, system-ui, + sans-serif; + --ch-font-text: "Inter Variable", "Inter Variable Fallback", ui-sans-serif, system-ui, sans-serif; } [data-ch-type-pairing="humanist"] { - --ch-font-display: "Source Sans 3 Variable", ui-sans-serif, system-ui, sans-serif; - --ch-font-text: "Source Sans 3 Variable", ui-sans-serif, system-ui, sans-serif; + --ch-font-display: + "Source Sans 3 Variable", "Source Sans 3 Variable Fallback", ui-sans-serif, system-ui, + sans-serif; + --ch-font-text: + "Source Sans 3 Variable", "Source Sans 3 Variable Fallback", ui-sans-serif, system-ui, + sans-serif; } [data-ch-type-pairing="technical"] { - --ch-font-display: "JetBrains Mono Variable", ui-monospace, monospace; - --ch-font-text: "Inter Variable", ui-sans-serif, system-ui, sans-serif; - --ch-font-code: "JetBrains Mono Variable", ui-monospace, monospace; + --ch-font-display: + "JetBrains Mono Variable", "JetBrains Mono Variable Fallback", ui-monospace, monospace; + --ch-font-text: "Inter Variable", "Inter Variable Fallback", ui-sans-serif, system-ui, sans-serif; + --ch-font-code: + "JetBrains Mono Variable", "JetBrains Mono Variable Fallback", ui-monospace, monospace; } [data-ch-type-pairing="classic"] { - --ch-font-display: "Source Serif 4 Variable", ui-serif, Georgia, serif; - --ch-font-text: "Source Serif 4 Variable", ui-serif, Georgia, serif; + --ch-font-display: + "Source Serif 4 Variable", "Source Serif 4 Variable Fallback", ui-serif, Georgia, serif; + --ch-font-text: + "Source Serif 4 Variable", "Source Serif 4 Variable Fallback", ui-serif, Georgia, serif; } [data-ch-type-pairing]:not([data-ch-type-pairing="system"]) .ch-body { font-family: var(--ch-font-text); diff --git a/src/styles/font-fallbacks.css b/src/styles/font-fallbacks.css new file mode 100644 index 0000000..495c7e8 --- /dev/null +++ b/src/styles/font-fallbacks.css @@ -0,0 +1,53 @@ +/* + * Generated by scripts/font-fallbacks.ts: do not edit. Metric-matched fallback faces for the + * pairings' web fonts (tokens.css lists each right after its web font), so the swap to the web + * font does not move the text. + */ +@font-face { + font-family: "Fraunces Variable Fallback"; + src: local("Times New Roman"), local("TimesNewRomanPSMT"); + size-adjust: 113.62%; + ascent-override: 86.08%; + descent-override: 22.44%; + line-gap-override: 0.00%; +} +@font-face { + font-family: "Inter Variable Fallback"; + src: local("Arial"), local("ArialMT"); + size-adjust: 107.12%; + ascent-override: 90.44%; + descent-override: 22.52%; + line-gap-override: 0.00%; +} +@font-face { + font-family: "Space Grotesk Variable Fallback"; + src: local("Arial"), local("ArialMT"); + size-adjust: 109.47%; + ascent-override: 89.89%; + descent-override: 26.67%; + line-gap-override: 0.00%; +} +@font-face { + font-family: "Source Sans 3 Variable Fallback"; + src: local("Arial"), local("ArialMT"); + size-adjust: 92.42%; + ascent-override: 110.80%; + descent-override: 43.28%; + line-gap-override: 0.00%; +} +@font-face { + font-family: "Source Serif 4 Variable Fallback"; + src: local("Times New Roman"), local("TimesNewRomanPSMT"); + size-adjust: 117.91%; + ascent-override: 87.87%; + descent-override: 28.41%; + line-gap-override: 0.00%; +} +@font-face { + font-family: "JetBrains Mono Variable Fallback"; + src: local("Courier New"), local("CourierNewPSMT"); + size-adjust: 99.98%; + ascent-override: 102.02%; + descent-override: 30.00%; + line-gap-override: 0.00%; +} diff --git a/src/styles/fonts.css b/src/styles/fonts.css index fea61f8..3317914 100644 --- a/src/styles/fonts.css +++ b/src/styles/fonts.css @@ -8,7 +8,7 @@ font-style: normal; font-display: swap; font-weight: 100 900; - src: url(/fonts/fraunces-latin-ext-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/fraunces-latin-ext-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } @@ -17,7 +17,7 @@ font-style: normal; font-display: swap; font-weight: 100 900; - src: url(/fonts/fraunces-latin-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/fraunces-latin-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @@ -26,7 +26,7 @@ font-style: normal; font-display: swap; font-weight: 100 900; - src: url(/fonts/inter-latin-ext-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/inter-latin-ext-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } @@ -35,7 +35,7 @@ font-style: normal; font-display: swap; font-weight: 100 900; - src: url(/fonts/inter-latin-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/inter-latin-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @@ -44,7 +44,7 @@ font-style: normal; font-display: swap; font-weight: 100 800; - src: url(/fonts/jetbrains-mono-latin-ext-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/jetbrains-mono-latin-ext-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } @@ -53,7 +53,7 @@ font-style: normal; font-display: swap; font-weight: 100 800; - src: url(/fonts/jetbrains-mono-latin-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/jetbrains-mono-latin-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @@ -62,7 +62,7 @@ font-style: normal; font-display: swap; font-weight: 200 900; - src: url(/fonts/source-sans-3-latin-ext-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/source-sans-3-latin-ext-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; } @@ -71,7 +71,7 @@ font-style: normal; font-display: swap; font-weight: 200 900; - src: url(/fonts/source-sans-3-latin-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/source-sans-3-latin-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @@ -80,7 +80,7 @@ font-style: normal; font-display: swap; font-weight: 200 900; - src: url(/fonts/source-serif-4-latin-ext-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/source-serif-4-latin-ext-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; } @@ -89,7 +89,7 @@ font-style: normal; font-display: swap; font-weight: 200 900; - src: url(/fonts/source-serif-4-latin-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/source-serif-4-latin-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @@ -98,7 +98,7 @@ font-style: normal; font-display: swap; font-weight: 300 700; - src: url(/fonts/space-grotesk-latin-ext-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/space-grotesk-latin-ext-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } @@ -107,7 +107,7 @@ font-style: normal; font-display: swap; font-weight: 300 700; - src: url(/fonts/space-grotesk-latin-wght-normal.woff2) format("woff2-variations"); + src: url(/fonts/v1/space-grotesk-latin-wght-normal.woff2) format("woff2-variations"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } diff --git a/src/styles/global.css b/src/styles/global.css index 74e6152..7717714 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,6 +1,7 @@ /* Scan `src/` only: automatic detection may walk up the file system (docs/design/starter-references.md §6). */ @import "tailwindcss" source(".."); @import "./fonts.css"; +@import "./font-fallbacks.css"; /* * One file per feature (docs/engineering/code-structure.md §8), in cascade order. Unlayered on