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
@@ -30,12 +30,7 @@ backdrop-filter: blur(20px) saturate(1.8);
30
30
 
31
31
  ## 3. The 97% panel
32
32
 
33
- `glass-panel` pairs a 24px blur with **97% opacity** because its menus' wrappers *scale open*, and a transform on an ancestor weakens `backdrop-filter` beneath it (depth law 9): for the whole animation the tint does the work. Where you can, the animating wrapper and the glass surface are different elements:
34
-
35
- ```tsx
36
- <DropMenu open={open}> {/* animates scale + y */}
37
- <ul className="glass-panel …"> {/* carries the blur, never transformed */}
38
- ```
33
+ `glass-panel` pairs a 24px blur with **97% opacity** because its menus' wrappers *scale open*, and a transform on an ancestor weakens `backdrop-filter` beneath it (depth law 9): for the whole animation the tint does the work. Where you can, the animating wrapper and the glass surface are different elements: `DropMenu` scales and moves, the `glass-panel` inside it carries the blur and is never transformed (`glass/motion/assets/DropMenu.tsx`).
39
34
 
40
35
  ## 4. Glass over glass
41
36
 
@@ -53,30 +48,7 @@ Depth law 3's translucent edge is the alpha ramp (`glass/color/guide.md`): `gray
53
48
 
54
49
  ### The shadow stack
55
50
 
56
- Four layers, one light source above (depth law 4), in **one class**, not inline:
57
-
58
- ```css
59
- .dock-shadow {
60
- box-shadow:
61
- inset 0 1px 0 rgba(255, 255, 255, 0.6), /* top-edge catch light */
62
- 0 1px 1px rgba(0, 0, 0, 0.02), /* contact */
63
- 0 8px 16px -4px rgba(0, 0, 0, 0.04), /* near ambient */
64
- 0 24px 32px -8px rgba(0, 0, 0, 0.06); /* far ambient */
65
- }
66
- :root[data-theme="dark"] .dock-shadow {
67
- box-shadow:
68
- inset 0 1px 0 rgba(255, 255, 255, 0.1),
69
- 0 1px 1px rgba(0, 0, 0, 0.3),
70
- 0 8px 16px -4px rgba(0, 0, 0, 0.5),
71
- 0 24px 32px -8px rgba(0, 0, 0, 0.6);
72
- }
73
- ```
74
-
75
- Opacities are 2–6% in light. Dark is a different shadow, not a re-tint (depth law 5): shadows ~10× stronger (4% black on `#0b0b0b` is invisible), highlight ~6× weaker (0.6 white on dark is a glowing edge, not a catch light).
76
-
77
- **Extracted the moment it appeared twice.** This ~400-character string was once inline in five places here, each with a dark twin. Not being a colour, it *can't* be a design token (colour law 10), so a class is its only home.
78
-
79
- → Every elevation tier, the pocket's edge, and stacking inside a glass surface: `glass/depth/depth-cues.md`
51
+ `.dock-shadow`: four layers, one light (depth law 4), 2–6% black in light and a different shadow in dark (depth law 5); one class with its dark twin, not a token or a JS constant. → `glass/depth/assets/glass.css`; tiers, the pocket's edge, stacking: `glass/depth/depth-cues.md`
80
52
 
81
53
  ## 6. A gradient edge, without a wrapper or a mask
82
54
 
@@ -141,54 +113,13 @@ The class drops on `animationend`; under reduced motion, plain accent colour.
141
113
 
142
114
  ## 8. Fallbacks
143
115
 
144
- Depth law 15, at glass's tokens.
145
-
146
- ### `prefers-reduced-transparency`
147
-
148
- ```css
149
- /* AFTER both theme blocks — it re-declares the same properties. */
150
- @media (prefers-reduced-transparency: reduce) {
151
- :root, :root[data-theme="dark"] {
152
- --glass-nav-bg: var(--color-gray-100);
153
- --glass-panel-bg: var(--color-gray-100);
154
- --folder-glass-bg: var(--color-background-100);
155
- }
156
- .glass-nav, .glass-panel, .folder-glass {
157
- backdrop-filter: none;
158
- }
159
- }
160
- ```
161
-
162
- ### `prefers-contrast: more`
163
-
164
- ```css
165
- @media (prefers-contrast: more) {
166
- .glass-nav, .glass-panel, .folder-glass { border-color: var(--color-gray-1000); }
167
- }
168
- ```
169
-
170
- ### `@supports` — the gap in the reference implementation
171
-
172
- The reference implementation has **no `@supports` guard**, so without `backdrop-filter` it gets a bare 72% tint: legible, but weak. `glass/depth/assets/glass.css` ships with the guard:
173
-
174
- ```css
175
- .glass-nav { background: var(--color-gray-100); } /* opaque baseline */
176
-
177
- @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
178
- .glass-nav {
179
- background: var(--glass-nav-bg);
180
- backdrop-filter: blur(20px) saturate(1.8);
181
- }
182
- }
183
- ```
184
-
185
- **Don't hand-write `-webkit-backdrop-filter`.** Tailwind v4's Lightning CSS adds it and keeps only the last of a hand-written pair, so the site ships the prefix alone and Firefox loses every blur (`glass/depth/performance-and-fallbacks.md`).
116
+ Depth law 15, at glass's tokens, in `glass/depth/assets/glass.css`: reduced transparency swaps the tints to `gray-100` / `background-100` and drops the filter, after both theme blocks; `prefers-contrast: more` turns the edge `gray-1000`; an `@supports` guard over a `gray-100` baseline, which the site lacks. **Never hand-write `-webkit-backdrop-filter`**: Lightning CSS keeps only the last of a pair, and Firefox loses every blur (§9).
186
117
 
187
118
  ## 9. Performance
188
119
 
189
120
  Depth law 16, within glass's budget: three or four surfaces at most, each small — a dock a few hundred pixels wide, not a full-viewport overlay — and closed menus unmount.
190
121
 
191
- → What keeps it affordable here, and the prefix bug in detail: `glass/depth/performance-and-fallbacks.md`
122
+ → What keeps it affordable, and the prefix bug: `glass/depth/performance-and-fallbacks.md`
192
123
 
193
124
  ## 10. Confirmations
194
125
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  @theme {
6
6
  /* ── The measure ────────────────────────────────────────────────────────
7
- 640px outer, 32px padding → 576px of text. At 16px prose that's ~75–85
8
- characters per line: the top of the comfortable range, which is the
7
+ 640px outer, 32px padding → 576px of text. At the 14px prose size that's
8
+ ~90 characters per line (85–95): past the comfortable range, which is the
9
9
  right place for a site that expects sustained reading (long lines are
10
10
  efficient once the reader is committed, and 1.75 leading pays for the
11
11
  longer return sweep).
@@ -139,7 +139,9 @@
139
139
  one dock. Renders as a true sub-pixel hairline on retina and
140
140
  rounds UP to 1px elsewhere, so it degrades rather than vanishes.
141
141
  1px a real edge — where a card ends.
142
- 2px a mark with editorial weight — a blockquote rule, a focus ring.
142
+ 2px a focus ring; imported Markdown's blockquote rule.
143
+ 3px the authored pull quote's bar — a rounded flex child, not a
144
+ border, since a border can't be rounded on its own.
143
145
 
144
146
  On translucent surfaces use the ALPHA ramp (border-gray-alpha-400). A
145
147
  solid border on glass doesn't participate in what's behind it and reads as
@@ -14,7 +14,7 @@ For mostly-text interfaces, where layout should get out of the way. One decision
14
14
  }
15
15
  ```
16
16
 
17
- At most 576px of text, roughly 75–85 characters at 16px: the top of comfortable, chosen for sustained reading. **Every page uses the class** (layout law 1).
17
+ At most 576px of text, about 90 characters a line (85–95) at the 14px prose size: past comfortable on purpose, with prose leading opened to match. **Every page uses the class** (layout law 1).
18
18
 
19
19
  - **Non-text content may leave the column**: a masonry image wall has no reading measure, so it goes full-bleed.
20
20
  - **An article's cover image stays in it.** Bled wide, it becomes a banner *above* the article, not part of it.
@@ -40,16 +40,16 @@ Rail and column share one centred 1280px frame, so their gap holds from 1280px u
40
40
  ## 3. Spacing groups; asymmetry is the mechanism
41
41
 
42
42
  ```
43
- mt-11 (44px) ← heading
43
+ mt-10 (40px) ← heading
44
44
  mb-3 (12px)
45
45
  ← the paragraph it introduces
46
46
  ```
47
47
 
48
- ~3.5:1 above to below makes them one unit (layout law 2). It holds at every scale:
48
+ ~3.3:1 above to below makes them one unit (layout law 2). It holds at every scale:
49
49
 
50
50
  | Relationship | Inner | Outer | Ratio |
51
51
  |---|---|---|---|
52
- | Heading → its paragraph | 12px below | 44px above | 3.7:1 |
52
+ | Heading → its paragraph | 12px below | 40px above | 3.3:1 |
53
53
  | List items → the next block | 8px between | 24px below | 3:1 |
54
54
  | Bio paragraphs → the links after | 16px between | 32px after last | 2:1 |
55
55
  | Section heading → its content | 16–32px | 36px+ between sections | ~2:1 |
@@ -88,7 +88,8 @@ Glass sets the corner shape on everything, pseudo-elements included, with no exc
88
88
  |---|---|
89
89
  | `0.5px` | Chrome borders, dividers, glass edges |
90
90
  | `1px` | Card borders, rings, disclosure outlines |
91
- | `2px` | Quote rules, focus rings |
91
+ | `2px` | Focus rings, an imported Markdown quote's rule |
92
+ | `3px` | The pull-quote bar: a rounded flex child, not a border |
92
93
  | `1px` (`h-px`) | Table-of-contents markers |
93
94
 
94
95
  Weight follows the job (layout law 6): dividing icon groups inside one dock, 0.5px; defining a card's edge, 1px. On translucent surfaces the edge is the **alpha ramp** (`border-gray-alpha-400`; see `glass/color/guide.md`).
@@ -247,10 +248,10 @@ The domain checklist applies in full; these are glass's values for it.
247
248
 
248
249
  - [ ] `.col` — 640px, 32px padding — on every page; article covers stay in it.
249
250
  - [ ] Rails in the 1280px frame: `left-8`, `w-[196px]`, `sticky top-28`, `max-h-[calc(100dvh-11rem)]`, shown from `xl`.
250
- - [ ] Prose headings `mt-11` / `mb-3`; home sections `py-9`; long pages `mt-16 border-t pt-10`, the first `mt-14`.
251
+ - [ ] Prose headings `mt-10` / `mb-3`; home sections `py-9`; long pages `mt-16 border-t pt-10`, the first `mt-14`.
251
252
  - [ ] Rules between Work, Experience and Life only; Watching and Socials follow on.
252
253
  - [ ] Radii from the ladder: 6 / 9 / 10 / 12 / 16px and full; `superellipse(1.5)` on everything.
253
- - [ ] 0.5px chrome, 1px object edges, 2px marks; `gray-alpha` on translucent surfaces.
254
+ - [ ] 0.5px chrome, 1px object edges, 2px focus rings, the pull quote's 3px rounded bar; `gray-alpha` on translucent surfaces.
254
255
  - [ ] `MobileDock` / `DesktopDock` and `SectionNav` / `SectionNavCompact`, sharing data and rows.
255
256
  - [ ] The gallery opens on the wall; its layout lives in module memory.
256
257
  - [ ] `pb-28` under the dock on every page that can end near the viewport bottom.
@@ -6,7 +6,7 @@ Glass's values for layout laws 1–3; the derivations and reasons every preset s
6
6
 
7
7
  `.col` (§1 of the guide) is 640px outer with 32px padding each side → **576px of text**.
8
8
 
9
- At 16px prose that's roughly 75–85 characters per line, at the top of the comfortable range on purpose: long lines are efficient once the reader is committed, and 1.75 leading compensates for the longer return sweep. At 14px UI copy the column holds ~85–95 characters, which is fine because that text is scanned, not read.
9
+ At the 14px prose size that's about 90 characters a line (85–95, measured in Archivo), past the comfortable range on purpose: long lines are efficient once the reader is committed, and 1.75 leading compensates for the longer return sweep. At 16px the same column would hold 75–85.
10
10
 
11
11
  The padding does double duty: on a 375px phone the column is `375 − 64 = 311px`, and on desktop the 32px keeps text off the edge of a maximised window.
12
12
 
@@ -64,13 +64,13 @@ Tailwind's 4px scale, with a preference for a few well-worn values:
64
64
  | `7` | 28 | Capability groups |
65
65
  | `8` | 32 | Section heading → content, column padding |
66
66
  | `9` | 36 | Home section padding |
67
- | `10` | 40 | Section content after a rule |
68
- | `11` | 44 | Prose heading top margin |
67
+ | `10` | 40 | Prose heading top margin, section content after a rule |
68
+ | `11` | 44 | Between roles on the experience page |
69
69
  | `14` | 56 | First section on a long page |
70
70
  | `16` | 64 | Between major sections on a long page |
71
71
  | `20`/`28` | 80/112 | Page top/bottom padding |
72
72
 
73
- The values in heaviest use are `2.5`, `3.5`, `9`, `11` — the half-steps and odd numbers. They exist because 8/12/36/44 are where the *asymmetry ratios* land, not because the scale demanded them.
73
+ The values in heaviest use are `2.5`, `3.5`, `9`, `10` — mostly half-steps and odd numbers. They exist because 10/14/36/40 are where the *asymmetry ratios* land, not because the scale demanded them.
74
74
 
75
75
  ## Asymmetry, in detail
76
76
 
@@ -46,9 +46,10 @@ The rule is in §4 of the guide: `superellipse(1.5)` on every element and pseudo
46
46
  |---|---|---|
47
47
  | 0.5px | `border-[0.5px]`, `h-[0.5px]` | Chrome borders, dividers, glass edges |
48
48
  | 1px | `border`, `ring-1`, `h-px` | Card borders, TOC markers |
49
- | 2px | `border-l-2` | Quote rules, focus rings |
49
+ | 2px | `outline-2`, `border-l-2` | Focus rings, an imported Markdown quote's rule |
50
+ | 3px | `w-[3px] rounded-full` | The pull-quote bar |
50
51
 
51
- By job (layout law 6): two groups of icons within one dock, 0.5px; where a card ends, 1px; a blockquote's left rule, 2px.
52
+ By job (layout law 6): two groups of icons within one dock, 0.5px; where a card ends, 1px; a focus ring, 2px. The authored pull quote's bar is 3px and rounded, so a flex child rather than a border (`glass/type/long-form-prose.md`, Quotes); imported Markdown, which can't take classes, keeps a 2px `gray-alpha-300` border.
52
53
 
53
54
  Vertical dividers use `w-[0.5px]` with an explicit height and `self-center`:
54
55
 
@@ -68,7 +68,7 @@ The class is applied only on an explicit toggle, then removed:
68
68
 
69
69
  ```ts
70
70
  export function setThemeChoice(choice: ThemeChoice): void {
71
- localStorage.setItem(KEY, choice);
71
+ try { localStorage.setItem(KEY, choice); } catch { /* won't persist */ }
72
72
  const root = document.documentElement;
73
73
  root.classList.add("theme-transition");
74
74
  applyTheme(choice);
@@ -38,7 +38,7 @@ Glass uses two of law 4's classes:
38
38
 
39
39
  One curve for the whole site. The `1.1` end control point overshoots slightly and settles back, physical but not bouncy. The exceptions:
40
40
 
41
- - **Opacity-only micro-transitions**: `ease-out` at 100ms.
41
+ - **100ms opacity hovers and icon fills** (§13): `ease-out`. Opacity entrances (`.reveal`, the route fade) keep the house curve on purpose: on opacity the overshoot clamps at 1.
42
42
  - **The theme crossfade**: plain `ease`.
43
43
  - **Anything spring-driven**: no bezier at all.
44
44
  - **Any track that also drives a filter**, or any property floored at zero (law 5): expo-out, `cubic-bezier(0.16, 1, 0.3, 1)`, which never exceeds 1. It is the same intent in a curve legal for the property, not a second character.
@@ -116,11 +116,7 @@ The details that keep it from flickering are in `domains/motion/patterns.md`, th
116
116
 
117
117
  ### When the DOM element isn't enough
118
118
 
119
- A *photograph* travelling between layouts escalates to the GPU (law 13): one viewport-sized canvas above the gallery draws the open photo into a screen rect, four numbers in a uniform, no reflow. The wall stays DOM, since a hundred tiles as textures is ~590MB before atlasing; the open photo is `alt=""`, its labels sitting beside it; failure returns `null` and keeps the caller's `<img>`.
120
-
121
- **Carry the reading position across**: open the other layout on the tile nearest the viewport centre, measured before React commits it, and give each end its own rect around a shared centre (law 8).
122
-
123
- → The measurement (`centreFrame`) and the WebGL notes: `glass/motion/shared-element.md`, escalating to the GPU
119
+ A *photograph* travelling between layouts escalates to the GPU (law 13), opening on the tile nearest the viewport centre (law 8): `glass/motion/shared-element.md`, escalating to the GPU
124
120
 
125
121
  ## 7. Reduced motion
126
122
 
@@ -152,34 +148,11 @@ One passive listener and one rAF per behaviour (law 12; the shape is in `domains
152
148
 
153
149
  ## 9. Reveal on scroll
154
150
 
155
- CSS animates; JavaScript only marks an element as arrived:
156
-
157
- ```css
158
- .reveal { opacity: 0; transform: translateY(8px);
159
- transition: opacity 300ms var(--ease-geist), transform 300ms var(--ease-geist); }
160
- .reveal[data-in] { opacity: 1; transform: translateY(0); }
161
-
162
- @media (prefers-reduced-motion: reduce) {
163
- .reveal { opacity: 1; transform: none; transition: none; }
164
- }
165
- ```
166
-
167
- `threshold: 0` with `rootMargin: "0px 0px -40px 0px"`, one-shot, re-run on the pathname. 8px of travel registers without a page of paragraphs seeming to slide. Every guard is law 11 and `domains/motion/guards.md`; the two the site hit:
168
-
169
- - **A fractional threshold on a tall element.** Every rendered SKILL.md on the site, eight to thirteen thousand pixels in one `.reveal`, once sat invisible at opacity 0 behind `0.1`.
170
- - **A class the framework owns.** Tag-filtered rows went blank as the pointer left, when React rewrote a state-dependent `className` and wiped an `in` added with `classList.add`. Hence `data-in`.
171
-
172
- The no-JavaScript reset, in the head:
173
-
174
- ```html
175
- <noscript>
176
- <style>.reveal { opacity: 1 !important; transform: none !important; }</style>
177
- </noscript>
178
- ```
151
+ 300ms on the house curve, 8px of travel, `threshold: 0`, `rootMargin: "0px 0px -40px 0px"`, marked by `data-in`; the guards are law 11, whose two bugs are this site's. → `glass/motion/assets/motion-tokens.css`, `glass/motion/assets/useReveal.ts`
179
152
 
180
153
  ## 10. Performance: `m` not `motion`
181
154
 
182
- `m.div` inside `<LazyMotion features={domAnimation} strict>`, never a bare `motion.*` (`domains/motion/performance.md`). The menu animates a bare `<m.div>` around the `glass-panel`, so the wrapper scales and the child carries the blur (law 13).
155
+ `m.div` inside `<LazyMotion features={domAnimation} strict>`, never a bare `motion.*` (`domains/motion/performance.md`). The menu's wrapper scales and its `glass-panel` child carries the blur (law 13; `glass/depth/guide.md` §3).
183
156
 
184
157
  ## 11. Hover intent
185
158
 
@@ -204,11 +177,7 @@ active:scale-[0.92] /* 8% — felt, not seen */
204
177
 
205
178
  On `pointerdown` (law 3).
206
179
 
207
- The site synthesises a "tok" on hover and click (Web Audio, no asset files): a sine fundamental, a fast-decaying marimba-like 3.9:1 partial, and a lowpassed noise transient for the wooden attack. Hover is quiet and high (1200Hz, 0.09s, 0.05 gain), click lower and louder (900Hz, 0.14s, 0.13 gain); it's gated on the autoplay policy, muteable, and persisted. Dense surfaces opt out of the hover tick with `[data-quiet-hover]`; clicks still sound. The rules for arming and muting are in `domains/motion/patterns.md`, sound.
208
-
209
- **Sound can be a phrase, not just an event.** The opening screen's run, a note per photograph, is written out in A major pentatonic, where no two notes clash, so a run cut short by a failed image still resolves; the landing note, the only accented one, sits a fifth *below* the start, closing the phrase rather than adding a step.
210
-
211
- Sound is *on* by default here, which is defensible only on a personal site and only if you're confident. → Synthesis walkthrough and delegation pattern: `glass/motion/sound.md`
180
+ Sound is *on* by default here, defensible only on a personal site: a synthesised "tok" on hover and click, muteable; `[data-quiet-hover]` silences hover on dense surfaces. → `glass/motion/sound.md`; the rules: `domains/motion/patterns.md`, sound
212
181
 
213
182
  ## 13. Fill means chosen
214
183
 
@@ -223,7 +192,7 @@ export const detailFade = (on: boolean) =>
223
192
  `transition-opacity duration-200 ease-out motion-reduce:transition-none ${on ? "opacity-0" : "opacity-100"}`;
224
193
  ```
225
194
 
226
- **200ms `ease-out`, not the house curve**: overshoot suits travel, not a fade. The knockout mask for interior lines is in `domains/motion/patterns.md`, fill means chosen.
195
+ **200ms `ease-out`, not the house curve** (§2). The knockout mask for interior lines is in `domains/motion/patterns.md`, fill means chosen.
227
196
 
228
197
  **How the marks that can't fill signal instead:**
229
198
 
@@ -277,7 +246,7 @@ The domain checklist first; then glass's values:
277
246
 
278
247
  - [ ] Every duration on the ladder: no 250ms, 500ms or 1s.
279
248
  - [ ] Off-ladder timing is the subject of the moment (the 620ms dissolve) or a once-a-session set piece, and says which.
280
- - [ ] `--ease-geist` everywhere, plus `ease-out` for opacity-only, plain `ease` for the theme fade, and expo-out on any track driving a filter.
249
+ - [ ] `--ease-geist` everywhere, opacity entrances included, plus `ease-out` for 100ms opacity hovers and icon fills, plain `ease` for the theme fade, and expo-out on any track driving a filter.
281
250
  - [ ] Menus open on the `GROW`/`CROSS` pair and close on `CLOSE`.
282
251
  - [ ] Hover timed by area: a link 100ms, a card-sized wash 200ms.
283
252
  - [ ] No translate on hover, bar the folder's rising cards.
@@ -60,7 +60,7 @@ One variable turns a blur-crossfade into a plain crossfade; `blur(0px)` → `blu
60
60
 
61
61
  ## In CSS
62
62
 
63
- The `.reveal` rule is §9 of `glass/motion/guide.md`; the theme crossfade's opt-in block is in `glass/motion/easing-and-duration.md`. The route fade:
63
+ The `.reveal` rule is in `glass/motion/assets/motion-tokens.css`; the theme crossfade's opt-in block is in `glass/motion/easing-and-duration.md`. The route fade:
64
64
 
65
65
  ```css
66
66
  @media (prefers-reduced-motion: reduce) {
@@ -114,7 +114,7 @@ export type Rect = { x: number; y: number; width: number; height: number };
114
114
  export function lerpRect(a: Rect, b: Rect, t: number): Rect { /* … */ }
115
115
  ```
116
116
 
117
- The rules are law 13 and `domains/motion/performance.md`, escalating to the GPU. Here the wall stays DOM and the open photo is `alt=""`, its labels in the strip beside it.
117
+ The rules are law 13 and `domains/motion/performance.md`, escalating to the GPU. Here the wall stays DOM, since a hundred tiles as textures is ~590MB before atlasing, and the open photo is `alt=""`, its labels in the strip beside it.
118
118
 
119
119
  **Failing returns `null`:**
120
120
 
@@ -169,7 +169,10 @@ It's still a set piece, not a texture — once a session. See §14 of `glass/mot
169
169
  ## Mute, and persistence
170
170
 
171
171
  ```ts
172
- let muted = localStorage.getItem("ui-sound") === "off";
172
+ // Every storage access is guarded: it throws in some private modes.
173
+ let muted = (() => {
174
+ try { return localStorage.getItem("ui-sound") === "off"; } catch { return false; }
175
+ })();
173
176
 
174
177
  export function setMuted(value: boolean): void {
175
178
  muted = value;
@@ -396,8 +396,10 @@
396
396
  white-space: nowrap;
397
397
  }
398
398
 
399
+ /* Imported Markdown can't take the authored pull quote's 3px rounded bar
400
+ (glass/type/assets/prose.tsx), so it keeps a plain 2px rule. */
399
401
  .prose-imported blockquote {
400
- padding-left: 14px;
402
+ padding-left: 15px;
401
403
  border-left: 2px solid var(--color-gray-alpha-300);
402
404
  color: var(--color-faint);
403
405
  }
@@ -108,7 +108,7 @@ Archivo's true italic is a separate file whose slant fights its geometric letter
108
108
  ```tsx
109
109
  // From the inline markdown renderer
110
110
  if (bold) return <strong className="font-medium text-gray-1000">{text}</strong>;
111
- if (em) return <em className="text-gray-800">{text}</em>;
111
+ if (em) return <em className="text-muted">{text}</em>;
112
112
  ```
113
113
 
114
114
  Why the mapping works, and the semantic tags it keeps: domain law 9 and `domains/type/prose-and-emphasis.md`.
@@ -184,9 +184,7 @@ Glass's two systems (domain law 8):
184
184
 
185
185
  The ranges overlap, so leading and measure carry the distinction. Reading leading on a UI row is the usual symptom here: it grows, and the dock loses its rhythm.
186
186
 
187
- **UI copy is sentence case, generated labels included.** Buttons, column heads, field names, dialog titles, toasts, and anything a template builds from a key: first word and proper nouns capitalised, acronyms and third-party names as written. Labels generated from code drift into Title Case unnoticed, and a second case is a second voice.
188
-
189
- **Icons beside text** follow domain law 13: 13px text takes a 13–14px icon with a 4–6px gap. It is the mass problem `glass/color/guide.md` solves for colour, by resting icons a tier below, solved here for size.
187
+ **Labels are sentence case, generated ones included** (patterns law 1), and **icons beside text** follow domain law 13: 13px text, a 13–14px icon, a 4–6px gap.
190
188
 
191
189
  → How prose blocks are set — headings, quotes, lists, spacing: `glass/type/long-form-prose.md`
192
190
 
@@ -277,6 +277,9 @@ export function run(argv) {
277
277
  }
278
278
 
279
279
  const result = check({ root: opts.root, paths: opts.paths.length ? opts.paths : ["."], ignores: opts.ignores, rules: opts.rules });
280
+ // The verdict counts every finding, so --quiet trims the output, never the exit code.
281
+ const all = summarise(result.findings);
282
+ const failed = all.error > 0 || (opts.strict && all.warning > 0);
280
283
  if (opts.quiet) result.findings = result.findings.filter((f) => f.severity === "error");
281
284
  const s = summarise(result.findings);
282
285
 
@@ -286,7 +289,7 @@ export function run(argv) {
286
289
  } else {
287
290
  console.log(human(result, { all: opts.all, color: process.stdout.isTTY && !process.env.NO_COLOR }));
288
291
  }
289
- return s.error > 0 || (opts.strict && s.warning > 0) ? 1 : 0;
292
+ return failed ? 1 : 0;
290
293
  }
291
294
 
292
295
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
@@ -80,9 +80,9 @@ Copy this shape. Drop any table or row that doesn't apply; add project roles und
80
80
 
81
81
  | Role | Name here | Value |
82
82
  |---|---|---|
83
- | `z.popover` | `z-popover` | 50 |
84
- | `z.overlay` | `z-overlay` | 80 |
85
- | `z.toast` | `z-toast` | 90 |
83
+ | `z.popover` | `z-popover` | 20 |
84
+ | `z.overlay` | `z-overlay` | 60 |
85
+ | `z.toast` | `z-toast` | 70 |
86
86
 
87
87
  **Breakpoints**
88
88
 
@@ -123,7 +123,7 @@ Plus springs by character, and the reduced-motion policy in one sentence.
123
123
  ## Decisions
124
124
 
125
125
  - YYYY-MM-DD — Card titles at 500, not 600: they read heavy beside the pane title. (User decision; the type guide dial.)
126
- - YYYY-MM-DD — Three weights, not two: the billing table needs a middle weight for column totals. (Client feedback; the type guide dial override.)
126
+ - YYYY-MM-DD — Three weights, not two: the billing screens need a 600 heading weight above the 500 that lifts lines inside rows. (Client feedback; the type guide dial override.)
127
127
  - YYYY-MM-DD — Page on white `surface.page`, not the grey `bg-neutral-50`: the text tiers failed against the grey. (Law; the colour guide.)
128
128
 
129
129
  ## Open issues
@@ -46,7 +46,7 @@ node <keyline>/scripts/contrast.mjs --ladder "#ffffff" "#16161a" "#4b4b52" "#6b6
46
46
 
47
47
  ## 3. Choose the preset
48
48
 
49
- From what the reader is mostly doing — reading, working, or being persuaded — propose `editorial`, `product` or `expressive` (`presets.md`). Offer `glass` only when the user wants modimihir.com's exact system rather than their own; it fills the dials from `glass/`, under the project's existing names wherever it has them.
49
+ From what the reader is mostly doing — reading, working, or being persuaded — propose `editorial`, `product` or `expressive` (`presets.md`). Offer `glass` only when the user wants modimihir.com's exact system rather than their own; it fills the dials from the tables in each `glass/<domain>/guide.md`, under the project's existing names wherever it has them.
50
50
 
51
51
  State the evidence: *"Mostly long-form pages under `app/blog`, two weights in use, one accent — this reads as `editorial`."* Mixed projects get a base preset plus areas: `product`, with `app/(marketing)/** → expressive`.
52
52
 
@@ -70,20 +70,16 @@ Don't ask what you can find out, and don't ask for approval of the whole mapping
70
70
  Follow `system/design-md.md`: the project's names in `Name here`, canonical roles in `Role`, values as the source states them, measured contrast for every text tier, per-theme columns only where themes exist, nothing for roles the project lacks. Then:
71
71
 
72
72
  - **`## Decisions`** — every dial set differently from the preset, with its reason, or a question mark if unknown.
73
- - **`## Open issues`** — every law the code breaks, one line each with evidence (a ratio, a count, a file). Before writing them, read the law headings (the `### N.` lines) of each domain guide you cite, so every citation names the right law. **Don't fix anything** unless asked; a setup run that also rewrites forty components can't be reviewed.
73
+ - **`## Open issues`** — every law the code breaks, one line each with evidence (a ratio, a count, a file). Cite each law by the right number (`commands/init.md`). **Don't fix anything** unless asked; a setup run that also rewrites forty components can't be reviewed.
74
74
 
75
75
  Tell the user briefly: the preset and why, how many roles mapped cleanly, what you proposed names for, and the three most important open issues.
76
76
 
77
77
  ## 6. Generating tokens, only when asked
78
78
 
79
- The set ships no token files, because the right file depends on the stack. Asked for tokens — for a new project, or to fill `proposed` rows — generate them **in the project's existing mechanism**: a Tailwind v4 `@theme` block, a `tailwind.config` extension, CSS custom properties, a JS theme object, a design-tokens JSON. Match the existing layout and naming; never add a second token system beside the first.
79
+ Apart from the `glass` preset's drop-in files under `glass/<domain>/assets/`, the set ships no token files, because the right file depends on the stack. Asked for tokens — for a new project, or to fill `proposed` rows — generate them **in the project's existing mechanism**: a Tailwind v4 `@theme` block, a `tailwind.config` extension, CSS custom properties, a JS theme object, a design-tokens JSON. Match the existing layout and naming; never add a second token system beside the first.
80
80
 
81
81
  With no tokens yet, `presets.md` and each domain guide's `## Dials` section give defaults. Say they're defaults, choose names with the user, and record the choices in `DESIGN.md`.
82
82
 
83
- ## How the other commands use this file
84
-
85
- Each domain command reads `DESIGN.md`, finds its section, and works in the project's names. If the file is missing, it runs steps 1–2 for its own domain, does the task and recommends `/keyline init` — it doesn't refuse. A decision that changes the system — a new role, a moved dial, a law knowingly broken, a change a law forced that a later session might undo — goes back under `Decisions` — dated, marked user decision, client feedback or law, newest first, replacing what it supersedes — which is what keeps the file true after the first day.
86
-
87
83
  ## Checklist
88
84
 
89
85
  - [ ] An existing `DESIGN.md` was reconciled, not overwritten; every user-chosen name and decision survived.
@@ -25,34 +25,34 @@ When the evidence doesn't settle it, propose one, say what evidence you used, an
25
25
 
26
26
  *The interface should disappear so the reading doesn't.* Restraint is the point, and a single loud moment reads as craft because nothing else competes with it.
27
27
 
28
- - **Type** — a dense ladder that keeps the reading loudest, with an article's title clearly opening the piece (up to 2.25 × body); index and chrome titles lower. Two weights. A sans for interface text is allowed beside a serif reading face. Roles that share a size separate on weight, leading or a rule.
29
- - **Colour** — near-monochrome. One accent, spent on focus. At most one further chromatic exception, justified structurally. Links usually in the text colour.
30
- - **Layout** — one reading measure every page shares; rails and asides live outside it. Generous, asymmetric vertical rhythm.
31
- - **Surfaces** — mostly flat. Elevation reserved for things that float; translucency, if any, only on chrome.
32
- - **Motion** — short, and only where it makes a change legible. Nothing loops. One set piece, at most, once a session.
28
+ - **Type** — a dense ladder that keeps the reading loudest.
29
+ - **Colour** — near-monochrome, the accent spent on focus.
30
+ - **Layout** — one reading measure every page shares.
31
+ - **Surfaces** — mostly flat; translucency, if any, only on chrome.
32
+ - **Motion** — short, and only where it makes a change legible.
33
33
 
34
34
  ## `product`
35
35
 
36
36
  *The interface is a tool, and the reader is fluent in it.* Density, predictability and scannability beat atmosphere. Signal has to survive a screen full of data.
37
37
 
38
- - **Type** — tighter, smaller steps; UI and data roles dominate. Two weights, strong at 500; labels in sentence case, never uppercase eyebrows. Tabular figures on every number, scoped to numeric elements.
39
- - **Colour** — near-monochrome neutral ground with a real status palette (danger, warning, success, info), each a foreground/background pair. One accent hue for primary action, focus, selection (the current nav item on a 25% accent tint with accent icon and label) and the primary data series, and no second highlight hue. Brand colour already in the design stays where it has a job. Colour marks what needs a person, not what is finished. Colour for meaning, never for decoration.
40
- - **Layout** — an app shell rather than a single measure: 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, forms and settings capped at a form measure. Improving one screen keeps the navigation it has. Sections stack, each opened by its heading, and peer sections are parted by a full-bleed hairline; no bento mosaics or stat tiles. That is the default for a new system; an existing design keeps its own section device (`commands/refine.md`, the limits). Tighter spacing, still grouped asymmetrically. Prose inside the product (settings help, empty-state copy) still sits at a reading measure.
41
- - **Surfaces** — content on the page, rows parted by hairlines; a data table may take one hairline frame; a settings group that saves together takes a hairline panel; a card only for one object, never a card in a card. A clear, small elevation ladder: flat page, raised cards, floating menus, modal overlays. Hairlines do more work than shadows.
42
- - **Motion** — shortest of the three. State feedback is near-instant; nothing delays a repeated action. Springs only where something is dragged or thrown.
38
+ - **Type** — tighter, smaller steps; UI and data roles dominate.
39
+ - **Colour** — near-monochrome with a real status palette, and one accent hue that acts.
40
+ - **Layout** — an app shell rather than a single measure; tighter spacing, still grouped.
41
+ - **Surfaces** — content on the page, rows parted by hairlines; a card only for one object.
42
+ - **Motion** — shortest of the three; nothing delays a repeated action.
43
43
 
44
44
  ## `expressive`
45
45
 
46
46
  *The interface is making an impression, usually once.* Scale, colour and motion are allowed to carry brand — but still by rule, or it turns into a template.
47
47
 
48
- - **Type** — a real display tier far above body, possibly a second family for it. Display, title and section headings fluid. Wider scale, still assigned to roles rather than derived from a ratio. Tracking correction matters most here.
49
- - **Colour** — a brand palette used broadly, with rules about where each colour may go. Chromatic exceptions still named and counted, just more of them.
50
- - **Layout** — sections may break the reading measure and go full-bleed; the measure still governs any paragraph longer than two lines.
51
- - **Surfaces** — depth, gradients and translucency allowed as brand material, each with its fallback.
52
- - **Motion** — longer and more characterful on entrances; exits still get out of the way. A few set pieces, each once. Reduced motion still gets a real alternative.
48
+ - **Type** — a real display tier far above body, possibly a second family.
49
+ - **Colour** — a brand palette used broadly, each colour placed by rule.
50
+ - **Layout** — sections may go full-bleed; paragraphs keep the measure.
51
+ - **Surfaces** — depth, gradients and translucency as brand material, each with its fallback.
52
+ - **Motion** — longer and more characterful on entrances; exits still get out of the way.
53
53
 
54
54
  ## `glass`
55
55
 
56
- The Glass preset's exact values: `editorial` in character, with every dial already decided on [modimihir.com](https://modimihir.com). Offer it only when the user wants that system itself rather than their own — `/keyline init` copies its values from `glass/` into `DESIGN.md`, in the project's own names wherever the project already has names, and `glass/<domain>/guide.md` supplies the exact recipes, with drop-in token files and components under each `assets/`.
56
+ The Glass preset's exact values: `editorial` in character, with every dial already decided on [modimihir.com](https://modimihir.com). Offer it only when the user wants that system itself rather than their own — `/keyline init` copies its values from the tables in each `glass/<domain>/guide.md` into `DESIGN.md`, in the project's own names wherever the project already has names, and each guide supplies the exact recipes, with drop-in token files and components under its `assets/`.
57
57
 
58
58
  A `glass` project can still override a dial. It should expect to: the Glass values were chosen for one text-heavy portfolio, and a project that isn't one will find the places they don't fit.
@@ -32,7 +32,7 @@ Sizes, leading, tracking and weight all hang off these. Owned by the type guide.
32
32
  | `type.data` | Numerals that must align — tables, prices, stats. Often `type.ui` or `type.body` plus tabular figures rather than its own size |
33
33
 
34
34
  Families: `family.text`, `family.display` (only if different), `family.ui` (only if different — interface text beside a serif reading face), `family.mono`.
35
- Weights: `weight.regular`, `weight.strong`. Two optional ones, each only where the preset allows it: `weight.medium`, a middle weight for dense UI where a project records a job the other two can't carry, and `weight.display`, the display tier's own weight, never reused below it.
35
+ Weights: `weight.regular`, `weight.strong`. Two optional ones, each only where the preset allows it: `weight.medium`, a third weight above `weight.strong` for headings on a dense screen, only where a project records a job the other two can't carry, and `weight.display`, the display tier's own weight, never reused below it.
36
36
 
37
37
  ## Colour
38
38
 
@@ -62,7 +62,7 @@ A project may slot extra tiers between secondary and tertiary (the Glass preset
62
62
  | `surface.raised` | A component's own surface — a card, an input, a menu |
63
63
  | `surface.hover` | That surface, hovered |
64
64
  | `surface.pressed` | That surface, pressed or active |
65
- | `surface.selected` | A persistent selected state, distinct from hover. Under product and expressive, the accent at about 25% over its surface, carrying `accent.ink` at 4.5:1 |
65
+ | `surface.selected` | A persistent selected state, distinct from hover. Under product and expressive, the accent tint colour law 7 defines |
66
66
  | `surface.floating` · `surface.overlay` | A floating or modal level's own surface, where it differs from `surface.raised` — usually only in dark themes, where elevation is shown by lightening |
67
67
  | `surface.inverse` | Tooltips, toasts, solid-filled chrome |
68
68
 
@@ -85,8 +85,8 @@ A project may slot extra tiers between secondary and tertiary (the Glass preset
85
85
  |---|---|
86
86
  | `focus` | The focus ring. Must hit 3:1 against every surface it can land on |
87
87
  | `accent` · `accent.hover` | The brand or action colour, if the system has one, and its hovered state. `accent` may share a token with `focus` |
88
- | `status.danger` · `status.warning` · `status.success` · `status.info` | Each a pair — `status.<name>.fg`, a foreground that clears 4.5:1 as text, and `status.<name>.bg`, a background tint — plus `status.<name>.solid` where a filled control needs it (a destructive button). Required in product systems, usually absent in editorial ones |
89
- | `exception.<name>` | A deliberate chromatic spend outside the accent — one per system in editorial, a few in expressive; in product none by default, plus one hue family per question the reader can't answer at a glance, named `exception.<question>.<member>` (`exception.type.task`, `exception.area.<name>`, `exception.person.<n>`), each listed in `DESIGN.md`. Always named after what it's *for* |
88
+ | `status.danger` · `status.warning` · `status.success` · `status.info` | Each a pair — `status.<name>.fg`, a foreground read as text, and `status.<name>.bg`, a background tint — plus `status.<name>.solid` where a filled control needs it (a destructive button) |
89
+ | `exception.<name>` | A deliberate chromatic spend outside the accent, always named after what it's *for*; a hue family answering one question is `exception.<question>.<member>` (`exception.type.task`, `exception.area.<name>`, `exception.person.<n>`), each listed in `DESIGN.md` |
90
90
 
91
91
  ## Layout
92
92
 
@@ -97,19 +97,19 @@ Owned by the layout guide.
97
97
  | `measure.reading` | The width of running text |
98
98
  | `measure.wide` | Content that isn't prose but should still align — grids, tables |
99
99
  | `measure.app` | An application shell's content area, if there is one |
100
- | `measure.form` | Forms and settings inside a panel, ≈ 640–760px, so controls stay near their labels |
100
+ | `measure.form` | Forms and settings inside a panel, so controls stay near their labels |
101
101
  | `measure.frame` | The wider frame that the reading column and its rails are both anchored to, so the gap between them holds as the window grows |
102
102
  | `measure.rail` | The width of a rail in the outer margin — a table of contents, an aside |
103
103
  | `gutter` | Horizontal page padding at the smallest width (Material's *margin*) |
104
104
  | `grid.columns` · `space.column` | A column grid's count per breakpoint, and the gap between columns (Material's *gutter*). Product and expressive only, and only where unlike blocks sit side by side |
105
- | `target.min` | The smallest hit area a control may have: 24px under a fine pointer, 44px on touch |
105
+ | `target.min` | The smallest hit area a control may have, per pointer: never under 24px fine or 44px on touch, which a preset's dial may raise |
106
106
  | `space.tight` | Inside a group — a label and its value, items in a list |
107
107
  | `space.group` | Between groups inside a section |
108
108
  | `space.section` | Between sections |
109
109
  | `space.heading-above` · `space.heading-below` | The asymmetric pair around a heading. Their *ratio* is the rule, not their values |
110
110
  | `radius.pill` · `radius.small` · `radius.control` · `radius.card` · `radius.container` | Smallest to outermost. Concentric corners obey inner = outer − padding; anything away from a corner takes a radius for its size |
111
111
  | `hairline.chrome` · `hairline.object` · `rule.emphasis` | Divider weight inside chrome, the edge of an object, and a deliberate heavy rule (a quote bar, a focus ring) |
112
- | `z.below` · `z.raised` · `z.popover` · `z.dismiss` · `z.nav` · `z.overlay` · `z.toast` · `z.skip-link` | The stacking ladder, low to high. `z.toast` sits over modals, because a notification about a modal's action must not hide behind it |
112
+ | `z.below` · `z.raised` · `z.popover` · `z.dismiss` · `z.nav` · `z.overlay` · `z.toast` · `z.skip-link` | The stacking ladder, low to high (layout law 7) |
113
113
  | `bp.<name>` | Breakpoints, named for what changes at them, not for devices |
114
114
 
115
115
  ## Surfaces and depth
@@ -134,7 +134,7 @@ Owned by the motion guide.
134
134
 
135
135
  | Role | Job |
136
136
  |---|---|
137
- | `duration.instant` | 0 — press feedback, focus rings |
137
+ | `duration.instant` | Press feedback, focus rings |
138
138
  | `duration.micro` | Opacity and colour on hover |
139
139
  | `duration.state` | Small state changes: colour, background, icon swaps, small transforms |
140
140
  | `duration.popover` | Something small appearing: a hover card, a menu, a chevron |