trykeyline 0.1.0 → 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 (49) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/LICENSE +0 -16
  3. package/package.json +1 -1
  4. package/plugins/keyline/.claude-plugin/plugin.json +1 -1
  5. package/plugins/keyline/plugin.json +22 -0
  6. package/plugins/keyline/skills/keyline/SKILL.md +22 -32
  7. package/plugins/keyline/skills/keyline/commands/audit.md +8 -3
  8. package/plugins/keyline/skills/keyline/commands/critique.md +3 -26
  9. package/plugins/keyline/skills/keyline/commands/init.md +5 -4
  10. package/plugins/keyline/skills/keyline/commands/polish.md +2 -2
  11. package/plugins/keyline/skills/keyline/commands/refine.md +6 -16
  12. package/plugins/keyline/skills/keyline/commands/sync.md +1 -3
  13. package/plugins/keyline/skills/keyline/commands/ux-laws.md +26 -0
  14. package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +1 -1
  15. package/plugins/keyline/skills/keyline/domains/color/guide.md +10 -46
  16. package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +2 -2
  17. package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +2 -8
  18. package/plugins/keyline/skills/keyline/domains/depth/guide.md +12 -41
  19. package/plugins/keyline/skills/keyline/domains/layout/guide.md +31 -56
  20. package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +16 -16
  21. package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +2 -2
  22. package/plugins/keyline/skills/keyline/domains/motion/guide.md +11 -47
  23. package/plugins/keyline/skills/keyline/domains/motion/patterns.md +1 -1
  24. package/plugins/keyline/skills/keyline/domains/motion/performance.md +1 -1
  25. package/plugins/keyline/skills/keyline/domains/patterns/guide.md +20 -62
  26. package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +1 -9
  27. package/plugins/keyline/skills/keyline/domains/type/guide.md +12 -36
  28. package/plugins/keyline/skills/keyline/domains/type/type-audit.md +5 -8
  29. package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +5 -25
  30. package/plugins/keyline/skills/keyline/glass/color/guide.md +2 -29
  31. package/plugins/keyline/skills/keyline/glass/color/theming.md +1 -7
  32. package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +1 -1
  33. package/plugins/keyline/skills/keyline/glass/depth/guide.md +4 -73
  34. package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +5 -3
  35. package/plugins/keyline/skills/keyline/glass/layout/guide.md +8 -7
  36. package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +4 -4
  37. package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +3 -2
  38. package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +1 -1
  39. package/plugins/keyline/skills/keyline/glass/motion/guide.md +7 -38
  40. package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +1 -1
  41. package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +1 -1
  42. package/plugins/keyline/skills/keyline/glass/motion/sound.md +4 -1
  43. package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +3 -1
  44. package/plugins/keyline/skills/keyline/glass/type/guide.md +2 -4
  45. package/plugins/keyline/skills/keyline/scripts/check.mjs +4 -1
  46. package/plugins/keyline/skills/keyline/system/design-md.md +4 -4
  47. package/plugins/keyline/skills/keyline/system/guide.md +3 -7
  48. package/plugins/keyline/skills/keyline/system/presets.md +16 -16
  49. package/plugins/keyline/skills/keyline/system/roles.md +8 -8
@@ -4,14 +4,9 @@ Every surface claims a place: a table row, that it is part of the page; a sticky
4
4
 
5
5
  ## Before you start
6
6
 
7
- Read `DESIGN.md` at the project root: `## Profile` for the preset and any areas on another one, `## Surfaces` for the project's name for each role and the decisions already made. Reason in roles; write code in the project's names.
7
+ Reads `DESIGN.md` `## Surfaces`.
8
8
 
9
- - **No `DESIGN.md`:** run `/keyline init` for this domain only, do the task, then recommend the full setup.
10
- - **A role you need has no row:** reuse a value already doing that job; otherwise add one in the project's naming style and record it in `DESIGN.md`.
11
- - **The task would break a law or move a dial off its preset:** say so and ask; if the user confirms, record it with its reason under `## Decisions`.
12
- - **Preset `glass`:** the values are in `DESIGN.md`; `glass/depth/guide.md` has the exact recipes behind them.
13
-
14
- Boundaries: the radius ladder and nesting rule, hairline weights and the z-index ladder are the layout guide's; surface, edge and tint colours, and why translucent tints are runtime values, the colour guide's; the timing of anything that lifts, glows or opens, the motion guide's; a confirmation's flow and a sign-in page's layout, the patterns guide'. This guide picks which a surface takes and checks they agree.
9
+ Boundaries: the radius ladder and nesting rule, hairline weights and the z-index ladder are the layout guide's; surface, edge and tint colours, and why translucent tints are runtime values, the colour guide's; the timing of anything that lifts, glows or opens, the motion guide's; a confirmation's flow and a sign-in page's layout, the patterns guide's. This guide picks which a surface takes and checks they agree.
15
10
 
16
11
  ## Laws
17
12
 
@@ -23,17 +18,17 @@ Mixing parts is the bug the ladder prevents: a card given the menu's shadow on h
23
18
 
24
19
  A level is also one component. Every dialog is the system's dialog, a destructive confirmation included: a one-off with its own red icon circle, red heading and padding reads as out of the system however right its level. The danger goes on the one action that does it (the colour guide); surface, edge, shadow and scrim stay the overlay's.
25
20
 
26
- The ladder has only the rungs this project uses; the dial lists the most a preset may have. The Glass preset's fourth translucent tier, for "a menu over a surface you already know", never found a user. A dead rung is worse than an absent one: the next person assumes it is load-bearing.
21
+ The ladder has only the rungs this project uses; the dial lists the most a preset may have. A dead rung is worse than an absent one: the next person assumes it is load-bearing.
27
22
 
28
23
  ### 2. A card is one object, and never holds another card
29
24
 
30
- **A card — a surface with its own edge — is reserved for one object the reader acts on or takes in whole, and a card never holds another card.** A box says "this is a thing". Put one round every section, stat and list and every region claims to be a thing, so none is; each nesting adds an edge, a padding and a radius, and a screen built of boxes in boxes reads as a template however good its type. **Which regions get a surface at all is decided once, in layout law 3's table** — peer sections take a line, a settings group takes a panel, regions on a tinted ground are surfaces, an existing design's panels stay. This law builds what that table gives a surface, and keeps everything else off one.
25
+ **A card — a surface with its own edge — is reserved for one object the reader acts on or takes in whole, and a card never holds another card.** A box says "this is a thing". Put one round every section, stat and list and every region claims to be a thing, so none is; each nesting adds an edge, a padding and a radius, and a screen built of boxes in boxes reads as a template however good its type. **Which regions get a surface at all is decided once, in layout law 3's table**; this law builds what that table gives a surface, and keeps everything else off one.
31
26
 
32
27
  - **One object** — a decision awaiting approval, a document, a side rail — is a card. A rail with two topics is one card in two parts, parted by a divider; several items of one kind (three pending approvals) share one card the same way, not a card each.
33
28
  - **A section panel** (from layout law 3) is `elevation.flat`: an edge and no shadow, because it doesn't float. Nothing inside it is boxed again: its rows part by `border.divider`.
34
29
  - **A note** — an aside or caveat — takes a hairline rule on its leading edge, not a tinted box.
35
30
 
36
- A tinted strip behind a group label, a bordered callout and a tile per figure are boxes by other names. The test: take the edge away. If the object still reads as one thing from space and its heading, the edge was decoration. Then check the measure: columns of text left under about 45 characters a line want fewer columns (layout law 1).
31
+ The quiet devices that replace a box are layout law 3's. The test: take the edge away. If the object still reads as one thing from space and its heading, the edge was decoration. Then check the measure: columns of text left under layout law 1's floor want fewer columns.
37
32
 
38
33
  ### 3. Depth needs an edge as well as a shadow
39
34
 
@@ -65,7 +60,7 @@ Nobody spots a `0.04` that became `0.05` in one of five copies, so **a layered s
65
60
  - **Video, canvas or anything animating** — the filter re-runs every backdrop frame.
66
61
  - **A preset that disallows it** (Dials) — in fluent product tools borders and opaque surfaces do the work, and translucency is decoration that costs contrast.
67
62
 
68
- The Glass preset, built around the material, uses it in four places, all floating chrome: the ceiling, not the target.
63
+ Even a preset built around the material uses it in four places, all floating chrome: the ceiling, not the target.
69
64
 
70
65
  ### 7. Blur scales with uncertainty about the backdrop
71
66
 
@@ -85,7 +80,7 @@ Role-tier ranges, not dial values; the preset picks which tiers exist (Dials). C
85
80
 
86
81
  ### 9. The tint carries legibility when the blur can't
87
82
 
88
- **A translucent surface is legible from its tint alone; the blur is a bonus.** A `transform`, `filter`, `perspective`, `contain: paint` or `will-change` on an ancestor weakens or breaks `backdrop-filter` beneath it — a menu's scaling wrapper does, for the whole animation — and a 72% tint without its blur is unreadable over a busy page. Animate one element and filter another; failing that, thicken the tint, as the Glass preset's floating panel does at 97%. The same ancestors trap `position: fixed` descendants, so check the whole ancestor chain when either misbehaves.
83
+ **A translucent surface is legible from its tint alone; the blur is a bonus.** A transformed or filtered ancestor — a menu's scaling wrapper, for the whole animation — weakens or breaks `backdrop-filter` beneath it, and a 72% tint without its blur is unreadable over a busy page. Animate one element and filter another; failing that, thicken the tint to the defensive tier (→ `domains/depth/translucent-materials.md`, the wrapper and the surface).
89
84
 
90
85
  ### 10. Translucent text is checked against the worst backdrop
91
86
 
@@ -109,7 +104,7 @@ Modal also hands back what it took: **on close, focus returns to whatever opened
109
104
 
110
105
  ### 13. Gradients on resizing elements are anchored in pixels
111
106
 
112
- **A gradient stroke or fill on an element that changes size places its stops in absolute units, and a gradient stroke is two background layers on one box.** Percentage stops rescale with the box: a Glass preset panel opening from a 46px bar to 250px ran its whole navy-to-pale sweep inside the closed bar — louder closed than open — and shimmered as it grew. Anchored at `0px / 120px / 240px`, the top holds still and the colour grows downward. The stroke also needs `background-origin: border-box`, or every side gets the wrong end of the ramp — which looks almost right.
107
+ **A gradient stroke or fill on an element that changes size places its stops in absolute units, and a gradient stroke is two background layers on one box.** Percentage stops rescale with the box, so a panel opening from a 46px bar runs its whole sweep inside the closed bar — louder closed than open — and shimmers as it grows. Anchored at `0px / 120px / 240px`, the top holds still and the colour grows downward. The stroke also needs `background-origin: border-box`, or every side gets the wrong end of the ramp — which looks almost right.
113
108
 
114
109
  → The stroke's two layers, with no wrapper or mask: `domains/depth/translucent-materials.md`
115
110
 
@@ -121,7 +116,7 @@ Modal also hands back what it took: **on close, focus returns to whatever opened
121
116
 
122
117
  **Each material ships an opaque baseline, then the material inside `@supports`, then its `prefers-reduced-transparency`, `prefers-contrast: more` and `forced-colors` rules — after every theme block that sets the same properties.** The order is the law: opaque first makes the non-supporting path the default, and so the tested one; why preferences go after every theme block is colour law 10.
123
118
 
124
- Reduced transparency needs both halves, an opaque surface *and* no filter: filter only leaves a thin tint over live content, tint only still pays for the blur. High contrast promotes the edge to a real border and leaves the surface alone; forced colours needs law 3's edge. Read the *compiled* CSS: a prefixing pipeline may treat a hand-written `-webkit-backdrop-filter` for Safari as a duplicate and drop the standard property Firefox needs — as the Glass preset's own advice did to the site it documents.
119
+ Reduced transparency needs both halves, an opaque surface *and* no filter: filter only leaves a thin tint over live content, tint only still pays for the blur. High contrast promotes the edge to a real border and leaves the surface alone; forced colours needs law 3's edge. Read the *compiled* CSS: a prefixing pipeline may treat a hand-written `-webkit-backdrop-filter` for Safari as a duplicate and drop the standard property Firefox needs.
125
120
 
126
121
  → The full fallback block, in order: `domains/depth/fallbacks-and-performance.md`
127
122
 
@@ -188,7 +183,7 @@ Then, for raised and above, the material:
188
183
 
189
184
  **4. Add materials last**, fallbacks in order (law 15), counted against the budget.
190
185
 
191
- **5. Measure** every text tier on every translucent surface against its worst backdrop (law 10) with `<keyline>/scripts/contrast.mjs`, passing the backdrop with `--over` so the tint is composited first — `node <keyline>/scripts/contrast.mjs --over "#0b1220" --ladder "rgba(250,250,250,0.82)" "#16161a" "#4b4b52"`; the focus ring at 3:1 on each level's surface.
186
+ **5. Measure** every text tier on every translucent surface against its worst backdrop (law 10) with `contrast.mjs --over` (colour law 3); the focus ring at 3:1 on each level's surface.
192
187
 
193
188
  → Bundle tables per preset and theme, shadow derivation and a worked example: `domains/depth/elevation-recipes.md`
194
189
  → Blur, saturation, tint, gradient strokes, glows and sheens: `domains/depth/translucent-materials.md`
@@ -217,39 +212,15 @@ Then collect evidence per law:
217
212
  - **Fallbacks**: present, both halves of reduced transparency, *after* the theme blocks; the prefix in compiled CSS.
218
213
  - **Budget**: filtered surfaces visible at once, any at `opacity: 0` rather than unmounted, any animated blur.
219
214
 
220
- One line per finding in `## Open issues`, with evidence: *"`.search-popover` has `shadow-lg` and no border; vanishes in forced colours (3 files)."* Don't fix unless asked.
221
-
222
215
  ## Writing back to DESIGN.md
223
216
 
224
217
  - **`## Surfaces`** — the bundle table (`Level | Surface | Edge | Shadow | Layer`) in the project's names, per theme where surface or shadow differs, plus a material table (`Tier | Blur | Saturate | Tint | Fallback | Used on`) if it uses translucency. Write it on first build or audit; update it as levels change.
225
- - **`## Decisions`** — dials moved off the preset, with reason and date: *"Translucent header on the docs area despite `product`: it scrolls over diagrams readers need to keep in view."* Laws knowingly broken, with the user's confirmation.
226
- - **`## Open issues`** — audit findings, one line each with evidence; remove each when fixed.
227
218
  - **Proposed roles** — level surfaces the canonical list lacks (`surface.floating`, `surface.overlay`), marked `proposed`.
228
219
 
229
220
  ## Checklist
230
221
 
231
- - [ ] Only what layout law 3's table gives a surface has one; a card holds exactly one object, and no card holds a card; section panels are flat, with nothing boxed inside.
232
- - [ ] Several objects of one kind, or a rail's topics, share one card parted by dividers; asides are a ruled note, not a tinted box.
233
- - [ ] Each surface on one `elevation.*` level, chosen by what it does, bundle and `z.*` included.
234
- - [ ] No borrowed parts: a lifted hover or drag moves the whole bundle, layer included.
235
222
  - [ ] Every dialog, destructive confirmations included, is the system's dialog; closing one returns focus to its opener, stored when it opened from state.
236
- - [ ] The ladder holds only the levels this project uses, no more than the dial allows.
237
- - [ ] Above `elevation.flat`, an edge (`border.default` opaque, `border.layered` translucent) that survives `forced-colors`; a ring edge becomes a 1px border under `prefers-contrast: more`.
238
- - [ ] `elevation.flat`: edge or surface step, never a faint shadow.
239
- - [ ] Shadows: contact plus ambient, negative spread, one light; no contact layer on transient surfaces.
240
- - [ ] One shadow definition per level per theme, used everywhere.
241
- - [ ] Shadow strength set by what's immediately behind; on a tinted ground, its hue.
242
- - [ ] Dark levels each lighter than the one below; page not `#000`.
243
- - [ ] Translucency only where content continues underneath and seeing it helps, or on still art; never cards, rows, sections, reading copy, flat backdrops.
244
- - [ ] Blur set by backdrop uncertainty, always with `saturate()`.
245
- - [ ] Animating wrapper and filtered surface are separate, or the tint is defensive.
246
- - [ ] Translucent-surface text measured against the tint over the worst backdrop.
247
- - [ ] Overlapping translucent surfaces at visibly different tiers.
248
- - [ ] `elevation.overlay` has a `scrim`; `elevation.floating` does not.
249
- - [ ] Resizing gradients use pixel stops; strokes use `background-origin: border-box`.
250
- - [ ] Glows side-weighted; sheens run once and are removed.
251
- - [ ] Opaque baseline, `@supports`, then reduced-transparency (tint and filter), high-contrast, forced-colours — after every theme block.
223
+ - [ ] A ring edge becomes a real 1px border under `prefers-contrast: more`; an edgeless surface carries `1px solid transparent` for `forced-colors`.
224
+ - [ ] Animating wrapper and filtered surface are separate elements, or the tint is defensive.
252
225
  - [ ] Compiled CSS has both `backdrop-filter` and `-webkit-backdrop-filter`.
253
226
  - [ ] Closed filtered surfaces unmount; no animated blur; nothing filtered over video.
254
- - [ ] Translucency, tier count and on-screen budget match the dials.
255
- - [ ] Level count, shadow strength, dark lift and scrim match the dials, or a decision says why.
@@ -6,18 +6,13 @@ This guide speaks in roles such as `text.secondary`; the code says whatever `DES
6
6
 
7
7
  ## Before you start
8
8
 
9
- Read `DESIGN.md` at the project root: `## Profile` for the preset and any areas on another one, `## Layout` for the project's name for each role and the decisions already made. Reason in roles; write code in the project's names.
10
-
11
- - **No `DESIGN.md`:** run `/keyline init` for this domain only, do the task, then recommend the full setup.
12
- - **A role you need has no row:** reuse a value already doing that job; otherwise add one in the project's naming style and record it in `DESIGN.md`.
13
- - **The task would break a law or move a dial off its preset:** say so and ask; if the user confirms, record it with its reason under `## Decisions`.
14
- - **Preset `glass`:** the values are in `DESIGN.md`; `glass/layout/guide.md` has the exact recipes behind them.
9
+ Reads `DESIGN.md` `## Layout`.
15
10
 
16
11
  ## Laws
17
12
 
18
13
  ### 1. Running text has one reading measure, and it doesn't move between templates
19
14
 
20
- **Every block of running prose sits at `measure.reading`, defined once and shared by every template that shows prose.** Past about 80 characters the return sweep lands on the wrong line; under about 45 the eye spends more effort returning than reading. So the measure is set in characters of `type.body` and resolved to a width, not picked as a round pixel number. Sharing it buys **positional continuity**: clicking from an index summary to its detail page reads as the summary expanding, not as arriving somewhere else, and a template with its own width breaks that on every route in or out.
15
+ **Every block of running prose sits at `measure.reading`, defined once and shared by every template that shows prose.** Past about 80 characters the return sweep lands on the wrong line; under about 55 the eye spends more effort returning than reading. So the measure is set in characters of `type.body` and resolved to a width, not picked as a round pixel number. Sharing it buys **positional continuity**: clicking from an index summary to its detail page reads as the summary expanding, not as arriving somewhere else, and a template with its own width breaks that on every route in or out.
21
16
 
22
17
  Non-prose content — an image wall, a data table or a diagram at `measure.wide`, a full-bleed band on an expressive page — may leave the measure, as may chrome and rails. **No paragraph longer than two lines may.** An article's cover — image or diagram — may run at `measure.wide` between the head and the body; images inside the body stay at the measure unless marked wide. **A divider is as wide as what it divides**: in a single-column reading layout, the measure, not the viewport. If a `type.title` wraps to four lines at the narrowest measure — the smallest viewport minus two `gutter`s, 320px say — the fix is usually the ladder (the type guide), not a responsive type step.
23
18
 
@@ -49,7 +44,7 @@ This is the law of **proximity**: things near each other are read as one group b
49
44
  | Sections on a tinted page ground — a dashboard whose page is `surface.subtle` with its regions as surfaces on it | Each region is a surface (`elevation.flat` or `raised` on `surface.raised`, depth law 1); the ground between them is the divider, so no lines |
50
45
  | Items in a collection | Hairlines between rows, or a grid of like cards — not a section boundary |
51
46
 
52
- **An existing design's divider stays.** Improving a design whose sections are panels keeps the panels and makes them quieter — one edge, nothing boxed inside — rather than unboxing it into a page that runs together; this table and the preset's divider are for a new system, and `commands/refine.md` (the limits) settles any disagreement. A shadow on a panel that doesn't float is reported to `depth`, not removed here. **One device per kind of boundary across the product** (similarity): lines between some peer sections and space between others reads as unfinished. **The test is a squint**: blur a screenshot until the text goes, and each section should still read as its own block. Where two run together, move one device up — space, then a line, then a band or a panel — and where the page reads as stripes or a stack of boxes, move one down.
47
+ **An existing design's divider stays.** Improving a design whose sections are panels keeps the panels and makes them quieter — one edge, nothing boxed inside — rather than unboxing it into a page that runs together; this table is for a new system (→ Refining an existing design, below). A shadow on a panel that doesn't float is reported to `depth`, not removed here. **One device per kind of boundary across the product** (similarity): lines between some peer sections and space between others reads as unfinished. **The test is a squint**: blur a screenshot until the text goes, and each section should still read as its own block. Where two run together, move one device up — space, then a line, then a band or a panel — and where the page reads as stripes or a stack of boxes, move one down.
53
48
 
54
49
  **Common region is real, which is why boxes are scarce.** A shared boundary makes things read as one group strongly enough to beat proximity, so boxing every region erases the grouping it was meant to show. Inside a section, the quietest device that holds a group wins: space (law 2), then a tint or a hairline, and a card only for one object the reader acts on (depth law 2). Figures are the commonest case: a set of stats is one row, each figure over its label, parted by vertical hairlines, not a tile per figure; the row wraps and stacks cleanly where tiles become a column of boxes. Where the figures count what the table below holds, they are also its filter. Boxed figures are a decision to record, and the boxes stay quiet: no hover lift, no icon tile, no more padding above than beside.
55
50
 
@@ -83,21 +78,19 @@ The absolute wrapper spans the section so the sticky child has a track, and keep
83
78
 
84
79
  ### 5. Nested radii: inner = outer − padding
85
80
 
86
- **Where one rounded object sits in another's corner, the inner radius is the outer radius minus the gap between their edges.** This is geometry, not taste: only then do the curves share a centre. Too large and the corner pinches; too small and the gap swells at the corners — visible even to people who couldn't say what's wrong. A 16px container with 4px of padding holds 12px buttons; a 12px button with an avatar inset 2px gives the avatar 10px. Snap to the ladder when the difference is 2px or less.
87
-
88
- The law binds concentric corners only: an inner corner tucked inside the outer one's curve, inset by less than the outer radius. Inset further, or set away from the corners — an icon tile in a padded card, a `kbd` in a search field — the two curves have no centre to share, and the inner element takes a radius for its own size from the ladder. So outer − padding reaching zero means the corners are no longer concentric, not that the inner corner goes square. Improving an existing design, the law never sharpens a corner: raise the outer radius or trim the inset instead (`commands/refine.md`, the limits).
81
+ **Where one rounded object sits in another's corner, the inner radius is the outer radius minus the gap between their edges.** This is geometry, not taste: only then do the curves share a centre. Too large and the corner pinches; too small and the gap swells at the corners — visible even to people who couldn't say what's wrong. A 16px container with 4px of padding holds 12px buttons.
89
82
 
90
- **Radius scales with size, not importance** — and sub-proportionally, or a large panel reads as a lozenge. `radius.pill` is only for things with no top and bottom — tags, dots, toggles, markers — never anything holding a paragraph.
83
+ The law binds concentric corners only, inset by less than the outer radius; anything inset further or set away from the corners takes the ladder's radius for its size, never a square corner by subtraction. Improving an existing design, it never sharpens a corner (→ Refining an existing design, below). Radius scales with size, not importance, and `radius.pill` is only for things with no top and bottom.
91
84
 
92
85
  ### 6. A line's weight says what it's doing
93
86
 
94
- **Lines are assigned by job — `hairline.chrome` for a divider inside one object, `hairline.object` for an object's edge, `rule.emphasis` for a deliberate mark such as a quote bar or a focus ring — and an internal divider is never more prominent than its object's edge**, or the object reads as two. Where chrome and object share a weight, the divider steps down in colour (`border.divider` against `border.default`). Three jobs, at most three weights. **A frame and the dividers inside it are one drawing**: same weight, same inset, meeting cleanly, or a table's edge and its row rules disagree in a way readers see as "the thickness is off" without naming it. Hairlines are drawn as borders, which a 1× screen rounds up to a pixel, never as half-pixel boxes, which it drops; focus rings and `prefers-contrast: more` never take a half pixel. On a translucent or unknown backdrop the line is translucent (`border.layered`; its colour is the colour guide's). A non-structural divider is `aria-hidden`, and an `<hr>` needs its UA border reset.
87
+ **Lines are assigned by job — `hairline.chrome` for a divider inside one object, `hairline.object` for an object's edge, `rule.emphasis` for a deliberate mark such as a quote bar or a focus ring — and an internal divider is never more prominent than its object's edge**, or the object reads as two. Where chrome and object share a weight, the divider steps down in colour (`border.divider` against `border.default`). Three jobs, at most three weights. **A frame and the dividers inside it are one drawing**: same weight, same inset, meeting cleanly, or a table's edge and its row rules disagree in a way readers see as "the thickness is off" without naming it. Hairlines are drawn as borders, never as half-pixel boxes; focus rings and `prefers-contrast: more` never take a half pixel. On a translucent or unknown backdrop the line is translucent (`border.layered`; its colour is the colour guide's). A non-structural divider is `aria-hidden`, and an `<hr>` needs its UA border reset.
95
88
 
96
89
  ### 7. The z-index ladder is documented, sparse, and ordered by permanence
97
90
 
98
91
  **Every z-index comes from one named ladder with gaps between the rungs; a literal `z-index: 9999` is a bug waiting for its pair.** The longer something stays on screen, the higher it sits; the skip link beats everything. Gaps of ten let a later layer, like the Glass preset's once-a-session greeting overlay, take a free rung without renumbering. Two pairs are constraints, not choices:
99
92
 
100
- - **A dismiss catcher sits above the page and directly below what it dismisses**: above its menu it eats the menu's clicks; below the page it catches nothing. `z.dismiss` serves the navigation; a content popover renders its own catcher in its portal, catcher first.
93
+ - **A dismiss catcher sits above the page and directly below what it dismisses.** `z.dismiss` serves the navigation; a content popover renders its own catcher in its portal, catcher first.
101
94
  - **`z.skip-link` sits above `z.overlay`**, so a keyboard user tabbing during a splash or a modal can always get past it.
102
95
 
103
96
  | Role | Default | Holds |
@@ -111,9 +104,9 @@ The law binds concentric corners only: an inner corner tucked inside the outer o
111
104
  | `z.raised` | 10 | A hovered or focused item |
112
105
  | `z.below` | −10 | A highlight behind content |
113
106
 
114
- A negative layer needs an `isolation: isolate` parent, and **a `transform`, `filter`, `backdrop-filter`, `perspective`, `contain` or `will-change` on an ancestor traps `position: fixed` descendants inside it** — check every ancestor before reaching for a higher number.
107
+ A negative layer needs an `isolation: isolate` parent, and a transformed or filtered ancestor traps `position: fixed` descendants — check every ancestor before reaching for a higher number.
115
108
 
116
- → Radius ladders and hairline weights per preset, nesting worked through, stacking-context techniques: `domains/layout/radius-lines-and-layers.md`
109
+ → Nesting worked through, corner shape, hairlines as borders, the catcher pair in code, containing blocks: `domains/layout/radius-lines-and-layers.md`
117
110
 
118
111
  ### 8. Fork the component when the interaction model changes, not when the arrangement does
119
112
 
@@ -123,7 +116,7 @@ A negative layer needs an `isolation: isolate` parent, and **a `transform`, `fil
123
116
 
124
117
  ### 9. A view choice is about the thing, so it doesn't persist by default
125
118
 
126
- **When the reader can switch between presentations of one collection, the choice resets on arrival unless you can name why it shouldn't, and the switch carries the reader's position across.** Persist preferences about the *reader* — theme, sound, motion, density — not about a *thing*, or readers land in a mode picked weeks ago for another collection and don't remember choosing it; the Glass galleries open on the wall every time, because the whole collection is what you want on arrival. A product's saved view — a board grouped by assignee, a filtered table — is the legitimate exception: persist it as a named, visible thing and record the decision, never as a toggle that silently turns sticky. So is board or list on a working page the reader returns to daily: persist it per page, the switch always visible in the page header, and let the default win where the default is the point. Which second view a collection gets is patterns law 10. On the switch, open the incoming view on the item nearest the viewport centre; leaving a grid at item 60 and arriving at item 1 is worse than not switching.
119
+ **When the reader can switch between presentations of one collection, the choice resets on arrival unless you can name why it shouldn't, and the switch carries the reader's position across.** Persist preferences about the *reader* — theme, sound, motion, density — not about a *thing*, or readers land in a mode picked weeks ago for another collection and don't remember choosing it. A named saved view and board-or-list on a page worked daily are the exceptions, each persisted visibly and recorded (→ `domains/layout/forks-gating-and-disclosures.md`, whether it persists). Which second view a collection gets is patterns law 10. On the switch, open the incoming view on the item nearest the viewport centre; leaving a grid at item 60 and arriving at item 1 is worse than not switching.
127
120
 
128
121
  ### 10. Owner-curated content disappears when empty
129
122
 
@@ -150,7 +143,7 @@ A negative layer needs an `isolation: isolate` parent, and **a `transform`, `fil
150
143
 
151
144
  ### 13. A target is as big as the pointer needs, and placed where the pointer already is
152
145
 
153
- **Every control has a hit area of at least 24 × 24px under a fine pointer and 44 × 44px on touch, and the action used most sits closest to where the reader's attention already is.** Fitts's law: the time to hit a target grows with its distance and shrinks with its size, so a 16px icon button in a corner costs a reach and a correction every time it's used. **The hit area is not the drawing**: an icon stays 16px while padding or a `::before` inset grows the target round it, so size never has to be bought with visual weight. Adjacent targets on touch keep at least 8px between their hit areas, or the larger finger presses both.
146
+ **Every control has a hit area of at least 24 × 24px under a fine pointer and 44 × 44px on touch, and the action used most sits closest to where the reader's attention already is.** That is Fitts's law. **The hit area is not the drawing**: an icon stays 16px while padding or a `::before` inset grows the target round it, so size never has to be bought with visual weight. Adjacent targets on touch keep at least 8px between their hit areas, or the larger finger presses both.
154
147
 
155
148
  Distance is the other half. A menu opens where it was clicked; a dialog's primary button sits where the eye finishes the dialog; a row's actions sit on the row, not in a toolbar across the screen. **The edges of the screen are infinitely deep for a mouse** — a pointer can't overshoot them — which is why the dock and the menu bar live there, and why a destructive action never takes the corner where a hurried flick lands (patterns law 3). Below a phone width, the frequent actions move within thumb reach, at the bottom, not into a top corner.
156
149
 
@@ -159,9 +152,9 @@ Distance is the other half. A menu opens where it was clicked; a dialog's primar
159
152
  | Dial | editorial | product | expressive |
160
153
  |---|---|---|---|
161
154
  | `measure.reading`, in characters of `type.body` | 65–75; default 70 | 60–70; default 65 — prose inside the product only | 55–70; default 62 |
162
- | Page structure | One column every template shares | App shell: one navigation surface — a sidebar holding every destination, no top bar repeating it — then content and an optional inspector; every page under its own header bar (breadcrumb, title, actions), which is not navigation | Sections may go full-bleed; text at the measure |
155
+ | Page structure | One column every template shares | App shell: a navigation sidebar, content and an optional inspector; every page under its own header bar | Sections may go full-bleed; text at the measure |
163
156
  | `measure.wide` | ≈ 1.5 × `measure.reading` | = `measure.app` | Card grids and multi-column text cap at ≈ 1200–1280px; only full-bleed bands and media run edge to edge |
164
- | `measure.app` | — | Content fills its panel, left-aligned; forms and settings cap at `measure.form`, ≈ 640–760px; nav 220–264px; inspector 320–400px | — |
157
+ | `measure.app` | — | Content fills its panel, left-aligned; `measure.form` 640–760px; nav 220–264px; inspector 320–400px | — |
165
158
  | `gutter` (smallest width) | 24–32px | 16px, growing to 24–32px from tablet up | 20–24px |
166
159
  | Column grid: `grid.columns` / `space.column` | None — the measure is the grid | 4 / 8 / 12 on the content panel, measured on the container; 16px, 24px from tablet up | 4 / 8 / 12 on the window for card grids and section layouts; 16px, 24px from tablet up; prose still at the measure |
167
160
  | `target.min`, fine pointer / touch | 24 / 44px | 24 / 44px | 24 / 48px |
@@ -179,17 +172,17 @@ Distance is the other half. A menu opens where it was clicked; a dialog's primar
179
172
 
180
173
  The `glass` preset takes its values from `glass/layout/`.
181
174
 
182
- **The measure.** Editorial's committed reader sits high, since long lines are efficient in sustained reading when leading pays for the return sweep. Product text is scanned in bursts and sits lower; expressive body runs larger, so fewer characters fit. Below 55 or above 80 characters costs reading on any preset.
175
+ **The measure.** Editorial's committed reader sits high, since long lines are efficient in sustained reading when leading pays for the return sweep. Product text is scanned in bursts and sits lower; expressive body runs larger, so fewer characters fit. Law 1's bounds hold on every preset.
183
176
 
184
177
  **Density.** Product halves editorial's spacing, since a working reader compares side by side and scrolling is friction, but keeps the asymmetry: a dense form with equal gaps is where grouping goes first. Expressive sections are scenes with large gaps, and the heading ratio still holds inside them.
185
178
 
186
- **The shell.** In product, navigation is one surface: in an app, a sidebar holding every destination, with the reader's own controls — account, role, theme — in its footer, and no top bar repeating it. A top bar that repeats it splits navigation across two edges and puts a second band of chrome over every screen. Every page still has a header bar — breadcrumb, title, actions — and that is not a navigation bar. **Improving one screen of an existing app keeps its navigation surface**: moving navigation is a restructure, done only when asked and otherwise listed under `## Open issues`. Primary destinations sit at the top; the long tail goes in collapsible folders, open state remembered and the current page's folder forced open; administration lives in the account menu, not among destinations. Rows are dense, about 30px, with the workspace name at the nav's own size, so the sidebar reads as one list. The sidebar collapses to an icon rail — icons hold their position while the width animates, labels become tooltips, counts become dots — toggled by a button and a shortcut and remembered, since it is a preference about the reader (law 9); it is a feature, so when improving an existing screen, don't add it — list it under `## Open issues`. Below `bp.shell` it opens as a sheet, always expanded. **Pages fill the content panel**, left-aligned at one `gutter` (16px at the smallest width, 24–32px from tablet up), each under its own header bar — breadcrumb at the start, the page's actions at the end as quiet buttons — which titles the page rather than repeating navigation. Forms and settings cap at `measure.form`, about 640–760px, so a switch stays within reach of its label. A page sets neither its own width nor its outer padding; the shell does, once.
179
+ **The shell.** In product, navigation is one surface — a sidebar holding every destination, no top bar repeating it, which would split navigation across two edges — and every page sits under its own header bar, which titles the page rather than navigating. Pages fill the content panel, and the shell, not the page, sets width and outer padding (→ `domains/layout/measure-and-rhythm.md`).
187
180
 
188
181
  **Tables and group labels.** Editorial tables share a text column's edge, so a frame only adds a box. A product panel has no column to align to: tables run full width beside toolbars and figures, so a frame says where the data stops, and a reader scrolling hundreds of rows in collapsible groups needs the group's name and its controls in view. What both keep is law 3: one table style and one group-label style, used identically everywhere.
189
182
 
190
- **Radius.** A sharp system (0 throughout) is a legitimate expressive character that satisfies the nesting law trivially; what costs is mixing a round button into a square card. Round systems read modern through a soft corner: an 8px control and 12px card, drawn as a light superellipse (a squircle) where the browser supports `corner-shape`, and a plain round corner of the same radius where it doesn't. When improving an existing design, its radii are the floor: keep them, and change one only to satisfy the nesting law.
183
+ **Radius.** A sharp system (0 throughout) is a legitimate expressive character that satisfies the nesting law trivially; what costs is mixing a round button into a square card. Round systems read modern through a soft corner, drawn as a light superellipse where the browser supports `corner-shape` (`radius-lines-and-layers.md`).
191
184
 
192
- **Hairlines.** A `0.5px` border is one device pixel on 2× screens and rounds up to 1px on 1×, so it never vanishes. Product draws every structural line at 0.5px, because a dense screen is mostly rows and a 1px rule on each reads heavy; editorial keeps a 1px object edge for the few objects it has. That is a dial for a new system: an existing design whose border sits close to its surface keeps its 1px (`commands/refine.md`, the limits).
185
+ **Hairlines.** Product draws every structural line at 0.5px, because a dense screen is mostly rows and a 1px rule on each reads heavy; editorial keeps a 1px object edge for the few objects it has.
193
186
 
194
187
  **Breakpoints.** A single-column site needs two: where the column becomes padding-limited, and where a rail fits. A header with many links needs a third, where they stop fitting on one line, or between the two it has nowhere to go.
195
188
 
@@ -206,11 +199,11 @@ The grid is a set of edges to align to, not a set of boxes. A block spans whole
206
199
  ## Building it for a project
207
200
 
208
201
  1. **Read the preset and the areas.** A `product` app with `app/(docs)/** → editorial` gets a shell for the app and a single column for the docs, sharing one `measure.reading`.
209
- 2. **Set the measure from the type**: a character count from the dials for `family.text` at `type.body`, resolved to a width by measuring the font (a 16px sans averages about 8px per character, so 70 characters ≈ 560px) or in `ch`, which runs wider than an average letter. Check it at the narrowest viewport minus two `gutter`s with the project's longest `type.title`.
202
+ 2. **Set the measure from the type**: a character count from the dials for `family.text` at `type.body`, resolved to a width and checked at the narrowest viewport (`domains/layout/measure-and-rhythm.md`).
210
203
  3. **Derive the frame.** With a rail, `measure.frame` = `measure.reading` + 2 × (`measure.rail` + gap) + 2 × `gutter`, and `bp.rail` is that width; for a shell, set the region widths and where the inspector, then the nav, collapses.
211
204
  4. **Build spacing from ratios**: the base (4px) and `space.section`, the heading pair at the preset's ratio, then `space.tight` and `space.group` so every group's inner gap is at most half its outer one.
212
205
  5. **Stack the sections and pick each boundary's divider** from law 3's table, one device per kind of boundary — or the one the existing design already uses: each a heading and its content, figures in hairline-parted rows; list the things that are single objects, which alone get a card (the depth guide). Pick the one table style and the one group-label style for the preset.
213
- 6. **Build the radius ladder inward** from `radius.container` through every concentric nesting (container → control → avatar), subtracting padding and snapping; anything inset past the outer radius takes the rung for its size. **Set the corner shape** from the dial in the same step: under editorial and product, `corner-shape: superellipse(1.5)` on everything, with pills and circles kept `round` (the snippet in `radius-lines-and-layers.md`). It costs one rule and is what makes rounded corners read as squircles rather than boxes.
206
+ 6. **Build the radius ladder inward** from `radius.container` through every concentric nesting (law 5), and **set the corner shape** from the dial in the same step (`radius-lines-and-layers.md`).
214
207
  7. **Assign the three line jobs** per the preset, drawn as borders; no divider outranks its object's edge, and a frame matches the dividers inside it.
215
208
  8. **Write the z ladder** from law 7, dropping unused rungs and keeping the gaps.
216
209
  9. **List the forks and the gates**: which components ship twice and why; which collections are owner-curated (gated) and which are user data (empty states).
@@ -240,45 +233,27 @@ Start from the setup scan's frequencies for measures, spacing, radii, z-index an
240
233
  | 13 | Icon, close and row-action buttons under 24px (44px on touch) with no padding or pseudo-element growing the hit area; touch targets closer than 8px; frequent actions far from what they act on; destructive actions in a corner | Element, drawn size, hit size |
241
234
  | Dials | Blocks side by side that share no edges; a 12-column grid behind a single reading column; columns stretching past `measure.wide` | Page, and the edges that don't line up |
242
235
 
243
- In a browser, resize from 320px to 2560px watching the rail gap, tab through collapsed disclosures, and run find-in-page on a closed one. Report one line per finding with its evidence — a ratio, a count, a file — under `## Open issues`, and fix nothing unless asked.
236
+ In a browser, resize from 320px to 2560px watching the rail gap, tab through collapsed disclosures, and run find-in-page on a closed one.
237
+
238
+ ## Refining an existing design
239
+
240
+ A preset's dials describe a new system. Where one disagrees with the design being improved, this decides, and each call is recorded under `## Decisions` so the next run makes the same one.
241
+
242
+ - **Dividers.** The design's section device stays (law 3). One thing changes either way: a row of stat tiles folds into one strip of figures (law 3), because that is a law about figures, not a divider dial — unless keeping the tiles is recorded under `## Decisions`.
243
+ - **Corners.** The design's radii are the floor, and the nesting law never sharpens one. Where inner = outer − inset would come out sharper than the inner corner is now, raise the outer radius or trim the inset until they share a centre; if neither fits, the corners aren't concentric and the inner keeps its radius.
244
+ - **Line weight.** The design's line weights stay, like its corners. Product's 0.5px hairline is for a new system, or for an existing line that reads heavy and whose colour is strong enough to survive halving; a border already near the surface colour keeps its 1px.
244
245
 
245
246
  ## Writing back to DESIGN.md
246
247
 
247
248
  - **Layout table rows** for every role set or found — measures, gutter, spacing roles, the heading pair and its ratio, the radius ladder and its nestings, the three line jobs, the z ladder, breakpoints named for what changes — plus one line per forked component naming the interaction model each version serves.
248
249
  - **The gate list**: owner-curated collections (gated) and user data (empty states).
249
- - **Decisions**, dated, with the reason: any dial moved off the preset (a 1px chrome hairline in an editorial project), persisted view preference, layer added to the ladder, or template allowed a different measure.
250
- - **Open issues** for every law the code breaks, with evidence, removed when the fix lands.
251
- - **Proposed roles**, marked `proposed`, for a job with no row — a rail width, a frame width, a toast layer.
250
+ - **Decisions** worth recording here: a persisted view preference, a layer added to the ladder, a template allowed a different measure.
251
+ - **Proposed roles** for a job with no row — a rail width, a frame width, a toast layer.
252
252
 
253
253
  ## Checklist
254
254
 
255
- - [ ] Prose sits at one `measure.reading`, defined once and shared by every template; no paragraph over two lines escapes it.
256
- - [ ] Only non-text content, chrome and rails leave the measure; dividers are as wide as what they divide.
257
255
  - [ ] The narrowest measure fits the longest `type.title` without a responsive type step — except an editorial article title's one phone step (type dials).
258
- - [ ] Headings sit closer to what follows than what precedes: `space.heading-above : space.heading-below` between 2:1 and 5:1.
259
- - [ ] Clutter is fixed with gaps, padding and tier order before colour or type; a card's tiers part by space.
260
- - [ ] Symmetric spacing only between peers.
261
- - [ ] Every spacing value is a role on the base unit; strays are listed.
262
- - [ ] Sections stack in reading order, each opened by its heading, and each boundary takes law 3's divider for what it holds — one device per kind across the product, an existing design's panels kept; every section survives the squint test; card grids only for collections of like objects; no bento mosaic.
263
- - [ ] Stats sit in one row parted by hairlines (product: a strip that filters its table; any boxes stay quiet); tabs are text on a shared hairline.
264
- - [ ] One table style and one group-label style, the preset's: unframed with labels over a rule, or one shared frame with one sticky group bar.
265
- - [ ] One chip shape per size across the product.
266
- - [ ] Product: one navigation surface — a sidebar holding every destination, no top bar repeating it; every page under its own header bar, which is not navigation; an existing app's navigation surface kept unless a restructure is asked for, and shell features it lacks listed, not added; pages fill the panel, forms and settings cap at `measure.form`, and no page sets its own width or padding.
267
- - [ ] Rails anchor to the content's frame; sticky panels are capped with `dvh` and scroll within.
256
+ - [ ] Stats sit in one row parted by hairlines; tabs are text on a shared hairline; one table style, one group-label style and one chip shape per size.
268
257
  - [ ] Below `bp.rail` the rail becomes an in-flow disclosure; fixed chrome has clearance on every template.
269
- - [ ] Corner shape matches the dial: editorial and product carry the `superellipse(1.5)` rule, with pills and circles `round`.
270
- - [ ] Inner radius = outer − padding at every concentric corner (inset under the outer radius); elements away from the corners take the ladder's radius for their size, never square by subtraction; `radius.pill` only on things without a top and bottom.
271
- - [ ] `hairline.chrome`, `hairline.object`, `rule.emphasis` have one job each; no divider outranks its object's edge; a frame matches its dividers; hairlines are borders; `border.layered` on translucent backdrops.
272
- - [ ] Every z-index is on the ladder; `z.dismiss` sits under what it dismisses; `z.skip-link` tops `z.overlay`.
273
258
  - [ ] No transformed or filtered ancestor traps a fixed descendant; negative layers sit in an isolated parent.
274
- - [ ] Components fork where the interaction model differs, share data and rows, and sync state both hold.
275
- - [ ] Breakpoints are named for what changes, and measured on the container inside a shell; resizing keeps the column count unless asked; every hover style, decoration included, is gated on `(hover: hover) and (pointer: fine)`.
276
- - [ ] No affordance hides where it can't come back; every informative hover has a touch path.
277
- - [ ] View choices reset on arrival unless a recorded decision says otherwise; switching keeps the reader's place.
278
- - [ ] Empty owner-curated collections lose route, nav entry, section and divider (user-data empty states: patterns law 9).
279
259
  - [ ] Grids look deliberate at 1, 2 and n items.
280
- - [ ] Collapsed disclosures keep their content in the HTML, with `inert` and `aria-hidden`, or use `<details>`; closed groups show a summary and open to a matching search.
281
- - [ ] Skip link by transform with `id="main"`; every `<nav>` labelled; heading levels follow structure; anchors carry `scroll-margin-top`; print hides chrome.
282
- - [ ] Every control's hit area is at least 24px (44px on touch), grown by padding or a pseudo-element, not a bigger icon; touch targets sit 8px apart; frequent actions sit on what they act on.
283
- - [ ] Product and expressive pages that place unlike blocks side by side share a column grid; its breakpoints sit where the content stops fitting; columns stop at `measure.wide`; editorial has none.
284
- - [ ] Dials (`measure.reading`, spacing density, heading ratio, radius ladder, hairline weights, breakpoint count) match the preset, or the difference is under `## Decisions`.
@@ -38,16 +38,20 @@ One container, one measure, every template; non-text content leaves the column b
38
38
 
39
39
  ### Product — an app shell
40
40
 
41
- A working reader needs navigation, content and detail together, so the page is regions:
41
+ A working reader needs navigation, content and detail together, so the page is regions, each sized by the `measure.app` dial:
42
42
 
43
- | Region | Width | Collapses |
43
+ | Region | Holds | Collapses |
44
44
  |---|---|---|
45
- | Navigation | 220–264px | Last — to an icon strip, then a menu button |
46
- | Content | Fills its panel, left-aligned at one `gutter` — 16px at the smallest width, 24–32px from tablet up; forms and settings cap at `measure.form`, ≈ 640–760px | Never |
47
- | Inspector | 320–400px | First — to an overlay sheet on `z.overlay` |
45
+ | Navigation | Every destination | Last — to an icon rail, then, below `bp.shell`, a sheet that opens always expanded |
46
+ | Content | The page, filling its panel, left-aligned at one `gutter`; forms and settings cap at `measure.form` | Never |
47
+ | Inspector | Detail on the selection | First — to an overlay sheet on `z.overlay` |
48
48
 
49
49
  Settings help, onboarding copy, a changelog entry and an empty state's explanation still sit at `measure.reading`; a 1200px-wide paragraph of help text in a settings pane is the most common product failure of law 1. The inspector is detail, so it collapses first; navigation is how the reader leaves, so it stays longest. A centred column inside the content panel wastes the width a working reader opened the window for, and a page that sets its own `max-width` or padding drifts from its neighbours: the shell owns both.
50
50
 
51
+ **The sidebar.** Navigation is this one surface, with the reader's own controls — account, role, theme — in its footer; a top bar repeating it puts a second band of chrome over every screen. Primary destinations sit at the top; the long tail goes in collapsible folders, open state remembered and the current page's folder forced open; administration lives in the account menu, not among destinations. Rows are dense, about 30px, with the workspace name at the nav's own size, so the sidebar reads as one list. Collapsed to an icon rail, icons hold their position while the width animates, labels become tooltips and counts become dots; the rail is toggled by a button and a shortcut and remembered, since it is a preference about the reader (layout law 9).
52
+
53
+ **Improving one screen of an existing app keeps its navigation surface.** Moving navigation is a restructure, and the icon rail a feature: neither is done unasked; list them under `## Open issues`.
54
+
51
55
  ### The product page
52
56
 
53
57
  Top to bottom, every page in the shell has the same parts, so a reader learns one page and can read them all:
@@ -63,7 +67,7 @@ Top to bottom, every page in the shell has the same parts, so a reader learns on
63
67
 
64
68
  **The group bar.** One shared component: a tinted band about 36px tall holding a chevron, the group's name, its count and an add action, sticky under the header row while its group scrolls. How long lists open, summarise and search is patterns law 8.
65
69
 
66
- **Forms.** A form or settings page caps at `measure.form`, about 640–760px, left-aligned in the panel: at the panel's full width a switch ends up a screen's width from its label. The field that decides the rest goes first, at the form's full width — a category before the free text that depends on it.
70
+ **Forms.** A form or settings page caps at `measure.form`, left-aligned in the panel: at the panel's full width a switch ends up a screen's width from its label. The field that decides the rest goes first, at the form's full width — a category before the free text that depends on it.
67
71
 
68
72
  **Overviews and dashboards.** What a dashboard holds and in what order is patterns law 12; on the page, each of its sections is parted from the next by a full-bleed hairline (layout law 3).
69
73
 
@@ -78,16 +82,12 @@ Sections may run to the viewport edge as bands — a colour field, a full-bleed
78
82
 
79
83
  ## Spacing ladders per preset
80
84
 
81
- One base unit (4px), a role for every job. Values are defaults; the ratios must survive.
85
+ One base unit (4px), a role for every job. `space.tight`, `space.group`, `space.section` and the pair ratio are the layout dials; the heading pair's defaults land inside that ratio. Values are defaults; the ratios must survive.
82
86
 
83
87
  | Role | editorial | product | expressive |
84
88
  |---|---|---|---|
85
- | `space.tight` | 8px | 4–8px | 8–12px |
86
- | `space.group` | 16–24px | 12–16px | 24–32px |
87
- | `space.section` | 48–72px | 24–32px | 96–160px |
88
89
  | `space.heading-above` | 40–48px | 16–24px | 48–80px |
89
90
  | `space.heading-below` | 12px | 8px | 16px |
90
- | Pair ratio | 3:1–4:1 | 2:1–3:1 | 3:1–5:1 |
91
91
 
92
92
  Half-steps (6px, 10px, 14px, 44px) are legitimate when a ratio lands between whole steps; a value used once with no role behind it isn't.
93
93
 
@@ -141,7 +141,7 @@ In prose, a short closing block that reads as a coda follows on without a rule;
141
141
 
142
142
  ## Page padding and chrome clearance
143
143
 
144
- - **Top space signals a document.** Editorial pages start well below the top edge (80–112px, about 48–64px at phone widths); an expressive page without a hero does the same; product content starts close under the page's header bar (16–24px), which already framed it.
144
+ - **Top space signals a document.** Editorial pages, and expressive ones without a hero, start well below the top edge; product content starts close under the page's header bar, which already framed it (values: the layout dials).
145
145
  - **Fixed chrome reserves its own clearance.** Under a fixed bottom bar or dock, the page's bottom padding must exceed the bar's height plus its offset on every template whose content can end near the viewport bottom; a page ending in a footer can rely on the footer's padding.
146
146
  - **Audit it per template.** The long article ends in a padded footer; the short index without one puts its last item under the bar — the layout bug that most often survives review, because the test page is always a long one.
147
147
 
@@ -175,10 +175,10 @@ A fictional freight-tracking app — `product`, docs area `editorial`, Inter at
175
175
  --space-heading-above: 24px; /* 3:1 */
176
176
  --space-heading-below: 8px;
177
177
 
178
- --radius-container: 12px; /* panel, 4px padding … */
179
- --radius-card: 8px; /* … holds 8px rows */
180
- --radius-control: 6px; /* button, 2px inset … */
181
- --radius-small: 4px; /* … holds a 4px badge */
178
+ --radius-container: 16px; /* panel, 4px padding … */
179
+ --radius-card: 12px; /* … holds 12px rows */
180
+ --radius-control: 8px; /* button, 2px inset … */
181
+ --radius-small: 6px; /* … holds a 6px badge */
182
182
  --radius-pill: 9999px;
183
183
 
184
184
  --hairline-chrome: 0.5px; /* a border: one device pixel on 2×, 1px on 1×; divider uses border.divider */
@@ -36,7 +36,7 @@ Derive the ladder from the nestings, and write the subtraction into the token so
36
36
  | `radius.card` | 12px | 12px | 20px | 0 |
37
37
  | `radius.container` | 16px | 16px | 28px | 0 |
38
38
 
39
- Product used to run smaller (6px controls, 8px cards), and in review that read as boxy and dated beside current apps. It now matches editorial: with content unboxed (layout law 3), the few cards don't tile edge to edge, which is where large radii leave triangular gaps (one more reason not to build a mosaic of panels). Both draw the corner as a light superellipse (below). An existing design's radii are the floor when improving it: a pass never sharpens corners, not even for the nesting law — it raises the outer radius or trims the inset instead (`commands/refine.md`, the limits). Expressive may go either way, but never mixes round and square components or keeps four near-identical radii that are really one.
39
+ Product matches editorial, because smaller radii (6px controls, 8px cards) read boxy and dated; with content unboxed (layout law 3), the few cards don't tile edge to edge, which is where large radii leave triangular gaps. Both draw the corner as a light superellipse (below). An existing design's radii are the floor (layout guide, Refining an existing design). Expressive may go either way, but never mixes round and square components or keeps four near-identical radii that are really one.
40
40
 
41
41
  **Radius scales with size, not importance**, and sub-proportionally: a 400px panel gets a larger radius than a 40px button because it is larger, but a proportional one reads as a lozenge. **`radius.pill` is for things with no top and bottom** — tags, dots, toggles, progress tracks. A paragraph in a pill-ended card collides with the curve.
42
42
 
@@ -63,7 +63,7 @@ Where chrome and object share a weight, the divider must step down in colour so
63
63
 
64
64
  ### Half a pixel, drawn as a border
65
65
 
66
- A `0.5px` border is one device pixel on a 2× screen and rounds up to a full pixel on 1×: browsers snap any border width between 0 and 1 device pixel up to one. A box half a pixel tall with a background gets no such snapping — on 1× it antialiases to a faint smear or nothing. So every hairline is a border, including a divider between rows:
66
+ A `0.5px` border is one device pixel on a 2× screen and rounds up to a full pixel on 1×: browsers snap any border width between 0 and 1 device pixel up to one. A box half a pixel tall with a background gets no such snapping — on 1× it antialiases to a faint smear or nothing. So every hairline is a border, including a divider between rows. A `box-shadow` ring doesn't snap either: on 1× its half pixel antialiases to a faint line, which the depth guide's product ring edge wants, and why it swaps to a real border under `prefers-contrast: more` (`domains/depth/elevation-recipes.md`).
67
67
 
68
68
  ```css
69
69
  :root { --hairline: 0.5px; }