@hegemonart/get-design-done 1.16.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +12 -4
  2. package/.claude-plugin/plugin.json +22 -4
  3. package/CHANGELOG.md +111 -0
  4. package/README.md +27 -2
  5. package/agents/design-auditor.md +65 -1
  6. package/agents/design-context-builder.md +6 -1
  7. package/agents/design-doc-writer.md +21 -0
  8. package/agents/design-executor.md +22 -4
  9. package/agents/design-pattern-mapper.md +62 -0
  10. package/agents/design-phase-researcher.md +1 -1
  11. package/agents/motion-mapper.md +74 -9
  12. package/agents/token-mapper.md +8 -0
  13. package/package.json +16 -2
  14. package/reference/components/README.md +27 -23
  15. package/reference/components/alert.md +198 -0
  16. package/reference/components/badge.md +202 -0
  17. package/reference/components/breadcrumbs.md +198 -0
  18. package/reference/components/chip.md +209 -0
  19. package/reference/components/command-palette.md +228 -0
  20. package/reference/components/date-picker.md +227 -0
  21. package/reference/components/file-upload.md +219 -0
  22. package/reference/components/list.md +217 -0
  23. package/reference/components/menu.md +212 -0
  24. package/reference/components/navbar.md +211 -0
  25. package/reference/components/pagination.md +205 -0
  26. package/reference/components/progress.md +210 -0
  27. package/reference/components/rich-text-editor.md +226 -0
  28. package/reference/components/sidebar.md +211 -0
  29. package/reference/components/skeleton.md +197 -0
  30. package/reference/components/slider.md +208 -0
  31. package/reference/components/stepper.md +220 -0
  32. package/reference/components/table.md +229 -0
  33. package/reference/components/toast.md +200 -0
  34. package/reference/components/tree.md +225 -0
  35. package/reference/css-grid-layout.md +835 -0
  36. package/reference/data-visualization.md +333 -0
  37. package/reference/external/NOTICE.hyperframes +28 -0
  38. package/reference/form-patterns.md +245 -0
  39. package/reference/image-optimization.md +582 -0
  40. package/reference/information-architecture.md +255 -0
  41. package/reference/motion-advanced.md +754 -0
  42. package/reference/motion-easings.md +381 -0
  43. package/reference/motion-interpolate.md +282 -0
  44. package/reference/motion-spring.md +234 -0
  45. package/reference/motion-transition-taxonomy.md +155 -0
  46. package/reference/motion.md +20 -0
  47. package/reference/onboarding-progressive-disclosure.md +250 -0
  48. package/reference/output-contracts/motion-map.schema.json +135 -0
  49. package/reference/platforms.md +346 -0
  50. package/reference/registry.json +445 -220
  51. package/reference/registry.schema.json +4 -0
  52. package/reference/rtl-cjk-cultural.md +353 -0
  53. package/reference/user-research.md +360 -0
  54. package/reference/variable-fonts-loading.md +532 -0
  55. package/scripts/lib/easings.cjs +280 -0
  56. package/scripts/lib/parse-contract.cjs +220 -0
  57. package/scripts/lib/spring.cjs +160 -0
  58. package/scripts/tests/test-motion-provenance.sh +64 -0
@@ -0,0 +1,532 @@
1
+ <!-- Source: Phase 18 — get-design-done -->
2
+
3
+ # Variable Fonts & Font Loading
4
+
5
+ ## Variable Font Axes
6
+
7
+ ### Registered Axes
8
+
9
+ Registered axes have standardized four-character tags and map to familiar CSS properties:
10
+
11
+ | Axis tag | CSS property mapping | Range (typical) | Description |
12
+ |----------|---------------------|-----------------|-------------|
13
+ | `wght` | `font-weight` | 100–900 | Weight |
14
+ | `ital` | `font-style: italic` | 0–1 | Italic (binary or interpolated) |
15
+ | `opsz` | `font-optical-sizing` / `font-variation-settings` | 8–144 | Optical size |
16
+ | `slnt` | `font-style: oblique Xdeg` | -90–0 | Slant |
17
+ | `GRAD` | `font-variation-settings: 'GRAD'` | -200–150 | Grade (weight without layout shift) |
18
+
19
+ Custom axes use all-uppercase four-character tags (e.g., `XTIL`, `WONK`, `SPAC`). Custom axes have no CSS property shorthand and must use `font-variation-settings`.
20
+
21
+ ### @font-face with Variable Font Ranges
22
+
23
+ Declare variable fonts with ranges so the browser knows the full axis span:
24
+
25
+ ```css
26
+ /* Single variable font file covering full weight range */
27
+ @font-face {
28
+ font-family: 'Inter';
29
+ src:
30
+ url('/fonts/inter-variable.woff2') format('woff2 supports variations'),
31
+ url('/fonts/inter-variable.woff2') format('woff2');
32
+ font-weight: 100 900; /* wght axis range */
33
+ font-style: normal;
34
+ font-display: swap;
35
+ }
36
+
37
+ /* Separate italic variable font */
38
+ @font-face {
39
+ font-family: 'Inter';
40
+ src: url('/fonts/inter-variable-italic.woff2') format('woff2 supports variations');
41
+ font-weight: 100 900;
42
+ font-style: italic;
43
+ font-display: swap;
44
+ }
45
+
46
+ /* Font with opsz axis — declare optical size range */
47
+ @font-face {
48
+ font-family: 'Source Serif';
49
+ src: url('/fonts/source-serif-variable.woff2') format('woff2 supports variations');
50
+ font-weight: 200 900;
51
+ font-style: normal oblique -15deg 0deg; /* slnt axis range */
52
+ font-display: swap;
53
+ }
54
+ ```
55
+
56
+ ### font-variation-settings Usage
57
+
58
+ `font-variation-settings` is a low-level override. Use registered-axis CSS properties first; fall back to `font-variation-settings` for custom axes or unsupported registered axes.
59
+
60
+ ```css
61
+ /* Prefer high-level properties */
62
+ h1 {
63
+ font-weight: 700; /* maps to wght */
64
+ font-style: oblique 5deg; /* maps to slnt */
65
+ font-optical-sizing: auto; /* maps to opsz */
66
+ }
67
+
68
+ /* Use font-variation-settings for custom axes or combined overrides */
69
+ .headline {
70
+ font-variation-settings:
71
+ 'wght' 750,
72
+ 'GRAD' 50, /* custom Grade axis — no CSS property */
73
+ 'opsz' 36;
74
+ }
75
+
76
+ /* Dark mode: GRAD axis adjustment (see section below) */
77
+ @media (prefers-color-scheme: dark) {
78
+ body {
79
+ font-variation-settings: 'GRAD' -50;
80
+ }
81
+ }
82
+ ```
83
+
84
+ **Do:** Set `font-variation-settings` on a parent and let children inherit.
85
+ **Don't:** Override `font-variation-settings` on a child while expecting inherited axes to persist — the property does not merge; it replaces entirely.
86
+
87
+ ```css
88
+ /* WRONG — child loses 'wght' axis set on parent */
89
+ body { font-variation-settings: 'wght' 400, 'GRAD' 0; }
90
+ .bold { font-variation-settings: 'wght' 700; } /* GRAD silently reset to default */
91
+
92
+ /* CORRECT — repeat all axes on child */
93
+ .bold { font-variation-settings: 'wght' 700, 'GRAD' 0; }
94
+
95
+ /* BETTER — use CSS custom properties to manage axes */
96
+ :root {
97
+ --font-wght: 400;
98
+ --font-grad: 0;
99
+ }
100
+ body {
101
+ font-variation-settings: 'wght' var(--font-wght), 'GRAD' var(--font-grad);
102
+ }
103
+ .bold { --font-wght: 700; }
104
+ @media (prefers-color-scheme: dark) {
105
+ :root { --font-grad: -50; }
106
+ }
107
+ ```
108
+
109
+ ### font-optical-sizing
110
+
111
+ `font-optical-sizing: auto` lets the browser use the `opsz` axis automatically based on computed `font-size`. It is enabled by default when the font has an `opsz` axis.
112
+
113
+ ```css
114
+ /* auto (default) — browser picks opsz value from font-size */
115
+ body { font-optical-sizing: auto; }
116
+
117
+ /* none — disable automatic optical sizing */
118
+ .logo-lockup { font-optical-sizing: none; }
119
+
120
+ /* Manual override via font-variation-settings */
121
+ .caption {
122
+ font-optical-sizing: none; /* disable auto so manual value wins */
123
+ font-variation-settings: 'opsz' 12;
124
+ }
125
+ ```
126
+
127
+ ---
128
+
129
+ ## font-display and Loading Behavior
130
+
131
+ ### FOIT, FOUT, FAIT Definitions
132
+
133
+ | Term | Full name | Behavior |
134
+ |------|-----------|----------|
135
+ | FOIT | Flash of Invisible Text | Browser hides text until the web font loads |
136
+ | FOUT | Flash of Unstyled Text | Browser shows fallback font, swaps to web font when ready |
137
+ | FAIT | Flash of Actually Invisible Text | Hybrid: short invisible period then fallback |
138
+
139
+ **Which is worse:** FOIT is worse for perceived performance and accessibility. Users see blank content, which degrades readability and Cumulative Layout Shift (CLS) scores when text suddenly appears. FOUT is preferable because content is readable immediately.
140
+
141
+ ### font-display Values
142
+
143
+ ```css
144
+ @font-face {
145
+ font-family: 'Inter';
146
+ src: url('/fonts/inter-variable.woff2') format('woff2');
147
+ font-weight: 100 900;
148
+ font-display: swap; /* change this per use case */
149
+ }
150
+ ```
151
+
152
+ | Value | Block period | Swap period | Best for |
153
+ |-------|-------------|-------------|----------|
154
+ | `auto` | Browser default (usually same as `block`) | Varies | Avoid — unpredictable |
155
+ | `block` | ~3s invisible | Infinite swap | Icon fonts where letters must match |
156
+ | `swap` | ~0ms invisible | Infinite swap | Body copy, headings — text visible immediately |
157
+ | `fallback` | ~100ms invisible | ~3s swap | Performance-sensitive text; graceful if font is slow |
158
+ | `optional` | ~100ms invisible | 0s (no swap) | Decorative fonts, hero text; skip swap if font not cached |
159
+
160
+ **Decision guide:**
161
+
162
+ - Body text, headings, UI labels → `font-display: swap`
163
+ - Performance-critical above-the-fold text → `font-display: fallback` (limits FOUT to 3s)
164
+ - Decorative / non-critical typefaces → `font-display: optional` (no layout shift at all on slow connections)
165
+ - Icon fonts (glyph mapping critical) → `font-display: block` (accept FOIT for correctness)
166
+
167
+ ```css
168
+ /* Body copy — always visible */
169
+ @font-face {
170
+ font-family: 'Inter';
171
+ src: url('/fonts/inter-variable.woff2') format('woff2');
172
+ font-weight: 100 900;
173
+ font-display: swap;
174
+ }
175
+
176
+ /* Optional decorative typeface */
177
+ @font-face {
178
+ font-family: 'Playfair Display';
179
+ src: url('/fonts/playfair-variable.woff2') format('woff2');
180
+ font-weight: 300 900;
181
+ font-display: optional;
182
+ }
183
+
184
+ /* Icon font — glyphs must match */
185
+ @font-face {
186
+ font-family: 'MyIcons';
187
+ src: url('/fonts/myicons.woff2') format('woff2');
188
+ font-display: block;
189
+ }
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Preload Strategies
195
+
196
+ ### Preload Syntax
197
+
198
+ ```html
199
+ <!-- Preload a WOFF2 variable font — crossorigin is required even same-origin -->
200
+ <link
201
+ rel="preload"
202
+ href="/fonts/inter-variable.woff2"
203
+ as="font"
204
+ type="font/woff2"
205
+ crossorigin
206
+ />
207
+ ```
208
+
209
+ `crossorigin` is mandatory for all font preloads regardless of origin. Without it the browser fetches the font twice.
210
+
211
+ ### Which Subset to Preload
212
+
213
+ Preload only the subset(s) used above the fold. Preloading unused fonts wastes bandwidth and delays critical resources.
214
+
215
+ ```html
216
+ <!-- Preload only the Latin subset for an English-language site -->
217
+ <link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin />
218
+
219
+ <!-- Do NOT preload every unicode-range subset — browser handles lazy loading of others -->
220
+ ```
221
+
222
+ ### How Many Fonts to Preload
223
+
224
+ - **1–2 font files maximum** as a general rule. More preloads compete with images, scripts, and CSS.
225
+ - Preload the **regular weight** (400) of the primary typeface first.
226
+ - Preload a **bold weight** (700) only if it appears above the fold in a critical heading.
227
+ - Never preload fonts with `font-display: optional` — the browser will skip the swap anyway on slow connections.
228
+
229
+ ```html
230
+ <!-- Minimal correct preload for a typical site -->
231
+ <head>
232
+ <!-- 1. Primary body font — preloaded -->
233
+ <link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin />
234
+
235
+ <!-- 2. Stylesheet that declares @font-face — load after preload hint -->
236
+ <link rel="stylesheet" href="/css/fonts.css" />
237
+ </head>
238
+ ```
239
+
240
+ ---
241
+
242
+ ## WOFF2 Subsetting
243
+
244
+ ### unicode-range Descriptor
245
+
246
+ `unicode-range` tells the browser which characters a font file covers. The browser only downloads the file if the page contains a character in that range.
247
+
248
+ ```css
249
+ /* Latin subset */
250
+ @font-face {
251
+ font-family: 'Inter';
252
+ src: url('/fonts/inter-latin.woff2') format('woff2');
253
+ font-weight: 100 900;
254
+ font-display: swap;
255
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
256
+ U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122,
257
+ U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
258
+ }
259
+
260
+ /* Cyrillic subset — only downloaded if Cyrillic characters present */
261
+ @font-face {
262
+ font-family: 'Inter';
263
+ src: url('/fonts/inter-cyrillic.woff2') format('woff2');
264
+ font-weight: 100 900;
265
+ font-display: swap;
266
+ unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
267
+ }
268
+
269
+ /* Greek subset */
270
+ @font-face {
271
+ font-family: 'Inter';
272
+ src: url('/fonts/inter-greek.woff2') format('woff2');
273
+ font-weight: 100 900;
274
+ font-display: swap;
275
+ unicode-range: U+0370-03FF;
276
+ }
277
+ ```
278
+
279
+ ### Subsetting Tools
280
+
281
+ **pyftsubset** (part of fonttools — Python):
282
+
283
+ ```bash
284
+ # Install
285
+ pip install fonttools brotli
286
+
287
+ # Subset to Latin characters and output WOFF2
288
+ pyftsubset inter-variable.ttf \
289
+ --output-file=inter-latin.woff2 \
290
+ --flavor=woff2 \
291
+ --layout-features="*" \
292
+ --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"
293
+
294
+ # Preserve variable font axes
295
+ pyftsubset inter-variable.ttf \
296
+ --output-file=inter-variable-latin.woff2 \
297
+ --flavor=woff2 \
298
+ --layout-features="*" \
299
+ --unicodes="U+0000-00FF" \
300
+ --retain-gids
301
+ ```
302
+
303
+ **glyphhanger** (Node.js — analyzes a live URL and generates subset):
304
+
305
+ ```bash
306
+ # Install
307
+ npm install -g glyphhanger
308
+
309
+ # Analyze a URL and output subset unicodes
310
+ glyphhanger https://example.com --subset=inter-variable.ttf --formats=woff2
311
+
312
+ # Subset from a text string
313
+ glyphhanger --whitelist="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 " \
314
+ --subset=inter-variable.ttf \
315
+ --formats=woff2
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Fallback Metric Overrides and CLS Prevention
321
+
322
+ When `font-display: swap` triggers, the fallback font (Arial, Georgia, etc.) has different metrics than the web font. The browser reflows layout, causing Cumulative Layout Shift (CLS).
323
+
324
+ **Fallback metric override descriptors** (in `@font-face` of a _fallback_ font face) adjust the fallback to match the web font's metrics, eliminating the reflow.
325
+
326
+ ```css
327
+ /* Step 1: Declare the real web font */
328
+ @font-face {
329
+ font-family: 'Inter';
330
+ src: url('/fonts/inter-variable.woff2') format('woff2');
331
+ font-weight: 100 900;
332
+ font-display: swap;
333
+ }
334
+
335
+ /* Step 2: Declare an adjusted fallback with matching metrics */
336
+ @font-face {
337
+ font-family: 'Inter-fallback';
338
+ src: local('Arial');
339
+ size-adjust: 107%; /* scale fallback to match web font cap height */
340
+ ascent-override: 90%; /* match web font ascender */
341
+ descent-override: 22%; /* match web font descender */
342
+ line-gap-override: 0%; /* match web font line gap */
343
+ }
344
+
345
+ /* Step 3: Use both in the font stack */
346
+ body {
347
+ font-family: 'Inter', 'Inter-fallback', Arial, sans-serif;
348
+ }
349
+ ```
350
+
351
+ ### Descriptor Reference
352
+
353
+ | Descriptor | What it adjusts | Value type |
354
+ |------------|----------------|------------|
355
+ | `size-adjust` | Overall em-square scale | `%` (100% = no change) |
356
+ | `ascent-override` | Ascender height above baseline | `%` of em |
357
+ | `descent-override` | Descender depth below baseline | `%` of em |
358
+ | `line-gap-override` | Extra space between lines built into the font | `%` of em |
359
+
360
+ ### Finding Metric Values
361
+
362
+ Use the [Font Style Matcher](https://meowni.ca/font-style-matcher/) or the `fonttools` Python library:
363
+
364
+ ```python
365
+ from fontTools.ttLib import TTFont
366
+
367
+ font = TTFont('inter-variable.ttf')
368
+ head = font['head']
369
+ hhea = font['hhea']
370
+ os2 = font['OS/2']
371
+
372
+ units_per_em = head.unitsPerEm
373
+ ascent = os2.sTypoAscender / units_per_em * 100
374
+ descent = abs(os2.sTypoDescender) / units_per_em * 100
375
+ line_gap = os2.sTypoLineGap / units_per_em * 100
376
+
377
+ print(f"ascent-override: {ascent:.0f}%")
378
+ print(f"descent-override: {descent:.0f}%")
379
+ print(f"line-gap-override: {line_gap:.0f}%")
380
+ ```
381
+
382
+ **Do:** Tune `size-adjust` first — it has the largest visual impact. Then fine-tune ascent/descent.
383
+ **Don't:** Use `ascent-override: 100%` blindly — 100% is the fallback default; only override when you have real metric data.
384
+
385
+ ---
386
+
387
+ ## Variable Fonts in Dark Mode
388
+
389
+ ### GRAD Axis (Grade)
390
+
391
+ The Grade axis (`GRAD`) adjusts apparent weight without changing the advance widths of any glyphs. This means **no layout reflow** when switching between light and dark modes — unlike changing `font-weight`, which can alter character widths.
392
+
393
+ In dark mode, light text on dark backgrounds appears heavier due to irradiation/halation. Lowering `GRAD` compensates for this optical effect.
394
+
395
+ ```css
396
+ :root {
397
+ --font-grad: 0;
398
+ }
399
+
400
+ @media (prefers-color-scheme: dark) {
401
+ :root {
402
+ --font-grad: -50; /* reduce apparent weight in dark mode */
403
+ }
404
+ }
405
+
406
+ body {
407
+ font-variation-settings: 'wght' var(--font-wght, 400), 'GRAD' var(--font-grad);
408
+ }
409
+ ```
410
+
411
+ ```css
412
+ /* Theme toggle via data attribute */
413
+ [data-theme="light"] { --font-grad: 0; }
414
+ [data-theme="dark"] { --font-grad: -50; }
415
+
416
+ body {
417
+ font-variation-settings: 'GRAD' var(--font-grad, 0);
418
+ }
419
+ ```
420
+
421
+ **GRAD vs wght in dark mode:**
422
+
423
+ | Approach | Layout shift | Glyph width change | Correct method |
424
+ |----------|-------------|-------------------|----------------|
425
+ | Change `font-weight` | Yes (possible) | Yes | No |
426
+ | Change `GRAD` axis | No | No | Yes |
427
+
428
+ ### Fonts with a GRAD Axis
429
+
430
+ Notable typefaces: Roboto Flex, Google Fonts variable fonts from 2022+, Amstelvar. Check if a font has `GRAD` using `font-variation-settings: 'GRAD' 0` — if it has no effect the font lacks the axis.
431
+
432
+ ---
433
+
434
+ ## System Font Stacks
435
+
436
+ ### Purpose
437
+
438
+ System fonts load instantly (zero network request) and match the platform's native UI. Use them for UI chrome, admin interfaces, and wherever custom branding is not required.
439
+
440
+ ### Per-Platform System Fonts
441
+
442
+ | Platform | Primary UI font | Year introduced |
443
+ |----------|----------------|----------------|
444
+ | macOS 10.11+ | San Francisco (`-apple-system`) | 2015 |
445
+ | iOS 9+ | San Francisco (`-apple-system`) | 2015 |
446
+ | Windows 10+ | Segoe UI | 2006 |
447
+ | Windows 11 | Segoe UI Variable | 2021 |
448
+ | Android 4.0+ | Roboto | 2011 |
449
+ | Linux (GNOME) | Cantarell | — |
450
+ | Linux (KDE) | Noto Sans | — |
451
+
452
+ ### Modern System Font Stack
453
+
454
+ ```css
455
+ /* Full cross-platform system font stack */
456
+ body {
457
+ font-family:
458
+ -apple-system, /* macOS/iOS Safari — San Francisco */
459
+ BlinkMacSystemFont, /* macOS Chrome — San Francisco */
460
+ 'Segoe UI Variable', /* Windows 11 — variable version of Segoe UI */
461
+ 'Segoe UI', /* Windows 10 */
462
+ system-ui, /* CSS standard keyword (Chrome/Firefox/Safari) */
463
+ Roboto, /* Android, ChromeOS */
464
+ Oxygen, /* KDE Linux */
465
+ Ubuntu, /* Ubuntu Linux */
466
+ Cantarell, /* GNOME Linux */
467
+ 'Helvetica Neue', /* macOS pre-San Francisco */
468
+ Arial, /* universal fallback */
469
+ sans-serif;
470
+ }
471
+ ```
472
+
473
+ ### Monospace System Stack
474
+
475
+ ```css
476
+ code, pre, kbd, samp {
477
+ font-family:
478
+ ui-monospace, /* CSS standard — maps to SF Mono on Apple */
479
+ 'Cascadia Code', /* Windows 11 Terminal default */
480
+ 'Cascadia Mono',
481
+ 'Segoe UI Mono', /* Windows 10 */
482
+ 'Ubuntu Mono', /* Ubuntu Linux */
483
+ 'Roboto Mono', /* Android */
484
+ Menlo, /* macOS pre-SF Mono */
485
+ Monaco,
486
+ Consolas, /* Windows */
487
+ 'Courier New', /* universal fallback */
488
+ monospace;
489
+ }
490
+ ```
491
+
492
+ ### Serif System Stack
493
+
494
+ ```css
495
+ .prose {
496
+ font-family:
497
+ ui-serif, /* CSS standard — not yet widely unique per platform */
498
+ Georgia, /* universal, well-hinted */
499
+ Cambria, /* Windows */
500
+ 'Times New Roman',
501
+ Times,
502
+ serif;
503
+ }
504
+ ```
505
+
506
+ ### Notes on `system-ui`
507
+
508
+ `system-ui` is a CSS level 4 generic family that maps to the OS UI font. It is well-supported (Chrome 56+, Firefox 92+, Safari 11+). Use it as the primary keyword in minimal stacks when you want the standard OS font without specifying exact names:
509
+
510
+ ```css
511
+ /* Minimal modern stack */
512
+ body {
513
+ font-family: system-ui, sans-serif;
514
+ }
515
+ ```
516
+
517
+ For maximum compatibility with older browsers and to ensure San Francisco on macOS/iOS Safari (which does not respond to `system-ui` in all versions), keep `-apple-system` and `BlinkMacSystemFont` before `system-ui`.
518
+
519
+ ---
520
+
521
+ ## Quick Reference: Common Mistakes
522
+
523
+ | Mistake | Fix |
524
+ |---------|-----|
525
+ | `font-variation-settings` on child loses parent axes | Use CSS custom properties to compose all axes in one declaration |
526
+ | Preloading without `crossorigin` attribute | Always add `crossorigin` — font fetches are CORS requests |
527
+ | Using `font-display: block` on body text | Use `swap` — block causes FOIT, text invisible for up to 3s |
528
+ | Changing `font-weight` between light/dark modes | Use `GRAD` axis — changes weight appearance without layout shift |
529
+ | Missing `font-weight` range in `@font-face` for variable font | Declare `font-weight: 100 900` so browser knows full range |
530
+ | Subsetting variable font without `--retain-gids` | Add `--retain-gids` to pyftsubset to preserve axis interpolation |
531
+ | Setting `font-optical-sizing: auto` alongside manual `opsz` in `font-variation-settings` | Set `font-optical-sizing: none` first, then set `'opsz'` manually |
532
+ | Preloading every unicode-range split | Preload only the primary Latin subset; browser lazy-loads others |