@motion-proto/live-tokens 0.91.2 → 0.92.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 (46) hide show
  1. package/.claude/skills/live-tokens-create-page/SKILL.md +10 -3
  2. package/.claude/skills/live-tokens-set-type/SKILL.md +3 -3
  3. package/CHANGELOG.md +140 -0
  4. package/bin/lib/tokenVocabulary.mjs +2 -2
  5. package/bin/rules/testRuns.mjs +1 -1
  6. package/bin/rules/tokens.mjs +3 -3
  7. package/dist-plugin/chunk-JOSSYGR2.js +1634 -0
  8. package/dist-plugin/index.cjs +932 -15
  9. package/dist-plugin/index.js +1 -1
  10. package/dist-plugin/setType/index.cjs +23 -2
  11. package/dist-plugin/setType/index.js +23 -2
  12. package/dist-plugin/tokensCssMigrations/index.cjs +936 -15
  13. package/dist-plugin/tokensCssMigrations/index.d.cts +23 -3
  14. package/dist-plugin/tokensCssMigrations/index.d.ts +23 -3
  15. package/dist-plugin/tokensCssMigrations/index.js +5 -1
  16. package/package.json +1 -1
  17. package/src/app/site.css +243 -9
  18. package/src/demo/TestingLoops.svelte +9 -9
  19. package/src/editor/core/fonts/weightCoverage.ts +31 -2
  20. package/src/editor/core/palettes/tokenRegistry.ts +36 -37
  21. package/src/editor/core/themes/parsers/mediaBlocks.ts +46 -0
  22. package/src/editor/core/typeScale/typeScale.ts +341 -0
  23. package/src/editor/skill-atlas/SourcePane.svelte +3 -3
  24. package/src/editor/skill-atlas/skillSources.generated.ts +2 -2
  25. package/src/editor/skill-atlas/trees/create-page.ts +14 -14
  26. package/src/editor/skill-atlas/trees/set-type.ts +2 -2
  27. package/src/editor/ui/UIFontFamilySelector.svelte +5 -1
  28. package/src/editor/ui/UIFontWeightSelector.svelte +4 -0
  29. package/src/editor/ui/UILetterSpacingSelector.svelte +4 -0
  30. package/src/editor/ui/UILineHeightSelector.svelte +4 -0
  31. package/src/editor/ui/UITextTransformSelector.svelte +4 -0
  32. package/src/editor/ui/UIVariantSelector.svelte +6 -8
  33. package/src/editor/ui/VariablesTab.svelte +4 -4
  34. package/src/editor/ui/sections/textStyles.ts +44 -92
  35. package/src/editor/ui/text-styles/ControlBody.svelte +100 -0
  36. package/src/editor/ui/text-styles/ResetIcon.svelte +60 -0
  37. package/src/editor/ui/text-styles/StepRow.svelte +330 -0
  38. package/src/editor/ui/text-styles/StyleControl.svelte +128 -0
  39. package/src/editor/ui/text-styles/TextStylesSection.svelte +551 -0
  40. package/src/editor/ui/text-styles/textStyleEdits.ts +308 -0
  41. package/src/editor/ui/variantScales.ts +16 -0
  42. package/src/system/styles/tokens.css +412 -48
  43. package/src/testing-js/page-compliance.contract.js +233 -80
  44. package/src/testing-js/page-compliance.contract.js.map +1 -1
  45. package/dist-plugin/chunk-GWMEYBZ2.js +0 -715
  46. package/src/editor/ui/TextStylesSection.svelte +0 -171
@@ -0,0 +1,341 @@
1
+ export const USAGES = ['display', 'heading', 'body', 'editorial', 'code'] as const;
2
+ export type Usage = (typeof USAGES)[number];
3
+
4
+ export const STEPS = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const;
5
+ export type Step = (typeof STEPS)[number];
6
+
7
+ export const EXPONENT: Readonly<Record<Step, number>> = {
8
+ '2xs': -3,
9
+ xs: -2,
10
+ sm: -1,
11
+ md: 0,
12
+ lg: 1,
13
+ xl: 2,
14
+ '2xl': 3,
15
+ };
16
+
17
+ export const VIEWPORTS = ['desktop', 'tablet', 'phone'] as const;
18
+ export type Viewport = (typeof VIEWPORTS)[number];
19
+ export type CompressedViewport = Exclude<Viewport, 'desktop'>;
20
+
21
+ export const MEDIA_QUERY: Readonly<Record<CompressedViewport, string>> = {
22
+ tablet: '(max-width: 768px)',
23
+ phone: '(max-width: 480px)',
24
+ };
25
+
26
+ export interface Interval {
27
+ key: string;
28
+ label: string;
29
+ /** Null for the custom entry, which takes any ratio in `CUSTOM_RATIO_RANGE`. */
30
+ ratio: number | null;
31
+ }
32
+
33
+ export const INTERVALS: readonly Interval[] = [
34
+ { key: 'minor-second', label: 'Minor second', ratio: 1.067 },
35
+ { key: 'major-second', label: 'Major second', ratio: 1.125 },
36
+ { key: 'minor-third', label: 'Minor third', ratio: 1.2 },
37
+ { key: 'major-third', label: 'Major third', ratio: 1.25 },
38
+ { key: 'perfect-fourth', label: 'Perfect fourth', ratio: 1.333 },
39
+ { key: 'augmented-fourth', label: 'Augmented fourth', ratio: 1.414 },
40
+ { key: 'perfect-fifth', label: 'Perfect fifth', ratio: 1.5 },
41
+ { key: 'golden', label: 'Golden ratio', ratio: 1.618 },
42
+ { key: 'custom', label: 'Custom', ratio: null },
43
+ ];
44
+
45
+ export const CUSTOM_RATIO_RANGE = { min: 1.01, max: 2 } as const;
46
+ export const COMPRESSION_RANGE = { min: 0.4, max: 1 } as const;
47
+
48
+ /** Pixels per rem. Bases are rem counts, and the power rule works in px. */
49
+ export const ROOT_PX = 16;
50
+
51
+ export type LineHeight = 'none' | 'tightest' | 'tighter' | 'tight' | 'normal' | 'relaxed';
52
+ export type LetterSpacing = 'tighter' | 'tight' | 'normal' | 'wide' | 'wider';
53
+
54
+ export interface UsageDefaults {
55
+ /** A token name, such as `--font-display`. */
56
+ face: string;
57
+ /** A token name, such as `--font-weight-semibold`. */
58
+ weight: string;
59
+ /** Desktop size of the md step, in rem. */
60
+ base: number;
61
+ ratio: number;
62
+ lineHeight: Readonly<Record<Step, LineHeight>>;
63
+ letterSpacing: Readonly<Record<Step, LetterSpacing>>;
64
+ /** Steps whose family ships unlinked, pinned to this font stack token. */
65
+ familyPins: Readonly<Partial<Record<Step, string>>>;
66
+ /** Elements `site.css` maps to each step. */
67
+ elements: Readonly<Partial<Record<Step, readonly string[]>>>;
68
+ }
69
+
70
+ export interface EyebrowDefaults {
71
+ /** The usage whose face and weight the eyebrow links to. */
72
+ usage: Usage;
73
+ /** The step of that usage whose sizes the eyebrow follows. */
74
+ sizeStep: Step;
75
+ lineHeight: LineHeight;
76
+ letterSpacing: LetterSpacing;
77
+ textTransform: string;
78
+ }
79
+
80
+ const allNormal: Record<Step, 'normal'> = {
81
+ '2xs': 'normal',
82
+ xs: 'normal',
83
+ sm: 'normal',
84
+ md: 'normal',
85
+ lg: 'normal',
86
+ xl: 'normal',
87
+ '2xl': 'normal',
88
+ };
89
+
90
+ /** The tokens.css migrations freeze their own copy of these values; a change here needs one. */
91
+ export const DEFAULTS: {
92
+ compression: Readonly<Record<CompressedViewport, number>>;
93
+ usages: Readonly<Record<Usage, UsageDefaults>>;
94
+ eyebrow: EyebrowDefaults;
95
+ } = {
96
+ compression: { tablet: 0.75, phone: 0.63 },
97
+ usages: {
98
+ display: {
99
+ face: '--font-display',
100
+ weight: '--font-weight-semibold',
101
+ base: 3,
102
+ ratio: 1.2,
103
+ lineHeight: {
104
+ '2xs': 'tighter',
105
+ xs: 'tighter',
106
+ sm: 'tighter',
107
+ md: 'tightest',
108
+ lg: 'tightest',
109
+ xl: 'tightest',
110
+ '2xl': 'tightest',
111
+ },
112
+ letterSpacing: { ...allNormal, lg: 'tight', xl: 'tight', '2xl': 'tight' },
113
+ familyPins: {},
114
+ elements: {},
115
+ },
116
+ heading: {
117
+ face: '--font-display',
118
+ weight: '--font-weight-semibold',
119
+ base: 1.5,
120
+ ratio: 1.25,
121
+ lineHeight: {
122
+ '2xs': 'tight',
123
+ xs: 'tight',
124
+ sm: 'tighter',
125
+ md: 'tighter',
126
+ lg: 'tightest',
127
+ xl: 'tightest',
128
+ '2xl': 'tightest',
129
+ },
130
+ letterSpacing: allNormal,
131
+ familyPins: { '2xs': '--font-sans', xs: '--font-sans', sm: '--font-sans' },
132
+ elements: { '2xs': ['h6'], xs: ['h5'], sm: ['h4'], md: ['h3'], lg: ['h2'], xl: ['h1'] },
133
+ },
134
+ body: {
135
+ face: '--font-sans',
136
+ weight: '--font-weight-normal',
137
+ base: 1,
138
+ ratio: 1.125,
139
+ lineHeight: {
140
+ '2xs': 'tight',
141
+ xs: 'tight',
142
+ sm: 'tight',
143
+ md: 'normal',
144
+ lg: 'tight',
145
+ xl: 'tighter',
146
+ '2xl': 'tighter',
147
+ },
148
+ letterSpacing: allNormal,
149
+ familyPins: {},
150
+ elements: { sm: ['small'], md: ['p', 'li'] },
151
+ },
152
+ editorial: {
153
+ face: '--font-editorial',
154
+ weight: '--font-weight-normal',
155
+ base: 1,
156
+ ratio: 1.125,
157
+ lineHeight: {
158
+ '2xs': 'tighter',
159
+ xs: 'tighter',
160
+ sm: 'tight',
161
+ md: 'normal',
162
+ lg: 'tight',
163
+ xl: 'tighter',
164
+ '2xl': 'tightest',
165
+ },
166
+ letterSpacing: allNormal,
167
+ familyPins: {},
168
+ elements: {},
169
+ },
170
+ code: {
171
+ face: '--font-mono',
172
+ weight: '--font-weight-normal',
173
+ base: 0.875,
174
+ ratio: 1.125,
175
+ lineHeight: {
176
+ '2xs': 'normal',
177
+ xs: 'normal',
178
+ sm: 'normal',
179
+ md: 'normal',
180
+ lg: 'normal',
181
+ xl: 'normal',
182
+ '2xl': 'normal',
183
+ },
184
+ letterSpacing: allNormal,
185
+ familyPins: {},
186
+ elements: { md: ['pre'] },
187
+ },
188
+ },
189
+ eyebrow: {
190
+ usage: 'body',
191
+ sizeStep: 'sm',
192
+ lineHeight: 'normal',
193
+ letterSpacing: 'normal',
194
+ textTransform: 'none',
195
+ },
196
+ };
197
+
198
+ export const EYEBROW_PREFIX = '--eyebrow';
199
+ export type StylePrefix = `--${Usage}-${Step}` | typeof EYEBROW_PREFIX;
200
+ export type UsageSetting = 'font-family' | 'font-weight' | 'scale-base' | 'scale-ratio';
201
+ export type StyleAxis = 'font-family' | 'font-weight' | 'line-height' | 'letter-spacing' | 'text-transform';
202
+
203
+ export const compressionName = (viewport: CompressedViewport): string =>
204
+ `--type-${viewport}-scale-compression`;
205
+ export const usageName = (usage: Usage, setting: UsageSetting): string => `--${usage}-${setting}`;
206
+ export const stepPrefix = (usage: Usage, step: Step): StylePrefix => `--${usage}-${step}`;
207
+ export const axisName = (prefix: StylePrefix, axis: StyleAxis): string => `${prefix}-${axis}`;
208
+ export const sizeName = (prefix: StylePrefix, viewport: Viewport): string =>
209
+ `${prefix}-${viewport}-font-size`;
210
+ /**
211
+ * The structural size. `tokens.css` points it at the desktop size and re-points
212
+ * it at each breakpoint, so the editor never writes it and no theme holds it.
213
+ */
214
+ export const fontSizeName = (prefix: StylePrefix): string => `${prefix}-font-size`;
215
+
216
+ /** Steps in `tokens.css` order, largest first. */
217
+ const STEPS_DOWN: readonly Step[] = [...STEPS].reverse();
218
+
219
+ /** Every text style, in `tokens.css` order: each usage's steps, then the eyebrow. */
220
+ export const STYLE_PREFIXES: readonly StylePrefix[] = [
221
+ ...USAGES.flatMap((usage) => STEPS_DOWN.map((step) => stepPrefix(usage, step))),
222
+ EYEBROW_PREFIX,
223
+ ];
224
+
225
+ export function scalePx(base: number, ratio: number, step: Step): number {
226
+ return base * ROOT_PX * ratio ** EXPONENT[step];
227
+ }
228
+
229
+ export function recommend(desktopPx: number, compression: number): number {
230
+ return Math.min(desktopPx, ROOT_PX * (desktopPx / ROOT_PX) ** compression);
231
+ }
232
+
233
+ /** A number as CSS writes it: at most four decimals, no trailing zeros. */
234
+ export function formatNumber(n: number): string {
235
+ return String(Number(n.toFixed(4)));
236
+ }
237
+
238
+ export function formatRem(rem: number): string {
239
+ return `${formatNumber(rem)}rem`;
240
+ }
241
+
242
+ /** The step's desktop size as a unitless rem count over the usage's scale settings. */
243
+ export function scaleTerm(usage: Usage, step: Step): string {
244
+ const base = `var(${usageName(usage, 'scale-base')})`;
245
+ const n = EXPONENT[step];
246
+ return n === 0 ? base : `${base} * pow(var(${usageName(usage, 'scale-ratio')}), ${n})`;
247
+ }
248
+
249
+ export function desktopScaleExpression(usage: Usage, step: Step): string {
250
+ return `calc(${scaleTerm(usage, step)} * 1rem)`;
251
+ }
252
+
253
+ function recommendationExpression(prefix: StylePrefix, remTerm: string, viewport: CompressedViewport): string {
254
+ return `min(var(${sizeName(prefix, 'desktop')}), calc(pow(${remTerm}, var(${compressionName(viewport)})) * 1rem))`;
255
+ }
256
+
257
+ /** The recommendation while the desktop size follows the scale. */
258
+ export function recommendedScaleExpression(usage: Usage, step: Step, viewport: CompressedViewport): string {
259
+ return recommendationExpression(stepPrefix(usage, step), scaleTerm(usage, step), viewport);
260
+ }
261
+
262
+ /** The recommendation once the desktop size holds an edit of `desktopRem`. */
263
+ export function recommendedEditedExpression(
264
+ prefix: StylePrefix,
265
+ desktopRem: number,
266
+ viewport: CompressedViewport,
267
+ ): string {
268
+ return recommendationExpression(prefix, formatNumber(desktopRem), viewport);
269
+ }
270
+
271
+ export interface TokenDeclaration {
272
+ name: string;
273
+ value: string;
274
+ }
275
+
276
+ const ref = (name: string): string => `var(${name})`;
277
+ const COMPRESSED: readonly CompressedViewport[] = ['tablet', 'phone'];
278
+
279
+ /** The text-style block of `tokens.css`, one entry per declaration, in file order. */
280
+ export function typeScaleDeclarations(): TokenDeclaration[] {
281
+ const out: TokenDeclaration[] = COMPRESSED.map((vp) => ({
282
+ name: compressionName(vp),
283
+ value: formatNumber(DEFAULTS.compression[vp]),
284
+ }));
285
+
286
+ for (const usage of USAGES) {
287
+ const d = DEFAULTS.usages[usage];
288
+ out.push(
289
+ { name: usageName(usage, 'font-family'), value: ref(d.face) },
290
+ { name: usageName(usage, 'font-weight'), value: ref(d.weight) },
291
+ { name: usageName(usage, 'scale-base'), value: formatNumber(d.base) },
292
+ { name: usageName(usage, 'scale-ratio'), value: formatNumber(d.ratio) },
293
+ );
294
+ for (const step of STEPS_DOWN) {
295
+ const prefix = stepPrefix(usage, step);
296
+ const pin = d.familyPins[step];
297
+ out.push(
298
+ { name: axisName(prefix, 'font-family'), value: ref(pin ?? usageName(usage, 'font-family')) },
299
+ { name: sizeName(prefix, 'desktop'), value: desktopScaleExpression(usage, step) },
300
+ ...COMPRESSED.map((vp) => ({
301
+ name: sizeName(prefix, vp),
302
+ value: recommendedScaleExpression(usage, step, vp),
303
+ })),
304
+ { name: fontSizeName(prefix), value: ref(sizeName(prefix, 'desktop')) },
305
+ { name: axisName(prefix, 'font-weight'), value: ref(usageName(usage, 'font-weight')) },
306
+ { name: axisName(prefix, 'line-height'), value: ref(`--line-height-${d.lineHeight[step]}`) },
307
+ { name: axisName(prefix, 'letter-spacing'), value: ref(`--letter-spacing-${d.letterSpacing[step]}`) },
308
+ );
309
+ }
310
+ }
311
+
312
+ const e = DEFAULTS.eyebrow;
313
+ const follows = stepPrefix(e.usage, e.sizeStep);
314
+ out.push(
315
+ { name: axisName(EYEBROW_PREFIX, 'font-family'), value: ref(usageName(e.usage, 'font-family')) },
316
+ ...VIEWPORTS.map((vp) => ({ name: sizeName(EYEBROW_PREFIX, vp), value: ref(sizeName(follows, vp)) })),
317
+ { name: fontSizeName(EYEBROW_PREFIX), value: ref(sizeName(EYEBROW_PREFIX, 'desktop')) },
318
+ { name: axisName(EYEBROW_PREFIX, 'font-weight'), value: ref(usageName(e.usage, 'font-weight')) },
319
+ { name: axisName(EYEBROW_PREFIX, 'line-height'), value: ref(`--line-height-${e.lineHeight}`) },
320
+ { name: axisName(EYEBROW_PREFIX, 'letter-spacing'), value: ref(`--letter-spacing-${e.letterSpacing}`) },
321
+ { name: axisName(EYEBROW_PREFIX, 'text-transform'), value: e.textTransform },
322
+ );
323
+ return out;
324
+ }
325
+
326
+ /** The breakpoint re-points of the structural sizes, in file order. */
327
+ export function typeScaleMediaDeclarations(viewport: CompressedViewport): TokenDeclaration[] {
328
+ return STYLE_PREFIXES.map((prefix) => ({
329
+ name: fontSizeName(prefix),
330
+ value: ref(sizeName(prefix, viewport)),
331
+ }));
332
+ }
333
+
334
+ export const STRUCTURAL_NAMES: ReadonlySet<string> = new Set(STYLE_PREFIXES.map(fontSizeName));
335
+
336
+ /** Every name the editor may write. */
337
+ export const EDITABLE_NAMES: ReadonlySet<string> = new Set(
338
+ typeScaleDeclarations()
339
+ .map((d) => d.name)
340
+ .filter((name) => !STRUCTURAL_NAMES.has(name)),
341
+ );
@@ -174,7 +174,7 @@
174
174
  text-align: right;
175
175
  font-family: var(--code-font-family);
176
176
  font-size: var(--font-size-md);
177
- line-height: var(--code-line-height);
177
+ line-height: var(--code-md-line-height);
178
178
  font-variant-numeric: tabular-nums;
179
179
  color: var(--text-muted);
180
180
  cursor: pointer;
@@ -194,8 +194,8 @@
194
194
  font-family: var(--code-font-family);
195
195
  font-size: var(--font-size-md);
196
196
  font-weight: var(--code-font-weight);
197
- line-height: var(--code-line-height);
198
- letter-spacing: var(--code-letter-spacing);
197
+ line-height: var(--code-md-line-height);
198
+ letter-spacing: var(--code-md-letter-spacing);
199
199
  color: var(--text-secondary);
200
200
  }
201
201
 
@@ -17,7 +17,7 @@ export const skillDocs: Record<string, Record<string, string[]>> = {
17
17
  "references/token-naming.md": ["# Suffix vocabulary","","The editor picker is chosen by suffix. There is no per-token override; if a","token renders with the wrong picker, rename it to one of these.","","`KIND_RULES` in `src/editor/core/components/aliasKinds.ts` is authoritative, and","`check-component` fails on a suffix outside it. `check:skills` holds this file","to that list, so the two cannot drift apart.","","## Color and surface","","| Suffix | Meaning |","|-------------|---------------------------------------------------------------|","| `-surface` | Fill / background color |","| `-border` | Border color |","| `-text` | Text color |","| `-icon` | Icon color |","| `-label` | Label text color |","| `-fill` | Inner fill (distinct from outer surface) |","| `-color` | A hairline rule's colour, or a color no role word above names |","| `-shadow` | Box-shadow |","| `-blur` | Backdrop or filter blur radius |","| `-tint` | A wash over the surface, aliasing a `--tint-*` stop |","| `-indicator` | The colour of the bar or stripe that marks an item |","| `-thumb` | A scrollbar or slider thumb's colour |","| `-title` | Title text colour |","| `-body` | Body text colour |","| `-eyebrow` | Eyebrow text colour |","| `-description` | Description text colour |","| `-hint` | Hint text colour |","| `-error` | Error text colour |","| `-placeholder` | Placeholder text colour |","| `-value` | A displayed value's colour |","","## Geometry","","| Suffix | Meaning |","|-----------------|---------------------------------------------------------------|","| `-radius` | Corner radius |","| `-border-width` | Stroke thickness (used even when CSS uses `outline:`) |","| `-indicator-width` | An indicator's thickness |","| `-hairline-width` | A hairline rule's thickness |","| `-dot-size` | A dot indicator's diameter |","| `-hairline-inset` | Inset trimmed from a stretched hairline |","| `-track-height` | A track's height (progress bar, slider) |","| `-icon-size` | An icon's rendered size |","| `-thumb-size` | A thumb's rendered size |","| `-height` | A measured height (a track, a panel) |","| `-margin` | Outer spacing, moved on the same scale as `-padding` |","| `-duration` | Motion duration |","| `-easing` | Motion easing curve |","| `-scale` | A transform scale factor |","| `-width` | Width dimension |","| `-size` | Square / uniform dimension |","| `-padding` | Internal spacing |","| `-gap` | Spacing between sibling elements |","","`-width`, `-height` and `-size` are the fall-through: any dimension with no","more specific name behind it. They read the `--space-*` scale through the same","picker `-gap` uses, and they match last, so `-border-width`, `-hairline-width`,","`-icon-size` and the rest claim their token first. Reach for the specific name","when one fits; a stroke is `-border-width` even where the CSS says `outline:`.","","## Typography","","| Suffix | Meaning |","|--------------------|--------------------------|","| `-font-family` | Font family reference |","| `-font-weight` | Font weight reference |","| `-font-size` | Font size reference |","| `-line-height` | Line height |","| `-letter-spacing` | Letter spacing |","","A suffix that is not here is either a rename away from one that is, or","an issue against `@motion-proto/live-tokens`. Inventing one costs the token its","picker: the editor falls back to a plain text input."],
18
18
  },
19
19
  "create-page": {
20
- "SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `npx live-tokens components <id>` prints a component's declared props, the values each union accepts, and its catalogue entry, including its constraints and when not to use it. `--json` prints the same as data. The list includes the project's own components.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","- An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant=\"danger\"`.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `--body-sm-*` in `--text-secondary`.","- A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. A `danger` Button sits apart from the group it could be mistaken for. A vertical stack of Buttons sets `fullWidth` on each Button; a row omits it.","- For a `MenuSelect` picker, toggle it from a Button with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.","","## Rules the checker enforces","","`check-page` fixes what it can and reports the rest by rule id.","","- `multiple-primary`","- `danger-without-dialog`","- `control-size`","- `native-control`","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Card title | the Card `title` prop |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Command or value | `code` in `--code-*` |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h4`, `p`, `code`, `pre`, and list items from these styles, so the tag carries the style. Type an element only when the table gives its tag a different style. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.","","### Emphasis","","`npx live-tokens components button` names each `variant` and the role it carries.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- Every action that destroys saved work meets the Button constraint `danger-without-dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
20
+ "SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `npx live-tokens components <id>` prints a component's declared props, the values each union accepts, and its catalogue entry, including its constraints and when not to use it. `--json` prints the same as data. The list includes the project's own components.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","- An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant=\"danger\"`.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `.body-sm` in `--text-secondary`.","- A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. A `danger` Button sits apart from the group it could be mistaken for. A vertical stack of Buttons sets `fullWidth` on each Button; a row omits it.","- For a `MenuSelect` picker, toggle it from a Button with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.","","## Rules the checker enforces","","`check-page` fixes what it can and reports the rest by rule id.","","- `multiple-primary`","- `danger-without-dialog`","- `control-size`","- `native-control`","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","The text styles fall into five usages, each a scale of seven steps from `2xs` to `2xl`: display for type outside the page outline, heading for the outline, body for interface text and short copy, editorial for long reading, and code. Each step is a style named `--{usage}-{step}-*`, such as `--body-sm-*`. Headings use the whole set. The other usages mostly take `md` and `sm`.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Title inside a section | `h3` to `h6` in `--heading-md-*` to `--heading-2xs-*` |","| Card title | the Card `title` prop |","| Hero title | `--display-xl-*` |","| Big number | `--display-md-*` |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Article copy | `p` in `--editorial-md-*` |","| Code block | `pre` in `--code-md-*` |","| Command or value in a line | `code`, at `0.875em` of the text around it |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h6`, `p`, `small`, `code`, `pre`, and list items from these styles, so the tag carries the style. It also gives every step a class of the same name, such as `.display-md` or `.body-sm`, that sets all five axes. Type an element only when the table gives its tag a different style, and type it with the step's class. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.","","### Emphasis","","`npx live-tokens components button` names each `variant` and the role it carries.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- Every action that destroys saved work meets the Button constraint `danger-without-dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
21
21
  "references/interaction-sources.md": ["# Interaction sources","","Read this when a Verify check or an emphasis rule in SKILL.md needs its reason.","Each principle below is held by one of three parties. A shipped component","holds it when the component's own design answers it, so the page's rule is","the component and pick-component names the test. The page holds it when only","the page can get it right, so SKILL.md states a read or the checker a rule.","The product holds it when no page decision touches it, and the row says so","to keep a later edit from reopening it.","","## Jakob Nielsen, ten usability heuristics","","| Heuristic | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Visibility of system status | Page | Verify: an action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`. |","| Match between system and the real world | Page | Verify: labels use the user's words. |","| User control and freedom | Page | Verify: every row of actions holds an action that leaves without committing. Emphasis: `multiple-primary`. |","| Consistency and standards | Page | One size, one primary action, one text style per element. The components carry the rest. |","| Error prevention | Page | Verify: `danger-without-dialog`. Verify: every field has a default and Reset restores it. |","| Recognition rather than recall | Component | `MenuSelect` lists the options; `Input` carries its label and hint; `Tooltip` defines in place. |","| Flexibility and efficiency of use | Product | Shortcuts and customisation are product decisions. |","| Aesthetic and minimalist design | Page | Layout: each element serves a purpose no other element serves. Verify: secondary settings sit in a `CollapsibleSection`. |","| Help users recognise, diagnose, and recover from errors | Component | `Input` carries the error line; `Callout variant=\"danger\"` carries a section's. |","| Help and documentation | Product | Contextual help is a product decision. |","","The complex-application version of the ten (Kaley, Nielsen Norman Group)","describes a tool with a stage and controls, which is the tool page SKILL.md","lays out. Its additions that the page holds: a wait past ten seconds shows","steps done and steps left, and the stage is the live preview of every","control. Undo, version history, and autosave are the product's.","","## Bruce Tognazzini, first principles of interaction design","","| Principle | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Anticipation | Page | Layout: show related items side by side. |","| Colour | Component | `Callout`, `Badge`, and `Notification` carry an icon or text beside the colour. |","| Consistency | Page | One size, one primary action. |","| Defaults | Page | Verify: every field has a default and Reset restores it. |","| Discoverability | Page | Verify: every control is in view. |","| Explorable interfaces | Page | Verify: every row of actions holds an action that leaves without committing. |","| Fitts's law | Component | The shipped default is the large target; SKILL.md's one-size rule keeps it. A toolbar sits on the section's bottom edge. |","| Protect users' work | Page | Verify: `danger-without-dialog`. |","| Readability | Component | live-tokens-set-colors gates every text pair at WCAG AA. |","| Simplicity | Page | Verify: secondary settings sit in a `CollapsibleSection`; no capability is removed for the sake of simplicity. |","| Visible navigation | Component | `SideNavigation` follows the current path. |","| Aesthetics, Autonomy, Efficiency of the user, Human-interface objects, Latency reduction, Learnability, Metaphors, State | Product | Measured, engineered, or researched outside a page. |","","Sources:","","- Nielsen, 10 Usability Heuristics for User Interface Design (1994, updated 2024). https://www.nngroup.com/articles/ten-usability-heuristics/","- Kaley, 10 Usability Heuristics Applied to Complex Applications. https://www.nngroup.com/articles/usability-heuristics-complex-applications/","- Tognazzini, First Principles of Interaction Design (revised 2014). https://asktog.com/atc/principles-of-interaction-design/","","## W3C Web Accessibility Initiative","","| Guidance | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Headings: one `h1`, levels in order, no skipped level | Page | Type: use the semantic element for each place. Verify: one `h1`, levels in order. |","| Images: text alternatives | Page | Verify: every `img` has `alt` text. |","| Focus order follows the reading order | Page | Verify: focus order follows the reading order. |","| Form labels and error messages | Component | `Input` carries its label, hint, and error line. |","","https://www.w3.org/WAI/tutorials/page-structure/headings/","https://www.w3.org/WAI/tutorials/images/","https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html"],
22
22
  "references/layout-sources.md": ["# Layout sources","","Read this when a layout decision in SKILL.md needs its reason. Each law in the","Layout section comes from one of these sources. The sources are for layout and","hierarchy only. Do not take color or type opinions from them: the theme owns","those.","","## Edward Tufte","","Tufte wrote about information graphics. His laws apply to a page because a page","is an information display with controls on it.","","| Law | Statement | Rule in SKILL.md |","|---|---|---|","| Smallest effective difference | Make all visual distinctions as subtle as possible, but still clear and effective. | Separate with space first, then a hairline rule, then a second surface. |","| 1+1=3 | Two heavy marks side by side make a third mark: the space between them. | A section of containers with borders and header bars reads as a set of posters. |","| Layering and separation | Put data on top, labels next, and scaffolding faintest. | Content, labels, and chrome each take their own token. |","| Administrative debris | The metaphor for the interface is the information. Remove the chrome that the tool adds for itself. | The stage takes the space. Controls take their shipped default. |","| Spatial over temporal | Show information adjacent in space. A control that hides it stacks it in time. | Show related items side by side. Frames in a strip are small multiples. |","| Erase non-data ink | Remove each mark that carries no information. | The Verify question: does the page lose information when this element is removed? |","| Micro and macro readings | A good display reads at a distance and up close. | Verify from a distance, then closely. |","","Sources:","","- Envisioning Information (1990): layering and separation, small multiples, micro and macro. https://www.edwardtufte.com/book/envisioning-information/","- The Visual Display of Quantitative Information (1983): data-ink, chartjunk.","- iPhone interface design, edwardtufte.com notebook: administrative debris, spatial over temporal. https://www.edwardtufte.com/notebook/iphone-interface-design/","","## Josef Müller-Brockmann","","Grid Systems in Graphic Design (1981) is the discipline behind the page column","grid. The grid does the separating, so an element needs no border to show","where it sits. His stated aim is compact planning, intelligibility, and","clarity. That is Tufte's aim in a typographer's words.","","## Refactoring UI","","Adam Wathan and Steve Schoger, Refactoring UI (2018), turns both into working","rules for product screens:","","- Put more space around a group than within it.","- Start with too much white space, then remove some.","- Use fewer borders. Separate with space, a shadow, or a second background.","- Emphasize by de-emphasizing the secondary content.","- Labels are a last resort.","- Keep a spacing scale where no two steps are closer than a quarter. The `--space-*` scale is that scale.","","https://www.refactoringui.com/","","## Material Design 3, canonical layouts","","The Page layouts table takes its rows from Material's canonical layouts.","","| Layout | Statement | Row in SKILL.md |","|---|---|---|","| Supporting pane | The primary area takes about two thirds of the window; the secondary pane takes the rest. At compact width the pane moves below the main content. | Main with a supporting pane, two thirds and one third; the stacking sentence. |","| List-detail | The list and the detail of the selected item sit side by side at expanded width. | List with detail. |","| Feed | Equivalent items in an adaptive grid. | Grid of equals. |","","https://m3.material.io/foundations/layout/canonical-examples/overview","https://developer.android.com/develop/adaptive-apps/guides/canonical-layouts","","## Cloudscape patterns","","| Pattern | Statement | Rule in SKILL.md |","|---|---|---|","| Dashboard | Three areas top to bottom: overview, data, support. \"Consider seven as the limit number for data representation.\" | Verify: the first section holds what the user came for. Grid of equals: up to seven per section. |","| Single-page create | One container; the essential fields first and few; secondary inputs in an expandable section; cancel then submit at the bottom. | Containers: a form. Single column. |","| Details page | The title with its actions, then a summary, then related blocks. | Stacked sections. |","| Empty state | A heading, an optional line, and one action. \"Always provide an action.\" Errors go elsewhere. | Containers: an empty stage. |","","https://cloudscape.design/patterns/general/service-dashboard/static-dashboard/","https://cloudscape.design/patterns/resource-management/create/single-page-create/","https://cloudscape.design/patterns/resource-management/details/details-page/","https://cloudscape.design/patterns/general/empty-states/","","## Matthew Butterick, line length","","\"45 to 90 characters per line, including spaces.\" The Separation paragraph and the Verify check carry the measure, and the half-width copy span holds it at body size.","","https://practicaltypography.com/line-length.html","","## Nielsen Norman Group, proximity","","\"Proximity is one of the most important grouping principles and can overpower competing visual cues such as similarity of color or shape.\" Space inside a group is smaller than space between groups. An unrelated action inside a group is camouflaged, so a `danger` Button sits apart from the toolbar group.","","https://www.nngroup.com/articles/gestalt-proximity/"],
23
23
  },
@@ -37,7 +37,7 @@ export const skillDocs: Record<string, Record<string, string[]>> = {
37
37
  "references/geometry-anchors.md": ["# Geometry anchors: feelings, idioms, and genres","","Read this when the geometry intent names one of these. An anchor overrides the","Idioms table in SKILL.md, because it is tuned to the same direction the color","came from, and a style's geometry is often targeted rather than global.","","Entries are written in the ops vocabulary. \"radius +2\" is a radius shift of 2.","\"borders +1\" is a border-width shift of 1. \"padding +1\" is a padding and gap","shift of 1. \"hairline borders\" is border-width `set: \"--border-width-1\"`, and","\"no borders\" is `set: \"--border-width-0\"`. \"hairline rules\" is hairline-width","`set: \"--border-width-1\"`. A named component means a targeted op. Controls squeeze before containers,","so a compaction of more than one step still spends its extra steps on","containers by name.","","An occasion (Christmas, Autumn, Ocean) fixes color only. Take its geometry from","the feeling it implies, or leave geometry alone.","","## Feelings","","| Anchor | Geometry |","|---|---|","| Joyful, exuberant, energetic | radius +2, padding +1, pill buttons |","| Playful, whimsical | pill buttons, rounder cards, wide gaps |","| Optimistic, hopeful | radius +1 |","| Confident, bold | radius -1, borders +1, tight gaps |","| Serene, tranquil | soft radius, padding +1, no borders |","| Tender, gentle, romantic | rounder, hairline borders |","| Cozy, comforting | radius +1, padding +1 |","| Wistful, nostalgic, vintage, faded | unchanged, hairline rules |","| Earthy, grounded, natural | radius +1, padding +1 |","| Clinical, sterile, precise | radius sm, tight gaps, hairline borders |","| Contemplative, focused | padding +1, minimal borders |","| Urgent, alarming | radius -2, borders +2, tight padding |","| Tense, anxious | tight gaps, radius sm |","| Defiant, rebellious, loud | radius none, borders +3 |","| Melancholy, moody, sad | padding +1, hairline borders |","| Somber, grave, mournful | sharp, tight gaps, hairline rules |","| Ominous, dramatic, haunted | sharp, borders +2 |","| Austere, severe, cold | radius none, tight padding, hairline borders |","","## Idioms, eras, and genres","","| Anchor | Geometry |","|---|---|","| Swiss, International | radius none, tight gaps, hairline borders |","| Bauhaus | radius none on containers, radius full on buttons alone, so the circle reads as a decision |","| Mid-century modern | radius +1 to +2, padding +1, no borders |","| Art deco, opulent, luxurious | sharp, padding +1, thin borders |","| Terminal, phosphor | radius none, tight padding, a 1px border on everything |","| Cyberpunk, neon noir, futuristic | sharp, tight gaps |","| Vaporwave | rounder, airier |","| Y2K, bubble | radius full on buttons with the padding a pill needs, generous spacing |","| Blueprint | radius none, 1px borders, tight grid |","| Scandinavian, hygge | soft radius, padding +1, hairline borders |","| Japandi, wabi-sabi | radius sm, padding +2, no borders |","| Cottagecore, botanical | rounder, airier |","| Editorial, magazine | sharp, padding +1, hairline rules |","| Newsprint, broadsheet | radius none, tight gaps, hairline rules |","| Risograph, zine | radius none, borders +2 |","| Corporate, professional, trustworthy | leave it alone |","| Brutalist | radius none, borders +2 to +3, tight padding |","| Memphis, postmodern | targeted rather than global: pill buttons against radius-none cards |","| Industrial, workshop, gritty | radius sm, thick borders, tight padding |"],
38
38
  },
39
39
  "set-type": {
40
- "SKILL.md": ["---","name: live-tokens-set-type","description: Set a live-tokens theme's type: a Google Fonts pairing for the shipped --font-* stacks. The CLI verifies each family for the weights it ships. Called with an anchor and a type intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks to pair fonts, pick a typeface, or set the fonts. Use when the user describes type by voice: editorial, friendlier, technical, elegant, less generic. Use when the user names a face for a role: a serif for headings, a display font. Changes type only. For a request that also names color or geometry, read live-tokens-create-theme.","---","","# Setting a theme's type","","Choose the families. The CLI verifies each against Google Fonts, builds the URL from the weights the family has, and writes the result to the buffer. Never hand-author font JSON or edit the data tree. Google Fonts is the pool because it is freely licensable and loads by URL. Other sources go in through the editor's Project fonts section.","","The result is on screen as soon as the run finishes. The three set skills write the same buffer, so color, type, and geometry compose in any order. When the user accepts the result, run `save-theme` to keep it as a theme. Loading a theme in the editor discards it.","","## Workflow","","1. Read the type intent and any anchor live-tokens-create-theme passed. When either names an anchor (a feeling, an idiom, or a genre), read its entry in `references/type-anchors.md`; it overrides the Voice table below.","2. Choose the pairing and write it to `scratch/font-pairing.json`.","3. Run `npx live-tokens set-type scratch/font-pairing.json`. It prints each stack that moved, each family's weights and URL, and, under Weight coverage, the weights the typography tokens ask for that the family lacks.","4. Read the report. Name a missing weight and offer an alternative only when it matters: a body face without 400, 700, or italic matters, and a display face without 300 does not. A family not on Google Fonts fails the run. Fix the spelling and re-run. A pairing the stacks already hold prints \"Nothing to change\" and writes nothing. A pairing equal to the open theme's discards the buffer, and the report says so.","5. Reply with the two families, the form model behind each, the matrix verdict, and any missing weight worth naming.","","Flags: `--dry-run` reports without writing. `--no-verify` skips the network and requires a URL per family; use it only offline.","","## The pairing file","","```json","{ \"display\": \"Fraunces\", \"body\": \"Nunito Sans\" }","```","","Every slot is optional; an omitted slot keeps its family. `display` is `--font-display` and `body` is `--font-sans`. `serif`, `mono`, and `editorial` exist when a theme needs them. `editorial` is `--font-editorial`, the long-reading face behind the `--editorial-*` text styles. An omitted `editorial` keeps its family, so set it only when essays and articles need a face of their own. Weight coverage is reported for `display`, `body`, `serif`, and `mono`. A family bound to `editorial` gets no coverage line. A slot may be `{ \"name\": \"...\", \"url\": \"...\" }` to pin a URL. A pinned URL is not probed, so the report shows no weights for it and coverage skips it. Spell families as Google does; the CLI reports the canonical spelling.","","## Choose the body face first","","The body face is the anchor. It carries most of the words, and text faces survive small sizes where display faces do not. Pick it against the type intent, then pick the display face against it. A body face has regular, bold, and italic; low to moderate stroke contrast; open apertures; and a large x-height. A face missing any of these is a display face, whatever its name says.","","The shipped text styles ask the display face for 600 and the body face for 400; `strong` and `em` add 700 and italic. Screen candidates against those four weights before running.","","## The font matrix","","Classify each candidate by form model and by stroke contrast and serifs.","","| Form model | Construction | Reads as |","|---|---|---|","| **Dynamic** | diagonal stress, open apertures, written origin | open, warm, humane, timeless |","| **Rational** | vertical stress, closed apertures, drawn not written | orderly, reserved, elegant, authoritative |","| **Geometric** | monolinear, circle-and-line | technical, modern, systematic, sober |","","- One form model with different stroke contrast or serifs pairs reliably. Helvetica and Bodoni are both rational, one a linear sans and one a high-contrast serif.","- Different form models with the same stroke contrast and serifs fail. The two look alike and fight underneath. Two arbitrary sans serifs clash for this reason.","- Different on both counts works. An unmistakable difference reads as a decision.","","Many faces sit between columns. When one straddles, say so and lean on the Voice table and the x-height check.","","## Voice","","| The intent says | Type voice |","|---|---|","| editorial, literary, considered | dynamic serif display over a humanist sans body |","| elegant, luxurious, formal | rational high-contrast serif display; keep the body quiet |","| friendly, warm, approachable | dynamic sans on both sides, or a soft serif display |","| technical, systematic, precise | geometric or neo-grotesque sans; a mono for code |","| playful, informal | an expressive display face over a plain workhorse body |","| serious, institutional, trustworthy | rational sans body, rational serif display |","| quiet, minimal, unbranded | one superfamily across both slots |","","The table covers an intent that names no anchor. An anchor's row in `references/type-anchors.md` wins.","","Match the type to the design direction the color came from. A warm autumn palette under a cold geometric sans reads as two projects.","","## Shortcuts","","Use these when the request is vague or the type should stay quiet.","","- **A superfamily.** A Google Fonts family with sans and serif siblings: Alegreya, Ancizar, IBM Plex, Inria, Merriweather, Noto, PT, Roboto, Source. The catalogue moves and this list does not; `set-type` fails on a family that is gone.","- **One family across weights.**","- **Same designer or foundry.**","- **Serif display over sans body** when nothing else decides it.","","## Watch for","","- **x-height parity.** Both faces share one size scale, so a small-x-height display face over a large-x-height body face gives a heading weaker than its own body text. Check it on the rendered page.","- **Print faces at small sizes.** Delicate serifs and high stroke contrast turn to mud below 16px.","- **Every family is a download.** Two is the target; three needs a reason.","- **Sets of themes.** No two share a display face or a body face.","","## Scope","","Type only. Color, component aliases, shape, and the type scale are untouched: `set-type` writes the font entries in the buffer and carries every other value forward. `save-theme` keeps the result; Adopt ships it and rewrites `fonts.css`, which is how a build without the editor loads the family.","","## Verify","","- The CLI exits 0 and names each stack that moved, before and after.","- Each URL matches the family's weights: a range for a variable family, an enumeration for a static one, a bare URL for a single-weight face. A pinned URL is written as given.","- The app shows the new type, and the editor's Fonts section lists both families with their fallbacks.","- To revert, run the previous pairing file, or load the open theme to discard the buffer."],
40
+ "SKILL.md": ["---","name: live-tokens-set-type","description: Set a live-tokens theme's type: a Google Fonts pairing for the shipped --font-* stacks. The CLI verifies each family for the weights it ships. Called with an anchor and a type intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks to pair fonts, pick a typeface, or set the fonts. Use when the user describes type by voice: editorial, friendlier, technical, elegant, less generic. Use when the user names a face for a role: a serif for headings, a display font. Changes type only. For a request that also names color or geometry, read live-tokens-create-theme.","---","","# Setting a theme's type","","Choose the families. The CLI verifies each against Google Fonts, builds the URL from the weights the family has, and writes the result to the buffer. Never hand-author font JSON or edit the data tree. Google Fonts is the pool because it is freely licensable and loads by URL. Other sources go in through the editor's Project fonts section.","","The result is on screen as soon as the run finishes. The three set skills write the same buffer, so color, type, and geometry compose in any order. When the user accepts the result, run `save-theme` to keep it as a theme. Loading a theme in the editor discards it.","","## Workflow","","1. Read the type intent and any anchor live-tokens-create-theme passed. When either names an anchor (a feeling, an idiom, or a genre), read its entry in `references/type-anchors.md`; it overrides the Voice table below.","2. Choose the pairing and write it to `scratch/font-pairing.json`.","3. Run `npx live-tokens set-type scratch/font-pairing.json`. It prints each stack that moved, each family's weights and URL, and, under Weight coverage, the weights the typography tokens ask for that the family lacks.","4. Read the report. Name a missing weight and offer an alternative only when it matters: a body face without 400, 600, or italic matters, and a display face without 300 does not. A family not on Google Fonts fails the run. Fix the spelling and re-run. A pairing the stacks already hold prints \"Nothing to change\" and writes nothing. A pairing equal to the open theme's discards the buffer, and the report says so.","5. Reply with the two families, the form model behind each, the matrix verdict, and any missing weight worth naming.","","Flags: `--dry-run` reports without writing. `--no-verify` skips the network and requires a URL per family; use it only offline.","","## The pairing file","","```json","{ \"display\": \"Fraunces\", \"body\": \"Nunito Sans\" }","```","","Every slot is optional; an omitted slot keeps its family. `display` is `--font-display`, the face of the display and heading usages (`--display-font-family`, `--heading-font-family`). `body` is `--font-sans`, the face of the body usage (`--body-font-family`) and of `h4` to `h6`. `mono` is `--font-mono`, the face of the code usage (`--code-font-family`). `serif` exists when a theme needs it. `editorial` is `--font-editorial`, the long-reading face behind the editorial usage (`--editorial-font-family`). An omitted `editorial` keeps its family, so set it only when essays and articles need a face of their own. Weight coverage is reported for `display`, `body`, `serif`, and `mono`. A family bound to `editorial` gets no coverage line. A slot may be `{ \"name\": \"...\", \"url\": \"...\" }` to pin a URL. A pinned URL is not probed, so the report shows no weights for it and coverage skips it. Spell families as Google does; the CLI reports the canonical spelling.","","## Choose the body face first","","The body face is the anchor. It carries most of the words, and text faces survive small sizes where display faces do not. Pick it against the type intent, then pick the display face against it. A body face has regular, bold, and italic; low to moderate stroke contrast; open apertures; and a large x-height. A face missing any of these is a display face, whatever its name says.","","The display and heading usages ask the display face for 600 through `--display-font-weight` and `--heading-font-weight`. The body usage asks the body face for 400 through `--body-font-weight`, and `h4` to `h6` ask it for 600. The code usage asks the mono face for 400 through `--code-font-weight`. `strong` asks for 600, and `em` adds italic. Screen candidates against those weights before running.","","## The font matrix","","Classify each candidate by form model and by stroke contrast and serifs.","","| Form model | Construction | Reads as |","|---|---|---|","| **Dynamic** | diagonal stress, open apertures, written origin | open, warm, humane, timeless |","| **Rational** | vertical stress, closed apertures, drawn not written | orderly, reserved, elegant, authoritative |","| **Geometric** | monolinear, circle-and-line | technical, modern, systematic, sober |","","- One form model with different stroke contrast or serifs pairs reliably. Helvetica and Bodoni are both rational, one a linear sans and one a high-contrast serif.","- Different form models with the same stroke contrast and serifs fail. The two look alike and fight underneath. Two arbitrary sans serifs clash for this reason.","- Different on both counts works. An unmistakable difference reads as a decision.","","Many faces sit between columns. When one straddles, say so and lean on the Voice table and the x-height check.","","## Voice","","| The intent says | Type voice |","|---|---|","| editorial, literary, considered | dynamic serif display over a humanist sans body |","| elegant, luxurious, formal | rational high-contrast serif display; keep the body quiet |","| friendly, warm, approachable | dynamic sans on both sides, or a soft serif display |","| technical, systematic, precise | geometric or neo-grotesque sans; a mono for code |","| playful, informal | an expressive display face over a plain workhorse body |","| serious, institutional, trustworthy | rational sans body, rational serif display |","| quiet, minimal, unbranded | one superfamily across both slots |","","The table covers an intent that names no anchor. An anchor's row in `references/type-anchors.md` wins.","","Match the type to the design direction the color came from. A warm autumn palette under a cold geometric sans reads as two projects.","","## Shortcuts","","Use these when the request is vague or the type should stay quiet.","","- **A superfamily.** A Google Fonts family with sans and serif siblings: Alegreya, Ancizar, IBM Plex, Inria, Merriweather, Noto, PT, Roboto, Source. The catalogue moves and this list does not; `set-type` fails on a family that is gone.","- **One family across weights.**","- **Same designer or foundry.**","- **Serif display over sans body** when nothing else decides it.","","## Watch for","","- **x-height parity.** Both faces share one size scale, so a small-x-height display face over a large-x-height body face gives a heading weaker than its own body text. Check it on the rendered page.","- **Print faces at small sizes.** Delicate serifs and high stroke contrast turn to mud below 16px.","- **Every family is a download.** Two is the target; three needs a reason.","- **Sets of themes.** No two share a display face or a body face.","","## Scope","","Type only. Color, component aliases, shape, and the type scale are untouched: `set-type` writes the font entries in the buffer and carries every other value forward. `save-theme` keeps the result; Adopt ships it and rewrites `fonts.css`, which is how a build without the editor loads the family.","","## Verify","","- The CLI exits 0 and names each stack that moved, before and after.","- Each URL matches the family's weights: a range for a variable family, an enumeration for a static one, a bare URL for a single-weight face. A pinned URL is written as given.","- The app shows the new type, and the editor's Fonts section lists both families with their fallbacks.","- To revert, run the previous pairing file, or load the open theme to discard the buffer."],
41
41
  "references/type-anchors.md": ["# Type anchors: feelings, idioms, and genres","","Read this when the type intent names one of these. An anchor overrides the","Voice table in SKILL.md, because it is tuned to the same direction the color","came from.","","An entry names a form model and a role, never a family. Take the families from","the font matrix and the body-face rule in SKILL.md, and screen them against the","weights the shipped text styles ask for.","","An occasion (Christmas, Autumn, Ocean) fixes color only. Take its type from the","feeling it implies: Autumn reads cozy, Halloween reads ominous, Spring reads","tender.","","## Feelings","","| Anchor | Type voice |","|---|---|","| Joyful, exuberant, energetic | dynamic sans in both slots |","| Playful, whimsical | expressive display over a plain workhorse |","| Optimistic, hopeful | humanist sans, low contrast |","| Confident, bold | heavy rational display, plain body |","| Serene, tranquil | dynamic sans, one family |","| Tender, gentle, romantic | soft serif display over humanist sans, light weights |","| Cozy, comforting | dynamic serif over dynamic sans |","| Wistful, nostalgic, vintage, faded | rational serif display, quiet body |","| Earthy, grounded, natural | dynamic serif over humanist sans |","| Clinical, sterile, precise | one neo-grotesque, tightly set |","| Contemplative, focused | one quiet superfamily |","| Urgent, alarming | condensed grotesque, heavy |","| Tense, anxious | neo-grotesque, tightly set |","| Defiant, rebellious, loud | heavy display; a mono body works |","| Melancholy, moody, sad | rational serif display over quiet sans |","| Somber, grave, mournful | rational serif in both slots |","| Ominous, dramatic, haunted | heavy high-contrast display |","| Austere, severe, cold | one rational family |","","## Idioms, eras, and genres","","| Anchor | Type voice |","|---|---|","| Swiss, International | one rational neo-grotesque across both slots |","| Bauhaus | geometric sans, heavy display |","| Mid-century modern | dynamic sans, or a geometric display over a dynamic body |","| Art deco, opulent, luxurious | rational high-contrast serif display, quiet body |","| Terminal, phosphor | mono in both slots |","| Cyberpunk, neon noir, futuristic | wide geometric display, neutral body |","| Vaporwave | wide retro display, serif welcome |","| Y2K, bubble | geometric sans, heavy display |","| Blueprint | mono, or a technical grotesque; a mono body works here and almost nowhere else |","| Scandinavian, hygge | dynamic sans on both sides |","| Japandi, wabi-sabi | rational serif display over a quiet humanist body |","| Cottagecore, botanical | dynamic serif display over humanist sans |","| Editorial, magazine | high-contrast serif display over a text serif or humanist sans |","| Newsprint, broadsheet | rational serif in both slots |","| Risograph, zine | expressive display over a plain workhorse |","| Corporate, professional, trustworthy | rational sans body with a rational serif or same-family display |","| Brutalist | neo-grotesque or mono at heavy weight |","| Memphis, postmodern | geometric display, plain body |","| Industrial, workshop, gritty | condensed grotesque display over a plain body |"],
42
42
  },
43
43
  };