trykeyline 0.0.0-stage → 0.1.1

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 (98) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +117 -2
  4. package/bin/keyline.mjs +280 -0
  5. package/package.json +38 -4
  6. package/plugins/keyline/.claude-plugin/plugin.json +14 -0
  7. package/plugins/keyline/plugin.json +22 -0
  8. package/plugins/keyline/skills/keyline/SKILL.md +47 -0
  9. package/plugins/keyline/skills/keyline/commands/audit.md +30 -0
  10. package/plugins/keyline/skills/keyline/commands/color.md +7 -0
  11. package/plugins/keyline/skills/keyline/commands/critique.md +20 -0
  12. package/plugins/keyline/skills/keyline/commands/depth.md +7 -0
  13. package/plugins/keyline/skills/keyline/commands/init.md +16 -0
  14. package/plugins/keyline/skills/keyline/commands/layout.md +7 -0
  15. package/plugins/keyline/skills/keyline/commands/motion.md +7 -0
  16. package/plugins/keyline/skills/keyline/commands/patterns.md +7 -0
  17. package/plugins/keyline/skills/keyline/commands/polish.md +19 -0
  18. package/plugins/keyline/skills/keyline/commands/refine.md +19 -0
  19. package/plugins/keyline/skills/keyline/commands/sync.md +15 -0
  20. package/plugins/keyline/skills/keyline/commands/type.md +7 -0
  21. package/plugins/keyline/skills/keyline/commands/ux-laws.md +26 -0
  22. package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +144 -0
  23. package/plugins/keyline/skills/keyline/domains/color/guide.md +207 -0
  24. package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +174 -0
  25. package/plugins/keyline/skills/keyline/domains/color/theming-mechanics.md +121 -0
  26. package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +185 -0
  27. package/plugins/keyline/skills/keyline/domains/depth/fallbacks-and-performance.md +132 -0
  28. package/plugins/keyline/skills/keyline/domains/depth/guide.md +226 -0
  29. package/plugins/keyline/skills/keyline/domains/depth/translucent-materials.md +159 -0
  30. package/plugins/keyline/skills/keyline/domains/layout/forks-gating-and-disclosures.md +201 -0
  31. package/plugins/keyline/skills/keyline/domains/layout/guide.md +259 -0
  32. package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +193 -0
  33. package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +122 -0
  34. package/plugins/keyline/skills/keyline/domains/motion/guards.md +162 -0
  35. package/plugins/keyline/skills/keyline/domains/motion/guide.md +198 -0
  36. package/plugins/keyline/skills/keyline/domains/motion/ladder-and-curves.md +139 -0
  37. package/plugins/keyline/skills/keyline/domains/motion/patterns.md +196 -0
  38. package/plugins/keyline/skills/keyline/domains/motion/performance.md +121 -0
  39. package/plugins/keyline/skills/keyline/domains/patterns/actions-and-forms.md +204 -0
  40. package/plugins/keyline/skills/keyline/domains/patterns/agents-and-live-states.md +111 -0
  41. package/plugins/keyline/skills/keyline/domains/patterns/collections-and-dashboards.md +131 -0
  42. package/plugins/keyline/skills/keyline/domains/patterns/copy.md +123 -0
  43. package/plugins/keyline/skills/keyline/domains/patterns/guide.md +174 -0
  44. package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +70 -0
  45. package/plugins/keyline/skills/keyline/domains/type/display-tier.md +108 -0
  46. package/plugins/keyline/skills/keyline/domains/type/guide.md +227 -0
  47. package/plugins/keyline/skills/keyline/domains/type/ladders.md +169 -0
  48. package/plugins/keyline/skills/keyline/domains/type/numerals-and-loading.md +107 -0
  49. package/plugins/keyline/skills/keyline/domains/type/prose-and-emphasis.md +125 -0
  50. package/plugins/keyline/skills/keyline/domains/type/type-audit.md +87 -0
  51. package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +299 -0
  52. package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +119 -0
  53. package/plugins/keyline/skills/keyline/glass/color/guide.md +195 -0
  54. package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +136 -0
  55. package/plugins/keyline/skills/keyline/glass/color/theming.md +106 -0
  56. package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +171 -0
  57. package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +322 -0
  58. package/plugins/keyline/skills/keyline/glass/depth/blur-and-tint.md +50 -0
  59. package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +87 -0
  60. package/plugins/keyline/skills/keyline/glass/depth/guide.md +147 -0
  61. package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +37 -0
  62. package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +231 -0
  63. package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +244 -0
  64. package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +201 -0
  65. package/plugins/keyline/skills/keyline/glass/layout/guide.md +259 -0
  66. package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +134 -0
  67. package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +103 -0
  68. package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +106 -0
  69. package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +210 -0
  70. package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +176 -0
  71. package/plugins/keyline/skills/keyline/glass/motion/assets/useReveal.ts +65 -0
  72. package/plugins/keyline/skills/keyline/glass/motion/assets/useScrollTick.ts +183 -0
  73. package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +108 -0
  74. package/plugins/keyline/skills/keyline/glass/motion/guide.md +255 -0
  75. package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +81 -0
  76. package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +58 -0
  77. package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +161 -0
  78. package/plugins/keyline/skills/keyline/glass/motion/sound.md +195 -0
  79. package/plugins/keyline/skills/keyline/glass/motion/springs.md +51 -0
  80. package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +268 -0
  81. package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +416 -0
  82. package/plugins/keyline/skills/keyline/glass/type/guide.md +220 -0
  83. package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +151 -0
  84. package/plugins/keyline/skills/keyline/glass/type/scale.md +69 -0
  85. package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +61 -0
  86. package/plugins/keyline/skills/keyline/scripts/check/css.mjs +112 -0
  87. package/plugins/keyline/skills/keyline/scripts/check/design.mjs +67 -0
  88. package/plugins/keyline/skills/keyline/scripts/check/files.mjs +44 -0
  89. package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +149 -0
  90. package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +584 -0
  91. package/plugins/keyline/skills/keyline/scripts/check.mjs +297 -0
  92. package/plugins/keyline/skills/keyline/scripts/contrast.mjs +153 -0
  93. package/plugins/keyline/skills/keyline/scripts/scan.mjs +340 -0
  94. package/plugins/keyline/skills/keyline/system/design-md.md +136 -0
  95. package/plugins/keyline/skills/keyline/system/detection.md +53 -0
  96. package/plugins/keyline/skills/keyline/system/guide.md +97 -0
  97. package/plugins/keyline/skills/keyline/system/presets.md +58 -0
  98. package/plugins/keyline/skills/keyline/system/roles.md +150 -0
@@ -0,0 +1,131 @@
1
+ # Collections and dashboards
2
+
3
+ ## Which view first
4
+
5
+ | The items… | Primary view | Second view |
6
+ |---|---|---|
7
+ | Move through stages (to do → in progress → review → done) | Board: columns in the order work moves | List, grouped by status |
8
+ | Happen at a time (visits, meetings, deadlines) | Week board with Week, 3 days and Day spans | List, grouped by day |
9
+ | Are compared on many fields (members, invoices) | Table | — |
10
+ | Are browsed by place (folders, projects) | Grid or list of places | — |
11
+
12
+ **Board columns** run left to right in workflow order, each headed by its status glyph, name and count; the done column sits last and may collapse. A column that work skips is still shown, so the board reads the same every day.
13
+
14
+ **Both views share everything but the container**: data, filters, sort, row rendering and the row menu (layout law 8). A filter set on the board is still set on the list.
15
+
16
+ ## Remembering the view
17
+
18
+ On a working page the reader returns to daily, the view and span are remembered **per page**, not globally — the reader set the tasks board deliberately and the meetings list deliberately — with the switch always visible in the page header (layout law 9 owns the rule; this is the code).
19
+
20
+ ```ts
21
+ const key = (page: string) => `view:${page}`;
22
+
23
+ export function useRememberedView<T extends string>(page: string, fallback: T, opts?: { pinned?: boolean }) {
24
+ const [view, setView] = useState<T>(() => {
25
+ if (opts?.pinned) return fallback; // the default is the point
26
+ try { return (localStorage.getItem(key(page)) as T) ?? fallback; } catch { return fallback; }
27
+ });
28
+ const choose = (next: T) => {
29
+ setView(next);
30
+ if (!opts?.pinned) try { localStorage.setItem(key(page), next); } catch {}
31
+ };
32
+ return [view, choose] as const;
33
+ }
34
+ ```
35
+
36
+ **Pinned pages** are those whose job is one view: folders open on folders, a calendar opens on this week. The reader can still switch for a visit; it just isn't remembered. List the remembered and pinned pages in `DESIGN.md` under `## Patterns`.
37
+
38
+ ## Optimistic moves with rollback
39
+
40
+ ```ts
41
+ let seq = 0;
42
+
43
+ async function moveCard(id: string, to: { column: string; index: number }) {
44
+ const mine = ++seq;
45
+ const before = board.positionOf(id);
46
+ board.move(id, to); // now
47
+ try {
48
+ await api.move(id, to);
49
+ } catch (err) {
50
+ if (mine !== seq && board.positionOf(id) !== to) return; // a later move owns this card
51
+ board.move(id, before); // visibly put it back
52
+ toast.error("Couldn't move task", { action: { label: "Retry", onClick: () => moveCard(id, to) } });
53
+ }
54
+ }
55
+ ```
56
+
57
+ - **Animate the rollback** with the same travel as the move (the motion guide), so the reader sees the card go back rather than finding it there later.
58
+ - **Keep sequence**: if two moves race, only the newest one's failure rolls back.
59
+ - **Don't disable the board while saving.** Saving is background work.
60
+ - **Never optimistic**: deletes without Undo, sends, payments, invitations — anything another person sees at once or the server decides.
61
+
62
+ ## The keyboard map
63
+
64
+ | Key | Does | Where |
65
+ |---|---|---|
66
+ | ⌘K / Ctrl+K | Command palette: go to, create, run any action by name | Everywhere |
67
+ | J / K | Next / previous row or card | Lists and boards |
68
+ | Enter / O | Open the focused item | Lists and boards |
69
+ | F | Open filters | Lists and boards |
70
+ | C | Create in the current collection | Lists and boards |
71
+ | Enter | Send | Composer |
72
+ | Shift+Enter | New line | Composer |
73
+ | Escape | Close the innermost layer only | Everywhere |
74
+ | ? | Show the shortcut list | Everywhere |
75
+
76
+ ```ts
77
+ const layers: Array<() => void> = []; // push on open, pop on close
78
+
79
+ window.addEventListener("keydown", (e) => {
80
+ const t = e.target as HTMLElement;
81
+ const typing = t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName);
82
+ if (e.key === "Escape" && layers.length) { e.preventDefault(); layers.pop()!(); return; }
83
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); openPalette(); return; }
84
+ if (typing || e.metaKey || e.ctrlKey || e.altKey) return; // single keys never fire while typing
85
+ shortcuts[e.key]?.(e);
86
+ });
87
+ ```
88
+
89
+ **Every drag has a keyboard path**: the row menu's "Move to…" opens a list of columns or dates; on a focused card, a shortcut can open it directly. **Shortcuts are shown where the action lives** — beside the menu item, in the tooltip, in the palette — from the one map in `DESIGN.md`, so the three never disagree.
90
+
91
+ ## Launchers and toolbars
92
+
93
+ **A launcher asks the fewest questions the task has.** A record button that opens a form of six fields makes the most frequent action the slowest. Ask the one question that routes the result — which project, or a personal note — and default everything else, editable afterwards.
94
+
95
+ **Toolbar filters are icons with tooltips**, each with its active state shown on the icon and a count when it holds several values. Text on every filter crowds the bar and pushes the page's own actions off narrow screens. The active filters show as removable chips under the bar, where they also explain an empty result.
96
+
97
+ **Long admin lists** group into collapsible bars, closed by default:
98
+
99
+ ```
100
+ ▸ Admins 3 of 12 with access
101
+ ▸ Members 9 of 12 with access
102
+ ▸ Guests 0 of 4
103
+ ```
104
+
105
+ Each bar's summary answers what the reader would open it to check. A search opens every group it matches and highlights the match; clearing the search restores what was open before.
106
+
107
+ ## Dashboards
108
+
109
+ **The order**: overview (where things stand: a few figures), trends (which way they're moving: one or two charts over a sensible window), action items (what needs the reader: a short list, each item one click from done). Anything that answers none of the three goes — a recent-activity feed is the usual casualty, unless the product or its brief requires it; then it stays, built to earn its place (`commands/refine.md`).
110
+
111
+ **Wireframe first**, in boxes and words, and share it before building. Questions to settle on the wireframe:
112
+
113
+ - Which three to five figures, and what each one counts — its label names the noun.
114
+ - Which figures report a problem (and so take a status colour while they do) and which are plain counts.
115
+ - What the reader does next from each action item.
116
+ - What the page shows on a new account with no data — usually the action items section offering the first action.
117
+
118
+ **A long page is fine.** Hiding action items behind tabs to keep the page short moves the cost to the reader.
119
+
120
+ **A scoped overview reuses the global pipeline**:
121
+
122
+ ```ts
123
+ // one pipeline, two scopes
124
+ const overview = (sources: Source[]) => buildOverview(sources);
125
+ const global = overview(allSources);
126
+ const project = overview(allSources.filter((s) => s.projectId === id));
127
+ ```
128
+
129
+ Two queries for one view drift: a filter fixed in one, a status added to the other, and the project's overdue count stops matching the global one for the same project.
130
+
131
+ **Figures that filter.** On a list page, the figures under the header can each apply their filter to the table below. They are then controls: focusable, `aria-pressed` when active, no hover lift (the motion guide), laid out as one hairline-parted row (layout law 3).
@@ -0,0 +1,123 @@
1
+ # UI copy
2
+
3
+ Copy in a product is interface, not prose: it is scanned in passing, compared across screens, and read again a thousand times. Each rule below makes a label faster to recognise the second time.
4
+
5
+ ## Labels
6
+
7
+ | Instead of | Write | Why |
8
+ |---|---|---|
9
+ | Record a voice note | Record | The surrounding screen already says what is recorded |
10
+ | Request a feature | Help & feedback | The item also takes questions and bug reports; a label covers everything behind it |
11
+ | Submit | Send · Save changes · Create project | Say what happens, not that a form was posted |
12
+ | OK · Yes | Delete project · Sign out | The confirm button repeats the dialog's verb, so it reads correctly without the question |
13
+ | Click here to view details | View details · or make the row itself the link | "Click here" carries no information out of context |
14
+ | Are you sure you want to delete? | Delete "Riverside"? | Name the thing, drop the hedge |
15
+ | Manage Team Members | Members | Sentence case; the verb is implied by the page |
16
+ | NEW TASK | New task | Uppercase strips word shapes and reads as shouting |
17
+
18
+ **Verb first, object only when needed.** One primary action on a screen needs no object ("Save"). Two verbs that could be confused do ("Save draft", "Publish"). A destructive button always takes its object ("Delete project"), because the object is what's lost.
19
+
20
+ **Labels in menus are nouns or verbs, not both styles at once.** A menu of "Rename", "Duplicate", "Move to…", "Delete" is all verbs; "Settings", "Members", "Billing" is all places. An ellipsis means the item asks for more before acting.
21
+
22
+ **Tooltips on icon buttons say the same words a text button would**, plus the shortcut: "Filter F".
23
+
24
+ ## Sentence case, including generated labels
25
+
26
+ Every label, title, column header, tab, field name and toast is sentence case. Keep acronyms, proper nouns and third-party product names as written ("Export to CSV", "Connect GitHub").
27
+
28
+ Labels assembled from keys need a converter with an exception list, or the first acronym breaks it:
29
+
30
+ ```ts
31
+ const KEEP = new Set(["ID", "URL", "API", "CSV", "PDF", "AI", "SSO"]);
32
+
33
+ export function labelFromKey(key: string): string {
34
+ const words = key
35
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
36
+ .replace(/[_-]+/g, " ")
37
+ .trim()
38
+ .split(/\s+/);
39
+ return words
40
+ .map((w, i) => {
41
+ const upper = w.toUpperCase();
42
+ if (KEEP.has(upper)) return upper;
43
+ const lower = w.toLowerCase();
44
+ return i === 0 ? lower[0].toUpperCase() + lower.slice(1) : lower;
45
+ })
46
+ .join(" ");
47
+ }
48
+ // dueDate → "Due date"; project_id → "Project ID"; AI_summary → "AI summary"
49
+ ```
50
+
51
+ Prefer a hand-written label map for anything the reader sees often; the converter is the fallback for long-tail keys, tool names and imported fields.
52
+
53
+ ## Row second lines
54
+
55
+ The formula is **`{type} from {origin}`**, in the product's nouns:
56
+
57
+ | Instead of | Write |
58
+ |---|---|
59
+ | Issue added to Riverside | Task from Riverside |
60
+ | Created by voice note on 3 Mar | Note from site walk |
61
+ | New item in Inbox | Request from Northgate |
62
+ | Riverside · Task · Open · 2h ago · Sam | Task from Riverside — with status as a glyph, time and owner in their own slots |
63
+
64
+ - **Type** is the kind of thing, from the product's glossary. If the list holds one type only, drop it: "From Riverside".
65
+ - **Origin** is where it came from or lives: a project, a meeting, a person, a source. Pick the one the reader sorts by.
66
+ - **No verbs of history** ("added", "created", "moved"). They describe one moment, and the moment is already in the time slot.
67
+ - **No status in the sentence.** Status has a glyph and a colour (the colour guide); words would say it twice.
68
+
69
+ ## The glossary
70
+
71
+ One noun per kind of thing, everywhere: navigation, page titles, row lines, toasts, empty states, buttons. Record it in `DESIGN.md`:
72
+
73
+ ```markdown
74
+ ## Patterns
75
+
76
+ **Nouns** — task (never "issue", "item" or "action"); note (a recording once processed); project; member (never "user" in UI).
77
+ **Row second line** — `{type} from {origin}`; origin is the project, or "Personal" for notes outside one.
78
+ ```
79
+
80
+ A synonym in one corner of the product reads as a second kind of thing. When a new screen needs a word, look it up first.
81
+
82
+ ## Messages
83
+
84
+ **Toasts** say what happened, then offer the one thing to do about it:
85
+
86
+ | Case | Write |
87
+ |---|---|
88
+ | Undo window | Task moved to Done · Undo |
89
+ | Failure, retryable | Couldn't move task — Retry |
90
+ | Failure, needs the reader | Couldn't save: the project was archived · View project |
91
+ | Result off screen | Note added to Riverside · Open |
92
+
93
+ Never "Successfully …" (the outcome is the message), never an exclamation mark, never an error code without words. A move the reader watched happen needs no toast.
94
+
95
+ **Errors** say what to do, not what went wrong inside: "Enter a date after the start date", not "Invalid date range". Field errors replace the field's hint (`domains/patterns/actions-and-forms.md`).
96
+
97
+ **Empty states** — one line on what goes here, and the action that makes the first one:
98
+
99
+ | Case | Line | Action |
100
+ |---|---|---|
101
+ | New, nothing yet | Tasks from your notes and meetings land here. | New task |
102
+ | Filtered to nothing | No tasks match "Overdue" and "Assigned to me". | Clear filters |
103
+ | Search with no results | Nothing matches "riverbank". | Search all projects |
104
+
105
+ **Confirmations** — the title asks with the thing's name; the body says what is lost in plain words; the button repeats the verb:
106
+
107
+ > **Delete "Riverside"?**
108
+ > Its 48 tasks, 12 notes and every file attached to them will be deleted for everyone. This can't be undone.
109
+ > [Cancel] [Delete project]
110
+
111
+ ## Calls to action, links and sign-ups
112
+
113
+ On a landing page or an article the words are most of the interface (law 14):
114
+
115
+ | Instead of | Write | Why |
116
+ |---|---|---|
117
+ | Get started · beside Start free | Start free · Book a demo | Each call to action names what happens next, so two can't be mistaken for each other |
118
+ | Learn more · Click here | Pricing · Read the case study | Link text names its destination, and still does in a list of links read out of context |
119
+ | An email field labelled only by "you@example.com" | "Email" above the field; "One essay a month. Unsubscribe any time." below it | A placeholder vanishes on typing; the line says what arrives and how often |
120
+
121
+ ## Numbers in copy
122
+
123
+ Counts beside a noun agree with it ("1 task", "3 tasks", "No tasks" — never "0 task(s)"). Use the platform's plural rules (`Intl.PluralRules`) once there is more than one language. A count that reports a problem takes its status colour; a plain count stays in ink (the colour guide).
@@ -0,0 +1,174 @@
1
+ # Product Patterns
2
+
3
+ The other domains decide how an interface looks: its type, colour, layout, surfaces and motion. This one decides how it behaves and what it says — the label on a button, what a confirmation asks, how an agent shows its work, what happens when a save fails. **A pattern is a promise the product keeps everywhere**: once delete asks one way, every delete must, or the reader has to read every dialog to learn what it wants. Most product bugs a reviewer can't name are a pattern kept in one place and broken in another.
4
+
5
+ The laws hold under every preset. How much keyboard, how much is remembered and how much copy a screen carries are dials.
6
+
7
+ ## Before you start
8
+
9
+ Reads `DESIGN.md` `## Patterns`: the product's nouns, its destructive pattern, its shortcut map, which views are remembered. Write copy in the project's nouns.
10
+
11
+ Boundaries: how a label or figure is set is the type guide's; what colour danger, brand and status take, the colour guide's; where things sit and which views fork, the layout guide's; what a dialog or card is built from, the depth guide's; how long anything takes to move, the motion guide's. This guide decides the flow and the words, and names the owner where the rest is someone else's.
12
+
13
+ ## Laws
14
+
15
+ ### 1. A label is as short as its action, in sentence case
16
+
17
+ **A label names the action in the fewest words that still say it: a button reads its verb — "Send", "Save changes", "Record" — not the sentence around it ("Record a voice note").** Labels are scanned, not read; every extra word moves the verb away from where the eye lands, and the tail is the first thing to truncate. A confirm button repeats its dialog's verb, never "OK" or "Yes".
18
+
19
+ **Sentence case everywhere**, labels the code generates from keys included. Title case marks every word as a name, and uppercase strips word shapes.
20
+
21
+ ### 2. A row's second line says what it is and where it's from
22
+
23
+ **Under a title, the second line names the record's type and origin in the product's own nouns — "Task from Riverside", "Note from site walk" — never an event ("Issue added to Riverside").** The title says which one; the reader's next questions are what kind and from where, and those let them triage without opening it. Event phrasing describes something that happened to the record once, in a verb nobody filters by, and often swaps the noun, so the reader wonders whether an "issue" here is a "task" everywhere else. **One noun per kind of thing across the product**, listed in `DESIGN.md`. Time, owner and status take their own slots — a column, a meta line, a glyph — not the sentence. The title stays the strongest line in the row (the type guide, the colour guide).
24
+
25
+ → Before-and-after labels, when a label takes an object, generated labels and their acronyms, toasts and error copy: `domains/patterns/copy.md`
26
+
27
+ ### 3. Destructive and session-ending actions look alike, and come last
28
+
29
+ **Every action that destroys data or ends the session — delete, remove, clear, revoke, sign out — takes one treatment and one place: set apart at the end of its menu, after a divider, with no icon.** Position and treatment are recognised before words are read; when delete has a bin, clear has none and sign out sits mid-menu, the reader learns three severities that don't exist. This is **similarity** at work — things that look alike are read as one kind — and **serial position**: the first and last items of a list are the ones remembered, so the end of a menu is a place readers learn to find, and to avoid. The colour is ground, not decoration (the colour guide). Such an action is never the default focus, never beside the item clicked most, and never the primary button of a dialog that isn't about it. The reverse holds too: **nothing that isn't destructive borrows its look** — a recording or live state is not danger.
30
+
31
+ ### 4. A confirmation says what is lost, in the system's own dialog
32
+
33
+ **A confirmation states in plain words what will be lost, uses the product's standard dialog, and asks for proof of intent in proportion to the loss.** The dialog is the system's own (depth law 1). Proof runs from none for the reversible, which gets Undo, to a typed word for a project, an account or a workspace (→ `domains/patterns/actions-and-forms.md`).
34
+
35
+ **One pattern everywhere**: deleting a project and deleting an account read the same. On close, focus goes back to what opened it (depth law 12).
36
+
37
+ ### 5. An agent's work is shown step by step, and approved where it happens
38
+
39
+ **Every tool call an AI agent makes appears as a collapsible card — the tool's name in sentence case, its arguments, its result — never as a lone spinner; a step that needs approval asks inline, under its own card.** A spinner asks for trust; a card lets the reader check what was done and catch it going wrong. A modal approval hides the card and the conversation the decision depends on. The approve button names the action and takes the brand action colour; reject is muted.
40
+
41
+ The conversation carries no avatar per message: the reader's turns sit right-aligned on a tint, the agent's run full width with no fill, because the agent's output is the content. Mark AI output with one small mark in brand, not a robot face or a coloured bubble (the colour guide). The panel is non-modal, so the reader keeps working beside it.
42
+
43
+ ### 6. Live output separates the settled from the forming
44
+
45
+ **A streaming state — a live transcript, an answer being generated — shows committed text in ink and the phrase still forming muted, with a caret; keeps the newest line in view under a top fade; and lets the reader scroll back without being dragged down.** Words that may still change shouldn't look final; ink that rewrites itself reads as a glitch. Follow the bottom while the reader is there; once they scroll up, stop following and offer "Jump to latest". Screen readers hear committed segments only.
46
+
47
+ **Paused means still** (motion law 1), and the paused state offers what the reader wants next — the text so far, Copy, Resume. One "working now" signal per screen; how a live control turns into Stop is the motion guide's.
48
+
49
+ → Tool-call cards, inline approvals, conversation layout, follow-the-bottom and announcing: `domains/patterns/agents-and-live-states.md`
50
+
51
+ ### 7. Dense tools are keyboard-first, and Escape closes one layer
52
+
53
+ **In a tool used all day, every frequent action has a key — ⌘K or Ctrl+K for a command palette, J and K to move through a list, a single key to filter, Enter to send and Shift+Enter for a new line — and Escape closes only the innermost layer.** The pointer is the slow path for an action repeated a hundred times a day, and an Escape that closes the dialog and the panel under it throws away context. Single-letter keys never fire while focus is in a text field. **Every drag has a keyboard path** ("Move to…" in the row menu), so dragging is a shortcut, never the only way. A shortcut is shown where its action lives — menu item, tooltip, palette — from one map recorded in `DESIGN.md`.
54
+
55
+ ### 8. Few choices up front; the rest one step away
56
+
57
+ **A launcher offers only the choices the task has, and a toolbar or a long list holds back what isn't needed every time.** Every option on the path of the most frequent action is something to read before starting: a record launcher asks one question with two answers — which project, or a personal note — not a form. Toolbar filters are icons with tooltips, not a text label on every control. A long admin list groups into collapsed bars, closed by default, each with a summary of what the reader would open it to check ("Admins · 3 of 12 with access"), and a search opens every group it matches.
58
+
59
+ **Hick's law** is why, and **progressive disclosure** the cure — the common path visible, the rest one step away (layout law 11 keeps it in the markup). By **Miller's law**, chunk long lists and long strings — three groups of four nav items, `4242 4242 4242 4242` — but don't treat "seven" as a cap that cuts what the product needs.
60
+
61
+ ### 9. An empty state offers the first action
62
+
63
+ **Where the user's own data is empty, say in one line what goes here and offer the one action that creates the first item — never "No items found."** "No … found" reports a failure and sends the reader looking for the create button elsewhere. Empty because filtered is a different state: name the filter and offer to clear it. Owner-curated collections don't get an empty state at all; they disappear (layout law 10).
64
+
65
+ ### 10. Work is shown the way it moves
66
+
67
+ **Items that move through stages get a board whose columns run in the order work moves, each headed by its status glyph; items placed in time get a week board with Week, 3 days and Day spans; the list stays as the second view of the same items.** The board makes the state of the work the layout; a list makes it a column to sort by. Both share data, filters and rows (layout law 8).
68
+
69
+ Whether the view and span are remembered is layout law 9; list each page that remembers under `## Patterns`. Status changes by drag (law 11) and by the keyboard path (law 7).
70
+
71
+ ### 11. A move happens now and rolls back visibly
72
+
73
+ **A drag, toggle, rename or reorder updates the screen at once, saves in the background, and on failure puts the item back where it was with a toast that says what failed and offers Retry.** Waiting on the network for a move the reader can already see makes every action feel broken. The rollback has to be seen: a card that silently snaps back reads as a missed drop, and one that silently stays reads as saved. Keep the previous state until the save confirms, and drop responses older than the latest change. **Not for the irreversible** (law 4) **or for results the server decides** — sending, paying, anything another person sees at once.
74
+
75
+ → Board and list, remembered views, rollback code, the shortcut map, launchers and grouped lists: `domains/patterns/collections-and-dashboards.md`
76
+
77
+ ### 12. A dashboard is overview, trends and action items, from one pipeline
78
+
79
+ **A dashboard answers three questions in order — where things stand (overview), which way they're moving (trends), what needs the reader (action items) — and drops anything nobody acts on.** A recent-activity feed nobody clicks is a column of noise next to the items that need a decision. **Wireframe it first and share the wireframe**: arguing about boxes is cheaper than arguing about a built page. A long page is fine; a page that hides what needs action to stay short is not. **A scoped overview runs the global overview's pipeline with its sources filtered**, never a parallel query, so the two can't disagree. Figures under a header can double as the table's filters (stat rows: layout law 3; figure colours: the colour guide). A panel the product or its brief requires stays; make it earn its place, don't cut it (`commands/refine.md`).
80
+
81
+ ### 13. A form leads with the field that decides the rest
82
+
83
+ **The field whose answer changes what follows comes first and full width — a category before the free text — and every field has its label above, its hint below, and its error in the hint's place.** Asking for the free text first makes the reader write before they know what kind of thing they're writing, then reshuffles the form under them. An error replaces the hint rather than adding a line, so nothing jumps; a failure of the whole form is one line with an icon above the button; the one primary button reads the verb.
84
+
85
+ **A sign-in screen is a form with nothing else**: unboxed in its own column of about 360px — title, one muted line, fields, one brand button — with an art panel taking the other half from a wide breakpoint, and the form alone on a phone. List the auth routes as an `expressive` area in `DESIGN.md`'s `## Profile`, since that is the only place the art may move; elsewhere it holds one still frame (the panel: the depth guide's art-panel recipe; its motion: the motion guide's ambient class; 16px inputs on phones: the type guide).
86
+
87
+ → The confirmation dialog, undo toasts, field anatomy and the sign-in layout in code: `domains/patterns/actions-and-forms.md`
88
+
89
+ ### 14. Every element keeps the promise its form makes
90
+
91
+ **What an element looks like says what it does, and it does that: a play icon opens a video, a link goes somewhere, and anything that doesn't act doesn't react.** Landing pages and articles are where these promises break, because they are built to look a certain way before anyone decides what each piece does, and a reader who clicks a dead logo or a silent play icon stops trusting the rest. A logo that isn't a link takes no hover state; an email address is a `mailto:` link; "Copy link" is a button, not a link to the page already open; link text names its destination ("Pricing", not "Learn more"). A call to action names what happens next — "Start free", "Book a demo" — so two on one page can't be mistaken for each other, as "Get started" beside "Start free" can. A subscribe or sign-up field has a visible label and says what will arrive and how often.
92
+
93
+ **Conventions are promises too** (Jakob's law): the logo goes home, search sits behind ⌘K or a magnifier, the account is at the end of the navigation, a link is underlined or obviously a link. Break a convention only for a gain the reader will feel on first use, and never for novelty alone.
94
+
95
+ → Calls to action, link text and sign-up lines: `domains/patterns/copy.md`
96
+
97
+ ### 15. Every action answers at once, and anything slow says what it's doing
98
+
99
+ **An input is acknowledged within 100ms — a press state, a toggle flipping, a row moving — and any result that takes longer than about 400ms shows what is happening in the place it was asked for.** 400ms is the Doherty threshold. The acknowledgement is instant and local (press feedback is the motion guide's), the slow part runs in the background where it can (law 11), and the wait is honest where it can't, its form set by how long it runs (→ `domains/patterns/actions-and-forms.md`, pending states).
100
+
101
+ **The loop closes where it opened**: the result appears where the action was taken, and what changed is visible without reading a toast. A save that succeeds silently, a filter that applies off screen, or an error reported in a corner the reader isn't looking at breaks the loop, and readers repeat the action to find out what happened.
102
+
103
+ ### 16. Input is forgiving, and the product does the remembering
104
+
105
+ **Accept what people type in any reasonable form and store one canonical form; show choices rather than ask for things from memory; and let the product carry the complexity the reader would otherwise carry.** Three laws meet here:
106
+
107
+ - **Postel's law** — spaces, dashes, case and a trailing space are valid input, normalised, never rejected for format. Errors are for what's actually wrong, and say how to fix it.
108
+ - **Recognition over recall** — recent and frequent items lead a picker; a command palette lists what it can do before the reader types; labels stay visible above fields; a filter shows its current value, not just its name.
109
+ - **Tesler's law** — default to the likely answer, infer what can be inferred, and never ask for what the product already knows.
110
+
111
+ ### 17. A flow shows how near the end is, and the end is designed
112
+
113
+ **A task of several steps shows where the reader is and how much is left, and its last screen is built as carefully as its first.** A visible count pulls the reader through (the goal-gradient effect), and the end is what is remembered (the peak-end rule): it says what happened in the product's nouns, shows the result, and offers the next action. A failure at the peak — a payment declined, an upload lost — gets the most careful copy in the product, never a code.
114
+
115
+ → Pending states, skeletons, forgiving inputs and progress in code: `domains/patterns/actions-and-forms.md`
116
+
117
+ ## Dials
118
+
119
+ | Dial | editorial | product | expressive |
120
+ |---|---|---|---|
121
+ | Label length | 1–3 words | 1–2 words, verb first | A call to action may run to a short phrase |
122
+ | Keyboard | Tab order, skip link, Escape | Palette, list movement, single-key actions, shown in menus | Tab order and Escape |
123
+ | View memory | Reset on arrival | Remembered per page on daily working pages (layout law 9) | Reset on arrival |
124
+ | Optimistic updates | Rare | Default for moves, toggles, renames, reorders | Rare |
125
+ | Proof of intent for large deletes | Typed word | Typed word | Typed word |
126
+ | Empty-state copy | One sentence and the action | One line and the action | A headline, a line and the action |
127
+ | Agent steps | Collapsed to one line each | Collapsed when done; running and failed steps open | Collapsed to one line each |
128
+ | Toasts | Failures and Undo only | Failures, Undo, and results off screen | Failures and Undo only |
129
+
130
+ The `glass` preset carries no pattern dials; take them from the preset nearest the reader.
131
+
132
+ **Keyboard.** Product readers repeat; editorial and expressive readers arrive, read and leave, so a palette there is a feature nobody discovers. Every preset keeps the floor: everything reachable by Tab, Escape closing one layer.
133
+
134
+ **Toasts.** A toast confirms what the reader can't see happen. A move they watched needs none; a save that failed, an undo window, or a result on another page does.
135
+
136
+ ## Iterating on feedback
137
+
138
+ Most product feedback is one step: "one step lighter", "25% smaller", "to 16px". Make exactly that change, at the size asked, sweep everywhere the rule reaches and report what moved, then record it under `## Decisions` (→ `domains/patterns/iteration.md`: the step table, the sweep, references and the preview harness). Wireframe a new page type before building it, and share the wireframe.
139
+
140
+ ## Auditing existing code
141
+
142
+ | Law | Look for | Evidence |
143
+ |---|---|---|
144
+ | 1 | Title case or uppercase labels; buttons that are sentences; "OK", "Yes", "Submit"; generated labels shown raw | Label, file, count |
145
+ | 2 | Second lines built from event verbs; two nouns for one kind of thing | The template string; the noun pairs |
146
+ | 3 | Destructive items with icons, mid-menu or styled differently from each other | Menu, item, treatment |
147
+ | 4 | Bespoke confirm dialogs; `window.confirm`; Cancel focused for large deletes; no pending label; focus lost on close | Component, and which step is missing |
148
+ | 5 | Spinners standing in for agent steps; approvals in modals; per-message avatars | Component |
149
+ | 6 | Interim text styled as final; auto-scroll that fights the reader; ambient motion while paused; interim text in a live region | Component and the behaviour |
150
+ | 7 | Frequent actions with no key; drag with no keyboard path; Escape handlers that close more than one layer; single keys firing in inputs | Action, and its key or "none" |
151
+ | 8 | Launchers with more than two or three choices; text-labelled filter rows; long flat admin lists | Screen, choice count |
152
+ | 9 | "No … found" / "No … yet" on user data; empty states with no action | Route, and which case |
153
+ | 10 | Status work with only a list; columns out of workflow order; view memory unrecorded | Page and view key |
154
+ | 11 | Moves awaiting the server; silent rollbacks; optimistic sends or payments | Mutation and its failure path |
155
+ | 12 | Panels nobody acts on; scoped views with their own query | Panel; the two queries |
156
+ | 13 | Free text before the field that decides it; errors added beside hints; several primary buttons | Form, field order |
157
+ | 14 | Play icons with no video; hover on logos that aren't links; plain-text email addresses; copy controls built as links; "Learn more", "Click here", "Get started" beside a second call to action; sign-up fields with a placeholder for a label or no line on what arrives; conventions broken for novelty — a logo that doesn't go home, search hidden in a menu | Element, and what it promises against what it does |
158
+ | 15 | Async actions with no pending label; centred spinners over blank regions; long jobs with no progress or cancel; results shown away from where they were asked for; silent saves | Action, its wait, and what the reader sees meanwhile |
159
+ | 16 | Inputs that reject spaces, dashes, case or a trailing space; placeholders as labels; pickers with no recent items; questions the product could answer itself | Field, the input it rejects or the question it needn't ask |
160
+ | 17 | Multi-step flows with no position or count; a last screen that says only "Success" or "Done"; failure copy with a code and no next step | Flow, step, and its end state |
161
+
162
+ Click through every flow once with the keyboard alone, and once with the network throttled to failure.
163
+
164
+ ## Writing back to DESIGN.md
165
+
166
+ - **`## Patterns`**: the product's nouns, one per kind of thing; the second-line formula; the destructive items and the proof each needs; the shortcut map; which pages remember their view and which default wins; the agent-step conventions.
167
+
168
+ ## Checklist
169
+
170
+ - [ ] Large deletes show "Deleting…" and hold the dialog open until done; focus returns to the opener.
171
+ - [ ] Filtered-empty names the filter and offers to clear it.
172
+ - [ ] Sends, payments and deletes are never optimistic.
173
+ - [ ] A sign-in route whose art moves is listed as an `expressive` area.
174
+ - [ ] Long lists and long strings are chunked.
@@ -0,0 +1,70 @@
1
+ # Iterating on feedback
2
+
3
+ A product UI is mostly finished in review, not in the first build. The feedback that arrives is small and specific, and most of the work is applying it everywhere it reaches.
4
+
5
+ ## One step at a time
6
+
7
+ | Feedback | Change | Not |
8
+ |---|---|---|
9
+ | "One step lighter" | The next weight down on the existing ladder, in that context | Re-picking the weight scale |
10
+ | "Make it 25% smaller" | Scale that element by 0.75 and snap to the nearest step | Fitting more per row |
11
+ | "Bring the title to 16px" | That role to 16px, everywhere the role is used | That one instance |
12
+ | "Too cluttered" | Spacing and grouping first (the layout guide) | Colour or type |
13
+ | "Not symmetric" | Match the sibling that looks right | A third variant |
14
+ | "Looks heavy everywhere" | Step weights down one, context by context, and show the result | A new font |
15
+
16
+ When feedback is ambiguous in size, ask one question with a recommendation: "One step lighter (500 → 400), or only the card titles?"
17
+
18
+ ## The sweep
19
+
20
+ A change to a shared rule — a role's size, a weight, a colour job, a spacing step, a component's variant — isn't done when the screen in front of you looks right.
21
+
22
+ 1. **Find every use.** Grep the token, the class, the component and any raw value that should have been the token (`text-sm` where the role was meant). Raw values are where partial updates hide.
23
+ 2. **Migrate raw values to the role** while you're there, so the next change is one edit.
24
+ 3. **Walk every route** in the preview harness, light and dark, at a phone and a desktop width.
25
+ 4. **Report what moved**, one line each, and what you deliberately left:
26
+
27
+ ```
28
+ Card title weight 600 → 500 (role `type.card-title`):
29
+ - Board, list, inbox, search results: updated via the role
30
+ - Calendar event chips: were raw `font-semibold`, migrated to the role
31
+ - Dashboard figures: left at 600 — figures aren't titles (say if you want them lighter)
32
+ ```
33
+
34
+ 5. **Ask before calling a page done.** Pages the owner has said are theirs are left alone, even when the sweep would touch them; list them in the report instead.
35
+
36
+ ## The decision log
37
+
38
+ Every decision goes under `## Decisions` in `DESIGN.md`, in the format and with the rules `system/design-md.md` sets.
39
+
40
+ ## Studying a reference
41
+
42
+ When the owner points at a product they like, study **how it structures information**, not how it looks:
43
+
44
+ | Look at | Note |
45
+ |---|---|
46
+ | Icons | Where they appear and where they don't — beside nav items but not menu items? On buttons or only in toolbars? |
47
+ | Tags and chips | What gets one and what is plain text |
48
+ | Tables | Frame or none; header treatment; row height; what's in the first column; where row actions live |
49
+ | Boxes | What is a card, what sits on the page, what is grouped by space alone |
50
+ | Alignment | What lines up with what — titles with table edges, icons with text |
51
+ | Density | Row heights, gaps, how much fits above the fold |
52
+ | Copy | Label length, case, how second lines and empty states read |
53
+
54
+ Then apply the structure in the project's own tokens. Copying the look imports a second system; copying the structure keeps one.
55
+
56
+ ## Wireframe first
57
+
58
+ A new page type — a dashboard, a settings area, a board — starts as a wireframe: boxes and words, no colour, shareable as a link or an image. It settles what's on the page and in what order, which is the expensive argument, before any styling makes it costly to change. Keep the wireframe beside the build until the page ships.
59
+
60
+ ## The preview harness
61
+
62
+ The real app, on mock data, with every route reachable and no backend:
63
+
64
+ - **A data layer the app already calls**, swapped at the boundary — a mock adapter, a request interceptor, or fixtures behind the same hooks — never a fork of the components.
65
+ - **Fixtures that cover the states**: empty, one item, many, very long names, every status, an error, a slow response. A switch per state (a query parameter or a dev menu) lets a review jump to it.
66
+ - **A route index**: one page listing every route and state, so a sweep can visit all of them in order.
67
+ - **Typed against the real models**, so the harness breaks at build time when the data shape changes rather than drifting quietly.
68
+ - **Signed in by default** with a demo identity, and sign-out returning to the sign-in screen, so the auth screens are reviewable too.
69
+
70
+ With it, a whole-product sweep or a rendered audit (the type guide, the layout guide) takes minutes instead of needing a staging account and real data.
@@ -0,0 +1,108 @@
1
+ # The display tier
2
+
3
+ Only `expressive` has one: the single oversized headline on a landing or campaign page, read first and perhaps alone. This file derives its size and covers leading, tracking, weight, wrapping and loading at that size.
4
+
5
+ ## Why it isn't body × ratio
6
+
7
+ Every other role is sized against its neighbours; a display headline is read against **the space it fills**, holding a line of a certain length across a certain column. Taken from the body by ratio, it is timid in a wide hero or five lines on a phone. The inputs are the column and the headline, not the ladder.
8
+
9
+ ## The derivation
10
+
11
+ **1. Decide characters per line.** Roughly 12–24, over two or three lines at most; under 12 every break is a design decision, over 24 it's a large paragraph. Pick a target for each end:
12
+
13
+ - widest: 18–24 characters (two lines of a 40-character headline)
14
+ - narrowest: 12–16 characters (three lines on a phone)
15
+
16
+ Use real headlines; from a CMS, the 90th-percentile length, not the average — the long ones break the hero.
17
+
18
+ **2. Measure the family's average advance**, in `em`, at the weight you will set. Grotesques run about 0.5–0.55em, condensed faces 0.4–0.45, wide faces 0.6 and over — but measure, with real headline copy for its real mix of spaces, capitals and round letters:
19
+
20
+ ```js
21
+ // After the font has loaded.
22
+ const ctx = document.createElement("canvas").getContext("2d");
23
+ ctx.font = `300 100px "Your Display Family"`; // the real weight; canvas can't read var()
24
+ const sample = "Invoices that chase themselves, so you don't have to";
25
+ const advance = ctx.measureText(sample).width / sample.length / 100; // em per character
26
+ ```
27
+
28
+ **3. Divide.** For each end of the range:
29
+
30
+ ```
31
+ size = column width ÷ (characters per line × advance)
32
+ ```
33
+
34
+ A 960px desktop column at 20 characters and 0.52em: 960 ÷ 10.4 ≈ 92px.
35
+ A 343px phone column (375px less two 16px gutters) at 14 characters: 343 ÷ 7.28 ≈ 47px.
36
+
37
+ **4. Interpolate, with a `rem` intercept.** Between the viewport where the narrow column applies (`v₁`, size `s₁`) and the one where the wide column is reached (`v₂`, size `s₂`):
38
+
39
+ ```
40
+ slope = (s₂ − s₁) ÷ (v₂ − v₁) → the vw coefficient × 100
41
+ intercept = s₁ − slope × v₁ → in px, then ÷ 16 for rem
42
+ ```
43
+
44
+ From 47px at 375 to 92px at 1280: slope 0.0497 (4.97vw), intercept 28.35px (1.772rem).
45
+
46
+ ```css
47
+ --text-display: clamp(2.9375rem, 1.772rem + 4.97vw, 5.75rem);
48
+ ```
49
+
50
+ The intercept is required: text sized in `vw` alone ignores zoom and fails WCAG 1.4.4. Keep the maximum within about 2.5 × the minimum, or a 200% zoom can't double the size inside the clamp.
51
+
52
+ **5. If the hero isn't the viewport, use container units.** In a card, split hero or sidebar layout, set `container-type: inline-size` on the hero and use `cqi` for `vw`, against container widths. In a split hero, derive from the text column, but the display must still clear 1.5 × `type.heading`; if the column can't hold that, widen the text column or stack the hero, rather than shrinking the display.
53
+
54
+ **6. Check the gap to the title.** At both ends display is at least 1.5 × `type.title`; closer, it reads as "the title got bigger". If the gap is too small, lower the title rather than raising display: display is derived, the title is a choice. On a page whose `h1` is the display — a landing page with no separate title — there is no `type.title`; check display against `type.heading` instead, at least 1.5× at every width.
55
+
56
+ ### The same headline in three families
57
+
58
+ Column and character targets held constant (960px at 20 characters, 343px at 14):
59
+
60
+ | Family | Advance | Phone | Desktop | `clamp()` (375 → 1280) |
61
+ |---|---|---|---|---|
62
+ | Condensed grotesque | 0.43em | 57px | 112px | `clamp(3.5625rem, 2.14rem + 6.08vw, 7rem)` |
63
+ | Neutral grotesque | 0.52em | 47px | 92px | `clamp(2.9375rem, 1.772rem + 4.97vw, 5.75rem)` |
64
+ | Wide serif | 0.60em | 41px | 80px | `clamp(2.5625rem, 1.55rem + 4.31vw, 5rem)` |
65
+
66
+ A condensed face earns a larger size for the same line, a wide one a smaller; a ratio off the body would have given all three one number and broken two.
67
+
68
+ ## Leading at display sizes
69
+
70
+ 0.95–1.05: the x-height makes a tight line box read generously, and at body leading a headline reads as separate lines, not one object. Before going below 1:
71
+
72
+ - **Accents and descenders.** Under 1, a line box can clip diacritics above and descenders below, especially with `overflow: hidden` on the hero. Test with "Ågj" and a ringed capital.
73
+ - **Mixed scripts.** A headline that may be translated into a script with taller marks takes the top of the range.
74
+
75
+ ## Tracking at display sizes
76
+
77
+ −0.03 to −0.045em, in `em` so it scales with the fluid size; looser families and larger sizes need more. A variable family with an optical-size (`opsz`) axis tightens and thins itself: set `font-optical-sizing: auto` and start at the gentle end.
78
+
79
+ Never give the title or heading display tracking "for consistency": tracking corrects for size, and the title is a different size.
80
+
81
+ ## Weight at display sizes
82
+
83
+ `weight.display` is its own role because the right weight at 90px is rarely right at 20. Above about 48px a 400 looks heavy, and a 300 — illegitimate wherever text is read at length — belongs here; a heavy weight as brand voice is also defensible. Either way it is one weight, one job, never reused below the tier.
84
+
85
+ A separate `family.display` is allowed in `expressive` for a register the text family can't carry — a wordmark's voice, a serif headline over sans text. One family, one role: in section headings, it makes two display tiers.
86
+
87
+ ## Wrapping
88
+
89
+ `text-wrap: balance` on display and title, so a headline doesn't end in an orphan. No manual `<br>`s: right at one width, wrong at every other. A phrase that must not break gets a non-breaking space.
90
+
91
+ ## Loading a display face
92
+
93
+ A display swap is the most visible there is: a two-line headline can become three and push the hero down. Everything in `numerals-and-loading.md` applies with more force:
94
+
95
+ - Preload the display file for the hero's weight; it is on the first frame's critical path.
96
+ - Metric-match its fallback on `size-adjust` **and** the vertical overrides; a width-only match still changes line height.
97
+ - Subset it. A dozen words need Latin at most, often less.
98
+
99
+ ## One display moment
100
+
101
+ Law 3: there is one loudest thing. Every section opener at display size flattens the hierarchy — all shout, none is heard. Openers are `type.heading`; for presence, give them space, not size.
102
+
103
+ ## Failures worth recognising
104
+
105
+ - **The big title.** Display at 1.2 × title: hero and page heading blur, and the page looks under-designed, not restrained.
106
+ - **The phone wall.** Derived at desktop only, clamped at 64px minimum: a 40-character headline fills a phone with five lines.
107
+ - **The zoom fail.** `font-size: 7vw`. Right at every width, unchanged at 200% zoom.
108
+ - **The reflow.** No metric-matched fallback: the headline swaps, gains a line, and the call to action jumps 90px as the visitor reaches for it.