Web Fonts
Font loading strategy — performance vs. FOUT/FOIT, variable fonts, subsetting, system font stacks, and font-display options
When to Use
- Adding custom fonts to a web project for brand differentiation
- Optimizing font loading to reduce Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS)
- Deciding between self-hosting fonts and using a CDN (Google Fonts, Adobe Fonts)
- Evaluating whether to adopt variable fonts for an existing design system
- Building a system font stack as a zero-cost baseline or permanent solution
- Debugging invisible text (FOIT) or text reflow (FOUT) during page load
Instructions
Choose a font-display strategy. The font-display CSS descriptor controls what happens while a web font is loading. This is the single most impactful decision for font-related performance:
| Value |
Behavior |
Block Period |
Swap Period |
Best For |
swap |
Shows fallback immediately, swaps when loaded |
~100ms |
Infinite |
Body text — user sees content instantly |
optional |
Shows fallback; may never swap if load is slow |
~100ms |
None |
Performance-critical sites — no layout shift |
fallback |
Brief invisible, then fallback, limited swap window |
~100ms |
~3s |
Balance between swap and optional |
block |
Invisible text for up to 3 seconds |
~3s |
Infinite |
Icon fonts only (never for text) |
auto |
Browser decides (usually block) |
Browser-dependent |
Browser-dependent |
Never use — behavior is unpredictable |
Decision procedure:
- For body text: use
swap (guarantees content is always visible)
- For headings on performance-critical pages: use
optional (prevents layout shift; font loads from cache on subsequent visits)
- For icon fonts: use
block (invisible squares are better than showing raw ligature text)
- Never use
auto or omit font-display — the browser default is effectively block, creating 3 seconds of invisible text
Understand FOUT and FOIT. These are the two failure modes of web font loading:
- FOIT (Flash of Invisible Text): text is invisible until the web font loads. Caused by
font-display: block or browser default. Users see a blank page or missing content for up to 3 seconds. This is the worse failure mode — it hides content.
- FOUT (Flash of Unstyled Text): text appears in the fallback font, then swaps to the web font when loaded. Caused by
font-display: swap. Users see a brief reflow. This is the better failure mode — content is always visible.
- Modern best practice: accept FOUT (use
swap) and minimize its visual impact by choosing a fallback font with similar metrics to your web font.
Use WOFF2 exclusively for modern browsers. Font file formats in order of preference:
- WOFF2: compressed (Brotli), smallest file size, supported by all modern browsers (97%+ global support). Use this.
- WOFF: compressed (zlib), ~20% larger than WOFF2. Only needed for IE11 (effectively dead).
- TTF/OTF: uncompressed, 2-3x larger than WOFF2. Never serve these to browsers.
- EOT: Internet Explorer only. Dead format.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
Subset fonts aggressively. Most projects use only Latin characters. Loading a full Unicode font wastes bandwidth:
- Inter full: ~300KB (all Unicode blocks)
- Inter Latin-only subset: ~20KB (Latin, Latin Extended)
- Inter Latin Basic: ~12KB (ASCII + common accented characters)
- Use
unicode-range in @font-face to load character sets on demand:
/* Latin characters only */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
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+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
font-display: swap;
}
Tools: glyphhanger (npm) analyzes which characters your site actually uses. pyftsubset (Python fonttools) creates subsets from any font file.
Evaluate variable fonts for multi-weight systems. A variable font contains all weights (and potentially widths, optical sizes) in a single file. The tradeoff:
- Single static weight (e.g., Inter Regular 400): ~20KB WOFF2
- Two static weights (400 + 700): ~40KB WOFF2
- Variable font (100-900 weight axis): ~95KB WOFF2
- Breakeven point: if you use 3+ weights from the same family, the variable font is smaller than individual static files (~60KB for 3 static weights vs ~95KB for one variable file)
- Bonus: variable fonts enable intermediate weights (350, 550) for fine-tuned hierarchy without additional file requests
- Decision rule: Using 1-2 weights? Static files. Using 3+ weights or need intermediate values? Variable font.
Preload critical fonts. Add <link rel="preload"> for fonts that appear above the fold:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
- Preload only 1-2 font files (the primary text font and possibly one heading font)
- The
crossorigin attribute is required even for same-origin fonts (browser specification requirement)
- Excessive preloading (3+ fonts) competes with other critical resources and can slow LCP
Build a system font stack as baseline or permanent solution. System fonts load in 0ms — they are already on the user's device:
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif;
Platform resolution:
- macOS/iOS: SF Pro (via
-apple-system)
- Windows: Segoe UI
- Android: Roboto
- Linux: Ubuntu or Cantarell (depending on distribution)
System font stacks are not a compromise — GitHub uses system fonts for their entire application UI. The only drawback is loss of brand-specific typography.
Details
Self-Hosting vs. CDN
Self-hosting (recommended for most projects):
- Full control over caching headers, subsetting, and file optimization
- No third-party DNS lookup or connection overhead
- No privacy concerns (Google Fonts CDN was ruled a GDPR violation in some EU jurisdictions)
- Can be served from the same domain, eliminating cross-origin overhead
Google Fonts CDN:
- Easy setup (
<link href="https://fonts.googleapis.com/css2?...">)
- Automatic format negotiation and subsetting
- Previously had a cache-sharing advantage (browser cached Google Fonts across sites) — this was eliminated when browsers partitioned third-party caches (Chrome 86+, 2020)
- Current recommendation: download from Google Fonts and self-host. The CDN's cache advantage no longer exists, and self-hosting is faster, more private, and more controllable.
Next.js Font Optimization
Next.js provides automatic font optimization via next/font:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
This automatically:
- Self-hosts the font (no Google Fonts CDN request)
- Subsets to the specified character ranges
- Generates CSS
@font-face with font-display: swap
- Creates a CSS custom property for use in Tailwind or CSS
- Adjusts fallback font metrics to minimize CLS during swap
- Inlines the CSS to avoid render-blocking requests
Font File Size Budget
A practical budget for font loading:
| Category |
Budget |
Example |
| Total font payload |
< 100KB |
1 variable font (80KB) + 1 monospace (20KB) |
| Per-font file |
< 50KB |
Single static weight: ~20KB; Variable: ~80-100KB |
| Critical (above fold) |
< 50KB |
Preloaded primary font only |
| Fonts per page |
1-3 files |
Primary text + optional heading + optional mono |
If your font payload exceeds 200KB, you are loading too many families, weights, or character sets. Audit with Chrome DevTools Network panel filtered to "Font."
Matching Fallback Font Metrics
To minimize FOUT layout shift, configure the fallback font to match the web font's metrics:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
ascent-override: 90.49%;
descent-override: 22.56%;
line-gap-override: 0%;
size-adjust: 107.64%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
The size-adjust, ascent-override, descent-override, and line-gap-override descriptors align the fallback font's metrics to the web font, reducing visual shift when the swap occurs. Tools like fontaine (npm) and next/font calculate these values automatically.
Anti-Patterns
Loading 6+ font files on initial page load. Each font file is a network request that blocks text rendering. Loading Regular, Italic, Bold, Bold Italic, Light, and Medium variants separately produces 6 requests and 120-180KB of font data. Fix: use a single variable font file, or limit to 2 static weights maximum.
No fallback font specified. A font-family: 'CustomBrandFont' declaration with no fallback produces invisible text if the font fails to load, or defaults to the browser's serif (Times New Roman on most systems) which likely has completely different proportions. Always provide a complete fallback stack.
Using font-display: block for body text. This hides all body text for up to 3 seconds while the font loads. On slow connections (3G mobile), users see a blank content area. The font may never load if the connection drops. Fix: use font-display: swap for all text content.
Loading full character sets for Latin-only content. A full Unicode build of Inter includes Greek, Cyrillic, Vietnamese, and other scripts. If your content is English-only, 90% of the font file is unused data. Fix: subset to Latin Basic (U+0000-00FF) plus any accent characters your content uses.
Google Fonts CDN without privacy audit. Google Fonts transmits the user's IP address to Google servers. In the EU, this has been ruled a GDPR violation (Munich Regional Court, January 2022). Even outside the EU, the unnecessary data transmission is a liability. Fix: download fonts and self-host them.
Real-World Examples
GitHub's System Font Strategy
GitHub uses system fonts exclusively for the application UI:
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji';
Result: zero font-loading overhead, zero FOUT/FOIT, instant text rendering. Font payload: 0KB. GitHub reserves custom fonts (Mona Sans, Hubot Sans) only for marketing pages where brand expression justifies the performance cost.
Vercel's Geist Font Implementation
Vercel self-hosts Geist as a variable font via next/font:
- Single WOFF2 variable file (~80KB) covering weights 100-900
- Subset to Latin characters
- Preloaded with
<link rel="preload">
font-display: swap for immediate text visibility
- Geist Mono loaded separately (~35KB) only on pages with code blocks
- Total font budget: 80-115KB depending on page type
Stripe's Font Loading
Stripe loads sohne-var (variable) self-hosted:
- Single variable font file covering weights 300-400 (the only weights they use)
- Custom subset: Latin + select currency symbols
- Preloaded on all pages
- Source Code Pro loaded only on API documentation pages
font-display: swap with carefully tuned fallback metrics to minimize CLS
Source
- web.dev — "Best practices for fonts" (Google Chrome team)
- MDN Web Docs —
font-display descriptor, @font-face at-rule
- Zach Leatherman — "A Comprehensive Guide to Font Loading Strategies"
- Chrome DevTools — Network panel font analysis
- WCAG 2.1 SC 1.4.12 — Text Spacing (font rendering implications)
Process
- Evaluate — Audit the current font loading strategy. Measure total font payload, count font file requests, check
font-display values, and identify whether subsetting is applied. Run Lighthouse to check CLS from font swap.
- Apply — Implement the optimal loading strategy: WOFF2 format, aggressive subsetting,
font-display: swap for text, preload critical fonts, self-host rather than CDN. Consider variable fonts if using 3+ weights.
- Verify — Confirm total font payload is under 100KB, no FOIT occurs on throttled connections (Chrome DevTools slow 3G), CLS from font swap is under 0.01, and fallback fonts have matched metrics.
Harness Integration
This is a knowledge skill. When activated, it provides font loading performance expertise to guide @font-face declarations, preload hints, and font file optimization. Use these principles when configuring next/font, adding custom fonts to any web framework, or auditing Core Web Vitals related to font loading. Cross-reference with design-typography-fundamentals for font selection and design-responsive-type for viewport-adaptive type.
Success Criteria
- Total font payload is under 100KB for a typical page load
- All text fonts use
font-display: swap or font-display: optional
- Fonts are served as WOFF2 only (no TTF, OTF, or WOFF fallbacks for modern browsers)
- Fonts are subsetted to only the character ranges the content requires
- No more than 3 font files are loaded on initial page render
- Fallback fonts have metric overrides to minimize CLS during swap
- Fonts are self-hosted (no third-party CDN for production)
1---2name: design-web-fonts3description: Web Fonts4---5# Web Fonts67> Font loading strategy — performance vs. FOUT/FOIT, variable fonts, subsetting, system font stacks, and font-display options89## When to Use1011- Adding custom fonts to a web project for brand differentiation12- Optimizing font loading to reduce Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS)13- Deciding between self-hosting fonts and using a CDN (Google Fonts, Adobe Fonts)14- Evaluating whether to adopt variable fonts for an existing design system15- Building a system font stack as a zero-cost baseline or permanent solution16- Debugging invisible text (FOIT) or text reflow (FOUT) during page load1718## Instructions19201. **Choose a font-display strategy.** The `font-display` CSS descriptor controls what happens while a web font is loading. This is the single most impactful decision for font-related performance:2122 | Value | Behavior | Block Period | Swap Period | Best For |23 | ---------- | --------------------------------------------------- | ----------------- | ----------------- | -------------------------------------------- |24 | `swap` | Shows fallback immediately, swaps when loaded | ~100ms | Infinite | Body text — user sees content instantly |25 | `optional` | Shows fallback; may never swap if load is slow | ~100ms | None | Performance-critical sites — no layout shift |26 | `fallback` | Brief invisible, then fallback, limited swap window | ~100ms | ~3s | Balance between swap and optional |27 | `block` | Invisible text for up to 3 seconds | ~3s | Infinite | Icon fonts only (never for text) |28 | `auto` | Browser decides (usually `block`) | Browser-dependent | Browser-dependent | Never use — behavior is unpredictable |2930 **Decision procedure:**31 - For body text: use `swap` (guarantees content is always visible)32 - For headings on performance-critical pages: use `optional` (prevents layout shift; font loads from cache on subsequent visits)33 - For icon fonts: use `block` (invisible squares are better than showing raw ligature text)34 - Never use `auto` or omit `font-display` — the browser default is effectively `block`, creating 3 seconds of invisible text35362. **Understand FOUT and FOIT.** These are the two failure modes of web font loading:37 - **FOIT (Flash of Invisible Text)**: text is invisible until the web font loads. Caused by `font-display: block` or browser default. Users see a blank page or missing content for up to 3 seconds. This is the worse failure mode — it hides content.38 - **FOUT (Flash of Unstyled Text)**: text appears in the fallback font, then swaps to the web font when loaded. Caused by `font-display: swap`. Users see a brief reflow. This is the better failure mode — content is always visible.39 - **Modern best practice**: accept FOUT (use `swap`) and minimize its visual impact by choosing a fallback font with similar metrics to your web font.40413. **Use WOFF2 exclusively for modern browsers.** Font file formats in order of preference:42 - **WOFF2**: compressed (Brotli), smallest file size, supported by all modern browsers (97%+ global support). Use this.43 - **WOFF**: compressed (zlib), ~20% larger than WOFF2. Only needed for IE11 (effectively dead).44 - **TTF/OTF**: uncompressed, 2-3x larger than WOFF2. Never serve these to browsers.45 - **EOT**: Internet Explorer only. Dead format.4647 ```css48 @font-face {49 font-family: 'Inter';50 src: url('/fonts/inter-var.woff2') format('woff2');51 font-weight: 100 900;52 font-display: swap;53 }54 ```55564. **Subset fonts aggressively.** Most projects use only Latin characters. Loading a full Unicode font wastes bandwidth:57 - **Inter full**: ~300KB (all Unicode blocks)58 - **Inter Latin-only subset**: ~20KB (Latin, Latin Extended)59 - **Inter Latin Basic**: ~12KB (ASCII + common accented characters)60 - Use `unicode-range` in `@font-face` to load character sets on demand:6162 ```css63 /* Latin characters only */64 @font-face {65 font-family: 'Inter';66 src: url('/fonts/inter-latin.woff2') format('woff2');67 unicode-range:68 U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308,69 U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;70 font-display: swap;71 }72 ```7374 Tools: `glyphhanger` (npm) analyzes which characters your site actually uses. `pyftsubset` (Python fonttools) creates subsets from any font file.75765. **Evaluate variable fonts for multi-weight systems.** A variable font contains all weights (and potentially widths, optical sizes) in a single file. The tradeoff:77 - **Single static weight** (e.g., Inter Regular 400): ~20KB WOFF278 - **Two static weights** (400 + 700): ~40KB WOFF279 - **Variable font** (100-900 weight axis): ~95KB WOFF280 - **Breakeven point**: if you use 3+ weights from the same family, the variable font is smaller than individual static files (~60KB for 3 static weights vs ~95KB for one variable file)81 - **Bonus**: variable fonts enable intermediate weights (350, 550) for fine-tuned hierarchy without additional file requests82 - **Decision rule**: Using 1-2 weights? Static files. Using 3+ weights or need intermediate values? Variable font.83846. **Preload critical fonts.** Add `<link rel="preload">` for fonts that appear above the fold:8586 ```html87 <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />88 ```8990 - Preload only 1-2 font files (the primary text font and possibly one heading font)91 - The `crossorigin` attribute is required even for same-origin fonts (browser specification requirement)92 - Excessive preloading (3+ fonts) competes with other critical resources and can slow LCP93947. **Build a system font stack as baseline or permanent solution.** System fonts load in 0ms — they are already on the user's device:9596 ```css97 font-family:98 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,99 'Helvetica Neue', sans-serif;100 ```101102 **Platform resolution:**103 - macOS/iOS: SF Pro (via `-apple-system`)104 - Windows: Segoe UI105 - Android: Roboto106 - Linux: Ubuntu or Cantarell (depending on distribution)107108 System font stacks are not a compromise — GitHub uses system fonts for their entire application UI. The only drawback is loss of brand-specific typography.109110## Details111112### Self-Hosting vs. CDN113114**Self-hosting** (recommended for most projects):115116- Full control over caching headers, subsetting, and file optimization117- No third-party DNS lookup or connection overhead118- No privacy concerns (Google Fonts CDN was ruled a GDPR violation in some EU jurisdictions)119- Can be served from the same domain, eliminating cross-origin overhead120121**Google Fonts CDN**:122123- Easy setup (`<link href="https://fonts.googleapis.com/css2?...">`)124- Automatic format negotiation and subsetting125- Previously had a cache-sharing advantage (browser cached Google Fonts across sites) — this was eliminated when browsers partitioned third-party caches (Chrome 86+, 2020)126- **Current recommendation**: download from Google Fonts and self-host. The CDN's cache advantage no longer exists, and self-hosting is faster, more private, and more controllable.127128### Next.js Font Optimization129130Next.js provides automatic font optimization via `next/font`:131132```typescript133import { Inter } from 'next/font/google';134135const inter = Inter({136 subsets: ['latin'],137 display: 'swap',138 variable: '--font-inter',139});140```141142This automatically:143144- Self-hosts the font (no Google Fonts CDN request)145- Subsets to the specified character ranges146- Generates CSS `@font-face` with `font-display: swap`147- Creates a CSS custom property for use in Tailwind or CSS148- Adjusts fallback font metrics to minimize CLS during swap149- Inlines the CSS to avoid render-blocking requests150151### Font File Size Budget152153A practical budget for font loading:154155| Category | Budget | Example |156| --------------------- | --------- | ------------------------------------------------ |157| Total font payload | < 100KB | 1 variable font (80KB) + 1 monospace (20KB) |158| Per-font file | < 50KB | Single static weight: ~20KB; Variable: ~80-100KB |159| Critical (above fold) | < 50KB | Preloaded primary font only |160| Fonts per page | 1-3 files | Primary text + optional heading + optional mono |161162If your font payload exceeds 200KB, you are loading too many families, weights, or character sets. Audit with Chrome DevTools Network panel filtered to "Font."163164### Matching Fallback Font Metrics165166To minimize FOUT layout shift, configure the fallback font to match the web font's metrics:167168```css169@font-face {170 font-family: 'Inter Fallback';171 src: local('Arial');172 ascent-override: 90.49%;173 descent-override: 22.56%;174 line-gap-override: 0%;175 size-adjust: 107.64%;176}177178body {179 font-family: 'Inter', 'Inter Fallback', sans-serif;180}181```182183The `size-adjust`, `ascent-override`, `descent-override`, and `line-gap-override` descriptors align the fallback font's metrics to the web font, reducing visual shift when the swap occurs. Tools like `fontaine` (npm) and `next/font` calculate these values automatically.184185### Anti-Patterns1861871. **Loading 6+ font files on initial page load.** Each font file is a network request that blocks text rendering. Loading Regular, Italic, Bold, Bold Italic, Light, and Medium variants separately produces 6 requests and 120-180KB of font data. Fix: use a single variable font file, or limit to 2 static weights maximum.1881892. **No fallback font specified.** A `font-family: 'CustomBrandFont'` declaration with no fallback produces invisible text if the font fails to load, or defaults to the browser's serif (Times New Roman on most systems) which likely has completely different proportions. Always provide a complete fallback stack.1901913. **Using font-display: block for body text.** This hides all body text for up to 3 seconds while the font loads. On slow connections (3G mobile), users see a blank content area. The font may never load if the connection drops. Fix: use `font-display: swap` for all text content.1921934. **Loading full character sets for Latin-only content.** A full Unicode build of Inter includes Greek, Cyrillic, Vietnamese, and other scripts. If your content is English-only, 90% of the font file is unused data. Fix: subset to Latin Basic (U+0000-00FF) plus any accent characters your content uses.1941955. **Google Fonts CDN without privacy audit.** Google Fonts transmits the user's IP address to Google servers. In the EU, this has been ruled a GDPR violation (Munich Regional Court, January 2022). Even outside the EU, the unnecessary data transmission is a liability. Fix: download fonts and self-host them.196197### Real-World Examples198199**GitHub's System Font Strategy**200GitHub uses system fonts exclusively for the application UI:201202```css203font-family:204 -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif,205 'Apple Color Emoji', 'Segoe UI Emoji';206```207208Result: zero font-loading overhead, zero FOUT/FOIT, instant text rendering. Font payload: 0KB. GitHub reserves custom fonts (Mona Sans, Hubot Sans) only for marketing pages where brand expression justifies the performance cost.209210**Vercel's Geist Font Implementation**211Vercel self-hosts Geist as a variable font via `next/font`:212213- Single WOFF2 variable file (~80KB) covering weights 100-900214- Subset to Latin characters215- Preloaded with `<link rel="preload">`216- `font-display: swap` for immediate text visibility217- Geist Mono loaded separately (~35KB) only on pages with code blocks218- Total font budget: 80-115KB depending on page type219220**Stripe's Font Loading**221Stripe loads sohne-var (variable) self-hosted:222223- Single variable font file covering weights 300-400 (the only weights they use)224- Custom subset: Latin + select currency symbols225- Preloaded on all pages226- Source Code Pro loaded only on API documentation pages227- `font-display: swap` with carefully tuned fallback metrics to minimize CLS228229## Source230231- web.dev — "Best practices for fonts" (Google Chrome team)232- MDN Web Docs — `font-display` descriptor, `@font-face` at-rule233- Zach Leatherman — "A Comprehensive Guide to Font Loading Strategies"234- Chrome DevTools — Network panel font analysis235- WCAG 2.1 SC 1.4.12 — Text Spacing (font rendering implications)236237## Process2382391. **Evaluate** — Audit the current font loading strategy. Measure total font payload, count font file requests, check `font-display` values, and identify whether subsetting is applied. Run Lighthouse to check CLS from font swap.2402. **Apply** — Implement the optimal loading strategy: WOFF2 format, aggressive subsetting, `font-display: swap` for text, preload critical fonts, self-host rather than CDN. Consider variable fonts if using 3+ weights.2413. **Verify** — Confirm total font payload is under 100KB, no FOIT occurs on throttled connections (Chrome DevTools slow 3G), CLS from font swap is under 0.01, and fallback fonts have matched metrics.242243## Harness Integration244245This is a knowledge skill. When activated, it provides font loading performance expertise to guide `@font-face` declarations, preload hints, and font file optimization. Use these principles when configuring `next/font`, adding custom fonts to any web framework, or auditing Core Web Vitals related to font loading. Cross-reference with `design-typography-fundamentals` for font selection and `design-responsive-type` for viewport-adaptive type.246247## Success Criteria248249- Total font payload is under 100KB for a typical page load250- All text fonts use `font-display: swap` or `font-display: optional`251- Fonts are served as WOFF2 only (no TTF, OTF, or WOFF fallbacks for modern browsers)252- Fonts are subsetted to only the character ranges the content requires253- No more than 3 font files are loaded on initial page render254- Fallback fonts have metric overrides to minimize CLS during swap255- Fonts are self-hosted (no third-party CDN for production)